@sleds/cli 0.1.16 → 0.1.17

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 (2) hide show
  1. package/dist/sleds.mjs +1484 -935
  2. package/package.json +1 -1
package/dist/sleds.mjs CHANGED
@@ -240,6 +240,8 @@ var configDir = (home, env = process.env) => {
240
240
  cfg.priorStatusLine = raw.priorStatusLine;
241
241
  if (raw.wiring === "seat-first" || raw.wiring === "proxy-always")
242
242
  cfg.wiring = raw.wiring;
243
+ if (raw.agentKind === "claude-code" || raw.agentKind === "codex")
244
+ cfg.agentKind = raw.agentKind;
243
245
  const org = parseOrgInfo(raw.org);
244
246
  if (org)
245
247
  cfg.org = org;
@@ -440,14 +442,14 @@ var init_format = __esm(() => {
440
442
  });
441
443
 
442
444
  // src/meter.ts
443
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
444
- import { join as join3 } from "node:path";
445
- var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, meterCachePath = (home) => join3(configDir(home), "meter.json"), readMeterCache = (home, now) => {
445
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
446
+ import { join as join4 } from "node:path";
447
+ var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, meterCachePath = (home) => join4(configDir(home), "meter.json"), readMeterCache = (home, now) => {
446
448
  const p = meterCachePath(home);
447
- if (!existsSync3(p))
449
+ if (!existsSync4(p))
448
450
  return null;
449
451
  try {
450
- const raw = JSON.parse(readFileSync3(p, "utf8"));
452
+ const raw = JSON.parse(readFileSync4(p, "utf8"));
451
453
  if (!isObject4(raw))
452
454
  return null;
453
455
  const fetchedAt = num2(raw.fetchedAt);
@@ -459,11 +461,11 @@ var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof
459
461
  }
460
462
  }, writeMeterCache = (home, meter, now) => {
461
463
  const dir = configDir(home);
462
- mkdirSync3(dir, { recursive: true, mode: 448 });
464
+ mkdirSync4(dir, { recursive: true, mode: 448 });
463
465
  const target = meterCachePath(home);
464
466
  const tmp = `${target}.${process.pid}.tmp`;
465
467
  const file = { fetchedAt: now, meter };
466
- writeFileSync3(tmp, `${JSON.stringify(file)}
468
+ writeFileSync4(tmp, `${JSON.stringify(file)}
467
469
  `, { mode: 384 });
468
470
  renameSync3(tmp, target);
469
471
  }, fetchMeter = async (o) => {
@@ -481,7 +483,7 @@ var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof
481
483
  }
482
484
  }, meterSource = (home) => {
483
485
  const cfg = readConfig(home);
484
- const settings = existsSync3(settingsPath(home)) ? readSettings(home) : {};
486
+ const settings = existsSync4(settingsPath(home)) ? readSettings(home) : {};
485
487
  const inSettings = readProxy(settings);
486
488
  const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
487
489
  const token = inSettings?.token ?? cfg.token;
@@ -537,9 +539,9 @@ __export(exports_seat, {
537
539
  shouldPostSeat: () => shouldPostSeat2,
538
540
  writeSeatState: () => writeSeatState2
539
541
  });
540
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync10, renameSync as renameSync5, writeFileSync as writeFileSync10 } from "node:fs";
541
- import { join as join11 } from "node:path";
542
- var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_MS2 = 1500, STDIN_TIMEOUT_MS2 = 300, isObject10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num6 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, EPOCH_MS_THRESHOLD2 = 100000000000, parseResetsAt2 = (v) => {
542
+ import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync13 } from "node:fs";
543
+ import { join as join14 } from "node:path";
544
+ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_MS2 = 1500, STDIN_TIMEOUT_MS2 = 300, isObject11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num6 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, EPOCH_MS_THRESHOLD2 = 100000000000, parseResetsAt2 = (v) => {
543
545
  const n = num6(v);
544
546
  if (n !== undefined) {
545
547
  if (n <= 0)
@@ -552,7 +554,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
552
554
  }
553
555
  return;
554
556
  }, parseBar2 = (v) => {
555
- if (!isObject10(v))
557
+ if (!isObject11(v))
556
558
  return;
557
559
  const usedPct = num6(v.used_percentage) ?? num6(v.usedPct);
558
560
  const resetsAt = parseResetsAt2(v.resets_at ?? v.resetsAt);
@@ -560,14 +562,14 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
560
562
  return;
561
563
  return { usedPct, resetsAt };
562
564
  }, parseSeatWindow2 = (raw, capturedAt) => {
563
- if (!isObject10(raw))
565
+ if (!isObject11(raw))
564
566
  return null;
565
- const limits = isObject10(raw.rate_limits) ? raw.rate_limits : raw;
567
+ const limits = isObject11(raw.rate_limits) ? raw.rate_limits : raw;
566
568
  const fiveHour = parseBar2(limits.five_hour ?? limits.fiveHour);
567
569
  const sevenDay = parseBar2(limits.seven_day ?? limits.sevenDay);
568
570
  const spendLimit = parseBar2(limits.spend_limit ?? limits.spendLimit);
569
571
  const models = {};
570
- if (isObject10(limits.models))
572
+ if (isObject11(limits.models))
571
573
  for (const [k, v] of Object.entries(limits.models)) {
572
574
  const bar = parseBar2(v);
573
575
  if (bar)
@@ -611,15 +613,15 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
611
613
  if (parts.length === 0)
612
614
  return null;
613
615
  return `seat ${parts.join(SEP)}`;
614
- }, seatStatePath2 = (home) => join11(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
615
- if (!isObject10(v))
616
+ }, seatStatePath2 = (home) => join14(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
617
+ if (!isObject11(v))
616
618
  return;
617
619
  const capturedAt = num6(v.capturedAt);
618
620
  if (capturedAt === undefined)
619
621
  return;
620
622
  return parseSeatWindow2(v, capturedAt) ?? undefined;
621
623
  }, parseSeatState2 = (raw) => {
622
- if (!isObject10(raw))
624
+ if (!isObject11(raw))
623
625
  return {};
624
626
  const out = {};
625
627
  const window = parseWindowStored2(raw.window);
@@ -628,7 +630,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
628
630
  const lastPostedAt = num6(raw.lastPostedAt);
629
631
  if (lastPostedAt !== undefined)
630
632
  out.lastPostedAt = lastPostedAt;
631
- if (isObject10(raw.lastPosted)) {
633
+ if (isObject11(raw.lastPosted)) {
632
634
  const lp = {};
633
635
  const f = num6(raw.lastPosted.fiveHour);
634
636
  const s = num6(raw.lastPosted.sevenDay);
@@ -649,7 +651,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
649
651
  out.session = session;
650
652
  return out;
651
653
  }, parseSeatSession2 = (v) => {
652
- if (!isObject10(v) || typeof v.id !== "string" || !v.id)
654
+ if (!isObject11(v) || typeof v.id !== "string" || !v.id)
653
655
  return;
654
656
  const at = num6(v.at);
655
657
  if (at === undefined)
@@ -666,12 +668,12 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
666
668
  return null;
667
669
  try {
668
670
  const raw = JSON.parse(trimmed);
669
- if (!isObject10(raw) || typeof raw.session_id !== "string" || !raw.session_id)
671
+ if (!isObject11(raw) || typeof raw.session_id !== "string" || !raw.session_id)
670
672
  return null;
671
673
  const out = { id: raw.session_id, at: now };
672
674
  if (typeof raw.cwd === "string" && raw.cwd)
673
675
  out.cwd = raw.cwd;
674
- const model = isObject10(raw.model) ? raw.model.id : raw.model;
676
+ const model = isObject11(raw.model) ? raw.model.id : raw.model;
675
677
  if (typeof model === "string" && model)
676
678
  out.model = model;
677
679
  return out;
@@ -692,21 +694,21 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
692
694
  return pick.length ? Math.min(...pick) : undefined;
693
695
  }, readSeatState2 = (home) => {
694
696
  const p = seatStatePath2(home);
695
- if (!existsSync13(p))
697
+ if (!existsSync16(p))
696
698
  return {};
697
699
  try {
698
- return parseSeatState2(JSON.parse(readFileSync10(p, "utf8")));
700
+ return parseSeatState2(JSON.parse(readFileSync13(p, "utf8")));
699
701
  } catch {
700
702
  return {};
701
703
  }
702
704
  }, writeSeatState2 = (home, state) => {
703
705
  const dir = configDir(home);
704
- mkdirSync10(dir, { recursive: true, mode: 448 });
706
+ mkdirSync13(dir, { recursive: true, mode: 448 });
705
707
  const target = seatStatePath2(home);
706
708
  const tmp = `${target}.${process.pid}.tmp`;
707
- writeFileSync10(tmp, `${JSON.stringify(state)}
709
+ writeFileSync13(tmp, `${JSON.stringify(state)}
708
710
  `, { mode: 384 });
709
- renameSync5(tmp, target);
711
+ renameSync6(tmp, target);
710
712
  }, pctOf2 = (b) => b?.usedPct, shouldPostSeat2 = (state, next, now) => {
711
713
  if (state.lastPostedAt === undefined || !state.lastPosted)
712
714
  return true;
@@ -757,9 +759,9 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
757
759
  } catch {
758
760
  return false;
759
761
  }
760
- }, postSeatWindow2 = (o, w) => postJson2(o, "/proxy/seat", seatWindowBody2(w)), postSeatWall2 = (o, input) => postJson2(o, "/proxy/seat-wall", input), SEAT_WALL_LINE2 = "Seat limit hit. sleds on --until-reset to continue on the org key, or sleds defer to resume here at the reset.", SEAT_WALL_LINE_NO_KEY2 = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.", seatWallLine2 = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE2 : SEAT_WALL_LINE_NO_KEY2, readStdin2 = (timeoutMs, stdin = process.stdin) => new Promise((resolve5) => {
762
+ }, postSeatWindow2 = (o, w) => postJson2(o, "/proxy/seat", seatWindowBody2(w)), postSeatWall2 = (o, input) => postJson2(o, "/proxy/seat-wall", input), SEAT_WALL_LINE2 = "Seat limit hit. sleds on --until-reset to continue on the platform key, or sleds defer to resume here at the reset.", SEAT_WALL_LINE_NO_KEY2 = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.", seatWallLine2 = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE2 : SEAT_WALL_LINE_NO_KEY2, readStdin2 = (timeoutMs, stdin = process.stdin) => new Promise((resolve6) => {
761
763
  if (stdin.isTTY) {
762
- resolve5("");
764
+ resolve6("");
763
765
  return;
764
766
  }
765
767
  let data = "";
@@ -769,7 +771,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
769
771
  return;
770
772
  settled = true;
771
773
  clearTimeout(timer);
772
- resolve5(data);
774
+ resolve6(data);
773
775
  };
774
776
  const timer = setTimeout(finish, timeoutMs);
775
777
  stdin.setEncoding("utf8");
@@ -791,16 +793,124 @@ import { realpathSync } from "fs";
791
793
  import { homedir, platform as platform2 } from "os";
792
794
  import { fileURLToPath } from "url";
793
795
 
796
+ // src/codex.ts
797
+ init_config();
798
+ import { spawnSync } from "node:child_process";
799
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
800
+ import { dirname as dirname2, join as join3 } from "node:path";
801
+ var PROVIDER_ID = "sleds";
802
+ var TOKEN_ENV = "SLEDS_TOKEN";
803
+ var codexDir = (home) => join3(home, ".codex");
804
+ var codexConfigPath = (home) => join3(codexDir(home), "config.toml");
805
+ function providerBlock(proxyUrl) {
806
+ const base = `${proxyUrl.replace(/\/+$/, "")}/v1`;
807
+ return [
808
+ `[model_providers.${PROVIDER_ID}]`,
809
+ 'name = "SLEDS"',
810
+ `base_url = "${base}"`,
811
+ `env_key = "${TOKEN_ENV}"`,
812
+ 'wire_api = "responses"'
813
+ ].join(`
814
+ `);
815
+ }
816
+ function applyCodexProvider(toml, proxyUrl) {
817
+ let out = removeCodexProvider(toml, { keepPrior: true });
818
+ const top = topLevel(out);
819
+ const m = top.match(/^model_provider[ \t]*=[ \t]*"([^"]*)"[ \t]*$/m);
820
+ if (m && m[1] !== PROVIDER_ID && !/^# sleds-prior-model-provider/m.test(out)) {
821
+ out = out.replace(m[0], `# sleds-prior-model-provider = "${m[1]}"
822
+ model_provider = "${PROVIDER_ID}"`);
823
+ } else if (m) {
824
+ out = out.replace(m[0], `model_provider = "${PROVIDER_ID}"`);
825
+ } else {
826
+ out = `model_provider = "${PROVIDER_ID}"
827
+ ${out.startsWith(`
828
+ `) || out === "" ? "" : `
829
+ `}${out}`;
830
+ }
831
+ return `${out.replace(/\s+$/, "")}
832
+
833
+ ${providerBlock(proxyUrl)}
834
+ `;
835
+ }
836
+ function removeCodexProvider(toml, o = {}) {
837
+ const lines = toml.split(`
838
+ `);
839
+ const out = [];
840
+ let skipping = false;
841
+ for (const line of lines) {
842
+ if (/^\s*\[/.test(line))
843
+ skipping = line.trim() === `[model_providers.${PROVIDER_ID}]`;
844
+ if (!skipping)
845
+ out.push(line);
846
+ }
847
+ let text = out.join(`
848
+ `);
849
+ if (!o.keepPrior) {
850
+ const prior = text.match(/^# sleds-prior-model-provider = "([^"]*)"\s*\n/m);
851
+ if (prior) {
852
+ text = text.replace(prior[0], "");
853
+ text = text.replace(/^model_provider\s*=\s*"sleds"\s*$/m, `model_provider = "${prior[1]}"`);
854
+ } else {
855
+ text = text.replace(/^model_provider\s*=\s*"sleds"\s*\n?/m, "");
856
+ }
857
+ }
858
+ return text.replace(/\n{3,}/g, `
859
+
860
+ `);
861
+ }
862
+ function topLevel(toml) {
863
+ const i = toml.search(/^\s*\[/m);
864
+ return i === -1 ? toml : toml.slice(0, i);
865
+ }
866
+ function initCodex(o) {
867
+ const wrote = [];
868
+ const path = codexConfigPath(o.home);
869
+ const prior = existsSync3(path) ? readFileSync3(path, "utf8") : "";
870
+ mkdirSync3(dirname2(path), { recursive: true });
871
+ writeFileSync3(path, applyCodexProvider(prior, o.proxyUrl), { mode: 384 });
872
+ wrote.push(`${prior ? "updated" : "created"} ${path}: provider "${PROVIDER_ID}" → ${o.proxyUrl}/v1 (key from $${TOKEN_ENV}), model_provider = "${PROVIDER_ID}"`);
873
+ const cfg = readConfig(o.home);
874
+ writeConfig(o.home, {
875
+ ...cfg,
876
+ token: o.token,
877
+ proxyUrl: o.proxyUrl,
878
+ agentKind: "codex",
879
+ ...o.email ? { email: o.email } : {}
880
+ });
881
+ wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
882
+ return wrote;
883
+ }
884
+ function runCodex(home, args, deps = {}) {
885
+ const cfg = readConfig(home);
886
+ const error = deps.error ?? console.error;
887
+ if (!cfg.token || !cfg.proxyUrl) {
888
+ error("No seat here. Run `sleds init --agent codex --token <sled_…> --url https://proxy.sleds.ai` first.");
889
+ return 2;
890
+ }
891
+ const env = {
892
+ ...deps.env ?? process.env,
893
+ [TOKEN_ENV]: cfg.token,
894
+ OPENAI_BASE_URL: `${cfg.proxyUrl.replace(/\/+$/, "")}/v1`
895
+ };
896
+ const r = (deps.spawnSync ?? spawnSync)("codex", args, { stdio: "inherit", env });
897
+ if (r.error) {
898
+ error(`could not start codex: ${r.error.message}. Is Codex CLI installed (npm i -g @openai/codex)?`);
899
+ return 127;
900
+ }
901
+ return r.status ?? 1;
902
+ }
903
+
794
904
  // src/commands/defer.ts
795
905
  init_config();
796
906
  init_meter();
797
- import { existsSync as existsSync5 } from "node:fs";
907
+ import { existsSync as existsSync6 } from "node:fs";
798
908
  import { hostname } from "node:os";
799
909
 
800
910
  // src/musher.ts
801
- import { spawnSync } from "node:child_process";
911
+ import { spawnSync as spawnSync2 } from "node:child_process";
802
912
  import { createPublicKey, verify as cryptoVerify } from "node:crypto";
803
- import { basename, dirname as dirname2, join as join4 } from "node:path";
913
+ import { basename, dirname as dirname3, join as join5 } from "node:path";
804
914
  var MUSHER_LABEL = "ai.sleds.musher";
805
915
  var MUSHER_INTERVAL_S = 300;
806
916
  var DEFAULT_PERMISSION_MODE = "acceptEdits";
@@ -813,15 +923,15 @@ var musherLabel = (env = process.env) => {
813
923
  const tag = basename(sh).replace(/^\.+/, "").replace(/[^A-Za-z0-9_-]+/g, "-") || "seat";
814
924
  return `${MUSHER_LABEL}.${tag}`;
815
925
  };
816
- var musherPlistPath = (home, env = process.env) => join4(home, "Library", "LaunchAgents", `${musherLabel(env)}.plist`);
817
- var musherLogPath = (configDir2) => join4(configDir2, "musher.log");
926
+ var musherPlistPath = (home, env = process.env) => join5(home, "Library", "LaunchAgents", `${musherLabel(env)}.plist`);
927
+ var musherLogPath = (configDir2) => join5(configDir2, "musher.log");
818
928
  var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
819
929
  var musherPath = (home, execPath, currentPath = "") => {
820
930
  const parts = [
821
- dirname2(execPath),
822
- join4(home, ".sleds", "bin"),
823
- join4(home, ".local", "bin"),
824
- join4(home, ".bun", "bin"),
931
+ dirname3(execPath),
932
+ join5(home, ".sleds", "bin"),
933
+ join5(home, ".local", "bin"),
934
+ join5(home, ".bun", "bin"),
825
935
  "/opt/homebrew/bin",
826
936
  "/usr/local/bin",
827
937
  "/usr/bin",
@@ -914,7 +1024,7 @@ var parseClaudeOutput = (stdout, exitCode) => {
914
1024
  return { ok: false, summary: tail || `claude exited ${exitCode ?? "without a code"}` };
915
1025
  };
916
1026
  var defaultExecCapture = (cmd, args, o) => {
917
- const r = spawnSync(cmd, [...args], {
1027
+ const r = spawnSync2(cmd, [...args], {
918
1028
  cwd: o.cwd,
919
1029
  encoding: "utf8",
920
1030
  timeout: o.timeoutMs,
@@ -1045,8 +1155,8 @@ var parsePayload = (payload) => {
1045
1155
  // src/seat.ts
1046
1156
  init_config();
1047
1157
  init_meter();
1048
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
1049
- import { join as join5 } from "node:path";
1158
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
1159
+ import { join as join6 } from "node:path";
1050
1160
  var SEAT_POST_MIN_DELTA_PCT = 1;
1051
1161
  var SEAT_POST_MIN_INTERVAL_MS = 5 * 60000;
1052
1162
  var SEAT_POST_TIMEOUT_MS = 1500;
@@ -1131,7 +1241,7 @@ var formatSeatFragment = (w) => {
1131
1241
  return null;
1132
1242
  return `seat ${parts.join(SEP)}`;
1133
1243
  };
1134
- var seatStatePath = (home) => join5(configDir(home), "seat.json");
1244
+ var seatStatePath = (home) => join6(configDir(home), "seat.json");
1135
1245
  var parseWindowStored = (v) => {
1136
1246
  if (!isObject5(v))
1137
1247
  return;
@@ -1218,20 +1328,20 @@ var bindingResetAt = (w, now) => {
1218
1328
  };
1219
1329
  var readSeatState = (home) => {
1220
1330
  const p = seatStatePath(home);
1221
- if (!existsSync4(p))
1331
+ if (!existsSync5(p))
1222
1332
  return {};
1223
1333
  try {
1224
- return parseSeatState(JSON.parse(readFileSync4(p, "utf8")));
1334
+ return parseSeatState(JSON.parse(readFileSync5(p, "utf8")));
1225
1335
  } catch {
1226
1336
  return {};
1227
1337
  }
1228
1338
  };
1229
1339
  var writeSeatState = (home, state) => {
1230
1340
  const dir = configDir(home);
1231
- mkdirSync4(dir, { recursive: true, mode: 448 });
1341
+ mkdirSync5(dir, { recursive: true, mode: 448 });
1232
1342
  const target = seatStatePath(home);
1233
1343
  const tmp = `${target}.${process.pid}.tmp`;
1234
- writeFileSync4(tmp, `${JSON.stringify(state)}
1344
+ writeFileSync5(tmp, `${JSON.stringify(state)}
1235
1345
  `, { mode: 384 });
1236
1346
  renameSync4(tmp, target);
1237
1347
  };
@@ -1293,7 +1403,7 @@ var postJson = async (o, path, body) => {
1293
1403
  };
1294
1404
  var postSeatWindow = (o, w) => postJson(o, "/proxy/seat", seatWindowBody(w));
1295
1405
  var postSeatWall = (o, input) => postJson(o, "/proxy/seat-wall", input);
1296
- var SEAT_WALL_LINE = "Seat limit hit. sleds on --until-reset to continue on the org key, or sleds defer to resume here at the reset.";
1406
+ var SEAT_WALL_LINE = "Seat limit hit. sleds on --until-reset to continue on the platform key, or sleds defer to resume here at the reset.";
1297
1407
  var SEAT_WALL_LINE_NO_KEY = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.";
1298
1408
  var seatWallLine = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE : SEAT_WALL_LINE_NO_KEY;
1299
1409
  var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) => {
@@ -1471,7 +1581,7 @@ var defer = async (o) => {
1471
1581
  log(`Deferred. This Mac resumes ${sessionId ? `session ${sessionId.slice(0, 8)}…` : "the work"}${model ? ` on ${model}` : ""} ${when}.`);
1472
1582
  log(` job ${r.jobId}${cwd ? ` · ${cwd}` : ""} · runs on your seat, no key · sleds musher --status to watch`);
1473
1583
  const cfg = readConfig(o.home);
1474
- if (process.platform === "darwin" && !existsSync5(musherPlistPath(o.home)) && cfg.musher?.agent !== "launchd")
1584
+ if (process.platform === "darwin" && !existsSync6(musherPlistPath(o.home)) && cfg.musher?.agent !== "launchd")
1475
1585
  log(` The musher agent is not installed on this Mac: run \`sleds musher --install\` once, or \`sleds run ${r.jobId}\` by hand at the reset.`);
1476
1586
  log(" Keep this Mac awake at that time (System Settings → Battery → prevent sleeping on power adapter).");
1477
1587
  return 0;
@@ -1481,25 +1591,25 @@ var defer = async (o) => {
1481
1591
  init_config();
1482
1592
  init_meter();
1483
1593
  import { execFileSync } from "node:child_process";
1484
- import { existsSync as existsSync7, lstatSync, readlinkSync } from "node:fs";
1594
+ import { existsSync as existsSync8, lstatSync, readlinkSync } from "node:fs";
1485
1595
  import { resolve as resolve3 } from "node:path";
1486
1596
  init_settings();
1487
1597
 
1488
1598
  // src/commands/musher.ts
1489
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
1599
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
1490
1600
  init_config();
1491
1601
  init_meter();
1492
1602
  var defaultWhich = (cmd) => {
1493
1603
  const path = process.env.PATH ?? "";
1494
1604
  for (const dir of path.split(":").filter(Boolean)) {
1495
1605
  const p = `${dir}/${cmd}`;
1496
- if (existsSync6(p))
1606
+ if (existsSync7(p))
1497
1607
  return p;
1498
1608
  }
1499
1609
  return null;
1500
1610
  };
1501
1611
  var findClaude = (home, configured, which) => {
1502
- if (configured && existsSync6(configured))
1612
+ if (configured && existsSync7(configured))
1503
1613
  return configured;
1504
1614
  const found = which("claude");
1505
1615
  if (found)
@@ -1510,16 +1620,16 @@ var findClaude = (home, configured, which) => {
1510
1620
  "/usr/local/bin/claude",
1511
1621
  `${home}/.npm-global/bin/claude`
1512
1622
  ])
1513
- if (existsSync6(p))
1623
+ if (existsSync7(p))
1514
1624
  return p;
1515
1625
  return null;
1516
1626
  };
1517
1627
  var musherAgentState = (home, env = process.env) => {
1518
1628
  const plist = musherPlistPath(home, env);
1519
- if (!existsSync6(plist))
1629
+ if (!existsSync7(plist))
1520
1630
  return { installed: false, thisSeat: false };
1521
1631
  try {
1522
- const text = readFileSync5(plist, "utf8");
1632
+ const text = readFileSync6(plist, "utf8");
1523
1633
  const want = env.SLEDS_HOME?.trim();
1524
1634
  const has = /<key>SLEDS_HOME<\/key>\s*<string>([^<]*)<\/string>/.exec(text)?.[1];
1525
1635
  return { installed: true, thisSeat: (want ?? undefined) === (has ?? undefined) };
@@ -1584,7 +1694,7 @@ async function runDoctor(o) {
1584
1694
  ].filter(Boolean);
1585
1695
  const cfgDir = configDir(home, env);
1586
1696
  const cfg = readConfig(home);
1587
- const cfgExists = existsSync7(configPath(home));
1697
+ const cfgExists = existsSync8(configPath(home));
1588
1698
  if (!cfgExists) {
1589
1699
  checks.push({
1590
1700
  level: "fail",
@@ -1600,7 +1710,7 @@ async function runDoctor(o) {
1600
1710
  });
1601
1711
  }
1602
1712
  const sPath = settingsPath(home);
1603
- const settings = existsSync7(sPath) ? readSettings(home) : null;
1713
+ const settings = existsSync8(sPath) ? readSettings(home) : null;
1604
1714
  const pointed = settings ? readProxy(settings) : null;
1605
1715
  if (!settings) {
1606
1716
  checks.push({
@@ -1658,7 +1768,7 @@ async function runDoctor(o) {
1658
1768
  fix: wrong.length === 0 ? undefined : `${overrides.length ? `${overrides.join(" ")} ` : ""}sleds hooks (re-writes the status line + hooks with this seat baked in)`
1659
1769
  });
1660
1770
  }
1661
- const skill = existsSync7(skillPath(home));
1771
+ const skill = existsSync8(skillPath(home));
1662
1772
  checks.push({
1663
1773
  level: skill ? "ok" : "warn",
1664
1774
  name: "skill",
@@ -1761,597 +1871,243 @@ var doctor = async (o) => {
1761
1871
  return checks.some((c) => c.level === "fail") ? 1 : 0;
1762
1872
  };
1763
1873
 
1764
- // src/commands/hooks.ts
1765
- init_settings();
1766
- var hooks = (o) => {
1767
- const log = o.log ?? console.log;
1768
- const error = o.error ?? console.error;
1769
- if (!hasClaudeCode(o.home)) {
1770
- error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
1771
- return 1;
1874
+ // ../../packages/shared/src/mcp/server.ts
1875
+ init_format();
1876
+ function fetchClient(o) {
1877
+ const fetchFn = o.fetchFn ?? fetch;
1878
+ const base = o.proxyUrl.replace(/\/+$/, "");
1879
+ const headers = { "content-type": "application/json", authorization: `Bearer ${o.token}` };
1880
+ const timeout = () => AbortSignal.timeout(o.timeoutMs ?? MCP_TIMEOUT_MS);
1881
+ return {
1882
+ async post(path, body) {
1883
+ try {
1884
+ const res = await fetchFn(`${base}${path}`, {
1885
+ method: "POST",
1886
+ headers,
1887
+ body: JSON.stringify(body),
1888
+ signal: timeout()
1889
+ });
1890
+ if (!res.ok)
1891
+ return null;
1892
+ const parsed = await res.json();
1893
+ return isObject7(parsed) ? parsed : null;
1894
+ } catch {
1895
+ return null;
1896
+ }
1897
+ },
1898
+ async postFull(path, body) {
1899
+ try {
1900
+ const res = await fetchFn(`${base}${path}`, {
1901
+ method: "POST",
1902
+ headers,
1903
+ body: JSON.stringify(body),
1904
+ signal: timeout()
1905
+ });
1906
+ let json = null;
1907
+ try {
1908
+ json = await res.json();
1909
+ } catch {
1910
+ json = null;
1911
+ }
1912
+ return { status: res.status, json: isObject7(json) ? json : null };
1913
+ } catch {
1914
+ return null;
1915
+ }
1916
+ },
1917
+ async get(path) {
1918
+ try {
1919
+ const res = await fetchFn(`${base}${path}`, { headers, signal: timeout() });
1920
+ if (!res.ok)
1921
+ return null;
1922
+ const parsed = await res.json();
1923
+ return isObject7(parsed) ? parsed : null;
1924
+ } catch {
1925
+ return null;
1926
+ }
1927
+ }
1928
+ };
1929
+ }
1930
+ var PROTOCOL_VERSION = "2025-06-18";
1931
+ var SERVER_INFO = { name: "sleds", version: "0.0.1" };
1932
+ var MCP_TIMEOUT_MS = 5000;
1933
+ var sourceSchema = {
1934
+ type: "object",
1935
+ description: "Where this item lives, so the Projects page groups it: a Claude Project doc, a repo spec, a Linear/GitHub issue. Same source + same text on a later call returns the same estimate (idempotent sync).",
1936
+ properties: {
1937
+ kind: { type: "string", enum: ["claude-project", "repo", "linear", "github", "manual"] },
1938
+ project: { type: "string", description: "Project / repo / board name." },
1939
+ doc: { type: "string", description: "Doc path or issue key inside it." },
1940
+ item: {
1941
+ type: "string",
1942
+ description: "The line or phase inside the doc, when a doc holds many."
1943
+ },
1944
+ url: { type: "string", description: "Link to open it, if any." }
1945
+ },
1946
+ required: ["kind", "project"]
1947
+ };
1948
+ var specSchema = {
1949
+ type: "object",
1950
+ properties: {
1951
+ spec: {
1952
+ type: "string",
1953
+ description: "What the job should do. The first line becomes the title."
1954
+ },
1955
+ model: { type: "string", description: "Model to price and run with; org default when absent." },
1956
+ source: sourceSchema
1957
+ },
1958
+ required: ["spec"]
1959
+ };
1960
+ var TOOLS = [
1961
+ {
1962
+ name: "usage",
1963
+ description: "The SLEDS meter for this seat: built today, month against the ceiling, window and reset, allowance, queued/running/delivered jobs, the last wall, and the seat's own Pro/Max limit bars.",
1964
+ inputSchema: { type: "object", properties: {} }
1965
+ },
1966
+ {
1967
+ name: "estimate",
1968
+ description: "Size a task before running it: the org estimator's cost and time band with confidence and a spend cap. Recorded, so its actuals calibrate the next estimate. Never a point figure.",
1969
+ inputSchema: specSchema
1970
+ },
1971
+ {
1972
+ name: "overnight",
1973
+ description: "Queue a task as an overnight batch job at half price. The estimator sizes it first and its cap bounds the spend; the job runs in the org's overnight window and is pulled into a session later with /sled resume <id>.",
1974
+ inputSchema: specSchema
1975
+ },
1976
+ {
1977
+ name: "jobs",
1978
+ description: "This seat's overnight jobs, newest first (id, status, route, cost, when), then the team's open hand-offs waiting for pickup.",
1979
+ inputSchema: { type: "object", properties: {} }
1980
+ },
1981
+ {
1982
+ name: "handoffs",
1983
+ description: "The team's open hand-offs: tasks and walled sessions parked for pickup, the ones meant for this seat first. Each has an id for the pickup tool (or /sled pickup <id> in Claude Code).",
1984
+ inputSchema: { type: "object", properties: {} }
1985
+ },
1986
+ {
1987
+ name: "handoff",
1988
+ description: "Park the current task for a teammate (or anyone on the team) to pick up. Give the task in full — what to do, where things stand, what is left — because the picker starts from this text alone. Optionally name the coworker by email. The team is told on Slack; they take it with /sled pickup <id>.",
1989
+ inputSchema: {
1990
+ type: "object",
1991
+ properties: {
1992
+ task: {
1993
+ type: "string",
1994
+ description: "The task to hand off, in full. First line becomes the title."
1995
+ },
1996
+ to: {
1997
+ type: "string",
1998
+ description: "Coworker's email — a member of your org, or a buddy in another org (see the buddy tool). Omit to park for anyone in your org."
1999
+ },
2000
+ note: { type: "string", description: "One line on where things stand (≤280 chars)." }
2001
+ },
2002
+ required: ["task"]
2003
+ }
2004
+ },
2005
+ {
2006
+ name: "defer",
2007
+ description: "Hit a limit? Defer the work to the reset — the musher runs it then. With an platform key it runs in the cloud on the pinned model; without one, the seat's own Mac resumes the last Claude Code session at the reset (sleds musher). Give `at` as +2h, +90m or an ISO time to override the binding reset.",
2008
+ inputSchema: {
2009
+ type: "object",
2010
+ properties: {
2011
+ at: {
2012
+ type: "string",
2013
+ description: "When to start: +2h, +90m, or an ISO timestamp. Omit for the binding reset."
2014
+ },
2015
+ model: { type: "string", description: "Pin the model the deferred run must use." },
2016
+ task: {
2017
+ type: "string",
2018
+ description: "Optional note on what the deferred run should finish (kept with the job)."
2019
+ }
2020
+ }
2021
+ }
2022
+ },
2023
+ {
2024
+ name: "actual",
2025
+ description: "Close the loop on an estimated item: record what actually happened — minutes, dollars, tokens, model, who did it, when it started and finished, where the output lives (PR url), time by phase (discuss/build/deploy/test), and its state (started · done · merged · shipped). Same `source` as the estimate; Projects shows estimate vs actual and calibrates future bands.",
2026
+ inputSchema: {
2027
+ type: "object",
2028
+ properties: {
2029
+ source: {
2030
+ type: "object",
2031
+ description: "The same source the estimate was filed under.",
2032
+ properties: {
2033
+ kind: {
2034
+ type: "string",
2035
+ enum: ["claude-project", "repo", "linear", "github", "manual"]
2036
+ },
2037
+ project: { type: "string" },
2038
+ doc: { type: "string" },
2039
+ item: { type: "string" },
2040
+ url: { type: "string" }
2041
+ },
2042
+ required: ["kind", "project"]
2043
+ },
2044
+ minutes: { type: "number", description: "Total wall-clock minutes of work." },
2045
+ usd: { type: "number", description: "Dollars spent, when known." },
2046
+ seatCreditsUsd: {
2047
+ type: "number",
2048
+ description: "Seat credits ($, Cowork/Claude Code overage) this item burned — the rail the proxy never sees."
2049
+ },
2050
+ tokensIn: { type: "number" },
2051
+ tokensOut: { type: "number" },
2052
+ model: { type: "string", description: "Model that did the work, e.g. claude-fable-5-1." },
2053
+ by: {
2054
+ type: "string",
2055
+ description: "Who or what: an email, 'claude-fable-5-1 · cowork', 'musher local'."
2056
+ },
2057
+ ref: { type: "string", description: "Where the output lives: PR url, commit, doc." },
2058
+ startedAt: { type: "string", description: "ISO time work began." },
2059
+ finishedAt: { type: "string", description: "ISO time work finished." },
2060
+ state: {
2061
+ type: "string",
2062
+ description: "started · done · merged · shipped · parked (default: done when minutes or finishedAt are given)."
2063
+ },
2064
+ phases: {
2065
+ type: "object",
2066
+ description: "Minutes by phase; total defaults to their sum when `minutes` is absent.",
2067
+ properties: {
2068
+ discussMin: { type: "number" },
2069
+ buildMin: { type: "number" },
2070
+ deployMin: { type: "number" },
2071
+ testMin: { type: "number" }
2072
+ }
2073
+ }
2074
+ },
2075
+ required: ["source"]
2076
+ }
2077
+ },
2078
+ {
2079
+ name: "buddy",
2080
+ description: "Pair with one person outside your org so hand-offs can cross to them (spec 04 §4): they get an email, click Accept, and from then on `handoff {to: their email}` lands in their Waiting for you and runs on their seat. Nothing else crosses — no keys, usage or membership. With no `email`, lists your buddies. `remove: true` unpairs.",
2081
+ inputSchema: {
2082
+ type: "object",
2083
+ properties: {
2084
+ email: { type: "string", description: "Their email. Omit to list." },
2085
+ remove: { type: "boolean", description: "Unpair instead of inviting." }
2086
+ }
2087
+ }
2088
+ },
2089
+ {
2090
+ name: "withdraw",
2091
+ description: "Take back a hand-off you parked before anyone picks it up (the id from the handoffs tool). Only the seat that parked it can withdraw it.",
2092
+ inputSchema: {
2093
+ type: "object",
2094
+ properties: { handoffId: { type: "string", description: "The hand-off id." } },
2095
+ required: ["handoffId"]
2096
+ }
2097
+ },
2098
+ {
2099
+ name: "pickup",
2100
+ description: "Take an open hand-off and get its context to continue here: a parked task comes back verbatim; a walled Claude Code session comes back as its first and last user messages. Marks it picked for the team.",
2101
+ inputSchema: {
2102
+ type: "object",
2103
+ properties: { handoffId: { type: "string", description: "From the handoffs tool." } },
2104
+ required: ["handoffId"]
2105
+ }
1772
2106
  }
1773
- const settings = readSettings(o.home);
1774
- const had = hasSeatHook(settings);
1775
- const hadSpec = hasSpecHook(settings);
1776
- const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
1777
- const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
1778
- const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
1779
- writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
1780
- log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
1781
- if (hadLine)
1782
- log(`refreshed statusLine in ${settingsPath(o.home)}`);
1783
- log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
1784
- return 0;
1785
- };
1786
-
1787
- // src/commands/init.ts
1788
- init_config();
1789
- init_settings();
1790
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
1791
- import { createInterface } from "node:readline";
1792
-
1793
- // src/skill.ts
1794
- var SKILL_MD = `---
1795
- name: sled
1796
- description: Hand work to SLEDS — continue past a rate limit or credit wall on the org's key, hold until reset, or run overnight at half price. Use when the user says /sled, hits a usage limit, mentions their window reset, asks what ran overnight, or asks to defer or schedule agent work.
1797
- ---
1798
-
1799
- # SLEDS
1800
-
1801
- Nine to five is 40 of 168 hours. SLEDS puts the other 128 to work: it carries a session past a wall, and it runs whole tasks overnight with the Musher, a night-shift orchestrator that answers the questions an agent would otherwise stop on. Nothing changes until a wall or a \`/sled\`.
1802
-
1803
- ## Hit a limit on your seat
1804
-
1805
- A Team or Max seat caps on Anthropic's side ("You've hit your limit…"). SLEDS is what you switch to. When the user reports a limit, a cap, or that Claude stopped for the day, tell them exactly this:
1806
-
1807
- \`\`\`
1808
- sleds on --until-reset +5h
1809
- \`\`\`
1810
-
1811
- then restart \`claude\`. Claude Code runs on the org key through the proxy; the seat is untouched. \`--until-reset\` takes \`+5h\`, \`+90m\`, or an ISO time; \`--reason "<text>"\` notes why. The switch reverts itself at the deadline where a scheduler exists.
1812
-
1813
- ## At the wall
1814
-
1815
- When the proxy is active it answers a walled request with its options. The user picks one by number:
1816
-
1817
- \`\`\`
1818
- /sled 1 continue now on the org key
1819
- /sled 2 hold; resume when the window resets
1820
- /sled 3 run overnight at half price
1821
- /sled 4 switch to a cheaper model for the rest of the session (offered when one saves ≥15%)
1822
- /sled 5 hand off to an idle coworker, or park for the team (offered on a team of two or more)
1823
- /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)
1824
- \`\`\`
1825
-
1826
- No key (Team plan, seat-first)? The musher still runs: \`sleds defer\` in a terminal schedules this Mac to resume the current session itself at the reset — \`claude --resume\` on the seat's own quota, the model pinned. \`sleds musher --status\` lists what is waiting; \`sleds run <jobId>\` runs one now; the Mac must be awake at the reset (\`sleds doctor\` checks). When the user says "defer this" or "pick it up at the reset" and the org has no key, tell them to run \`sleds defer\`.
1827
-
1828
- 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.
1829
-
1830
- ## Window reset
1831
-
1832
- When the user mentions their window has reset, or the deadline shown by \`sleds status\` has passed:
1833
-
1834
- \`\`\`
1835
- sleds off
1836
- \`\`\`
1837
-
1838
- then restart \`claude\`. Settings go back to exactly what they were.
1839
-
1840
- ## Check
1841
-
1842
- \`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\`), \`sleds defer\` (this Mac resumes at the reset) and hand-off (\`/sled 5\`) — continue, overnight and switch need a key an admin adds on the dashboard.
1843
-
1844
- 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\` (org key) or \`sleds defer\` (this Mac resumes at the reset, no key).
1845
-
1846
- ## /sled — estimate the task, offer overnight
1847
-
1848
- Bare \`/sled\` (or \`/sled <spec>\`) means: size the current task and offer to hand it to the night shift. Do this in order.
1849
-
1850
- 1. Read the task. The spec is the argument if given, otherwise the current conversation's open task (the todo list, the last user request).
1851
- 2. Estimate with \`/sled estimate <spec>\` — the proxy answers with the calibrated band and cap, and the estimate is recorded so its actuals feed the next one. Quote that band, never a point and never your own guess.
1852
- 3. Offer the window when any of these hold: the high end of the cost band is above the org's approval threshold, the wall-clock is over 30 minutes, or the result is not needed before end of day. The offer is one line: \`Run this overnight from 18:00? Estimated $40–120 at half price.\`
1853
- 4. On yes, write the hand-off the Musher will work from: the spec, the acceptance test, the org conventions that apply, and every decision already made in this session as \`question -> answer\` pairs. Ask one thing before queueing: the autonomy for this job.
1854
- - \`ask\`: every question pages on-call.
1855
- - \`decide_and_flag\` (default): reversible defaults are taken and listed in the morning.
1856
- - \`decide\`: reversible defaults are taken quietly.
1857
- Irreversible questions always page, whatever the autonomy. No answer in 30 minutes parks the job and the Musher moves to the next one.
1858
- 5. Queue it with \`/sled overnight <spec>\` (the hand-off from step 4 is the spec). The proxy runs the real estimator (its cap bounds the job's spend), schedules the job for the org's overnight window, and snapshots this session so the batch runner has the context. Confirm with the job id and the band from the reply — not your own guess.
1859
-
1860
- ## /sled review — the morning queue
1861
-
1862
- Morning ritual. List what the night shift did, grouped:
1863
-
1864
- - Delivered: jobs with a PR or a result, with actual cost against the estimate.
1865
- - Flagged: decisions the Musher took on a \`decide_and_flag\` job, each as \`question -> answer\` with the rung that answered it (spec, convention, or default). These need a human eye, not a redo.
1866
- - Parked: questions nobody answered in 30 minutes. Each shows the option set; answering resumes the job.
1867
-
1868
- Read the Musher log for every delivered job before summarizing, and put each decision into the PR body with its reasoning. Then offer the next step for each: merge, kick back with a note, or \`/sled resume <id>\`. On Monday the review covers the weekend.
1869
-
1870
- ## /sled weekend — Friday sweep
1871
-
1872
- Friday afternoon. Sweep the open task list into a weekend batch: everything the user has queued or labeled for the weekend becomes overnight jobs with the org's default autonomy, and the Monday review covers all of it. For now, explain that the sweep queues the current task list as overnight jobs; sweeping issues by GitHub label is a later ticket. Confirm the count and the total estimate band before queueing.
1873
-
1874
- ## /sled overnight [spec]
1875
-
1876
- Queue a batch job without waiting for a wall. \`/sled overnight <spec>\` runs the spec; bare \`/sled overnight\` runs the session's original ask. The reply carries the band, the cap, when it runs, and \`/sled resume <id>\` for the morning. Half price on the batch tier.
1877
-
1878
- ## /sled jobs
1879
-
1880
- List the org's jobs: queued, running, delivered, parked, with the estimate band, the actual so far, and the autonomy. Parked jobs show the unanswered question. Keep it to one line per job.
1881
-
1882
- ## /sled resume <id>
1883
-
1884
- Resume a parked job. Show the unanswered question and its options, take the answer, and confirm the job is back in the queue. The answer is logged with the user's name, and it becomes a prior decision the Musher can reuse on the next question like it.
1885
-
1886
- ## /sled sync-backlog — price the backlog
1887
-
1888
- With the \`sleds\` MCP server available, size every open issue so the dashboard's Projects page shows the backlog in dollars and hours, not T-shirt sizes or points. SLEDS is not a tracker and never the system of record: Linear and GitHub keep the issues; SLEDS mirrors their words (cycle, labels, priority, assignee, state) and adds the price.
1889
-
1890
- 1. Find the issues. In Cowork with a Claude Project attached: each spec doc, each phase or slice inside a spec, and each line of a backlog list (a numbered or bulleted list under a heading like Backlog, Next, To do, Roadmap). With a Linear or GitHub MCP available: the open issues of the team / repo the user names, with their cycle or milestone, labels, priority, assignee and state. Skip anything done, closed or shipped.
1891
- 2. For each issue call the \`estimate\` tool with \`spec\` = the title on the first line, then the body (quote the issue or doc, do not paraphrase), and \`source\` = \`{ kind: "linear" | "github" | "claude-project", project: <team, repo or Project name>, doc: <issue key like ENG-42 or #123, or the doc path>, item: <heading or line, only when a doc holds many>, url, cycle, labels, priority, assignee, state }\` — the tracker's values verbatim. The estimator dedupes: an unchanged issue returns its existing estimate, so re-running is safe.
1892
- 3. Report one line per issue — \`ENG-42 · Cycle 34 · high · priya · $6–18 · 60–150 min · 55%\` — then the total of what is open, and the Projects page address (\`/o/<slug>/projects\`). Never invent a number: the band is the estimator's. Do not change anything in the tracker from this command; the page's verbs (defer, prioritize, hand off) do that, and write back in the tracker's terms.
1893
-
1894
- To keep the backlog priced as issues change, set a Cowork scheduled task: "Every weekday at 7:00, run /sled sync-backlog for <team or Project> and post the open total." An issue whose text changed gets a fresh band; the page counts how many times each was re-sized.
1895
-
1896
- ## Voice
1897
-
1898
- Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what happens next, not what could.
1899
-
1900
- ## Overnight jobs (how it runs)
1901
-
1902
- At a wall the proxy prints its options; \`/sled 3\` queues the session for the org's overnight window on the batch tier at half price. The next morning:
1903
-
1904
- - \`/sled jobs\` — this seat's jobs: id, status, route, cost, when — then the team's open hand-offs waiting for pickup (yours first).
1905
- - \`/sled resume <id>\` — pull a delivered job's output into the session as the assistant's reply and keep going.
1906
-
1907
- A job that is \`parked\` or \`failed\` shows its reason in \`/sled jobs\`; the admin can see and cancel jobs on the dashboard's Jobs page.
1908
-
1909
- ## Team hand-off
1910
-
1911
- \`/sled 5\` at a wall hands the snapshotted session to the most idle coworker (or parks it for anyone on the team). The team hears on Slack and the dashboard's Jobs page lists open hand-offs with the pickup command.
1912
-
1913
- - \`/sled pickup <id>\` — in the coworker's Claude Code session: the handed-off session resumes there on the org key, metered to them. A task parked from Cowork (no session) starts this turn from the task text. A hand-off meant for someone else or already taken gets one line saying so.
1914
-
1915
- From Cowork or any MCP client (\`sleds mcp\`): the \`handoffs\` tool lists what is waiting, \`handoff\` parks the current task for a teammate (give the task in full — the picker starts from that text), and \`pickup\` takes one and returns its context to continue there.
1916
- `;
1917
-
1918
- // src/commands/hooks.ts
1919
- init_settings();
1920
- var hooks2 = (o) => {
1921
- const log = o.log ?? console.log;
1922
- const error = o.error ?? console.error;
1923
- if (!hasClaudeCode(o.home)) {
1924
- error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
1925
- return 1;
1926
- }
1927
- const settings = readSettings(o.home);
1928
- const had = hasSeatHook(settings);
1929
- const hadSpec = hasSpecHook(settings);
1930
- const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
1931
- const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
1932
- const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
1933
- writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
1934
- log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
1935
- if (hadLine)
1936
- log(`refreshed statusLine in ${settingsPath(o.home)}`);
1937
- log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
1938
- return 0;
1939
- };
1940
-
1941
- // src/commands/init.ts
1942
- var DEFAULT_PROXY_URL = "http://localhost:8787";
1943
- var promptStdin = (question) => new Promise((resolve4) => {
1944
- const rl = createInterface({ input: process.stdin, output: process.stderr });
1945
- rl.question(question, (answer) => {
1946
- rl.close();
1947
- resolve4(answer.trim());
1948
- });
1949
- });
1950
- async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
1951
- return signupRequest(proxyUrl, { email, apiKey: credential }, fetchFn);
1952
- }
1953
- async function signupTeam(proxyUrl, email, team, credential, fetchFn = fetch) {
1954
- return signupRequest(proxyUrl, { email, team, ...credential ? { apiKey: credential } : {} }, fetchFn);
1955
- }
1956
- async function signupRequest(proxyUrl, payload, fetchFn) {
1957
- try {
1958
- const res = await fetchFn(`${proxyUrl}/signup`, {
1959
- method: "POST",
1960
- headers: { "content-type": "application/json" },
1961
- body: JSON.stringify(payload),
1962
- signal: AbortSignal.timeout(20000)
1963
- });
1964
- const body = await res.json().catch(() => ({}));
1965
- if (res.ok && typeof body.token === "string")
1966
- return {
1967
- ok: true,
1968
- token: body.token,
1969
- created: body.created === true,
1970
- verifyPending: body.verifyPending === true
1971
- };
1972
- return {
1973
- ok: false,
1974
- reason: typeof body.reason === "string" ? body.reason : `http_${res.status}`,
1975
- status: res.status
1976
- };
1977
- } catch {
1978
- return { ok: false, reason: "unreachable", status: 0 };
1979
- }
1980
- }
1981
- var SIGNUP_REASONS = {
1982
- email: "that does not look like an email address.",
1983
- key: "that does not look like an Anthropic API key (sk-ant-…).",
1984
- key_rejected: "Anthropic did not accept that API key. Check it at console.anthropic.com → API keys.",
1985
- rate_limited: "too many signups from this network in the last hour. Try again later.",
1986
- not_configured: "the proxy is not set up for self-serve signup yet.",
1987
- team: "the team needs a name (2–80 characters).",
1988
- 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.",
1989
- 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 …`.",
1990
- unavailable: "the proxy could not reach its control plane. Try again in a moment.",
1991
- unreachable: "could not reach the proxy."
1992
- };
1993
- function signupFailureText(r) {
1994
- return `Signup failed: ${SIGNUP_REASONS[r.reason] ?? `unexpected answer (${r.reason}, HTTP ${r.status}).`}`;
1995
- }
1996
- function normalizeProxyUrl(raw) {
1997
- let t = raw.trim();
1998
- if (!t)
1999
- return null;
2000
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(t))
2001
- t = `https://${t}`;
2002
- t = t.replace(/[./]+$/, "");
2003
- try {
2004
- const u = new URL(t);
2005
- if (u.protocol !== "http:" && u.protocol !== "https:")
2006
- return null;
2007
- if (!u.hostname)
2008
- return null;
2009
- u.hostname = u.hostname.replace(/\.+$/, "");
2010
- return `${u.protocol}//${u.host}${u.pathname === "/" ? "" : u.pathname.replace(/\/+$/, "")}`;
2011
- } catch {
2012
- return null;
2013
- }
2014
- }
2015
- async function fetchOrgInfo(url, token, fetchFn = fetch) {
2016
- try {
2017
- const res = await fetchFn(`${url}/proxy/meter`, {
2018
- headers: { authorization: `Bearer ${token}` },
2019
- signal: AbortSignal.timeout(1e4)
2020
- });
2021
- if (!res.ok)
2022
- return null;
2023
- const body = await res.json();
2024
- return body.org?.capabilities ? body.org : null;
2025
- } catch {
2026
- return null;
2027
- }
2028
- }
2029
- async function probeProxy(url, fetchFn = fetch, opts = {}) {
2030
- const tries = opts.tries ?? 2;
2031
- const timeoutMs = opts.timeoutMs ?? 1e4;
2032
- for (let i = 0;i < tries; i++) {
2033
- try {
2034
- const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
2035
- if (res.ok)
2036
- return true;
2037
- } catch {}
2038
- }
2039
- return false;
2040
- }
2041
- var resolveToken = async (o) => {
2042
- const env = o.env ?? process.env;
2043
- if (o.token)
2044
- return { token: o.token.trim(), source: "flag" };
2045
- if (env.SLEDS_TOKEN)
2046
- return { token: env.SLEDS_TOKEN.trim(), source: "env" };
2047
- const fromFile = readTokenFile(o.home);
2048
- if (fromFile)
2049
- return { token: fromFile, source: "file" };
2050
- if (o.nonInteractive || !o.prompt)
2051
- return null;
2052
- if (!o.signup) {
2053
- const typed = (await o.prompt("Paste your SLEDS seat token (sled_…), or press enter to sign up as an individual: ")).trim();
2054
- if (typed)
2055
- return { token: typed, source: "prompt" };
2056
- }
2057
- const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
2058
- if (!url)
2059
- return null;
2060
- const log = o.log ?? console.log;
2061
- const err = o.error ?? console.error;
2062
- let team = o.team?.trim() || null;
2063
- if (team === null && !o.email) {
2064
- const which = (await o.prompt("Set up SLEDS for (1) just you, or (2) your team? [1/2]: ")).trim();
2065
- if (which === "2") {
2066
- team = (await o.prompt("Team name (e.g. your company): ")).trim();
2067
- if (!team)
2068
- return null;
2069
- }
2070
- }
2071
- const email = (o.email ?? await o.prompt(team ? "Your work email: " : "Your email: ")).trim();
2072
- if (!email.includes("@"))
2073
- return null;
2074
- if (team) {
2075
- 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();
2076
- const r2 = await signupTeam(url, email, team, credential2 || null, o.fetchFn);
2077
- if (!r2.ok) {
2078
- err(signupFailureText(r2));
2079
- return null;
2080
- }
2081
- 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."}`);
2082
- if (r2.verifyPending)
2083
- log(`Check ${email} for a verification link and click it — inviting members unlocks then (an unverified team is removed after 48 h; your own seat works meanwhile).`);
2084
- log("Invite your team on the dashboard (Members → invite by email, Admin → Mint a seat); each person runs `sleds init --token …`.");
2085
- return { token: r2.token, source: "signup" };
2086
- }
2087
- const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
2088
- if (!credential)
2089
- return null;
2090
- const r = await signupIndividual(url, email, credential, o.fetchFn);
2091
- if (!r.ok) {
2092
- err(signupFailureText(r));
2093
- return null;
2094
- }
2095
- 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.`);
2096
- return { token: r.token, source: "signup" };
2097
- };
2098
- var init = async (o) => {
2099
- const log = o.log ?? console.log;
2100
- const error = o.error ?? console.error;
2101
- const env = o.env ?? process.env;
2102
- if (!hasClaudeCode(o.home)) {
2103
- error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
2104
- return 1;
2105
- }
2106
- const resolved = await resolveToken(o);
2107
- if (!resolved) {
2108
- error(o.signup ? "Signup did not complete. Nothing was changed." : "No seat token. Pass --token, set SLEDS_TOKEN, run without --non-interactive, or use --signup.");
2109
- return 2;
2110
- }
2111
- if (!isSeatToken(resolved.token)) {
2112
- error("Invalid seat token: expected something like sled_xxxxxxxx.");
2113
- return 2;
2114
- }
2115
- const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
2116
- if (!url) {
2117
- error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
2118
- return 2;
2119
- }
2120
- if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
2121
- error(`No SLEDS proxy answered at ${url}/healthz. Check the URL (or pass --no-probe to save it anyway).`);
2122
- return 2;
2123
- }
2124
- const wrote = [];
2125
- const org = await fetchOrgInfo(url, resolved.token, o.fetchFn);
2126
- const wiring = o.proxyAlways ? "proxy-always" : o.seatFirst ? "seat-first" : org?.mode ?? "proxy-always";
2127
- if (wiring === "proxy-always" && org && !org.capabilities.upstreamKey) {
2128
- error(`${org.name} has no platform key for the proxy to forward with; --proxy-always would break every request. Using seat-first.`);
2129
- }
2130
- const finalWiring = wiring === "proxy-always" && org && !org.capabilities.upstreamKey ? "seat-first" : wiring;
2131
- if (backupSettings(o.home))
2132
- wrote.push(`backed up settings to ${backupPath(o.home)}`);
2133
- const current = readSettings(o.home);
2134
- const wired = finalWiring === "proxy-always" ? applyProxy(current, { url, token: resolved.token }) : removeProxy(current);
2135
- const statusLine = applyStatusLine(wired, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
2136
- writeSettings(o.home, statusLine.settings);
2137
- 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`);
2138
- wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
2139
- hooks2({
2140
- home: o.home,
2141
- env: o.env,
2142
- refreshStatusLine: false,
2143
- seatEventCommand: o.seatEventCommand,
2144
- specEventCommand: o.specEventCommand,
2145
- log: (line) => wrote.push(line),
2146
- error
2147
- });
2148
- if (resolved.source !== "file") {
2149
- writeTokenFile(o.home, resolved.token);
2150
- wrote.push(`saved token to ${tokenPath(o.home)} (0600)`);
2151
- }
2152
- const prior = readConfig(o.home);
2153
- writeConfig(o.home, {
2154
- ...prior,
2155
- token: resolved.token,
2156
- proxyUrl: url,
2157
- wiring: finalWiring,
2158
- ...org ? { org } : {},
2159
- ...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
2160
- });
2161
- wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
2162
- mkdirSync6(skillDir(o.home), { recursive: true });
2163
- writeFileSync6(skillPath(o.home), SKILL_MD);
2164
- wrote.push(`installed skill at ${skillPath(o.home)}`);
2165
- if (o.installMusher) {
2166
- const r = o.installMusher();
2167
- if (r)
2168
- wrote.push(r);
2169
- }
2170
- 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)";
2171
- if (finalWiring === "proxy-always") {
2172
- log(`Claude Code now runs through SLEDS on the org key. (${orgLine})`);
2173
- log("");
2174
- for (const w of wrote)
2175
- log(` ${w}`);
2176
- log("");
2177
- log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
2178
- log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
2179
- log("Prefer your seat first? `sleds init --seat-first` leaves Claude Code on it and switches only at a wall.");
2180
- } else {
2181
- log(`SLEDS is watching your seat. Nothing changes until a wall. (${orgLine})`);
2182
- log("");
2183
- for (const w of wrote)
2184
- log(` ${w}`);
2185
- log("");
2186
- log("Claude Code keeps running on your seat; the status line reports its quota to SLEDS on every turn.");
2187
- if (org && !org.capabilities.upstreamKey) {
2188
- log("This org has no platform key, so at a wall the choices are hold until reset, `sleds defer` (this Mac resumes the session itself at the reset), or hand off to a teammate. An admin can add a key on the dashboard to unlock continue / overnight / switch.");
2189
- } else {
2190
- 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.");
2191
- }
2192
- }
2193
- return 0;
2194
- };
2195
-
2196
- // ../../packages/shared/src/mcp/server.ts
2197
- init_format();
2198
- function fetchClient(o) {
2199
- const fetchFn = o.fetchFn ?? fetch;
2200
- const base = o.proxyUrl.replace(/\/+$/, "");
2201
- const headers = { "content-type": "application/json", authorization: `Bearer ${o.token}` };
2202
- const timeout = () => AbortSignal.timeout(o.timeoutMs ?? MCP_TIMEOUT_MS);
2203
- return {
2204
- async post(path, body) {
2205
- try {
2206
- const res = await fetchFn(`${base}${path}`, {
2207
- method: "POST",
2208
- headers,
2209
- body: JSON.stringify(body),
2210
- signal: timeout()
2211
- });
2212
- if (!res.ok)
2213
- return null;
2214
- const parsed = await res.json();
2215
- return isObject7(parsed) ? parsed : null;
2216
- } catch {
2217
- return null;
2218
- }
2219
- },
2220
- async postFull(path, body) {
2221
- try {
2222
- const res = await fetchFn(`${base}${path}`, {
2223
- method: "POST",
2224
- headers,
2225
- body: JSON.stringify(body),
2226
- signal: timeout()
2227
- });
2228
- let json = null;
2229
- try {
2230
- json = await res.json();
2231
- } catch {
2232
- json = null;
2233
- }
2234
- return { status: res.status, json: isObject7(json) ? json : null };
2235
- } catch {
2236
- return null;
2237
- }
2238
- },
2239
- async get(path) {
2240
- try {
2241
- const res = await fetchFn(`${base}${path}`, { headers, signal: timeout() });
2242
- if (!res.ok)
2243
- return null;
2244
- const parsed = await res.json();
2245
- return isObject7(parsed) ? parsed : null;
2246
- } catch {
2247
- return null;
2248
- }
2249
- }
2250
- };
2251
- }
2252
- var PROTOCOL_VERSION = "2025-06-18";
2253
- var SERVER_INFO = { name: "sleds", version: "0.0.1" };
2254
- var MCP_TIMEOUT_MS = 5000;
2255
- var sourceSchema = {
2256
- type: "object",
2257
- description: "Where this item lives, so the Projects page groups it: a Claude Project doc, a repo spec, a Linear/GitHub issue. Same source + same text on a later call returns the same estimate (idempotent sync).",
2258
- properties: {
2259
- kind: { type: "string", enum: ["claude-project", "repo", "linear", "github", "manual"] },
2260
- project: { type: "string", description: "Project / repo / board name." },
2261
- doc: { type: "string", description: "Doc path or issue key inside it." },
2262
- item: {
2263
- type: "string",
2264
- description: "The line or phase inside the doc, when a doc holds many."
2265
- },
2266
- url: { type: "string", description: "Link to open it, if any." }
2267
- },
2268
- required: ["kind", "project"]
2269
- };
2270
- var specSchema = {
2271
- type: "object",
2272
- properties: {
2273
- spec: {
2274
- type: "string",
2275
- description: "What the job should do. The first line becomes the title."
2276
- },
2277
- model: { type: "string", description: "Model to price and run with; org default when absent." },
2278
- source: sourceSchema
2279
- },
2280
- required: ["spec"]
2281
- };
2282
- var TOOLS = [
2283
- {
2284
- name: "usage",
2285
- description: "The SLEDS meter for this seat: built today, month against the ceiling, window and reset, allowance, queued/running/delivered jobs, the last wall, and the seat's own Pro/Max limit bars.",
2286
- inputSchema: { type: "object", properties: {} }
2287
- },
2288
- {
2289
- name: "estimate",
2290
- description: "Size a task before running it: the org estimator's cost and time band with confidence and a spend cap. Recorded, so its actuals calibrate the next estimate. Never a point figure.",
2291
- inputSchema: specSchema
2292
- },
2293
- {
2294
- name: "overnight",
2295
- description: "Queue a task as an overnight batch job at half price. The estimator sizes it first and its cap bounds the spend; the job runs in the org's overnight window and is pulled into a session later with /sled resume <id>.",
2296
- inputSchema: specSchema
2297
- },
2298
- {
2299
- name: "jobs",
2300
- description: "This seat's overnight jobs, newest first (id, status, route, cost, when), then the team's open hand-offs waiting for pickup.",
2301
- inputSchema: { type: "object", properties: {} }
2302
- },
2303
- {
2304
- name: "handoffs",
2305
- description: "The team's open hand-offs: tasks and walled sessions parked for pickup, the ones meant for this seat first. Each has an id for the pickup tool (or /sled pickup <id> in Claude Code).",
2306
- inputSchema: { type: "object", properties: {} }
2307
- },
2308
- {
2309
- name: "handoff",
2310
- description: "Park the current task for a teammate (or anyone on the team) to pick up. Give the task in full — what to do, where things stand, what is left — because the picker starts from this text alone. Optionally name the coworker by email. The team is told on Slack; they take it with /sled pickup <id>.",
2311
- inputSchema: {
2312
- type: "object",
2313
- properties: {
2314
- task: {
2315
- type: "string",
2316
- description: "The task to hand off, in full. First line becomes the title."
2317
- },
2318
- to: { type: "string", description: "Coworker's email; omit to park for anyone." },
2319
- note: { type: "string", description: "One line on where things stand (≤280 chars)." }
2320
- },
2321
- required: ["task"]
2322
- }
2323
- },
2324
- {
2325
- name: "defer",
2326
- description: "Hit a limit? Defer the work to the reset — the musher runs it then. With an org key it runs in the cloud on the pinned model; without one, the seat's own Mac resumes the last Claude Code session at the reset (sleds musher). Give `at` as +2h, +90m or an ISO time to override the binding reset.",
2327
- inputSchema: {
2328
- type: "object",
2329
- properties: {
2330
- at: {
2331
- type: "string",
2332
- description: "When to start: +2h, +90m, or an ISO timestamp. Omit for the binding reset."
2333
- },
2334
- model: { type: "string", description: "Pin the model the deferred run must use." },
2335
- task: {
2336
- type: "string",
2337
- description: "Optional note on what the deferred run should finish (kept with the job)."
2338
- }
2339
- }
2340
- }
2341
- },
2342
- {
2343
- name: "pickup",
2344
- description: "Take an open hand-off and get its context to continue here: a parked task comes back verbatim; a walled Claude Code session comes back as its first and last user messages. Marks it picked for the team.",
2345
- inputSchema: {
2346
- type: "object",
2347
- properties: { handoffId: { type: "string", description: "From the handoffs tool." } },
2348
- required: ["handoffId"]
2349
- }
2350
- }
2351
- ];
2352
- var NOT_SET_UP = {
2353
- text: "SLEDS is not connected to a seat here. Run `sleds init` (CLI) or connect SLEDS as a connector.",
2354
- isError: true
2107
+ ];
2108
+ var NOT_SET_UP = {
2109
+ text: "SLEDS is not connected to a seat here. Run `sleds init` (CLI) or connect SLEDS as a connector.",
2110
+ isError: true
2355
2111
  };
2356
2112
  var UNREACHABLE = {
2357
2113
  text: "Control plane unreachable. Nothing was recorded → try again in a moment.",
@@ -2497,11 +2253,12 @@ async function jobsTool(ctx) {
2497
2253
  return UNREACHABLE;
2498
2254
  const rows = Array.isArray(r.jobs) ? r.jobs.filter(isObject7) : [];
2499
2255
  const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject7) : [];
2256
+ const me = isObject7(r.seat) ? str2(r.seat.email) : undefined;
2500
2257
  const now = (ctx.now ?? Date.now)();
2501
2258
  if (rows.length === 0 && handoffs.length === 0)
2502
2259
  return { text: "No jobs for this seat. Queue one with the overnight tool." };
2503
2260
  if (rows.length === 0)
2504
- return { text: ["No jobs for this seat.", ...handoffLines(handoffs, now)].join(`
2261
+ return { text: ["No jobs for this seat.", ...handoffLines(handoffs, now, me)].join(`
2505
2262
  `) };
2506
2263
  const lines = rows.map((j) => {
2507
2264
  const when = (ts) => num4(ts) !== undefined ? shortDuration(Math.abs(now - num4(ts))) : "";
@@ -2516,251 +2273,835 @@ async function jobsTool(ctx) {
2516
2273
  tail
2517
2274
  ].join(" ").trimEnd();
2518
2275
  });
2519
- lines.push("Resume a delivered job in a session with /sled resume <id>.");
2520
- lines.push(...handoffLines(handoffs, now));
2276
+ lines.push("Resume a delivered job in a session with /sled resume <id>.");
2277
+ lines.push(...handoffLines(handoffs, now, me));
2278
+ return { text: lines.join(`
2279
+ `) };
2280
+ }
2281
+ function handoffLines(handoffs, now, me) {
2282
+ if (handoffs.length === 0)
2283
+ return [];
2284
+ const out = ["", `Hand-offs waiting (${handoffs.length}):`];
2285
+ for (const h of handoffs) {
2286
+ const forMe = h.forMe === true;
2287
+ const to = str2(h.to);
2288
+ const mine = me !== undefined && str2(h.from)?.toLowerCase() === me.toLowerCase();
2289
+ const who = mine ? `yours · for ${to ?? "anyone"}` : forMe ? to ? "for you" : "parked for anyone" : `for ${to ?? "anyone"}`;
2290
+ const created = num4(h.createdAt);
2291
+ const age = created !== undefined ? `${shortDuration(Math.max(0, now - created))} ago` : "";
2292
+ const note = str2(h.note);
2293
+ const parts = [`from ${str2(h.from) ?? "?"}`, who, str2(h.wallKind) ?? "", age].filter(Boolean);
2294
+ if (h.hasSnapshot === false)
2295
+ parts.push("note only");
2296
+ out.push(`${str2(h.handoffId) ?? "?"} ${parts.join(" · ")}${note ? ` «${note.length > 60 ? `${note.slice(0, 57)}…` : note}»` : ""}`);
2297
+ }
2298
+ out.push(me !== undefined && handoffs.some((h) => str2(h.from)?.toLowerCase() === me.toLowerCase()) ? "Take one with /sled pickup <id>; take back one of yours with the withdraw tool." : "Take one in Claude Code with /sled pickup <id>.");
2299
+ return out;
2300
+ }
2301
+ async function handoffsTool(ctx) {
2302
+ if (!ctx.client)
2303
+ return NOT_SET_UP;
2304
+ const r = await ctx.client.get("/proxy/jobs");
2305
+ if (!r)
2306
+ return UNREACHABLE;
2307
+ const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject7) : [];
2308
+ if (handoffs.length === 0)
2309
+ return { text: "No open hand-offs on your team. Park one with the handoff tool." };
2310
+ const now = (ctx.now ?? Date.now)();
2311
+ const packets = handoffs.map((h) => str2(h.packet)).filter((p) => Boolean(p)).join(`
2312
+
2313
+ ---
2314
+
2315
+ `);
2316
+ const me = isObject7(r.seat) ? str2(r.seat.email) : undefined;
2317
+ return {
2318
+ text: [handoffLines(handoffs, now, me).slice(1).join(`
2319
+ `), packets ? `
2320
+ ${packets}` : ""].join(`
2321
+ `)
2322
+ };
2323
+ }
2324
+ async function withdrawTool(ctx, args) {
2325
+ if (!ctx.client)
2326
+ return NOT_SET_UP;
2327
+ const handoffId = str2(args.handoffId)?.trim();
2328
+ if (!handoffId)
2329
+ return { text: "Pass handoffId: the id from the handoffs tool.", isError: true };
2330
+ const r = await ctx.client.postFull("/proxy/handoff/withdraw", { handoffId });
2331
+ if (!r)
2332
+ return { text: `Nothing withdrawn. ${UNREACHABLE.text}`, isError: true };
2333
+ if (r.status < 200 || r.status >= 300) {
2334
+ const msg = isObject7(r.json) && isObject7(r.json.error) ? str2(r.json.error.message) : undefined;
2335
+ return { text: msg ?? `Could not withdraw ${handoffId} (${r.status}).`, isError: true };
2336
+ }
2337
+ return { text: `Withdrawn ${handoffId}. It is off the team's list; nobody can pick it up.` };
2338
+ }
2339
+ async function handoffTool(ctx, args) {
2340
+ if (!ctx.client)
2341
+ return NOT_SET_UP;
2342
+ const task = str2(args.task)?.trim();
2343
+ if (!task)
2344
+ return { text: "Nothing to hand off. Pass task: what the picker should do.", isError: true };
2345
+ const to = str2(args.to)?.trim();
2346
+ const r = await ctx.client.postFull("/proxy/handoff/park", {
2347
+ task,
2348
+ to: to || undefined,
2349
+ note: str2(args.note)?.trim() || undefined
2350
+ });
2351
+ if (!r)
2352
+ return { text: `Nothing was parked. ${UNREACHABLE.text}`, isError: true };
2353
+ if (r.status === 404 && to)
2354
+ return {
2355
+ text: `${to} is not a member of your org and not a buddy. Nothing was parked. Pair first with the buddy tool (they accept by email), then hand off again.`,
2356
+ isError: true
2357
+ };
2358
+ if (r.status !== 200)
2359
+ return {
2360
+ text: errorMessage(r.json) ?? `Nothing was parked (HTTP ${r.status}).`,
2361
+ isError: true
2362
+ };
2363
+ const id = str2(r.json?.handoffId) ?? "unknown";
2364
+ return {
2365
+ text: `${to ? `Handed off to ${to}` : "Parked for the team"}. They take it in Claude Code with /sled pickup ${id}, or with the pickup tool here. The team was told on Slack.`
2366
+ };
2367
+ }
2368
+ async function pickupTool(ctx, args) {
2369
+ if (!ctx.client)
2370
+ return NOT_SET_UP;
2371
+ const handoffId = str2(args.handoffId)?.trim();
2372
+ if (!handoffId)
2373
+ return { text: "Pass handoffId, from the handoffs tool.", isError: true };
2374
+ const r = await ctx.client.postFull("/proxy/handoff/pickup", { handoffId });
2375
+ if (!r)
2376
+ return { text: `Nothing was picked up. ${UNREACHABLE.text}`, isError: true };
2377
+ if (r.status !== 200)
2378
+ return {
2379
+ text: errorMessage(r.json) ?? `Could not pick it up (HTTP ${r.status}).`,
2380
+ isError: true
2381
+ };
2382
+ const j = r.json ?? {};
2383
+ const from = str2(j.from) ?? "a teammate";
2384
+ const note = str2(j.note);
2385
+ const head = `Picked up from ${from}.${note ? ` Where it stands: ${note}` : ""}`;
2386
+ if (j.kind === "task")
2387
+ return { text: `${head}
2388
+
2389
+ The task:
2390
+ ${str2(j.task) ?? ""}
2391
+
2392
+ Carry it through from here.` };
2393
+ const first = str2(j.firstUserText) ?? "";
2394
+ const last = str2(j.lastUserText) ?? "";
2395
+ const n = num4(j.messageCount);
2396
+ return {
2397
+ text: [
2398
+ `${head} This was a Claude Code session${n !== undefined ? ` (${n} messages)` : ""} that hit a wall; its snapshot can also be replayed in Claude Code with /sled pickup ${handoffId} — but it is now marked picked by you.`,
2399
+ "",
2400
+ `It started with:
2401
+ ${first}`,
2402
+ ...last && last !== first ? ["", `The last ask was:
2403
+ ${last}`] : [],
2404
+ "",
2405
+ "Continue the work from here."
2406
+ ].join(`
2407
+ `)
2408
+ };
2409
+ }
2410
+ function parseAt2(v, now) {
2411
+ const rel = /^\+(\d+)([hm])$/.exec(v.trim());
2412
+ if (rel)
2413
+ return now + Number.parseInt(rel[1] ?? "0", 10) * (rel[2] === "h" ? 3600000 : 60000);
2414
+ const t = Date.parse(v);
2415
+ return Number.isFinite(t) ? t : undefined;
2416
+ }
2417
+ async function deferTool(ctx, args) {
2418
+ if (!ctx.client)
2419
+ return NOT_SET_UP;
2420
+ const now = (ctx.now ?? Date.now)();
2421
+ const atRaw = str2(args.at)?.trim();
2422
+ const at = atRaw ? parseAt2(atRaw, now) : undefined;
2423
+ if (atRaw && at === undefined)
2424
+ return { text: "`at` takes +2h, +90m or an ISO time.", isError: true };
2425
+ const r = await ctx.client.post("/proxy/defer", {
2426
+ ...at !== undefined ? { resetAt: at } : {},
2427
+ ...str2(args.model) ? { model: str2(args.model) } : {},
2428
+ host: "mcp"
2429
+ });
2430
+ if (!r)
2431
+ return { text: `Nothing was deferred. ${UNREACHABLE.text}`, isError: true };
2432
+ const jobId = str2(r.jobId) ?? "unknown";
2433
+ const when = num4(r.scheduledAt);
2434
+ const startsIn = when !== undefined && when > now + 60000 ? `starts in ${shortDuration(when - now)}` : "starts at the next musher tick (within 5 min)";
2435
+ return {
2436
+ text: `Deferred. Job ${jobId} ${startsIn}. With no platform key it runs on the seat's own Mac (sleds musher must be installed there — sleds doctor checks); with a key, in the cloud. Watch it with the jobs tool or /sled jobs.${str2(args.task) ? ` Note kept: ${str2(args.task)}` : ""}`
2437
+ };
2438
+ }
2439
+ async function actualTool(ctx, args) {
2440
+ if (!ctx.client)
2441
+ return NOT_SET_UP;
2442
+ const source = args.source;
2443
+ if (typeof source !== "object" || source === null)
2444
+ return {
2445
+ text: "`source` is required — the same one the estimate was filed under.",
2446
+ isError: true
2447
+ };
2448
+ const iso = (v) => {
2449
+ const t = str2(v) ? Date.parse(str2(v)) : Number.NaN;
2450
+ return Number.isFinite(t) ? t : undefined;
2451
+ };
2452
+ const phasesRaw = typeof args.phases === "object" && args.phases !== null ? args.phases : undefined;
2453
+ const phases = phasesRaw ? Object.fromEntries(["discussMin", "buildMin", "deployMin", "testMin"].map((k) => [k, num4(phasesRaw[k])]).filter(([, v]) => v !== undefined)) : undefined;
2454
+ const work = {
2455
+ ...iso(args.startedAt) !== undefined ? { startedAt: iso(args.startedAt) } : {},
2456
+ ...iso(args.finishedAt) !== undefined ? { finishedAt: iso(args.finishedAt) } : {},
2457
+ ...str2(args.model) ? { model: str2(args.model) } : {},
2458
+ ...num4(args.tokensIn) !== undefined ? { tokensIn: num4(args.tokensIn) } : {},
2459
+ ...num4(args.tokensOut) !== undefined ? { tokensOut: num4(args.tokensOut) } : {},
2460
+ ...str2(args.by) ? { by: str2(args.by) } : {},
2461
+ ...str2(args.ref) ? { ref: str2(args.ref) } : {},
2462
+ ...phases && Object.keys(phases).length > 0 ? { phases } : {},
2463
+ ...num4(args.seatCreditsUsd) !== undefined ? { seatCreditsUsd: num4(args.seatCreditsUsd) } : {}
2464
+ };
2465
+ const r = await ctx.client.post("/proxy/estimate/actual", {
2466
+ source,
2467
+ ...num4(args.minutes) !== undefined ? { actualMinutes: num4(args.minutes) } : {},
2468
+ ...num4(args.usd) !== undefined ? { actualUsd: num4(args.usd) } : {},
2469
+ ...str2(args.state) ? { state: str2(args.state) } : {},
2470
+ ...Object.keys(work).length > 0 ? { work } : {}
2471
+ });
2472
+ if (!r)
2473
+ return { text: `Nothing recorded. ${UNREACHABLE.text}`, isError: true };
2474
+ if (r.ok === false)
2475
+ return {
2476
+ text: `No estimate on file for that source (${str2(r.reason) ?? "unknown"}). Estimate it first.`,
2477
+ isError: true
2478
+ };
2479
+ const bits = [
2480
+ num4(args.minutes) !== undefined ? `${num4(args.minutes)} min` : phases ? "phases recorded" : null,
2481
+ num4(args.usd) !== undefined ? usd(num4(args.usd)) : null,
2482
+ num4(args.tokensIn) !== undefined || num4(args.tokensOut) !== undefined ? `${(num4(args.tokensIn) ?? 0) + (num4(args.tokensOut) ?? 0)} tokens` : null,
2483
+ str2(args.model),
2484
+ str2(args.state) ?? "done"
2485
+ ].filter(Boolean);
2486
+ return { text: `Recorded: ${bits.join(" · ")}. Projects shows it beside the estimate.` };
2487
+ }
2488
+ async function buddyTool(ctx, args) {
2489
+ if (!ctx.client)
2490
+ return NOT_SET_UP;
2491
+ const email = str2(args.email)?.trim();
2492
+ if (!email) {
2493
+ const r2 = await ctx.client.get("/proxy/buddy");
2494
+ if (!r2)
2495
+ return UNREACHABLE;
2496
+ const list = Array.isArray(r2.buddies) ? r2.buddies : [];
2497
+ if (list.length === 0)
2498
+ return {
2499
+ text: "No buddies yet. `buddy {email}` sends someone an invite; once they accept, `handoff {to: email}` reaches them in their own org."
2500
+ };
2501
+ const lines = list.map((b) => `${str2(b.email)} ${b.status === "active" ? "paired" : b.mine ? "invited · waiting for them" : "invited you · accept from the email"}`);
2502
+ return { text: `Buddies:
2503
+ ${lines.join(`
2504
+ `)}` };
2505
+ }
2506
+ if (args.remove === true) {
2507
+ const r2 = await ctx.client.postFull("/proxy/buddy", { email, remove: true });
2508
+ if (!r2)
2509
+ return UNREACHABLE;
2510
+ return { text: r2.json?.ok ? `Unpaired from ${email}.` : `${email} was not a buddy.` };
2511
+ }
2512
+ const r = await ctx.client.postFull("/proxy/buddy", { email });
2513
+ if (!r)
2514
+ return UNREACHABLE;
2515
+ if (r.status !== 200) {
2516
+ const reason = str2(r.json?.reason);
2517
+ const why = reason === "teammate" ? `${email} is already in your org — hand off to them directly.` : reason === "self" ? "That is your own address." : reason === "bad_email" ? "That does not look like an email." : errorMessage(r.json) ?? `HTTP ${r.status}`;
2518
+ return { text: `Not paired. ${why}`, isError: true };
2519
+ }
2520
+ return {
2521
+ text: r.json?.status === "active" ? `Already paired with ${email}.` : `Invite sent to ${email}. When they click Accept, hand off with \`handoff {to: "${email}"}\` — it shows in their Waiting for you and runs on their seat.`
2522
+ };
2523
+ }
2524
+ async function callTool(name, args, ctx) {
2525
+ switch (name) {
2526
+ case "usage":
2527
+ return usageTool(ctx);
2528
+ case "estimate":
2529
+ return estimateTool(ctx, args);
2530
+ case "overnight":
2531
+ return overnightTool(ctx, args);
2532
+ case "jobs":
2533
+ return jobsTool(ctx);
2534
+ case "handoffs":
2535
+ return handoffsTool(ctx);
2536
+ case "handoff":
2537
+ return handoffTool(ctx, args);
2538
+ case "pickup":
2539
+ return pickupTool(ctx, args);
2540
+ case "defer":
2541
+ return deferTool(ctx, args);
2542
+ case "actual":
2543
+ return actualTool(ctx, args);
2544
+ case "buddy":
2545
+ return buddyTool(ctx, args);
2546
+ case "withdraw":
2547
+ return withdrawTool(ctx, args);
2548
+ default:
2549
+ return { text: `Unknown tool: ${name}`, isError: true };
2550
+ }
2551
+ }
2552
+ var response = (id, result) => JSON.stringify({ jsonrpc: "2.0", id, result });
2553
+ var errorResponse = (id, code, message) => JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
2554
+ async function handleLine(line, ctx) {
2555
+ const trimmed = line.trim();
2556
+ if (trimmed.length === 0)
2557
+ return null;
2558
+ let msg;
2559
+ try {
2560
+ msg = JSON.parse(trimmed);
2561
+ } catch {
2562
+ return errorResponse(null, -32700, "parse error");
2563
+ }
2564
+ if (!isObject7(msg))
2565
+ return errorResponse(null, -32600, "invalid request");
2566
+ const id = typeof msg.id === "string" || typeof msg.id === "number" ? msg.id : null;
2567
+ const method = str2(msg.method);
2568
+ const isNotification = msg.id === undefined;
2569
+ if (!method)
2570
+ return isNotification ? null : errorResponse(id, -32600, "invalid request");
2571
+ if (method.startsWith("notifications/"))
2572
+ return null;
2573
+ const params = isObject7(msg.params) ? msg.params : {};
2574
+ switch (method) {
2575
+ case "initialize": {
2576
+ const requested = str2(params.protocolVersion);
2577
+ return response(id, {
2578
+ protocolVersion: requested ?? PROTOCOL_VERSION,
2579
+ capabilities: { tools: {} },
2580
+ serverInfo: SERVER_INFO
2581
+ });
2582
+ }
2583
+ case "ping":
2584
+ return response(id, {});
2585
+ case "tools/list":
2586
+ return response(id, { tools: TOOLS });
2587
+ case "tools/call": {
2588
+ const name = str2(params.name) ?? "";
2589
+ const args = isObject7(params.arguments) ? params.arguments : {};
2590
+ const r = await callTool(name, args, ctx);
2591
+ return response(id, {
2592
+ content: [{ type: "text", text: r.text }],
2593
+ ...r.isError ? { isError: true } : {}
2594
+ });
2595
+ }
2596
+ default:
2597
+ return isNotification ? null : errorResponse(id, -32601, `method not found: ${method}`);
2598
+ }
2599
+ }
2600
+
2601
+ // src/mcp.ts
2602
+ init_meter();
2603
+ async function usageTool2(deps) {
2604
+ if (!meterSource(deps.home))
2605
+ return { text: "SLEDS is not set up on this machine. Run `sleds init` first.", isError: true };
2606
+ const now = (deps.now ?? Date.now)();
2607
+ const m = await loadMeter({
2608
+ home: deps.home,
2609
+ now,
2610
+ fetchFn: deps.fetchFn,
2611
+ timeoutMs: deps.timeoutMs
2612
+ });
2613
+ const frag = formatSeatFragment(readSeatState(deps.home).window ?? null);
2614
+ if (!m) {
2615
+ return {
2616
+ text: frag ? `sleds offline (proxy did not answer) · ${frag}` : "sleds offline (proxy did not answer).",
2617
+ isError: true
2618
+ };
2619
+ }
2620
+ const lines = formatMeter(m, now);
2621
+ if (frag)
2622
+ lines.push(`${"Limits:".padEnd(12)}${frag.replace(/^seat /, "")}`);
2521
2623
  return { text: lines.join(`
2522
2624
  `) };
2523
2625
  }
2524
- function handoffLines(handoffs, now) {
2525
- if (handoffs.length === 0)
2526
- return [];
2527
- const out = ["", `Hand-offs waiting (${handoffs.length}):`];
2528
- for (const h of handoffs) {
2529
- const forMe = h.forMe === true;
2530
- const to = str2(h.to);
2531
- const who = forMe ? to ? "for you" : "parked for anyone" : `for ${to ?? "anyone"}`;
2532
- const created = num4(h.createdAt);
2533
- const age = created !== undefined ? `${shortDuration(Math.max(0, now - created))} ago` : "";
2534
- const note = str2(h.note);
2535
- const parts = [`from ${str2(h.from) ?? "?"}`, who, str2(h.wallKind) ?? "", age].filter(Boolean);
2536
- if (h.hasSnapshot === false)
2537
- parts.push("note only");
2538
- out.push(`${str2(h.handoffId) ?? "?"} ${parts.join(" · ")}${note ? ` «${note.length > 60 ? `${note.slice(0, 57)}…` : note}»` : ""}`);
2539
- }
2540
- out.push("Take one in Claude Code with /sled pickup <id>.");
2541
- return out;
2626
+ function contextFor(deps) {
2627
+ const src = meterSource(deps.home);
2628
+ const client = src ? fetchClient({ ...src, fetchFn: deps.fetchFn, timeoutMs: deps.timeoutMs }) : null;
2629
+ return { client, now: deps.now, usage: () => usageTool2(deps) };
2542
2630
  }
2543
- async function handoffsTool(ctx) {
2544
- if (!ctx.client)
2545
- return NOT_SET_UP;
2546
- const r = await ctx.client.get("/proxy/jobs");
2547
- if (!r)
2548
- return UNREACHABLE;
2549
- const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject7) : [];
2550
- if (handoffs.length === 0)
2551
- return { text: "No open hand-offs on your team. Park one with the handoff tool." };
2552
- const now = (ctx.now ?? Date.now)();
2553
- const packets = handoffs.map((h) => str2(h.packet)).filter((p) => Boolean(p)).join(`
2631
+ var callTool2 = (name, args, deps) => callTool(name, args, contextFor(deps));
2632
+ var handleLine2 = (line, deps) => handleLine(line, contextFor(deps));
2633
+
2634
+ // src/commands/handoffs.ts
2635
+ var handoffsCmd = async (o) => {
2636
+ const r = await callTool2("handoffs", {}, { home: o.home });
2637
+ (r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
2638
+ return r.isError ? 1 : 0;
2639
+ };
2640
+ var pickupCmd = async (o) => {
2641
+ const id = o.argv[0];
2642
+ if (!id) {
2643
+ (o.error ?? console.error)("usage: sleds pickup <handoffId> (ids from `sleds handoffs`)");
2644
+ return 2;
2645
+ }
2646
+ const r = await callTool2("pickup", { handoffId: id }, { home: o.home });
2647
+ (r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
2648
+ return r.isError ? 1 : 0;
2649
+ };
2650
+ var withdrawCmd = async (o) => {
2651
+ const id = o.argv[0];
2652
+ if (!id) {
2653
+ (o.error ?? console.error)("usage: sleds withdraw <handoffId> (one you parked)");
2654
+ return 2;
2655
+ }
2656
+ const r = await callTool2("withdraw", { handoffId: id }, { home: o.home });
2657
+ (r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
2658
+ return r.isError ? 1 : 0;
2659
+ };
2660
+
2661
+ // src/commands/hooks.ts
2662
+ init_settings();
2663
+ var hooks = (o) => {
2664
+ const log = o.log ?? console.log;
2665
+ const error = o.error ?? console.error;
2666
+ if (!hasClaudeCode(o.home)) {
2667
+ error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
2668
+ return 1;
2669
+ }
2670
+ const settings = readSettings(o.home);
2671
+ const had = hasSeatHook(settings);
2672
+ const hadSpec = hasSpecHook(settings);
2673
+ const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
2674
+ const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
2675
+ const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
2676
+ writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
2677
+ log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
2678
+ if (hadLine)
2679
+ log(`refreshed statusLine in ${settingsPath(o.home)}`);
2680
+ log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
2681
+ return 0;
2682
+ };
2683
+
2684
+ // src/commands/init.ts
2685
+ init_config();
2686
+ init_settings();
2687
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
2688
+ import { createInterface } from "node:readline";
2554
2689
 
2690
+ // src/skill.ts
2691
+ var SKILL_MD = `---
2692
+ name: sled
2693
+ description: Hand work to SLEDS — continue past a rate limit or credit wall on the org's key, hold until reset, or run overnight at half price. Use when the user says /sled, hits a usage limit, mentions their window reset, asks what ran overnight, or asks to defer or schedule agent work.
2555
2694
  ---
2556
2695
 
2557
- `);
2558
- return {
2559
- text: [handoffLines(handoffs, now).slice(1).join(`
2560
- `), packets ? `
2561
- ${packets}` : ""].join(`
2562
- `)
2563
- };
2564
- }
2565
- async function handoffTool(ctx, args) {
2566
- if (!ctx.client)
2567
- return NOT_SET_UP;
2568
- const task = str2(args.task)?.trim();
2569
- if (!task)
2570
- return { text: "Nothing to hand off. Pass task: what the picker should do.", isError: true };
2571
- const to = str2(args.to)?.trim();
2572
- const r = await ctx.client.postFull("/proxy/handoff/park", {
2573
- task,
2574
- to: to || undefined,
2575
- note: str2(args.note)?.trim() || undefined
2696
+ # SLEDS
2697
+
2698
+ Nine to five is 40 of 168 hours. SLEDS puts the other 128 to work: it carries a session past a wall, and it runs whole tasks overnight with the Musher, a night-shift orchestrator that answers the questions an agent would otherwise stop on. Nothing changes until a wall or a \`/sled\`.
2699
+
2700
+ ## Hit a limit on your seat
2701
+
2702
+ A Team or Max seat caps on Anthropic's side ("You've hit your limit…"). SLEDS is what you switch to. When the user reports a limit, a cap, or that Claude stopped for the day, tell them exactly this:
2703
+
2704
+ \`\`\`
2705
+ sleds on --until-reset +5h
2706
+ \`\`\`
2707
+
2708
+ then restart \`claude\`. Claude Code runs on the platform key through the proxy; the seat is untouched. \`--until-reset\` takes \`+5h\`, \`+90m\`, or an ISO time; \`--reason "<text>"\` notes why. The switch reverts itself at the deadline where a scheduler exists.
2709
+
2710
+ ## At the wall
2711
+
2712
+ When the proxy is active it answers a walled request with its options. The user picks one by number:
2713
+
2714
+ \`\`\`
2715
+ /sled 1 continue now on the platform key
2716
+ /sled 2 hold; resume when the window resets
2717
+ /sled 3 run overnight at half price
2718
+ /sled 4 switch to a cheaper model for the rest of the session (offered when one saves ≥15%)
2719
+ /sled 5 hand off to an idle coworker, or park for the team (offered on a team of two or more)
2720
+ /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)
2721
+ \`\`\`
2722
+
2723
+ No key (Team plan, seat-first)? The musher still runs: \`sleds defer\` in a terminal schedules this Mac to resume the current session itself at the reset — \`claude --resume\` on the seat's own quota, the model pinned. \`sleds musher --status\` lists what is waiting; \`sleds run <jobId>\` runs one now; the Mac must be awake at the reset (\`sleds doctor\` checks). When the user says "defer this" or "pick it up at the reset" and the org has no key, tell them to run \`sleds defer\`.
2724
+
2725
+ 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.
2726
+
2727
+ ## Window reset
2728
+
2729
+ When the user mentions their window has reset, or the deadline shown by \`sleds status\` has passed:
2730
+
2731
+ \`\`\`
2732
+ sleds off
2733
+ \`\`\`
2734
+
2735
+ then restart \`claude\`. Settings go back to exactly what they were.
2736
+
2737
+ ## Check
2738
+
2739
+ \`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\`), \`sleds defer\` (this Mac resumes at the reset) and hand-off (\`/sled 5\`) — continue, overnight and switch need a key an admin adds on the dashboard.
2740
+
2741
+ 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 platform key's headroom through the proxy. At \`wk 100%\` the answer is \`sleds on --until-reset\` (platform key) or \`sleds defer\` (this Mac resumes at the reset, no key).
2742
+
2743
+ ## /sled — estimate the task, offer overnight
2744
+
2745
+ Bare \`/sled\` (or \`/sled <spec>\`) means: size the current task and offer to hand it to the night shift. Do this in order.
2746
+
2747
+ 1. Read the task. The spec is the argument if given, otherwise the current conversation's open task (the todo list, the last user request).
2748
+ 2. Estimate with \`/sled estimate <spec>\` — the proxy answers with the calibrated band and cap, and the estimate is recorded so its actuals feed the next one. Quote that band, never a point and never your own guess.
2749
+ 3. Offer the window when any of these hold: the high end of the cost band is above the org's approval threshold, the wall-clock is over 30 minutes, or the result is not needed before end of day. The offer is one line: \`Run this overnight from 18:00? Estimated $40–120 at half price.\`
2750
+ 4. On yes, write the hand-off the Musher will work from: the spec, the acceptance test, the org conventions that apply, and every decision already made in this session as \`question -> answer\` pairs. Ask one thing before queueing: the autonomy for this job.
2751
+ - \`ask\`: every question pages on-call.
2752
+ - \`decide_and_flag\` (default): reversible defaults are taken and listed in the morning.
2753
+ - \`decide\`: reversible defaults are taken quietly.
2754
+ Irreversible questions always page, whatever the autonomy. No answer in 30 minutes parks the job and the Musher moves to the next one.
2755
+ 5. Queue it with \`/sled overnight <spec>\` (the hand-off from step 4 is the spec). The proxy runs the real estimator (its cap bounds the job's spend), schedules the job for the org's overnight window, and snapshots this session so the batch runner has the context. Confirm with the job id and the band from the reply — not your own guess.
2756
+
2757
+ ## /sled review — the morning queue
2758
+
2759
+ Morning ritual. List what the night shift did, grouped:
2760
+
2761
+ - Delivered: jobs with a PR or a result, with actual cost against the estimate.
2762
+ - Flagged: decisions the Musher took on a \`decide_and_flag\` job, each as \`question -> answer\` with the rung that answered it (spec, convention, or default). These need a human eye, not a redo.
2763
+ - Parked: questions nobody answered in 30 minutes. Each shows the option set; answering resumes the job.
2764
+
2765
+ Read the Musher log for every delivered job before summarizing, and put each decision into the PR body with its reasoning. Then offer the next step for each: merge, kick back with a note, or \`/sled resume <id>\`. On Monday the review covers the weekend.
2766
+
2767
+ ## /sled weekend — Friday sweep
2768
+
2769
+ Friday afternoon. Sweep the open task list into a weekend batch: everything the user has queued or labeled for the weekend becomes overnight jobs with the org's default autonomy, and the Monday review covers all of it. For now, explain that the sweep queues the current task list as overnight jobs; sweeping issues by GitHub label is a later ticket. Confirm the count and the total estimate band before queueing.
2770
+
2771
+ ## /sled overnight [spec]
2772
+
2773
+ Queue a batch job without waiting for a wall. \`/sled overnight <spec>\` runs the spec; bare \`/sled overnight\` runs the session's original ask. The reply carries the band, the cap, when it runs, and \`/sled resume <id>\` for the morning. Half price on the batch tier.
2774
+
2775
+ ## /sled jobs
2776
+
2777
+ List the org's jobs: queued, running, delivered, parked, with the estimate band, the actual so far, and the autonomy. Parked jobs show the unanswered question. Keep it to one line per job.
2778
+
2779
+ ## /sled resume <id>
2780
+
2781
+ Resume a parked job. Show the unanswered question and its options, take the answer, and confirm the job is back in the queue. The answer is logged with the user's name, and it becomes a prior decision the Musher can reuse on the next question like it.
2782
+
2783
+ ## /sled sync-backlog — price the backlog
2784
+
2785
+ With the \`sleds\` MCP server available, size every open issue so the dashboard's Projects page shows the backlog in dollars and hours, not T-shirt sizes or points. SLEDS is not a tracker and never the system of record: Linear and GitHub keep the issues; SLEDS mirrors their words (cycle, labels, priority, assignee, state) and adds the price.
2786
+
2787
+ 1. Find the issues. In Cowork with a Claude Project attached: each spec doc, each phase or slice inside a spec, and each line of a backlog list (a numbered or bulleted list under a heading like Backlog, Next, To do, Roadmap). With a Linear or GitHub MCP available: the open issues of the team / repo the user names, with their cycle or milestone, labels, priority, assignee and state. Skip anything done, closed or shipped.
2788
+ 2. For each issue call the \`estimate\` tool with \`spec\` = the title on the first line, then the body (quote the issue or doc, do not paraphrase), and \`source\` = \`{ kind: "linear" | "github" | "claude-project", project: <team, repo or Project name>, doc: <issue key like ENG-42 or #123, or the doc path>, item: <heading or line, only when a doc holds many>, url, cycle, labels, priority, assignee, state }\` — the tracker's values verbatim. The estimator dedupes: an unchanged issue returns its existing estimate, so re-running is safe.
2789
+ 3. Report one line per issue — \`ENG-42 · Cycle 34 · high · priya · $6–18 · 60–150 min · 55%\` — then the total of what is open, and the Projects page address (\`/o/<slug>/projects\`). Never invent a number: the band is the estimator's. Do not change anything in the tracker from this command; the page's verbs (defer, prioritize, hand off) do that, and write back in the tracker's terms.
2790
+
2791
+ To keep the backlog priced as issues change, set a Cowork scheduled task: "Every weekday at 7:00, run /sled sync-backlog for <team or Project> and post the open total." An issue whose text changed gets a fresh band; the page counts how many times each was re-sized.
2792
+
2793
+ ## Voice
2794
+
2795
+ Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what happens next, not what could.
2796
+
2797
+ ## Overnight jobs (how it runs)
2798
+
2799
+ At a wall the proxy prints its options; \`/sled 3\` queues the session for the org's overnight window on the batch tier at half price. The next morning:
2800
+
2801
+ - \`/sled jobs\` — this seat's jobs: id, status, route, cost, when — then the team's open hand-offs waiting for pickup (yours first).
2802
+ - \`/sled resume <id>\` — pull a delivered job's output into the session as the assistant's reply and keep going.
2803
+
2804
+ A job that is \`parked\` or \`failed\` shows its reason in \`/sled jobs\`; the admin can see and cancel jobs on the dashboard's Jobs page.
2805
+
2806
+ ## Team hand-off
2807
+
2808
+ \`/sled 5\` at a wall hands the snapshotted session to the most idle coworker (or parks it for anyone on the team). The team hears on Slack and the dashboard's Jobs page lists open hand-offs with the pickup command.
2809
+
2810
+ - \`/sled pickup <id>\` — in the coworker's Claude Code session: the handed-off session resumes there on the platform key, metered to them. A task parked from Cowork (no session) starts this turn from the task text. A hand-off meant for someone else or already taken gets one line saying so.
2811
+
2812
+ - \`/sled handoffs\` — the team's open hand-offs, yours marked \`yours · for <who>\`; \`/sled withdraw <id>\` takes back one you parked before anyone picks it up.
2813
+
2814
+ From Cowork or any MCP client (\`sleds mcp\`): the \`handoffs\` tool lists what is waiting, \`handoff\` parks the current task for a teammate (give the task in full — the picker starts from that text), \`pickup\` takes one and returns its context to continue there, and \`withdraw\` takes back one of yours. From a plain terminal (a Codex seat, or Claude Code closed): \`sleds handoffs\`, \`sleds pickup <id>\`, \`sleds withdraw <id>\` — the same words.
2815
+
2816
+
2817
+ ## Buddies (spec 04 §4)
2818
+
2819
+ A buddy is one person outside your org you can hand work to and from. Pair once (MCP \`buddy {email}\` — they click Accept in the mail); then \`handoff {task, to: "<their email>"}\` lands in their Waiting for you, in whatever org their seat is, and \`pickup\` there runs it on their seat, metered to them. Only the packet crosses — never keys, usage or membership. \`buddy\` with no email lists your pairs; \`buddy {email, remove: true}\` unpairs. A teammate is never a buddy: hand off to them directly.
2820
+ `;
2821
+
2822
+ // src/commands/hooks.ts
2823
+ init_settings();
2824
+ var hooks2 = (o) => {
2825
+ const log = o.log ?? console.log;
2826
+ const error = o.error ?? console.error;
2827
+ if (!hasClaudeCode(o.home)) {
2828
+ error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
2829
+ return 1;
2830
+ }
2831
+ const settings = readSettings(o.home);
2832
+ const had = hasSeatHook(settings);
2833
+ const hadSpec = hasSpecHook(settings);
2834
+ const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
2835
+ const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
2836
+ const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
2837
+ writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
2838
+ log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
2839
+ if (hadLine)
2840
+ log(`refreshed statusLine in ${settingsPath(o.home)}`);
2841
+ log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
2842
+ return 0;
2843
+ };
2844
+
2845
+ // src/commands/init.ts
2846
+ var DEFAULT_PROXY_URL = "http://localhost:8787";
2847
+ var promptStdin = (question) => new Promise((resolve4) => {
2848
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
2849
+ rl.question(question, (answer) => {
2850
+ rl.close();
2851
+ resolve4(answer.trim());
2576
2852
  });
2577
- if (!r)
2578
- return { text: `Nothing was parked. ${UNREACHABLE.text}`, isError: true };
2579
- if (r.status === 404 && to)
2580
- return { text: `${to} is not a member of your org. Nothing was parked.`, isError: true };
2581
- if (r.status !== 200)
2853
+ });
2854
+ async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
2855
+ return signupRequest(proxyUrl, { email, apiKey: credential }, fetchFn);
2856
+ }
2857
+ async function signupTeam(proxyUrl, email, team, credential, fetchFn = fetch) {
2858
+ return signupRequest(proxyUrl, { email, team, ...credential ? { apiKey: credential } : {} }, fetchFn);
2859
+ }
2860
+ async function signupRequest(proxyUrl, payload, fetchFn) {
2861
+ try {
2862
+ const res = await fetchFn(`${proxyUrl}/signup`, {
2863
+ method: "POST",
2864
+ headers: { "content-type": "application/json" },
2865
+ body: JSON.stringify(payload),
2866
+ signal: AbortSignal.timeout(20000)
2867
+ });
2868
+ const body = await res.json().catch(() => ({}));
2869
+ if (res.ok && typeof body.token === "string")
2870
+ return {
2871
+ ok: true,
2872
+ token: body.token,
2873
+ created: body.created === true,
2874
+ verifyPending: body.verifyPending === true
2875
+ };
2582
2876
  return {
2583
- text: errorMessage(r.json) ?? `Nothing was parked (HTTP ${r.status}).`,
2584
- isError: true
2877
+ ok: false,
2878
+ reason: typeof body.reason === "string" ? body.reason : `http_${res.status}`,
2879
+ status: res.status
2585
2880
  };
2586
- const id = str2(r.json?.handoffId) ?? "unknown";
2587
- return {
2588
- text: `${to ? `Handed off to ${to}` : "Parked for the team"}. They take it in Claude Code with /sled pickup ${id}, or with the pickup tool here. The team was told on Slack.`
2589
- };
2881
+ } catch {
2882
+ return { ok: false, reason: "unreachable", status: 0 };
2883
+ }
2590
2884
  }
2591
- async function pickupTool(ctx, args) {
2592
- if (!ctx.client)
2593
- return NOT_SET_UP;
2594
- const handoffId = str2(args.handoffId)?.trim();
2595
- if (!handoffId)
2596
- return { text: "Pass handoffId, from the handoffs tool.", isError: true };
2597
- const r = await ctx.client.postFull("/proxy/handoff/pickup", { handoffId });
2598
- if (!r)
2599
- return { text: `Nothing was picked up. ${UNREACHABLE.text}`, isError: true };
2600
- if (r.status !== 200)
2885
+ var SIGNUP_REASONS = {
2886
+ email: "that does not look like an email address.",
2887
+ key: "that does not look like an Anthropic API key (sk-ant-…).",
2888
+ key_rejected: "Anthropic did not accept that API key. Check it at console.anthropic.com → API keys.",
2889
+ rate_limited: "too many signups from this network in the last hour. Try again later.",
2890
+ not_configured: "the proxy is not set up for self-serve signup yet.",
2891
+ team: "the team needs a name (2–80 characters).",
2892
+ 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.",
2893
+ 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 …`.",
2894
+ unavailable: "the proxy could not reach its control plane. Try again in a moment.",
2895
+ unreachable: "could not reach the proxy."
2896
+ };
2897
+ function signupFailureText(r) {
2898
+ return `Signup failed: ${SIGNUP_REASONS[r.reason] ?? `unexpected answer (${r.reason}, HTTP ${r.status}).`}`;
2899
+ }
2900
+ function normalizeProxyUrl(raw) {
2901
+ let t = raw.trim();
2902
+ if (!t)
2903
+ return null;
2904
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(t))
2905
+ t = `https://${t}`;
2906
+ t = t.replace(/[./]+$/, "");
2907
+ try {
2908
+ const u = new URL(t);
2909
+ if (u.protocol !== "http:" && u.protocol !== "https:")
2910
+ return null;
2911
+ if (!u.hostname)
2912
+ return null;
2913
+ u.hostname = u.hostname.replace(/\.+$/, "");
2914
+ return `${u.protocol}//${u.host}${u.pathname === "/" ? "" : u.pathname.replace(/\/+$/, "")}`;
2915
+ } catch {
2916
+ return null;
2917
+ }
2918
+ }
2919
+ async function fetchSeatInfo(url, token, fetchFn = fetch) {
2920
+ try {
2921
+ const res = await fetchFn(`${url}/proxy/meter`, {
2922
+ headers: { authorization: `Bearer ${token}` },
2923
+ signal: AbortSignal.timeout(1e4)
2924
+ });
2925
+ if (!res.ok)
2926
+ return { org: null, email: null };
2927
+ const body = await res.json();
2601
2928
  return {
2602
- text: errorMessage(r.json) ?? `Could not pick it up (HTTP ${r.status}).`,
2603
- isError: true
2929
+ org: body.org?.capabilities ? body.org : null,
2930
+ email: typeof body.seat?.email === "string" ? body.seat.email : null
2604
2931
  };
2605
- const j = r.json ?? {};
2606
- const from = str2(j.from) ?? "a teammate";
2607
- const note = str2(j.note);
2608
- const head = `Picked up from ${from}.${note ? ` Where it stands: ${note}` : ""}`;
2609
- if (j.kind === "task")
2610
- return { text: `${head}
2611
-
2612
- The task:
2613
- ${str2(j.task) ?? ""}
2614
-
2615
- Carry it through from here.` };
2616
- const first = str2(j.firstUserText) ?? "";
2617
- const last = str2(j.lastUserText) ?? "";
2618
- const n = num4(j.messageCount);
2619
- return {
2620
- text: [
2621
- `${head} This was a Claude Code session${n !== undefined ? ` (${n} messages)` : ""} that hit a wall; its snapshot can also be replayed in Claude Code with /sled pickup ${handoffId} — but it is now marked picked by you.`,
2622
- "",
2623
- `It started with:
2624
- ${first}`,
2625
- ...last && last !== first ? ["", `The last ask was:
2626
- ${last}`] : [],
2627
- "",
2628
- "Continue the work from here."
2629
- ].join(`
2630
- `)
2631
- };
2632
- }
2633
- function parseAt2(v, now) {
2634
- const rel = /^\+(\d+)([hm])$/.exec(v.trim());
2635
- if (rel)
2636
- return now + Number.parseInt(rel[1] ?? "0", 10) * (rel[2] === "h" ? 3600000 : 60000);
2637
- const t = Date.parse(v);
2638
- return Number.isFinite(t) ? t : undefined;
2932
+ } catch {
2933
+ return { org: null, email: null };
2934
+ }
2639
2935
  }
2640
- async function deferTool(ctx, args) {
2641
- if (!ctx.client)
2642
- return NOT_SET_UP;
2643
- const now = (ctx.now ?? Date.now)();
2644
- const atRaw = str2(args.at)?.trim();
2645
- const at = atRaw ? parseAt2(atRaw, now) : undefined;
2646
- if (atRaw && at === undefined)
2647
- return { text: "`at` takes +2h, +90m or an ISO time.", isError: true };
2648
- const r = await ctx.client.post("/proxy/defer", {
2649
- ...at !== undefined ? { resetAt: at } : {},
2650
- ...str2(args.model) ? { model: str2(args.model) } : {},
2651
- host: "mcp"
2652
- });
2653
- if (!r)
2654
- return { text: `Nothing was deferred. ${UNREACHABLE.text}`, isError: true };
2655
- const jobId = str2(r.jobId) ?? "unknown";
2656
- const when = num4(r.scheduledAt);
2657
- const startsIn = when !== undefined && when > now + 60000 ? `starts in ${shortDuration(when - now)}` : "starts at the next musher tick (within 5 min)";
2658
- return {
2659
- text: `Deferred. Job ${jobId} ${startsIn}. With no org key it runs on the seat's own Mac (sleds musher must be installed there — sleds doctor checks); with a key, in the cloud. Watch it with the jobs tool or /sled jobs.${str2(args.task) ? ` Note kept: ${str2(args.task)}` : ""}`
2660
- };
2936
+ async function probeProxy(url, fetchFn = fetch, opts = {}) {
2937
+ const tries = opts.tries ?? 2;
2938
+ const timeoutMs = opts.timeoutMs ?? 1e4;
2939
+ for (let i = 0;i < tries; i++) {
2940
+ try {
2941
+ const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
2942
+ if (res.ok)
2943
+ return true;
2944
+ } catch {}
2945
+ }
2946
+ return false;
2661
2947
  }
2662
- async function callTool(name, args, ctx) {
2663
- switch (name) {
2664
- case "usage":
2665
- return usageTool(ctx);
2666
- case "estimate":
2667
- return estimateTool(ctx, args);
2668
- case "overnight":
2669
- return overnightTool(ctx, args);
2670
- case "jobs":
2671
- return jobsTool(ctx);
2672
- case "handoffs":
2673
- return handoffsTool(ctx);
2674
- case "handoff":
2675
- return handoffTool(ctx, args);
2676
- case "pickup":
2677
- return pickupTool(ctx, args);
2678
- case "defer":
2679
- return deferTool(ctx, args);
2680
- default:
2681
- return { text: `Unknown tool: ${name}`, isError: true };
2948
+ var resolveToken = async (o) => {
2949
+ const env = o.env ?? process.env;
2950
+ if (o.token)
2951
+ return { token: o.token.trim(), source: "flag" };
2952
+ if (env.SLEDS_TOKEN)
2953
+ return { token: env.SLEDS_TOKEN.trim(), source: "env" };
2954
+ const fromFile = readTokenFile(o.home);
2955
+ if (fromFile)
2956
+ return { token: fromFile, source: "file" };
2957
+ if (o.nonInteractive || !o.prompt)
2958
+ return null;
2959
+ if (!o.signup) {
2960
+ const typed = (await o.prompt("Paste your SLEDS seat token (sled_…), or press enter to sign up as an individual: ")).trim();
2961
+ if (typed)
2962
+ return { token: typed, source: "prompt" };
2963
+ }
2964
+ const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
2965
+ if (!url)
2966
+ return null;
2967
+ const log = o.log ?? console.log;
2968
+ const err = o.error ?? console.error;
2969
+ let team = o.team?.trim() || null;
2970
+ if (team === null && !o.email) {
2971
+ const which = (await o.prompt("Set up SLEDS for (1) just you, or (2) your team? [1/2]: ")).trim();
2972
+ if (which === "2") {
2973
+ team = (await o.prompt("Team name (e.g. your company): ")).trim();
2974
+ if (!team)
2975
+ return null;
2976
+ }
2977
+ }
2978
+ const email = (o.email ?? await o.prompt(team ? "Your work email: " : "Your email: ")).trim();
2979
+ if (!email.includes("@"))
2980
+ return null;
2981
+ if (team) {
2982
+ 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();
2983
+ const r2 = await signupTeam(url, email, team, credential2 || null, o.fetchFn);
2984
+ if (!r2.ok) {
2985
+ err(signupFailureText(r2));
2986
+ return null;
2987
+ }
2988
+ 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."}`);
2989
+ if (r2.verifyPending)
2990
+ log(`Check ${email} for a verification link and click it — inviting members unlocks then (an unverified team is removed after 48 h; your own seat works meanwhile).`);
2991
+ log("Invite your team on the dashboard (Members → invite by email, Admin → Mint a seat); each person runs `sleds init --token …`.");
2992
+ return { token: r2.token, source: "signup" };
2993
+ }
2994
+ const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
2995
+ if (!credential)
2996
+ return null;
2997
+ const r = await signupIndividual(url, email, credential, o.fetchFn);
2998
+ if (!r.ok) {
2999
+ err(signupFailureText(r));
3000
+ return null;
3001
+ }
3002
+ 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.`);
3003
+ return { token: r.token, source: "signup" };
3004
+ };
3005
+ var init = async (o) => {
3006
+ const log = o.log ?? console.log;
3007
+ const error = o.error ?? console.error;
3008
+ const env = o.env ?? process.env;
3009
+ if (!hasClaudeCode(o.home)) {
3010
+ error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
3011
+ return 1;
3012
+ }
3013
+ const resolved = await resolveToken(o);
3014
+ if (!resolved) {
3015
+ error(o.signup ? "Signup did not complete. Nothing was changed." : "No seat token. Pass --token, set SLEDS_TOKEN, run without --non-interactive, or use --signup.");
3016
+ return 2;
3017
+ }
3018
+ if (!isSeatToken(resolved.token)) {
3019
+ error("Invalid seat token: expected something like sled_xxxxxxxx.");
3020
+ return 2;
2682
3021
  }
2683
- }
2684
- var response = (id, result) => JSON.stringify({ jsonrpc: "2.0", id, result });
2685
- var errorResponse = (id, code, message) => JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
2686
- async function handleLine(line, ctx) {
2687
- const trimmed = line.trim();
2688
- if (trimmed.length === 0)
2689
- return null;
2690
- let msg;
2691
- try {
2692
- msg = JSON.parse(trimmed);
2693
- } catch {
2694
- return errorResponse(null, -32700, "parse error");
3022
+ const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
3023
+ if (!url) {
3024
+ error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
3025
+ return 2;
2695
3026
  }
2696
- if (!isObject7(msg))
2697
- return errorResponse(null, -32600, "invalid request");
2698
- const id = typeof msg.id === "string" || typeof msg.id === "number" ? msg.id : null;
2699
- const method = str2(msg.method);
2700
- const isNotification = msg.id === undefined;
2701
- if (!method)
2702
- return isNotification ? null : errorResponse(id, -32600, "invalid request");
2703
- if (method.startsWith("notifications/"))
2704
- return null;
2705
- const params = isObject7(msg.params) ? msg.params : {};
2706
- switch (method) {
2707
- case "initialize": {
2708
- const requested = str2(params.protocolVersion);
2709
- return response(id, {
2710
- protocolVersion: requested ?? PROTOCOL_VERSION,
2711
- capabilities: { tools: {} },
2712
- serverInfo: SERVER_INFO
2713
- });
2714
- }
2715
- case "ping":
2716
- return response(id, {});
2717
- case "tools/list":
2718
- return response(id, { tools: TOOLS });
2719
- case "tools/call": {
2720
- const name = str2(params.name) ?? "";
2721
- const args = isObject7(params.arguments) ? params.arguments : {};
2722
- const r = await callTool(name, args, ctx);
2723
- return response(id, {
2724
- content: [{ type: "text", text: r.text }],
2725
- ...r.isError ? { isError: true } : {}
2726
- });
2727
- }
2728
- default:
2729
- return isNotification ? null : errorResponse(id, -32601, `method not found: ${method}`);
3027
+ if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
3028
+ error(`No SLEDS proxy answered at ${url}/healthz. Check the URL (or pass --no-probe to save it anyway).`);
3029
+ return 2;
2730
3030
  }
2731
- }
2732
-
2733
- // src/mcp.ts
2734
- init_meter();
2735
- async function usageTool2(deps) {
2736
- if (!meterSource(deps.home))
2737
- return { text: "SLEDS is not set up on this machine. Run `sleds init` first.", isError: true };
2738
- const now = (deps.now ?? Date.now)();
2739
- const m = await loadMeter({
2740
- home: deps.home,
2741
- now,
2742
- fetchFn: deps.fetchFn,
2743
- timeoutMs: deps.timeoutMs
3031
+ const wrote = [];
3032
+ const seatInfo = await fetchSeatInfo(url, resolved.token, o.fetchFn);
3033
+ const org = seatInfo.org;
3034
+ const wiring = o.proxyAlways ? "proxy-always" : o.seatFirst ? "seat-first" : org?.mode ?? "proxy-always";
3035
+ if (wiring === "proxy-always" && org && !org.capabilities.upstreamKey) {
3036
+ error(`${org.name} has no platform key for the proxy to forward with; --proxy-always would break every request. Using seat-first.`);
3037
+ }
3038
+ const finalWiring = wiring === "proxy-always" && org && !org.capabilities.upstreamKey ? "seat-first" : wiring;
3039
+ if (backupSettings(o.home))
3040
+ wrote.push(`backed up settings to ${backupPath(o.home)}`);
3041
+ const current = readSettings(o.home);
3042
+ const wired = finalWiring === "proxy-always" ? applyProxy(current, { url, token: resolved.token }) : removeProxy(current);
3043
+ const statusLine = applyStatusLine(wired, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
3044
+ writeSettings(o.home, statusLine.settings);
3045
+ 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`);
3046
+ wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
3047
+ hooks2({
3048
+ home: o.home,
3049
+ env: o.env,
3050
+ refreshStatusLine: false,
3051
+ seatEventCommand: o.seatEventCommand,
3052
+ specEventCommand: o.specEventCommand,
3053
+ log: (line) => wrote.push(line),
3054
+ error
2744
3055
  });
2745
- const frag = formatSeatFragment(readSeatState(deps.home).window ?? null);
2746
- if (!m) {
2747
- return {
2748
- text: frag ? `sleds offline (proxy did not answer) · ${frag}` : "sleds offline (proxy did not answer).",
2749
- isError: true
2750
- };
3056
+ if (resolved.source !== "file") {
3057
+ writeTokenFile(o.home, resolved.token);
3058
+ wrote.push(`saved token to ${tokenPath(o.home)} (0600)`);
2751
3059
  }
2752
- const lines = formatMeter(m, now);
2753
- if (frag)
2754
- lines.push(`${"Limits:".padEnd(12)}${frag.replace(/^seat /, "")}`);
2755
- return { text: lines.join(`
2756
- `) };
2757
- }
2758
- function contextFor(deps) {
2759
- const src = meterSource(deps.home);
2760
- const client = src ? fetchClient({ ...src, fetchFn: deps.fetchFn, timeoutMs: deps.timeoutMs }) : null;
2761
- return { client, now: deps.now, usage: () => usageTool2(deps) };
2762
- }
2763
- var handleLine2 = (line, deps) => handleLine(line, contextFor(deps));
3060
+ const prior = readConfig(o.home);
3061
+ writeConfig(o.home, {
3062
+ ...prior,
3063
+ token: resolved.token,
3064
+ proxyUrl: url,
3065
+ wiring: finalWiring,
3066
+ agentKind: "claude-code",
3067
+ ...seatInfo.email ? { email: seatInfo.email } : {},
3068
+ ...org ? { org } : {},
3069
+ ...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
3070
+ });
3071
+ wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
3072
+ mkdirSync7(skillDir(o.home), { recursive: true });
3073
+ writeFileSync7(skillPath(o.home), SKILL_MD);
3074
+ wrote.push(`installed skill at ${skillPath(o.home)}`);
3075
+ if (o.installMusher) {
3076
+ const r = o.installMusher();
3077
+ if (r)
3078
+ wrote.push(r);
3079
+ }
3080
+ const orgLine = org ? `${org.name} · ${org.plan}${org.capabilities.upstreamKey ? ` · ${org.capabilities.upstreamSource === "own" ? "platform key" : "shared key"}` : " · no platform key"}` : "org unknown (proxy did not answer /proxy/meter)";
3081
+ if (finalWiring === "proxy-always") {
3082
+ log(`Claude Code now runs through SLEDS on the platform key. (${orgLine})`);
3083
+ log("");
3084
+ for (const w of wrote)
3085
+ log(` ${w}`);
3086
+ log("");
3087
+ log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
3088
+ log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
3089
+ log("Prefer your seat first? `sleds init --seat-first` leaves Claude Code on it and switches only at a wall.");
3090
+ } else {
3091
+ log(`SLEDS is watching your seat. Nothing changes until a wall. (${orgLine})`);
3092
+ log("");
3093
+ for (const w of wrote)
3094
+ log(` ${w}`);
3095
+ log("");
3096
+ log("Claude Code keeps running on your seat; the status line reports its quota to SLEDS on every turn.");
3097
+ if (org && !org.capabilities.upstreamKey) {
3098
+ log("This org has no platform key, so at a wall the choices are hold until reset, `sleds defer` (this Mac resumes the session itself at the reset), or hand off to a teammate. An admin can add a key on the dashboard to unlock continue / overnight / switch.");
3099
+ } else {
3100
+ log("At a wall: `sleds on --until-reset +5h` (or /sled 1) switches to the platform key through the proxy until the reset; `sleds off` puts you back.");
3101
+ }
3102
+ }
3103
+ return 0;
3104
+ };
2764
3105
 
2765
3106
  // src/commands/mcp.ts
2766
3107
  var mcp = async (o) => {
@@ -2814,21 +3155,21 @@ var meter = async (o) => {
2814
3155
  // src/commands/musher.ts
2815
3156
  init_config();
2816
3157
  init_meter();
2817
- import { spawnSync as spawnSync2 } from "node:child_process";
2818
- import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
3158
+ import { spawnSync as spawnSync3 } from "node:child_process";
3159
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "node:fs";
2819
3160
  import { hostname as hostname2 } from "node:os";
2820
- import { dirname as dirname3 } from "node:path";
3161
+ import { dirname as dirname4 } from "node:path";
2821
3162
  var defaultWhich3 = (cmd) => {
2822
3163
  const path = process.env.PATH ?? "";
2823
3164
  for (const dir of path.split(":").filter(Boolean)) {
2824
3165
  const p = `${dir}/${cmd}`;
2825
- if (existsSync8(p))
3166
+ if (existsSync9(p))
2826
3167
  return p;
2827
3168
  }
2828
3169
  return null;
2829
3170
  };
2830
3171
  var findClaude2 = (home, configured, which) => {
2831
- if (configured && existsSync8(configured))
3172
+ if (configured && existsSync9(configured))
2832
3173
  return configured;
2833
3174
  const found = which("claude");
2834
3175
  if (found)
@@ -2839,7 +3180,7 @@ var findClaude2 = (home, configured, which) => {
2839
3180
  "/usr/local/bin/claude",
2840
3181
  `${home}/.npm-global/bin/claude`
2841
3182
  ])
2842
- if (existsSync8(p))
3183
+ if (existsSync9(p))
2843
3184
  return p;
2844
3185
  return null;
2845
3186
  };
@@ -2855,8 +3196,8 @@ var musherInstall = (o) => {
2855
3196
  const plist = musherPlistPath(o.home, env);
2856
3197
  const label = musherLabel(env);
2857
3198
  const dir = configDir(o.home, env);
2858
- mkdirSync7(dirname3(plist), { recursive: true });
2859
- mkdirSync7(dir, { recursive: true, mode: 448 });
3199
+ mkdirSync8(dirname4(plist), { recursive: true });
3200
+ mkdirSync8(dir, { recursive: true, mode: 448 });
2860
3201
  const execPath = o.sledsArgv[0] ?? process.execPath;
2861
3202
  const agentEnv = {
2862
3203
  PATH: musherPath(o.home, execPath, env.PATH ?? ""),
@@ -2865,9 +3206,9 @@ var musherInstall = (o) => {
2865
3206
  ...env.CLAUDE_CONFIG_DIR ? { CLAUDE_CONFIG_DIR: env.CLAUDE_CONFIG_DIR } : {}
2866
3207
  };
2867
3208
  const exec = o.exec ?? defaultExecSync;
2868
- if (existsSync8(plist))
3209
+ if (existsSync9(plist))
2869
3210
  exec("launchctl", ["unload", plist]);
2870
- writeFileSync7(plist, buildMusherPlist({ sledsArgv: o.sledsArgv, env: agentEnv, logPath: musherLogPath(dir), label }));
3211
+ writeFileSync8(plist, buildMusherPlist({ sledsArgv: o.sledsArgv, env: agentEnv, logPath: musherLogPath(dir), label }));
2871
3212
  const r = exec("launchctl", ["load", plist]);
2872
3213
  if (!r.ok) {
2873
3214
  error(`launchctl load failed: ${r.stderr.trim() || "unknown"}`);
@@ -2886,7 +3227,7 @@ var musherUninstall = (o) => {
2886
3227
  const env = o.env ?? process.env;
2887
3228
  const plist = musherPlistPath(o.home, env);
2888
3229
  const exec = o.exec ?? defaultExecSync;
2889
- if (existsSync8(plist)) {
3230
+ if (existsSync9(plist)) {
2890
3231
  exec("launchctl", ["unload", plist]);
2891
3232
  rmSync2(plist, { force: true });
2892
3233
  log(`musher removed: ${musherLabel(env)}`);
@@ -2921,7 +3262,7 @@ var musherStatus = async (o) => {
2921
3262
  const env = o.env ?? process.env;
2922
3263
  const plist = musherPlistPath(o.home, env);
2923
3264
  const cfg = readConfig(o.home);
2924
- log(existsSync8(plist) ? `agent ${musherLabel(env)} installed (${plist})` : "agent not installed — sleds musher --install");
3265
+ log(existsSync9(plist) ? `agent ${musherLabel(env)} installed (${plist})` : "agent not installed — sleds musher --install");
2925
3266
  const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3);
2926
3267
  log(`claude ${claude ?? "not found — sleds musher --claude /path/to/claude"}`);
2927
3268
  log(`mode ${cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE} · timeout ${cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES} min`);
@@ -2994,7 +3335,7 @@ var musherConfigure = (o, flags) => {
2994
3335
  return 0;
2995
3336
  };
2996
3337
  var defaultExecSync = (cmd, args) => {
2997
- const r = spawnSync2(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
3338
+ const r = spawnSync3(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
2998
3339
  if (r.error)
2999
3340
  return { ok: false, stderr: r.error.message };
3000
3341
  return { ok: r.status === 0, stderr: r.stderr ?? "" };
@@ -3033,10 +3374,10 @@ var musher = async (o) => {
3033
3374
  init_config();
3034
3375
 
3035
3376
  // src/schedule.ts
3036
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
3037
- import { join as join7 } from "node:path";
3377
+ import { existsSync as existsSync10, mkdirSync as mkdirSync9, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "node:fs";
3378
+ import { join as join8 } from "node:path";
3038
3379
  var REVERT_LABEL = "ai.sleds.revert";
3039
- var plistPath = (home) => join7(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
3380
+ var plistPath = (home) => join8(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
3040
3381
  var xmlEscape2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3041
3382
  var buildLaunchdPlist = (sledsArgv, deadline) => {
3042
3383
  const args = [...sledsArgv, "off"].map((a) => ` <string>${xmlEscape2(a)}</string>`).join(`
@@ -3075,9 +3416,9 @@ var scheduleRevert = (d, deadlineMs) => {
3075
3416
  if (d.platform === "darwin") {
3076
3417
  const p = plistPath(d.home);
3077
3418
  try {
3078
- mkdirSync8(join7(d.home, "Library", "LaunchAgents"), { recursive: true });
3419
+ mkdirSync9(join8(d.home, "Library", "LaunchAgents"), { recursive: true });
3079
3420
  d.exec("launchctl", ["unload", p]);
3080
- writeFileSync8(p, buildLaunchdPlist(d.sledsArgv, deadline));
3421
+ writeFileSync9(p, buildLaunchdPlist(d.sledsArgv, deadline));
3081
3422
  const r = d.exec("launchctl", ["load", p]);
3082
3423
  if (r.ok)
3083
3424
  return { kind: "launchd", detail: `launchd agent ${REVERT_LABEL}` };
@@ -3104,7 +3445,7 @@ var scheduleRevert = (d, deadlineMs) => {
3104
3445
  var cancelRevert = (d, kind) => {
3105
3446
  if (kind === "launchd" || d.platform === "darwin") {
3106
3447
  const p = plistPath(d.home);
3107
- if (existsSync9(p)) {
3448
+ if (existsSync10(p)) {
3108
3449
  d.exec("launchctl", ["unload", p]);
3109
3450
  rmSync3(p, { force: true });
3110
3451
  }
@@ -3173,7 +3514,7 @@ var on = (o) => {
3173
3514
  return 2;
3174
3515
  }
3175
3516
  if (cfg.org && !cfg.org.capabilities.upstreamKey) {
3176
- 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.`);
3517
+ error(`${cfg.org.name} has no platform key, so there is no platform key to switch to. Hold until the reset, or hand off with /sled 5. An admin can add a key on the dashboard.`);
3177
3518
  return 2;
3178
3519
  }
3179
3520
  const url = (o.url ?? cfg.proxyUrl ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL2).replace(/\/+$/, "");
@@ -3216,7 +3557,7 @@ var on = (o) => {
3216
3557
  next.reason = o.reason?.trim() || undefined;
3217
3558
  next.schedule = schedule;
3218
3559
  writeConfig(o.home, next);
3219
- log(`Sleds on. Claude Code now runs on the org key through ${url}. Restart claude to apply.`);
3560
+ log(`Sleds on. Claude Code now runs on the platform key through ${url}. Restart claude to apply.`);
3220
3561
  if (untilResetMs !== undefined) {
3221
3562
  log(`Reverts at ${formatLocal(untilResetMs, o.tz)} — run \`sleds off\` earlier if you want.`);
3222
3563
  if (schedule === undefined || schedule === "manual") {
@@ -3346,14 +3687,14 @@ var seatEvent = async (o) => {
3346
3687
 
3347
3688
  // src/commands/seatSync.ts
3348
3689
  import { execFileSync as execFileSync2 } from "node:child_process";
3349
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync8, readdirSync as readdirSync2, writeFileSync as writeFileSync9 } from "node:fs";
3690
+ import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, writeFileSync as writeFileSync10 } from "node:fs";
3350
3691
  import { platform } from "node:os";
3351
- import { join as join9 } from "node:path";
3692
+ import { join as join10 } from "node:path";
3352
3693
 
3353
3694
  // src/cookieDb.ts
3354
3695
  import { copyFileSync, rmSync as rmSync4 } from "node:fs";
3355
3696
  import { tmpdir } from "node:os";
3356
- import { join as join8 } from "node:path";
3697
+ import { join as join9 } from "node:path";
3357
3698
  var QUERY = "SELECT name, hex(encrypted_value) AS ev FROM cookies WHERE host_key LIKE '%claude.ai%'";
3358
3699
  var isBun = () => typeof globalThis.Bun !== "undefined";
3359
3700
  async function queryRows(dbPath) {
@@ -3382,7 +3723,7 @@ function nodeSqlite() {
3382
3723
  return mod;
3383
3724
  }
3384
3725
  async function readCookieRows(dbPath) {
3385
- const copy = join8(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
3726
+ const copy = join9(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
3386
3727
  copyFileSync(dbPath, copy);
3387
3728
  try {
3388
3729
  const rows = await queryRows(copy);
@@ -3658,7 +3999,7 @@ function findFile(root, name) {
3658
3999
  const stack = [root];
3659
4000
  while (stack.length > 0) {
3660
4001
  const dir = stack.pop();
3661
- if (!dir || !existsSync10(dir))
4002
+ if (!dir || !existsSync11(dir))
3662
4003
  continue;
3663
4004
  let entries;
3664
4005
  try {
@@ -3667,7 +4008,7 @@ function findFile(root, name) {
3667
4008
  continue;
3668
4009
  }
3669
4010
  for (const e of entries) {
3670
- const p = join9(dir, e.name);
4011
+ const p = join10(dir, e.name);
3671
4012
  if (e.isDirectory())
3672
4013
  stack.push(p);
3673
4014
  else if (e.name === name)
@@ -3678,8 +4019,8 @@ function findFile(root, name) {
3678
4019
  }
3679
4020
  function findCookieDb(home, source) {
3680
4021
  const b = BROWSERS[source.browser];
3681
- const root = join9(home, APP_SUPPORT, b.appDir);
3682
- if (!existsSync10(root))
4022
+ const root = join10(home, APP_SUPPORT, b.appDir);
4023
+ if (!existsSync11(root))
3683
4024
  throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
3684
4025
  if (!b.profiled) {
3685
4026
  const db2 = findFile(root, "Cookies");
@@ -3687,15 +4028,15 @@ function findCookieDb(home, source) {
3687
4028
  throw new Error(`no cookie store under ${root}`);
3688
4029
  return db2;
3689
4030
  }
3690
- const localStatePath = join9(root, "Local State");
3691
- const localState = existsSync10(localStatePath) ? readFileSync8(localStatePath, "utf8") : null;
4031
+ const localStatePath = join10(root, "Local State");
4032
+ const localState = existsSync11(localStatePath) ? readFileSync9(localStatePath, "utf8") : null;
3692
4033
  const wanted = source.profile ?? "Default";
3693
4034
  const { dir, ambiguous } = resolveProfileDir(localState, wanted);
3694
4035
  if (ambiguous)
3695
4036
  throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
3696
4037
  if (!dir)
3697
4038
  throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
3698
- const db = cookieDbCandidates(join9(root, dir)).find((p) => existsSync10(p));
4039
+ const db = cookieDbCandidates(join10(root, dir)).find((p) => existsSync11(p));
3699
4040
  if (!db)
3700
4041
  throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
3701
4042
  return db;
@@ -3738,7 +4079,7 @@ function fetchSeatUsage(jar, org, now, run2) {
3738
4079
  }
3739
4080
  function launchAgentPlist(bun, script, home, source) {
3740
4081
  const label = launchLabel(source);
3741
- const log = join9(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
4082
+ const log = join10(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
3742
4083
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3743
4084
  const args = [bun, script, "seat-sync", ...sourceArgs(source)].map((a) => ` <string>${esc(a)}</string>`).join(`
3744
4085
  `);
@@ -3760,7 +4101,7 @@ ${args}
3760
4101
  `;
3761
4102
  }
3762
4103
  function plistPath2(home, source) {
3763
- return join9(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
4104
+ return join10(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
3764
4105
  }
3765
4106
  function install(home, argv, source, log) {
3766
4107
  const bun = argv[0] ?? process.execPath;
@@ -3769,10 +4110,10 @@ function install(home, argv, source, log) {
3769
4110
  log("cannot resolve the sleds script path to install the timer");
3770
4111
  return 1;
3771
4112
  }
3772
- const dir = join9(home, "Library", "LaunchAgents");
3773
- mkdirSync9(dir, { recursive: true });
4113
+ const dir = join10(home, "Library", "LaunchAgents");
4114
+ mkdirSync10(dir, { recursive: true });
3774
4115
  const path = plistPath2(home, source);
3775
- writeFileSync9(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
4116
+ writeFileSync10(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
3776
4117
  try {
3777
4118
  execFileSync2("launchctl", ["unload", path], { stdio: "ignore" });
3778
4119
  } catch {}
@@ -3785,7 +4126,7 @@ function uninstall(home, source, log) {
3785
4126
  try {
3786
4127
  execFileSync2("launchctl", ["unload", "-w", path], { stdio: "ignore" });
3787
4128
  } catch {}
3788
- log(existsSync10(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
4129
+ log(existsSync11(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
3789
4130
  return 0;
3790
4131
  }
3791
4132
  var seatSync = async (o) => {
@@ -3881,7 +4222,7 @@ async function postSeat(proxyUrl, token, body, fetchFn) {
3881
4222
 
3882
4223
  // src/commands/specEvent.ts
3883
4224
  init_meter();
3884
- import { readFileSync as readFileSync9, statSync } from "node:fs";
4225
+ import { readFileSync as readFileSync10, statSync } from "node:fs";
3885
4226
  import { basename as basename2, isAbsolute, relative } from "node:path";
3886
4227
  var HOOK_STDIN_TIMEOUT_MS2 = 1000;
3887
4228
  var ESTIMATE_TIMEOUT_MS = 4000;
@@ -3937,7 +4278,7 @@ function readFileOrNull(path) {
3937
4278
  try {
3938
4279
  if (statSync(path).size > 2000000)
3939
4280
  return null;
3940
- return readFileSync9(path, "utf8");
4281
+ return readFileSync10(path, "utf8");
3941
4282
  } catch {
3942
4283
  return null;
3943
4284
  }
@@ -3982,7 +4323,7 @@ var specEvent = async (o) => {
3982
4323
  // src/commands/status.ts
3983
4324
  init_config();
3984
4325
  init_settings();
3985
- import { existsSync as existsSync11 } from "node:fs";
4326
+ import { existsSync as existsSync12 } from "node:fs";
3986
4327
  var checkHealth2 = async (url, fetchFn = fetch, timeoutMs = 1000) => {
3987
4328
  try {
3988
4329
  const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
@@ -4040,9 +4381,11 @@ var formatStatus = (s) => {
4040
4381
  }
4041
4382
  }
4042
4383
  if (s.email)
4043
- lines.push(row2("Seat:", s.email));
4384
+ lines.push(row2("Seat:", `${s.email}${s.agentKind ? ` · ${s.agentKind}` : ""}`));
4385
+ else if (s.agentKind)
4386
+ lines.push(row2("Agent:", s.agentKind));
4044
4387
  if (s.org)
4045
- 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"}`));
4388
+ 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" ? "platform key" : "shared key"}`));
4046
4389
  if (s.wiring)
4047
4390
  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"));
4048
4391
  if (s.mode === "off") {
@@ -4054,7 +4397,7 @@ var status = async (o) => {
4054
4397
  const log = o.log ?? console.log;
4055
4398
  const nowMs = (o.now ?? Date.now)();
4056
4399
  const cfg = readConfig(o.home);
4057
- const settings = existsSync11(settingsPath(o.home)) ? readSettings(o.home) : {};
4400
+ const settings = existsSync12(settingsPath(o.home)) ? readSettings(o.home) : {};
4058
4401
  const inSettings = readProxy(settings);
4059
4402
  const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
4060
4403
  const token = inSettings?.token ?? cfg.token;
@@ -4076,6 +4419,8 @@ var status = async (o) => {
4076
4419
  info.tokenMasked = maskToken(token);
4077
4420
  if (cfg.email)
4078
4421
  info.email = cfg.email;
4422
+ if (cfg.agentKind)
4423
+ info.agentKind = cfg.agentKind;
4079
4424
  if (cfg.wiring)
4080
4425
  info.wiring = cfg.wiring;
4081
4426
  if (cfg.org)
@@ -4163,19 +4508,69 @@ var rememberSeat = (o, window, session, now) => {
4163
4508
  return posts;
4164
4509
  };
4165
4510
 
4511
+ // src/commands/uninstall.ts
4512
+ import { spawnSync as spawnSync4 } from "node:child_process";
4513
+ import { existsSync as existsSync14, rmSync as rmSync5 } from "node:fs";
4514
+
4515
+ // src/codex.ts
4516
+ init_config();
4517
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
4518
+ import { dirname as dirname5, join as join11 } from "node:path";
4519
+ var PROVIDER_ID2 = "sleds";
4520
+ var codexDir2 = (home) => join11(home, ".codex");
4521
+ var codexConfigPath2 = (home) => join11(codexDir2(home), "config.toml");
4522
+ function removeCodexProvider2(toml, o = {}) {
4523
+ const lines = toml.split(`
4524
+ `);
4525
+ const out = [];
4526
+ let skipping = false;
4527
+ for (const line of lines) {
4528
+ if (/^\s*\[/.test(line))
4529
+ skipping = line.trim() === `[model_providers.${PROVIDER_ID2}]`;
4530
+ if (!skipping)
4531
+ out.push(line);
4532
+ }
4533
+ let text = out.join(`
4534
+ `);
4535
+ if (!o.keepPrior) {
4536
+ const prior = text.match(/^# sleds-prior-model-provider = "([^"]*)"\s*\n/m);
4537
+ if (prior) {
4538
+ text = text.replace(prior[0], "");
4539
+ text = text.replace(/^model_provider\s*=\s*"sleds"\s*$/m, `model_provider = "${prior[1]}"`);
4540
+ } else {
4541
+ text = text.replace(/^model_provider\s*=\s*"sleds"\s*\n?/m, "");
4542
+ }
4543
+ }
4544
+ return text.replace(/\n{3,}/g, `
4545
+
4546
+ `);
4547
+ }
4548
+ function uninstallCodex(home) {
4549
+ const path = codexConfigPath2(home);
4550
+ if (!existsSync13(path))
4551
+ return null;
4552
+ const prior = readFileSync11(path, "utf8");
4553
+ const next = removeCodexProvider2(prior);
4554
+ if (next === prior)
4555
+ return null;
4556
+ writeFileSync11(path, next, { mode: 384 });
4557
+ return `removed the "${PROVIDER_ID2}" provider from ${path}`;
4558
+ }
4559
+
4166
4560
  // src/commands/uninstall.ts
4167
4561
  init_config();
4168
4562
  init_meter();
4169
- import { spawnSync as spawnSync3 } from "node:child_process";
4170
- import { existsSync as existsSync12, rmSync as rmSync5 } from "node:fs";
4171
4563
  init_settings();
4172
4564
  var defaultUnload = (plist) => {
4173
- spawnSync3("launchctl", ["unload", plist], { stdio: "ignore" });
4565
+ spawnSync4("launchctl", ["unload", plist], { stdio: "ignore" });
4174
4566
  };
4175
4567
  var uninstall2 = (o) => {
4568
+ const codexLine = uninstallCodex(o.home);
4569
+ if (codexLine)
4570
+ (o.log ?? console.log)(` ${codexLine}`);
4176
4571
  const log = o.log ?? console.log;
4177
4572
  const removed = [];
4178
- if (existsSync12(settingsPath(o.home))) {
4573
+ if (existsSync14(settingsPath(o.home))) {
4179
4574
  let settings = readSettings(o.home);
4180
4575
  let changed = false;
4181
4576
  if (hasEitherKey(settings)) {
@@ -4207,18 +4602,18 @@ var uninstall2 = (o) => {
4207
4602
  writeSettings(o.home, settings);
4208
4603
  }
4209
4604
  for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
4210
- if (existsSync12(file)) {
4605
+ if (existsSync14(file)) {
4211
4606
  rmSync5(file, { force: true });
4212
4607
  removed.push(file);
4213
4608
  }
4214
4609
  }
4215
4610
  const skill = skillDir(o.home);
4216
- if (existsSync12(skill)) {
4611
+ if (existsSync14(skill)) {
4217
4612
  rmSync5(skill, { recursive: true, force: true });
4218
4613
  removed.push(skill);
4219
4614
  }
4220
4615
  const plist = musherPlistPath(o.home);
4221
- if (existsSync12(plist)) {
4616
+ if (existsSync14(plist)) {
4222
4617
  (o.unloadAgent ?? defaultUnload)(plist);
4223
4618
  rmSync5(plist, { force: true });
4224
4619
  removed.push(`launchd agent ${musherLabel()}`);
@@ -4288,25 +4683,141 @@ var verify = async (o) => {
4288
4683
  return ok ? 0 : 1;
4289
4684
  };
4290
4685
 
4686
+ // src/config.ts
4687
+ init_settings();
4688
+ import { chmodSync as chmodSync2, existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync12, renameSync as renameSync5, writeFileSync as writeFileSync12 } from "node:fs";
4689
+ import { join as join12, resolve as resolve4 } from "node:path";
4690
+ var configDir2 = (home, env = process.env) => {
4691
+ const o = env.SLEDS_HOME?.trim();
4692
+ return o ? resolve4(home, o) : join12(home, ".config", "sleds");
4693
+ };
4694
+ var configPath2 = (home) => join12(configDir2(home), "config.json");
4695
+ var isObject10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4696
+ var optString2 = (v) => typeof v === "string" ? v : undefined;
4697
+ var parsePriorEnv2 = (v) => {
4698
+ if (!isObject10(v))
4699
+ return;
4700
+ const out = {};
4701
+ const url = optString2(v[BASE_URL_KEY]);
4702
+ const token = optString2(v[AUTH_TOKEN_KEY]);
4703
+ if (url !== undefined)
4704
+ out[BASE_URL_KEY] = url;
4705
+ if (token !== undefined)
4706
+ out[AUTH_TOKEN_KEY] = token;
4707
+ if (typeof v.hadEnv === "boolean")
4708
+ out.hadEnv = v.hadEnv;
4709
+ return out;
4710
+ };
4711
+ var parseSchedule2 = (v) => v === "launchd" || v === "systemd" || v === "manual" ? v : undefined;
4712
+ var parseConfig2 = (raw) => {
4713
+ if (!isObject10(raw))
4714
+ return {};
4715
+ const cfg = {};
4716
+ const token = optString2(raw.token);
4717
+ const proxyUrl = optString2(raw.proxyUrl);
4718
+ const email = optString2(raw.email);
4719
+ const since = optString2(raw.since);
4720
+ const untilReset = optString2(raw.untilReset);
4721
+ const reason = optString2(raw.reason);
4722
+ const priorEnv = parsePriorEnv2(raw.priorEnv);
4723
+ const schedule = parseSchedule2(raw.schedule);
4724
+ if (token !== undefined)
4725
+ cfg.token = token;
4726
+ if (proxyUrl !== undefined)
4727
+ cfg.proxyUrl = proxyUrl;
4728
+ if (email !== undefined)
4729
+ cfg.email = email;
4730
+ if (raw.mode === "on" || raw.mode === "off")
4731
+ cfg.mode = raw.mode;
4732
+ if (since !== undefined)
4733
+ cfg.since = since;
4734
+ if (untilReset !== undefined)
4735
+ cfg.untilReset = untilReset;
4736
+ if (reason !== undefined)
4737
+ cfg.reason = reason;
4738
+ if (priorEnv !== undefined)
4739
+ cfg.priorEnv = priorEnv;
4740
+ if (schedule !== undefined)
4741
+ cfg.schedule = schedule;
4742
+ if (raw.priorStatusLine !== undefined)
4743
+ cfg.priorStatusLine = raw.priorStatusLine;
4744
+ if (raw.wiring === "seat-first" || raw.wiring === "proxy-always")
4745
+ cfg.wiring = raw.wiring;
4746
+ if (raw.agentKind === "claude-code" || raw.agentKind === "codex")
4747
+ cfg.agentKind = raw.agentKind;
4748
+ const org = parseOrgInfo2(raw.org);
4749
+ if (org)
4750
+ cfg.org = org;
4751
+ const musher2 = parseMusher2(raw.musher);
4752
+ if (musher2)
4753
+ cfg.musher = musher2;
4754
+ return cfg;
4755
+ };
4756
+ var parseMusher2 = (raw) => {
4757
+ if (!isObject10(raw))
4758
+ return;
4759
+ const out = {};
4760
+ const pm = optString2(raw.permissionMode);
4761
+ const cp = optString2(raw.claudePath);
4762
+ if (pm)
4763
+ out.permissionMode = pm;
4764
+ if (cp)
4765
+ out.claudePath = cp;
4766
+ if (typeof raw.timeoutMinutes === "number" && raw.timeoutMinutes > 0)
4767
+ out.timeoutMinutes = raw.timeoutMinutes;
4768
+ if (raw.agent === "launchd" || raw.agent === "none")
4769
+ out.agent = raw.agent;
4770
+ return out;
4771
+ };
4772
+ var parseOrgInfo2 = (raw) => {
4773
+ if (!isObject10(raw) || !isObject10(raw.capabilities))
4774
+ return;
4775
+ const name = optString2(raw.name);
4776
+ const slug2 = optString2(raw.slug);
4777
+ const plan = raw.plan;
4778
+ const c = raw.capabilities;
4779
+ const src = c.upstreamSource;
4780
+ if (name === undefined || slug2 === undefined || plan !== "team" && plan !== "enterprise" && plan !== "api" && plan !== "individual" || src !== "own" && src !== "shared" && src !== "none")
4781
+ return;
4782
+ return {
4783
+ name,
4784
+ slug: slug2,
4785
+ plan,
4786
+ capabilities: {
4787
+ upstreamKey: c.upstreamKey === true,
4788
+ adminKey: c.adminKey === true,
4789
+ seats: c.seats !== false,
4790
+ upstreamSource: src
4791
+ },
4792
+ mode: raw.mode === "proxy-always" ? "proxy-always" : "seat-first"
4793
+ };
4794
+ };
4795
+ var readConfig2 = (home) => {
4796
+ const p = configPath2(home);
4797
+ if (!existsSync15(p))
4798
+ return {};
4799
+ return parseConfig2(JSON.parse(readFileSync12(p, "utf8")));
4800
+ };
4801
+
4291
4802
  // src/schedule.ts
4292
- import { spawnSync as spawnSync4 } from "node:child_process";
4803
+ import { spawnSync as spawnSync5 } from "node:child_process";
4293
4804
  var defaultExec = (cmd, args) => {
4294
- const r = spawnSync4(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
4805
+ const r = spawnSync5(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
4295
4806
  if (r.error)
4296
4807
  return { ok: false, stderr: r.error.message };
4297
4808
  return { ok: r.status === 0, stderr: r.stderr ?? "" };
4298
4809
  };
4299
4810
 
4300
4811
  // src/settings.ts
4301
- import { dirname as dirname4, join as join10, resolve as resolve4 } from "node:path";
4812
+ import { dirname as dirname6, join as join13, resolve as resolve5 } from "node:path";
4302
4813
  var withSeatEnv2 = (command, home, env = process.env) => {
4303
4814
  const parts = [];
4304
4815
  const sh = env.SLEDS_HOME?.trim();
4305
4816
  const cc = env.CLAUDE_CONFIG_DIR?.trim();
4306
4817
  if (sh)
4307
- parts.push(`SLEDS_HOME=${shellWord2(resolve4(home, sh))}`);
4818
+ parts.push(`SLEDS_HOME=${shellWord2(resolve5(home, sh))}`);
4308
4819
  if (cc)
4309
- parts.push(`CLAUDE_CONFIG_DIR=${shellWord2(resolve4(home, cc))}`);
4820
+ parts.push(`CLAUDE_CONFIG_DIR=${shellWord2(resolve5(home, cc))}`);
4310
4821
  parts.push(command);
4311
4822
  return parts.join(" ");
4312
4823
  };
@@ -4321,14 +4832,16 @@ commands:
4321
4832
  --url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
4322
4833
  --no-probe skip the /healthz check of the proxy URL
4323
4834
  --seat-first keep Claude Code on your seat; the proxy only at a wall (default for seat plans)
4324
- --proxy-always every request through the proxy on the org key (default for API-plan orgs)
4835
+ --proxy-always every request through the proxy on the platform key (default for API-plan orgs)
4325
4836
  --non-interactive never prompt; exit 2 if no token is available
4837
+ --agent codex Codex CLI instead of Claude Code: writes ~/.codex/config.toml
4838
+ (provider "sleds" \u2192 the proxy, key from $SLEDS_TOKEN); no hooks/status line
4326
4839
  --signup no org? create one: (1) just you \u2014 your email + your own Anthropic
4327
4840
  API key (validated, stored encrypted, the only key it spends), or
4328
4841
  (2) your team \u2014 work email + team name, platform key optional
4329
4842
  --team "<name>" with --signup: create the team org (you become its owner)
4330
4843
  --email <you@work.com> with --signup: skip the email prompt
4331
- on Hit a limit on your seat? Switch Claude Code to the org key through the proxy.
4844
+ on Hit a limit on your seat? Switch Claude Code to the platform key through the proxy.
4332
4845
  --until-reset <when> revert at this time: +5h, +90m, or an ISO timestamp
4333
4846
  --reason "<text>" note shown by \`sleds status\`
4334
4847
  --no-schedule record the deadline but do not install a launchd/systemd revert
@@ -4339,6 +4852,9 @@ commands:
4339
4852
  musher The agent that runs deferred jobs here: --install / --uninstall / --status,
4340
4853
  --claude <path>, --mode <permission-mode> (default acceptEdits), --timeout <min>.
4341
4854
  run <jobId> Run one deferred job on this Mac now (the manual musher).
4855
+ codex [...] Run Codex CLI through this seat (sets SLEDS_TOKEN + OPENAI_BASE_URL for the run).
4856
+ handoffs The team's open hand-offs (yours marked); pickup <id> takes one here; withdraw <id>
4857
+ takes back one you parked.
4342
4858
  verify <jobId | SLEDS-Attest line> Check a finished job's attestation: which model built it,
4343
4859
  signed by the org's key, verified locally.
4344
4860
  off Switch Claude Code back to your seat. Restores whatever \`on\` replaced.
@@ -4422,7 +4938,39 @@ var main = async (argv) => {
4422
4938
  exec: defaultExec
4423
4939
  };
4424
4940
  switch (cmd) {
4425
- case "init":
4941
+ case "handoffs":
4942
+ return handoffsCmd({ home, argv: argv.slice(1) });
4943
+ case "pickup":
4944
+ return pickupCmd({ home, argv: argv.slice(1) });
4945
+ case "withdraw":
4946
+ return withdrawCmd({ home, argv: argv.slice(1) });
4947
+ case "codex":
4948
+ return runCodex(home, [...argv.slice(1)]);
4949
+ case "init": {
4950
+ if (stringFlag(flags, "agent") === "codex") {
4951
+ const token = stringFlag(flags, "token") ?? process.env.SLEDS_TOKEN;
4952
+ const url = stringFlag(flags, "url") ?? process.env.SLEDS_PROXY_URL ?? readConfig2(home).proxyUrl;
4953
+ if (!token || !token.startsWith("sled_")) {
4954
+ console.error("sleds init --agent codex needs --token <sled_\u2026> (from Settings \u203A Your seat & tools).");
4955
+ return 2;
4956
+ }
4957
+ if (!url) {
4958
+ console.error("sleds init --agent codex needs --url https://proxy.sleds.ai");
4959
+ return 2;
4960
+ }
4961
+ const proxyUrl = url.replace(/\/+$/, "");
4962
+ const info = await fetchSeatInfo(proxyUrl, token);
4963
+ const wrote = initCodex({ home, token, proxyUrl, email: info.email });
4964
+ if (info.org)
4965
+ console.log(`Seat ${info.email ?? "?"} \xB7 ${info.org.name} \xB7 ${info.org.plan}. Codex requests go to ${proxyUrl}/v1 and are metered to this seat.`);
4966
+ console.log("Codex now runs through SLEDS on this seat.");
4967
+ console.log("");
4968
+ for (const w of wrote)
4969
+ console.log(` ${w}`);
4970
+ console.log("");
4971
+ console.log("Start it with `sleds codex` (sets SLEDS_TOKEN for the run), or export SLEDS_TOKEN yourself and run `codex`.");
4972
+ return 0;
4973
+ }
4426
4974
  return init({
4427
4975
  home,
4428
4976
  token: stringFlag(flags, "token"),
@@ -4452,6 +5000,7 @@ var main = async (argv) => {
4452
5000
  seatEventCommand: seatEventCommand(sledsArgv()),
4453
5001
  specEventCommand: specEventCommand(sledsArgv())
4454
5002
  });
5003
+ }
4455
5004
  case "hooks":
4456
5005
  return hooks({
4457
5006
  home,
@@ -4503,7 +5052,7 @@ var main = async (argv) => {
4503
5052
  case "version":
4504
5053
  case "--version":
4505
5054
  case "-v":
4506
- console.log("sleds 0.1.16");
5055
+ console.log("sleds 0.1.17");
4507
5056
  return 0;
4508
5057
  default:
4509
5058
  console.error(`unknown command: ${cmd}