Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
435 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'worktree-agent-a1b995daa94e4e1b7'1/**
2 * How the billing pages speak of money, read their forms and word a
3 * workspace's alerts. Pure, so it can be tested.
4 */
5
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.6import type { CreditGrant, Entitlements, FeatureState, Limit, LimitRequest, MeterUsage, PriceBook, Usage, UsageAlert } from "@g1t/contracts";
Merge branch 'worktree-agent-a1b995daa94e4e1b7'7
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.8import { MICROS_PER_DOLLAR, money, wholeDollars } from "./money.ts";
Merge branch 'worktree-agent-a1b995daa94e4e1b7'9
Merge Stripe Tax, the card fee on card payments, and one free workspace per person10/** Prices on g1t exclude tax; Stripe adds it at checkout from the billing address. */
11export const PLUS_TAX = "plus tax where it applies";
12
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.13/** A card processing fee: Stripe's percent (in millionths) and fixed fee. */
14export type CardFee = { on: boolean; percentMicros: number; fixedCents: number };
15
16/**
17 * The card fee as the price book lists it (`card_fee_percent` and
18 * `card_fee_fixed`), or null when it does not.
19 */
20export function cardFeeOf(book: PriceBook | null | undefined): CardFee | null {
21 const percent = book?.prices.find((price) => price.meter === "card_fee_percent");
22 const fixed = book?.prices.find((price) => price.meter === "card_fee_fixed");
23 if (!percent || !fixed) return null;
24 return { on: true, percentMicros: percent.priceMicros, fixedCents: Math.max(0, Math.round(fixed.priceMicros / 10_000)) };
25}
26
Merge Stripe Tax, the card fee on card payments, and one free workspace per person27/**
28 * The card processing fee on a card payment of `cents`, as billing works it
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.29 * out (`tax::card_fee_cents`): Stripe's percent and fixed fee grossed up, so
Merge Stripe Tax, the card fee on card payments, and one free workspace per person30 * what is left after Stripe's fee is the amount, rounded up to the cent.
31 * 0 when the fee is off.
32 */
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.33export function cardFeeCents(cents: number, fee: CardFee | null | undefined): number {
Merge Stripe Tax, the card fee on card payments, and one free workspace per person34 if (!fee?.on || !(cents > 0)) return 0;
35 const rate = fee.percentMicros / MICROS_PER_DOLLAR;
36 if (!(rate >= 0 && rate < 0.5)) return 0;
37 return Math.max(0, Math.ceil((cents + fee.fixedCents) / (1 - rate)) - cents);
38}
39
40/** "Card processing fee $1.06, plus tax where it applies", as shown before paying. */
41export function feeAndTax(feeCents: number): string {
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.42 return feeCents > 0 ? `Card processing fee ${money(feeCents * 10_000)}, ${PLUS_TAX}` : `Plus tax where it applies`;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person43}
44
Merge branch 'worktree-agent-a1b995daa94e4e1b7'45/** A form's dollar amount as micros, or null when it is empty or not a number. */
46export function readDollars(value: FormDataEntryValue | null | undefined): number | null {
47 const text = String(value ?? "").replace(/[$,\s]/g, "");
48 if (!text) return null;
49 const amount = Number(text);
50 if (!Number.isFinite(amount)) return null;
51 return Math.round(amount * MICROS_PER_DOLLAR);
52}
53
54export type Parsed<T> = { ok: true; value: T } | { ok: false; error: string };
55
56/** The smallest prepayment, the most by card, and where bank transfer starts, in dollars. */
57export const PREPAY = { min: 25, maxCard: 10_000, bankFrom: 1_000, presets: [100, 500, 1_000] } as const;
58
59/** A prepayment: a preset or a custom amount, by card or bank transfer. */
60export function parsePrepay(form: {
61 amount?: FormDataEntryValue | null;
62 custom?: FormDataEntryValue | null;
63 method?: FormDataEntryValue | null;
64}): Parsed<{ amountCents: number; method: "card" | "bank_transfer" }> {
65 const micros = readDollars(form.custom) ?? readDollars(form.amount);
66 const method = String(form.method ?? "card") === "bank_transfer" ? "bank_transfer" : "card";
67 if (micros == null) return { ok: false, error: "Choose an amount to prepay." };
68 const amount = micros / MICROS_PER_DOLLAR;
69 if (amount < PREPAY.min) return { ok: false, error: `The smallest prepayment is $${PREPAY.min}.` };
70 if (method === "card" && amount > PREPAY.maxCard)
71 return { ok: false, error: `By card, the most is $${PREPAY.maxCard.toLocaleString("en-US")}; pay more by bank transfer.` };
72 if (method === "bank_transfer" && amount < PREPAY.bankFrom)
73 return { ok: false, error: `Bank transfer is for $${PREPAY.bankFrom.toLocaleString("en-US")} or more.` };
74 return { ok: true, value: { amountCents: Math.round(amount * 100), method } };
75}
76
77/** What owners may set each cap to, in micros, and what applies when they set none. */
78export const CAPS = {
79 run: { min: 100_000, max: 100_000_000, default: 2_000_000 },
80 issue: { min: 1_000_000, max: 1_000_000_000, default: 10_000_000 },
81} as const;
82
83/** The owners' caps: empty goes back to the default. */
84export function parseCaps(form: {
85 run?: FormDataEntryValue | null;
86 issue?: FormDataEntryValue | null;
87}): Parsed<{ runCapMicros: number | null; issueCapMicros: number | null }> {
88 const run = readDollars(form.run);
89 const issue = readDollars(form.issue);
90 if (String(form.run ?? "").trim() && run == null) return { ok: false, error: "A run's cap is a dollar amount." };
91 if (String(form.issue ?? "").trim() && issue == null) return { ok: false, error: "An issue's cap is a dollar amount." };
92 if (run != null && (run < CAPS.run.min || run > CAPS.run.max))
93 return { ok: false, error: `A run's cap is between ${wholeDollars(CAPS.run.min)} and ${wholeDollars(CAPS.run.max)}.` };
94 if (issue != null && (issue < CAPS.issue.min || issue > CAPS.issue.max))
95 return {
96 ok: false,
97 error: `An issue's cap is between ${wholeDollars(CAPS.issue.min)} and ${wholeDollars(CAPS.issue.max)}.`,
98 };
99 return { ok: true, value: { runCapMicros: run, issueCapMicros: issue } };
100}
101
102/** A request for a higher limit, or for help with usage past what was meant. */
103export function parseLimitRequest(form: {
104 kind?: FormDataEntryValue | null;
105 amount?: FormDataEntryValue | null;
106 reason?: FormDataEntryValue | null;
107 expected?: FormDataEntryValue | null;
108}): Parsed<{ kind: "limit" | "overage"; amountMicros: number; reason: string; expectedMonthlyMicros: number }> {
109 const kind = String(form.kind ?? "limit") === "overage" ? "overage" : "limit";
110 const reason = String(form.reason ?? "").trim();
111 const amount = readDollars(form.amount);
112 const expected = readDollars(form.expected);
113 if (!reason)
114 return {
115 ok: false,
116 error: kind === "limit" ? "Say what the higher limit is for." : "Say what happened, so we can look at it.",
117 };
118 if (kind === "limit") {
119 if (amount == null || amount <= 0) return { ok: false, error: "Say the limit you need, in dollars." };
120 if (expected == null || expected < 0) return { ok: false, error: "Say what you expect to spend in a month." };
121 }
122 return {
123 ok: true,
124 value: {
125 kind,
126 amountMicros: Math.max(0, amount ?? 0),
127 reason: reason.slice(0, 2000),
128 expectedMonthlyMicros: Math.max(0, expected ?? 0),
129 },
130 };
131}
132
133/**
134 * A spend limit as owners set it: automatic, fixed at an amount, as high as
135 * is available, or the one-time raise (an amount, or blank for as high as
136 * it goes, which the page fills in from the limit).
137 */
138export function parseSpendLimit(form: {
139 mode?: FormDataEntryValue | null;
140 limit?: FormDataEntryValue | null;
141}): Parsed<{ micros: number | null; useFull: boolean; raiseOnce: boolean }> {
142 const mode = String(form.mode ?? "automatic");
143 if (mode === "full") return { ok: true, value: { micros: null, useFull: true, raiseOnce: false } };
144 if (mode !== "fixed" && mode !== "raise") return { ok: true, value: { micros: null, useFull: false, raiseOnce: false } };
145 const micros = readDollars(form.limit);
146 if (mode === "raise" && micros == null) return { ok: true, value: { micros: null, useFull: false, raiseOnce: true } };
147 if (micros == null || micros < MICROS_PER_DOLLAR) return { ok: false, error: "A spend limit is a dollar amount, $1 or more." };
148 return { ok: true, value: { micros, useFull: false, raiseOnce: mode === "raise" } };
149}
150
151/**
152 * How far owners may set their spend limit themselves: up to the highest
153 * ceiling the workspace has had plus what is prepaid, and, once, up to
154 * twice that highest ceiling. Null `selfServeMicros` means no ceiling
155 * (g1t's own, or staff set it).
156 */
157export type SpendRange = { selfServeMicros: number | null; raiseOnceMicros: number | null; raisedAt: string | null };
158
159export function spendRange(
160 limit: Pick<Limit, "availableMicros" | "ceilingMicros" | "raiseOnceMicros" | "raisedAt">,
161): SpendRange {
162 const selfServe = limit.availableMicros !== undefined ? limit.availableMicros : limit.ceilingMicros;
163 const once = limit.raiseOnceMicros ?? null;
164 return {
165 selfServeMicros: selfServe ?? null,
166 // The raise only matters when it goes further than owners can already.
167 raiseOnceMicros: once != null && (selfServe == null || once > selfServe) ? once : null,
168 raisedAt: limit.raisedAt ?? null,
169 };
170}
171
172/**
173 * What setting the spend limit to `micros` takes, as billing decides it:
174 * nothing (`self`), the one-time raise (`raise`), or a request to g1t
175 * (`ask`).
176 */
177export function spendPath(micros: number, range: SpendRange): "self" | "raise" | "ask" {
178 if (range.selfServeMicros == null || micros <= range.selfServeMicros) return "self";
179 if (range.raiseOnceMicros != null && micros <= range.raiseOnceMicros) return "raise";
180 return "ask";
181}
182
183/** Where the workspace stands with the g1t plan, for the plan card. */
184export type PlanStatus = {
185 kind: "free" | "trial" | "paid" | "canceling" | "past_due" | "comped" | "enterprise";
186 label: string;
187};
188
189export function planStatus(
190 plan: Pick<FeatureState, "on" | "included" | "subscription"> | null | undefined,
191 entitlements: Pick<Entitlements, "plan" | "trialMicrosLeft" | "trialVerified" | "firstMonth"> | null | undefined,
192): PlanStatus {
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging193 if (entitlements?.plan === "internal") return { kind: "comped", label: "100% discount from g1t" };
Merge branch 'worktree-agent-a1b995daa94e4e1b7'194 if (entitlements?.plan === "enterprise") return { kind: "enterprise", label: "Paid by an enterprise" };
195 if (plan?.included) return { kind: "comped", label: "Included by g1t" };
196 const subscription = plan?.subscription;
197 if (subscription?.status === "past_due") return { kind: "past_due", label: "Payment failed" };
198 if (plan?.on && subscription?.status === "canceling") return { kind: "canceling", label: "Ends at the end of the period" };
199 if ((plan?.on && subscription) || entitlements?.plan === "paid") {
200 return { kind: "paid", label: entitlements?.firstMonth ? "On the g1t plan, first month" : "On the g1t plan" };
201 }
202 if (entitlements?.trialVerified && entitlements.trialMicrosLeft > 0) return { kind: "trial", label: "Free, on the trial" };
203 return { kind: "free", label: "Free" };
204}
205
206/** Meters only the plan runs: a free workspace never builds, serves apps or adds custom domains. */
207const PLAN_ONLY_METERS = new Set(["builds", "requests", "domains"]);
208
209/**
210 * The lines of "This month's usage": every meter on the plan, so it reads
211 * the same each month; without it, only what a free workspace can use,
212 * plus anything that was used anyway (from before the plan ended).
213 */
214export function shownMeters(meters: MeterUsage[], onPlan: boolean): MeterUsage[] {
215 return meters.filter((meter) => onPlan || !PLAN_ONLY_METERS.has(meter.key) || meter.micros > 0);
216}
217
218/** `1 GB`, `500 MB`: storage as it is priced, in powers of ten. */
219export function gigabytes(bytes: number): string {
220 if (bytes >= 1e9) return `${Math.round((bytes / 1e9) * 10) / 10} GB`;
221 return `${Math.round(bytes / 1e6)} MB`;
222}
223
224/** A share from 0 to 1 of `used` against `of`, for a meter. */
225export function share(used: number, of: number | null | undefined): number {
226 if (!of || of <= 0) return 0;
227 return Math.min(1, Math.max(0, used / of));
228}
229
230/** Where the trial stands after a card check, in a sentence for the owner. */
231export function cardCheckResult(
232 entitlements: Pick<Entitlements, "trialVerified" | "trialMicrosLeft">,
233 trialMicros: number,
234): string {
235 if (!entitlements.trialVerified) return "The card check did not finish. Try again; the card is never charged.";
236 if (entitlements.trialMicrosLeft > 0) return `Card checked. Your ${wholeDollars(entitlements.trialMicrosLeft)} trial is ready to use.`;
237 return `Card checked, but no trial started. The ${wholeDollars(trialMicros)} trial needs a credit or debit card that has not started one before, and comes from a monthly pool that can run out. Prepaid cards can still pay for the plan.`;
238}
239
240const METERS: Record<string, string> = {
241 included: "the plan's included usage",
242 spend_limit: "your spend limit",
243 ceiling: "what g1t lets go unpaid",
244};
245
246/** One alert as a sentence: "90% of the plan's included usage: $9.00 of $10.00." */
247export function alertText(alert: UsageAlert): string {
248 const what = METERS[alert.meter] ?? alert.meter.replace(/_/g, " ");
249 const reached = alert.level >= 100 ? `All of ${what}` : `${alert.level}% of ${what}`;
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.250 return `${reached}: ${money(alert.usedMicros)} of ${money(alert.limitMicros)}.`;
Merge branch 'worktree-agent-a1b995daa94e4e1b7'251}
252
253/** How loud an alert is. */
254export function alertTone(level: number): "ok" | "warning" | "stopped" {
255 if (level >= 100) return "stopped";
256 return level >= 75 ? "warning" : "ok";
257}
258
259/** Whether the workspace needs an owner's eye now: compute paused, a spike waiting, or an alert at 90% or more. */
260export function needsAttention(entitlements: Pick<Entitlements, "paused" | "spike" | "alerts"> | null | undefined): boolean {
261 if (!entitlements) return false;
262 if (entitlements.paused) return true;
263 if (entitlements.spike && entitlements.spike.status !== "continued") return true;
264 return (entitlements.alerts ?? []).some((alert) => alert.level >= 90);
265}
266
267/** A request's state as the owner sees it. */
268export function requestStatus(request: LimitRequest): string {
269 if (request.status === "approved")
270 return request.decidedMicros != null ? `Approved at ${wholeDollars(request.decidedMicros)}` : "Approved";
271 if (request.status === "declined") return "Declined";
272 return "Waiting for an answer";
273}
274
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.275/** What each kind of work is called, and its colour, on Usage and the workspace's overview; the agent kinds are in older months. */
Merge branch 'worktree-agent-a1b995daa94e4e1b7'276export const USAGE_TASKS: Record<string, { label: string; color: string }> = {
277 implement: { label: "Making changes", color: "var(--color-merged)" },
278 review: { label: "Reviews", color: "var(--color-info)" },
279 revise: { label: "Revisions", color: "var(--color-warn)" },
Merge branch 'worktree-agent-ad7c6d88d93adc817'280 update: { label: "Catching up", color: "var(--color-success)" },
Merge branch 'worktree-agent-a1b995daa94e4e1b7'281 plan: { label: "Planning", color: "#f0a6ca" },
282 /** Time on the workspace's own runners: its minutes, at $0. */
283 self_hosted: { label: "Self-hosted runners ($0)", color: "var(--color-line-strong)" },
284 other: { label: "Other", color: "var(--color-faint)" },
285};
286
287/** A kind of work's words and colour, or Other's. */
288export function usageTask(key: string): { label: string; color: string } {
289 return USAGE_TASKS[key] ?? USAGE_TASKS.other!;
290}
291
292/**
293 * Usage by kind of work with every kind that has no words of its own
294 * added into one Other, last, so Other is listed once.
295 */
296export function foldTasks<T extends { key: string; micros: number; runs: number }>(slices: T[]): T[] {
297 const known = slices.filter((slice) => slice.key !== "other" && slice.key in USAGE_TASKS);
298 const rest = slices.filter((slice) => !known.includes(slice));
299 if (rest.length === 0) return known;
300 const other = rest.reduce(
301 (sum, slice) => ({ ...sum, micros: sum.micros + slice.micros, runs: sum.runs + slice.runs }),
302 { ...rest[0]!, key: "other", micros: 0, runs: 0 },
303 );
304 return [...known, other];
305}
306
307/** The workspace's month at a glance, for the Usage card on its overview. */
308export type UsageGlance = {
309 /**
310 * `beta` while g1t charges nothing; `comped` when g1t or an enterprise
311 * pays; `plan` on the g1t plan; `trial` on trial credit; `forge` with
312 * neither, where only what runs no compute is open.
313 */
314 kind: "beta" | "comped" | "plan" | "trial" | "forge";
315 /** Charged this month, or used at cost while g1t is free. */
316 spentMicros: number;
317 /** What pays first, and how much of it is used: the plan's included usage, or the trial. */
318 credit: { label: string; usedMicros: number; ofMicros: number } | null;
319 /** Charged past what is included, and the spend limit if there is one; null when it does not apply. */
320 onDemand: { micros: number; limitMicros: number | null } | null;
321 /** What it went on, most first. */
322 lines: { key: string; label: string; micros: number; runs: number }[];
323};
324
325export function usageGlance(input: {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit326 usage: Pick<Usage, "free" | "spentMicros" | "usedMicros" | "byTask" | "priceMicros">;
Merge branch 'worktree-agent-a1b995daa94e4e1b7'327 status: PlanStatus;
328 entitlements: Pick<Entitlements, "includedMicros" | "includedUsedMicros" | "trialMicrosLeft"> | null;
329 limit: Pick<Limit, "spentMicros" | "spendLimitMicros"> | null;
330 trialMicros: number;
331}): UsageGlance {
332 const { usage, status, entitlements, limit, trialMicros } = input;
333 const lines = [...usage.byTask]
334 .filter((slice) => slice.micros > 0)
335 .sort((a, b) => b.micros - a.micros)
336 .map((slice) => ({ key: slice.key, label: usageTask(slice.key).label, micros: slice.micros, runs: slice.runs }));
Usage, Billing settings and prepaid AI credit; fixes from the UX audit337 // Usage at price, the one figure every page shows.
338 const spentMicros = usage.free ? usage.usedMicros : (usage.priceMicros ?? usage.spentMicros);
Merge branch 'worktree-agent-a1b995daa94e4e1b7'339 const plain = { spentMicros, credit: null, onDemand: null, lines };
340 if (usage.free) return { kind: "beta", ...plain };
341 if (status.kind === "comped" || status.kind === "enterprise") return { kind: "comped", ...plain };
342 if (status.kind === "trial") {
343 const left = Math.max(0, entitlements?.trialMicrosLeft ?? 0);
344 const of = Math.max(trialMicros, left);
345 return { ...plain, kind: "trial", credit: { label: "Trial credit", usedMicros: of - left, ofMicros: of } };
346 }
347 if (status.kind === "free") return { kind: "forge", ...plain };
348 const included = entitlements?.includedMicros ?? 0;
349 const includedUsed = Math.min(entitlements?.includedUsedMicros ?? 0, included);
350 return {
351 ...plain,
352 kind: "plan",
353 credit: included > 0 ? { label: "Included usage", usedMicros: includedUsed, ofMicros: included } : null,
354 onDemand: {
355 micros: limit?.spentMicros ?? Math.max(0, spentMicros - includedUsed),
356 limitMicros: limit?.spendLimitMicros ?? null,
357 },
358 };
359}
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging360
361/** What a credit from g1t is for, as the Billing page names it. */
362export const CREDIT_KIND: Record<CreditGrant["kind"], string> = {
363 promotional: "Promotional",
364 goodwill: "Goodwill",
365 refund: "Refund",
366 purchased: "Purchased",
367};
368
369/** `Jan 5`, or `Jan 5, 2028` outside this year (UTC). */
370export function shortDay(at: string, now = new Date()): string {
371 const date = new Date(at);
372 const sameYear = date.getUTCFullYear() === now.getUTCFullYear();
373 return date.toLocaleDateString("en-US", { month: "short", day: "numeric", ...(sameYear ? {} : { year: "numeric" }), timeZone: "UTC" });
374}
375
376/**
377 * A credit from g1t in a line: `$25.00 credit, $12.40 left, expires Jan 5`;
378 * once it is spent, expired or withdrawn, says so.
379 */
380export function creditLine(grant: Pick<CreditGrant, "amountMicros" | "leftMicros" | "expiresAt" | "state">, now = new Date()): string {
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.381 const parts = [`${money(grant.amountMicros)} credit`];
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging382 if (grant.state === "open") {
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.383 parts.push(`${money(grant.leftMicros)} left`);
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging384 if (grant.expiresAt) parts.push(`expires ${shortDay(grant.expiresAt, now)}`);
385 } else {
386 parts.push(grant.state === "used" ? "all used" : grant.state === "expired" ? "expired" : "withdrawn");
387 }
388 return parts.join(", ");
Usage, Billing settings and prepaid AI credit; fixes from the UX audit389}
390
391/** The budget's alerts form: the levels ticked, whether usage pauses, and a webhook. */
392export function parseBudgetAlerts(form: {
393 alerts: FormDataEntryValue[];
394 pause?: FormDataEntryValue | null;
395 webhook?: FormDataEntryValue | null;
396}): Parsed<{ alerts: number[]; pauseAtLimit: boolean; webhook: string | null }> {
397 const alerts = [...new Set(form.alerts.map((a) => Number(a)).filter((a) => [50, 75, 90, 100].includes(a)))].sort((a, b) => b - a);
398 const webhook = String(form.webhook ?? "").trim();
399 if (webhook && !/^https:\/\/[^\s/]+\.[^\s]+$/.test(webhook)) return { ok: false, error: "The webhook is an https:// address." };
400 return { ok: true, value: { alerts, pauseAtLimit: form.pause === "on", webhook: webhook || null } };
401}
402
403/** The invoice details form, each field as given (empty clears it on Stripe). */
404export function parseInvoiceDetails(form: FormData): Parsed<{
405 email: string;
406 name: string;
407 address: { line1: string; line2: string; city: string; state: string; postalCode: string; country: string };
408 taxIdType: string;
409 taxId: string;
410 poNumber: string;
411 language: string;
412}> {
413 const text = (name: string) => String(form.get(name) ?? "").trim();
414 const email = text("email");
415 if (email && !/^[^\s@]+@[^\s@]+$/.test(email)) return { ok: false, error: "That is not an email address." };
416 const country = text("country").toUpperCase();
417 if (country && !/^[A-Z]{2}$/.test(country)) return { ok: false, error: "The country is two letters, such as US or DE." };
Merge Stripe Tax, the card fee on card payments, and one free workspace per person418 // Stripe Tax places a US customer by ZIP code: without it, tax cannot be worked out.
419 if (country === "US" && !text("postalCode")) return { ok: false, error: "Add the ZIP code: in the US, tax is worked out from it." };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit420 const taxIdType = text("taxIdType");
421 const taxId = text("taxId");
422 if (Boolean(taxIdType) !== Boolean(taxId)) return { ok: false, error: "Give the tax ID's kind and its number together." };
423 return {
424 ok: true,
425 value: {
426 email,
427 name: text("name"),
428 address: { line1: text("line1"), line2: text("line2"), city: text("city"), state: text("state"), postalCode: text("postalCode"), country },
429 taxIdType,
430 taxId,
431 poNumber: text("poNumber"),
432 language: text("language"),
433 },
434 };
435}

This file's history is long; its oldest lines are credited to the oldest commit read.