@knpkv/agent-usage 0.1.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  4. package/dist/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  5. package/dist/client/assets/index-CUuLXODk.css +1 -0
  6. package/dist/client/assets/index-DB-VXWqy.js +55 -0
  7. package/dist/client/index.html +15 -0
  8. package/dist/core/Attribution.d.ts +59 -0
  9. package/dist/core/Attribution.js +108 -0
  10. package/dist/core/Attribution.js.map +1 -0
  11. package/dist/core/Balances.d.ts +27 -0
  12. package/dist/core/Balances.js +63 -0
  13. package/dist/core/Balances.js.map +1 -0
  14. package/dist/core/ClaudeLimits.d.ts +58 -0
  15. package/dist/core/ClaudeLimits.js +125 -0
  16. package/dist/core/ClaudeLimits.js.map +1 -0
  17. package/dist/core/ClaudeLimitsLive.d.ts +18 -0
  18. package/dist/core/ClaudeLimitsLive.js +36 -0
  19. package/dist/core/ClaudeLimitsLive.js.map +1 -0
  20. package/dist/core/ClaudeReader.d.ts +27 -0
  21. package/dist/core/ClaudeReader.js +154 -0
  22. package/dist/core/ClaudeReader.js.map +1 -0
  23. package/dist/core/CodexReader.d.ts +91 -0
  24. package/dist/core/CodexReader.js +365 -0
  25. package/dist/core/CodexReader.js.map +1 -0
  26. package/dist/core/Database.d.ts +4 -0
  27. package/dist/core/Database.js +65 -0
  28. package/dist/core/Database.js.map +1 -0
  29. package/dist/core/Ingest.d.ts +55 -0
  30. package/dist/core/Ingest.js +267 -0
  31. package/dist/core/Ingest.js.map +1 -0
  32. package/dist/core/Model.d.ts +160 -0
  33. package/dist/core/Model.js +125 -0
  34. package/dist/core/Model.js.map +1 -0
  35. package/dist/core/Pricing.d.ts +42 -0
  36. package/dist/core/Pricing.js +126 -0
  37. package/dist/core/Pricing.js.map +1 -0
  38. package/dist/core/Readers.d.ts +42 -0
  39. package/dist/core/Readers.js +18 -0
  40. package/dist/core/Readers.js.map +1 -0
  41. package/dist/core/Report.d.ts +53 -0
  42. package/dist/core/Report.js +270 -0
  43. package/dist/core/Report.js.map +1 -0
  44. package/dist/core/Store.d.ts +94 -0
  45. package/dist/core/Store.js +376 -0
  46. package/dist/core/Store.js.map +1 -0
  47. package/dist/core/Tickets.d.ts +42 -0
  48. package/dist/core/Tickets.js +125 -0
  49. package/dist/core/Tickets.js.map +1 -0
  50. package/dist/index.d.ts +14 -0
  51. package/dist/index.js +15 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/main.d.ts +2 -0
  54. package/dist/main.js +65 -0
  55. package/dist/main.js.map +1 -0
  56. package/dist/server/Api.d.ts +304 -0
  57. package/dist/server/Api.js +20 -0
  58. package/dist/server/Api.js.map +1 -0
  59. package/dist/server/Config.d.ts +27 -0
  60. package/dist/server/Config.js +49 -0
  61. package/dist/server/Config.js.map +1 -0
  62. package/dist/server/Handlers.d.ts +5 -0
  63. package/dist/server/Handlers.js +48 -0
  64. package/dist/server/Handlers.js.map +1 -0
  65. package/dist/server/HttpApplication.d.ts +7 -0
  66. package/dist/server/HttpApplication.js +70 -0
  67. package/dist/server/HttpApplication.js.map +1 -0
  68. package/dist/server/OwnerSession.d.ts +60 -0
  69. package/dist/server/OwnerSession.js +206 -0
  70. package/dist/server/OwnerSession.js.map +1 -0
  71. package/dist/server/Runtime.d.ts +57 -0
  72. package/dist/server/Runtime.js +85 -0
  73. package/dist/server/Runtime.js.map +1 -0
  74. package/dist/server/Server.d.ts +15 -0
  75. package/dist/server/Server.js +39 -0
  76. package/dist/server/Server.js.map +1 -0
  77. package/dist/shared/contracts.d.ts +309 -0
  78. package/dist/shared/contracts.js +105 -0
  79. package/dist/shared/contracts.js.map +1 -0
  80. package/dist/tsconfig.tsbuildinfo +1 -0
  81. package/package.json +68 -0
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta content="width=device-width, initial-scale=1" name="viewport" />
6
+ <!-- The bootstrap code rides in the fragment; keep it out of any outbound request. -->
7
+ <meta content="no-referrer" name="referrer" />
8
+ <title>Agent usage</title>
9
+ <script type="module" crossorigin src="/assets/index-DB-VXWqy.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-CUuLXODk.css">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ </body>
15
+ </html>
@@ -0,0 +1,59 @@
1
+ import type { AttributionInputs } from "./Model.js";
2
+ /** What a Usage Event's consumption counts toward. */
3
+ export type Booking = {
4
+ readonly _tag: "Ticket";
5
+ readonly key: string;
6
+ } | {
7
+ readonly _tag: "Repo";
8
+ readonly name: string;
9
+ };
10
+ /** The first key a branch names: naming a branch is deliberate. */
11
+ export declare const ticketKeyFromBranch: (branch: string) => string | null;
12
+ /** The deepest key a path names, so `worktrees/PROJ-1/PROJ-2` is working on `PROJ-2`. */
13
+ export declare const ticketKeyFromPath: (cwd: string) => string | null;
14
+ /**
15
+ * The one distinct key in typed text, or null when there are none or several. Stored as typed:
16
+ * whether it books is decided when read, by the Known Projects, so `RPS-123` (a real ticket number
17
+ * that looks like filler) is never lost at ingest.
18
+ */
19
+ export declare const singleTicket: (text: string) => string | null;
20
+ /**
21
+ * The repo a working directory belongs to. Worktrees live at `…/worktrees/<repo>/<branch>`, so
22
+ * that segment wins; otherwise the last path segment.
23
+ */
24
+ export declare const repoName: (cwd: string) => string;
25
+ /** The project a key belongs to: `RPS` for `RPS-7071`. */
26
+ export declare const projectOf: (key: string) => string;
27
+ /**
28
+ * The Known Projects: every project a branch or worktree path has named, plus the configured ones.
29
+ * Naming a branch is deliberate, so those keys vouch for their project.
30
+ */
31
+ export declare const knownProjects: (places: Iterable<{
32
+ readonly branch: string;
33
+ readonly cwd: string;
34
+ }>, configured: Iterable<string>) => ReadonlySet<string>;
35
+ /** A Usage Event's Booking, and the typed key it set aside, if any. */
36
+ export interface Attribution {
37
+ readonly booking: Booking;
38
+ /** An Active Ticket whose project is not a Known Project: `GPT-6`, `SHA-256`. */
39
+ readonly ignoredKey: string | null;
40
+ }
41
+ /**
42
+ * Branch first, then path, then the Active Ticket when its project is known; otherwise the repo.
43
+ * Branch and path keys are trusted as they are; only typed text is checked against the projects.
44
+ */
45
+ export declare const attribute: (inputs: AttributionInputs, projects: ReadonlySet<string>) => Attribution;
46
+ /** A stable id that keeps a ticket and a repo of the same spelling apart. */
47
+ export declare const bookingId: (booking: Booking) => string;
48
+ /** A Claude user turn's content: a string, or blocks of which only `text` blocks are typed words. */
49
+ export type ClaudeContent = string | ReadonlyArray<{
50
+ readonly type: string;
51
+ readonly text?: string | undefined;
52
+ }>;
53
+ /**
54
+ * The text a human typed into one Claude user turn: string content or `text` blocks, with tool
55
+ * results and injected reminder/command blocks removed.
56
+ */
57
+ export declare const claudeHumanText: (content: ClaudeContent) => string;
58
+ /** The text a human typed in one Codex `input_text` item; injected context yields "". */
59
+ export declare const codexHumanText: (text: string) => string;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Deriving a Usage Event's Booking from its Attribution Inputs, and mining the Active Ticket from
3
+ * what the human typed.
4
+ *
5
+ * **Mental model**
6
+ *
7
+ * - **A Booking is computed, never stored** (ADR 0002): {@link bookingOf} is pure, so a sharper
8
+ * rule reaches every event already recorded, including those whose transcripts were pruned.
9
+ * - **Branch, then path, then Active Ticket.** Naming a branch or a worktree is deliberate; a key
10
+ * typed into a shared session is the weakest signal and only counts when it is the only one and
11
+ * its project is a Known Project, so `GPT-6` or `SHA-256` in a prompt books nothing.
12
+ * - **Only the human's words count.** Agents inject instruction files and environment blocks as
13
+ * user-role messages, and those quote ticket-shaped examples. They are stripped before mining.
14
+ *
15
+ * Ported from session-counter's attribution, with jira-clockify's stricter key pattern.
16
+ *
17
+ * @module
18
+ */
19
+ import { Predicate } from "effect";
20
+ /**
21
+ * A Jira key: an uppercase letter, one to nine more uppercase alphanumerics, a dash and a number.
22
+ * Bounded by non-alphanumerics rather than `\b`, because `_` is a word character and branch names
23
+ * like `feature_PROJ-42_work` would otherwise match nothing.
24
+ */
25
+ const TICKET_KEY = /(?<![A-Za-z0-9])[A-Z][A-Z0-9]{1,9}-\d{1,6}(?![A-Za-z0-9])/g;
26
+ const ticketKeys = (text) => [...text.matchAll(TICKET_KEY)].map((match) => match[0]);
27
+ /** The first key a branch names: naming a branch is deliberate. */
28
+ export const ticketKeyFromBranch = (branch) => ticketKeys(branch)[0] ?? null;
29
+ /** The deepest key a path names, so `worktrees/PROJ-1/PROJ-2` is working on `PROJ-2`. */
30
+ export const ticketKeyFromPath = (cwd) => ticketKeys(cwd).at(-1) ?? null;
31
+ /**
32
+ * The one distinct key in typed text, or null when there are none or several. Stored as typed:
33
+ * whether it books is decided when read, by the Known Projects, so `RPS-123` (a real ticket number
34
+ * that looks like filler) is never lost at ingest.
35
+ */
36
+ export const singleTicket = (text) => {
37
+ const distinct = [...new Set(ticketKeys(text))];
38
+ return distinct.length === 1 ? distinct[0] ?? null : null;
39
+ };
40
+ /**
41
+ * The repo a working directory belongs to. Worktrees live at `…/worktrees/<repo>/<branch>`, so
42
+ * that segment wins; otherwise the last path segment.
43
+ */
44
+ export const repoName = (cwd) => {
45
+ const worktree = /\/worktrees\/([^/]+)\//u.exec(cwd)?.[1];
46
+ if (worktree !== undefined)
47
+ return worktree;
48
+ const base = cwd.split("/").filter((segment) => segment !== "").at(-1);
49
+ return base ?? "unknown";
50
+ };
51
+ /** The project a key belongs to: `RPS` for `RPS-7071`. */
52
+ export const projectOf = (key) => key.slice(0, key.lastIndexOf("-"));
53
+ /**
54
+ * The Known Projects: every project a branch or worktree path has named, plus the configured ones.
55
+ * Naming a branch is deliberate, so those keys vouch for their project.
56
+ */
57
+ export const knownProjects = (places, configured) => {
58
+ const projects = new Set(configured);
59
+ for (const place of places) {
60
+ for (const key of [ticketKeyFromBranch(place.branch), ticketKeyFromPath(place.cwd)]) {
61
+ if (key !== null)
62
+ projects.add(projectOf(key));
63
+ }
64
+ }
65
+ return projects;
66
+ };
67
+ /**
68
+ * Branch first, then path, then the Active Ticket when its project is known; otherwise the repo.
69
+ * Branch and path keys are trusted as they are; only typed text is checked against the projects.
70
+ */
71
+ export const attribute = (inputs, projects) => {
72
+ const deliberate = ticketKeyFromBranch(inputs.branch) ?? ticketKeyFromPath(inputs.cwd);
73
+ if (deliberate !== null)
74
+ return { booking: { _tag: "Ticket", key: deliberate }, ignoredKey: null };
75
+ const typed = inputs.activeTicket;
76
+ if (typed !== null && projects.has(projectOf(typed))) {
77
+ return { booking: { _tag: "Ticket", key: typed }, ignoredKey: null };
78
+ }
79
+ return { booking: { _tag: "Repo", name: repoName(inputs.cwd) }, ignoredKey: typed };
80
+ };
81
+ /** A stable id that keeps a ticket and a repo of the same spelling apart. */
82
+ export const bookingId = (booking) => booking._tag === "Ticket" ? `ticket:${booking.key}` : `repo:${booking.name}`;
83
+ /**
84
+ * Blocks Claude Code wraps around context it injects into a user turn. `command-args` is not one:
85
+ * it is what the human typed after a slash command, so only its tags are removed.
86
+ */
87
+ const CLAUDE_INJECTED = /<(system-reminder|command-name|command-message|local-command-[a-z-]+)>[\s\S]*?<\/\1>/gu;
88
+ const COMMAND_ARGS_TAG = /<\/?command-args>/gu;
89
+ /**
90
+ * The text a human typed into one Claude user turn: string content or `text` blocks, with tool
91
+ * results and injected reminder/command blocks removed.
92
+ */
93
+ export const claudeHumanText = (content) => {
94
+ const raw = Predicate.isString(content)
95
+ ? content
96
+ : content.flatMap((block) => block.type === "text" && block.text !== undefined ? [block.text] : []).join("\n");
97
+ return raw.replace(CLAUDE_INJECTED, "").replace(COMMAND_ARGS_TAG, " ").trim();
98
+ };
99
+ /** Codex input items that are context it injected, not words the human typed. */
100
+ const CODEX_INJECTED_PREFIXES = ["# AGENTS.md instructions", "<environment_context>", "<user_instructions>"];
101
+ /** Reminder blocks the harness wraps around context it adds to a Codex user item. */
102
+ const CODEX_REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/gu;
103
+ /** The text a human typed in one Codex `input_text` item; injected context yields "". */
104
+ export const codexHumanText = (text) => {
105
+ const trimmed = text.replace(CODEX_REMINDER, "").trim();
106
+ return CODEX_INJECTED_PREFIXES.some((prefix) => trimmed.startsWith(prefix)) ? "" : trimmed;
107
+ };
108
+ //# sourceMappingURL=Attribution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Attribution.js","sourceRoot":"","sources":["../../src/core/Attribution.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Classifying what a provider says about a spendable balance into a Balance Value, without ever
3
+ * collapsing "we could not tell" into a number. Ported from claude-statusline's credit readers.
4
+ *
5
+ * @module
6
+ */
7
+ import { Schema } from "effect";
8
+ import type { BalanceValue } from "./Model.js";
9
+ /** Codex's `rate_limits.credits` block on a rollout token_count event. */
10
+ export declare const CodexCredits: Schema.Struct<{
11
+ readonly has_credits: Schema.Boolean;
12
+ readonly unlimited: Schema.Boolean;
13
+ readonly balance: Schema.NullOr<Schema.String>;
14
+ }>;
15
+ export type CodexCredits = typeof CodexCredits.Type;
16
+ export declare const classifyCodexCredits: (credits: CodexCredits) => BalanceValue;
17
+ /** Claude's `extra_usage` block on the `/api/oauth/usage` reply. */
18
+ export declare const ExtraUsage: Schema.Struct<{
19
+ readonly is_enabled: Schema.Boolean;
20
+ readonly monthly_limit: Schema.optionalKey<Schema.NullOr<Schema.Finite>>;
21
+ readonly used_credits: Schema.optionalKey<Schema.NullOr<Schema.Finite>>;
22
+ readonly currency: Schema.optionalKey<Schema.NullOr<Schema.String>>;
23
+ readonly decimal_places: Schema.optionalKey<Schema.NullOr<Schema.Int>>;
24
+ readonly spend_limit_reached: Schema.optionalKey<Schema.NullOr<Schema.Boolean>>;
25
+ }>;
26
+ export type ExtraUsage = typeof ExtraUsage.Type;
27
+ export declare const classifyExtraUsage: (extra: ExtraUsage | null | undefined) => BalanceValue;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Classifying what a provider says about a spendable balance into a Balance Value, without ever
3
+ * collapsing "we could not tell" into a number. Ported from claude-statusline's credit readers.
4
+ *
5
+ * @module
6
+ */
7
+ import { Option, Schema } from "effect";
8
+ const unknown = (reason) => ({ _tag: "Unknown", reason });
9
+ /** Codex's `rate_limits.credits` block on a rollout token_count event. */
10
+ export const CodexCredits = Schema.Struct({
11
+ has_credits: Schema.Boolean,
12
+ unlimited: Schema.Boolean,
13
+ balance: Schema.NullOr(Schema.String)
14
+ });
15
+ export const classifyCodexCredits = (credits) => {
16
+ if (credits.unlimited)
17
+ return { _tag: "Known", balance: { _tag: "Unlimited" } };
18
+ // No credits on the account: a "0" here is not a balance that ran out.
19
+ if (!credits.has_credits)
20
+ return unknown("NotSupported");
21
+ if (credits.balance === null)
22
+ return unknown("NoData");
23
+ const raw = credits.balance.trim();
24
+ const amount = Number(raw);
25
+ if (raw === "" || !Number.isFinite(amount))
26
+ return unknown("Parse");
27
+ if (amount <= 0)
28
+ return { _tag: "Known", balance: { _tag: "Exhausted" } };
29
+ return { _tag: "Known", balance: { _tag: "Credits", credits: amount } };
30
+ };
31
+ /** Claude's `extra_usage` block on the `/api/oauth/usage` reply. */
32
+ export const ExtraUsage = Schema.Struct({
33
+ is_enabled: Schema.Boolean,
34
+ monthly_limit: Schema.optionalKey(Schema.NullOr(Schema.Finite)),
35
+ used_credits: Schema.optionalKey(Schema.NullOr(Schema.Finite)),
36
+ currency: Schema.optionalKey(Schema.NullOr(Schema.String)),
37
+ decimal_places: Schema.optionalKey(Schema.NullOr(Schema.Int)),
38
+ spend_limit_reached: Schema.optionalKey(Schema.NullOr(Schema.Boolean))
39
+ });
40
+ export const classifyExtraUsage = (extra) => {
41
+ if (extra === undefined || extra === null)
42
+ return unknown("NotSupported");
43
+ if (!extra.is_enabled) {
44
+ return { _tag: "Known", balance: { _tag: extra.spend_limit_reached === true ? "Exhausted" : "Disabled" } };
45
+ }
46
+ // Amounts are minor units of `currency`; without both, any figure is a guess.
47
+ const currency = Option.fromNullishOr(extra.currency);
48
+ const decimals = Option.fromNullishOr(extra.decimal_places);
49
+ if (Option.isNone(currency) || Option.isNone(decimals))
50
+ return unknown("Parse");
51
+ const limit = extra.monthly_limit;
52
+ const used = extra.used_credits;
53
+ if (limit === undefined || limit === null || used === undefined || used === null)
54
+ return unknown("NoData");
55
+ const left = limit - used;
56
+ if (left <= 0)
57
+ return { _tag: "Known", balance: { _tag: "Exhausted" } };
58
+ return {
59
+ _tag: "Known",
60
+ balance: { _tag: "Amount", leftMinor: left, limitMinor: limit, decimals: decimals.value, currency: currency.value }
61
+ };
62
+ };
63
+ //# sourceMappingURL=Balances.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Balances.js","sourceRoot":"","sources":["../../src/core/Balances.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Claude's subscription limits and extra-usage balance, polled from `GET /api/oauth/usage`: the
3
+ * endpoint Claude Code's `/usage` dialog reads. Claude limits appear in no transcript, so a poll is
4
+ * the only way to see them, and a Limit Snapshot not taken is gone.
5
+ *
6
+ * **Mental model** (the rules claude-statusline already follows)
7
+ *
8
+ * - **The token goes to Anthropic and nowhere else.** It is read per poll, put in the request
9
+ * header, and never cached, logged or carried in an error.
10
+ * - **Never refresh.** Rotating the refresh token would race Claude Code's own refresh; an expired
11
+ * token waits for Claude Code to renew it.
12
+ * - **Every failure is a reading.** No credentials, an expired token, a failed request and an
13
+ * unparseable reply each become an Unknown Limit Snapshot (label `*`) and an Unknown balance, so
14
+ * the gap shows on the graph instead of the last value lingering.
15
+ * - **Provider keys are labels.** Any top-level window with a numeric `utilization` is recorded under
16
+ * its key; `five_hour` and the `seven_day*` keys have known lengths, the rest are listed with an
17
+ * unknown length rather than dropped.
18
+ *
19
+ * @module
20
+ */
21
+ import { Effect } from "effect";
22
+ import type { BalanceReading, LimitSnapshot } from "./Model.js";
23
+ declare const CredentialsMissing_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
24
+ readonly _tag: "CredentialsMissing";
25
+ } & Readonly<A>;
26
+ export declare class CredentialsMissing extends CredentialsMissing_base<{}> {
27
+ }
28
+ declare const TokenExpired_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
29
+ readonly _tag: "TokenExpired";
30
+ } & Readonly<A>;
31
+ export declare class TokenExpired extends TokenExpired_base<{}> {
32
+ }
33
+ declare const UsageFetchFailed_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
34
+ readonly _tag: "UsageFetchFailed";
35
+ } & Readonly<A>;
36
+ export declare class UsageFetchFailed extends UsageFetchFailed_base<{
37
+ readonly cause: unknown;
38
+ }> {
39
+ }
40
+ export interface UsageReply {
41
+ readonly status: number;
42
+ readonly body: string;
43
+ }
44
+ /** How a poll reaches the token and the endpoint; the live pair lives at the executable boundary. */
45
+ export interface ClaudeUsageDeps {
46
+ readonly readToken: Effect.Effect<string, CredentialsMissing | TokenExpired>;
47
+ readonly get: (token: string) => Effect.Effect<UsageReply, UsageFetchFailed>;
48
+ }
49
+ export declare const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
50
+ /** The access token in Claude Code's credentials JSON, refusing one that has already expired. */
51
+ export declare const tokenFromCredentials: (raw: string) => Effect.Effect<string, CredentialsMissing | TokenExpired>;
52
+ interface Observations {
53
+ readonly snapshots: ReadonlyArray<LimitSnapshot>;
54
+ readonly balances: ReadonlyArray<BalanceReading>;
55
+ }
56
+ /** One poll's observations. Never fails: every failure is an Unknown reading. */
57
+ export declare const pollClaudeLimits: (deps: ClaudeUsageDeps, machine: string) => Effect.Effect<Observations>;
58
+ export {};
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Claude's subscription limits and extra-usage balance, polled from `GET /api/oauth/usage`: the
3
+ * endpoint Claude Code's `/usage` dialog reads. Claude limits appear in no transcript, so a poll is
4
+ * the only way to see them, and a Limit Snapshot not taken is gone.
5
+ *
6
+ * **Mental model** (the rules claude-statusline already follows)
7
+ *
8
+ * - **The token goes to Anthropic and nowhere else.** It is read per poll, put in the request
9
+ * header, and never cached, logged or carried in an error.
10
+ * - **Never refresh.** Rotating the refresh token would race Claude Code's own refresh; an expired
11
+ * token waits for Claude Code to renew it.
12
+ * - **Every failure is a reading.** No credentials, an expired token, a failed request and an
13
+ * unparseable reply each become an Unknown Limit Snapshot (label `*`) and an Unknown balance, so
14
+ * the gap shows on the graph instead of the last value lingering.
15
+ * - **Provider keys are labels.** Any top-level window with a numeric `utilization` is recorded under
16
+ * its key; `five_hour` and the `seven_day*` keys have known lengths, the rest are listed with an
17
+ * unknown length rather than dropped.
18
+ *
19
+ * @module
20
+ */
21
+ import { Clock, Data, Effect, Option, Predicate, Schema } from "effect";
22
+ import { classifyExtraUsage, ExtraUsage } from "./Balances.js";
23
+ export class CredentialsMissing extends Data.TaggedError("CredentialsMissing") {
24
+ }
25
+ export class TokenExpired extends Data.TaggedError("TokenExpired") {
26
+ }
27
+ export class UsageFetchFailed extends Data.TaggedError("UsageFetchFailed") {
28
+ }
29
+ export const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
30
+ const Credentials = Schema.fromJsonString(Schema.Struct({
31
+ claudeAiOauth: Schema.Struct({
32
+ accessToken: Schema.NonEmptyString,
33
+ expiresAt: Schema.optionalKey(Schema.Finite)
34
+ })
35
+ }));
36
+ const decodeCredentials = Schema.decodeUnknownOption(Credentials);
37
+ /** The access token in Claude Code's credentials JSON, refusing one that has already expired. */
38
+ export const tokenFromCredentials = (raw) => Effect.gen(function* () {
39
+ const credentials = decodeCredentials(raw);
40
+ if (Option.isNone(credentials))
41
+ return yield* new CredentialsMissing();
42
+ const { accessToken, expiresAt } = credentials.value.claudeAiOauth;
43
+ const now = yield* Clock.currentTimeMillis;
44
+ if (expiresAt !== undefined && expiresAt <= now)
45
+ return yield* new TokenExpired();
46
+ return accessToken;
47
+ });
48
+ const Window = Schema.Struct({
49
+ utilization: Schema.Finite,
50
+ resets_at: Schema.optionalKey(Schema.NullOr(Schema.String))
51
+ });
52
+ const decodeWindow = Schema.decodeUnknownOption(Window);
53
+ const decodeReply = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)));
54
+ const decodeExtraUsage = Schema.decodeUnknownOption(Schema.NullOr(ExtraUsage));
55
+ const windowMinutes = (label) => label === "five_hour" ? 300 : label.startsWith("seven_day") ? 10_080 : null;
56
+ const unknownObservations = (machine, observedAt, reason) => ({
57
+ snapshots: [{
58
+ agent: "claude",
59
+ machine,
60
+ source: "claude-oauth-usage",
61
+ label: "*",
62
+ windowMinutes: null,
63
+ observedAt,
64
+ reading: { _tag: "Unknown", reason }
65
+ }],
66
+ balances: [{ kind: "claude-extra-usage", machine, observedAt, value: { _tag: "Unknown", reason } }]
67
+ });
68
+ const classify = (machine, observedAt, reply) => {
69
+ if (reply.status === 401 || reply.status === 403)
70
+ return unknownObservations(machine, observedAt, "AuthExpired");
71
+ if (reply.status !== 200)
72
+ return unknownObservations(machine, observedAt, "Fetch");
73
+ const decoded = decodeReply(reply.body);
74
+ if (Option.isNone(decoded))
75
+ return unknownObservations(machine, observedAt, "Parse");
76
+ // Extra usage is a balance, not a percentage window, even when it reports a utilization.
77
+ const windows = Object.entries(decoded.value).filter(([label]) => label !== "extra_usage");
78
+ const snapshots = windows.flatMap(([label, value]) => {
79
+ const window = decodeWindow(value);
80
+ const snapshot = (reading) => [{
81
+ agent: "claude",
82
+ machine,
83
+ source: "claude-oauth-usage",
84
+ label,
85
+ windowMinutes: windowMinutes(label),
86
+ observedAt,
87
+ reading
88
+ }];
89
+ if (Option.isNone(window)) {
90
+ // A window that is there but does not read is a failed reading of it, not silence: its last
91
+ // level must not carry on.
92
+ const claimsWindow = Predicate.hasProperty(value, "utilization") && value.utilization !== null;
93
+ return claimsWindow ? snapshot({ _tag: "Unknown", reason: "Parse" }) : [];
94
+ }
95
+ const resets = window.value.resets_at;
96
+ if (resets === undefined || resets === null) {
97
+ return snapshot({ _tag: "Known", usedPercent: window.value.utilization, resetsAt: null });
98
+ }
99
+ const resetsAt = Date.parse(resets);
100
+ return Number.isFinite(resetsAt)
101
+ ? snapshot({ _tag: "Known", usedPercent: window.value.utilization, resetsAt })
102
+ : snapshot({ _tag: "Unknown", reason: "Parse" });
103
+ });
104
+ const extra = decodeExtraUsage(decoded.value["extra_usage"] ?? null);
105
+ const balance = {
106
+ kind: "claude-extra-usage",
107
+ machine,
108
+ observedAt,
109
+ value: Option.isNone(extra) ? { _tag: "Unknown", reason: "Parse" } : classifyExtraUsage(extra.value)
110
+ };
111
+ return {
112
+ snapshots: snapshots.length === 0 ? unknownObservations(machine, observedAt, "NoData").snapshots : snapshots,
113
+ balances: [balance]
114
+ };
115
+ };
116
+ /** One poll's observations. Never fails: every failure is an Unknown reading. */
117
+ export const pollClaudeLimits = (deps, machine) => Effect.gen(function* () {
118
+ const observedAt = yield* Clock.currentTimeMillis;
119
+ return yield* deps.readToken.pipe(Effect.flatMap(deps.get), Effect.map((reply) => classify(machine, observedAt, reply)), Effect.catchTags({
120
+ CredentialsMissing: () => Effect.succeed(unknownObservations(machine, observedAt, "NoAuth")),
121
+ TokenExpired: () => Effect.succeed(unknownObservations(machine, observedAt, "AuthExpired")),
122
+ UsageFetchFailed: () => Effect.succeed(unknownObservations(machine, observedAt, "Fetch"))
123
+ }));
124
+ });
125
+ //# sourceMappingURL=ClaudeLimits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClaudeLimits.js","sourceRoot":"","sources":["../../src/core/ClaudeLimits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvE,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG9D,MAAM,OAAO,kBAAmB,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAAK;CAAG;AACrF,MAAM,OAAO,YAAa,SAAQ,IAAI,CAAC,WAAW,CAAC,cAAc,CAAK;CAAG;AACzE,MAAM,OAAO,gBAAiB,SAAQ,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAA8B;CAAG;AAa1G,MAAM,CAAC,MAAM,SAAS,GAAG,2CAA2C,CAAA;AAEpE,MAAM,WAAW,GAAG,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC;IACtD,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC;QAC3B,WAAW,EAAE,MAAM,CAAC,cAAc;QAClC,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;KAC7C,CAAC;CACH,CAAC,CAAC,CAAA;AACH,MAAM,iBAAiB,GAAG,MAAM,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAA;AAEjE,iGAAiG;AACjG,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,GAAW,EAA4D,EAAE,CAC5G,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,WAAW,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAA;IAC1C,IAAI,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,IAAI,kBAAkB,EAAE,CAAA;IACtE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,aAAa,CAAA;IAClE,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAA;IAC1C,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,IAAI,GAAG;QAAE,OAAO,KAAK,CAAC,CAAC,IAAI,YAAY,EAAE,CAAA;IACjF,OAAO,WAAW,CAAA;AACpB,CAAC,CAAC,CAAA;AAEJ,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC,MAAM;IAC1B,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;CAC5D,CAAC,CAAA;AACF,MAAM,YAAY,GAAG,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAA;AACvD,MAAM,WAAW,GAAG,MAAM,CAAC,mBAAmB,CAC5C,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CACpE,CAAA;AACD,MAAM,gBAAgB,GAAG,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAA;AAE9E,MAAM,aAAa,GAAG,CAAC,KAAa,EAAiB,EAAE,CACrD,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;AAO7E,MAAM,mBAAmB,GAAG,CAAC,OAAe,EAAE,UAAkB,EAAE,MAAqB,EAAgB,EAAE,CAAC,CAAC;IACzG,SAAS,EAAE,CAAC;YACV,KAAK,EAAE,QAAQ;YACf,OAAO;YACP,MAAM,EAAE,oBAAoB;YAC5B,KAAK,EAAE,GAAG;YACV,aAAa,EAAE,IAAI;YACnB,UAAU;YACV,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE;SACrC,CAAC;IACF,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;CACpG,CAAC,CAAA;AAEF,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,UAAkB,EAAE,KAAiB,EAAgB,EAAE;IACxF,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,aAAa,CAAC,CAAA;IAChH,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;IAClF,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACvC,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;IACpF,yFAAyF;IACzF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,aAAa,CAAC,CAAA;IAC1F,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAgC,EAAE;QACjF,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;QAClC,MAAM,QAAQ,GAAG,CAAC,OAAiC,EAAgC,EAAE,CAAC,CAAC;gBACrF,KAAK,EAAE,QAAQ;gBACf,OAAO;gBACP,MAAM,EAAE,oBAAoB;gBAC5B,KAAK;gBACL,aAAa,EAAE,aAAa,CAAC,KAAK,CAAC;gBACnC,UAAU;gBACV,OAAO;aACR,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,4FAA4F;YAC5F,2BAA2B;YAC3B,MAAM,YAAY,GAAG,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,CAAA;YAC9F,OAAO,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAC3E,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAA;QACrC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAC5C,OAAO,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3F,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC9B,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,CAAC;YAC9E,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IACF,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,CAAA;IACpE,MAAM,OAAO,GAAmB;QAC9B,IAAI,EAAE,oBAAoB;QAC1B,OAAO;QACP,UAAU;QACV,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC;KACrG,CAAA;IACD,OAAO;QACL,SAAS,EAAE,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;QAC5G,QAAQ,EAAE,CAAC,OAAO,CAAC;KACpB,CAAA;AACH,CAAC,CAAA;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAqB,EAAE,OAAe,EAA+B,EAAE,CACtG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAA;IACjD,OAAO,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAC/B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EACxB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,EAC3D,MAAM,CAAC,SAAS,CAAC;QACf,kBAAkB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC5F,YAAY,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC;QAC3F,gBAAgB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;KAC1F,CAAC,CACH,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The live token and endpoint behind {@link pollClaudeLimits}.
3
+ *
4
+ * Linux keeps Claude Code's credentials in `<config>/.credentials.json`; macOS keeps them in the
5
+ * login Keychain. The file is tried first and the Keychain second, so no platform check is needed:
6
+ * on Linux the `security` binary does not exist and the lookup fails as missing credentials.
7
+ *
8
+ * The executable provides `FetchHttpClient.layer`; redirects are refused so the token cannot
9
+ * follow one off Anthropic's host.
10
+ *
11
+ * @module
12
+ */
13
+ import { Effect, FileSystem, Path } from "effect";
14
+ import { HttpClient } from "effect/http";
15
+ import { ChildProcessSpawner } from "effect/process";
16
+ import { type ClaudeUsageDeps } from "./ClaudeLimits.js";
17
+ /** Builds the live dependencies for a Claude config directory (`CLAUDE_CONFIG_DIR` or `~/.claude`). */
18
+ export declare const liveClaudeUsageDeps: (claudeConfigDir: string) => Effect.Effect<ClaudeUsageDeps, never, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The live token and endpoint behind {@link pollClaudeLimits}.
3
+ *
4
+ * Linux keeps Claude Code's credentials in `<config>/.credentials.json`; macOS keeps them in the
5
+ * login Keychain. The file is tried first and the Keychain second, so no platform check is needed:
6
+ * on Linux the `security` binary does not exist and the lookup fails as missing credentials.
7
+ *
8
+ * The executable provides `FetchHttpClient.layer`; redirects are refused so the token cannot
9
+ * follow one off Anthropic's host.
10
+ *
11
+ * @module
12
+ */
13
+ import { Duration, Effect, FileSystem, Path } from "effect";
14
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
15
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
16
+ import { CredentialsMissing, tokenFromCredentials, USAGE_URL, UsageFetchFailed } from "./ClaudeLimits.js";
17
+ const TIMEOUT = Duration.seconds(5);
18
+ /** Builds the live dependencies for a Claude config directory (`CLAUDE_CONFIG_DIR` or `~/.claude`). */
19
+ export const liveClaudeUsageDeps = (claudeConfigDir) => Effect.gen(function* () {
20
+ const fs = yield* FileSystem.FileSystem;
21
+ const path = yield* Path.Path;
22
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
23
+ const client = yield* HttpClient.HttpClient;
24
+ const fromFile = fs.readFileString(path.join(claudeConfigDir, ".credentials.json")).pipe(Effect.mapError(() => new CredentialsMissing()));
25
+ const fromKeychain = spawner.string(ChildProcess.make("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"])).pipe(Effect.mapError(() => new CredentialsMissing()));
26
+ const deps = {
27
+ readToken: fromFile.pipe(Effect.catchTag("CredentialsMissing", () => fromKeychain), Effect.flatMap(tokenFromCredentials)),
28
+ get: (token) => client.execute(HttpClientRequest.get(USAGE_URL).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeaders({ "anthropic-beta": "oauth-2025-04-20", "User-Agent": "agent-usage" }))).pipe(Effect.flatMap((response) => Effect.map(response.text, (body) => ({ status: response.status, body }))), Effect.timeout(TIMEOUT),
29
+ // A redirect would carry the bearer token to another host; refuse to follow one.
30
+ Effect.provideService(FetchHttpClient.RequestInit, { redirect: "error" }),
31
+ // Only the failure's kind is kept: the request it came from carries the token.
32
+ Effect.mapError((error) => new UsageFetchFailed({ cause: error._tag })))
33
+ };
34
+ return deps;
35
+ });
36
+ //# sourceMappingURL=ClaudeLimitsLive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClaudeLimitsLive.js","sourceRoot":"","sources":["../../src/core/ClaudeLimitsLive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC3D,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAClE,OAAO,EAEL,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,gBAAgB,EACjB,MAAM,mBAAmB,CAAA;AAE1B,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAEnC,uGAAuG;AACvG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,eAAuB,EAAE,EAAE,CAC7D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAA;IAC7B,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,mBAAmB,CAAC,mBAAmB,CAAA;IAC9D,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IAE3C,MAAM,QAAQ,GAAG,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC,CAAC,IAAI,CACtF,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,kBAAkB,EAAE,CAAC,CAChD,CAAA;IACD,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CACjC,YAAY,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,uBAAuB,EAAE,IAAI,EAAE,yBAAyB,EAAE,IAAI,CAAC,CAAC,CAChG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,kBAAkB,EAAE,CAAC,CAAC,CAAA;IAEvD,MAAM,IAAI,GAAoB;QAC5B,SAAS,EAAE,QAAQ,CAAC,IAAI,CACtB,MAAM,CAAC,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,EACzD,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,CACrC;QACD,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CACb,MAAM,CAAC,OAAO,CACZ,iBAAiB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CACnC,iBAAiB,CAAC,WAAW,CAAC,KAAK,CAAC,EACpC,iBAAiB,CAAC,UAAU,CAAC,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC,CACpG,CACF,CAAC,IAAI,CACJ,MAAM,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EACtG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;QACvB,iFAAiF;QACjF,MAAM,CAAC,cAAc,CAAC,eAAe,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;QACzE,+EAA+E;QAC/E,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CACxE;KACJ,CAAA;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAC,CAAA"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Reading Claude Code transcripts (`<config>/projects/<project>/<session>.jsonl`, and each session's
3
+ * `subagents/*.jsonl`, booked to the parent session) into Usage Events.
4
+ *
5
+ * **Mental model**
6
+ *
7
+ * - **One event per model request.** Claude Code writes a message once per content block, each line
8
+ * repeating the usage; message id + request id dedupes them. Should a later line carry larger
9
+ * counts (a streamed request's final usage), the larger counts win, here and in the store.
10
+ * - **Zero-token messages are placeholders** (`<synthetic>` local replies), not requests.
11
+ * - **The Active Ticket follows the human.** Each typed user turn sets it to the one ticket it names,
12
+ * or clears it; tool results, meta turns, sidechain (subagent) prompts and injected reminders
13
+ * never touch it.
14
+ * - **Prompt and response text is read here and dropped here.** Nothing but counts and metadata
15
+ * leaves this module.
16
+ *
17
+ * @module
18
+ */
19
+ import { Schema } from "effect";
20
+ import { type ReadResult, type SourceFile, type SourceLine } from "./Readers.js";
21
+ export declare const ClaudeReaderState: Schema.Struct<{
22
+ readonly activeTicket: Schema.NullOr<Schema.String>;
23
+ }>;
24
+ export type ClaudeReaderState = typeof ClaudeReaderState.Type;
25
+ export declare const initialClaudeState: ClaudeReaderState;
26
+ /** Reads one chunk of a transcript, continuing from the state the previous chunk ended in. */
27
+ export declare const readClaude: (file: SourceFile, lines: ReadonlyArray<SourceLine>, state: ClaudeReaderState) => ReadResult<ClaudeReaderState>;