@2kw/ai 6.3.0-dev.128 → 6.3.0-dev.131

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.
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { getClient, runAction } from "../lib/client.js";
3
3
  import { isJsonOutput } from "../lib/config.js";
4
+ import { CliUsageError } from "../lib/errors.js";
4
5
  import { formatDetail, formatList } from "../lib/output.js";
5
6
  import chalk from "chalk";
6
7
  export function makeBillingCommand() {
@@ -133,6 +134,107 @@ export function makeBillingCommand() {
133
134
  }
134
135
  });
135
136
  });
137
+ cmd.addCommand(makeOverageCommand());
138
+ cmd.addCommand(makeTopUpsCommand());
139
+ return cmd;
140
+ }
141
+ /** Whole euros from cents, e.g. 1234 → "€12.34". Amounts are net of VAT (#973 D-O). */
142
+ function euros(cents) {
143
+ return `€${((cents ?? 0) / 100).toFixed(2)}`;
144
+ }
145
+ function parseWhole(value, name, min, max) {
146
+ const n = /^\d+$/.test(value) ? Number(value) : NaN;
147
+ if (!Number.isInteger(n) || n < min || n > max) {
148
+ throw new CliUsageError(`${name} must be a whole number between ${min} and ${max}, not '${value}'`);
149
+ }
150
+ return n;
151
+ }
152
+ /** `bb billing overage get|set` — the opt-in overage cap (#973, plan D-O). Reads need a member login. */
153
+ function makeOverageCommand() {
154
+ const cmd = new Command("overage").description("Show or set the opt-in overage cap (euros, net of VAT)");
155
+ cmd
156
+ .command("get")
157
+ .description("Show the overage setting and this period's overage (needs `bb auth login`)")
158
+ .action(async (_opts, command) => {
159
+ await runAction(command, async () => {
160
+ const client = getClient(command);
161
+ const { data } = await client.GET("/v1/billing/overage");
162
+ if (isJsonOutput(command)) {
163
+ console.log(JSON.stringify(data, null, 2));
164
+ return;
165
+ }
166
+ console.log(`Overage: ${data?.enabled ? chalk.green("on") : chalk.dim("off")}`);
167
+ console.log(`Cap: €${data?.capEuros ?? 0} (max €${data?.maxCapEuros ?? 0})`);
168
+ console.log(`Available: ${data?.available ? chalk.green("yes") : chalk.yellow(`no (${data?.unavailableReason ?? "unknown"})`)}`);
169
+ console.log(`Spent: ${euros(data?.spentCents)}`);
170
+ console.log(`Reserved: ${euros(data?.reservedCents)}`);
171
+ if (data?.periodStart || data?.periodEnd) {
172
+ console.log(chalk.dim(`Period: ${data?.periodStart?.slice(0, 10) ?? "?"} → ${data?.periodEnd?.slice(0, 10) ?? "?"}`));
173
+ }
174
+ });
175
+ });
176
+ cmd
177
+ .command("set")
178
+ .description("Turn overage on with a cap, or off (admin)")
179
+ .option("--cap <euros>", "Monthly cap in whole euros, 1-100; turns overage on")
180
+ .option("--off", "Turn overage off, keeping the stored cap")
181
+ .action(async (opts, command) => {
182
+ await runAction(command, async () => {
183
+ if (opts.off === Boolean(opts.cap)) {
184
+ throw new CliUsageError("Pass exactly one of --cap <euros> or --off");
185
+ }
186
+ // --off sends no cap: the backend keeps the stored one, and no read is needed (the read
187
+ // refuses API keys, the write admits an admin's).
188
+ const body = opts.off
189
+ ? { enabled: false }
190
+ : { enabled: true, capEuros: parseWhole(String(opts.cap), "cap", 1, 100) };
191
+ const client = getClient(command);
192
+ const { data } = await client.PUT("/v1/billing/overage", { body });
193
+ formatDetail(data, command);
194
+ });
195
+ });
196
+ return cmd;
197
+ }
198
+ /** `bb billing top-ups list|buy` — prepaid credit packs (#973, plan D-E). Never opens a browser. */
199
+ function makeTopUpsCommand() {
200
+ const cmd = new Command("top-ups").description("List or buy prepaid credit (packs of 1,000 credits at €15)");
201
+ cmd
202
+ .command("list")
203
+ .description("List unexpired prepaid credit (needs `bb auth login`)")
204
+ .action(async (_opts, command) => {
205
+ await runAction(command, async () => {
206
+ const client = getClient(command);
207
+ const { data } = await client.GET("/v1/billing/top-ups");
208
+ if (isJsonOutput(command)) {
209
+ console.log(JSON.stringify(data, null, 2));
210
+ return;
211
+ }
212
+ const rows = (data ?? []).map((lot) => ({
213
+ kind: lot.kind,
214
+ purchased: euros(lot.purchasedCents),
215
+ remaining: euros(lot.remainingCents),
216
+ expires: lot.expiresAt?.slice(0, 10),
217
+ }));
218
+ formatList(rows, command, ["kind", "purchased", "remaining", "expires"]);
219
+ });
220
+ });
221
+ cmd
222
+ .command("buy")
223
+ .description("Start a Stripe Checkout for top-up packs and print its URL (admin)")
224
+ .requiredOption("--packs <n>", "Number of packs, 1-20")
225
+ .action(async (opts, command) => {
226
+ await runAction(command, async () => {
227
+ const packs = parseWhole(String(opts.packs), "packs", 1, 20);
228
+ const client = getClient(command);
229
+ const { data } = await client.POST("/v1/billing/top-ups", { body: { packs } });
230
+ if (isJsonOutput(command)) {
231
+ console.log(JSON.stringify(data, null, 2));
232
+ return;
233
+ }
234
+ console.log(`Open this page to pay: ${data?.checkoutUrl}`);
235
+ console.log(chalk.dim(`Expires: ${data?.expiresAt ?? "?"}`));
236
+ });
237
+ });
136
238
  return cmd;
137
239
  }
138
240
  //# sourceMappingURL=billing.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2kw/ai",
3
- "version": "6.3.0-dev.128",
3
+ "version": "6.3.0-dev.131",
4
4
  "description": "CLI for 2kw.ai — schema-driven document extraction, an OpenAI-compatible EU LLM gateway, transcription, prompts, datasets, and experiments from your terminal or agentic workflows. Ships as 2kw, backbone, and bb.",
5
5
  "keywords": [
6
6
  "cli",