@yagni-app/code 0.1.0 → 0.2.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/README.md +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +194 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +205 -0
- package/dist/extension/pipeline/eval.js +226 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +645 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +53 -0
- package/dist/extension/pipeline/stages.js +115 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/rerouteNotice.d.ts +34 -0
- package/dist/extension/rerouteNotice.js +79 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +145 -0
- package/dist/extension/subagents.js +326 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +11 -15
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 - /cost command + a session usage accumulator.
|
|
3
|
+
*
|
|
4
|
+
* pi already shows model + usage in its footer, so this does not rebuild a live
|
|
5
|
+
* counter. The net-new value is a pull `/cost` command backed by a pure,
|
|
6
|
+
* session-scoped accumulator and an optional, fail-soft credits-headroom fetch, so
|
|
7
|
+
* invisible enforcement becomes visible trust.
|
|
8
|
+
*
|
|
9
|
+
* Feasibility correction (verified against pi 0.80.2): the interactive
|
|
10
|
+
* `after_provider_response` event carries only { status, headers }, NOT usage. The
|
|
11
|
+
* usage-bearing seam is `turn_end`, whose `message: AgentMessage` carries `usage`
|
|
12
|
+
* (the same shape events.ts already reads off the NDJSON stream). So the
|
|
13
|
+
* accumulator hangs off `turn_end`, not `after_provider_response`.
|
|
14
|
+
*
|
|
15
|
+
* YAG-383: the backend now serves `GET /api/yagni-code/spend?sessionId=`, a
|
|
16
|
+
* server-authoritative figure invisible to `turn_end` (subagents and advisor
|
|
17
|
+
* consults bill under this same session id, so a plain `sessionId=` query
|
|
18
|
+
* already sees them). `/go`'s children are the one exception: they bill under
|
|
19
|
+
* their OWN run id (`llm_usage.session_id` holds the run id, not the driver's
|
|
20
|
+
* session id, for a run-attributed dispatch), so the fetch also widens with
|
|
21
|
+
* `&runIds=` for every run this session has launched (see sessionRuns.ts and
|
|
22
|
+
* the goCommand.ts seam that records one as soon as it is known). `/cost`
|
|
23
|
+
* PREFERS that combined number. The local `turn_end` accumulator remains as
|
|
24
|
+
* the fail-soft fallback: it only ever sees the driver's own turns, so
|
|
25
|
+
* whenever it is shown it is explicitly labeled "(local, driver only)" rather
|
|
26
|
+
* than presented as the whole session's spend.
|
|
27
|
+
*/
|
|
28
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
/** A single turn's usage delta, normalized from pi's message.usage shape. */
|
|
30
|
+
export interface UsageDelta {
|
|
31
|
+
input: number;
|
|
32
|
+
output: number;
|
|
33
|
+
cacheRead: number;
|
|
34
|
+
cacheWrite: number;
|
|
35
|
+
cost: number;
|
|
36
|
+
}
|
|
37
|
+
/** Running session totals (turns = number of assistant turns accumulated). */
|
|
38
|
+
export interface CostSnapshot extends UsageDelta {
|
|
39
|
+
turns: number;
|
|
40
|
+
}
|
|
41
|
+
export interface CostAccumulator {
|
|
42
|
+
add(delta: UsageDelta): void;
|
|
43
|
+
snapshot(): CostSnapshot;
|
|
44
|
+
reset(): void;
|
|
45
|
+
}
|
|
46
|
+
/** A pure, session-scoped usage accumulator. */
|
|
47
|
+
export declare function makeCostAccumulator(): CostAccumulator;
|
|
48
|
+
/** Extract a normalized {@link UsageDelta} from a turn_end message, defensively. */
|
|
49
|
+
export declare function usageFromMessage(message: unknown): UsageDelta;
|
|
50
|
+
/** Credit headroom returned by the backend (fail-soft; null when unavailable). */
|
|
51
|
+
export interface Headroom {
|
|
52
|
+
remaining: number;
|
|
53
|
+
unit: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function formatCostLine(snap: CostSnapshot, headroom?: Headroom | null, advisorLine?: string, source?: string, boosted?: boolean): string;
|
|
56
|
+
/**
|
|
57
|
+
* One row of `GET /api/yagni-code/spend`'s per-caller x rate-tier breakdown.
|
|
58
|
+
* Mirrored locally rather than importing `@yagni/shared`, since this extension
|
|
59
|
+
* has no dependency on the shared package (see `YagniCodeSpendRow` in
|
|
60
|
+
* `packages/shared/src/types.ts` for the canonical, fuller-commented
|
|
61
|
+
* definition this must stay shape-compatible with).
|
|
62
|
+
*/
|
|
63
|
+
export interface SpendRow {
|
|
64
|
+
caller: string;
|
|
65
|
+
rateTier: string;
|
|
66
|
+
boost: boolean;
|
|
67
|
+
dispatches: number;
|
|
68
|
+
promptTokens: number;
|
|
69
|
+
completionTokens: number;
|
|
70
|
+
cacheReadTokens: number;
|
|
71
|
+
cacheCreationTokens: number;
|
|
72
|
+
sellMillicents: number;
|
|
73
|
+
counterfactualMillicents: number | null;
|
|
74
|
+
}
|
|
75
|
+
/** `GET /api/yagni-code/spend`'s response shape (mirrors `YagniCodeSpendResponse`). */
|
|
76
|
+
export interface SpendResponse {
|
|
77
|
+
rows: SpendRow[];
|
|
78
|
+
totalSellMillicents: number;
|
|
79
|
+
unbilledSellMillicents: number;
|
|
80
|
+
totalCounterfactualMillicents: number;
|
|
81
|
+
savingsMillicents: number;
|
|
82
|
+
savingsPercent: number | null;
|
|
83
|
+
counterfactualIncomplete: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Millicents (1/1000 of a cent) -> dollars, 2 decimals. Guards against a
|
|
87
|
+
* malformed response field (NaN/Infinity) rendering as literal "NaN"/"Infinity"
|
|
88
|
+
* in a dollar amount: render $0.00 instead, since a network response is
|
|
89
|
+
* untyped at runtime and every other guard in this module is best-effort.
|
|
90
|
+
*
|
|
91
|
+
* Exported so `/go`'s per-stage cost table (runCostTable.ts, Task 8) reuses
|
|
92
|
+
* this exact money formatting instead of duplicating it.
|
|
93
|
+
*/
|
|
94
|
+
export declare const usd: (millicents: number) => string;
|
|
95
|
+
/**
|
|
96
|
+
* Render the server-authoritative /cost lines: PREFERRED over
|
|
97
|
+
* {@link formatCostLine}'s local accumulator whenever the spend fetch succeeds
|
|
98
|
+
* (see `registerCostCommand`'s empty-response guard for the one exception),
|
|
99
|
+
* because it covers children the local accumulator cannot see (subagents,
|
|
100
|
+
* advisor consults, and every /go run this session launched, widened in via
|
|
101
|
+
* `runIds` before the fetch even runs). Pure, no em-dashes, multi-line
|
|
102
|
+
* (joined with "\n").
|
|
103
|
+
*
|
|
104
|
+
* Line order: total spend, one line per rate tier (aggregated across callers,
|
|
105
|
+
* sorted by spend descending), a boosted-spend subtotal per tier that has any
|
|
106
|
+
* boosted rows, a live-toggle boost line (see below), the savings line (only
|
|
107
|
+
* when a counterfactual total resolved), an incomplete-counterfactual note,
|
|
108
|
+
* an unbilled note, a dropped-run-ids note (carry-over from the /cost
|
|
109
|
+
* re-review — see sessionRuns.ts's `droppedSessionRuns`), then headroom.
|
|
110
|
+
*
|
|
111
|
+
* `droppedRunCount` defaults to 0 (no note) so every existing direct caller
|
|
112
|
+
* of this pure function keeps behaving identically without passing it.
|
|
113
|
+
*
|
|
114
|
+
* `boosted` (default false) is the LIVE client-side toggle from
|
|
115
|
+
* `boostCommand.ts`'s `isBoosted()`, threaded in exactly like `advisorLine`
|
|
116
|
+
* is for {@link formatCostLine}. It is independent of the per-tier "Boosted
|
|
117
|
+
* (tier) spend" subtotal above: that subtotal only reflects server rows
|
|
118
|
+
* carrying `x-yagni-boost`, which only `/go` children (and other spawned
|
|
119
|
+
* processes) ever send — the driver's own turns never do (see
|
|
120
|
+
* boostCommand.ts's KNOWN-asymmetry docblock). Without this flag, a session
|
|
121
|
+
* that only chats while boosted would show no boost line at all.
|
|
122
|
+
*/
|
|
123
|
+
export declare function formatServerCostLines(spend: SpendResponse, headroom?: Headroom | null, droppedRunCount?: number, boosted?: boolean): string;
|
|
124
|
+
export interface RegisterCostDeps {
|
|
125
|
+
/** Fetch remaining credit headroom; return null when unavailable. Fail-soft. */
|
|
126
|
+
fetchHeadroom?: (signal?: AbortSignal) => Promise<Headroom | null>;
|
|
127
|
+
/**
|
|
128
|
+
* Render the ask_advisor subtotal for this session, or "" when it never
|
|
129
|
+
* escalated. Threaded in because advisor consults run in a child process and
|
|
130
|
+
* so never reach the `turn_end` accumulator. Only used in the local fallback
|
|
131
|
+
* line: the server-authoritative line already includes advisor spend as an
|
|
132
|
+
* ordinary caller row.
|
|
133
|
+
*/
|
|
134
|
+
advisorSubtotal?: () => string;
|
|
135
|
+
/**
|
|
136
|
+
* Whether the session is currently boosted to Peak (boostCommand.ts's
|
|
137
|
+
* `isBoosted()`, threaded in the same way as `advisorSubtotal`). Unlike
|
|
138
|
+
* `advisorSubtotal`, this is read on BOTH the server-authoritative and the
|
|
139
|
+
* local-fallback branch: the driver's own turns never carry a server-side
|
|
140
|
+
* boost marker (see boostCommand.ts's KNOWN-asymmetry docblock), so this is
|
|
141
|
+
* the only signal that would otherwise be missing from the server branch.
|
|
142
|
+
*/
|
|
143
|
+
isBoosted?: () => boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Fetch the server-authoritative session spend (YAG-383). Absent, throwing,
|
|
146
|
+
* or resolving null all fall back to the local `turn_end` accumulator, with
|
|
147
|
+
* its source explicitly labeled so it is never mistaken for the full total.
|
|
148
|
+
*/
|
|
149
|
+
fetchSpend?: (signal?: AbortSignal) => Promise<SpendResponse | null>;
|
|
150
|
+
/**
|
|
151
|
+
* Quiet diagnostic hook: fired when the server's DRIVER-scoped spend (the
|
|
152
|
+
* `spend.rows` entries with `caller === "driver"` — the same scope the
|
|
153
|
+
* local `turn_end` accumulator can see) and the local accumulator disagree
|
|
154
|
+
* by more than {@link DIVERGENCE_THRESHOLD} (relative to the larger), so
|
|
155
|
+
* long as both are available. NEVER user-facing: the caller decides
|
|
156
|
+
* whether/where to log it. Defaults to a no-op.
|
|
157
|
+
*
|
|
158
|
+
* Carry-over fix from the /cost re-review: this used to compare against
|
|
159
|
+
* `spend.totalSellMillicents`, which (since YAG-383's `runIds` widening)
|
|
160
|
+
* includes every `/go` run this session launched — spend the LOCAL
|
|
161
|
+
* accumulator can never see (every /go stage runs in a child process). That
|
|
162
|
+
* made the two totals disagree by construction on any session with a /go
|
|
163
|
+
* run behind it, firing on effectively every post-run /cost rather than on
|
|
164
|
+
* a genuine drift between the two driver-scoped measurements.
|
|
165
|
+
*/
|
|
166
|
+
onDivergence?: (driverServerUsd: number, localUsd: number) => void;
|
|
167
|
+
/**
|
|
168
|
+
* Count of `/go` run ids this session has ever dropped off the
|
|
169
|
+
* `MAX_TRACKED_RUN_IDS` cap (sessionRuns.ts's `droppedSessionRuns`).
|
|
170
|
+
* Carry-over from the /cost re-review: surfaced as an honest "Excludes N
|
|
171
|
+
* earlier /go runs." note on the server-authoritative line so a very
|
|
172
|
+
* long-lived session's total is never silently missing runs. Absent (or
|
|
173
|
+
* returning 0) omits the note entirely.
|
|
174
|
+
*/
|
|
175
|
+
droppedSessionRuns?: () => number;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Wire the turn_end accumulator + the /cost command. /cost prefers the
|
|
179
|
+
* server-authoritative spend (fetched fresh on every call); a failing, absent,
|
|
180
|
+
* or null-resolving fetch falls back to the local session accumulator, labeled
|
|
181
|
+
* "(local, driver only)" so it is never mistaken for the whole session's spend.
|
|
182
|
+
* `/cost reset` zeroes only the local counter. The server figure is
|
|
183
|
+
* unaffected, and the reset notice says so.
|
|
184
|
+
*/
|
|
185
|
+
export declare function registerCostCommand(pi: ExtensionAPI, deps?: RegisterCostDeps): void;
|
|
186
|
+
//# sourceMappingURL=costHud.d.ts.map
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 - /cost command + a session usage accumulator.
|
|
3
|
+
*
|
|
4
|
+
* pi already shows model + usage in its footer, so this does not rebuild a live
|
|
5
|
+
* counter. The net-new value is a pull `/cost` command backed by a pure,
|
|
6
|
+
* session-scoped accumulator and an optional, fail-soft credits-headroom fetch, so
|
|
7
|
+
* invisible enforcement becomes visible trust.
|
|
8
|
+
*
|
|
9
|
+
* Feasibility correction (verified against pi 0.80.2): the interactive
|
|
10
|
+
* `after_provider_response` event carries only { status, headers }, NOT usage. The
|
|
11
|
+
* usage-bearing seam is `turn_end`, whose `message: AgentMessage` carries `usage`
|
|
12
|
+
* (the same shape events.ts already reads off the NDJSON stream). So the
|
|
13
|
+
* accumulator hangs off `turn_end`, not `after_provider_response`.
|
|
14
|
+
*
|
|
15
|
+
* YAG-383: the backend now serves `GET /api/yagni-code/spend?sessionId=`, a
|
|
16
|
+
* server-authoritative figure invisible to `turn_end` (subagents and advisor
|
|
17
|
+
* consults bill under this same session id, so a plain `sessionId=` query
|
|
18
|
+
* already sees them). `/go`'s children are the one exception: they bill under
|
|
19
|
+
* their OWN run id (`llm_usage.session_id` holds the run id, not the driver's
|
|
20
|
+
* session id, for a run-attributed dispatch), so the fetch also widens with
|
|
21
|
+
* `&runIds=` for every run this session has launched (see sessionRuns.ts and
|
|
22
|
+
* the goCommand.ts seam that records one as soon as it is known). `/cost`
|
|
23
|
+
* PREFERS that combined number. The local `turn_end` accumulator remains as
|
|
24
|
+
* the fail-soft fallback: it only ever sees the driver's own turns, so
|
|
25
|
+
* whenever it is shown it is explicitly labeled "(local, driver only)" rather
|
|
26
|
+
* than presented as the whole session's spend.
|
|
27
|
+
*/
|
|
28
|
+
/** A pure, session-scoped usage accumulator. */
|
|
29
|
+
export function makeCostAccumulator() {
|
|
30
|
+
let s = { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
31
|
+
return {
|
|
32
|
+
add(d) {
|
|
33
|
+
s = {
|
|
34
|
+
turns: s.turns + 1,
|
|
35
|
+
input: s.input + d.input,
|
|
36
|
+
output: s.output + d.output,
|
|
37
|
+
cacheRead: s.cacheRead + d.cacheRead,
|
|
38
|
+
cacheWrite: s.cacheWrite + d.cacheWrite,
|
|
39
|
+
cost: s.cost + d.cost,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
snapshot() {
|
|
43
|
+
return { ...s };
|
|
44
|
+
},
|
|
45
|
+
reset() {
|
|
46
|
+
s = { turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Extract a normalized {@link UsageDelta} from a turn_end message, defensively. */
|
|
51
|
+
export function usageFromMessage(message) {
|
|
52
|
+
const u = message?.usage;
|
|
53
|
+
if (!u)
|
|
54
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
55
|
+
const cost = typeof u.cost === "number" ? u.cost : (u.cost?.total ?? 0);
|
|
56
|
+
return {
|
|
57
|
+
input: u.input ?? 0,
|
|
58
|
+
output: u.output ?? 0,
|
|
59
|
+
cacheRead: u.cacheRead ?? 0,
|
|
60
|
+
cacheWrite: u.cacheWrite ?? 0,
|
|
61
|
+
cost,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
65
|
+
/**
|
|
66
|
+
* Render the /cost line. Pure, no em-dashes. `headroom` null means it was
|
|
67
|
+
* unavailable. `advisorLine` is the ask_advisor subtotal (empty when the session
|
|
68
|
+
* never escalated). `source`, when given, is parenthesized right after "Session
|
|
69
|
+
* usage", used to explicitly label this as the LOCAL, driver-only fallback when
|
|
70
|
+
* the server-authoritative spend fetch is unavailable (see
|
|
71
|
+
* {@link formatServerCostLines}, the preferred path).
|
|
72
|
+
*
|
|
73
|
+
* HONESTY NOTE: this counter can only see the DRIVER session, because it is fed
|
|
74
|
+
* by pi's `turn_end`. Work that runs in a child process — every /go stage, and
|
|
75
|
+
* every advisor consult — never emits a parent `turn_end`, so it is invisible
|
|
76
|
+
* here. The advisor subtotal is threaded in explicitly for exactly that reason.
|
|
77
|
+
*/
|
|
78
|
+
/** The boost-active line appended to /cost, both branches (see boostCommand.ts's docblock for why). */
|
|
79
|
+
const BOOST_LINE = "Boost is on. Driver turns bill at the peak tier.";
|
|
80
|
+
export function formatCostLine(snap, headroom, advisorLine, source, boosted = false) {
|
|
81
|
+
const turns = `${snap.turns} turn${snap.turns === 1 ? "" : "s"}`;
|
|
82
|
+
const cached = snap.cacheRead > 0 ? ` (${fmt(snap.cacheRead)} cached)` : "";
|
|
83
|
+
const label = source ? `Session usage (${source})` : "Session usage";
|
|
84
|
+
const base = `${label}: ${turns}, ${fmt(snap.input)} in / ${fmt(snap.output)} out tokens${cached}, ` +
|
|
85
|
+
`$${snap.cost.toFixed(2)} this session.`;
|
|
86
|
+
const advisor = advisorLine?.trim() ? ` ${advisorLine.trim()}` : "";
|
|
87
|
+
const boost = boosted ? ` ${BOOST_LINE}` : "";
|
|
88
|
+
if (headroom)
|
|
89
|
+
return `${base}${advisor}${boost} Credit headroom: ${headroom.remaining} ${headroom.unit}.`;
|
|
90
|
+
if (headroom === null)
|
|
91
|
+
return `${base}${advisor}${boost} Credit headroom unavailable right now.`;
|
|
92
|
+
return `${base}${advisor}${boost}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Millicents (1/1000 of a cent) -> dollars, 2 decimals. Guards against a
|
|
96
|
+
* malformed response field (NaN/Infinity) rendering as literal "NaN"/"Infinity"
|
|
97
|
+
* in a dollar amount: render $0.00 instead, since a network response is
|
|
98
|
+
* untyped at runtime and every other guard in this module is best-effort.
|
|
99
|
+
*
|
|
100
|
+
* Exported so `/go`'s per-stage cost table (runCostTable.ts, Task 8) reuses
|
|
101
|
+
* this exact money formatting instead of duplicating it.
|
|
102
|
+
*/
|
|
103
|
+
export const usd = (millicents) => {
|
|
104
|
+
if (!Number.isFinite(millicents))
|
|
105
|
+
return "0.00";
|
|
106
|
+
return (millicents / 100_000).toFixed(2);
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Render the server-authoritative /cost lines: PREFERRED over
|
|
110
|
+
* {@link formatCostLine}'s local accumulator whenever the spend fetch succeeds
|
|
111
|
+
* (see `registerCostCommand`'s empty-response guard for the one exception),
|
|
112
|
+
* because it covers children the local accumulator cannot see (subagents,
|
|
113
|
+
* advisor consults, and every /go run this session launched, widened in via
|
|
114
|
+
* `runIds` before the fetch even runs). Pure, no em-dashes, multi-line
|
|
115
|
+
* (joined with "\n").
|
|
116
|
+
*
|
|
117
|
+
* Line order: total spend, one line per rate tier (aggregated across callers,
|
|
118
|
+
* sorted by spend descending), a boosted-spend subtotal per tier that has any
|
|
119
|
+
* boosted rows, a live-toggle boost line (see below), the savings line (only
|
|
120
|
+
* when a counterfactual total resolved), an incomplete-counterfactual note,
|
|
121
|
+
* an unbilled note, a dropped-run-ids note (carry-over from the /cost
|
|
122
|
+
* re-review — see sessionRuns.ts's `droppedSessionRuns`), then headroom.
|
|
123
|
+
*
|
|
124
|
+
* `droppedRunCount` defaults to 0 (no note) so every existing direct caller
|
|
125
|
+
* of this pure function keeps behaving identically without passing it.
|
|
126
|
+
*
|
|
127
|
+
* `boosted` (default false) is the LIVE client-side toggle from
|
|
128
|
+
* `boostCommand.ts`'s `isBoosted()`, threaded in exactly like `advisorLine`
|
|
129
|
+
* is for {@link formatCostLine}. It is independent of the per-tier "Boosted
|
|
130
|
+
* (tier) spend" subtotal above: that subtotal only reflects server rows
|
|
131
|
+
* carrying `x-yagni-boost`, which only `/go` children (and other spawned
|
|
132
|
+
* processes) ever send — the driver's own turns never do (see
|
|
133
|
+
* boostCommand.ts's KNOWN-asymmetry docblock). Without this flag, a session
|
|
134
|
+
* that only chats while boosted would show no boost line at all.
|
|
135
|
+
*/
|
|
136
|
+
export function formatServerCostLines(spend, headroom, droppedRunCount = 0, boosted = false) {
|
|
137
|
+
const lines = [`Session spend: $${usd(spend.totalSellMillicents)} (server).`];
|
|
138
|
+
const tierTotals = new Map();
|
|
139
|
+
for (const row of spend.rows) {
|
|
140
|
+
const agg = tierTotals.get(row.rateTier) ?? { sellMillicents: 0, dispatches: 0 };
|
|
141
|
+
agg.sellMillicents += row.sellMillicents;
|
|
142
|
+
agg.dispatches += row.dispatches;
|
|
143
|
+
tierTotals.set(row.rateTier, agg);
|
|
144
|
+
}
|
|
145
|
+
const tiersSorted = [...tierTotals.entries()].sort((a, b) => b[1].sellMillicents - a[1].sellMillicents);
|
|
146
|
+
for (const [tier, agg] of tiersSorted) {
|
|
147
|
+
const calls = `${fmt(agg.dispatches)} call${agg.dispatches === 1 ? "" : "s"}`;
|
|
148
|
+
lines.push(` ${tier}: $${usd(agg.sellMillicents)} over ${calls}.`);
|
|
149
|
+
}
|
|
150
|
+
const boostTotals = new Map();
|
|
151
|
+
for (const row of spend.rows) {
|
|
152
|
+
if (!row.boost)
|
|
153
|
+
continue;
|
|
154
|
+
boostTotals.set(row.rateTier, (boostTotals.get(row.rateTier) ?? 0) + row.sellMillicents);
|
|
155
|
+
}
|
|
156
|
+
if (boostTotals.size > 0) {
|
|
157
|
+
for (const [tier] of tiersSorted) {
|
|
158
|
+
const tierBoosted = boostTotals.get(tier);
|
|
159
|
+
if (tierBoosted === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
lines.push(` Boosted (${tier}) spend: $${usd(tierBoosted)}.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (boosted) {
|
|
165
|
+
lines.push(BOOST_LINE);
|
|
166
|
+
}
|
|
167
|
+
// typeof guard (not just !== null): a network response is untyped at
|
|
168
|
+
// runtime, and a stray string/boolean here must never sneak "NN%" into the
|
|
169
|
+
// rendered line.
|
|
170
|
+
if (typeof spend.savingsPercent === "number") {
|
|
171
|
+
lines.push(`Saved $${usd(spend.savingsMillicents)} (${spend.savingsPercent}%) vs frontier list prices.`);
|
|
172
|
+
}
|
|
173
|
+
if (spend.counterfactualIncomplete) {
|
|
174
|
+
lines.push("Savings shown for priced tiers only.");
|
|
175
|
+
}
|
|
176
|
+
if (spend.unbilledSellMillicents > 0) {
|
|
177
|
+
// Matches YagniCodeUsageView.tsx's wording for the same figure.
|
|
178
|
+
lines.push(`Includes $${usd(spend.unbilledSellMillicents)} not yet billed (insufficient credits or a billing error).`);
|
|
179
|
+
}
|
|
180
|
+
if (droppedRunCount > 0) {
|
|
181
|
+
lines.push(`Excludes ${fmt(droppedRunCount)} earlier /go run${droppedRunCount === 1 ? "" : "s"}.`);
|
|
182
|
+
}
|
|
183
|
+
if (headroom) {
|
|
184
|
+
lines.push(`Credit headroom: ${headroom.remaining} ${headroom.unit}.`);
|
|
185
|
+
}
|
|
186
|
+
else if (headroom === null) {
|
|
187
|
+
lines.push("Credit headroom unavailable right now.");
|
|
188
|
+
}
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|
|
191
|
+
/** A server/local total pair diverges enough to be worth a quiet debug note. */
|
|
192
|
+
const DIVERGENCE_THRESHOLD = 0.1; // 10%, relative to the larger of the two
|
|
193
|
+
/**
|
|
194
|
+
* Wire the turn_end accumulator + the /cost command. /cost prefers the
|
|
195
|
+
* server-authoritative spend (fetched fresh on every call); a failing, absent,
|
|
196
|
+
* or null-resolving fetch falls back to the local session accumulator, labeled
|
|
197
|
+
* "(local, driver only)" so it is never mistaken for the whole session's spend.
|
|
198
|
+
* `/cost reset` zeroes only the local counter. The server figure is
|
|
199
|
+
* unaffected, and the reset notice says so.
|
|
200
|
+
*/
|
|
201
|
+
export function registerCostCommand(pi, deps = {}) {
|
|
202
|
+
const acc = makeCostAccumulator();
|
|
203
|
+
const fetchHeadroom = deps.fetchHeadroom;
|
|
204
|
+
const fetchSpend = deps.fetchSpend;
|
|
205
|
+
pi.on("turn_end", (event) => {
|
|
206
|
+
try {
|
|
207
|
+
acc.add(usageFromMessage(event.message));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* usage accounting must never disrupt a turn */
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
pi.registerCommand("cost", {
|
|
214
|
+
description: "Show this session's token + cost usage, and remaining credit headroom. /cost reset to zero it.",
|
|
215
|
+
handler: async (args, ctx) => {
|
|
216
|
+
if (args.trim().toLowerCase() === "reset") {
|
|
217
|
+
acc.reset();
|
|
218
|
+
if (ctx.hasUI) {
|
|
219
|
+
ctx.ui.notify("Session usage counter reset. Server totals are unaffected.", "info");
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Run headroom + spend concurrently: two SEQUENTIAL fail-soft fetches
|
|
224
|
+
// would each burn their own timeout on a slow/unreachable backend
|
|
225
|
+
// (worst case double the silence before /cost says anything at all).
|
|
226
|
+
// Promise.all bounds the wait to whichever is slower, not their sum.
|
|
227
|
+
const [headroom, spend] = await Promise.all([
|
|
228
|
+
fetchHeadroom
|
|
229
|
+
? fetchHeadroom(ctx.signal).catch(() => null) // fail-soft: still report session usage
|
|
230
|
+
: Promise.resolve(undefined),
|
|
231
|
+
fetchSpend
|
|
232
|
+
? fetchSpend(ctx.signal).catch(() => null) // fail-soft: fall back to the local accumulator
|
|
233
|
+
: Promise.resolve(null),
|
|
234
|
+
]);
|
|
235
|
+
const localSnap = acc.snapshot();
|
|
236
|
+
// Read fresh on every /cost call, both branches (see RegisterCostDeps's
|
|
237
|
+
// isBoosted doc comment for why the local fallback needs it too).
|
|
238
|
+
let boosted = false;
|
|
239
|
+
try {
|
|
240
|
+
boosted = deps.isBoosted?.() ?? false;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
/* a diagnostic must never break /cost */
|
|
244
|
+
}
|
|
245
|
+
// Quiet divergence check: both totals must be available, and it never
|
|
246
|
+
// affects what the user sees. Scoped to `caller === "driver"` rows only
|
|
247
|
+
// (like-for-like with the local `turn_end` accumulator, which can only
|
|
248
|
+
// ever see the driver's own turns) — NOT spend.totalSellMillicents,
|
|
249
|
+
// which includes every /go run this session launched and would
|
|
250
|
+
// otherwise disagree with the local total by construction. See the
|
|
251
|
+
// onDivergence doc comment above for the full story.
|
|
252
|
+
if (spend) {
|
|
253
|
+
try {
|
|
254
|
+
const driverServerUsd = spend.rows.filter((r) => r.caller === "driver").reduce((sum, r) => sum + r.sellMillicents, 0) / 100_000;
|
|
255
|
+
const localUsd = localSnap.cost;
|
|
256
|
+
const larger = Math.max(driverServerUsd, localUsd);
|
|
257
|
+
if (larger > 0 && Math.abs(driverServerUsd - localUsd) / larger > DIVERGENCE_THRESHOLD) {
|
|
258
|
+
deps.onDivergence?.(driverServerUsd, localUsd);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
/* a diagnostic must never break /cost */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// An empty server response (no rows at all) alongside a NONZERO local
|
|
266
|
+
// accumulator is more likely billing lag (the row has not landed yet)
|
|
267
|
+
// than an honestly-zero session: showing "$0.00 (server)" over real,
|
|
268
|
+
// already-observed local spend would read as a regression, not a fix.
|
|
269
|
+
// Prefer the labeled local line in exactly this state.
|
|
270
|
+
const serverLooksStale = spend !== null && spend.rows.length === 0 && localSnap.turns > 0;
|
|
271
|
+
if (spend && !serverLooksStale) {
|
|
272
|
+
let dropped = 0;
|
|
273
|
+
try {
|
|
274
|
+
dropped = deps.droppedSessionRuns?.() ?? 0;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
/* a diagnostic must never break /cost */
|
|
278
|
+
}
|
|
279
|
+
await pi.sendUserMessage(formatServerCostLines(spend, headroom, dropped, boosted));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
let advisorLine = "";
|
|
283
|
+
try {
|
|
284
|
+
advisorLine = deps.advisorSubtotal?.() ?? "";
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
/* usage accounting must never break /cost */
|
|
288
|
+
}
|
|
289
|
+
await pi.sendUserMessage(formatCostLine(localSnap, headroom, advisorLine, "local, driver only", boosted));
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=costHud.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash reporting for the pi extension: a sanitized, fire-and-forget POST to
|
|
3
|
+
* the YAGNI backend's /api/yagni-code/crash. No third-party telemetry SDK —
|
|
4
|
+
* crash reports are the only thing that phones home, they contain no code and
|
|
5
|
+
* no prompts, and `YAGNI_DISABLE_CRASH_REPORTS=1` turns them off.
|
|
6
|
+
*
|
|
7
|
+
* Two report paths use this module:
|
|
8
|
+
* - `uncaughtExceptionMonitor` (installed by index.ts): the MONITOR hook is
|
|
9
|
+
* chosen deliberately — unlike an `uncaughtException` listener it never
|
|
10
|
+
* changes crash semantics in pi's process (which YAGNI does not own); it
|
|
11
|
+
* only observes the crash that is already happening. This is also the
|
|
12
|
+
* "tool error that indicates a crash, not a user error" discriminator:
|
|
13
|
+
* a user-visible tool failure is caught and rendered by pi, while
|
|
14
|
+
* anything reaching the monitor was about to kill the session.
|
|
15
|
+
* - the /go pipeline's terminal catch (goCommand.ts), where a thrown error
|
|
16
|
+
* — not a verdict-shaped failure — ends the run.
|
|
17
|
+
*
|
|
18
|
+
* Everything is fail-soft: never throws, never retries, single short-timeout
|
|
19
|
+
* attempt, and reporting can never affect the user's session.
|
|
20
|
+
*
|
|
21
|
+
* The sanitizer (env-value redaction + path collapse + caps around the shared
|
|
22
|
+
* scrubSecrets patterns) is kept in sync with
|
|
23
|
+
* `yagni-code-cli/src/crashReport.ts`; the backend re-runs its own copy in
|
|
24
|
+
* `backend/src/yagniCode/crashReports.ts`. Spec:
|
|
25
|
+
* docs/superpowers/specs/2026-08-08-crash-reporting-design.md
|
|
26
|
+
*/
|
|
27
|
+
export declare const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
|
|
28
|
+
export declare const CRASH_REPORT_TIMEOUT_MS = 1500;
|
|
29
|
+
export declare const MAX_CRASH_MESSAGE = 512;
|
|
30
|
+
export declare const MAX_CRASH_ERROR_CLASS = 128;
|
|
31
|
+
export declare const MAX_CRASH_STACK = 8000;
|
|
32
|
+
export declare const MAX_CRASH_STACK_FRAMES = 40;
|
|
33
|
+
export declare const MAX_CRASH_PAYLOAD_BYTES = 16384;
|
|
34
|
+
/** Same truthiness rule as the CLI's YAGNI_DISABLE_* family. */
|
|
35
|
+
export declare function crashReportsDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
36
|
+
export interface SanitizeCrashOptions {
|
|
37
|
+
env?: NodeJS.ProcessEnv;
|
|
38
|
+
repoRoot?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Env values → [ENV:NAME], scrubSecrets patterns, ~-collapse, repo-relative, basename. */
|
|
41
|
+
export declare function sanitizeCrashText(text: string, opts?: SanitizeCrashOptions): string;
|
|
42
|
+
export interface SanitizedCrash {
|
|
43
|
+
errorClass: string;
|
|
44
|
+
message: string;
|
|
45
|
+
stack?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Reads ONLY name/message/stack; custom error fields are never touched. */
|
|
48
|
+
export declare function sanitizeCrashError(err: unknown, opts?: SanitizeCrashOptions): SanitizedCrash;
|
|
49
|
+
export interface CrashReporterOpts {
|
|
50
|
+
baseUrl: string;
|
|
51
|
+
getToken: () => string | undefined;
|
|
52
|
+
fetchImpl?: typeof fetch;
|
|
53
|
+
env?: NodeJS.ProcessEnv;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
}
|
|
56
|
+
export type CrashReporter = (error: unknown, context?: string, repoRoot?: string) => Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Build the fail-soft reporter. The extension runs inside pi's process, so
|
|
59
|
+
* the client label follows the surface: `desktop` under the desktop shell
|
|
60
|
+
* (YAGNI_SURFACE=desktop), `cli` in the terminal. Version comes from
|
|
61
|
+
* `YAGNI_CODE_VERSION` (forwarded by the launcher; the bundled extension has
|
|
62
|
+
* no package.json on disk). Resolves on every outcome; never throws.
|
|
63
|
+
*/
|
|
64
|
+
export declare function makeCrashReporter(opts: CrashReporterOpts): CrashReporter;
|
|
65
|
+
export type SpawnLike = (command: string, args: string[], options: {
|
|
66
|
+
detached: boolean;
|
|
67
|
+
stdio: "ignore";
|
|
68
|
+
env: Record<string, string | undefined>;
|
|
69
|
+
}) => {
|
|
70
|
+
unref: () => void;
|
|
71
|
+
};
|
|
72
|
+
export interface FatalCrashOpts extends CrashReporterOpts {
|
|
73
|
+
/** Spawn seam for tests (defaults to node:child_process spawn). */
|
|
74
|
+
spawnImpl?: SpawnLike;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Deliver a crash report from a process that is about to die: build the
|
|
78
|
+
* sanitized payload in-process (cheap, synchronous), then spawn a detached
|
|
79
|
+
* one-shot sender that outlives the crash. Never throws.
|
|
80
|
+
*/
|
|
81
|
+
export declare function reportFatalCrash(error: unknown, opts: FatalCrashOpts, context?: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Observe (never alter) a fatal crash in pi's process. Uses
|
|
84
|
+
* `uncaughtExceptionMonitor`, which fires before the process dies without
|
|
85
|
+
* installing a handler that would change node's default crash behavior —
|
|
86
|
+
* YAGNI does not own pi's process, so crash semantics must stay untouched.
|
|
87
|
+
*/
|
|
88
|
+
export declare function installUncaughtExceptionMonitor(opts: FatalCrashOpts, proc?: Pick<NodeJS.Process, "on">): void;
|
|
89
|
+
//# sourceMappingURL=crashReport.d.ts.map
|