@sleds/cli 0.1.6 → 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.
- package/README.md +3 -1
- package/dist/sleds.mjs +225 -37
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @sleds/cli
|
|
2
2
|
|
|
3
|
+
> Private while SLEDS is in stealth: the package is published with restricted access, so `npm i -g @sleds/cli` / `npx @sleds/cli` need an `npm login` that belongs to the `sleds` org. Public release flips `publishConfig.access` back to `public`.
|
|
4
|
+
|
|
3
5
|
The cost and limits of AI agent work, decided at the wall.
|
|
4
6
|
|
|
5
7
|
SLEDS sits in front of Claude Code. It meters what you spend, sizes a job before it runs, and when you hit a limit — a rate limit, a weekly cap, exhausted credits — it gives you a choice right there in the terminal: continue on the org key, hold until the window resets, switch to a cheaper model, or run it overnight at half price.
|
|
@@ -10,7 +12,7 @@ SLEDS sits in front of Claude Code. It meters what you spend, sizes a job before
|
|
|
10
12
|
npx @sleds/cli init --signup
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
No org?
|
|
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:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
npx @sleds/cli init --token sled_…
|
package/dist/sleds.mjs
CHANGED
|
@@ -41,6 +41,38 @@ var claudeDir = (home, env = process.env) => {
|
|
|
41
41
|
};
|
|
42
42
|
var hasHomeOverride = (env = process.env) => Boolean(env.SLEDS_HOME?.trim() || env.CLAUDE_CONFIG_DIR?.trim());
|
|
43
43
|
var settingsPath = (home) => join(claudeDir(home), "settings.json");
|
|
44
|
+
var sledsCommand = (sub, home, env = process.env) => {
|
|
45
|
+
const parts = [];
|
|
46
|
+
const sh = env.SLEDS_HOME?.trim();
|
|
47
|
+
const cc = env.CLAUDE_CONFIG_DIR?.trim();
|
|
48
|
+
if (sh)
|
|
49
|
+
parts.push(`SLEDS_HOME=${shellWord(resolve(home, sh))}`);
|
|
50
|
+
if (cc)
|
|
51
|
+
parts.push(`CLAUDE_CONFIG_DIR=${shellWord(resolve(home, cc))}`);
|
|
52
|
+
parts.push(`sleds ${sub}`);
|
|
53
|
+
return parts.join(" ");
|
|
54
|
+
};
|
|
55
|
+
var sledsCommandsIn = (settings) => {
|
|
56
|
+
const out = [];
|
|
57
|
+
const visit = (v) => {
|
|
58
|
+
if (Array.isArray(v)) {
|
|
59
|
+
for (const x of v)
|
|
60
|
+
visit(x);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!isObject(v))
|
|
64
|
+
return;
|
|
65
|
+
if (typeof v.command === "string" && /\bsleds\b/.test(v.command))
|
|
66
|
+
out.push(v.command);
|
|
67
|
+
for (const x of Object.values(v))
|
|
68
|
+
visit(x);
|
|
69
|
+
};
|
|
70
|
+
visit(settings[STATUS_LINE_KEY]);
|
|
71
|
+
visit(settings.hooks);
|
|
72
|
+
return out;
|
|
73
|
+
};
|
|
74
|
+
var bakedHome = (command) => command.match(/(?:^|\s)SLEDS_HOME=('([^']*)'|(\S+))/)?.[2] ?? command.match(/(?:^|\s)SLEDS_HOME=(\S+)/)?.[1];
|
|
75
|
+
var shellWord = (v) => /^[\w./~-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
|
|
44
76
|
var backupPath = (home) => join(claudeDir(home), "settings.json.sleds-backup");
|
|
45
77
|
var tokenPath = (home, env = process.env) => {
|
|
46
78
|
const o = env.SLEDS_HOME?.trim();
|
|
@@ -273,8 +305,36 @@ var parseConfig = (raw) => {
|
|
|
273
305
|
cfg.schedule = schedule;
|
|
274
306
|
if (raw.priorStatusLine !== undefined)
|
|
275
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;
|
|
276
313
|
return cfg;
|
|
277
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
|
+
};
|
|
278
338
|
var readConfig = (home) => {
|
|
279
339
|
const p = configPath(home);
|
|
280
340
|
if (!existsSync2(p))
|
|
@@ -798,6 +858,8 @@ var postJson = async (o, path, body) => {
|
|
|
798
858
|
var postSeatWindow = (o, w) => postJson(o, "/proxy/seat", seatWindowBody(w));
|
|
799
859
|
var postSeatWall = (o, input) => postJson(o, "/proxy/seat-wall", input);
|
|
800
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;
|
|
801
863
|
var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) => {
|
|
802
864
|
if (stdin.isTTY) {
|
|
803
865
|
resolve3("");
|
|
@@ -934,7 +996,7 @@ var isDanglingLink = (p) => {
|
|
|
934
996
|
// src/commands/doctor.ts
|
|
935
997
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
936
998
|
import { existsSync as existsSync6, lstatSync as lstatSync2, readFileSync as readFileSync5, readdirSync, readlinkSync } from "node:fs";
|
|
937
|
-
import { join as join7 } from "node:path";
|
|
999
|
+
import { join as join7, resolve as resolve5 } from "node:path";
|
|
938
1000
|
|
|
939
1001
|
// src/commands/bar.ts
|
|
940
1002
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -1164,6 +1226,20 @@ async function runDoctor(o) {
|
|
|
1164
1226
|
detail: `seat-event ${seat2 ? "on" : "missing"} · spec-event ${spec ? "on" : "missing"}`,
|
|
1165
1227
|
fix: seat2 && spec ? undefined : "sleds hooks"
|
|
1166
1228
|
});
|
|
1229
|
+
const want = env.SLEDS_HOME?.trim() ? resolve5(home, env.SLEDS_HOME.trim()) : undefined;
|
|
1230
|
+
const cmds = sledsCommandsIn(settings);
|
|
1231
|
+
const wrong = cmds.filter((c) => {
|
|
1232
|
+
const got = bakedHome(c);
|
|
1233
|
+
return want ? got !== want : got !== undefined;
|
|
1234
|
+
});
|
|
1235
|
+
if (cmds.length > 0) {
|
|
1236
|
+
checks.push({
|
|
1237
|
+
level: wrong.length === 0 ? "ok" : "fail",
|
|
1238
|
+
name: "commands",
|
|
1239
|
+
detail: wrong.length === 0 ? `${cmds.length} command${cmds.length === 1 ? "" : "s"} in settings.json ${want ? `carry SLEDS_HOME=${want}` : "run the default seat"}` : `${wrong.length} of ${cmds.length} commands in settings.json ${want ? `do not carry SLEDS_HOME=${want}` : "carry another seat's SLEDS_HOME"} — their reads land on the wrong seat`,
|
|
1240
|
+
fix: wrong.length === 0 ? undefined : `${overrides.length ? `${overrides.join(" ")} ` : ""}sleds hooks (re-writes the status line + hooks with this seat baked in)`
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1167
1243
|
const skill = existsSync6(skillPath(home));
|
|
1168
1244
|
checks.push({
|
|
1169
1245
|
level: skill ? "ok" : "warn",
|
|
@@ -1249,8 +1325,6 @@ var doctor = async (o) => {
|
|
|
1249
1325
|
};
|
|
1250
1326
|
|
|
1251
1327
|
// src/commands/hooks.ts
|
|
1252
|
-
var DEFAULT_SEAT_EVENT_COMMAND = "sleds seat-event";
|
|
1253
|
-
var DEFAULT_SPEC_EVENT_COMMAND = "sleds spec-event";
|
|
1254
1328
|
var hooks = (o) => {
|
|
1255
1329
|
const log = o.log ?? console.log;
|
|
1256
1330
|
const error = o.error ?? console.error;
|
|
@@ -1261,9 +1335,13 @@ var hooks = (o) => {
|
|
|
1261
1335
|
const settings = readSettings(o.home);
|
|
1262
1336
|
const had = hasSeatHook(settings);
|
|
1263
1337
|
const hadSpec = hasSpecHook(settings);
|
|
1264
|
-
const withSeat = applySeatHook(settings, o.seatEventCommand ??
|
|
1265
|
-
|
|
1338
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
1339
|
+
const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
|
|
1340
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
1341
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
1266
1342
|
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
1343
|
+
if (hadLine)
|
|
1344
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
1267
1345
|
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
1268
1346
|
return 0;
|
|
1269
1347
|
};
|
|
@@ -1302,8 +1380,11 @@ When the proxy is active it answers a walled request with its options. The user
|
|
|
1302
1380
|
/sled 3 run overnight at half price
|
|
1303
1381
|
/sled 4 switch to a cheaper model for the rest of the session (offered when one saves ≥15%)
|
|
1304
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)
|
|
1305
1384
|
\`\`\`
|
|
1306
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
|
+
|
|
1307
1388
|
## Window reset
|
|
1308
1389
|
|
|
1309
1390
|
When the user mentions their window has reset, or the deadline shown by \`sleds status\` has passed:
|
|
@@ -1316,7 +1397,7 @@ then restart \`claude\`. Settings go back to exactly what they were.
|
|
|
1316
1397
|
|
|
1317
1398
|
## Check
|
|
1318
1399
|
|
|
1319
|
-
\`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.
|
|
1320
1401
|
|
|
1321
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\`.
|
|
1322
1403
|
|
|
@@ -1393,8 +1474,6 @@ From Cowork or any MCP client (\`sleds mcp\`): the \`handoffs\` tool lists what
|
|
|
1393
1474
|
`;
|
|
1394
1475
|
|
|
1395
1476
|
// src/commands/hooks.ts
|
|
1396
|
-
var DEFAULT_SEAT_EVENT_COMMAND2 = "sleds seat-event";
|
|
1397
|
-
var DEFAULT_SPEC_EVENT_COMMAND2 = "sleds spec-event";
|
|
1398
1477
|
var hooks2 = (o) => {
|
|
1399
1478
|
const log = o.log ?? console.log;
|
|
1400
1479
|
const error = o.error ?? console.error;
|
|
@@ -1405,29 +1484,38 @@ var hooks2 = (o) => {
|
|
|
1405
1484
|
const settings = readSettings(o.home);
|
|
1406
1485
|
const had = hasSeatHook(settings);
|
|
1407
1486
|
const hadSpec = hasSpecHook(settings);
|
|
1408
|
-
const withSeat = applySeatHook(settings, o.seatEventCommand ??
|
|
1409
|
-
|
|
1487
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
1488
|
+
const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
|
|
1489
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
1490
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
1410
1491
|
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
1492
|
+
if (hadLine)
|
|
1493
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
1411
1494
|
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
1412
1495
|
return 0;
|
|
1413
1496
|
};
|
|
1414
1497
|
|
|
1415
1498
|
// src/commands/init.ts
|
|
1416
1499
|
var DEFAULT_PROXY_URL = "http://localhost:8787";
|
|
1417
|
-
var
|
|
1418
|
-
var promptStdin = (question) => new Promise((resolve5) => {
|
|
1500
|
+
var promptStdin = (question) => new Promise((resolve6) => {
|
|
1419
1501
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1420
1502
|
rl.question(question, (answer) => {
|
|
1421
1503
|
rl.close();
|
|
1422
|
-
|
|
1504
|
+
resolve6(answer.trim());
|
|
1423
1505
|
});
|
|
1424
1506
|
});
|
|
1425
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) {
|
|
1426
1514
|
try {
|
|
1427
1515
|
const res = await fetchFn(`${proxyUrl}/signup`, {
|
|
1428
1516
|
method: "POST",
|
|
1429
1517
|
headers: { "content-type": "application/json" },
|
|
1430
|
-
body: JSON.stringify(
|
|
1518
|
+
body: JSON.stringify(payload),
|
|
1431
1519
|
signal: AbortSignal.timeout(20000)
|
|
1432
1520
|
});
|
|
1433
1521
|
const body = await res.json().catch(() => ({}));
|
|
@@ -1448,6 +1536,9 @@ var SIGNUP_REASONS = {
|
|
|
1448
1536
|
key_rejected: "Anthropic did not accept that API key. Check it at console.anthropic.com → API keys.",
|
|
1449
1537
|
rate_limited: "too many signups from this network in the last hour. Try again later.",
|
|
1450
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 …`.",
|
|
1451
1542
|
unavailable: "the proxy could not reach its control plane. Try again in a moment.",
|
|
1452
1543
|
unreachable: "could not reach the proxy."
|
|
1453
1544
|
};
|
|
@@ -1473,6 +1564,20 @@ function normalizeProxyUrl(raw) {
|
|
|
1473
1564
|
return null;
|
|
1474
1565
|
}
|
|
1475
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
|
+
}
|
|
1476
1581
|
async function probeProxy(url, fetchFn = fetch, opts = {}) {
|
|
1477
1582
|
const tries = opts.tries ?? 2;
|
|
1478
1583
|
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
@@ -1504,18 +1609,40 @@ var resolveToken = async (o) => {
|
|
|
1504
1609
|
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL);
|
|
1505
1610
|
if (!url)
|
|
1506
1611
|
return null;
|
|
1507
|
-
const
|
|
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();
|
|
1508
1624
|
if (!email.includes("@"))
|
|
1509
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
|
+
}
|
|
1510
1637
|
const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
|
|
1511
1638
|
if (!credential)
|
|
1512
1639
|
return null;
|
|
1513
1640
|
const r = await signupIndividual(url, email, credential, o.fetchFn);
|
|
1514
1641
|
if (!r.ok) {
|
|
1515
|
-
(
|
|
1642
|
+
err(signupFailureText(r));
|
|
1516
1643
|
return null;
|
|
1517
1644
|
}
|
|
1518
|
-
|
|
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.`);
|
|
1519
1646
|
return { token: r.token, source: "signup" };
|
|
1520
1647
|
};
|
|
1521
1648
|
var init = async (o) => {
|
|
@@ -1545,17 +1672,26 @@ var init = async (o) => {
|
|
|
1545
1672
|
return 2;
|
|
1546
1673
|
}
|
|
1547
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;
|
|
1548
1681
|
if (backupSettings(o.home))
|
|
1549
1682
|
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
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));
|
|
1552
1686
|
writeSettings(o.home, statusLine.settings);
|
|
1553
|
-
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`);
|
|
1554
1688
|
wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
|
|
1555
1689
|
hooks2({
|
|
1556
1690
|
home: o.home,
|
|
1557
|
-
|
|
1558
|
-
|
|
1691
|
+
env: o.env,
|
|
1692
|
+
refreshStatusLine: false,
|
|
1693
|
+
seatEventCommand: o.seatEventCommand,
|
|
1694
|
+
specEventCommand: o.specEventCommand,
|
|
1559
1695
|
log: (line) => wrote.push(line),
|
|
1560
1696
|
error
|
|
1561
1697
|
});
|
|
@@ -1568,20 +1704,37 @@ var init = async (o) => {
|
|
|
1568
1704
|
...prior,
|
|
1569
1705
|
token: resolved.token,
|
|
1570
1706
|
proxyUrl: url,
|
|
1707
|
+
wiring: finalWiring,
|
|
1708
|
+
...org ? { org } : {},
|
|
1571
1709
|
...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
|
|
1572
1710
|
});
|
|
1573
1711
|
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
1574
1712
|
mkdirSync6(skillDir(o.home), { recursive: true });
|
|
1575
1713
|
writeFileSync6(skillPath(o.home), SKILL_MD);
|
|
1576
1714
|
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
log(
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
+
}
|
|
1585
1738
|
return 0;
|
|
1586
1739
|
};
|
|
1587
1740
|
|
|
@@ -1941,8 +2094,17 @@ async function handoffsTool(deps) {
|
|
|
1941
2094
|
if (handoffs.length === 0)
|
|
1942
2095
|
return { text: "No open hand-offs on your team. Park one with the handoff tool." };
|
|
1943
2096
|
const now = (deps.now ?? Date.now)();
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
+
};
|
|
1946
2108
|
}
|
|
1947
2109
|
async function handoffTool(deps, args) {
|
|
1948
2110
|
if (!meterSource(deps.home))
|
|
@@ -2261,6 +2423,10 @@ var on = (o) => {
|
|
|
2261
2423
|
error("Saved token is not a sled_ token. Run `sleds init --token <sled_…>`.");
|
|
2262
2424
|
return 2;
|
|
2263
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
|
+
}
|
|
2264
2430
|
const url = (o.url ?? cfg.proxyUrl ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL2).replace(/\/+$/, "");
|
|
2265
2431
|
let untilResetMs;
|
|
2266
2432
|
if (o.untilReset !== undefined) {
|
|
@@ -2356,10 +2522,11 @@ var seatEvent = async (o) => {
|
|
|
2356
2522
|
if (week && week.resetsAt > now)
|
|
2357
2523
|
writeSeatState(o.home, { ...state, lastWallResetsAt: week.resetsAt });
|
|
2358
2524
|
} catch {}
|
|
2359
|
-
|
|
2525
|
+
const line = seatWallLine(readConfig(o.home).org?.capabilities.upstreamKey !== false);
|
|
2526
|
+
log(line);
|
|
2360
2527
|
if ((o.platform ?? process.platform) === "darwin" && o.exec) {
|
|
2361
2528
|
try {
|
|
2362
|
-
o.exec("osascript", osascriptArgs(NOTIFICATION_TITLE,
|
|
2529
|
+
o.exec("osascript", osascriptArgs(NOTIFICATION_TITLE, line));
|
|
2363
2530
|
} catch {}
|
|
2364
2531
|
}
|
|
2365
2532
|
return 0;
|
|
@@ -3056,6 +3223,10 @@ var formatStatus = (s) => {
|
|
|
3056
3223
|
}
|
|
3057
3224
|
if (s.email)
|
|
3058
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"));
|
|
3059
3230
|
if (s.mode === "off") {
|
|
3060
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`.");
|
|
3061
3232
|
}
|
|
@@ -3087,6 +3258,14 @@ var status = async (o) => {
|
|
|
3087
3258
|
info.tokenMasked = maskToken(token);
|
|
3088
3259
|
if (cfg.email)
|
|
3089
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
|
+
};
|
|
3090
3269
|
if (cfg.reason)
|
|
3091
3270
|
info.reason = cfg.reason;
|
|
3092
3271
|
if (o.tz)
|
|
@@ -3243,9 +3422,14 @@ commands:
|
|
|
3243
3422
|
--token <sled_\u2026> seat token (else SLEDS_TOKEN, ~/.sleds-token, or prompt)
|
|
3244
3423
|
--url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
|
|
3245
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)
|
|
3246
3427
|
--non-interactive never prompt; exit 2 if no token is available
|
|
3247
|
-
--signup no org? create
|
|
3248
|
-
|
|
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
|
|
3249
3433
|
on Hit a limit on your seat? Switch Claude Code to the org key through the proxy.
|
|
3250
3434
|
--until-reset <when> revert at this time: +5h, +90m, or an ISO timestamp
|
|
3251
3435
|
--reason "<text>" note shown by \`sleds status\`
|
|
@@ -3333,7 +3517,11 @@ var main = async (argv) => {
|
|
|
3333
3517
|
url: stringFlag(flags, "url"),
|
|
3334
3518
|
nonInteractive: flags["non-interactive"] === true,
|
|
3335
3519
|
signup: flags.signup === true,
|
|
3520
|
+
team: typeof flags.team === "string" ? flags.team : undefined,
|
|
3521
|
+
email: typeof flags.email === "string" ? flags.email : undefined,
|
|
3336
3522
|
noProbe: flags["no-probe"] === true,
|
|
3523
|
+
proxyAlways: flags["proxy-always"] === true,
|
|
3524
|
+
seatFirst: flags["seat-first"] === true,
|
|
3337
3525
|
prompt: promptStdin,
|
|
3338
3526
|
statusLineCommand: statusLineCommand(sledsArgv()),
|
|
3339
3527
|
seatEventCommand: seatEventCommand(sledsArgv()),
|
|
@@ -3384,7 +3572,7 @@ var main = async (argv) => {
|
|
|
3384
3572
|
case "version":
|
|
3385
3573
|
case "--version":
|
|
3386
3574
|
case "-v":
|
|
3387
|
-
console.log(`sleds ${"0.1.
|
|
3575
|
+
console.log(`sleds ${"0.1.11"}`);
|
|
3388
3576
|
return 0;
|
|
3389
3577
|
default:
|
|
3390
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.
|
|
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",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"batch",
|
|
25
25
|
"mcp"
|
|
26
26
|
],
|
|
27
|
-
"publishConfig": { "access": "
|
|
27
|
+
"publishConfig": { "access": "restricted" },
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "bun scripts/build.ts",
|
|
30
30
|
"prepack": "bun scripts/build.ts",
|