@sleds/cli 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/sleds.mjs +670 -153
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,6 +29,7 @@ sleds on hit a limit on your seat? move Claude Code onto the org key
29
29
  sleds off back to your seat
30
30
  sleds seat-sync read your Claude subscription's own limits into SLEDS (macOS)
31
31
  sleds bar the same numbers for the menu bar (SwiftBar); --install links the plugin
32
+ sleds doctor check the whole install: seat, Claude settings, proxy, hooks, Node, plugin, timer
32
33
  sleds mcp MCP server: usage / estimate / overnight / jobs, for Cowork and other clients
33
34
  ```
34
35
 
package/dist/sleds.mjs CHANGED
@@ -5,7 +5,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
 
6
6
  // src/index.ts
7
7
  import { realpathSync } from "fs";
8
- import { homedir as homedir2, platform as platform2 } from "os";
8
+ import { homedir as homedir3, platform as platform2 } from "os";
9
9
  import { fileURLToPath } from "url";
10
10
 
11
11
  // src/commands/bar.ts
@@ -20,7 +20,7 @@ import {
20
20
  writeFileSync as writeFileSync5
21
21
  } from "node:fs";
22
22
  import { homedir } from "node:os";
23
- import { join as join5 } from "node:path";
23
+ import { join as join5, resolve as resolve3 } from "node:path";
24
24
 
25
25
  // src/meter.ts
26
26
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
@@ -32,7 +32,7 @@ import { join as join2, resolve as resolve2 } from "node:path";
32
32
 
33
33
  // src/settings.ts
34
34
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
35
- import { join, resolve } from "node:path";
35
+ import { dirname, join, resolve } from "node:path";
36
36
  var BASE_URL_KEY = "ANTHROPIC_BASE_URL";
37
37
  var AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN";
38
38
  var claudeDir = (home, env = process.env) => {
@@ -42,7 +42,10 @@ var claudeDir = (home, env = process.env) => {
42
42
  var hasHomeOverride = (env = process.env) => Boolean(env.SLEDS_HOME?.trim() || env.CLAUDE_CONFIG_DIR?.trim());
43
43
  var settingsPath = (home) => join(claudeDir(home), "settings.json");
44
44
  var backupPath = (home) => join(claudeDir(home), "settings.json.sleds-backup");
45
- var tokenPath = (home) => join(home, ".sleds-token");
45
+ var tokenPath = (home, env = process.env) => {
46
+ const o = env.SLEDS_HOME?.trim();
47
+ return o ? join(resolve(home, o), ".sleds-token") : join(home, ".sleds-token");
48
+ };
46
49
  var skillDir = (home) => join(claudeDir(home), "skills", "sled");
47
50
  var skillPath = (home) => join(skillDir(home), "SKILL.md");
48
51
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -210,6 +213,7 @@ var readTokenFile = (home) => {
210
213
  return t.length > 0 ? t : null;
211
214
  };
212
215
  var writeTokenFile = (home, token) => {
216
+ mkdirSync(dirname(tokenPath(home)), { recursive: true, mode: 448 });
213
217
  writeFileSync(tokenPath(home), `${token}
214
218
  `, { mode: 384 });
215
219
  };
@@ -570,13 +574,15 @@ var formatBar = (m, seat, now) => {
570
574
  lines.push("Refresh | refresh=true");
571
575
  return lines;
572
576
  };
573
- var pluginScript = (nodePath, cliPath) => [
577
+ var pluginScript = (nodePath, cliPath, env = {}) => [
574
578
  "#!/bin/bash",
575
579
  "# SLEDS menu-bar meter for SwiftBar — written by `sleds bar --install`; re-run it after moving the CLI.",
576
580
  "# <swiftbar.title>SLEDS</swiftbar.title>",
577
581
  "# <swiftbar.hideAbout>true</swiftbar.hideAbout>",
578
582
  "# <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>",
579
583
  "# <swiftbar.hideSwiftBar>true</swiftbar.hideSwiftBar>",
584
+ ...env.sledsHome ? [`export SLEDS_HOME=${shellQuote(env.sledsHome)}`] : ["unset SLEDS_HOME"],
585
+ ...env.claudeConfigDir ? [`export CLAUDE_CONFIG_DIR=${shellQuote(env.claudeConfigDir)}`] : ["unset CLAUDE_CONFIG_DIR"],
580
586
  'export PATH="$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"',
581
587
  `if [ -x ${shellQuote(nodePath)} ] && [ -f ${shellQuote(cliPath)} ]; then`,
582
588
  ` exec ${shellQuote(nodePath)} ${shellQuote(cliPath)} bar`,
@@ -817,7 +823,23 @@ var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) =>
817
823
  });
818
824
 
819
825
  // src/commands/bar.ts
826
+ function parseInstallArgs(argv, env) {
827
+ const flag = (name2) => {
828
+ const i = argv.indexOf(`--${name2}`);
829
+ const v = i >= 0 ? argv[i + 1] : undefined;
830
+ return v && !v.startsWith("--") ? v : undefined;
831
+ };
832
+ const sledsHome = flag("home") ?? (env.SLEDS_HOME?.trim() || undefined);
833
+ const claudeConfigDir = flag("claude-config") ?? (env.CLAUDE_CONFIG_DIR?.trim() || undefined);
834
+ const name = flag("name");
835
+ return {
836
+ ...sledsHome ? { sledsHome } : {},
837
+ ...claudeConfigDir ? { claudeConfigDir } : {},
838
+ ...name ? { name } : {}
839
+ };
840
+ }
820
841
  var PLUGIN_NAME = "sleds.1m.sh";
842
+ var pluginName = (name) => name ? `sleds-${name}.1m.sh` : PLUGIN_NAME;
821
843
  var bar = async (o) => {
822
844
  const log = o.log ?? console.log;
823
845
  const argv = o.argv ?? [];
@@ -849,22 +871,28 @@ function swiftBarPluginDir() {
849
871
  return null;
850
872
  }
851
873
  }
852
- var pluginPath = (home) => join5(configDir(home), "swiftbar", PLUGIN_NAME);
874
+ var pluginPath = (home, name) => join5(configDir(home, {}), "swiftbar", pluginName(name));
853
875
  function install(o, log) {
854
- const script = pluginPath(o.home);
876
+ const opts = parseInstallArgs(o.argv ?? [], o.env ?? process.env);
877
+ const script = pluginPath(o.home, opts.name);
855
878
  mkdirSync5(join5(script, ".."), { recursive: true, mode: 448 });
856
- writeFileSync5(script, pluginScript(o.nodePath ?? process.execPath, o.cliPath ?? process.argv[1] ?? ""), {
879
+ writeFileSync5(script, pluginScript(o.nodePath ?? process.execPath, o.cliPath ?? process.argv[1] ?? "", {
880
+ ...opts.sledsHome ? { sledsHome: resolve3(o.home, opts.sledsHome) } : {},
881
+ ...opts.claudeConfigDir ? { claudeConfigDir: resolve3(o.home, opts.claudeConfigDir) } : {}
882
+ }), {
857
883
  mode: 493
858
884
  });
859
885
  chmodSync2(script, 493);
860
886
  log(`wrote ${script}`);
887
+ if (opts.sledsHome || opts.claudeConfigDir)
888
+ log(`tracks the seat at ${[opts.sledsHome && `SLEDS_HOME=${opts.sledsHome}`, opts.claudeConfigDir && `CLAUDE_CONFIG_DIR=${opts.claudeConfigDir}`].filter(Boolean).join(" ")}`);
861
889
  const dir = (o.swiftBarPluginDir ?? swiftBarPluginDir)();
862
890
  if (!dir) {
863
891
  log("SwiftBar is not set up yet: `brew install --cask swiftbar`, launch it once and pick a plugin");
864
892
  log("folder, then run `sleds bar --install` again — or symlink the file above into that folder.");
865
893
  return 0;
866
894
  }
867
- const link = join5(dir, PLUGIN_NAME);
895
+ const link = join5(dir, pluginName(opts.name));
868
896
  try {
869
897
  mkdirSync5(dir, { recursive: true });
870
898
  if (existsSync5(link) || isDanglingLink(link))
@@ -880,14 +908,15 @@ function install(o, log) {
880
908
  }
881
909
  }
882
910
  function uninstall(o, log) {
911
+ const { name } = parseInstallArgs(o.argv ?? [], {});
883
912
  const dir = (o.swiftBarPluginDir ?? swiftBarPluginDir)();
884
- const link = dir ? join5(dir, PLUGIN_NAME) : null;
913
+ const link = dir ? join5(dir, pluginName(name)) : null;
885
914
  if (link && (existsSync5(link) || isDanglingLink(link))) {
886
915
  unlinkSync(link);
887
916
  log(`removed ${link}`);
888
917
  } else
889
918
  log("no SwiftBar plugin link to remove");
890
- const script = pluginPath(o.home);
919
+ const script = pluginPath(o.home, name);
891
920
  if (existsSync5(script)) {
892
921
  unlinkSync(script);
893
922
  log(`removed ${script}`);
@@ -902,6 +931,323 @@ var isDanglingLink = (p) => {
902
931
  }
903
932
  };
904
933
 
934
+ // src/commands/doctor.ts
935
+ import { execFileSync as execFileSync3 } from "node:child_process";
936
+ import { existsSync as existsSync6, lstatSync as lstatSync2, readFileSync as readFileSync5, readdirSync, readlinkSync } from "node:fs";
937
+ import { join as join7 } from "node:path";
938
+
939
+ // src/commands/bar.ts
940
+ import { execFileSync as execFileSync2 } from "node:child_process";
941
+ import { homedir as homedir2 } from "node:os";
942
+ import { join as join6, resolve as resolve4 } from "node:path";
943
+ var PLUGIN_NAME2 = "sleds.1m.sh";
944
+ var pluginName2 = (name) => name ? `sleds-${name}.1m.sh` : PLUGIN_NAME2;
945
+ function swiftBarPluginDir2() {
946
+ try {
947
+ const out = execFileSync2("defaults", ["read", "com.ameba.SwiftBar", "PluginDirectory"], {
948
+ encoding: "utf8",
949
+ stdio: ["ignore", "pipe", "ignore"]
950
+ }).trim();
951
+ return out ? out.replace(/^~(?=$|\/)/, homedir2()) : null;
952
+ } catch {
953
+ return null;
954
+ }
955
+ }
956
+ var pluginPath2 = (home, name) => join6(configDir(home, {}), "swiftbar", pluginName2(name));
957
+
958
+ // src/switch.ts
959
+ var isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
960
+ var envOf2 = (settings) => isObject5(settings.env) ? settings.env : {};
961
+ var capturePriorEnv = (settings) => {
962
+ const env = envOf2(settings);
963
+ const prior = { hadEnv: isObject5(settings.env) };
964
+ const url = env[BASE_URL_KEY];
965
+ const token = env[AUTH_TOKEN_KEY];
966
+ if (typeof url === "string")
967
+ prior[BASE_URL_KEY] = url;
968
+ if (typeof token === "string")
969
+ prior[AUTH_TOKEN_KEY] = token;
970
+ return prior;
971
+ };
972
+ var switchOn = (settings, cfg) => applyProxy(settings, cfg);
973
+ var switchOff = (settings, prior) => {
974
+ const hadEnv = prior.hadEnv === true;
975
+ const stripped = removeProxy(settings);
976
+ const env = { ...envOf2(stripped) };
977
+ const url = prior[BASE_URL_KEY];
978
+ const token = prior[AUTH_TOKEN_KEY];
979
+ if (url !== undefined)
980
+ env[BASE_URL_KEY] = url;
981
+ if (token !== undefined)
982
+ env[AUTH_TOKEN_KEY] = token;
983
+ if (Object.keys(env).length === 0 && !hadEnv) {
984
+ const { env: _env, ...rest } = stripped;
985
+ return rest;
986
+ }
987
+ return { ...stripped, env };
988
+ };
989
+ var isPointedAtProxy = (settings) => {
990
+ const env = envOf2(settings);
991
+ return BASE_URL_KEY in env || AUTH_TOKEN_KEY in env;
992
+ };
993
+ var RELATIVE = /^\+(?:(\d+)h)?(?:(\d+)m)?$/;
994
+ var parseUntilReset = (input, nowMs) => {
995
+ const trimmed = input.trim();
996
+ const rel = RELATIVE.exec(trimmed);
997
+ let deadline;
998
+ if (rel) {
999
+ const hours = Number(rel[1] ?? 0);
1000
+ const minutes = Number(rel[2] ?? 0);
1001
+ if (hours === 0 && minutes === 0) {
1002
+ throw new Error(`--until-reset: "${input}" is zero; use something like +5h or +90m`);
1003
+ }
1004
+ deadline = nowMs + (hours * 60 + minutes) * 60000;
1005
+ } else {
1006
+ deadline = Date.parse(trimmed);
1007
+ if (Number.isNaN(deadline)) {
1008
+ throw new Error(`--until-reset: "${input}" is not +<n>h[<n>m] or an ISO timestamp`);
1009
+ }
1010
+ }
1011
+ if (deadline <= nowMs) {
1012
+ throw new Error(`--until-reset: ${trimmed} is in the past`);
1013
+ }
1014
+ return deadline;
1015
+ };
1016
+ var formatLocal = (ms, tz) => new Intl.DateTimeFormat("en-US", {
1017
+ month: "short",
1018
+ day: "numeric",
1019
+ hour: "numeric",
1020
+ minute: "2-digit",
1021
+ timeZoneName: "short",
1022
+ ...tz ? { timeZone: tz } : {}
1023
+ }).format(new Date(ms));
1024
+ var formatDuration = (ms) => {
1025
+ const total = Math.max(0, Math.round(ms / 60000));
1026
+ const h = Math.floor(total / 60);
1027
+ const m = total % 60;
1028
+ if (h === 0)
1029
+ return `${m}m`;
1030
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
1031
+ };
1032
+
1033
+ // src/commands/status.ts
1034
+ var checkHealth = async (url, fetchFn = fetch, timeoutMs = 1000) => {
1035
+ try {
1036
+ const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
1037
+ return res.ok ? "ok" : "unreachable";
1038
+ } catch {
1039
+ return "unreachable";
1040
+ }
1041
+ };
1042
+
1043
+ // src/commands/doctor.ts
1044
+ var NODE_MIN = "22.5.0";
1045
+ function nodeOk(version, min = NODE_MIN) {
1046
+ const parse = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
1047
+ const [a, b, c] = parse(version);
1048
+ const [x, y, z] = parse(min);
1049
+ return (a ?? 0) !== (x ?? 0) ? (a ?? 0) > (x ?? 0) : (b ?? 0) !== (y ?? 0) ? (b ?? 0) > (y ?? 0) : (c ?? 0) >= (z ?? 0);
1050
+ }
1051
+ function pluginEnv(body) {
1052
+ const out = {};
1053
+ const home = /^export SLEDS_HOME='([^']*)'/m.exec(body);
1054
+ const cfg = /^export CLAUDE_CONFIG_DIR='([^']*)'/m.exec(body);
1055
+ if (home?.[1])
1056
+ out.sledsHome = home[1];
1057
+ if (cfg?.[1])
1058
+ out.claudeConfigDir = cfg[1];
1059
+ return out;
1060
+ }
1061
+ var MARK = { ok: "ok ", warn: "warn", fail: "FAIL" };
1062
+ function formatDoctor(checks) {
1063
+ const lines = checks.map((c) => `${MARK[c.level]} ${c.name.padEnd(16)}${c.detail}`);
1064
+ const fixes = checks.filter((c) => c.level !== "ok" && c.fix);
1065
+ if (fixes.length > 0) {
1066
+ lines.push("");
1067
+ for (const c of fixes)
1068
+ lines.push(` ${c.name}: ${c.fix}`);
1069
+ }
1070
+ const fails = checks.filter((c) => c.level === "fail").length;
1071
+ const warns = checks.filter((c) => c.level === "warn").length;
1072
+ lines.push("");
1073
+ lines.push(fails === 0 && warns === 0 ? "All good." : `${fails} problem${fails === 1 ? "" : "s"}, ${warns} warning${warns === 1 ? "" : "s"}.`);
1074
+ return lines;
1075
+ }
1076
+ function defaultWhich(cmd) {
1077
+ try {
1078
+ return execFileSync3("which", ["-a", cmd], {
1079
+ encoding: "utf8",
1080
+ stdio: ["ignore", "pipe", "ignore"]
1081
+ }).split(`
1082
+ `).map((s) => s.trim()).filter(Boolean);
1083
+ } catch {
1084
+ return [];
1085
+ }
1086
+ }
1087
+ function defaultLaunchctl() {
1088
+ try {
1089
+ return execFileSync3("launchctl", ["list"], {
1090
+ encoding: "utf8",
1091
+ stdio: ["ignore", "pipe", "ignore"]
1092
+ });
1093
+ } catch {
1094
+ return "";
1095
+ }
1096
+ }
1097
+ async function runDoctor(o) {
1098
+ const env = o.env ?? process.env;
1099
+ const checks = [];
1100
+ const home = o.home;
1101
+ const overrides = [
1102
+ env.SLEDS_HOME?.trim() ? `SLEDS_HOME=${env.SLEDS_HOME.trim()}` : null,
1103
+ env.CLAUDE_CONFIG_DIR?.trim() ? `CLAUDE_CONFIG_DIR=${env.CLAUDE_CONFIG_DIR.trim()}` : null
1104
+ ].filter(Boolean);
1105
+ const cfgDir = configDir(home, env);
1106
+ const cfg = readConfig(home);
1107
+ const cfgExists = existsSync6(configPath(home));
1108
+ if (!cfgExists) {
1109
+ checks.push({
1110
+ level: "fail",
1111
+ name: "config",
1112
+ detail: `${configPath(home)} does not exist${overrides.length ? ` (this shell has ${overrides.join(" ")})` : ""}`,
1113
+ fix: overrides.length ? "unset SLEDS_HOME CLAUDE_CONFIG_DIR — or run `sleds init` for this seat" : "sleds init --token <sled_…>"
1114
+ });
1115
+ } else {
1116
+ checks.push({
1117
+ level: "ok",
1118
+ name: "config",
1119
+ detail: `${cfgDir}${cfg.email ? ` · ${cfg.email}` : ""}${cfg.token ? ` · ${maskToken(cfg.token)}` : ""}${overrides.length ? ` · ${overrides.join(" ")}` : ""}`
1120
+ });
1121
+ }
1122
+ const sPath = settingsPath(home);
1123
+ const settings = existsSync6(sPath) ? readSettings(home) : null;
1124
+ const pointed = settings ? readProxy(settings) : null;
1125
+ if (!settings) {
1126
+ checks.push({
1127
+ level: "fail",
1128
+ name: "claude",
1129
+ detail: `${claudeDir(home)} has no settings.json — is Claude Code installed for this config dir?`
1130
+ });
1131
+ } else if (!pointed) {
1132
+ checks.push({
1133
+ level: cfg.mode === "off" && cfg.token ? "ok" : "warn",
1134
+ name: "claude",
1135
+ detail: cfg.mode === "off" && cfg.token ? `${sPath} on your own seat (sleds off)` : `${sPath} does not point at the proxy`,
1136
+ fix: cfg.mode === "off" && cfg.token ? undefined : "sleds init (or `sleds on` to switch)"
1137
+ });
1138
+ } else if (cfg.token && pointed.token !== cfg.token) {
1139
+ checks.push({
1140
+ level: "warn",
1141
+ name: "claude",
1142
+ detail: `${sPath} carries a different token (${maskToken(pointed.token)}) than ${configPath(home)} (${maskToken(cfg.token)})`,
1143
+ fix: "sleds init --token <the one you mean>"
1144
+ });
1145
+ } else {
1146
+ checks.push({ level: "ok", name: "claude", detail: `${sPath} → ${pointed.url}` });
1147
+ }
1148
+ const src = meterSource(home);
1149
+ if (src) {
1150
+ const health = await checkHealth(src.proxyUrl, o.fetchFn, o.timeoutMs);
1151
+ checks.push({
1152
+ level: health === "ok" ? "ok" : "fail",
1153
+ name: "proxy",
1154
+ detail: `${src.proxyUrl} ${health === "ok" ? "answers /healthz" : "did not answer"}`,
1155
+ fix: health === "ok" ? undefined : "check the URL (a trailing dot or wrong host); `sleds init --url https://proxy.sleds.ai`"
1156
+ });
1157
+ }
1158
+ if (settings) {
1159
+ const seat2 = hasSeatHook(settings);
1160
+ const spec = hasSpecHook(settings);
1161
+ checks.push({
1162
+ level: seat2 && spec ? "ok" : "warn",
1163
+ name: "hooks",
1164
+ detail: `seat-event ${seat2 ? "on" : "missing"} · spec-event ${spec ? "on" : "missing"}`,
1165
+ fix: seat2 && spec ? undefined : "sleds hooks"
1166
+ });
1167
+ const skill = existsSync6(skillPath(home));
1168
+ checks.push({
1169
+ level: skill ? "ok" : "warn",
1170
+ name: "skill",
1171
+ detail: skill ? skillPath(home) : `no /sled skill at ${skillPath(home)}`,
1172
+ fix: skill ? undefined : "sleds init (re-run; keeps your token)"
1173
+ });
1174
+ }
1175
+ const nodeVersion = o.nodeVersion ?? process.version;
1176
+ checks.push({
1177
+ level: nodeOk(nodeVersion) ? "ok" : "warn",
1178
+ name: "node",
1179
+ detail: `${nodeVersion}${nodeOk(nodeVersion) ? "" : ` — seat-sync needs ≥ ${NODE_MIN} (node:sqlite)`}`,
1180
+ fix: nodeOk(nodeVersion) ? undefined : `brew install node@22 && echo 'export PATH="/opt/homebrew/opt/node@22/bin:$PATH"' >> ~/.zshrc`
1181
+ });
1182
+ const which = (o.which ?? defaultWhich)("sleds");
1183
+ const stale = which.filter((p) => {
1184
+ try {
1185
+ const target = lstatSync2(p).isSymbolicLink() ? readlinkSync(p) : p;
1186
+ return /\/src\/index\.ts$/.test(target) || /\.bun\/bin\/sleds$/.test(p);
1187
+ } catch {
1188
+ return false;
1189
+ }
1190
+ });
1191
+ if (stale.length > 0)
1192
+ checks.push({
1193
+ level: "warn",
1194
+ name: "path",
1195
+ detail: `an old link shadows the installed CLI: ${stale.join(", ")}`,
1196
+ fix: `rm -f ${stale.join(" ")}`
1197
+ });
1198
+ else if (which.length > 0)
1199
+ checks.push({ level: "ok", name: "path", detail: which[0] ?? "" });
1200
+ const pluginBody = existsSync6(pluginPath2(home)) ? readFileSync5(pluginPath2(home), "utf8") : null;
1201
+ const dir = (o.swiftBarPluginDir ?? swiftBarPluginDir2)();
1202
+ if (pluginBody) {
1203
+ const baked = pluginEnv(pluginBody);
1204
+ const linked = dir ? existsSync6(join7(dir, PLUGIN_NAME2)) : false;
1205
+ const seatDesc = baked.sledsHome || baked.claudeConfigDir ? `seat at ${[baked.sledsHome && `SLEDS_HOME=${baked.sledsHome}`, baked.claudeConfigDir && `CLAUDE_CONFIG_DIR=${baked.claudeConfigDir}`].filter(Boolean).join(" ")}` : "default seat";
1206
+ checks.push({
1207
+ level: linked ? "ok" : "warn",
1208
+ name: "menu bar",
1209
+ detail: linked ? `${PLUGIN_NAME2} linked · ${seatDesc}` : `plugin written but not linked into SwiftBar${dir ? ` (${dir})` : " (SwiftBar not set up)"}`,
1210
+ fix: linked ? undefined : "sleds bar --install"
1211
+ });
1212
+ } else {
1213
+ checks.push({
1214
+ level: "warn",
1215
+ name: "menu bar",
1216
+ detail: "no SwiftBar plugin",
1217
+ fix: "sleds bar --install"
1218
+ });
1219
+ }
1220
+ const agents = join7(home, "Library", "LaunchAgents");
1221
+ const plists = existsSync6(agents) ? readdirSync(agents).filter((f) => f.startsWith("ai.sleds.") && f.endsWith(".plist")) : [];
1222
+ const loaded = (o.launchctlList ?? defaultLaunchctl)();
1223
+ const seat = readSeatState(home);
1224
+ const age = seat.window ? Math.round((Date.now() - seat.window.capturedAt) / 60000) : null;
1225
+ if (plists.length === 0)
1226
+ checks.push({
1227
+ level: "warn",
1228
+ name: "seat-sync",
1229
+ detail: "no timer installed (seat bars stay empty)",
1230
+ fix: "sleds seat-sync --install"
1231
+ });
1232
+ else {
1233
+ const running = plists.filter((p) => loaded.includes(p.replace(/\.plist$/, "")));
1234
+ checks.push({
1235
+ level: running.length === plists.length ? age !== null && age < 6 * 60 ? "ok" : "warn" : "warn",
1236
+ name: "seat-sync",
1237
+ detail: `${running.length}/${plists.length} timer${plists.length === 1 ? "" : "s"} loaded${age !== null ? ` · last read ${age}m ago` : " · no read yet"}`,
1238
+ fix: running.length === plists.length && age !== null && age < 6 * 60 ? undefined : "sleds seat-sync (run once by hand and read its message)"
1239
+ });
1240
+ }
1241
+ return checks;
1242
+ }
1243
+ var doctor = async (o) => {
1244
+ const log = o.log ?? console.log;
1245
+ const checks = await runDoctor(o);
1246
+ for (const line of formatDoctor(checks))
1247
+ log(line);
1248
+ return checks.some((c) => c.level === "fail") ? 1 : 0;
1249
+ };
1250
+
905
1251
  // src/commands/hooks.ts
906
1252
  var DEFAULT_SEAT_EVENT_COMMAND = "sleds seat-event";
907
1253
  var DEFAULT_SPEC_EVENT_COMMAND = "sleds spec-event";
@@ -1014,6 +1360,16 @@ List the org's jobs: queued, running, delivered, parked, with the estimate band,
1014
1360
 
1015
1361
  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.
1016
1362
 
1363
+ ## /sled sync-backlog — price the backlog
1364
+
1365
+ 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.
1366
+
1367
+ 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.
1368
+ 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.
1369
+ 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.
1370
+
1371
+ 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.
1372
+
1017
1373
  ## Voice
1018
1374
 
1019
1375
  Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what happens next, not what could.
@@ -1022,7 +1378,7 @@ Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what
1022
1378
 
1023
1379
  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:
1024
1380
 
1025
- - \`/sled jobs\` — this seat's jobs: id, status, route, cost, when.
1381
+ - \`/sled jobs\` — this seat's jobs: id, status, route, cost, when — then the team's open hand-offs waiting for pickup (yours first).
1026
1382
  - \`/sled resume <id>\` — pull a delivered job's output into the session as the assistant's reply and keep going.
1027
1383
 
1028
1384
  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.
@@ -1031,7 +1387,9 @@ A job that is \`parked\` or \`failed\` shows its reason in \`/sled jobs\`; the a
1031
1387
 
1032
1388
  \`/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.
1033
1389
 
1034
- - \`/sled pickup <id>\` — in the coworker's Claude Code session: the handed-off session resumes there on the org key, metered to them. A hand-off meant for someone else, already taken, or without a snapshot gets one line saying so.
1390
+ - \`/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.
1391
+
1392
+ 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.
1035
1393
  `;
1036
1394
 
1037
1395
  // src/commands/hooks.ts
@@ -1057,11 +1415,11 @@ var hooks2 = (o) => {
1057
1415
  // src/commands/init.ts
1058
1416
  var DEFAULT_PROXY_URL = "http://localhost:8787";
1059
1417
  var DEFAULT_STATUS_LINE_COMMAND = "sleds statusline";
1060
- var promptStdin = (question) => new Promise((resolve3) => {
1418
+ var promptStdin = (question) => new Promise((resolve5) => {
1061
1419
  const rl = createInterface({ input: process.stdin, output: process.stderr });
1062
1420
  rl.question(question, (answer) => {
1063
1421
  rl.close();
1064
- resolve3(answer.trim());
1422
+ resolve5(answer.trim());
1065
1423
  });
1066
1424
  });
1067
1425
  async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
@@ -1096,6 +1454,33 @@ var SIGNUP_REASONS = {
1096
1454
  function signupFailureText(r) {
1097
1455
  return `Signup failed: ${SIGNUP_REASONS[r.reason] ?? `unexpected answer (${r.reason}, HTTP ${r.status}).`}`;
1098
1456
  }
1457
+ function normalizeProxyUrl(raw) {
1458
+ let t = raw.trim();
1459
+ if (!t)
1460
+ return null;
1461
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(t))
1462
+ t = `https://${t}`;
1463
+ t = t.replace(/[./]+$/, "");
1464
+ try {
1465
+ const u = new URL(t);
1466
+ if (u.protocol !== "http:" && u.protocol !== "https:")
1467
+ return null;
1468
+ if (!u.hostname)
1469
+ return null;
1470
+ u.hostname = u.hostname.replace(/\.+$/, "");
1471
+ return `${u.protocol}//${u.host}${u.pathname === "/" ? "" : u.pathname.replace(/\/+$/, "")}`;
1472
+ } catch {
1473
+ return null;
1474
+ }
1475
+ }
1476
+ async function probeProxy(url, fetchFn = fetch) {
1477
+ try {
1478
+ const res = await fetchFn(`${url}/health`, { signal: AbortSignal.timeout(3000) });
1479
+ return res.ok;
1480
+ } catch {
1481
+ return false;
1482
+ }
1483
+ }
1099
1484
  var resolveToken = async (o) => {
1100
1485
  const env = o.env ?? process.env;
1101
1486
  if (o.token)
@@ -1112,7 +1497,9 @@ var resolveToken = async (o) => {
1112
1497
  if (typed)
1113
1498
  return { token: typed, source: "prompt" };
1114
1499
  }
1115
- const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/+$/, "");
1500
+ const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL);
1501
+ if (!url)
1502
+ return null;
1116
1503
  const email = (await o.prompt("Your email: ")).trim();
1117
1504
  if (!email.includes("@"))
1118
1505
  return null;
@@ -1144,7 +1531,15 @@ var init = async (o) => {
1144
1531
  error("Invalid seat token: expected something like sled_xxxxxxxx.");
1145
1532
  return 2;
1146
1533
  }
1147
- const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/+$/, "");
1534
+ const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL);
1535
+ if (!url) {
1536
+ error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
1537
+ return 2;
1538
+ }
1539
+ if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
1540
+ error(`No SLEDS proxy answered at ${url}/health. Check the URL (or pass --no-probe to save it anyway).`);
1541
+ return 2;
1542
+ }
1148
1543
  const wrote = [];
1149
1544
  if (backupSettings(o.home))
1150
1545
  wrote.push(`backed up settings to ${backupPath(o.home)}`);
@@ -1175,14 +1570,14 @@ var init = async (o) => {
1175
1570
  mkdirSync6(skillDir(o.home), { recursive: true });
1176
1571
  writeFileSync6(skillPath(o.home), SKILL_MD);
1177
1572
  wrote.push(`installed skill at ${skillPath(o.home)}`);
1178
- log("Nothing changes until a wall.");
1573
+ log("Claude Code now runs through SLEDS on the org key.");
1179
1574
  log("");
1180
1575
  for (const w of wrote)
1181
1576
  log(` ${w}`);
1182
1577
  log("");
1183
- log(`Claude Code now talks to ${url}. Run \`sleds status\` to check.`);
1578
+ log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
1184
1579
  log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
1185
- log("On a Team or Max seat, keep your seat and switch only at a wall: `sleds on` / `sleds off`.");
1580
+ log("Prefer to burn your own Pro/Max/Team seat first? `sleds off` puts Claude Code back on it; `sleds on --until-reset +5h` switches to the org key at a wall.");
1186
1581
  return 0;
1187
1582
  };
1188
1583
 
@@ -1190,6 +1585,21 @@ var init = async (o) => {
1190
1585
  var PROTOCOL_VERSION = "2025-06-18";
1191
1586
  var SERVER_INFO = { name: "sleds", version: "0.0.1" };
1192
1587
  var MCP_TIMEOUT_MS = 5000;
1588
+ var sourceSchema = {
1589
+ type: "object",
1590
+ 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).",
1591
+ properties: {
1592
+ kind: { type: "string", enum: ["claude-project", "repo", "linear", "github", "manual"] },
1593
+ project: { type: "string", description: "Project / repo / board name." },
1594
+ doc: { type: "string", description: "Doc path or issue key inside it." },
1595
+ item: {
1596
+ type: "string",
1597
+ description: "The line or phase inside the doc, when a doc holds many."
1598
+ },
1599
+ url: { type: "string", description: "Link to open it, if any." }
1600
+ },
1601
+ required: ["kind", "project"]
1602
+ };
1193
1603
  var specSchema = {
1194
1604
  type: "object",
1195
1605
  properties: {
@@ -1197,7 +1607,8 @@ var specSchema = {
1197
1607
  type: "string",
1198
1608
  description: "What the job should do. The first line becomes the title."
1199
1609
  },
1200
- model: { type: "string", description: "Model to price and run with; org default when absent." }
1610
+ model: { type: "string", description: "Model to price and run with; org default when absent." },
1611
+ source: sourceSchema
1201
1612
  },
1202
1613
  required: ["spec"]
1203
1614
  };
@@ -1219,8 +1630,38 @@ var TOOLS = [
1219
1630
  },
1220
1631
  {
1221
1632
  name: "jobs",
1222
- description: "This seat's overnight jobs, newest first: id, status, route, cost, when.",
1633
+ description: "This seat's overnight jobs, newest first (id, status, route, cost, when), then the team's open hand-offs waiting for pickup.",
1223
1634
  inputSchema: { type: "object", properties: {} }
1635
+ },
1636
+ {
1637
+ name: "handoffs",
1638
+ 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).",
1639
+ inputSchema: { type: "object", properties: {} }
1640
+ },
1641
+ {
1642
+ name: "handoff",
1643
+ 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>.",
1644
+ inputSchema: {
1645
+ type: "object",
1646
+ properties: {
1647
+ task: {
1648
+ type: "string",
1649
+ description: "The task to hand off, in full. First line becomes the title."
1650
+ },
1651
+ to: { type: "string", description: "Coworker's email; omit to park for anyone." },
1652
+ note: { type: "string", description: "One line on where things stand (≤280 chars)." }
1653
+ },
1654
+ required: ["task"]
1655
+ }
1656
+ },
1657
+ {
1658
+ name: "pickup",
1659
+ 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.",
1660
+ inputSchema: {
1661
+ type: "object",
1662
+ properties: { handoffId: { type: "string", description: "From the handoffs tool." } },
1663
+ required: ["handoffId"]
1664
+ }
1224
1665
  }
1225
1666
  ];
1226
1667
  var NOT_SET_UP = {
@@ -1231,7 +1672,7 @@ var UNREACHABLE = {
1231
1672
  text: "Control plane unreachable. Nothing was recorded → try again in a moment.",
1232
1673
  isError: true
1233
1674
  };
1234
- var isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1675
+ var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1235
1676
  var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
1236
1677
  var str2 = (v) => typeof v === "string" ? v : undefined;
1237
1678
  async function proxyPost(deps, path, body) {
@@ -1249,11 +1690,35 @@ async function proxyPost(deps, path, body) {
1249
1690
  if (!res.ok)
1250
1691
  return null;
1251
1692
  const parsed = await res.json();
1252
- return isObject5(parsed) ? parsed : null;
1693
+ return isObject6(parsed) ? parsed : null;
1253
1694
  } catch {
1254
1695
  return null;
1255
1696
  }
1256
1697
  }
1698
+ async function proxyPostFull(deps, path, body) {
1699
+ const src = meterSource(deps.home);
1700
+ if (!src)
1701
+ return null;
1702
+ const fetchFn = deps.fetchFn ?? fetch;
1703
+ try {
1704
+ const res = await fetchFn(`${src.proxyUrl}${path}`, {
1705
+ method: "POST",
1706
+ headers: { "content-type": "application/json", authorization: `Bearer ${src.token}` },
1707
+ body: JSON.stringify(body),
1708
+ signal: AbortSignal.timeout(deps.timeoutMs ?? MCP_TIMEOUT_MS)
1709
+ });
1710
+ let json = null;
1711
+ try {
1712
+ json = await res.json();
1713
+ } catch {
1714
+ json = null;
1715
+ }
1716
+ return { status: res.status, json: isObject6(json) ? json : null };
1717
+ } catch {
1718
+ return null;
1719
+ }
1720
+ }
1721
+ var errorMessage = (j) => isObject6(j?.error) ? str2(j.error.message) : undefined;
1257
1722
  async function proxyGet(deps, path) {
1258
1723
  const src = meterSource(deps.home);
1259
1724
  if (!src)
@@ -1267,7 +1732,7 @@ async function proxyGet(deps, path) {
1267
1732
  if (!res.ok)
1268
1733
  return null;
1269
1734
  const parsed = await res.json();
1270
- return isObject5(parsed) ? parsed : null;
1735
+ return isObject6(parsed) ? parsed : null;
1271
1736
  } catch {
1272
1737
  return null;
1273
1738
  }
@@ -1296,7 +1761,7 @@ async function usageTool(deps) {
1296
1761
  `) };
1297
1762
  }
1298
1763
  function bandLine(band) {
1299
- if (!isObject5(band))
1764
+ if (!isObject6(band))
1300
1765
  return null;
1301
1766
  const low = num3(band.lowUsd);
1302
1767
  const high = num3(band.highUsd);
@@ -1316,10 +1781,15 @@ async function estimateTool(deps, args) {
1316
1781
  const spec = str2(args.spec)?.trim();
1317
1782
  if (!spec)
1318
1783
  return { text: "Nothing to estimate. Pass spec: what the job should do.", isError: true };
1319
- const r = await proxyPost(deps, "/proxy/estimate", { spec, model: str2(args.model) });
1784
+ const source = isObject6(args.source) && str2(args.source.kind) && str2(args.source.project) ? args.source : undefined;
1785
+ const r = await proxyPost(deps, "/proxy/estimate", {
1786
+ spec,
1787
+ model: str2(args.model),
1788
+ ...source ? { source } : {}
1789
+ });
1320
1790
  if (!r)
1321
1791
  return UNREACHABLE;
1322
- const band = isObject5(r.band) ? r.band : {};
1792
+ const band = isObject6(r.band) ? r.band : {};
1323
1793
  const line = bandLine(band) ?? "band unreadable";
1324
1794
  const method = str2(band.method) ?? "plan";
1325
1795
  const cap = num3(band.capUsd);
@@ -1331,14 +1801,18 @@ Assumes: ${assumptions.join("; ")}.` : "";
1331
1801
  const capText = cap !== undefined ? ` Cap ${usd(cap)}.` : "";
1332
1802
  const models = byModelText(r.byModel);
1333
1803
  const routing = routingText(r.routing);
1334
- return { text: `Estimate: ${line} (${method}).${capText}${over}${tail}${models}${routing}` };
1804
+ const filed = source ? `
1805
+ Filed under ${str2(source.kind)} ${str2(source.project)}${str2(source.doc) ? ` › ${str2(source.doc)}` : ""}${str2(source.item) ? ` › ${str2(source.item)}` : ""} on the Projects page.` : "";
1806
+ return {
1807
+ text: `Estimate: ${line} (${method}).${capText}${over}${tail}${models}${routing}${filed}`
1808
+ };
1335
1809
  }
1336
1810
  function byModelText(raw) {
1337
1811
  if (!Array.isArray(raw))
1338
1812
  return "";
1339
1813
  const parts = [];
1340
1814
  for (const row2 of raw) {
1341
- if (!isObject5(row2) || !isObject5(row2.standard))
1815
+ if (!isObject6(row2) || !isObject6(row2.standard))
1342
1816
  continue;
1343
1817
  const model = str2(row2.model);
1344
1818
  const low = num3(row2.standard.lowUsd);
@@ -1353,7 +1827,7 @@ function byModelText(raw) {
1353
1827
  By model: ${parts.join(" · ")} (overnight batch ½).` : "";
1354
1828
  }
1355
1829
  function routingText(raw) {
1356
- if (!isObject5(raw) || !Array.isArray(raw.segments) || !isObject5(raw.cost))
1830
+ if (!isObject6(raw) || !Array.isArray(raw.segments) || !isObject6(raw.cost))
1357
1831
  return "";
1358
1832
  const short = (model) => {
1359
1833
  const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?/.exec(model);
@@ -1361,7 +1835,7 @@ function routingText(raw) {
1361
1835
  };
1362
1836
  const parts = [];
1363
1837
  for (const s of raw.segments) {
1364
- if (!isObject5(s))
1838
+ if (!isObject6(s))
1365
1839
  continue;
1366
1840
  const from = num3(s.from);
1367
1841
  const to = num3(s.to);
@@ -1396,7 +1870,7 @@ async function overnightTool(deps, args) {
1396
1870
  const at = num3(r.scheduledAt);
1397
1871
  const when = at !== undefined && at > now ? `Runs in ${shortDuration(at - now)}` : "Runs at the next tick";
1398
1872
  const band = bandLine(r.band);
1399
- const cap = isObject5(r.band) ? num3(r.band.capUsd) : undefined;
1873
+ const cap = isObject6(r.band) ? num3(r.band.capUsd) : undefined;
1400
1874
  const sized = band ? ` ${band}${cap !== undefined ? ` · cap ${usd(cap)}` : ""}.` : "";
1401
1875
  return {
1402
1876
  text: `Queued overnight. Batch tier · ½ price.${sized} ${when}. Check with the jobs tool; pull the result into a session with /sled resume ${jobId}.`
@@ -1408,10 +1882,14 @@ async function jobsTool(deps) {
1408
1882
  const r = await proxyGet(deps, "/proxy/jobs");
1409
1883
  if (!r)
1410
1884
  return UNREACHABLE;
1411
- const rows = Array.isArray(r.jobs) ? r.jobs.filter(isObject5) : [];
1412
- if (rows.length === 0)
1413
- return { text: "No jobs for this seat. Queue one with the overnight tool." };
1885
+ const rows = Array.isArray(r.jobs) ? r.jobs.filter(isObject6) : [];
1886
+ const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject6) : [];
1414
1887
  const now = (deps.now ?? Date.now)();
1888
+ if (rows.length === 0 && handoffs.length === 0)
1889
+ return { text: "No jobs for this seat. Queue one with the overnight tool." };
1890
+ if (rows.length === 0)
1891
+ return { text: ["No jobs for this seat.", ...handoffLines(handoffs, now)].join(`
1892
+ `) };
1415
1893
  const lines = rows.map((j) => {
1416
1894
  const when = (ts) => num3(ts) !== undefined ? shortDuration(Math.abs(now - num3(ts))) : "";
1417
1895
  const status = str2(j.status) ?? "unknown";
@@ -1426,9 +1904,110 @@ async function jobsTool(deps) {
1426
1904
  ].join(" ").trimEnd();
1427
1905
  });
1428
1906
  lines.push("Resume a delivered job in a session with /sled resume <id>.");
1907
+ lines.push(...handoffLines(handoffs, now));
1429
1908
  return { text: lines.join(`
1430
1909
  `) };
1431
1910
  }
1911
+ function handoffLines(handoffs, now) {
1912
+ if (handoffs.length === 0)
1913
+ return [];
1914
+ const out = ["", `Hand-offs waiting (${handoffs.length}):`];
1915
+ for (const h of handoffs) {
1916
+ const forMe = h.forMe === true;
1917
+ const to = str2(h.to);
1918
+ const who = forMe ? to ? "for you" : "parked for anyone" : `for ${to ?? "anyone"}`;
1919
+ const created = num3(h.createdAt);
1920
+ const age = created !== undefined ? `${shortDuration(Math.max(0, now - created))} ago` : "";
1921
+ const note = str2(h.note);
1922
+ const parts = [`from ${str2(h.from) ?? "?"}`, who, str2(h.wallKind) ?? "", age].filter(Boolean);
1923
+ if (h.hasSnapshot === false)
1924
+ parts.push("note only");
1925
+ out.push(`${str2(h.handoffId) ?? "?"} ${parts.join(" · ")}${note ? ` «${note.length > 60 ? `${note.slice(0, 57)}…` : note}»` : ""}`);
1926
+ }
1927
+ out.push("Take one in Claude Code with /sled pickup <id>.");
1928
+ return out;
1929
+ }
1930
+ async function handoffsTool(deps) {
1931
+ if (!meterSource(deps.home))
1932
+ return NOT_SET_UP;
1933
+ const r = await proxyGet(deps, "/proxy/jobs");
1934
+ if (!r)
1935
+ return UNREACHABLE;
1936
+ const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject6) : [];
1937
+ if (handoffs.length === 0)
1938
+ return { text: "No open hand-offs on your team. Park one with the handoff tool." };
1939
+ const now = (deps.now ?? Date.now)();
1940
+ return { text: handoffLines(handoffs, now).slice(1).join(`
1941
+ `) };
1942
+ }
1943
+ async function handoffTool(deps, args) {
1944
+ if (!meterSource(deps.home))
1945
+ return NOT_SET_UP;
1946
+ const task = str2(args.task)?.trim();
1947
+ if (!task)
1948
+ return { text: "Nothing to hand off. Pass task: what the picker should do.", isError: true };
1949
+ const to = str2(args.to)?.trim();
1950
+ const r = await proxyPostFull(deps, "/proxy/handoff/park", {
1951
+ task,
1952
+ to: to || undefined,
1953
+ note: str2(args.note)?.trim() || undefined
1954
+ });
1955
+ if (!r)
1956
+ return { text: `Nothing was parked. ${UNREACHABLE.text}`, isError: true };
1957
+ if (r.status === 404 && to)
1958
+ return { text: `${to} is not a member of your org. Nothing was parked.`, isError: true };
1959
+ if (r.status !== 200)
1960
+ return {
1961
+ text: errorMessage(r.json) ?? `Nothing was parked (HTTP ${r.status}).`,
1962
+ isError: true
1963
+ };
1964
+ const id = str2(r.json?.handoffId) ?? "unknown";
1965
+ return {
1966
+ 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.`
1967
+ };
1968
+ }
1969
+ async function pickupTool(deps, args) {
1970
+ if (!meterSource(deps.home))
1971
+ return NOT_SET_UP;
1972
+ const handoffId = str2(args.handoffId)?.trim();
1973
+ if (!handoffId)
1974
+ return { text: "Pass handoffId, from the handoffs tool.", isError: true };
1975
+ const r = await proxyPostFull(deps, "/proxy/handoff/pickup", { handoffId });
1976
+ if (!r)
1977
+ return { text: `Nothing was picked up. ${UNREACHABLE.text}`, isError: true };
1978
+ if (r.status !== 200)
1979
+ return {
1980
+ text: errorMessage(r.json) ?? `Could not pick it up (HTTP ${r.status}).`,
1981
+ isError: true
1982
+ };
1983
+ const j = r.json ?? {};
1984
+ const from = str2(j.from) ?? "a teammate";
1985
+ const note = str2(j.note);
1986
+ const head = `Picked up from ${from}.${note ? ` Where it stands: ${note}` : ""}`;
1987
+ if (j.kind === "task")
1988
+ return { text: `${head}
1989
+
1990
+ The task:
1991
+ ${str2(j.task) ?? ""}
1992
+
1993
+ Carry it through from here.` };
1994
+ const first = str2(j.firstUserText) ?? "";
1995
+ const last = str2(j.lastUserText) ?? "";
1996
+ const n = num3(j.messageCount);
1997
+ return {
1998
+ text: [
1999
+ `${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.`,
2000
+ "",
2001
+ `It started with:
2002
+ ${first}`,
2003
+ ...last && last !== first ? ["", `The last ask was:
2004
+ ${last}`] : [],
2005
+ "",
2006
+ "Continue the work from here."
2007
+ ].join(`
2008
+ `)
2009
+ };
2010
+ }
1432
2011
  async function callTool(name, args, deps) {
1433
2012
  switch (name) {
1434
2013
  case "usage":
@@ -1439,6 +2018,12 @@ async function callTool(name, args, deps) {
1439
2018
  return overnightTool(deps, args);
1440
2019
  case "jobs":
1441
2020
  return jobsTool(deps);
2021
+ case "handoffs":
2022
+ return handoffsTool(deps);
2023
+ case "handoff":
2024
+ return handoffTool(deps, args);
2025
+ case "pickup":
2026
+ return pickupTool(deps, args);
1442
2027
  default:
1443
2028
  return { text: `Unknown tool: ${name}`, isError: true };
1444
2029
  }
@@ -1455,7 +2040,7 @@ async function handleLine(line, deps) {
1455
2040
  } catch {
1456
2041
  return errorResponse(null, -32700, "parse error");
1457
2042
  }
1458
- if (!isObject5(msg))
2043
+ if (!isObject6(msg))
1459
2044
  return errorResponse(null, -32600, "invalid request");
1460
2045
  const id = typeof msg.id === "string" || typeof msg.id === "number" ? msg.id : null;
1461
2046
  const method = str2(msg.method);
@@ -1464,7 +2049,7 @@ async function handleLine(line, deps) {
1464
2049
  return isNotification ? null : errorResponse(id, -32600, "invalid request");
1465
2050
  if (method.startsWith("notifications/"))
1466
2051
  return null;
1467
- const params = isObject5(msg.params) ? msg.params : {};
2052
+ const params = isObject6(msg.params) ? msg.params : {};
1468
2053
  switch (method) {
1469
2054
  case "initialize": {
1470
2055
  const requested = str2(params.protocolVersion);
@@ -1480,7 +2065,7 @@ async function handleLine(line, deps) {
1480
2065
  return response(id, { tools: TOOLS });
1481
2066
  case "tools/call": {
1482
2067
  const name = str2(params.name) ?? "";
1483
- const args = isObject5(params.arguments) ? params.arguments : {};
2068
+ const args = isObject6(params.arguments) ? params.arguments : {};
1484
2069
  const r = await callTool(name, args, deps);
1485
2070
  return response(id, {
1486
2071
  content: [{ type: "text", text: r.text }],
@@ -1540,10 +2125,10 @@ var meter = async (o) => {
1540
2125
  };
1541
2126
 
1542
2127
  // src/schedule.ts
1543
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync7 } from "node:fs";
1544
- import { join as join6 } from "node:path";
2128
+ import { existsSync as existsSync7, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync7 } from "node:fs";
2129
+ import { join as join8 } from "node:path";
1545
2130
  var REVERT_LABEL = "ai.sleds.revert";
1546
- var plistPath = (home) => join6(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
2131
+ var plistPath = (home) => join8(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
1547
2132
  var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1548
2133
  var buildLaunchdPlist = (sledsArgv, deadline) => {
1549
2134
  const args = [...sledsArgv, "off"].map((a) => ` <string>${xmlEscape(a)}</string>`).join(`
@@ -1582,7 +2167,7 @@ var scheduleRevert = (d, deadlineMs) => {
1582
2167
  if (d.platform === "darwin") {
1583
2168
  const p = plistPath(d.home);
1584
2169
  try {
1585
- mkdirSync7(join6(d.home, "Library", "LaunchAgents"), { recursive: true });
2170
+ mkdirSync7(join8(d.home, "Library", "LaunchAgents"), { recursive: true });
1586
2171
  d.exec("launchctl", ["unload", p]);
1587
2172
  writeFileSync7(p, buildLaunchdPlist(d.sledsArgv, deadline));
1588
2173
  const r = d.exec("launchctl", ["load", p]);
@@ -1611,7 +2196,7 @@ var scheduleRevert = (d, deadlineMs) => {
1611
2196
  var cancelRevert = (d, kind) => {
1612
2197
  if (kind === "launchd" || d.platform === "darwin") {
1613
2198
  const p = plistPath(d.home);
1614
- if (existsSync6(p)) {
2199
+ if (existsSync7(p)) {
1615
2200
  d.exec("launchctl", ["unload", p]);
1616
2201
  rmSync(p, { force: true });
1617
2202
  }
@@ -1621,81 +2206,6 @@ var cancelRevert = (d, kind) => {
1621
2206
  }
1622
2207
  };
1623
2208
 
1624
- // src/switch.ts
1625
- var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1626
- var envOf2 = (settings) => isObject6(settings.env) ? settings.env : {};
1627
- var capturePriorEnv = (settings) => {
1628
- const env = envOf2(settings);
1629
- const prior = { hadEnv: isObject6(settings.env) };
1630
- const url = env[BASE_URL_KEY];
1631
- const token = env[AUTH_TOKEN_KEY];
1632
- if (typeof url === "string")
1633
- prior[BASE_URL_KEY] = url;
1634
- if (typeof token === "string")
1635
- prior[AUTH_TOKEN_KEY] = token;
1636
- return prior;
1637
- };
1638
- var switchOn = (settings, cfg) => applyProxy(settings, cfg);
1639
- var switchOff = (settings, prior) => {
1640
- const hadEnv = prior.hadEnv === true;
1641
- const stripped = removeProxy(settings);
1642
- const env = { ...envOf2(stripped) };
1643
- const url = prior[BASE_URL_KEY];
1644
- const token = prior[AUTH_TOKEN_KEY];
1645
- if (url !== undefined)
1646
- env[BASE_URL_KEY] = url;
1647
- if (token !== undefined)
1648
- env[AUTH_TOKEN_KEY] = token;
1649
- if (Object.keys(env).length === 0 && !hadEnv) {
1650
- const { env: _env, ...rest } = stripped;
1651
- return rest;
1652
- }
1653
- return { ...stripped, env };
1654
- };
1655
- var isPointedAtProxy = (settings) => {
1656
- const env = envOf2(settings);
1657
- return BASE_URL_KEY in env || AUTH_TOKEN_KEY in env;
1658
- };
1659
- var RELATIVE = /^\+(?:(\d+)h)?(?:(\d+)m)?$/;
1660
- var parseUntilReset = (input, nowMs) => {
1661
- const trimmed = input.trim();
1662
- const rel = RELATIVE.exec(trimmed);
1663
- let deadline;
1664
- if (rel) {
1665
- const hours = Number(rel[1] ?? 0);
1666
- const minutes = Number(rel[2] ?? 0);
1667
- if (hours === 0 && minutes === 0) {
1668
- throw new Error(`--until-reset: "${input}" is zero; use something like +5h or +90m`);
1669
- }
1670
- deadline = nowMs + (hours * 60 + minutes) * 60000;
1671
- } else {
1672
- deadline = Date.parse(trimmed);
1673
- if (Number.isNaN(deadline)) {
1674
- throw new Error(`--until-reset: "${input}" is not +<n>h[<n>m] or an ISO timestamp`);
1675
- }
1676
- }
1677
- if (deadline <= nowMs) {
1678
- throw new Error(`--until-reset: ${trimmed} is in the past`);
1679
- }
1680
- return deadline;
1681
- };
1682
- var formatLocal = (ms, tz) => new Intl.DateTimeFormat("en-US", {
1683
- month: "short",
1684
- day: "numeric",
1685
- hour: "numeric",
1686
- minute: "2-digit",
1687
- timeZoneName: "short",
1688
- ...tz ? { timeZone: tz } : {}
1689
- }).format(new Date(ms));
1690
- var formatDuration = (ms) => {
1691
- const total = Math.max(0, Math.round(ms / 60000));
1692
- const h = Math.floor(total / 60);
1693
- const m = total % 60;
1694
- if (h === 0)
1695
- return `${m}m`;
1696
- return m === 0 ? `${h}h` : `${h}h ${m}m`;
1697
- };
1698
-
1699
2209
  // src/commands/off.ts
1700
2210
  var off = (o) => {
1701
2211
  const log = o.log ?? console.log;
@@ -1852,15 +2362,15 @@ var seatEvent = async (o) => {
1852
2362
  };
1853
2363
 
1854
2364
  // src/commands/seatSync.ts
1855
- import { execFileSync as execFileSync2 } from "node:child_process";
1856
- import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync5, readdirSync, writeFileSync as writeFileSync8 } from "node:fs";
2365
+ import { execFileSync as execFileSync4 } from "node:child_process";
2366
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync8 } from "node:fs";
1857
2367
  import { platform } from "node:os";
1858
- import { join as join8 } from "node:path";
2368
+ import { join as join10 } from "node:path";
1859
2369
 
1860
2370
  // src/cookieDb.ts
1861
2371
  import { copyFileSync, rmSync as rmSync2 } from "node:fs";
1862
2372
  import { tmpdir } from "node:os";
1863
- import { join as join7 } from "node:path";
2373
+ import { join as join9 } from "node:path";
1864
2374
  var QUERY = "SELECT name, hex(encrypted_value) AS ev FROM cookies WHERE host_key LIKE '%claude.ai%'";
1865
2375
  var isBun = () => typeof globalThis.Bun !== "undefined";
1866
2376
  async function queryRows(dbPath) {
@@ -1889,7 +2399,7 @@ function nodeSqlite() {
1889
2399
  return mod;
1890
2400
  }
1891
2401
  async function readCookieRows(dbPath) {
1892
- const copy = join7(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
2402
+ const copy = join9(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
1893
2403
  copyFileSync(dbPath, copy);
1894
2404
  try {
1895
2405
  const rows = await queryRows(copy);
@@ -2162,16 +2672,16 @@ function findFile(root, name) {
2162
2672
  const stack = [root];
2163
2673
  while (stack.length > 0) {
2164
2674
  const dir = stack.pop();
2165
- if (!dir || !existsSync7(dir))
2675
+ if (!dir || !existsSync8(dir))
2166
2676
  continue;
2167
2677
  let entries;
2168
2678
  try {
2169
- entries = readdirSync(dir, { withFileTypes: true });
2679
+ entries = readdirSync2(dir, { withFileTypes: true });
2170
2680
  } catch {
2171
2681
  continue;
2172
2682
  }
2173
2683
  for (const e of entries) {
2174
- const p = join8(dir, e.name);
2684
+ const p = join10(dir, e.name);
2175
2685
  if (e.isDirectory())
2176
2686
  stack.push(p);
2177
2687
  else if (e.name === name)
@@ -2182,8 +2692,8 @@ function findFile(root, name) {
2182
2692
  }
2183
2693
  function findCookieDb(home, source) {
2184
2694
  const b = BROWSERS[source.browser];
2185
- const root = join8(home, APP_SUPPORT, b.appDir);
2186
- if (!existsSync7(root))
2695
+ const root = join10(home, APP_SUPPORT, b.appDir);
2696
+ if (!existsSync8(root))
2187
2697
  throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
2188
2698
  if (!b.profiled) {
2189
2699
  const db2 = findFile(root, "Cookies");
@@ -2191,15 +2701,15 @@ function findCookieDb(home, source) {
2191
2701
  throw new Error(`no cookie store under ${root}`);
2192
2702
  return db2;
2193
2703
  }
2194
- const localStatePath = join8(root, "Local State");
2195
- const localState = existsSync7(localStatePath) ? readFileSync5(localStatePath, "utf8") : null;
2704
+ const localStatePath = join10(root, "Local State");
2705
+ const localState = existsSync8(localStatePath) ? readFileSync6(localStatePath, "utf8") : null;
2196
2706
  const wanted = source.profile ?? "Default";
2197
2707
  const { dir, ambiguous } = resolveProfileDir(localState, wanted);
2198
2708
  if (ambiguous)
2199
2709
  throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
2200
2710
  if (!dir)
2201
2711
  throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
2202
- const db = cookieDbCandidates(join8(root, dir)).find((p) => existsSync7(p));
2712
+ const db = cookieDbCandidates(join10(root, dir)).find((p) => existsSync8(p));
2203
2713
  if (!db)
2204
2714
  throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
2205
2715
  return db;
@@ -2209,7 +2719,7 @@ async function readCookieRowsBun(home, source) {
2209
2719
  }
2210
2720
  function keychainKey(service) {
2211
2721
  try {
2212
- return execFileSync2("security", ["find-generic-password", "-w", "-s", service], {
2722
+ return execFileSync4("security", ["find-generic-password", "-w", "-s", service], {
2213
2723
  encoding: "utf8"
2214
2724
  }).trim();
2215
2725
  } catch {
@@ -2221,7 +2731,7 @@ function decryptJar(rows, safeStoragePw) {
2221
2731
  }
2222
2732
  var curlUsage = (org, cookie) => {
2223
2733
  try {
2224
- const out = execFileSync2("curl", usageCurlArgs(org, cookie), {
2734
+ const out = execFileSync4("curl", usageCurlArgs(org, cookie), {
2225
2735
  encoding: "utf8",
2226
2736
  maxBuffer: 8000000
2227
2737
  });
@@ -2242,7 +2752,7 @@ function fetchSeatUsage(jar, org, now, run) {
2242
2752
  }
2243
2753
  function launchAgentPlist(bun, script, home, source) {
2244
2754
  const label = launchLabel(source);
2245
- const log = join8(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
2755
+ const log = join10(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
2246
2756
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2247
2757
  const args = [bun, script, "seat-sync", ...sourceArgs(source)].map((a) => ` <string>${esc(a)}</string>`).join(`
2248
2758
  `);
@@ -2264,7 +2774,7 @@ ${args}
2264
2774
  `;
2265
2775
  }
2266
2776
  function plistPath2(home, source) {
2267
- return join8(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
2777
+ return join10(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
2268
2778
  }
2269
2779
  function install2(home, argv, source, log) {
2270
2780
  const bun = argv[0] ?? process.execPath;
@@ -2273,23 +2783,23 @@ function install2(home, argv, source, log) {
2273
2783
  log("cannot resolve the sleds script path to install the timer");
2274
2784
  return 1;
2275
2785
  }
2276
- const dir = join8(home, "Library", "LaunchAgents");
2786
+ const dir = join10(home, "Library", "LaunchAgents");
2277
2787
  mkdirSync8(dir, { recursive: true });
2278
2788
  const path = plistPath2(home, source);
2279
2789
  writeFileSync8(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
2280
2790
  try {
2281
- execFileSync2("launchctl", ["unload", path], { stdio: "ignore" });
2791
+ execFileSync4("launchctl", ["unload", path], { stdio: "ignore" });
2282
2792
  } catch {}
2283
- execFileSync2("launchctl", ["load", "-w", path]);
2793
+ execFileSync4("launchctl", ["load", "-w", path]);
2284
2794
  log(`seat-sync timer installed (every ${SYNC_INTERVAL_S / 60} min) → ${path}`);
2285
2795
  return 0;
2286
2796
  }
2287
2797
  function uninstall2(home, source, log) {
2288
2798
  const path = plistPath2(home, source);
2289
2799
  try {
2290
- execFileSync2("launchctl", ["unload", "-w", path], { stdio: "ignore" });
2800
+ execFileSync4("launchctl", ["unload", "-w", path], { stdio: "ignore" });
2291
2801
  } catch {}
2292
- log(existsSync7(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
2802
+ log(existsSync8(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
2293
2803
  return 0;
2294
2804
  }
2295
2805
  var seatSync = async (o) => {
@@ -2384,7 +2894,7 @@ async function postSeat(proxyUrl, token, body, fetchFn) {
2384
2894
  }
2385
2895
 
2386
2896
  // src/commands/specEvent.ts
2387
- import { readFileSync as readFileSync6, statSync } from "node:fs";
2897
+ import { readFileSync as readFileSync7, statSync } from "node:fs";
2388
2898
  import { basename, isAbsolute, relative } from "node:path";
2389
2899
  var HOOK_STDIN_TIMEOUT_MS2 = 1000;
2390
2900
  var ESTIMATE_TIMEOUT_MS = 4000;
@@ -2440,7 +2950,7 @@ function readFileOrNull(path) {
2440
2950
  try {
2441
2951
  if (statSync(path).size > 2000000)
2442
2952
  return null;
2443
- return readFileSync6(path, "utf8");
2953
+ return readFileSync7(path, "utf8");
2444
2954
  } catch {
2445
2955
  return null;
2446
2956
  }
@@ -2483,8 +2993,8 @@ var specEvent = async (o) => {
2483
2993
  };
2484
2994
 
2485
2995
  // src/commands/status.ts
2486
- import { existsSync as existsSync8 } from "node:fs";
2487
- var checkHealth = async (url, fetchFn = fetch, timeoutMs = 1000) => {
2996
+ import { existsSync as existsSync9 } from "node:fs";
2997
+ var checkHealth2 = async (url, fetchFn = fetch, timeoutMs = 1000) => {
2488
2998
  try {
2489
2999
  const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
2490
3000
  return res.ok ? "ok" : "unreachable";
@@ -2551,7 +3061,7 @@ var status = async (o) => {
2551
3061
  const log = o.log ?? console.log;
2552
3062
  const nowMs = (o.now ?? Date.now)();
2553
3063
  const cfg = readConfig(o.home);
2554
- const settings = existsSync8(settingsPath(o.home)) ? readSettings(o.home) : {};
3064
+ const settings = existsSync9(settingsPath(o.home)) ? readSettings(o.home) : {};
2555
3065
  const inSettings = readProxy(settings);
2556
3066
  const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
2557
3067
  const token = inSettings?.token ?? cfg.token;
@@ -2585,7 +3095,7 @@ var status = async (o) => {
2585
3095
  info.untilResetMs = until;
2586
3096
  if (proxyUrl) {
2587
3097
  const [health, proxy] = await Promise.all([
2588
- checkHealth(proxyUrl, o.fetchFn, o.timeoutMs),
3098
+ checkHealth2(proxyUrl, o.fetchFn, o.timeoutMs),
2589
3099
  fetchProxyStatus(proxyUrl, o.fetchFn, o.timeoutMs)
2590
3100
  ]);
2591
3101
  info.health = health;
@@ -2649,11 +3159,11 @@ var rememberSeat = (o, window, now) => {
2649
3159
  };
2650
3160
 
2651
3161
  // src/commands/uninstall.ts
2652
- import { existsSync as existsSync9, rmSync as rmSync3 } from "node:fs";
3162
+ import { existsSync as existsSync10, rmSync as rmSync3 } from "node:fs";
2653
3163
  var uninstall3 = (o) => {
2654
3164
  const log = o.log ?? console.log;
2655
3165
  const removed = [];
2656
- if (existsSync9(settingsPath(o.home))) {
3166
+ if (existsSync10(settingsPath(o.home))) {
2657
3167
  let settings = readSettings(o.home);
2658
3168
  let changed = false;
2659
3169
  if (hasEitherKey(settings)) {
@@ -2685,13 +3195,13 @@ var uninstall3 = (o) => {
2685
3195
  writeSettings(o.home, settings);
2686
3196
  }
2687
3197
  for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
2688
- if (existsSync9(file)) {
3198
+ if (existsSync10(file)) {
2689
3199
  rmSync3(file, { force: true });
2690
3200
  removed.push(file);
2691
3201
  }
2692
3202
  }
2693
3203
  const skill = skillDir(o.home);
2694
- if (existsSync9(skill)) {
3204
+ if (existsSync10(skill)) {
2695
3205
  rmSync3(skill, { recursive: true, force: true });
2696
3206
  removed.push(skill);
2697
3207
  }
@@ -2728,6 +3238,7 @@ commands:
2728
3238
  init Point Claude Code at a SLEDS proxy, install the /sled skill and the status line.
2729
3239
  --token <sled_\u2026> seat token (else SLEDS_TOKEN, ~/.sleds-token, or prompt)
2730
3240
  --url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
3241
+ --no-probe skip the /health check of the proxy URL
2731
3242
  --non-interactive never prompt; exit 2 if no token is available
2732
3243
  --signup no org? create an individual seat: your email + your own
2733
3244
  Anthropic API key (validated, stored encrypted, the only key it spends)
@@ -2738,8 +3249,11 @@ commands:
2738
3249
  off Switch Claude Code back to your seat. Restores whatever \`on\` replaced.
2739
3250
  uninstall Remove the proxy settings, skill and status line. Keeps ~/.sleds-token.
2740
3251
  status Show the switch state, deadline, proxy health and wall mode.
3252
+ doctor Check the whole install: which seat this shell is, Claude settings, proxy, hooks,
3253
+ Node version, stale links, the SwiftBar plugin, the seat-sync timer.
2741
3254
  meter Show the pipeline: built today, queued with its band, delivered overnight.
2742
- bar Print the menu-bar meter in SwiftBar format; --install links it into SwiftBar.
3255
+ bar Print the menu-bar meter in SwiftBar format; --install links it into SwiftBar
3256
+ (--home/--claude-config pick the seat it tracks; --name <x> adds a second item).
2743
3257
  mcp Run the MCP server on stdio: tools usage, estimate, overnight, jobs.
2744
3258
  Register with \`claude mcp add sleds -- sleds mcp\` (Cowork sees the same server).
2745
3259
  seat-sync Read the seat's subscription usage (weekly/5h limits, per-model, credits) from a
@@ -2799,7 +3313,7 @@ var seatEventCommand = (argv) => [...argv, "seat-event"].map(shellQuote2).join("
2799
3313
  var specEventCommand = (argv) => [...argv, "spec-event"].map(shellQuote2).join(" ");
2800
3314
  var main = async (argv) => {
2801
3315
  const { cmd, flags } = parseArgs(argv);
2802
- const home = homedir2();
3316
+ const home = homedir3();
2803
3317
  const scheduler = {
2804
3318
  platform: platform2(),
2805
3319
  home,
@@ -2814,6 +3328,7 @@ var main = async (argv) => {
2814
3328
  url: stringFlag(flags, "url"),
2815
3329
  nonInteractive: flags["non-interactive"] === true,
2816
3330
  signup: flags.signup === true,
3331
+ noProbe: flags["no-probe"] === true,
2817
3332
  prompt: promptStdin,
2818
3333
  statusLineCommand: statusLineCommand(sledsArgv()),
2819
3334
  seatEventCommand: seatEventCommand(sledsArgv()),
@@ -2842,6 +3357,8 @@ var main = async (argv) => {
2842
3357
  return off({ home, scheduler });
2843
3358
  case "uninstall":
2844
3359
  return uninstall3({ home });
3360
+ case "doctor":
3361
+ return doctor({ home });
2845
3362
  case "status":
2846
3363
  return status({ home });
2847
3364
  case "meter":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "SLEDS CLI — the cost and limits of AI agent work, decided at the wall. Point Claude Code at a SLEDS proxy; estimate before you run; hold, switch model, or run overnight when you hit a limit.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",