@hasna/economy 0.4.0 → 0.5.1
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.
- package/CHANGELOG.md +31 -0
- package/README.md +12 -9
- package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
- package/dist/chunks/billing-bkpxy4kv.js +13 -0
- package/dist/chunks/config-j9mv3ehk.js +15 -0
- package/dist/chunks/index-2hwzmfgv.js +3407 -0
- package/dist/chunks/index-49f12css.js +124 -0
- package/dist/chunks/index-d480p2wh.js +1322 -0
- package/dist/chunks/index-ez9thsr7.js +3440 -0
- package/dist/chunks/index-hesxq90g.js +62 -0
- package/dist/chunks/index-k540hfww.js +258 -0
- package/dist/chunks/index-p35cyhc4.js +4 -0
- package/dist/chunks/index-r1jv3kpy.js +451 -0
- package/dist/chunks/index-w3bvv0ns.js +26 -0
- package/dist/chunks/index-xsah93s2.js +2085 -0
- package/dist/chunks/index-zpvea08j.js +3372 -0
- package/dist/chunks/menubar-6xtkt17h.js +296 -0
- package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
- package/dist/chunks/pricing-f80p81gr.js +29 -0
- package/dist/chunks/pricing-vf2pycmh.js +30 -0
- package/dist/chunks/pricing-y21mcjsa.js +30 -0
- package/dist/chunks/serve-2knem98x.js +4655 -0
- package/dist/chunks/sqlite-store-501f32ts.js +258 -0
- package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
- package/dist/chunks/sqlite-store-st99garx.js +258 -0
- package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
- package/dist/chunks/tui-khmnkat2.js +22 -0
- package/dist/chunks/watch-4y064yys.js +196 -0
- package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
- package/dist/cli/commands/brief.d.ts.map +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/journal.d.ts +3 -0
- package/dist/cli/commands/journal.d.ts.map +1 -0
- package/dist/cli/commands/menubar.d.ts +97 -4
- package/dist/cli/commands/menubar.d.ts.map +1 -1
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +1358 -10114
- package/dist/db/cloud.d.ts +7 -9
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +14 -12
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/ingest-concurrency-worker.d.ts +2 -0
- package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
- package/dist/db/ingest-errors.d.ts +9 -0
- package/dist/db/ingest-errors.d.ts.map +1 -0
- package/dist/db/ingest-schema.d.ts +3 -0
- package/dist/db/ingest-schema.d.ts.map +1 -0
- package/dist/db/ingest-specs.d.ts +8 -0
- package/dist/db/ingest-specs.d.ts.map +1 -0
- package/dist/db/ingest-test-contract.d.ts +3 -0
- package/dist/db/ingest-test-contract.d.ts.map +1 -0
- package/dist/db/ingest-validation.d.ts +12 -0
- package/dist/db/ingest-validation.d.ts.map +1 -0
- package/dist/db/ingest.d.ts +27 -0
- package/dist/db/ingest.d.ts.map +1 -0
- package/dist/db/legacy-schema.d.ts +7 -0
- package/dist/db/legacy-schema.d.ts.map +1 -0
- package/dist/db/observation-contract.d.ts +3 -0
- package/dist/db/observation-contract.d.ts.map +1 -0
- package/dist/db/observation-provenance.d.ts +23 -0
- package/dist/db/observation-provenance.d.ts.map +1 -0
- package/dist/db/observation-schema.d.ts +24 -0
- package/dist/db/observation-schema.d.ts.map +1 -0
- package/dist/db/pg-connection.d.ts +10 -0
- package/dist/db/pg-connection.d.ts.map +1 -0
- package/dist/db/pg-legacy-schema.d.ts +12 -0
- package/dist/db/pg-legacy-schema.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +4 -9
- package/dist/db/pg-migrate.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/pg-protocol.d.ts +28 -0
- package/dist/db/pg-protocol.d.ts.map +1 -0
- package/dist/db/sqlite-store.d.ts +4 -0
- package/dist/db/sqlite-store.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +17 -21
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/db/third-party-sqlite.d.ts +10 -0
- package/dist/db/third-party-sqlite.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1171 -2465
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +25 -0
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/collection-outcome.d.ts +17 -0
- package/dist/ingest/collection-outcome.d.ts.map +1 -0
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/quota-observation.d.ts +16 -0
- package/dist/ingest/quota-observation.d.ts.map +1 -0
- package/dist/ingest/source-time.d.ts +4 -0
- package/dist/ingest/source-time.d.ts.map +1 -0
- package/dist/lib/accounts-store.d.ts +39 -13
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +1 -1
- package/dist/lib/autosync-gate.d.ts +12 -5
- package/dist/lib/autosync-gate.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +18 -40
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +2 -0
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/budget-decimal.d.ts +14 -0
- package/dist/lib/budget-decimal.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +15 -0
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/ingest-batches.d.ts +21 -0
- package/dist/lib/ingest-batches.d.ts.map +1 -0
- package/dist/lib/ingest-journal-client.d.ts +37 -0
- package/dist/lib/ingest-journal-client.d.ts.map +1 -0
- package/dist/lib/ingest-journal-lock.d.ts +5 -0
- package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
- package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
- package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
- package/dist/lib/ingest-journal-operation.d.ts +55 -0
- package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
- package/dist/lib/ingest-journal.d.ts +73 -0
- package/dist/lib/ingest-journal.d.ts.map +1 -0
- package/dist/lib/money-display.d.ts +13 -0
- package/dist/lib/money-display.d.ts.map +1 -0
- package/dist/lib/periods.d.ts +12 -0
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +14 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +64 -7
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +25 -21
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/summary-contract.d.ts +32 -0
- package/dist/lib/summary-contract.d.ts.map +1 -0
- package/dist/lib/summary-test-contract.d.ts +3 -0
- package/dist/lib/summary-test-contract.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +4 -0
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/sync-maintenance.d.ts.map +1 -1
- package/dist/lib/sync-report.d.ts +4 -0
- package/dist/lib/sync-report.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +9 -0
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
- package/dist/mcp/agent-registry-store.d.ts +30 -0
- package/dist/mcp/agent-registry-store.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +9 -16
- package/dist/mcp/agent-registry.d.ts.map +1 -1
- package/dist/mcp/index.js +2291 -3458
- package/dist/mcp/ingest-journal-tool.d.ts +2 -0
- package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +1960 -289
- package/dist/server/human-auth.d.ts +20 -0
- package/dist/server/human-auth.d.ts.map +1 -0
- package/dist/server/index.js +10639 -6720
- package/dist/server/legacy-inventory.d.ts +52 -0
- package/dist/server/legacy-inventory.d.ts.map +1 -0
- package/dist/server/legacy-operations.d.ts +64 -0
- package/dist/server/legacy-operations.d.ts.map +1 -0
- package/dist/server/legacy-plan.d.ts +55 -0
- package/dist/server/legacy-plan.d.ts.map +1 -0
- package/dist/server/legacy-preservation.d.ts +58 -0
- package/dist/server/legacy-preservation.d.ts.map +1 -0
- package/dist/server/pg-sync-worker.js +195 -21
- package/dist/server/serve.d.ts +20 -16
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/service-admission.d.ts +10 -0
- package/dist/server/service-admission.d.ts.map +1 -0
- package/dist/server/service-dispatcher.d.ts +45 -0
- package/dist/server/service-dispatcher.d.ts.map +1 -0
- package/dist/server/service-operations.d.ts +5 -0
- package/dist/server/service-operations.d.ts.map +1 -0
- package/dist/server/service-protocol.d.ts +50 -0
- package/dist/server/service-protocol.d.ts.map +1 -0
- package/dist/server/service-worker.d.ts +2 -0
- package/dist/server/service-worker.d.ts.map +1 -0
- package/dist/server/service-worker.js +8038 -0
- package/dist/types/index.d.ts +66 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/cli.md +1 -1
- package/docs/configuration.md +5 -5
- package/docs/cost-reporting.md +49 -0
- package/docs/historical-data-reconciliation.md +203 -0
- package/docs/ingest-journal.md +108 -0
- package/docs/ingestion.md +60 -4
- package/docs/mcp.md +2 -2
- package/docs/native-budget-save.md +84 -0
- package/docs/native-human-auth.md +102 -0
- package/docs/postgresql-legacy-schema-repair.md +30 -0
- package/docs/rest-api.md +2 -2
- package/package.json +25 -13
- package/dist/lib/test-hermetic-accounts.d.ts +0 -7
- package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
- package/dist/lib/test-hermetic-fleet-env.d.ts +0 -4
- package/dist/lib/test-hermetic-fleet-env.d.ts.map +0 -1
package/docs/ingestion.md
CHANGED
|
@@ -2,7 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
`economy sync` imports local coding-agent usage into Economy's SQLite database. A sync with no source flag runs every source; one or more source flags limit the run. Reads such as `economy today` also auto-sync all local sources before querying.
|
|
4
4
|
|
|
5
|
-
In cloud-client mode, CLI and MCP reads/writes go directly to the shared HTTP API.
|
|
5
|
+
In cloud-client mode, CLI and MCP reads/writes go directly to the shared HTTP API (on-box auto-sync is skipped). The explicit `economy sync` / `economy billing sync` verbs still run the same on-box provider ingest against a scratch store and push the rows to `/v1/ingest`.
|
|
6
|
+
|
|
7
|
+
## Hosted batch integrity
|
|
8
|
+
|
|
9
|
+
`POST /v1/ingest` accepts the supported table arrays, optional `schema_version: 1`
|
|
10
|
+
and an `idempotency_key` of 1–200 letters, digits, dots, underscores, colons or
|
|
11
|
+
hyphens. A command is limited to 10000 rows total and 4 MiB of JSON. The client
|
|
12
|
+
splits large scans into deterministic commands of at most 500 rows; each command
|
|
13
|
+
commits separately. All rows, session totals, predecessor snapshots and the
|
|
14
|
+
receipt for one command share a transaction.
|
|
15
|
+
|
|
16
|
+
Supply source `updated_at` values as ISO UTC timestamps ending in `Z`. An older
|
|
17
|
+
version is skipped, while different content at the same version is a conflict.
|
|
18
|
+
An unversioned row may be inserted or replayed, but cannot silently replace
|
|
19
|
+
different existing content. The server does not treat arrival time as a source
|
|
20
|
+
version. Session totals are derived from canonical request rows when present.
|
|
21
|
+
Collectors use recorded source revisions or event times, never a fresh scan time
|
|
22
|
+
as revision authority. A changed event without a newer source version conflicts;
|
|
23
|
+
changing a file's mtime does not grant permission to replace accepted usage.
|
|
24
|
+
|
|
25
|
+
The response's `data` contains `ingested`, `total`, `stale`, `unchanged`,
|
|
26
|
+
`operation_key` and `replayed`. Reuse the **identical body and key** after an
|
|
27
|
+
uncertain response: a committed command returns its original receipt. Reusing
|
|
28
|
+
that key for different content returns 409. Keys are scoped to the authenticated
|
|
29
|
+
principal. Legacy commands without a key derive one from their exact content.
|
|
30
|
+
|
|
31
|
+
Invalid fields return 400, source/key conflicts return 409, and oversized
|
|
32
|
+
commands return 413, with `code`, `field` and `expected` details. These are
|
|
33
|
+
definitive refusals. A timeout or lost response has an uncertain outcome; the
|
|
34
|
+
client never automatically repeats it. The authority-bound durable journal
|
|
35
|
+
retains the exact intent and its committed receipt or proven terminal refusal.
|
|
36
|
+
Use [journal status and reconciliation](ingest-journal.md) after an interruption;
|
|
37
|
+
a newly reconstructed scan is not exact reconciliation of the previous command.
|
|
6
38
|
|
|
7
39
|
## Sources
|
|
8
40
|
|
|
@@ -16,11 +48,35 @@ In cloud-client mode, CLI and MCP reads/writes go directly to the shared HTTP AP
|
|
|
16
48
|
| OpenCode | `~/.local/share/opencode/storage/message/**/*.json` | Imports assistant-message usage and uses the recorded cost when present. |
|
|
17
49
|
| Cursor | Cursor `/api/usage` and `/api/usage-summary` | Requires `CURSOR_SESSION_TOKEN` (or `CURSOR_API_TOKEN`). Creates daily usage snapshots and a subscription rollup when spend is present. |
|
|
18
50
|
| Pi | `~/.pi/agent/sessions/**/*.json` | Override with `PI_CODING_AGENT_SESSION_DIR`. Uses recorded turn cost; a missing cost remains zero until pricing is repaired or the source records one. |
|
|
19
|
-
| Hermes | `~/.hermes/state.db` |
|
|
20
|
-
| OpenLoops | `~/.hasna/loops/loops.db` | Override with `HASNA_ECONOMY_LOOPS_DB_PATH`; price with `HASNA_ECONOMY_LOOPS_MODEL` or `ECONOMY_LOOPS_MODEL`. Imports orchestration/judge `goal_runs.tokens_used` only, into `loop:*` cost centers. |
|
|
51
|
+
| Hermes | `~/.hermes/state.db` | Retains session lifetime totals and their actual start/end span. It cannot establish individual request times. |
|
|
52
|
+
| OpenLoops | `~/.hasna/loops/loops.db` | **Local lane only** — a hosted client refuses this cross-app on-box read (`loops ingest skipped`, one stderr line); pending a hosted loops read. Override with `HASNA_ECONOMY_LOOPS_DB_PATH`; price with `HASNA_ECONOMY_LOOPS_MODEL` or `ECONOMY_LOOPS_MODEL`. Imports orchestration/judge `goal_runs.tokens_used` only, into `loop:*` cost centers. |
|
|
21
53
|
|
|
22
54
|
Full, unfiltered sync also attempts to import active metadata from `@hasna/projects`. Missing files, optional registries, and unavailable quota credentials are skipped; use `--verbose` to see source-level diagnostics.
|
|
23
55
|
|
|
56
|
+
Claude/Takumi, Gemini, Pi and OpenCode usage without a valid recorded event timestamp remains uncollected and
|
|
57
|
+
reports `SOURCE_INVALID`, including on repeat scans. The original source stays
|
|
58
|
+
intact. A file's modification time or the containing session's update time cannot
|
|
59
|
+
date an otherwise undated request. Hermes lifetime-only usage reports `USAGE_EVENTS_UNAVAILABLE`; it creates
|
|
60
|
+
no artificial end-date request. Summaries identify overlapping session-only
|
|
61
|
+
lifetime totals separately and leave the complete period total unavailable.
|
|
62
|
+
These values are not allocated to days or months. Existing Hermes rollup requests
|
|
63
|
+
require a separately preserved historical migration and are not rewritten by sync.
|
|
64
|
+
|
|
65
|
+
Ingestion treats subscriptions as `quota_observation` by default, preserving the
|
|
66
|
+
existing assignment, active status, plan label and billing-cycle settings. Explicit
|
|
67
|
+
subscription edits use `subscription_mode: "configuration"` with a subscriptions
|
|
68
|
+
array; the normal subscription command supplies this mode. Both modes remain
|
|
69
|
+
subject to the same source-version, current write-authority and receipt checks.
|
|
70
|
+
Authenticated mutating API requests require `economy:write`; a read-only key
|
|
71
|
+
cannot change subscription configuration or other stored settings.
|
|
72
|
+
An explicit configuration replaces monetary values and their provenance, including
|
|
73
|
+
unknown amounts. Omitting a fee on a plan change does not retain the previous plan's
|
|
74
|
+
fee; the comparison stays unavailable until the new fee is known.
|
|
75
|
+
Quota refreshes with unknown monetary amounts preserve a subscription's existing
|
|
76
|
+
configured or observed fee and included value. A deliberate known zero must carry
|
|
77
|
+
`configured` or `observed` provenance. A zero-priced request with no affirmative
|
|
78
|
+
price evidence keeps both summaries and subscription comparisons unavailable.
|
|
79
|
+
|
|
24
80
|
## Incremental and repair behavior
|
|
25
81
|
|
|
26
82
|
File/database state is cached in the `ingest_state` table, and request IDs are upserted and deduplicated. Consequently, normal repeated syncs are incremental.
|
|
@@ -32,7 +88,7 @@ File/database state is cached in the `ingest_state` table, and request IDs are u
|
|
|
32
88
|
|
|
33
89
|
## Account and cost-center attribution
|
|
34
90
|
|
|
35
|
-
Economy first checks agent-specific overrides such as `ECONOMY_CODEX_ACCOUNT`, then generic `ECONOMY_ACCOUNT` overrides, then matching
|
|
91
|
+
Economy first checks agent-specific overrides such as `ECONOMY_CODEX_ACCOUNT`, then generic `ECONOMY_ACCOUNT` overrides, then matching accounts env-dir/applied/current profiles from the accounts API. The on-box accounts registry (`~/.hasna/accounts/accounts.json`) is consulted **only** under the explicit local opt-in `HASNA_ECONOMY_LOCAL=1`; without an accounts credential and without that flag, attribution is omitted and the refusal is printed once on stderr. An override may be `tool:name`, an email, or separate `*_ACCOUNT_TOOL`, `*_ACCOUNT_NAME`, and `*_ACCOUNT_EMAIL` fields. See [configuration](configuration.md#account-attribution).
|
|
36
92
|
|
|
37
93
|
Apps and services can report explicit project, repository, account, attribution-tag, and cost-center fields through [`economy-otel`](otel.md). `ECONOMY_TAG` supplies a fallback attribution tag for records written through the local database helpers.
|
|
38
94
|
|
package/docs/mcp.md
CHANGED
|
@@ -60,8 +60,8 @@ Shared agent lifecycle tools:
|
|
|
60
60
|
|
|
61
61
|
- `register_agent`, `heartbeat`, `set_focus`, `list_agents`
|
|
62
62
|
|
|
63
|
-
The agent registry behind these tools is opened on first use, never at startup. A hosted server (a resolved credential) keeps it in memory for the life of the process — no SQLite file is created under `~/.hasna/economy` in hosted mode; under the explicit local opt-in (`HASNA_ECONOMY_LOCAL=1`) it persists as `agent-registry.db` beside the local store and is shared by every MCP process on the box. `HASNA_AGENT_REGISTRY_DB_PATH`
|
|
63
|
+
The agent registry behind these tools is opened on first use, never at startup — through a dynamic import, so the published `dist/mcp` bundle contains no `bun:sqlite` reference at all. A hosted server (a resolved credential) keeps it in memory for the life of the process — no SQLite file is created under `~/.hasna/economy` in hosted mode; under the explicit local opt-in (`HASNA_ECONOMY_LOCAL=1`) it persists as `agent-registry.db` beside the local store and is shared by every MCP process on the box. `HASNA_AGENT_REGISTRY_DB_PATH` overrides that file only in the explicit local lane; hosted transport ignores it and remains in memory.
|
|
64
64
|
|
|
65
65
|
High-cardinality tools return compact text by default. Where the schema offers them, use `limit`, `verbose=true`, or `json=true`. Limits are clamped to 100 for MCP calls.
|
|
66
66
|
|
|
67
|
-
The `sync` tool accepts `all`, any supported coding agent, or `loops`. It ingests on-box files (in local mode into the local SQLite; in cloud-client mode it reads the on-box files on this machine and pushes the rows to the shared API).
|
|
67
|
+
The `sync` tool accepts `all`, any supported coding agent, or `loops`. It ingests on-box files (in local mode into the local SQLite; in cloud-client mode it reads the on-box files on this machine and pushes the rows to the shared API). `loops` is the exception: it reads ANOTHER app's on-box SQLite, so a hosted server refuses it and reports `loops ingest skipped`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Native metered API budget rules
|
|
2
|
+
|
|
3
|
+
The native Save action uses the existing authenticated `POST /v1/ingest` command
|
|
4
|
+
and `GET /v1/ingest/receipts` recovery path. It creates one rule; it does not edit
|
|
5
|
+
an existing rule, cap spending, or request notification delivery. Existing
|
|
6
|
+
`POST /v1/budgets`, CLI `budget set` and MCP `set_budget` retain legacy semantics.
|
|
7
|
+
|
|
8
|
+
Capture the rule ID, idempotency key and timestamps once when the user confirms.
|
|
9
|
+
Keep the complete command unchanged until its outcome is resolved. For example:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"schema_version": 1,
|
|
14
|
+
"idempotency_key": "budget-create-example-1",
|
|
15
|
+
"budget_mode": "create",
|
|
16
|
+
"budgets": [{
|
|
17
|
+
"id": "budget-example-1",
|
|
18
|
+
"metric": "metered_api_usd",
|
|
19
|
+
"period": "monthly",
|
|
20
|
+
"limit_usd": 50,
|
|
21
|
+
"alert_at_percent": 80,
|
|
22
|
+
"project_path": null,
|
|
23
|
+
"agent": null,
|
|
24
|
+
"created_at": "2026-09-30T12:00:00.000Z",
|
|
25
|
+
"updated_at": "2026-09-30T12:00:00.000Z"
|
|
26
|
+
}]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Use fresh caller-owned UUIDs for real rule IDs and keys. The command requires
|
|
31
|
+
schema version 1, the explicit create mode, exactly one rule and no other tables.
|
|
32
|
+
Limits are positive finite USD values. Thresholds are integers from 1 through
|
|
33
|
+
100. Periods are `daily`, `weekly`, or `monthly`. Omitted or null project, agent
|
|
34
|
+
and cost-center fields mean all values; provided text cannot be empty. Agents
|
|
35
|
+
must be members of Economy's published agent enum. Both timestamps are explicit
|
|
36
|
+
ISO UTC instants ending in `Z` and are part of the exact retry intent.
|
|
37
|
+
|
|
38
|
+
The transaction inserts the rule only when its ID is absent. Concurrent creation
|
|
39
|
+
or another actor's use of the same ID cannot overwrite it. A committed identical
|
|
40
|
+
command by the same authenticated actor returns the original receipt with
|
|
41
|
+
`replayed: true`. Reusing a key for different content returns 409. A new key is
|
|
42
|
+
not a way to edit an existing ID. Generic observation ingestion can recognize an
|
|
43
|
+
exact unchanged new rule but cannot edit it or convert a legacy rule's metric.
|
|
44
|
+
|
|
45
|
+
After a timeout or lost response, do not automatically create a new command. Read
|
|
46
|
+
`/v1/ingest/receipts?idempotency_key=…&body_hash=…` using the same authenticated
|
|
47
|
+
principal. `body_hash` is lowercase SHA-256 of `canonicalIngestBodyHash`'s stable
|
|
48
|
+
JSON encoding: recursively sort object keys, retain array order and JSON scalar
|
|
49
|
+
encoding. The hash includes the key, mode and all fields exactly as submitted.
|
|
50
|
+
Keep the serialized command and its hash together. A committed receipt must
|
|
51
|
+
match the principal and hash. `absent` is only a current observation; an earlier
|
|
52
|
+
request may still commit. Explicit retry, if chosen, uses that same full body/key.
|
|
53
|
+
Read-only or revoked principals cannot write or reconcile a write receipt.
|
|
54
|
+
|
|
55
|
+
After confirmation, fetch `GET /v1/budgets` and show the matching ID and its
|
|
56
|
+
authoritative threshold status. Do not label it saved merely because a request
|
|
57
|
+
was sent or a matching-looking row was listed before the transaction.
|
|
58
|
+
|
|
59
|
+
`metered_api_usd` sums stored requests whose `cost_basis` is exactly
|
|
60
|
+
`metered_api`. It excludes `subscription_included`, `estimated`, `unknown`,
|
|
61
|
+
subscription fees, quota values and separate provider billing rows. These are
|
|
62
|
+
recorded API usage values, not a guarantee of complete collection or a reconciled
|
|
63
|
+
provider invoice. Scope combines selected project, agent and cost center.
|
|
64
|
+
The new metric sums the stored numbers' round-trip decimal representations and
|
|
65
|
+
compares thresholds in decimal arithmetic. Fractional-cent API charges remain
|
|
66
|
+
intact; neither individual charges nor the comparison are rounded to cents.
|
|
67
|
+
The numeric display fields keep their existing JSON-number representation, while
|
|
68
|
+
the alert and limit flags come from the exact comparison. A total or percentage
|
|
69
|
+
outside that numeric display range fails visibly rather than reporting a false
|
|
70
|
+
zero or serializing infinity as null. Legacy rules retain their previous sum.
|
|
71
|
+
Thresholds are in-app status, not spending enforcement or guaranteed alerts.
|
|
72
|
+
Creating or replaying this rule never invokes optional webhook delivery.
|
|
73
|
+
|
|
74
|
+
Periods use the existing full UTC calendar windows. Days begin at 00:00 UTC,
|
|
75
|
+
weeks begin on Sunday, and months begin on the first. Each budget status exposes
|
|
76
|
+
`period_bounds` with `time_zone`, `start_date` and `end_date_exclusive`; display
|
|
77
|
+
those semantics rather than interpreting the date in the device's timezone.
|
|
78
|
+
|
|
79
|
+
The additive migration appends `budgets.metric` with default
|
|
80
|
+
`legacy_request_cost_usd`. Existing rows and their request-cost sum behavior are
|
|
81
|
+
preserved; historical migration bytes do not change. Apply the new migration
|
|
82
|
+
through the reviewed deployment path before enabling the native Save action.
|
|
83
|
+
This document and fixture results alone do not establish hosted activation,
|
|
84
|
+
native human authentication, push delivery or production data changes.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Native human authentication
|
|
2
|
+
|
|
3
|
+
This additive protocol leaves signed agent keys under their existing verifier.
|
|
4
|
+
It does not activate human access by default. Deployment requires an operator to
|
|
5
|
+
verify which tenant and human principals own the entire existing Economy report
|
|
6
|
+
store. The store is not partitioned by the tenant in an incoming token.
|
|
7
|
+
|
|
8
|
+
## Identity protocol
|
|
9
|
+
|
|
10
|
+
The native client uses the existing Identities authority at
|
|
11
|
+
`https://api.hasna.com/identities/v1`, with HTTPS and redirects disabled:
|
|
12
|
+
|
|
13
|
+
1. `POST /auth/login` with `{ "email": "<human email>" }` requests an email code.
|
|
14
|
+
The response contains `challenge: true` and `expires_in`.
|
|
15
|
+
2. `POST /auth/verify` with `{ "email": "<human email>", "code": "<code>" }`
|
|
16
|
+
returns `session`, `session_expires_in`,
|
|
17
|
+
`principal: { user_id, kind, email, display_name }` and
|
|
18
|
+
`tenants: [{ tenant_id, role }]`. Store the session in the platform credential
|
|
19
|
+
store. It is an Identities credential and must never be sent to Economy.
|
|
20
|
+
3. Select an explicit tenant from the returned memberships. `POST /auth/token`
|
|
21
|
+
with that session in the Bearer header and body
|
|
22
|
+
`{ "app": "economy", "tenant_id": "<selected tenant>" }` returns
|
|
23
|
+
`access_token`, `token_type: "Bearer"`, `uid`, `tid`, `scope`, `expires_in`,
|
|
24
|
+
`aud: "economy"`, `pt: "user"`, `alg: "EdDSA"` and `kid`.
|
|
25
|
+
Omit `scopes` to receive current grants; never require write scope just to
|
|
26
|
+
sign in. Explicit scopes can only narrow access. Economy token TTL defaults
|
|
27
|
+
to 300 seconds, cannot exceed 300, and cannot outlive the session.
|
|
28
|
+
4. Use the access token as Bearer at `https://api.hasna.com/economy/v1`.
|
|
29
|
+
`GET /auth/session` returns
|
|
30
|
+
`{ data: { subject, tenant_id, scopes, session_id, expires_at }, meta: {} }`.
|
|
31
|
+
`expires_at` is integer epoch seconds; `scopes` is the effective intersection
|
|
32
|
+
of current Identities grants and Economy's store binding. Hide write controls
|
|
33
|
+
unless it includes `economy:write`. Human report and session responses are
|
|
34
|
+
`Cache-Control: no-store`.
|
|
35
|
+
5. Refresh by repeating step 3 while the session is valid. Stable account identity
|
|
36
|
+
is the verified `(tenant_id, subject)` pair, not token id or signing key.
|
|
37
|
+
Fence all pending requests when account, tenant or session changes. Do not
|
|
38
|
+
display a prior account's cached report after a change.
|
|
39
|
+
6. Sign out locally first, cancelling requests and clearing session, access
|
|
40
|
+
token and account-scoped caches. Make one best-effort `POST /auth/logout` to
|
|
41
|
+
Identities using the previous session bearer. Success is
|
|
42
|
+
`{ "signed_out": true }`. This revokes only that session and its Economy
|
|
43
|
+
tokens. Report a failed remote sign-out accurately; other legacy app tokens
|
|
44
|
+
retain their existing semantics.
|
|
45
|
+
|
|
46
|
+
This is the existing email-code protocol, not OAuth/PKCE. No browser callback,
|
|
47
|
+
embedded station key, signup or new identity account is needed by the client.
|
|
48
|
+
Clients bound request/response sizes and deadlines and never follow redirects
|
|
49
|
+
while sending a credential. No token, code or raw auth response belongs in logs.
|
|
50
|
+
|
|
51
|
+
## Economy store binding
|
|
52
|
+
|
|
53
|
+
`ECONOMY_HUMAN_AUTH_BINDING` is non-secret server configuration:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"tenantId": "<operator-verified data tenant>",
|
|
58
|
+
"principals": [
|
|
59
|
+
{ "subject": "<verified human user id>", "scopes": ["economy:read"] }
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
These values must come from independently verified ownership and current grants;
|
|
65
|
+
never populate them from the requesting token or an unverified native-client
|
|
66
|
+
choice. Only the named tenant and principals are admitted. Add write scope only
|
|
67
|
+
where the existing budget/configuration authority permits it. Agent permissions
|
|
68
|
+
remain unchanged. Missing configuration returns `503 human_auth_not_configured`
|
|
69
|
+
for human credentials while agent keys continue working. Malformed configuration
|
|
70
|
+
refuses server startup.
|
|
71
|
+
|
|
72
|
+
The server sends only the Economy access token to the fixed default endpoint
|
|
73
|
+
`https://api.hasna.com/identities/v1/auth/economy-authority`. An explicit HTTPS
|
|
74
|
+
`authorityUrl` permits a self-hosted authority with the same contract; it cannot
|
|
75
|
+
contain credentials, query parameters or fragments. No response can select a
|
|
76
|
+
different origin. Redirects are refused. Each authority call has a three-second
|
|
77
|
+
deadline and a 16 KiB response cap; tokens are capped at 8 KiB. Failure is closed
|
|
78
|
+
and redacted (`human_authority_unavailable`); no offline grant cache is used.
|
|
79
|
+
|
|
80
|
+
Identities verifies the EdDSA signature, issuer `identities`, audience `economy`,
|
|
81
|
+
300-second lifetime and signed session id, then checks the current session,
|
|
82
|
+
human, tenant and membership together in one database statement. Only concrete
|
|
83
|
+
read/write grants survive the role and explicit membership-scope ceiling.
|
|
84
|
+
Economy intersects that response with its store binding. It checks at request
|
|
85
|
+
admission and again after body reading/queue waiting immediately before dispatch.
|
|
86
|
+
Actor identity uses the tenant and stable human subject across token refreshes.
|
|
87
|
+
|
|
88
|
+
This establishes current authority at dispatch. It does not introduce a
|
|
89
|
+
distributed transaction with Identities, undo already-started work on logout, or
|
|
90
|
+
claim that a later revocation is observed at database commit. Native session
|
|
91
|
+
generation fencing remains required for responses already in flight.
|
|
92
|
+
|
|
93
|
+
## Verification and activation
|
|
94
|
+
|
|
95
|
+
Package tests cover config refusal, stable actor, effective scopes, agent-key
|
|
96
|
+
compatibility, changed authority before dispatch and bounded/redacted failures.
|
|
97
|
+
The owning Identities fixture uses fresh PostgreSQL, real HTTP login/verification
|
|
98
|
+
and signed tokens; the cross-app acceptance fixture also exercises actual TLS
|
|
99
|
+
requests, foreign tenant/principal refusal, queued revocation and redirect
|
|
100
|
+
refusal. These are source/fixture proofs. Live email delivery, existing human
|
|
101
|
+
grants/store ownership, service releases and production activation are separate
|
|
102
|
+
requirements. No production key or record is created by these source changes.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Legacy PostgreSQL cost-center schema repair
|
|
2
|
+
|
|
3
|
+
Some existing databases record migrations 0–84 while missing `cost_centers`
|
|
4
|
+
and the nullable `cost_center_id` columns on `requests`, `sessions` and
|
|
5
|
+
`budgets`. Migration 102 requires those columns.
|
|
6
|
+
|
|
7
|
+
The explicit `economy-serve migrate` command checks this known layout before
|
|
8
|
+
running the numbered migrations. It adds the missing table, columns and four
|
|
9
|
+
indexes using their unchanged original definitions. It does not infer cost
|
|
10
|
+
center assignments, change existing row values or rewrite migration history.
|
|
11
|
+
Fresh and already complete databases need no compatibility repair.
|
|
12
|
+
|
|
13
|
+
The repair serializes with the existing migration runner. Its DDL and one
|
|
14
|
+
`legacy-cost-centers-v1` receipt commit in one transaction. The receipt records
|
|
15
|
+
the exact repair SQL hash. An existing receipt ledger must have the canonical
|
|
16
|
+
permanent shape, with no triggers, rewrite rules or row security. A completed
|
|
17
|
+
receipt with changed schema or hash is refused. Incompatible existing tables,
|
|
18
|
+
columns or indexes are also refused without replacing them.
|
|
19
|
+
|
|
20
|
+
Preserve and restore-test the database before using the normal deployment
|
|
21
|
+
migration operation. Server startup does not run this repair. If a connection
|
|
22
|
+
is lost during commit, keep the outcome uncertain and inspect the database
|
|
23
|
+
before an explicit retry; the command never retries automatically. A subsequent
|
|
24
|
+
invocation recognizes a committed matching receipt as `already_applied`.
|
|
25
|
+
Numbered migrations retain their separate atomic completion markers.
|
|
26
|
+
|
|
27
|
+
The migration JSON includes `repair.id`, `repair.status` and `repair.added`.
|
|
28
|
+
Possible statuses are `not_needed`, `applied` and `already_applied`. Retain that
|
|
29
|
+
result with the deployment and preservation records. Rolling application code
|
|
30
|
+
back does not remove the added schema or repair receipt.
|
package/docs/rest-api.md
CHANGED
|
@@ -30,7 +30,7 @@ Authorization: Bearer <token>
|
|
|
30
30
|
X-Economy-Token: <token>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
With a Postgres backend (database URL configured), send a valid Economy API key as `x-api-key` or a bearer token. All non-probe routes require a valid key when the server has an authenticator. Bulk ingest and feedback additionally request the `economy:write` scope.
|
|
34
34
|
|
|
35
35
|
## Read routes
|
|
36
36
|
|
|
@@ -83,7 +83,7 @@ Common period values are `today`, `yesterday`, `week`, `month`, `year`, and `all
|
|
|
83
83
|
| `DELETE` | `/v1/project-registry/{path}` | Delete a URL-encoded project path. |
|
|
84
84
|
| `POST` | `/v1/sync` | `{ "sources": "all" }` or one of the eight agents/`loops`; triggers server-local ingestion. |
|
|
85
85
|
| `POST` | `/v1/billing/sync` | Optional `days` (1–366) and `providers` from `anthropic`, `openai`, `gemini`. Provider failures are returned per provider. |
|
|
86
|
-
| `POST` | `/v1/ingest` |
|
|
86
|
+
| `POST` | `/v1/ingest` | Atomically ingest versioned, bounded arrays named `requests`, `sessions`, `projects`, `budgets`, `goals`, `billing_daily`, `model_pricing`, `subscriptions`, and `usage_snapshots`. |
|
|
87
87
|
| `POST` | `/v1/feedback` | Required `message`; optional `email` and `category=bug|feature|general`. |
|
|
88
88
|
|
|
89
89
|
Agent fields accept `claude`, `takumi`, `codex`, `gemini`, `opencode`, `cursor`, `pi`, or `hermes`.
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI coding cost tracker
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "
|
|
7
|
+
"url": "https://github.com/hasna/apps.git",
|
|
8
|
+
"directory": "apps/economy"
|
|
8
9
|
},
|
|
9
10
|
"bugs": {
|
|
10
|
-
"url": "https://github.com/hasna/
|
|
11
|
+
"url": "https://github.com/hasna/apps/issues"
|
|
11
12
|
},
|
|
12
|
-
"homepage": "https://github.com/hasna/economy#readme",
|
|
13
|
+
"homepage": "https://github.com/hasna/apps/tree/main/apps/economy#readme",
|
|
13
14
|
"type": "module",
|
|
14
15
|
"main": "dist/index.js",
|
|
15
16
|
"types": "dist/index.d.ts",
|
|
@@ -38,14 +39,15 @@
|
|
|
38
39
|
"CHANGELOG.md",
|
|
39
40
|
"SECURITY.md",
|
|
40
41
|
"CONTRIBUTING.md",
|
|
41
|
-
"CODE_OF_CONDUCT.md"
|
|
42
|
+
"CODE_OF_CONDUCT.md",
|
|
43
|
+
"!dist/lib/test-hermetic-*"
|
|
42
44
|
],
|
|
43
45
|
"scripts": {
|
|
44
|
-
"build": "bun build src/cli/index.ts --outdir dist
|
|
45
|
-
"build:cli": "bun build src/cli/index.ts --outdir dist
|
|
46
|
-
"build:mcp": "bun build src/mcp/index.ts --outdir dist
|
|
47
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
48
|
-
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
46
|
+
"build": "bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming \"chunks/[name]-[hash].[ext]\" && bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming \"chunks/[name]-[hash].[ext]\" && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming \"chunks/[name]-[hash].[ext]\" && tsc --emitDeclarationOnly --outDir dist",
|
|
47
|
+
"build:cli": "bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming \"chunks/[name]-[hash].[ext]\"",
|
|
48
|
+
"build:mcp": "bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming \"chunks/[name]-[hash].[ext]\"",
|
|
49
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external",
|
|
50
|
+
"build:lib": "bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming \"chunks/[name]-[hash].[ext]\"",
|
|
49
51
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
50
52
|
"prepack": "bun run build && bun run artifact-scan",
|
|
51
53
|
"typecheck": "tsc --noEmit",
|
|
@@ -82,7 +84,8 @@
|
|
|
82
84
|
"chalk": "^5.4.1",
|
|
83
85
|
"commander": "^13.1.0",
|
|
84
86
|
"pg": "8.13.1",
|
|
85
|
-
"zod": "^3.24.2"
|
|
87
|
+
"zod": "^3.24.2",
|
|
88
|
+
"@hasna/trash": "0.2.3"
|
|
86
89
|
},
|
|
87
90
|
"devDependencies": {
|
|
88
91
|
"@types/bun": "1.3.14",
|
|
@@ -92,5 +95,14 @@
|
|
|
92
95
|
},
|
|
93
96
|
"trustedDependencies": [
|
|
94
97
|
"@hasna/projects"
|
|
95
|
-
]
|
|
98
|
+
],
|
|
99
|
+
"overrides": {
|
|
100
|
+
"fast-uri": "3.1.8",
|
|
101
|
+
"hono": "4.13.7",
|
|
102
|
+
"ip-address": "10.7.1",
|
|
103
|
+
"nanoid": "5.1.16",
|
|
104
|
+
"path-to-regexp": "8.4.0",
|
|
105
|
+
"undici": "6.28.1",
|
|
106
|
+
"express-rate-limit": "8.7.0"
|
|
107
|
+
}
|
|
96
108
|
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stash and delete the hosted-accounts env vars, and redirect the resolver's
|
|
3
|
-
* disk root to an empty temp dir. Returns a restore function for
|
|
4
|
-
* afterEach/afterAll. Safe to call when the variables are unset.
|
|
5
|
-
*/
|
|
6
|
-
export declare function isolateHostedAccountsEnv(): () => void;
|
|
7
|
-
//# sourceMappingURL=test-hermetic-accounts.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test-hermetic-accounts.d.ts","sourceRoot":"","sources":["../../src/lib/test-hermetic-accounts.ts"],"names":[],"mappings":"AAuBA;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,IAAI,CAgBrD"}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare function hermeticFleetEnv(tempRoot: string, extra?: Record<string, string>): Record<string, string>;
|
|
2
|
-
/** Every SQLite file — WAL/SHM/journal sidecars included — under a root. */
|
|
3
|
-
export declare function sqliteFilesUnder(dir: string): string[];
|
|
4
|
-
//# sourceMappingURL=test-hermetic-fleet-env.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"test-hermetic-fleet-env.d.ts","sourceRoot":"","sources":["../../src/lib/test-hermetic-fleet-env.ts"],"names":[],"mappings":"AAcA,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmB7G;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAStD"}
|