@sleds/cli 0.1.7 → 0.1.11

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 +1 -1
  2. package/dist/sleds.mjs +160 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@ SLEDS sits in front of Claude Code. It meters what you spend, sizes a job before
12
12
  npx @sleds/cli init --signup
13
13
  ```
14
14
 
15
- No org? That's the whole setup: your email plus your own Anthropic API key. SLEDS checks the key, stores it encrypted, and it's the only key your seat ever spends. If your team already runs SLEDS, use the seat token your admin gave you instead:
15
+ No org? `--signup` asks: just you, or your team. Just you is your email plus your own Anthropic API key. SLEDS checks the key, stores it encrypted, and it's the only key your seat ever spends. Your team is a work email and a team name — you become its owner; a platform key is optional (a Team plan without one still gets seat tracking, hold and hand-off at a wall). If your team already runs SLEDS, use the seat token your admin gave you instead:
16
16
 
17
17
  ```bash
18
18
  npx @sleds/cli init --token sled_…
package/dist/sleds.mjs CHANGED
@@ -305,8 +305,36 @@ var parseConfig = (raw) => {
305
305
  cfg.schedule = schedule;
306
306
  if (raw.priorStatusLine !== undefined)
307
307
  cfg.priorStatusLine = raw.priorStatusLine;
308
+ if (raw.wiring === "seat-first" || raw.wiring === "proxy-always")
309
+ cfg.wiring = raw.wiring;
310
+ const org = parseOrgInfo(raw.org);
311
+ if (org)
312
+ cfg.org = org;
308
313
  return cfg;
309
314
  };
315
+ var parseOrgInfo = (raw) => {
316
+ if (!isObject2(raw) || !isObject2(raw.capabilities))
317
+ return;
318
+ const name = optString(raw.name);
319
+ const slug = optString(raw.slug);
320
+ const plan = raw.plan;
321
+ const c = raw.capabilities;
322
+ const src = c.upstreamSource;
323
+ if (name === undefined || slug === undefined || plan !== "team" && plan !== "enterprise" && plan !== "api" && plan !== "individual" || src !== "own" && src !== "shared" && src !== "none")
324
+ return;
325
+ return {
326
+ name,
327
+ slug,
328
+ plan,
329
+ capabilities: {
330
+ upstreamKey: c.upstreamKey === true,
331
+ adminKey: c.adminKey === true,
332
+ seats: c.seats !== false,
333
+ upstreamSource: src
334
+ },
335
+ mode: raw.mode === "proxy-always" ? "proxy-always" : "seat-first"
336
+ };
337
+ };
310
338
  var readConfig = (home) => {
311
339
  const p = configPath(home);
312
340
  if (!existsSync2(p))
@@ -830,6 +858,8 @@ var postJson = async (o, path, body) => {
830
858
  var postSeatWindow = (o, w) => postJson(o, "/proxy/seat", seatWindowBody(w));
831
859
  var postSeatWall = (o, input) => postJson(o, "/proxy/seat-wall", input);
832
860
  var SEAT_WALL_LINE = "Seat limit hit. sleds on --until-reset to continue on the org key.";
861
+ var SEAT_WALL_LINE_NO_KEY = "Seat limit hit. Hold until the reset, or /sled 5 to hand off — this org has no platform key to switch to.";
862
+ var seatWallLine = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE : SEAT_WALL_LINE_NO_KEY;
833
863
  var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) => {
834
864
  if (stdin.isTTY) {
835
865
  resolve3("");
@@ -1350,8 +1380,11 @@ When the proxy is active it answers a walled request with its options. The user
1350
1380
  /sled 3 run overnight at half price
1351
1381
  /sled 4 switch to a cheaper model for the rest of the session (offered when one saves ≥15%)
1352
1382
  /sled 5 hand off to an idle coworker, or park for the team (offered on a team of two or more)
1383
+ /sled 6 defer to reset: the job runs itself when the seat resets — same model, done in minutes, full price (offered when the wall has a reset and the org has a key)
1353
1384
  \`\`\`
1354
1385
 
1386
+ A hand-off carries a brief (the packet): what, note, cost so far, how to pick it up. The \`handoffs\` MCP tool and the dashboard's Jobs page show it; read it before taking one.
1387
+
1355
1388
  ## Window reset
1356
1389
 
1357
1390
  When the user mentions their window has reset, or the deadline shown by \`sleds status\` has passed:
@@ -1364,7 +1397,7 @@ then restart \`claude\`. Settings go back to exactly what they were.
1364
1397
 
1365
1398
  ## Check
1366
1399
 
1367
- \`sleds status\` — on/off, since, reverts at, proxy health, wall mode.
1400
+ \`sleds status\` — on/off, since, reverts at, proxy health, wall mode, and the org's shape (\`Org:\` / \`Wiring:\`). An org with no platform key runs seat-first and its walls offer hold (\`/sled 2\`) and hand-off (\`/sled 5\`) only — continue, overnight and switch need a key an admin adds on the dashboard.
1368
1401
 
1369
1402
  The status line's \`seat wk 52% · 5h 12%\` is the user's own Pro/Max window as Claude Code reports it (weekly, then 5-hour); \`window\` is the org key's headroom through the proxy. At \`wk 100%\` the answer is \`sleds on --until-reset\`.
1370
1403
 
@@ -1472,11 +1505,17 @@ var promptStdin = (question) => new Promise((resolve6) => {
1472
1505
  });
1473
1506
  });
1474
1507
  async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
1508
+ return signupRequest(proxyUrl, { email, apiKey: credential }, fetchFn);
1509
+ }
1510
+ async function signupTeam(proxyUrl, email, team, credential, fetchFn = fetch) {
1511
+ return signupRequest(proxyUrl, { email, team, ...credential ? { apiKey: credential } : {} }, fetchFn);
1512
+ }
1513
+ async function signupRequest(proxyUrl, payload, fetchFn) {
1475
1514
  try {
1476
1515
  const res = await fetchFn(`${proxyUrl}/signup`, {
1477
1516
  method: "POST",
1478
1517
  headers: { "content-type": "application/json" },
1479
- body: JSON.stringify({ email, apiKey: credential }),
1518
+ body: JSON.stringify(payload),
1480
1519
  signal: AbortSignal.timeout(20000)
1481
1520
  });
1482
1521
  const body = await res.json().catch(() => ({}));
@@ -1497,6 +1536,9 @@ var SIGNUP_REASONS = {
1497
1536
  key_rejected: "Anthropic did not accept that API key. Check it at console.anthropic.com → API keys.",
1498
1537
  rate_limited: "too many signups from this network in the last hour. Try again later.",
1499
1538
  not_configured: "the proxy is not set up for self-serve signup yet.",
1539
+ team: "the team needs a name (2–80 characters).",
1540
+ personal_email: "a team needs a work email — that domain is a personal mailbox. Sign up as an individual instead, or use your company address.",
1541
+ org_exists: "someone at your company already set up SLEDS. Ask them for a seat (Members → invite, Admin → Mint a seat) and run `sleds init --token …`.",
1500
1542
  unavailable: "the proxy could not reach its control plane. Try again in a moment.",
1501
1543
  unreachable: "could not reach the proxy."
1502
1544
  };
@@ -1522,6 +1564,20 @@ function normalizeProxyUrl(raw) {
1522
1564
  return null;
1523
1565
  }
1524
1566
  }
1567
+ async function fetchOrgInfo(url, token, fetchFn = fetch) {
1568
+ try {
1569
+ const res = await fetchFn(`${url}/proxy/meter`, {
1570
+ headers: { authorization: `Bearer ${token}` },
1571
+ signal: AbortSignal.timeout(1e4)
1572
+ });
1573
+ if (!res.ok)
1574
+ return null;
1575
+ const body = await res.json();
1576
+ return body.org?.capabilities ? body.org : null;
1577
+ } catch {
1578
+ return null;
1579
+ }
1580
+ }
1525
1581
  async function probeProxy(url, fetchFn = fetch, opts = {}) {
1526
1582
  const tries = opts.tries ?? 2;
1527
1583
  const timeoutMs = opts.timeoutMs ?? 1e4;
@@ -1553,18 +1609,40 @@ var resolveToken = async (o) => {
1553
1609
  const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL);
1554
1610
  if (!url)
1555
1611
  return null;
1556
- const email = (await o.prompt("Your email: ")).trim();
1612
+ const log = o.log ?? console.log;
1613
+ const err = o.error ?? console.error;
1614
+ let team = o.team?.trim() || null;
1615
+ if (team === null && !o.email) {
1616
+ const which = (await o.prompt("Set up SLEDS for (1) just you, or (2) your team? [1/2]: ")).trim();
1617
+ if (which === "2") {
1618
+ team = (await o.prompt("Team name (e.g. your company): ")).trim();
1619
+ if (!team)
1620
+ return null;
1621
+ }
1622
+ }
1623
+ const email = (o.email ?? await o.prompt(team ? "Your work email: " : "Your email: ")).trim();
1557
1624
  if (!email.includes("@"))
1558
1625
  return null;
1626
+ if (team) {
1627
+ const credential2 = (await o.prompt("Your organization's Anthropic API key, if you have one (sk-ant-…; press enter to skip — a Team plan without one still works, walls offer hold and hand-off): ")).trim();
1628
+ const r2 = await signupTeam(url, email, team, credential2 || null, o.fetchFn);
1629
+ if (!r2.ok) {
1630
+ err(signupFailureText(r2));
1631
+ return null;
1632
+ }
1633
+ log(`Created ${team} with you as owner (${email}).${credential2 ? " Your platform key is stored encrypted as the org's upstream key." : " No platform key yet — add one on the dashboard (Admin → Keys) to unlock continue / overnight / switch at a wall."}`);
1634
+ log("Invite your team on the dashboard (Members → invite by email, Admin → Mint a seat); each person runs `sleds init --token …`.");
1635
+ return { token: r2.token, source: "signup" };
1636
+ }
1559
1637
  const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
1560
1638
  if (!credential)
1561
1639
  return null;
1562
1640
  const r = await signupIndividual(url, email, credential, o.fetchFn);
1563
1641
  if (!r.ok) {
1564
- (o.error ?? console.error)(signupFailureText(r));
1642
+ err(signupFailureText(r));
1565
1643
  return null;
1566
1644
  }
1567
- (o.log ?? console.log)(r.created ? `Created your individual seat (${email}). Your key is stored encrypted and is the only key this seat spends.` : `Re-issued your individual seat (${email}); the previous token is revoked.`);
1645
+ log(r.created ? `Created your individual seat (${email}). Your key is stored encrypted and is the only key this seat spends.` : `Re-issued your individual seat (${email}); the previous token is revoked.`);
1568
1646
  return { token: r.token, source: "signup" };
1569
1647
  };
1570
1648
  var init = async (o) => {
@@ -1594,12 +1672,19 @@ var init = async (o) => {
1594
1672
  return 2;
1595
1673
  }
1596
1674
  const wrote = [];
1675
+ const org = await fetchOrgInfo(url, resolved.token, o.fetchFn);
1676
+ const wiring = o.proxyAlways ? "proxy-always" : o.seatFirst ? "seat-first" : org?.mode ?? "proxy-always";
1677
+ if (wiring === "proxy-always" && org && !org.capabilities.upstreamKey) {
1678
+ error(`${org.name} has no platform key for the proxy to forward with; --proxy-always would break every request. Using seat-first.`);
1679
+ }
1680
+ const finalWiring = wiring === "proxy-always" && org && !org.capabilities.upstreamKey ? "seat-first" : wiring;
1597
1681
  if (backupSettings(o.home))
1598
1682
  wrote.push(`backed up settings to ${backupPath(o.home)}`);
1599
- const withProxy = applyProxy(readSettings(o.home), { url, token: resolved.token });
1600
- const statusLine = applyStatusLine(withProxy, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
1683
+ const current = readSettings(o.home);
1684
+ const wired = finalWiring === "proxy-always" ? applyProxy(current, { url, token: resolved.token }) : removeProxy(current);
1685
+ const statusLine = applyStatusLine(wired, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
1601
1686
  writeSettings(o.home, statusLine.settings);
1602
- wrote.push(`set env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN in ${settingsPath(o.home)}`);
1687
+ wrote.push(finalWiring === "proxy-always" ? `set env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN in ${settingsPath(o.home)} (everything through the proxy)` : `left Claude Code on your seat (no ANTHROPIC_BASE_URL in ${settingsPath(o.home)}); \`sleds on\` switches to the proxy at a wall`);
1603
1688
  wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
1604
1689
  hooks2({
1605
1690
  home: o.home,
@@ -1619,20 +1704,37 @@ var init = async (o) => {
1619
1704
  ...prior,
1620
1705
  token: resolved.token,
1621
1706
  proxyUrl: url,
1707
+ wiring: finalWiring,
1708
+ ...org ? { org } : {},
1622
1709
  ...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
1623
1710
  });
1624
1711
  wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
1625
1712
  mkdirSync6(skillDir(o.home), { recursive: true });
1626
1713
  writeFileSync6(skillPath(o.home), SKILL_MD);
1627
1714
  wrote.push(`installed skill at ${skillPath(o.home)}`);
1628
- log("Claude Code now runs through SLEDS on the org key.");
1629
- log("");
1630
- for (const w of wrote)
1631
- log(` ${w}`);
1632
- log("");
1633
- log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
1634
- log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
1635
- log("Prefer to burn your own Pro/Max/Team seat first? `sleds off` puts Claude Code back on it; `sleds on --until-reset +5h` switches to the org key at a wall.");
1715
+ const orgLine = org ? `${org.name} · ${org.plan}${org.capabilities.upstreamKey ? ` · ${org.capabilities.upstreamSource === "own" ? "org key" : "shared key"}` : " · no platform key"}` : "org unknown (proxy did not answer /proxy/meter)";
1716
+ if (finalWiring === "proxy-always") {
1717
+ log(`Claude Code now runs through SLEDS on the org key. (${orgLine})`);
1718
+ log("");
1719
+ for (const w of wrote)
1720
+ log(` ${w}`);
1721
+ log("");
1722
+ log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
1723
+ log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
1724
+ log("Prefer your seat first? `sleds init --seat-first` leaves Claude Code on it and switches only at a wall.");
1725
+ } else {
1726
+ log(`SLEDS is watching your seat. Nothing changes until a wall. (${orgLine})`);
1727
+ log("");
1728
+ for (const w of wrote)
1729
+ log(` ${w}`);
1730
+ log("");
1731
+ log("Claude Code keeps running on your seat; the status line reports its quota to SLEDS on every turn.");
1732
+ if (org && !org.capabilities.upstreamKey) {
1733
+ log("This org has no platform key, so at a wall the choices are hold until reset or hand off to a teammate. An admin can add a key on the dashboard to unlock continue / overnight / switch.");
1734
+ } else {
1735
+ log("At a wall: `sleds on --until-reset +5h` (or /sled 1) switches to the org key through the proxy until the reset; `sleds off` puts you back.");
1736
+ }
1737
+ }
1636
1738
  return 0;
1637
1739
  };
1638
1740
 
@@ -1992,8 +2094,17 @@ async function handoffsTool(deps) {
1992
2094
  if (handoffs.length === 0)
1993
2095
  return { text: "No open hand-offs on your team. Park one with the handoff tool." };
1994
2096
  const now = (deps.now ?? Date.now)();
1995
- return { text: handoffLines(handoffs, now).slice(1).join(`
1996
- `) };
2097
+ const packets = handoffs.map((h) => str2(h.packet)).filter((p) => Boolean(p)).join(`
2098
+
2099
+ ---
2100
+
2101
+ `);
2102
+ return {
2103
+ text: [handoffLines(handoffs, now).slice(1).join(`
2104
+ `), packets ? `
2105
+ ${packets}` : ""].join(`
2106
+ `)
2107
+ };
1997
2108
  }
1998
2109
  async function handoffTool(deps, args) {
1999
2110
  if (!meterSource(deps.home))
@@ -2312,6 +2423,10 @@ var on = (o) => {
2312
2423
  error("Saved token is not a sled_ token. Run `sleds init --token <sled_…>`.");
2313
2424
  return 2;
2314
2425
  }
2426
+ if (cfg.org && !cfg.org.capabilities.upstreamKey) {
2427
+ error(`${cfg.org.name} has no platform key, so there is no org key to switch to. Hold until the reset, or hand off with /sled 5. An admin can add a key on the dashboard.`);
2428
+ return 2;
2429
+ }
2315
2430
  const url = (o.url ?? cfg.proxyUrl ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL2).replace(/\/+$/, "");
2316
2431
  let untilResetMs;
2317
2432
  if (o.untilReset !== undefined) {
@@ -2407,10 +2522,11 @@ var seatEvent = async (o) => {
2407
2522
  if (week && week.resetsAt > now)
2408
2523
  writeSeatState(o.home, { ...state, lastWallResetsAt: week.resetsAt });
2409
2524
  } catch {}
2410
- log(SEAT_WALL_LINE);
2525
+ const line = seatWallLine(readConfig(o.home).org?.capabilities.upstreamKey !== false);
2526
+ log(line);
2411
2527
  if ((o.platform ?? process.platform) === "darwin" && o.exec) {
2412
2528
  try {
2413
- o.exec("osascript", osascriptArgs(NOTIFICATION_TITLE, SEAT_WALL_LINE));
2529
+ o.exec("osascript", osascriptArgs(NOTIFICATION_TITLE, line));
2414
2530
  } catch {}
2415
2531
  }
2416
2532
  return 0;
@@ -3107,6 +3223,10 @@ var formatStatus = (s) => {
3107
3223
  }
3108
3224
  if (s.email)
3109
3225
  lines.push(row2("Seat:", s.email));
3226
+ if (s.org)
3227
+ lines.push(row2("Org:", `${s.org.name} · ${s.org.plan} · ${s.org.upstream === "none" ? "no platform key (walls: hold or hand off)" : s.org.upstream === "own" ? "org key" : "shared key"}`));
3228
+ if (s.wiring)
3229
+ lines.push(row2("Wiring:", s.wiring === "seat-first" ? "seat-first — Claude Code on your seat; the proxy only after `sleds on`" : "proxy-always — every request through the proxy"));
3110
3230
  if (s.mode === "off") {
3111
3231
  lines.push(s.pointed ? "Claude Code is pointed at the proxy by settings.json (set by `sleds init`)." : "Claude Code is on your seat. Hit a limit? `sleds on --until-reset +5h`.");
3112
3232
  }
@@ -3138,6 +3258,14 @@ var status = async (o) => {
3138
3258
  info.tokenMasked = maskToken(token);
3139
3259
  if (cfg.email)
3140
3260
  info.email = cfg.email;
3261
+ if (cfg.wiring)
3262
+ info.wiring = cfg.wiring;
3263
+ if (cfg.org)
3264
+ info.org = {
3265
+ name: cfg.org.name,
3266
+ plan: cfg.org.plan,
3267
+ upstream: cfg.org.capabilities.upstreamSource
3268
+ };
3141
3269
  if (cfg.reason)
3142
3270
  info.reason = cfg.reason;
3143
3271
  if (o.tz)
@@ -3294,9 +3422,14 @@ commands:
3294
3422
  --token <sled_\u2026> seat token (else SLEDS_TOKEN, ~/.sleds-token, or prompt)
3295
3423
  --url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
3296
3424
  --no-probe skip the /healthz check of the proxy URL
3425
+ --seat-first keep Claude Code on your seat; the proxy only at a wall (default for seat plans)
3426
+ --proxy-always every request through the proxy on the org key (default for API-plan orgs)
3297
3427
  --non-interactive never prompt; exit 2 if no token is available
3298
- --signup no org? create an individual seat: your email + your own
3299
- Anthropic API key (validated, stored encrypted, the only key it spends)
3428
+ --signup no org? create one: (1) just you \u2014 your email + your own Anthropic
3429
+ API key (validated, stored encrypted, the only key it spends), or
3430
+ (2) your team \u2014 work email + team name, platform key optional
3431
+ --team "<name>" with --signup: create the team org (you become its owner)
3432
+ --email <you@work.com> with --signup: skip the email prompt
3300
3433
  on Hit a limit on your seat? Switch Claude Code to the org key through the proxy.
3301
3434
  --until-reset <when> revert at this time: +5h, +90m, or an ISO timestamp
3302
3435
  --reason "<text>" note shown by \`sleds status\`
@@ -3384,7 +3517,11 @@ var main = async (argv) => {
3384
3517
  url: stringFlag(flags, "url"),
3385
3518
  nonInteractive: flags["non-interactive"] === true,
3386
3519
  signup: flags.signup === true,
3520
+ team: typeof flags.team === "string" ? flags.team : undefined,
3521
+ email: typeof flags.email === "string" ? flags.email : undefined,
3387
3522
  noProbe: flags["no-probe"] === true,
3523
+ proxyAlways: flags["proxy-always"] === true,
3524
+ seatFirst: flags["seat-first"] === true,
3388
3525
  prompt: promptStdin,
3389
3526
  statusLineCommand: statusLineCommand(sledsArgv()),
3390
3527
  seatEventCommand: seatEventCommand(sledsArgv()),
@@ -3435,7 +3572,7 @@ var main = async (argv) => {
3435
3572
  case "version":
3436
3573
  case "--version":
3437
3574
  case "-v":
3438
- console.log(`sleds ${"0.1.7"}`);
3575
+ console.log(`sleds ${"0.1.11"}`);
3439
3576
  return 0;
3440
3577
  default:
3441
3578
  console.error(`unknown command: ${cmd}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.11",
4
4
  "description": "SLEDS CLI — the cost and limits of AI agent work, decided at the wall. Point Claude Code at a SLEDS proxy; estimate before you run; hold, switch model, or run overnight when you hit a limit.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",