scaledthought 0.2.1 → 0.3.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 (3) hide show
  1. package/README.md +8 -8
  2. package/dist/index.js +785 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@ JSON when stdout isn't a terminal, and every error is one JSON line on stderr wi
7
7
  Agents: the full setup protocol is at <https://scaledthought.com/llms.txt>.
8
8
 
9
9
  ```bash
10
- npx -y scaledthought@latest init --region us # or: --region ca (data stays in Canada)
10
+ npx -y scaledthought@0.3 init --region us # or: --region ca (data stays in Canada)
11
11
  ```
12
12
 
13
13
  `init` creates an account, writes `ST_API_KEY` and `ST_BASE_URL` to `./.env` (mode 0600), adds `.env` to
@@ -30,9 +30,9 @@ Inference is prepaid, and a new account has no credits: until your human pays, c
30
30
  `402 insufficient_credits`. So the first thing after `init` is a top-up your human approves:
31
31
 
32
32
  ```bash
33
- npx -y scaledthought@latest billing topup --amount-usd 5 # prints approval_url and topup_id
33
+ npx -y scaledthought@0.3 billing topup --amount-usd 20 # prints approval_url and topup_id
34
34
  # show your human the approval_url, exactly as printed; they pay by card
35
- npx -y scaledthought@latest billing wait # returns once they've paid; on topup_pending run it again
35
+ npx -y scaledthought@0.3 billing wait # returns once they've paid; on topup_pending run it again
36
36
  ```
37
37
 
38
38
  `billing wait` returns within about 90 seconds (`--timeout-s` to change it), under Claude Code's default 2-minute
@@ -40,11 +40,11 @@ shell timeout. Prefer it to `billing topup --wait`: an agent's shell tool usuall
40
40
  ends, so the link would reach the human only after the payment it is waiting for.
41
41
 
42
42
  ```bash
43
- npx -y scaledthought@latest models --live # models serving in your region now
44
- npx -y scaledthought@latest chat -m auto "Reply with OK" # one real completion
45
- npx -y scaledthought@latest mcp # `claude mcp add …` command + JSON config
46
- npx -y scaledthought@latest account # plan, region, spend
47
- npx -y scaledthought@latest account ad-measurement off # opt out of ad measurement
43
+ npx -y scaledthought@0.3 models --live # models serving in your region now
44
+ npx -y scaledthought@0.3 chat -m auto "Reply with OK" # one real completion
45
+ npx -y scaledthought@0.3 mcp # `claude mcp add …` command + JSON config
46
+ npx -y scaledthought@0.3 account # plan, region, spend
47
+ npx -y scaledthought@0.3 account ad-measurement off # opt out of ad measurement
48
48
  ```
49
49
 
50
50
  Other commands: `keys list|create|rotate|revoke|revoke-self` (`keys create --expires-in 30d --scopes inference --child`, `keys rotate <id> --grace 24h`), `usage`, `billing status|topup|wait|auto-reload|transactions`, `batches …`,
package/dist/index.js CHANGED
@@ -553,7 +553,7 @@ import { join } from "node:path";
553
553
  // package.json
554
554
  var package_default = {
555
555
  name: "scaledthought",
556
- version: "0.2.1",
556
+ version: "0.3.0",
557
557
  description: "Headless open-model inference, run by your agent. Prepaid, pay per token. US or Canada.",
558
558
  license: "MIT",
559
559
  keywords: [
@@ -631,9 +631,20 @@ function readDotEnv(cwd) {
631
631
  }
632
632
  return out;
633
633
  }
634
- var CREDITS_HINT = "This account has no credits for this request (inference is prepaid). Run `npx -y scaledthought@latest billing topup --amount-usd 5` (creating it charges nothing), end your turn giving your human its message_for_human (the approval_url), and once they say they've paid run `npx -y scaledthought@latest billing wait`, then retry. Don't retry before that or create another account.";
634
+ var CREDITS_HINT = "This account has no credits for this request (inference is prepaid). Run `npx -y scaledthought@0.3 billing topup --amount-usd 20` (creating it charges nothing) and give your human its message_for_human. If it printed opened_in_browser: true, run `npx -y scaledthought@0.3 billing wait` in the same turn (again on topup_pending, up to 15 minutes): it returns as soon as they pay. Otherwise end your turn with the message and run billing wait once they're back. Then retry. Don't retry before that or create another account.";
635
635
  var HINTS = {
636
- no_key: "Run `npx -y scaledthought@latest init --region us|ca` in this directory, or set ST_API_KEY.",
636
+ no_key: "For an existing account run `npx -y scaledthought@0.3 login`; for a new one, `init --region us|ca`. Or set ST_API_KEY.",
637
+ key_exists: "This folder already has a key (ST_API_KEY in ./.env or the environment). Use it, or pass --force to replace it.",
638
+ authorization_pending: "Your human hasn't typed the code yet. If they have the page open, run `npx -y scaledthought@0.3 login --wait` again right away (codes last 10 minutes); otherwise remind them of the code and the URL.",
639
+ access_denied: "Your human denied the connection. Ask them before running login again.",
640
+ expired_token: "The code expired. Run `npx -y scaledthought@0.3 login` again for a new one.",
641
+ already_claimed: "This code was already used. Run `npx -y scaledthought@0.3 login` again for a new one.",
642
+ invalid_connect_code: "A connect line works once, for 15 minutes, and its code starts with stl_. Ask your human for a new line from the dashboard.",
643
+ too_many_failed_claims: "Too many failed connect-line claims in this region. Wait a few minutes, then retry.",
644
+ invalid_device_code: "Run `npx -y scaledthought@0.3 login` again for a new code.",
645
+ no_login_pending: "Run `npx -y scaledthought@0.3 login` first: it prints a code for your human.",
646
+ account_not_found: "Pass a saved account's id or exact name (see choose_account from `init`), or connect another with `login`.",
647
+ no_saved_accounts: "No account is saved on this machine. Run `npx -y scaledthought@0.3 login` to connect an existing one.",
637
648
  bad_key: "ST_API_KEY must look like st_{us|ca}_{live|test}_<40 chars>. Re-run init in a directory without one.",
638
649
  invalid_api_key: "The key is missing, revoked or mistyped. Check ST_API_KEY in .env; create a new one with `keys create`.",
639
650
  missing_api_key: "Send the key as `Authorization: Bearer $ST_API_KEY`.",
@@ -647,12 +658,12 @@ var HINTS = {
647
658
  credits_required_for_fine_tuning: `Fine-tuning is paid from prepaid credits. ${CREDITS_HINT}`,
648
659
  approval_required: "This change needs your human's approval. Send them the approval_url from `billing topup`; disabling or lowering auto-reload needs none.",
649
660
  daily_cap_exceeded: "An operator-set daily cap on this account is reached. It resets at 00:00 UTC. Tell your human.",
650
- free_tier_limit: "Accounts that haven't bought credits have lower limits. The first credit purchase lifts them: run `billing topup --amount-usd 5` and show your human the approval_url.",
661
+ free_tier_limit: "Accounts that haven't bought credits have lower limits. The first credit purchase lifts them: run `billing topup --amount-usd 20` and show your human the approval_url.",
651
662
  free_tier_key_limit: "Accounts that haven't bought credits have at most 5 active keys. Revoke one with `keys revoke <id>`, or buy credits with `billing topup`.",
652
663
  too_many_pending_topups: "Two approval links are still unpaid. Ask your human to use one (see `billing status` pending_topups), or wait for them to expire.",
653
664
  daily_topup_limit: "Top-ups are limited to $2,000 per account per rolling 24 hours (open links and auto-reloads included). Use a smaller amount (see available_usd) or wait.",
654
- billing_upgrade_removed: "Run `billing topup --amount-usd 5` and show your human the approval_url.",
655
- gone: "This endpoint was removed. For credits use `billing topup --amount-usd 5`.",
665
+ billing_upgrade_removed: "Run `billing topup --amount-usd 20` and show your human the approval_url.",
666
+ gone: "This endpoint was removed. For credits use `billing topup --amount-usd 20`.",
656
667
  fine_tuning_requires_self_hosted: "This base model can't be fine-tuned in your region (it isn't served on our own GPUs). Choose another base model.",
657
668
  rate_limit_exceeded: "Wait the number of seconds in retry_after, then retry.",
658
669
  session_limit_exceeded: "Too many realtime sessions are open on this key. Close one, then retry.",
@@ -905,8 +916,8 @@ async function speak(ctx, o) {
905
916
 
906
917
  // src/commands.ts
907
918
  import { spawn } from "node:child_process";
908
- import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync } from "node:fs";
909
- import { join as join3, resolve as resolve2 } from "node:path";
919
+ import { chmodSync as chmodSync3, existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
920
+ import { join as join5, resolve as resolve2 } from "node:path";
910
921
 
911
922
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js
912
923
  function getEnumValues(entries) {
@@ -8964,6 +8975,8 @@ var EXPERIMENT_METRICS = [
8964
8975
  "signups",
8965
8976
  "activation_rate",
8966
8977
  "paid_rate",
8978
+ "topup_request_rate",
8979
+ "topup_completion_rate",
8967
8980
  "cost_per_activated",
8968
8981
  "cost_per_payer",
8969
8982
  "onboarding_success_rate",
@@ -9238,27 +9251,516 @@ var TERMS_URL = `${LEGAL_SITE}${LEGAL_PATHS.terms}`;
9238
9251
  var PRIVACY_URL = `${LEGAL_SITE}${LEGAL_PATHS.privacy}`;
9239
9252
  var ACCEPTABLE_USE_URL = `${LEGAL_SITE}${LEGAL_PATHS.acceptableUse}`;
9240
9253
  var REFUND_POLICY_URL = `${LEGAL_SITE}${LEGAL_PATHS.creditsAndRefunds}`;
9241
- var TERMS_NOTICE = `By creating this account you accept the ScaledThought Terms of Service (${TERMS_URL}) and Privacy Policy ` + `(${PRIVACY_URL}) on behalf of the person or organisation you act for, who is the customer.`;
9254
+ var TERMS_NOTICE = `Nothing has been accepted on your behalf. You accept the ScaledThought Terms of Service (${TERMS_URL}) and the ` + `Credits & Refunds policy (${REFUND_POLICY_URL}) yourself when you buy credits. Privacy Policy: ${PRIVACY_URL}.`;
9255
+
9256
+ // src/connect.ts
9257
+ import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
9258
+ import { hostname } from "node:os";
9259
+ import { basename as basename2, join as join4 } from "node:path";
9260
+
9261
+ // src/credentials.ts
9262
+ import { spawnSync } from "node:child_process";
9263
+ import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
9264
+ import { join as join3 } from "node:path";
9265
+ var SERVICE = "scaledthought";
9266
+ var slot = (a) => `${a.region}:${a.account_id}`;
9267
+ var SLOT_RE = /^(us|ca):acct_[0-9A-Za-z]{1,40}$/;
9268
+ function writePrivate(file, data) {
9269
+ writeFileSync(file, `${JSON.stringify(data, null, 2)}
9270
+ `, { mode: 384 });
9271
+ chmodSync(file, 384);
9272
+ }
9273
+ function readJson(file, fallback) {
9274
+ try {
9275
+ return existsSync2(file) ? JSON.parse(readFileSync3(file, "utf8")) : fallback;
9276
+ } catch {
9277
+ return fallback;
9278
+ }
9279
+ }
9280
+ function fileBackend(dir) {
9281
+ const file = join3(dir, "credentials.json");
9282
+ const all = () => readJson(file, {});
9283
+ return {
9284
+ where: "~/.config/scaledthought/credentials.json (0600)",
9285
+ get: (s) => all()[s] ?? null,
9286
+ set: (s, key) => {
9287
+ writePrivate(file, { ...all(), [s]: key });
9288
+ return true;
9289
+ },
9290
+ del: (s) => {
9291
+ const a = all();
9292
+ delete a[s];
9293
+ writePrivate(file, a);
9294
+ }
9295
+ };
9296
+ }
9297
+ function keychainBackend() {
9298
+ return {
9299
+ where: "macOS Keychain (service scaledthought)",
9300
+ get: (s) => {
9301
+ const r = spawnSync("security", ["find-generic-password", "-s", SERVICE, "-a", s, "-w"], { encoding: "utf8" });
9302
+ return r.status === 0 ? r.stdout.trim() || null : null;
9303
+ },
9304
+ set: (s, key) => spawnSync("security", ["-i"], {
9305
+ input: `add-generic-password -U -s ${SERVICE} -a ${s} -l ScaledThought -w ${key}
9306
+ `,
9307
+ encoding: "utf8"
9308
+ }).status === 0,
9309
+ del: (s) => {
9310
+ spawnSync("security", ["delete-generic-password", "-s", SERVICE, "-a", s], { encoding: "utf8" });
9311
+ }
9312
+ };
9313
+ }
9314
+ function secretToolBackend() {
9315
+ return {
9316
+ where: "the Secret Service keyring (secret-tool, service scaledthought)",
9317
+ get: (s) => {
9318
+ const r = spawnSync("secret-tool", ["lookup", "service", SERVICE, "account", s], { encoding: "utf8" });
9319
+ return r.status === 0 ? r.stdout.trim() || null : null;
9320
+ },
9321
+ set: (s, key) => spawnSync("secret-tool", ["store", `--label=ScaledThought ${s}`, "service", SERVICE, "account", s], {
9322
+ input: key,
9323
+ encoding: "utf8"
9324
+ }).status === 0,
9325
+ del: (s) => {
9326
+ spawnSync("secret-tool", ["clear", "service", SERVICE, "account", s], { encoding: "utf8" });
9327
+ }
9328
+ };
9329
+ }
9330
+ var has = (bin) => spawnSync(bin, ["--help"], { stdio: "ignore" }).error === undefined;
9331
+ function storeAt(dir, os) {
9332
+ const indexFile = join3(dir, "accounts.json");
9333
+ const file = fileBackend(dir);
9334
+ const index = () => readJson(indexFile, []).filter((e) => SLOT_RE.test(slot(e)));
9335
+ const ensureDir = () => mkdirSync(dir, { recursive: true, mode: 448 });
9336
+ let where = os?.where ?? file.where;
9337
+ return {
9338
+ get where() {
9339
+ return where;
9340
+ },
9341
+ list: () => index().map(({ in: _in, ...a }) => a),
9342
+ getKey: (a) => {
9343
+ const e = index().find((x) => slot(x) === slot(a));
9344
+ if (!e)
9345
+ return null;
9346
+ return (e.in === "os" && os ? os.get(slot(a)) : file.get(slot(a))) ?? null;
9347
+ },
9348
+ save: (a, key) => {
9349
+ ensureDir();
9350
+ let into = "file";
9351
+ if (os?.set(slot(a), key))
9352
+ into = "os";
9353
+ else
9354
+ file.set(slot(a), key);
9355
+ where = into === "os" && os ? os.where : file.where;
9356
+ writePrivate(indexFile, [...index().filter((x) => slot(x) !== slot(a)), { ...a, in: into }]);
9357
+ },
9358
+ remove: (a) => {
9359
+ const e = index().find((x) => slot(x) === slot(a));
9360
+ if (!e)
9361
+ return;
9362
+ if (e.in === "os" && os)
9363
+ os.del(slot(a));
9364
+ else
9365
+ file.del(slot(a));
9366
+ writePrivate(indexFile, index().filter((x) => slot(x) !== slot(a)));
9367
+ },
9368
+ touch: (a) => {
9369
+ const all = index();
9370
+ const e = all.find((x) => slot(x) === slot(a));
9371
+ if (!e)
9372
+ return;
9373
+ e.last_used_at = new Date().toISOString();
9374
+ writePrivate(indexFile, all);
9375
+ }
9376
+ };
9377
+ }
9378
+ function machineStore(env, platform = process.platform) {
9379
+ const mode = env.ST_CREDENTIAL_STORE;
9380
+ const home = env.HOME || env.USERPROFILE;
9381
+ if (mode === "off" || !home)
9382
+ return null;
9383
+ const dir = join3(env.XDG_CONFIG_HOME || join3(home, ".config"), "scaledthought");
9384
+ let os = null;
9385
+ if (mode !== "file") {
9386
+ if (platform === "darwin" && existsSync2("/usr/bin/security"))
9387
+ os = keychainBackend();
9388
+ else if (platform === "linux" && env.DBUS_SESSION_BUS_ADDRESS && has("secret-tool"))
9389
+ os = secretToolBackend();
9390
+ }
9391
+ return storeAt(dir, os);
9392
+ }
9393
+
9394
+ // src/connect.ts
9395
+ var NPX = "npx -y scaledthought@0.3";
9396
+ var LOGIN_DIR = ".scaledthought";
9397
+ var LOGIN_FILE = join4(LOGIN_DIR, "login.json");
9398
+ var KEY_SAVED_TO = ".env (ST_API_KEY, ST_BASE_URL)";
9399
+ var CONNECT_CODE_RE = /^stl_[A-Za-z0-9_-]{22,64}$/;
9400
+ var PROJECT_SCOPES = ["inference", "read", "keys", "billing"];
9401
+ var storeOf = (ctx) => ctx.store !== undefined ? ctx.store : machineStore(ctx.env);
9402
+ function defaultClientLabel(env, host = hostname()) {
9403
+ const keys = Object.keys(env).filter((k) => env[k] !== undefined && env[k] !== "");
9404
+ const tool = env.CLAUDECODE ? "Claude Code" : keys.some((k) => k.startsWith("CODEX_")) ? "Codex" : keys.some((k) => k.startsWith("CURSOR_")) ? "Cursor" : "CLI";
9405
+ return `${tool} on ${host.replace(/\.local$/, "")}`.slice(0, 80);
9406
+ }
9407
+ function readLoginState(cwd) {
9408
+ const f = join4(cwd, LOGIN_FILE);
9409
+ if (!existsSync3(f))
9410
+ return null;
9411
+ try {
9412
+ const s = JSON.parse(readFileSync4(f, "utf8"));
9413
+ return typeof s.device_code === "string" && (s.region === "us" || s.region === "ca") ? s : null;
9414
+ } catch {
9415
+ return null;
9416
+ }
9417
+ }
9418
+ function writeLoginState(cwd, s) {
9419
+ mkdirSync2(join4(cwd, LOGIN_DIR), { recursive: true, mode: 448 });
9420
+ const f = join4(cwd, LOGIN_FILE);
9421
+ writeFileSync2(f, `${JSON.stringify(s)}
9422
+ `, { mode: 384 });
9423
+ chmodSync2(f, 384);
9424
+ ensureIgnored(cwd, `${LOGIN_DIR}/`);
9425
+ }
9426
+ var clearLoginState = (cwd) => rmSync(join4(cwd, LOGIN_FILE), { force: true });
9427
+ function ensureIgnored(cwd, entry) {
9428
+ const gi = join4(cwd, ".gitignore");
9429
+ const text = existsSync3(gi) ? readFileSync4(gi, "utf8") : "";
9430
+ const bare = entry.replace(/\/$/, "");
9431
+ if (text.split(/\r?\n/).some((l) => [entry, bare, `/${entry}`, `/${bare}`].includes(l.trim())))
9432
+ return;
9433
+ const sep = text === "" || text.endsWith(`
9434
+ `) ? "" : `
9435
+ `;
9436
+ writeFileSync2(gi, `${text}${sep}${entry}
9437
+ `);
9438
+ }
9439
+ function refuseExistingKey(ctx, force) {
9440
+ if (force)
9441
+ return;
9442
+ const where = ctx.env.ST_API_KEY ? "the ST_API_KEY environment variable" : readDotEnv(ctx.cwd).ST_API_KEY ? "./.env" : null;
9443
+ if (where)
9444
+ throw new CliError("key_exists", `This folder already has a ScaledThought key (${where}). Nothing was changed.`);
9445
+ }
9446
+ function saveToStore(ctx, a, key, noSave) {
9447
+ if (noSave)
9448
+ return false;
9449
+ const store = storeOf(ctx);
9450
+ if (!store)
9451
+ return false;
9452
+ try {
9453
+ store.save({ ...a, saved_at: new Date().toISOString() }, key);
9454
+ return store.where;
9455
+ } catch {
9456
+ return false;
9457
+ }
9458
+ }
9459
+ function finish(ctx, c, o) {
9460
+ writeEnv(ctx.cwd, { ST_API_KEY: c.api_key, ST_BASE_URL: c.base_url });
9461
+ ensureGitignored(ctx.cwd);
9462
+ const saved = saveToStore(ctx, { region: c.region, account_id: c.account_id, account_name: c.account_name, base_url: c.base_url }, c.api_key, o.noSave);
9463
+ clearLoginState(ctx.cwd);
9464
+ return {
9465
+ status: "connected",
9466
+ account_id: c.account_id,
9467
+ account_name: c.account_name,
9468
+ region: c.region,
9469
+ key_name: c.key_name,
9470
+ key_saved_to: KEY_SAVED_TO,
9471
+ saved_to_credential_store: saved !== false,
9472
+ ...saved ? { credential_store: saved } : {}
9473
+ };
9474
+ }
9475
+ var claim2 = (ctx, region, body) => api(ctx, "POST", "/v1/device/token", { auth: false, region, body });
9476
+ async function login(ctx, o = {}) {
9477
+ refuseExistingKey(ctx, o.force);
9478
+ const region = o.region ?? "us";
9479
+ const label = (o.label ?? defaultClientLabel(ctx.env)).slice(0, 80);
9480
+ const r = await api(ctx, "POST", "/v1/device/codes", { auth: false, region, body: { client_label: label, origin: "agent" } });
9481
+ writeLoginState(ctx.cwd, {
9482
+ device_code: r.device_code,
9483
+ region,
9484
+ origin: "agent",
9485
+ user_code: r.user_code,
9486
+ created_at: new Date().toISOString()
9487
+ });
9488
+ const noOpen = o.noOpen || ctx.env.ST_NO_BROWSER === "1";
9489
+ const opened = !noOpen && await (o.open ?? openInBrowser)(r.verification_url);
9490
+ return {
9491
+ status: "pending",
9492
+ user_code: r.user_code,
9493
+ verification_url: r.verification_url,
9494
+ opened_in_browser: opened,
9495
+ expires_in: r.expires_in,
9496
+ client_label: label,
9497
+ message_for_human: opened ? `To connect this agent to your ScaledThought account, I've opened ${r.verification_url}. Sign in and type the code ${r.user_code}.` : `To connect this agent to your ScaledThought account, open ${r.verification_url}, sign in and type the code ${r.user_code}.`,
9498
+ next_command: `${NPX} login --wait`,
9499
+ next_steps: [
9500
+ opened ? `Tell your human message_for_human, then in the same turn run: ${NPX} login --wait. On authorization_pending run it again, for up to 10 minutes in all.` : `Give your human message_for_human and end your turn. When they say they've typed the code, run: ${NPX} login --wait.`,
9501
+ "Never put the code in a link: the human types it on the page."
9502
+ ]
9503
+ };
9504
+ }
9505
+ async function loginWait(ctx, o = {}) {
9506
+ const s = readLoginState(ctx.cwd);
9507
+ if (s?.origin !== "agent")
9508
+ throw new CliError("no_login_pending", "No login is waiting in this folder.");
9509
+ refuseExistingKey(ctx, o.force);
9510
+ const deadline = Date.now() + (o.timeoutS ?? 90) * 1000;
9511
+ for (;; ) {
9512
+ try {
9513
+ return finish(ctx, await claim2(ctx, s.region, { device_code: s.device_code }), o);
9514
+ } catch (e) {
9515
+ if (!(e instanceof CliError))
9516
+ throw e;
9517
+ if (e.code !== "authorization_pending") {
9518
+ if (["expired_token", "access_denied", "already_claimed", "invalid_device_code"].includes(e.code))
9519
+ clearLoginState(ctx.cwd);
9520
+ throw e;
9521
+ }
9522
+ if (Date.now() >= deadline)
9523
+ throw new CliError("authorization_pending", "Your human hasn't approved the code yet.", undefined, {
9524
+ user_code: s.user_code ?? null
9525
+ });
9526
+ }
9527
+ await new Promise((r) => setTimeout(r, o.pollMs ?? 5000));
9528
+ }
9529
+ }
9530
+ async function loginWithCode(ctx, o) {
9531
+ const code = o.code.trim();
9532
+ if (!CONNECT_CODE_RE.test(code))
9533
+ throw new CliError("invalid_connect_code", "That isn't a connect line's code (it starts with stl_). Copy the whole line from the dashboard.");
9534
+ refuseExistingKey(ctx, o.force);
9535
+ const region = o.region ?? "us";
9536
+ const label = (o.label ?? defaultClientLabel(ctx.env)).slice(0, 80);
9537
+ return finish(ctx, await claim2(ctx, region, { connect_code: code, client_label: label }), o);
9538
+ }
9539
+ var usdOf = (b) => b && typeof b.balance_usd === "number" ? b.balance_usd : null;
9540
+ var withKey = (ctx, key) => ({ ...ctx, env: { ...ctx.env, ST_API_KEY: key } });
9541
+ async function projectKey(ctx, store, a) {
9542
+ const key = store.getKey(a);
9543
+ if (!key || regionOfKey(key) !== a.region)
9544
+ return null;
9545
+ const folder = basename2(ctx.cwd) || "project";
9546
+ const name = `project: ${folder}`.slice(0, 80);
9547
+ let created;
9548
+ try {
9549
+ created = await api(withKey(ctx, key), "POST", "/v1/keys", {
9550
+ body: {
9551
+ name,
9552
+ child: true,
9553
+ scopes: PROJECT_SCOPES,
9554
+ lives_in: `env:${hostname()}:${ctx.cwd}`.slice(0, 300)
9555
+ }
9556
+ });
9557
+ } catch (e) {
9558
+ if (e instanceof CliError && ["invalid_api_key", "key_expired", "account_suspended"].includes(e.code)) {
9559
+ store.remove(a);
9560
+ return null;
9561
+ }
9562
+ if (e instanceof CliError && (e.code === "insufficient_scope" || e.code === "scope_not_allowed")) {
9563
+ writeEnv(ctx.cwd, { ST_API_KEY: key, ST_BASE_URL: a.base_url });
9564
+ ensureGitignored(ctx.cwd);
9565
+ store.touch(a);
9566
+ return { key, key_id: null, key_name: "this machine's saved key (it can't make project keys)" };
9567
+ }
9568
+ throw e;
9569
+ }
9570
+ writeEnv(ctx.cwd, { ST_API_KEY: created.api_key, ST_BASE_URL: a.base_url });
9571
+ ensureGitignored(ctx.cwd);
9572
+ store.touch(a);
9573
+ return { key: created.api_key, key_id: created.id, key_name: created.name };
9574
+ }
9575
+ var label = (a, balance) => balance == null ? a.account_name : `${a.account_name} ($${balance.toFixed(2)})`;
9576
+ async function reuseSavedAccount(ctx, o) {
9577
+ const store = storeOf(ctx);
9578
+ const saved = (store?.list() ?? []).filter((a) => !o.region || a.region === o.region);
9579
+ if (!store || saved.length === 0) {
9580
+ if (o.account)
9581
+ throw new CliError("no_saved_accounts", `No account is saved on this machine${o.region ? ` in ${o.region}` : ""}.`);
9582
+ return null;
9583
+ }
9584
+ let pick;
9585
+ if (o.account) {
9586
+ const want = o.account.trim().toLowerCase();
9587
+ pick = saved.find((a) => a.account_id.toLowerCase() === want) ?? saved.find((a) => a.account_name.toLowerCase() === want);
9588
+ if (!pick)
9589
+ throw new CliError("account_not_found", `No saved account named or numbered "${o.account}".`, undefined, {
9590
+ saved_accounts: saved.map((a) => ({ account_id: a.account_id, name: a.account_name, region: a.region }))
9591
+ });
9592
+ } else if (saved.length > 1) {
9593
+ const rows = await Promise.all(saved.map(async (a) => {
9594
+ const key = store.getKey(a);
9595
+ const b = key ? await api(withKey(ctx, key), "GET", "/v1/billing").catch(() => null) : null;
9596
+ return { a, balance: usdOf(b) };
9597
+ }));
9598
+ rows.sort((x, y) => (y.a.last_used_at ?? y.a.saved_at).localeCompare(x.a.last_used_at ?? x.a.saved_at));
9599
+ const names = rows.map((r) => label(r.a, r.balance));
9600
+ return {
9601
+ status: "choose_account",
9602
+ choose_account: rows.map((r) => ({
9603
+ account_id: r.a.account_id,
9604
+ name: r.a.account_name,
9605
+ region: r.a.region,
9606
+ balance_usd: r.balance,
9607
+ last_used: (r.a.last_used_at ?? r.a.saved_at).slice(0, 10)
9608
+ })),
9609
+ message_for_human: `Which ScaledThought account should this repo use: ${names.slice(0, -1).join(", ")} or ${names.at(-1)}? Or a new one for separate billing?`,
9610
+ next_steps: [
9611
+ "Ask your human message_for_human and end your turn. Nothing was created.",
9612
+ `Then run: ${NPX} init --account "<name or account_id>" (or ${NPX} init --new-account for a separate, new account).`
9613
+ ]
9614
+ };
9615
+ } else
9616
+ pick = saved[0];
9617
+ const made = await projectKey(ctx, store, pick);
9618
+ if (!made) {
9619
+ if (o.account)
9620
+ throw new CliError("account_not_found", `The key saved for ${pick.account_name} no longer works, so it was forgotten.`, `Connect it again with \`${NPX} login\`.`);
9621
+ return reuseSavedAccount(ctx, o);
9622
+ }
9623
+ const billing = await api(withKey(ctx, made.key), "GET", "/v1/billing").catch(() => null);
9624
+ return {
9625
+ already_initialized: false,
9626
+ reused_account: true,
9627
+ account_id: pick.account_id,
9628
+ account_name: pick.account_name,
9629
+ region: pick.region,
9630
+ base_url: pick.base_url,
9631
+ key_name: made.key_name,
9632
+ key_id: made.key_id,
9633
+ key_saved_to: KEY_SAVED_TO,
9634
+ saved_to_credential_store: true,
9635
+ note: `This folder uses ${pick.account_name}, the account saved on this machine, with its own project key. To switch: ${NPX} use "<name>". For a separate account: ${NPX} init --new-account.`,
9636
+ ...await liveSummary(withKey(ctx, made.key), pick.region, usdOf(billing))
9637
+ };
9638
+ }
9639
+ async function afterNewAccount(ctx, a, o) {
9640
+ const saved = saveToStore(ctx, {
9641
+ region: a.region,
9642
+ account_id: a.account_id,
9643
+ account_name: a.name?.trim() || `Account ${a.account_id.slice(-6)}`,
9644
+ base_url: a.base_url
9645
+ }, a.api_key, o.noSave);
9646
+ let existingCode = null;
9647
+ try {
9648
+ const r = await api(withKey(ctx, a.api_key), "POST", "/v1/device/codes", { body: { origin: "init", client_label: (o.label ?? defaultClientLabel(ctx.env)).slice(0, 80) } });
9649
+ if (typeof r.device_code === "string") {
9650
+ writeLoginState(ctx.cwd, {
9651
+ device_code: r.device_code,
9652
+ region: a.region,
9653
+ origin: "init",
9654
+ user_code: typeof r.user_code === "string" ? r.user_code : undefined,
9655
+ new_account_id: a.account_id,
9656
+ created_at: new Date().toISOString()
9657
+ });
9658
+ existingCode = typeof r.user_code === "string" ? r.user_code : null;
9659
+ }
9660
+ } catch {}
9661
+ return { saved, existingCode };
9662
+ }
9663
+ function existingAccountCode(cwd) {
9664
+ const s = readLoginState(cwd);
9665
+ return s?.origin === "init" && s.user_code ? s.user_code : null;
9666
+ }
9667
+ var existingAccountHint = (code) => `If you already have a ScaledThought account, choose "Use it instead" on that page and type the code ${code}: I'll use that account and nothing is charged.`;
9668
+ async function useAccount(ctx, o) {
9669
+ const store = storeOf(ctx);
9670
+ const saved = store?.list() ?? [];
9671
+ if (!store || saved.length === 0)
9672
+ throw new CliError("no_saved_accounts", "No account is saved on this machine.");
9673
+ const want = o.target.trim().toLowerCase();
9674
+ const pick = saved.find((a) => a.account_id.toLowerCase() === want) ?? saved.find((a) => a.account_name.toLowerCase() === want);
9675
+ if (!pick)
9676
+ throw new CliError("account_not_found", `No saved account named or numbered "${o.target}".`, undefined, {
9677
+ saved_accounts: saved.map((a) => ({ account_id: a.account_id, name: a.account_name, region: a.region }))
9678
+ });
9679
+ const oldKey = readDotEnv(ctx.cwd).ST_API_KEY;
9680
+ if (oldKey) {
9681
+ const cur = await api(withKey(ctx, oldKey), "GET", "/v1/account").catch(() => null);
9682
+ if (cur?.account_id === pick.account_id)
9683
+ return { status: "unchanged", account_id: pick.account_id, account_name: pick.account_name };
9684
+ }
9685
+ const made = await projectKey(ctx, store, pick);
9686
+ if (!made)
9687
+ throw new CliError("account_not_found", `The key saved for ${pick.account_name} no longer works, so it was forgotten.`, `Connect it again with \`${NPX} login --force\`.`);
9688
+ let oldRevoked = false;
9689
+ if (oldKey && !saved.some((a) => store.getKey(a) === oldKey)) {
9690
+ oldRevoked = await api(withKey(ctx, oldKey), "DELETE", "/v1/keys/self").then(() => true, () => false);
9691
+ }
9692
+ return {
9693
+ status: "switched",
9694
+ account_id: pick.account_id,
9695
+ account_name: pick.account_name,
9696
+ region: pick.region,
9697
+ key_name: made.key_name,
9698
+ key_saved_to: KEY_SAVED_TO,
9699
+ old_key_revoked: oldRevoked,
9700
+ ...ctx.env.ST_API_KEY ? { warnings: ["ST_API_KEY is set in the environment and overrides ./.env: unset it to use the new key."] } : {}
9701
+ };
9702
+ }
9703
+ async function connectedExisting(ctx, o = {}) {
9704
+ const s = readLoginState(ctx.cwd);
9705
+ if (s?.origin !== "init")
9706
+ return null;
9707
+ let c;
9708
+ try {
9709
+ c = await claim2(ctx, s.region, { device_code: s.device_code });
9710
+ } catch (e) {
9711
+ if (e instanceof CliError && ["expired_token", "access_denied", "already_claimed"].includes(e.code))
9712
+ clearLoginState(ctx.cwd);
9713
+ return null;
9714
+ }
9715
+ const done = finish(ctx, c, o);
9716
+ const store = storeOf(ctx);
9717
+ if (c.closed_account && store)
9718
+ store.remove({ region: s.region, account_id: c.closed_account });
9719
+ const billing = await api(withKey(ctx, c.api_key), "GET", "/v1/billing").catch(() => null);
9720
+ const balance = usdOf(billing);
9721
+ return {
9722
+ status: "connected_existing",
9723
+ account_id: c.account_id,
9724
+ account_name: c.account_name,
9725
+ balance_usd: balance,
9726
+ key_name: c.key_name,
9727
+ key_saved_to: KEY_SAVED_TO,
9728
+ saved_to_credential_store: done.saved_to_credential_store,
9729
+ closed_account: c.closed_account ? `${c.closed_account} (new, never paid)` : null,
9730
+ message_for_human: `You already had an account, so I've connected to it instead: ${c.account_name}${balance == null ? "" : `, $${balance.toFixed(2)} left`}. Nothing was charged.`
9731
+ };
9732
+ }
9733
+ async function readCodeFrom(stream) {
9734
+ let text = "";
9735
+ for await (const chunk of stream) {
9736
+ text += typeof chunk === "string" ? chunk : chunk.toString("utf8");
9737
+ if (text.includes(`
9738
+ `) || text.length > 4096)
9739
+ break;
9740
+ }
9741
+ return /\bstl_[A-Za-z0-9_-]+/.exec(text)?.[0] ?? (text.split(/\r?\n/)[0] ?? "").trim();
9742
+ }
9242
9743
 
9243
9744
  // src/commands.ts
9244
- var NPX = "npx -y scaledthought@latest";
9245
- var FIRST_TOPUP_USD = 5;
9246
- var TOPUP_COMMAND = `${NPX} billing topup --amount-usd ${FIRST_TOPUP_USD}`;
9247
- var WAIT_COMMAND = `${NPX} billing wait`;
9745
+ var NPX2 = "npx -y scaledthought@0.3";
9746
+ var FIRST_TOPUP_USD = 20;
9747
+ var TOPUP_COMMAND = `${NPX2} billing topup --amount-usd ${FIRST_TOPUP_USD}`;
9748
+ var WAIT_COMMAND = `${NPX2} billing wait`;
9749
+ var WAIT_PATIENCE_MIN = 15;
9248
9750
  var CREDIT_STEPS = [
9249
9751
  `Inference is prepaid and this account has no credits yet: completions return 402 insufficient_credits until your human pays. Do these first, in order:`,
9250
9752
  `1. Run: ${TOPUP_COMMAND}. It opens the checkout page in your human's browser and prints approval_url, topup_id and message_for_human. Creating it charges nothing: don't ask first.`,
9251
- `2. End your turn with message_for_human (it says the checkout page is open and holds the approval_url exactly as printed, in case it didn't open): the one-time payment page where your human accepts the Terms and pays $${FIRST_TOPUP_USD} by card. Don't run anything that waits before they have it, don't retry the call and don't create another account.`,
9252
- `3. When they say they've paid, run: ${WAIT_COMMAND}. It returns status "paid" with the new balance (at once if already paid); topup_pending means not yet.`,
9753
+ `2. If opened_in_browser is true, the funding page (where your human confirms $${FIRST_TOPUP_USD}, can turn on auto-reload, accepts the Terms and pays by card) is already in front of them: tell them so with message_for_human and, in the same turn, run: ${WAIT_COMMAND}. It returns status "paid" with the new balance as soon as they pay; on topup_pending run it again, for up to ${WAIT_PATIENCE_MIN} minutes in all. Don't wait for them to say they've paid.`,
9754
+ `3. If opened_in_browser is false (or it's still unpaid after ${WAIT_PATIENCE_MIN} minutes), end your turn with message_for_human: it holds the approval_url exactly as printed. When they're back, run ${WAIT_COMMAND}. Either way, don't retry the call and don't create another account.`,
9253
9755
  "4. Then make the call below."
9254
9756
  ];
9255
9757
  function nextSteps(model, needsCredits) {
9256
9758
  return [
9257
9759
  ...needsCredits ? CREDIT_STEPS : [],
9258
- model ? `Verify with one real completion: ${NPX} chat -m ${model} "Reply with OK" (or POST $ST_BASE_URL/chat/completions with that model)` : `No chat model is live in this region right now. Check again with: ${NPX} models --live`,
9259
- `Or let the router pick: ${NPX} chat -m auto "Reply with OK"`,
9760
+ model ? `Verify with one real completion: ${NPX2} chat -m ${model} "Reply with OK" (or POST $ST_BASE_URL/chat/completions with that model)` : `No chat model is live in this region right now. Check again with: ${NPX2} models --live`,
9761
+ `Or let the router pick: ${NPX2} chat -m auto "Reply with OK"`,
9260
9762
  "Use it from code: any OpenAI SDK with base_url=$ST_BASE_URL and api_key=$ST_API_KEY (both are in .env)",
9261
- `Optional, connect MCP: ${NPX} mcp`,
9763
+ `Optional, connect MCP: ${NPX2} mcp`,
9262
9764
  ...needsCredits ? [] : [`More credits when needed: ${TOPUP_COMMAND} returns an approval_url for your human (inference is prepaid)`]
9263
9765
  ];
9264
9766
  }
@@ -9278,7 +9780,7 @@ async function liveSummary(ctx, region, balanceUsd) {
9278
9780
  return {
9279
9781
  balance_usd: balanceUsd ?? 0,
9280
9782
  needs_credits: needsCredits,
9281
- next_command: needsCredits ? TOPUP_COMMAND : `${NPX} chat -m ${suggested ?? "auto"} "Reply with OK"`,
9783
+ next_command: needsCredits ? TOPUP_COMMAND : `${NPX2} chat -m ${suggested ?? "auto"} "Reply with OK"`,
9282
9784
  newest_models: live.slice(0, 5),
9283
9785
  suggested_model: suggested,
9284
9786
  next_steps: nextSteps(suggested, needsCredits)
@@ -9328,6 +9830,13 @@ async function init(ctx, opts) {
9328
9830
  ...ignored.length ? { warnings: [`${ignored.join(" and ")} apply only to a new account and were not sent.`] } : {}
9329
9831
  };
9330
9832
  }
9833
+ if (opts.account && opts.newAccount)
9834
+ throw new CliError("bad_argument", "Pass --account or --new-account, not both.");
9835
+ if (!opts.newAccount) {
9836
+ const reused = await reuseSavedAccount(ctx, { region: opts.region, account: opts.account });
9837
+ if (reused)
9838
+ return reused;
9839
+ }
9331
9840
  const region = opts.region ?? "us";
9332
9841
  const attribution = signupAttribution(region, opts);
9333
9842
  const acct = await api(ctx, "POST", "/v1/accounts", {
@@ -9343,6 +9852,7 @@ async function init(ctx, opts) {
9343
9852
  });
9344
9853
  writeEnv(ctx.cwd, { ST_API_KEY: acct.api_key, ST_BASE_URL: acct.base_url });
9345
9854
  ensureGitignored(ctx.cwd);
9855
+ const { saved, existingCode } = await afterNewAccount(ctx, { ...acct, name: opts.name }, { noSave: opts.noSave });
9346
9856
  const keyed = { ...ctx, env: { ...ctx.env, ST_API_KEY: acct.api_key } };
9347
9857
  const billing = await api(keyed, "GET", "/v1/billing").catch(() => null);
9348
9858
  return {
@@ -9351,6 +9861,11 @@ async function init(ctx, opts) {
9351
9861
  region: acct.region,
9352
9862
  base_url: acct.base_url,
9353
9863
  key_saved_to: ".env (ST_API_KEY, ST_BASE_URL)",
9864
+ saved_to_credential_store: saved !== false,
9865
+ ...saved ? {
9866
+ credential_store: `${saved}: other folders on this machine reuse this account (init there makes a project key). Opt out with --no-save.`
9867
+ } : {},
9868
+ ...existingCode ? { existing_account_code: existingCode, existing_account_hint: existingAccountHint(existingCode) } : {},
9354
9869
  terms: termsOf(acct),
9355
9870
  ...attribution.source ? { source: acct.source ?? attribution.source } : {},
9356
9871
  ...await liveSummary(keyed, acct.region, balanceOf(billing)),
@@ -9358,26 +9873,26 @@ async function init(ctx, opts) {
9358
9873
  };
9359
9874
  }
9360
9875
  function writeEnv(cwd, vars) {
9361
- const f = join3(cwd, ".env");
9876
+ const f = join5(cwd, ".env");
9362
9877
  const names = new Set(Object.keys(vars));
9363
- const kept = existsSync2(f) ? readFileSync3(f, "utf8").split(/\r?\n/).filter((l) => !names.has(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(l)?.[1] ?? "")) : [];
9878
+ const kept = existsSync4(f) ? readFileSync5(f, "utf8").split(/\r?\n/).filter((l) => !names.has(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(l)?.[1] ?? "")) : [];
9364
9879
  while (kept.length && kept[kept.length - 1].trim() === "")
9365
9880
  kept.pop();
9366
9881
  const lines = [...kept, ...Object.entries(vars).map(([k, v]) => `${k}=${v}`)];
9367
- writeFileSync(f, `${lines.join(`
9882
+ writeFileSync3(f, `${lines.join(`
9368
9883
  `)}
9369
9884
  `, { mode: 384 });
9370
- chmodSync(f, 384);
9885
+ chmodSync3(f, 384);
9371
9886
  }
9372
9887
  function ensureGitignored(cwd) {
9373
- const gi = join3(cwd, ".gitignore");
9374
- const text = existsSync2(gi) ? readFileSync3(gi, "utf8") : "";
9888
+ const gi = join5(cwd, ".gitignore");
9889
+ const text = existsSync4(gi) ? readFileSync5(gi, "utf8") : "";
9375
9890
  if (/^\/?\.env\*?\s*$/m.test(text))
9376
9891
  return;
9377
9892
  const sep = text === "" || text.endsWith(`
9378
9893
  `) ? "" : `
9379
9894
  `;
9380
- writeFileSync(gi, `${text}${sep}.env
9895
+ writeFileSync3(gi, `${text}${sep}.env
9381
9896
  `);
9382
9897
  }
9383
9898
  function isCatalogRow(m) {
@@ -9539,9 +10054,9 @@ async function chat(ctx, o) {
9539
10054
  };
9540
10055
  }
9541
10056
  var billingStatus = async (ctx) => withoutFreeFields(await api(ctx, "GET", "/v1/billing"));
9542
- var TOPUP_LIMITS = { min_usd: 5, max_usd: 2000, daily_max_usd: 2000, auto_reload_monthly_max_usd: 1e4 };
10057
+ var TOPUP_LIMITS = { min_usd: 20, max_usd: 2000, daily_max_usd: 2000 };
9543
10058
  function checkTopupLimits(amountUsd, autoReload) {
9544
- const { min_usd, max_usd, daily_max_usd, auto_reload_monthly_max_usd } = TOPUP_LIMITS;
10059
+ const { min_usd, max_usd, daily_max_usd } = TOPUP_LIMITS;
9545
10060
  const range = (flag, v, lo, hi) => {
9546
10061
  if (v < lo || v > hi)
9547
10062
  throw new CliError("bad_argument", `--${flag} must be between ${lo} and ${hi.toLocaleString("en-US")}, got ${v}.`, `Top-ups are $${min_usd} to $${max_usd.toLocaleString("en-US")} each, and at most $${daily_max_usd.toLocaleString("en-US")} per account per rolling 24 hours (auto-reloads included).`);
@@ -9550,19 +10065,22 @@ function checkTopupLimits(amountUsd, autoReload) {
9550
10065
  if (!autoReload)
9551
10066
  return;
9552
10067
  range("auto-reload-amount", autoReload.amount_usd, min_usd, max_usd);
9553
- range("auto-reload-below", autoReload.threshold_usd, 0.01, max_usd);
9554
- if (autoReload.monthly_limit_usd !== undefined)
9555
- range("auto-reload-monthly-max", autoReload.monthly_limit_usd, autoReload.amount_usd, auto_reload_monthly_max_usd);
10068
+ range("auto-reload-below", autoReload.threshold_usd, 5, max_usd);
10069
+ if (autoReload.monthly_limit_usd != null)
10070
+ range("auto-reload-monthly-max", autoReload.monthly_limit_usd, autoReload.amount_usd, Number.POSITIVE_INFINITY);
9556
10071
  }
9557
10072
  var TOPUP_DONE = new Set(["paid", "expired", "failed", "canceled"]);
9558
10073
  function topupNextSteps(topupId, amountUsd, opened, autoReload) {
10074
+ const wait = `${WAIT_COMMAND}${topupId ? ` ${topupId}` : ""}`;
9559
10075
  return [
9560
- ...opened ? ["The checkout page is already open in your human's browser: don't open it again or paste the link on its own."] : [],
9561
- `Give your human message_for_human now and end your turn there: it holds the approval_url exactly as printed (a one-time payment link; don't shorten or reformat it). They accept the Terms and the Credits & Refunds policy and pay $${amountUsd} by card; credits are added once the payment is confirmed. Nothing is charged without their approval. Don't run billing wait (or anything else that waits) before they have the link: they may not see your messages until your turn ends.`,
10076
+ opened ? `The checkout page is already open in your human's browser: don't open it again. Tell them with message_for_human (it holds the approval_url in case it didn't open), then in the same turn run: ${wait}. It returns status "paid" with the balance as soon as they pay; on topup_pending run it again, for up to ${WAIT_PATIENCE_MIN} minutes in all. Don't wait for them to say they've paid. If it's still unpaid after that, end your turn with message_for_human.` : `Give your human message_for_human now and end your turn there: it holds the approval_url exactly as printed (a one-time payment link; don't shorten or reformat it). Don't run billing wait (or anything else that waits) before they have the link: they may not see your messages until your turn ends.`,
10077
+ `On that page they accept the Terms and the Credits & Refunds policy and pay $${amountUsd} by card; credits are added once the payment is confirmed. Nothing is charged without their approval.`,
9562
10078
  ...autoReload ? [
9563
10079
  `Approving also saves the card for auto-reload: $${autoReload.amount_usd} whenever the balance drops below $${autoReload.threshold_usd}${autoReload.monthly_limit_usd ? `, at most $${autoReload.monthly_limit_usd} a month` : ""}.`
9564
10080
  ] : [],
9565
- `When they say they've paid: ${WAIT_COMMAND}${topupId ? ` ${topupId}` : ""} (returns status "paid" with the balance, at once if already paid; topup_pending means not yet: tell them, don't make a new link).`,
10081
+ ...opened ? [] : [
10082
+ `When they say they've paid: ${wait} (returns status "paid" with the balance, at once if already paid; topup_pending means not yet: tell them, don't make a new link).`
10083
+ ],
9566
10084
  "Once paid, retry the call that returned 402. Don't create another top-up while this one is pending: the link stays valid until expires_at."
9567
10085
  ];
9568
10086
  }
@@ -9589,32 +10107,87 @@ async function billingTopup(ctx, o) {
9589
10107
  });
9590
10108
  const noOpen = o.noOpen || ctx.env.ST_NO_BROWSER === "1";
9591
10109
  const opened = !noOpen && !!created.approval_url && await (o.open ?? openInBrowser)(created.approval_url);
10110
+ const existing = existingAccountCode(ctx.cwd);
10111
+ const message = messageForHuman(created.approval_url, o.amountUsd, o.autoReload, opened);
9592
10112
  return {
9593
10113
  status: "pending",
9594
10114
  ...created,
9595
10115
  opened_in_browser: opened,
9596
- message_for_human: messageForHuman(created.approval_url, o.amountUsd, o.autoReload, opened),
10116
+ message_for_human: message && existing ? `${message}
10117
+
10118
+ ${existingAccountHint(existing)}` : message,
9597
10119
  next_steps: topupNextSteps(created.topup_id, o.amountUsd, opened, o.autoReload)
9598
10120
  };
9599
10121
  }
10122
+ async function billingAutoReloadSetup(ctx, o = {}) {
10123
+ const body = {
10124
+ ...o.thresholdUsd !== undefined ? { threshold_usd: o.thresholdUsd } : {},
10125
+ ...o.amountUsd !== undefined ? { amount_usd: o.amountUsd } : {}
10126
+ };
10127
+ const created = await api(ctx, "POST", "/v1/billing/auto-reload/setup", {
10128
+ body
10129
+ });
10130
+ const noOpen = o.noOpen || ctx.env.ST_NO_BROWSER === "1";
10131
+ const opened = !noOpen && !!created.approval_url && await (o.open ?? openInBrowser)(created.approval_url);
10132
+ const wait = `${WAIT_COMMAND} ${created.topup_id}`;
10133
+ return {
10134
+ status: "pending",
10135
+ ...created,
10136
+ opened_in_browser: opened,
10137
+ message_for_human: setupMessageForHuman(created.approval_url, created.auto_reload, opened),
10138
+ next_steps: [
10139
+ opened ? `The page is already open in your human's browser: don't open it again. Tell them with message_for_human, then in the same turn run: ${wait}. It returns status "paid" once the card is saved and auto-reload is on; on topup_pending run it again, for up to ${WAIT_PATIENCE_MIN} minutes in all.` : `Give your human message_for_human now and end your turn there: it holds the approval_url exactly as printed. When they're back, run: ${wait}.`,
10140
+ "Nothing is charged now. `billing auto-reload off` turns it off without approval."
10141
+ ]
10142
+ };
10143
+ }
9600
10144
  function messageForHuman(approvalUrl, amountUsd, autoReload, opened = false) {
9601
10145
  if (!approvalUrl)
9602
10146
  return null;
9603
- const reload = autoReload ? ` It also turns on auto-reload: $${autoReload.amount_usd} whenever the balance drops below $${autoReload.threshold_usd}${autoReload.monthly_limit_usd ? `, at most $${autoReload.monthly_limit_usd} a month` : ""}.` : "";
10147
+ const reload = autoReload ? `Auto-reload is pre-selected there: $${autoReload.amount_usd} whenever the balance drops below $${autoReload.threshold_usd}, so I don't stall when credits run low; untick it if you'd rather not.` : "I recommend turning on auto-reload there, so I don't stall when credits run low.";
10148
+ const rest = `${reload} Then you accept the Terms of Service and the Credits & Refunds policy and pay by card. Nothing is charged until you approve, and we email you a receipt.`;
9604
10149
  if (opened)
9605
- return `To finish the setup, I've opened the ScaledThought checkout page in your browser to buy $${amountUsd} of prepaid credits (USD). If it didn't open, use this link:
10150
+ return `To finish the setup, I've opened the ScaledThought funding page in your browser for $${amountUsd} of prepaid credits (USD); you can change the amount there. If it didn't open, use this link:
9606
10151
 
9607
10152
  ${approvalUrl}
9608
10153
 
9609
- On that page you accept the Terms of Service and the Credits & Refunds policy and pay by card. Nothing is charged until you approve.${reload} Tell me when it's done and I'll continue.`;
9610
- return `To finish the setup, please buy $${amountUsd} of prepaid ScaledThought credits (USD) here:
10154
+ ${rest} I'm watching for the payment and will carry on as soon as it goes through.`;
10155
+ return `To finish the setup, please add $${amountUsd} of prepaid ScaledThought credits (USD) here (you can change the amount):
9611
10156
 
9612
10157
  ${approvalUrl}
9613
10158
 
9614
- On that page you accept the Terms of Service and the Credits & Refunds policy and pay by card. Nothing is charged until you approve.${reload} Tell me when it's done and I'll continue.`;
10159
+ ${rest} Tell me when it's done and I'll continue.`;
10160
+ }
10161
+ function setupMessageForHuman(approvalUrl, terms, opened = false) {
10162
+ if (!approvalUrl)
10163
+ return null;
10164
+ const what = `turn on auto-reload: whenever the balance drops below $${terms.threshold_usd}, $${terms.amount_usd} of prepaid credits (USD) is added from a card you save, so I don't stall when credits run low. You can change those amounts on the page. Nothing is charged now, and I can turn it off any time.`;
10165
+ if (opened)
10166
+ return `I've opened a ScaledThought page in your browser to ${what} If it didn't open, use this link:
10167
+
10168
+ ${approvalUrl}
10169
+
10170
+ I'm watching and will carry on as soon as it's set up.`;
10171
+ return `Please use this ScaledThought page to ${what}
10172
+
10173
+ ${approvalUrl}
10174
+
10175
+ Tell me when it's done and I'll continue.`;
9615
10176
  }
9616
10177
  var WAIT_DEFAULT_S = 90;
9617
10178
  async function billingWait(ctx, o = {}) {
10179
+ try {
10180
+ return await billingWaitOnce(ctx, o);
10181
+ } catch (e) {
10182
+ if (e instanceof CliError && ["invalid_api_key", "topup_canceled", "account_suspended"].includes(e.code)) {
10183
+ const switched = await connectedExisting(ctx, { noSave: o.noSave });
10184
+ if (switched)
10185
+ return switched;
10186
+ }
10187
+ throw e;
10188
+ }
10189
+ }
10190
+ async function billingWaitOnce(ctx, o) {
9618
10191
  let id = o.topupId;
9619
10192
  if (id !== undefined && !/^top_[0-9A-Za-z]+$/.test(id))
9620
10193
  throw new CliError("bad_argument", `"${id}" is not a top-up id (top_...).`, "Use topup_id from `billing topup`.");
@@ -9637,7 +10210,7 @@ async function billingWait(ctx, o = {}) {
9637
10210
  if (TOPUP_DONE.has(t.status ?? ""))
9638
10211
  throw new CliError(`topup_${t.status}`, t.status === "expired" ? `Top-up ${id} expired before it was paid.` : `Top-up ${id} was not paid (${t.status}${t.failure_code ? `: ${t.failure_code}` : ""}).`, `Run \`${TOPUP_COMMAND}\` again and show your human the new approval_url.`, { topup_id: id });
9639
10212
  if (Date.now() >= deadline)
9640
- throw new CliError("topup_pending", `Top-up ${id} isn't paid yet.`, `Not paid yet. If your human doesn't have the approval_url (below) yet, end your turn now and give it to them. Otherwise tell them it's still unpaid and run \`${WAIT_COMMAND} ${id}\` again once they say they've paid. Don't create a new top-up.`, { topup_id: id, approval_url: t.approval_url ?? null });
10213
+ throw new CliError("topup_pending", `Top-up ${id} isn't paid yet.`, `Not paid yet. If the checkout page is open in your human's browser (billing topup printed opened_in_browser: true), run \`${WAIT_COMMAND} ${id}\` again right away, for up to ${WAIT_PATIENCE_MIN} minutes in all. If they don't have the approval_url (below), or it's still unpaid after that, end your turn and give it to them, then run it again when they're back. Don't create a new top-up.`, { topup_id: id, approval_url: t.approval_url ?? null });
9641
10214
  await new Promise((r) => setTimeout(r, o.pollMs ?? 3000));
9642
10215
  }
9643
10216
  }
@@ -9649,7 +10222,7 @@ async function billingTopupAndWait(ctx, o) {
9649
10222
  let t = created;
9650
10223
  while (!TOPUP_DONE.has(t.status ?? "")) {
9651
10224
  if (Date.now() > deadline)
9652
- throw new CliError("topup_wait_timeout", `Top-up ${id} is still ${t.status}.`, `The link may still work. Check later with: ${NPX} billing status`, { topup_id: id });
10225
+ throw new CliError("topup_wait_timeout", `Top-up ${id} is still ${t.status}.`, `The link may still work. Check later with: ${NPX2} billing status`, { topup_id: id });
9653
10226
  await new Promise((r) => setTimeout(r, o.pollMs ?? 3000));
9654
10227
  t = await api(ctx, "GET", `/v1/billing/topups/${encodeURIComponent(id)}`);
9655
10228
  }
@@ -9660,7 +10233,7 @@ async function billingTopupAndWait(ctx, o) {
9660
10233
  var billingAutoReloadOff = (ctx) => api(ctx, "PATCH", "/v1/billing/auto-reload", { body: { enabled: false } });
9661
10234
  var billingTransactions = (ctx, o = {}) => api(ctx, "GET", `/v1/billing/transactions${o.cursor ? `?cursor=${encodeURIComponent(o.cursor)}` : ""}`);
9662
10235
  function billingUpgradeRemoved() {
9663
- throw new CliError("billing_upgrade_removed", "billing upgrade (monthly caps) was replaced by prepaid credits.", `Run \`${NPX} billing topup --amount-usd 5\` and send your human the approval_url. Optional auto-reload: add --auto-reload-below 2 --auto-reload-amount 10.`);
10236
+ throw new CliError("billing_upgrade_removed", "billing upgrade (monthly caps) was replaced by prepaid credits.", `Run \`${NPX2} billing topup --amount-usd 20\` and send your human the approval_url. They can turn on auto-reload on the same page.`);
9664
10237
  }
9665
10238
  var benchmarks = (ctx, id, region) => api(ctx, "GET", `/v1/models/${encodeURIComponent(id)}/benchmarks`, {
9666
10239
  auth: false,
@@ -9669,10 +10242,10 @@ var benchmarks = (ctx, id, region) => api(ctx, "GET", `/v1/models/${encodeURICom
9669
10242
  async function uploadJsonl(ctx, path, purpose) {
9670
10243
  const { key, region } = resolveKey(ctx);
9671
10244
  const file = resolve2(ctx.cwd, path);
9672
- if (!existsSync2(file))
10245
+ if (!existsSync4(file))
9673
10246
  throw new CliError("file_not_found", `No file at ${path}.`, "Pass the path to a .jsonl file.");
9674
10247
  const form = new FormData;
9675
- form.set("file", new Blob([readFileSync3(file)], { type: "application/jsonl" }), path.split("/").pop() ?? "data.jsonl");
10248
+ form.set("file", new Blob([readFileSync5(file)], { type: "application/jsonl" }), path.split("/").pop() ?? "data.jsonl");
9676
10249
  form.set("purpose", purpose);
9677
10250
  const res = await ctx.fetch(`${controlUrl(region, ctx.env)}/v1/files`, {
9678
10251
  method: "POST",
@@ -9727,9 +10300,9 @@ var VIDEO_WAIT_MS = 30 * 60000;
9727
10300
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9728
10301
  function imageDataUrl(ctx, path) {
9729
10302
  const file = resolve2(ctx.cwd, path);
9730
- if (!existsSync2(file))
10303
+ if (!existsSync4(file))
9731
10304
  throw new CliError("file_not_found", `No file at ${path}.`, "Pass the path to a PNG, JPEG or WebP image.");
9732
- const data = readFileSync3(file);
10305
+ const data = readFileSync5(file);
9733
10306
  if (data.byteLength > 10 * 1024 * 1024)
9734
10307
  throw new CliError("input_reference_too_large", "Images are limited to 10 MiB.");
9735
10308
  const type = data[0] === 255 && data[1] === 216 ? "image/jpeg" : data[0] === 137 && data[1] === 80 ? "image/png" : data.subarray(8, 12).toString() === "WEBP" ? "image/webp" : null;
@@ -9765,7 +10338,7 @@ async function videoDownload(ctx, id, output, variant = "video") {
9765
10338
  throw await responseError(res);
9766
10339
  const data = Buffer.from(await res.arrayBuffer());
9767
10340
  const path = resolve2(ctx.cwd, output);
9768
- writeFileSync(path, data);
10341
+ writeFileSync3(path, data);
9769
10342
  return {
9770
10343
  id,
9771
10344
  variant,
@@ -9870,9 +10443,23 @@ function mcpSetup(ctx) {
9870
10443
  openapi: `${controlUrl(region, ctx.env)}/openapi.json`
9871
10444
  };
9872
10445
  }
10446
+ var DASH_PAGES = ["overview", "billing", "keys", "members", "connect"];
10447
+ async function dashboardOpen(ctx, o = {}) {
10448
+ if (o.page !== undefined && !DASH_PAGES.includes(o.page))
10449
+ throw new CliError("bad_argument", `--page must be one of ${DASH_PAGES.join(", ")}.`, "e.g. --page billing");
10450
+ const r = await api(ctx, "POST", "/v1/dashboard/link", { body: { ...o.page ? { page: o.page } : {}, ...o.email ? { email: o.email } : {} } });
10451
+ const noOpen = o.noOpen || ctx.env.ST_NO_BROWSER === "1";
10452
+ const opened = !noOpen && await (o.open ?? openInBrowser)(r.url);
10453
+ return {
10454
+ ...r,
10455
+ opened_in_browser: opened,
10456
+ message_for_human: opened ? r.message_for_human : r.message_for_human.replace("I've opened your ScaledThought dashboard.", `Your ScaledThought dashboard: ${r.url}`)
10457
+ };
10458
+ }
10459
+ var membersList = (ctx) => api(ctx, "GET", "/v1/members");
9873
10460
 
9874
10461
  // src/decide.ts
9875
- import { readFileSync as readFileSync4 } from "node:fs";
10462
+ import { readFileSync as readFileSync6 } from "node:fs";
9876
10463
  import { resolve as resolve3 } from "node:path";
9877
10464
  var DEFAULT_DECISION_MODEL = "open-jev-27b";
9878
10465
  var usage2 = (msg) => new CliError("usage_error", msg, 'Example: --question urgent:noul:"Is this urgent?" (see decide --help)');
@@ -9944,7 +10531,7 @@ function readState(ctx, o) {
9944
10531
  if (o.stateFile !== undefined) {
9945
10532
  let text;
9946
10533
  try {
9947
- text = readFileSync4(resolve3(ctx.cwd, o.stateFile), "utf8");
10534
+ text = readFileSync6(resolve3(ctx.cwd, o.stateFile), "utf8");
9948
10535
  } catch {
9949
10536
  throw usage2("--state-file could not be read.");
9950
10537
  }
@@ -9961,7 +10548,7 @@ function decisionRequest(ctx, o) {
9961
10548
  throw usage2("--file holds the whole request; don't combine it with --state or --question.");
9962
10549
  let body;
9963
10550
  try {
9964
- body = JSON.parse(readFileSync4(resolve3(ctx.cwd, o.file), "utf8"));
10551
+ body = JSON.parse(readFileSync6(resolve3(ctx.cwd, o.file), "utf8"));
9965
10552
  } catch {
9966
10553
  throw usage2("--file must be a readable JSON file.");
9967
10554
  }
@@ -9995,7 +10582,7 @@ async function decide2(ctx, body) {
9995
10582
  }
9996
10583
 
9997
10584
  // src/realtime.ts
9998
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
10585
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
9999
10586
  import { resolve as resolve4 } from "node:path";
10000
10587
  var SAMPLE_RATE = 24000;
10001
10588
  var MAX_INPUT_SECONDS = 300;
@@ -10060,7 +10647,7 @@ async function realtime(ctx, o) {
10060
10647
  throw new CliError("unsupported_runtime", "This command needs a WebSocket client (Node.js 22 or newer).", "Upgrade Node.js to 22+.");
10061
10648
  let pcm;
10062
10649
  try {
10063
- pcm = readWav(readFileSync5(resolve4(ctx.cwd, o.input)));
10650
+ pcm = readWav(readFileSync7(resolve4(ctx.cwd, o.input)));
10064
10651
  } catch (e) {
10065
10652
  if (e instanceof CliError)
10066
10653
  throw e;
@@ -10152,7 +10739,7 @@ async function realtime(ctx, o) {
10152
10739
  });
10153
10740
  const audio = Buffer.concat(out);
10154
10741
  const path = resolve4(ctx.cwd, o.output);
10155
- writeFileSync2(path, wavOf(audio));
10742
+ writeFileSync4(path, wavOf(audio));
10156
10743
  return {
10157
10744
  model: o.model,
10158
10745
  region,
@@ -10202,7 +10789,7 @@ function num(flag, v) {
10202
10789
  var init2 = defineCommand({
10203
10790
  meta: {
10204
10791
  name: "init",
10205
- description: "Create an account, save ST_API_KEY and ST_BASE_URL to ./.env, list live models and the next steps (a new account has no credits: top up first). Idempotent. " + "Creating an account accepts the Terms of Service (scaledthought.com/legal/terms) for the human you act for."
10792
+ description: "Create an account, save ST_API_KEY and ST_BASE_URL to ./.env, list live models and the next steps (a new account has no credits: top up first). Idempotent. " + "Creating an account accepts nothing for your human: they accept the Terms themselves when they buy credits."
10206
10793
  },
10207
10794
  args: {
10208
10795
  ...jsonFlag,
@@ -10216,6 +10803,19 @@ var init2 = defineCommand({
10216
10803
  "ad-click": {
10217
10804
  type: "string",
10218
10805
  description: "Signed ad-click token, from a prompt ending in `(ad click <token>)` that a human copied on a US campaign's landing page (optional; us region only). Opt out later with `account ad-measurement off`"
10806
+ },
10807
+ account: {
10808
+ type: "string",
10809
+ description: "Use this account saved on this machine (id or name, from choose_account) with a new project key, instead of creating one"
10810
+ },
10811
+ "new-account": {
10812
+ type: "boolean",
10813
+ description: "Create a new account even when one is saved on this machine (separate billing)"
10814
+ },
10815
+ save: {
10816
+ type: "boolean",
10817
+ default: true,
10818
+ description: "Save the account's key in this machine's credential store so other folders reuse it (--no-save to opt out)"
10219
10819
  }
10220
10820
  },
10221
10821
  run: ({ args }) => run(() => init(makeCtx(), {
@@ -10223,9 +10823,70 @@ var init2 = defineCommand({
10223
10823
  name: args.name,
10224
10824
  email: args.email,
10225
10825
  source: args.source,
10226
- adClick: args["ad-click"]
10826
+ adClick: args["ad-click"],
10827
+ account: args.account,
10828
+ newAccount: args["new-account"],
10829
+ noSave: args.save === false
10227
10830
  }), args)
10228
10831
  });
10832
+ var login2 = defineCommand({
10833
+ meta: {
10834
+ name: "login",
10835
+ description: "Connect this folder to an existing account: prints a code your human types on the dashboard (opened in their browser). Then `login --wait` saves a new key to ./.env. `login --code XXXX-XXXX` uses a connect line from the dashboard"
10836
+ },
10837
+ args: {
10838
+ ...jsonFlag,
10839
+ region: { type: "string", description: "The account's region: us (default) or ca" },
10840
+ open: { type: "boolean", default: true, description: "Open the connect page (--no-open only prints it)" },
10841
+ label: { type: "string", description: 'How this agent is named to the human (default "<tool> on <hostname>")' },
10842
+ wait: {
10843
+ type: "boolean",
10844
+ description: "Wait (about 90 s) for your human to approve the code, then save the key. Run again on authorization_pending"
10845
+ },
10846
+ code: {
10847
+ type: "string",
10848
+ description: "A connect line's code from the dashboard (stl_…): already approved, works once. `--code -` reads it from stdin"
10849
+ },
10850
+ force: { type: "boolean", description: "Replace the key already in ./.env" },
10851
+ save: {
10852
+ type: "boolean",
10853
+ default: true,
10854
+ description: "Save the key in this machine's credential store (--no-save to opt out)"
10855
+ },
10856
+ "timeout-s": { type: "string", description: "With --wait: seconds to wait (default 90, at most 600)" }
10857
+ },
10858
+ run: async ({ args }) => {
10859
+ const ctx = makeCtx();
10860
+ const noSave = args.save === false;
10861
+ const t = args["timeout-s"] === undefined ? undefined : num("timeout-s", args["timeout-s"]);
10862
+ if (t !== undefined && t > 600)
10863
+ throw new CliError("bad_argument", `--timeout-s must be at most 600, got ${t}.`, "Run login --wait again.");
10864
+ await run(async () => args.code ? loginWithCode(ctx, {
10865
+ code: args.code === "-" ? await readCodeFrom(process.stdin) : args.code,
10866
+ region: region(args.region),
10867
+ label: args.label,
10868
+ force: args.force,
10869
+ noSave
10870
+ }) : args.wait ? loginWait(ctx, { timeoutS: t, noSave, force: args.force }) : login(ctx, {
10871
+ region: region(args.region),
10872
+ label: args.label,
10873
+ noOpen: args.open === false,
10874
+ force: args.force,
10875
+ noSave
10876
+ }), args);
10877
+ }
10878
+ });
10879
+ var use = defineCommand({
10880
+ meta: {
10881
+ name: "use",
10882
+ description: "Point this folder at another account saved on this machine: a new project key in ./.env, and the folder's old project key revoked"
10883
+ },
10884
+ args: {
10885
+ ...jsonFlag,
10886
+ account: { type: "positional", required: true, description: "The account's name or id" }
10887
+ },
10888
+ run: ({ args }) => run(() => useAccount(makeCtx(), { target: args.account }), args)
10889
+ });
10229
10890
  var models = defineCommand({
10230
10891
  meta: { name: "models", description: "List models with prices and benchmarks, newest first; or show one" },
10231
10892
  args: {
@@ -10504,7 +11165,7 @@ function autoReloadArgs(args) {
10504
11165
  if (below === undefined && amount === undefined && max === undefined)
10505
11166
  return;
10506
11167
  if (below === undefined || amount === undefined)
10507
- throw new CliError("bad_argument", "Auto-reload needs both --auto-reload-below and --auto-reload-amount.", "e.g. --auto-reload-below 2 --auto-reload-amount 10 [--auto-reload-monthly-max 50]");
11168
+ throw new CliError("bad_argument", "Auto-reload needs both --auto-reload-below and --auto-reload-amount.", "e.g. --auto-reload-below 5 --auto-reload-amount 10");
10508
11169
  return { threshold_usd: below, amount_usd: amount, ...max !== undefined ? { monthly_limit_usd: max } : {} };
10509
11170
  }
10510
11171
  var billing = defineCommand({
@@ -10521,20 +11182,22 @@ var billing = defineCommand({
10521
11182
  topup: defineCommand({
10522
11183
  meta: {
10523
11184
  name: "topup",
10524
- description: "Buy prepaid credits: opens the checkout page in your human's browser (approval_url), then run `billing wait`. Optional auto-reload"
11185
+ description: "Buy prepaid credits: opens the funding page in your human's browser (approval_url), where they confirm the amount, can turn on auto-reload (recommended) and pay; then run `billing wait`, which returns once they pay"
10525
11186
  },
10526
11187
  args: {
10527
11188
  ...jsonFlag,
10528
11189
  "amount-usd": {
10529
11190
  type: "string",
10530
- required: true,
10531
- description: "Credits to buy in USD ($5 to $2,000; at most $2,000 per 24 h)"
11191
+ description: "Credits to buy in USD (default 20; $20 to $2,000, at most $2,000 per 24 h). Your human can change it"
11192
+ },
11193
+ "auto-reload-below": {
11194
+ type: "string",
11195
+ description: "Suggest auto-reload (pre-selected on the page) when the balance drops below this (USD, at least 5)"
10532
11196
  },
10533
- "auto-reload-below": { type: "string", description: "Auto-reload when the balance drops below this (USD)" },
10534
- "auto-reload-amount": { type: "string", description: "Amount of each auto-reload (USD)" },
11197
+ "auto-reload-amount": { type: "string", description: "Amount of each suggested auto-reload (USD)" },
10535
11198
  "auto-reload-monthly-max": {
10536
11199
  type: "string",
10537
- description: "Most auto-reload may charge per calendar month (USD, default 5x the amount)"
11200
+ description: "Optional: most auto-reload may charge per calendar month (USD; default: no monthly limit)"
10538
11201
  },
10539
11202
  open: {
10540
11203
  type: "boolean",
@@ -10547,7 +11210,7 @@ var billing = defineCommand({
10547
11210
  }
10548
11211
  },
10549
11212
  run: async ({ args }) => {
10550
- const amountUsd = num("amount-usd", args["amount-usd"]);
11213
+ const amountUsd = num("amount-usd", args["amount-usd"]) ?? 20;
10551
11214
  const autoReload = autoReloadArgs(args);
10552
11215
  checkTopupLimits(amountUsd, autoReload);
10553
11216
  const noOpen = args.open === false;
@@ -10575,8 +11238,35 @@ var billing = defineCommand({
10575
11238
  }
10576
11239
  }),
10577
11240
  "auto-reload": defineCommand({
10578
- meta: { name: "auto-reload", description: "Turn auto-reload off (turning it on or up needs a topup approval)" },
11241
+ meta: {
11242
+ name: "auto-reload",
11243
+ description: "Turn auto-reload on (a link your human approves; nothing is bought) or off (no approval)"
11244
+ },
10579
11245
  subCommands: {
11246
+ setup: defineCommand({
11247
+ meta: {
11248
+ name: "setup",
11249
+ description: "Opens a page in your human's browser where they save a card and turn auto-reload on, buying nothing; then run `billing wait`"
11250
+ },
11251
+ args: {
11252
+ ...jsonFlag,
11253
+ below: {
11254
+ type: "string",
11255
+ description: "Reload when the balance drops below this (USD, default and minimum 5)"
11256
+ },
11257
+ amount: { type: "string", description: "Credits each reload adds (USD, default 20)" },
11258
+ open: {
11259
+ type: "boolean",
11260
+ default: true,
11261
+ description: "Open the page in the browser (--no-open only prints approval_url)"
11262
+ }
11263
+ },
11264
+ run: ({ args }) => run(() => billingAutoReloadSetup(makeCtx(), {
11265
+ thresholdUsd: num("below", args.below),
11266
+ amountUsd: num("amount", args.amount),
11267
+ noOpen: args.open === false
11268
+ }), args)
11269
+ }),
10580
11270
  off: defineCommand({
10581
11271
  meta: { name: "off", description: "Disable auto-reload. No approval needed" },
10582
11272
  args: jsonFlag,
@@ -10802,6 +11492,31 @@ var mcp = defineCommand({
10802
11492
  args: jsonFlag,
10803
11493
  run: ({ args }) => run(() => mcpSetup(makeCtx()), args)
10804
11494
  });
11495
+ var dashboard = defineCommand({
11496
+ meta: {
11497
+ name: "dashboard",
11498
+ description: "Open your human's ScaledThought dashboard (billing, keys, members, connect an agent) in their browser. No typing: if they're not signed in, one click emails them a link"
11499
+ },
11500
+ args: {
11501
+ ...jsonFlag,
11502
+ page: { type: "string", description: "overview (default), billing, keys, members or connect" },
11503
+ email: {
11504
+ type: "string",
11505
+ description: "Set the owner's email (only before anyone has signed in; normally the first payer is the owner)"
11506
+ },
11507
+ open: { type: "boolean", default: true, description: "Open it in the browser (--no-open only prints the URL)" }
11508
+ },
11509
+ run: ({ args }) => run(() => dashboardOpen(makeCtx(), {
11510
+ page: args.page,
11511
+ email: args.email,
11512
+ noOpen: args.open === false
11513
+ }), args)
11514
+ });
11515
+ var members2 = defineCommand({
11516
+ meta: { name: "members", description: "Who can sign in to this account's dashboard (email, role, status)" },
11517
+ args: jsonFlag,
11518
+ run: ({ args }) => run(() => membersList(makeCtx()), args)
11519
+ });
10805
11520
  var root = defineCommand({
10806
11521
  meta: {
10807
11522
  name: "scaledthought",
@@ -10810,6 +11525,8 @@ var root = defineCommand({
10810
11525
  },
10811
11526
  subCommands: {
10812
11527
  init: init2,
11528
+ login: login2,
11529
+ use,
10813
11530
  models,
10814
11531
  benchmarks: benchmarks2,
10815
11532
  status: status2,
@@ -10827,7 +11544,9 @@ var root = defineCommand({
10827
11544
  video,
10828
11545
  webhooks,
10829
11546
  doctor: doctor2,
10830
- mcp
11547
+ mcp,
11548
+ dashboard,
11549
+ members: members2
10831
11550
  }
10832
11551
  });
10833
11552
  var NUMERIC_FLAGS = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scaledthought",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Headless open-model inference, run by your agent. Prepaid, pay per token. US or Canada.",
5
5
  "license": "MIT",
6
6
  "keywords": [