@sleds/cli 0.1.1 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/sleds.mjs +680 -153
- package/package.json +1 -1
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
|
|
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
|
|
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",
|
|
874
|
+
var pluginPath = (home, name) => join5(configDir(home, {}), "swiftbar", pluginName(name));
|
|
853
875
|
function install(o, log) {
|
|
854
|
-
const
|
|
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,
|
|
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,
|
|
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
|
|
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((
|
|
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
|
-
|
|
1422
|
+
resolve5(answer.trim());
|
|
1065
1423
|
});
|
|
1066
1424
|
});
|
|
1067
1425
|
async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
|
|
@@ -1096,6 +1454,37 @@ 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, opts = {}) {
|
|
1477
|
+
const tries = opts.tries ?? 2;
|
|
1478
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
1479
|
+
for (let i = 0;i < tries; i++) {
|
|
1480
|
+
try {
|
|
1481
|
+
const res = await fetchFn(`${url}/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
1482
|
+
if (res.ok)
|
|
1483
|
+
return true;
|
|
1484
|
+
} catch {}
|
|
1485
|
+
}
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1099
1488
|
var resolveToken = async (o) => {
|
|
1100
1489
|
const env = o.env ?? process.env;
|
|
1101
1490
|
if (o.token)
|
|
@@ -1112,7 +1501,9 @@ var resolveToken = async (o) => {
|
|
|
1112
1501
|
if (typed)
|
|
1113
1502
|
return { token: typed, source: "prompt" };
|
|
1114
1503
|
}
|
|
1115
|
-
const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL)
|
|
1504
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL);
|
|
1505
|
+
if (!url)
|
|
1506
|
+
return null;
|
|
1116
1507
|
const email = (await o.prompt("Your email: ")).trim();
|
|
1117
1508
|
if (!email.includes("@"))
|
|
1118
1509
|
return null;
|
|
@@ -1144,7 +1535,15 @@ var init = async (o) => {
|
|
|
1144
1535
|
error("Invalid seat token: expected something like sled_xxxxxxxx.");
|
|
1145
1536
|
return 2;
|
|
1146
1537
|
}
|
|
1147
|
-
const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL)
|
|
1538
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL);
|
|
1539
|
+
if (!url) {
|
|
1540
|
+
error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
|
|
1541
|
+
return 2;
|
|
1542
|
+
}
|
|
1543
|
+
if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
|
|
1544
|
+
error(`No SLEDS proxy answered at ${url}/health. Check the URL (or pass --no-probe to save it anyway).`);
|
|
1545
|
+
return 2;
|
|
1546
|
+
}
|
|
1148
1547
|
const wrote = [];
|
|
1149
1548
|
if (backupSettings(o.home))
|
|
1150
1549
|
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
@@ -1175,14 +1574,14 @@ var init = async (o) => {
|
|
|
1175
1574
|
mkdirSync6(skillDir(o.home), { recursive: true });
|
|
1176
1575
|
writeFileSync6(skillPath(o.home), SKILL_MD);
|
|
1177
1576
|
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
1178
|
-
log("
|
|
1577
|
+
log("Claude Code now runs through SLEDS on the org key.");
|
|
1179
1578
|
log("");
|
|
1180
1579
|
for (const w of wrote)
|
|
1181
1580
|
log(` ${w}`);
|
|
1182
1581
|
log("");
|
|
1183
|
-
log(`
|
|
1582
|
+
log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
|
|
1184
1583
|
log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
|
|
1185
|
-
log("
|
|
1584
|
+
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
1585
|
return 0;
|
|
1187
1586
|
};
|
|
1188
1587
|
|
|
@@ -1190,6 +1589,21 @@ var init = async (o) => {
|
|
|
1190
1589
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
1191
1590
|
var SERVER_INFO = { name: "sleds", version: "0.0.1" };
|
|
1192
1591
|
var MCP_TIMEOUT_MS = 5000;
|
|
1592
|
+
var sourceSchema = {
|
|
1593
|
+
type: "object",
|
|
1594
|
+
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).",
|
|
1595
|
+
properties: {
|
|
1596
|
+
kind: { type: "string", enum: ["claude-project", "repo", "linear", "github", "manual"] },
|
|
1597
|
+
project: { type: "string", description: "Project / repo / board name." },
|
|
1598
|
+
doc: { type: "string", description: "Doc path or issue key inside it." },
|
|
1599
|
+
item: {
|
|
1600
|
+
type: "string",
|
|
1601
|
+
description: "The line or phase inside the doc, when a doc holds many."
|
|
1602
|
+
},
|
|
1603
|
+
url: { type: "string", description: "Link to open it, if any." }
|
|
1604
|
+
},
|
|
1605
|
+
required: ["kind", "project"]
|
|
1606
|
+
};
|
|
1193
1607
|
var specSchema = {
|
|
1194
1608
|
type: "object",
|
|
1195
1609
|
properties: {
|
|
@@ -1197,7 +1611,8 @@ var specSchema = {
|
|
|
1197
1611
|
type: "string",
|
|
1198
1612
|
description: "What the job should do. The first line becomes the title."
|
|
1199
1613
|
},
|
|
1200
|
-
model: { type: "string", description: "Model to price and run with; org default when absent." }
|
|
1614
|
+
model: { type: "string", description: "Model to price and run with; org default when absent." },
|
|
1615
|
+
source: sourceSchema
|
|
1201
1616
|
},
|
|
1202
1617
|
required: ["spec"]
|
|
1203
1618
|
};
|
|
@@ -1219,8 +1634,38 @@ var TOOLS = [
|
|
|
1219
1634
|
},
|
|
1220
1635
|
{
|
|
1221
1636
|
name: "jobs",
|
|
1222
|
-
description: "This seat's overnight jobs, newest first
|
|
1637
|
+
description: "This seat's overnight jobs, newest first (id, status, route, cost, when), then the team's open hand-offs waiting for pickup.",
|
|
1638
|
+
inputSchema: { type: "object", properties: {} }
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
name: "handoffs",
|
|
1642
|
+
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).",
|
|
1223
1643
|
inputSchema: { type: "object", properties: {} }
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
name: "handoff",
|
|
1647
|
+
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>.",
|
|
1648
|
+
inputSchema: {
|
|
1649
|
+
type: "object",
|
|
1650
|
+
properties: {
|
|
1651
|
+
task: {
|
|
1652
|
+
type: "string",
|
|
1653
|
+
description: "The task to hand off, in full. First line becomes the title."
|
|
1654
|
+
},
|
|
1655
|
+
to: { type: "string", description: "Coworker's email; omit to park for anyone." },
|
|
1656
|
+
note: { type: "string", description: "One line on where things stand (≤280 chars)." }
|
|
1657
|
+
},
|
|
1658
|
+
required: ["task"]
|
|
1659
|
+
}
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
name: "pickup",
|
|
1663
|
+
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.",
|
|
1664
|
+
inputSchema: {
|
|
1665
|
+
type: "object",
|
|
1666
|
+
properties: { handoffId: { type: "string", description: "From the handoffs tool." } },
|
|
1667
|
+
required: ["handoffId"]
|
|
1668
|
+
}
|
|
1224
1669
|
}
|
|
1225
1670
|
];
|
|
1226
1671
|
var NOT_SET_UP = {
|
|
@@ -1231,7 +1676,7 @@ var UNREACHABLE = {
|
|
|
1231
1676
|
text: "Control plane unreachable. Nothing was recorded → try again in a moment.",
|
|
1232
1677
|
isError: true
|
|
1233
1678
|
};
|
|
1234
|
-
var
|
|
1679
|
+
var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1235
1680
|
var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
1236
1681
|
var str2 = (v) => typeof v === "string" ? v : undefined;
|
|
1237
1682
|
async function proxyPost(deps, path, body) {
|
|
@@ -1249,11 +1694,35 @@ async function proxyPost(deps, path, body) {
|
|
|
1249
1694
|
if (!res.ok)
|
|
1250
1695
|
return null;
|
|
1251
1696
|
const parsed = await res.json();
|
|
1252
|
-
return
|
|
1697
|
+
return isObject6(parsed) ? parsed : null;
|
|
1253
1698
|
} catch {
|
|
1254
1699
|
return null;
|
|
1255
1700
|
}
|
|
1256
1701
|
}
|
|
1702
|
+
async function proxyPostFull(deps, path, body) {
|
|
1703
|
+
const src = meterSource(deps.home);
|
|
1704
|
+
if (!src)
|
|
1705
|
+
return null;
|
|
1706
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
1707
|
+
try {
|
|
1708
|
+
const res = await fetchFn(`${src.proxyUrl}${path}`, {
|
|
1709
|
+
method: "POST",
|
|
1710
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${src.token}` },
|
|
1711
|
+
body: JSON.stringify(body),
|
|
1712
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? MCP_TIMEOUT_MS)
|
|
1713
|
+
});
|
|
1714
|
+
let json = null;
|
|
1715
|
+
try {
|
|
1716
|
+
json = await res.json();
|
|
1717
|
+
} catch {
|
|
1718
|
+
json = null;
|
|
1719
|
+
}
|
|
1720
|
+
return { status: res.status, json: isObject6(json) ? json : null };
|
|
1721
|
+
} catch {
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
var errorMessage = (j) => isObject6(j?.error) ? str2(j.error.message) : undefined;
|
|
1257
1726
|
async function proxyGet(deps, path) {
|
|
1258
1727
|
const src = meterSource(deps.home);
|
|
1259
1728
|
if (!src)
|
|
@@ -1267,7 +1736,7 @@ async function proxyGet(deps, path) {
|
|
|
1267
1736
|
if (!res.ok)
|
|
1268
1737
|
return null;
|
|
1269
1738
|
const parsed = await res.json();
|
|
1270
|
-
return
|
|
1739
|
+
return isObject6(parsed) ? parsed : null;
|
|
1271
1740
|
} catch {
|
|
1272
1741
|
return null;
|
|
1273
1742
|
}
|
|
@@ -1296,7 +1765,7 @@ async function usageTool(deps) {
|
|
|
1296
1765
|
`) };
|
|
1297
1766
|
}
|
|
1298
1767
|
function bandLine(band) {
|
|
1299
|
-
if (!
|
|
1768
|
+
if (!isObject6(band))
|
|
1300
1769
|
return null;
|
|
1301
1770
|
const low = num3(band.lowUsd);
|
|
1302
1771
|
const high = num3(band.highUsd);
|
|
@@ -1316,10 +1785,15 @@ async function estimateTool(deps, args) {
|
|
|
1316
1785
|
const spec = str2(args.spec)?.trim();
|
|
1317
1786
|
if (!spec)
|
|
1318
1787
|
return { text: "Nothing to estimate. Pass spec: what the job should do.", isError: true };
|
|
1319
|
-
const
|
|
1788
|
+
const source = isObject6(args.source) && str2(args.source.kind) && str2(args.source.project) ? args.source : undefined;
|
|
1789
|
+
const r = await proxyPost(deps, "/proxy/estimate", {
|
|
1790
|
+
spec,
|
|
1791
|
+
model: str2(args.model),
|
|
1792
|
+
...source ? { source } : {}
|
|
1793
|
+
});
|
|
1320
1794
|
if (!r)
|
|
1321
1795
|
return UNREACHABLE;
|
|
1322
|
-
const band =
|
|
1796
|
+
const band = isObject6(r.band) ? r.band : {};
|
|
1323
1797
|
const line = bandLine(band) ?? "band unreadable";
|
|
1324
1798
|
const method = str2(band.method) ?? "plan";
|
|
1325
1799
|
const cap = num3(band.capUsd);
|
|
@@ -1331,14 +1805,18 @@ Assumes: ${assumptions.join("; ")}.` : "";
|
|
|
1331
1805
|
const capText = cap !== undefined ? ` Cap ${usd(cap)}.` : "";
|
|
1332
1806
|
const models = byModelText(r.byModel);
|
|
1333
1807
|
const routing = routingText(r.routing);
|
|
1334
|
-
|
|
1808
|
+
const filed = source ? `
|
|
1809
|
+
Filed under ${str2(source.kind)} ${str2(source.project)}${str2(source.doc) ? ` › ${str2(source.doc)}` : ""}${str2(source.item) ? ` › ${str2(source.item)}` : ""} on the Projects page.` : "";
|
|
1810
|
+
return {
|
|
1811
|
+
text: `Estimate: ${line} (${method}).${capText}${over}${tail}${models}${routing}${filed}`
|
|
1812
|
+
};
|
|
1335
1813
|
}
|
|
1336
1814
|
function byModelText(raw) {
|
|
1337
1815
|
if (!Array.isArray(raw))
|
|
1338
1816
|
return "";
|
|
1339
1817
|
const parts = [];
|
|
1340
1818
|
for (const row2 of raw) {
|
|
1341
|
-
if (!
|
|
1819
|
+
if (!isObject6(row2) || !isObject6(row2.standard))
|
|
1342
1820
|
continue;
|
|
1343
1821
|
const model = str2(row2.model);
|
|
1344
1822
|
const low = num3(row2.standard.lowUsd);
|
|
@@ -1353,7 +1831,7 @@ function byModelText(raw) {
|
|
|
1353
1831
|
By model: ${parts.join(" · ")} (overnight batch ½).` : "";
|
|
1354
1832
|
}
|
|
1355
1833
|
function routingText(raw) {
|
|
1356
|
-
if (!
|
|
1834
|
+
if (!isObject6(raw) || !Array.isArray(raw.segments) || !isObject6(raw.cost))
|
|
1357
1835
|
return "";
|
|
1358
1836
|
const short = (model) => {
|
|
1359
1837
|
const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?/.exec(model);
|
|
@@ -1361,7 +1839,7 @@ function routingText(raw) {
|
|
|
1361
1839
|
};
|
|
1362
1840
|
const parts = [];
|
|
1363
1841
|
for (const s of raw.segments) {
|
|
1364
|
-
if (!
|
|
1842
|
+
if (!isObject6(s))
|
|
1365
1843
|
continue;
|
|
1366
1844
|
const from = num3(s.from);
|
|
1367
1845
|
const to = num3(s.to);
|
|
@@ -1396,7 +1874,7 @@ async function overnightTool(deps, args) {
|
|
|
1396
1874
|
const at = num3(r.scheduledAt);
|
|
1397
1875
|
const when = at !== undefined && at > now ? `Runs in ${shortDuration(at - now)}` : "Runs at the next tick";
|
|
1398
1876
|
const band = bandLine(r.band);
|
|
1399
|
-
const cap =
|
|
1877
|
+
const cap = isObject6(r.band) ? num3(r.band.capUsd) : undefined;
|
|
1400
1878
|
const sized = band ? ` ${band}${cap !== undefined ? ` · cap ${usd(cap)}` : ""}.` : "";
|
|
1401
1879
|
return {
|
|
1402
1880
|
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 +1886,14 @@ async function jobsTool(deps) {
|
|
|
1408
1886
|
const r = await proxyGet(deps, "/proxy/jobs");
|
|
1409
1887
|
if (!r)
|
|
1410
1888
|
return UNREACHABLE;
|
|
1411
|
-
const rows = Array.isArray(r.jobs) ? r.jobs.filter(
|
|
1412
|
-
|
|
1413
|
-
return { text: "No jobs for this seat. Queue one with the overnight tool." };
|
|
1889
|
+
const rows = Array.isArray(r.jobs) ? r.jobs.filter(isObject6) : [];
|
|
1890
|
+
const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject6) : [];
|
|
1414
1891
|
const now = (deps.now ?? Date.now)();
|
|
1892
|
+
if (rows.length === 0 && handoffs.length === 0)
|
|
1893
|
+
return { text: "No jobs for this seat. Queue one with the overnight tool." };
|
|
1894
|
+
if (rows.length === 0)
|
|
1895
|
+
return { text: ["No jobs for this seat.", ...handoffLines(handoffs, now)].join(`
|
|
1896
|
+
`) };
|
|
1415
1897
|
const lines = rows.map((j) => {
|
|
1416
1898
|
const when = (ts) => num3(ts) !== undefined ? shortDuration(Math.abs(now - num3(ts))) : "";
|
|
1417
1899
|
const status = str2(j.status) ?? "unknown";
|
|
@@ -1426,9 +1908,110 @@ async function jobsTool(deps) {
|
|
|
1426
1908
|
].join(" ").trimEnd();
|
|
1427
1909
|
});
|
|
1428
1910
|
lines.push("Resume a delivered job in a session with /sled resume <id>.");
|
|
1911
|
+
lines.push(...handoffLines(handoffs, now));
|
|
1429
1912
|
return { text: lines.join(`
|
|
1430
1913
|
`) };
|
|
1431
1914
|
}
|
|
1915
|
+
function handoffLines(handoffs, now) {
|
|
1916
|
+
if (handoffs.length === 0)
|
|
1917
|
+
return [];
|
|
1918
|
+
const out = ["", `Hand-offs waiting (${handoffs.length}):`];
|
|
1919
|
+
for (const h of handoffs) {
|
|
1920
|
+
const forMe = h.forMe === true;
|
|
1921
|
+
const to = str2(h.to);
|
|
1922
|
+
const who = forMe ? to ? "for you" : "parked for anyone" : `for ${to ?? "anyone"}`;
|
|
1923
|
+
const created = num3(h.createdAt);
|
|
1924
|
+
const age = created !== undefined ? `${shortDuration(Math.max(0, now - created))} ago` : "";
|
|
1925
|
+
const note = str2(h.note);
|
|
1926
|
+
const parts = [`from ${str2(h.from) ?? "?"}`, who, str2(h.wallKind) ?? "", age].filter(Boolean);
|
|
1927
|
+
if (h.hasSnapshot === false)
|
|
1928
|
+
parts.push("note only");
|
|
1929
|
+
out.push(`${str2(h.handoffId) ?? "?"} ${parts.join(" · ")}${note ? ` «${note.length > 60 ? `${note.slice(0, 57)}…` : note}»` : ""}`);
|
|
1930
|
+
}
|
|
1931
|
+
out.push("Take one in Claude Code with /sled pickup <id>.");
|
|
1932
|
+
return out;
|
|
1933
|
+
}
|
|
1934
|
+
async function handoffsTool(deps) {
|
|
1935
|
+
if (!meterSource(deps.home))
|
|
1936
|
+
return NOT_SET_UP;
|
|
1937
|
+
const r = await proxyGet(deps, "/proxy/jobs");
|
|
1938
|
+
if (!r)
|
|
1939
|
+
return UNREACHABLE;
|
|
1940
|
+
const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject6) : [];
|
|
1941
|
+
if (handoffs.length === 0)
|
|
1942
|
+
return { text: "No open hand-offs on your team. Park one with the handoff tool." };
|
|
1943
|
+
const now = (deps.now ?? Date.now)();
|
|
1944
|
+
return { text: handoffLines(handoffs, now).slice(1).join(`
|
|
1945
|
+
`) };
|
|
1946
|
+
}
|
|
1947
|
+
async function handoffTool(deps, args) {
|
|
1948
|
+
if (!meterSource(deps.home))
|
|
1949
|
+
return NOT_SET_UP;
|
|
1950
|
+
const task = str2(args.task)?.trim();
|
|
1951
|
+
if (!task)
|
|
1952
|
+
return { text: "Nothing to hand off. Pass task: what the picker should do.", isError: true };
|
|
1953
|
+
const to = str2(args.to)?.trim();
|
|
1954
|
+
const r = await proxyPostFull(deps, "/proxy/handoff/park", {
|
|
1955
|
+
task,
|
|
1956
|
+
to: to || undefined,
|
|
1957
|
+
note: str2(args.note)?.trim() || undefined
|
|
1958
|
+
});
|
|
1959
|
+
if (!r)
|
|
1960
|
+
return { text: `Nothing was parked. ${UNREACHABLE.text}`, isError: true };
|
|
1961
|
+
if (r.status === 404 && to)
|
|
1962
|
+
return { text: `${to} is not a member of your org. Nothing was parked.`, isError: true };
|
|
1963
|
+
if (r.status !== 200)
|
|
1964
|
+
return {
|
|
1965
|
+
text: errorMessage(r.json) ?? `Nothing was parked (HTTP ${r.status}).`,
|
|
1966
|
+
isError: true
|
|
1967
|
+
};
|
|
1968
|
+
const id = str2(r.json?.handoffId) ?? "unknown";
|
|
1969
|
+
return {
|
|
1970
|
+
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.`
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
async function pickupTool(deps, args) {
|
|
1974
|
+
if (!meterSource(deps.home))
|
|
1975
|
+
return NOT_SET_UP;
|
|
1976
|
+
const handoffId = str2(args.handoffId)?.trim();
|
|
1977
|
+
if (!handoffId)
|
|
1978
|
+
return { text: "Pass handoffId, from the handoffs tool.", isError: true };
|
|
1979
|
+
const r = await proxyPostFull(deps, "/proxy/handoff/pickup", { handoffId });
|
|
1980
|
+
if (!r)
|
|
1981
|
+
return { text: `Nothing was picked up. ${UNREACHABLE.text}`, isError: true };
|
|
1982
|
+
if (r.status !== 200)
|
|
1983
|
+
return {
|
|
1984
|
+
text: errorMessage(r.json) ?? `Could not pick it up (HTTP ${r.status}).`,
|
|
1985
|
+
isError: true
|
|
1986
|
+
};
|
|
1987
|
+
const j = r.json ?? {};
|
|
1988
|
+
const from = str2(j.from) ?? "a teammate";
|
|
1989
|
+
const note = str2(j.note);
|
|
1990
|
+
const head = `Picked up from ${from}.${note ? ` Where it stands: ${note}` : ""}`;
|
|
1991
|
+
if (j.kind === "task")
|
|
1992
|
+
return { text: `${head}
|
|
1993
|
+
|
|
1994
|
+
The task:
|
|
1995
|
+
${str2(j.task) ?? ""}
|
|
1996
|
+
|
|
1997
|
+
Carry it through from here.` };
|
|
1998
|
+
const first = str2(j.firstUserText) ?? "";
|
|
1999
|
+
const last = str2(j.lastUserText) ?? "";
|
|
2000
|
+
const n = num3(j.messageCount);
|
|
2001
|
+
return {
|
|
2002
|
+
text: [
|
|
2003
|
+
`${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.`,
|
|
2004
|
+
"",
|
|
2005
|
+
`It started with:
|
|
2006
|
+
${first}`,
|
|
2007
|
+
...last && last !== first ? ["", `The last ask was:
|
|
2008
|
+
${last}`] : [],
|
|
2009
|
+
"",
|
|
2010
|
+
"Continue the work from here."
|
|
2011
|
+
].join(`
|
|
2012
|
+
`)
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
1432
2015
|
async function callTool(name, args, deps) {
|
|
1433
2016
|
switch (name) {
|
|
1434
2017
|
case "usage":
|
|
@@ -1439,6 +2022,12 @@ async function callTool(name, args, deps) {
|
|
|
1439
2022
|
return overnightTool(deps, args);
|
|
1440
2023
|
case "jobs":
|
|
1441
2024
|
return jobsTool(deps);
|
|
2025
|
+
case "handoffs":
|
|
2026
|
+
return handoffsTool(deps);
|
|
2027
|
+
case "handoff":
|
|
2028
|
+
return handoffTool(deps, args);
|
|
2029
|
+
case "pickup":
|
|
2030
|
+
return pickupTool(deps, args);
|
|
1442
2031
|
default:
|
|
1443
2032
|
return { text: `Unknown tool: ${name}`, isError: true };
|
|
1444
2033
|
}
|
|
@@ -1455,7 +2044,7 @@ async function handleLine(line, deps) {
|
|
|
1455
2044
|
} catch {
|
|
1456
2045
|
return errorResponse(null, -32700, "parse error");
|
|
1457
2046
|
}
|
|
1458
|
-
if (!
|
|
2047
|
+
if (!isObject6(msg))
|
|
1459
2048
|
return errorResponse(null, -32600, "invalid request");
|
|
1460
2049
|
const id = typeof msg.id === "string" || typeof msg.id === "number" ? msg.id : null;
|
|
1461
2050
|
const method = str2(msg.method);
|
|
@@ -1464,7 +2053,7 @@ async function handleLine(line, deps) {
|
|
|
1464
2053
|
return isNotification ? null : errorResponse(id, -32600, "invalid request");
|
|
1465
2054
|
if (method.startsWith("notifications/"))
|
|
1466
2055
|
return null;
|
|
1467
|
-
const params =
|
|
2056
|
+
const params = isObject6(msg.params) ? msg.params : {};
|
|
1468
2057
|
switch (method) {
|
|
1469
2058
|
case "initialize": {
|
|
1470
2059
|
const requested = str2(params.protocolVersion);
|
|
@@ -1480,7 +2069,7 @@ async function handleLine(line, deps) {
|
|
|
1480
2069
|
return response(id, { tools: TOOLS });
|
|
1481
2070
|
case "tools/call": {
|
|
1482
2071
|
const name = str2(params.name) ?? "";
|
|
1483
|
-
const args =
|
|
2072
|
+
const args = isObject6(params.arguments) ? params.arguments : {};
|
|
1484
2073
|
const r = await callTool(name, args, deps);
|
|
1485
2074
|
return response(id, {
|
|
1486
2075
|
content: [{ type: "text", text: r.text }],
|
|
@@ -1540,10 +2129,10 @@ var meter = async (o) => {
|
|
|
1540
2129
|
};
|
|
1541
2130
|
|
|
1542
2131
|
// src/schedule.ts
|
|
1543
|
-
import { existsSync as
|
|
1544
|
-
import { join as
|
|
2132
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2133
|
+
import { join as join8 } from "node:path";
|
|
1545
2134
|
var REVERT_LABEL = "ai.sleds.revert";
|
|
1546
|
-
var plistPath = (home) =>
|
|
2135
|
+
var plistPath = (home) => join8(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
|
|
1547
2136
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1548
2137
|
var buildLaunchdPlist = (sledsArgv, deadline) => {
|
|
1549
2138
|
const args = [...sledsArgv, "off"].map((a) => ` <string>${xmlEscape(a)}</string>`).join(`
|
|
@@ -1582,7 +2171,7 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
1582
2171
|
if (d.platform === "darwin") {
|
|
1583
2172
|
const p = plistPath(d.home);
|
|
1584
2173
|
try {
|
|
1585
|
-
mkdirSync7(
|
|
2174
|
+
mkdirSync7(join8(d.home, "Library", "LaunchAgents"), { recursive: true });
|
|
1586
2175
|
d.exec("launchctl", ["unload", p]);
|
|
1587
2176
|
writeFileSync7(p, buildLaunchdPlist(d.sledsArgv, deadline));
|
|
1588
2177
|
const r = d.exec("launchctl", ["load", p]);
|
|
@@ -1611,7 +2200,7 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
1611
2200
|
var cancelRevert = (d, kind) => {
|
|
1612
2201
|
if (kind === "launchd" || d.platform === "darwin") {
|
|
1613
2202
|
const p = plistPath(d.home);
|
|
1614
|
-
if (
|
|
2203
|
+
if (existsSync7(p)) {
|
|
1615
2204
|
d.exec("launchctl", ["unload", p]);
|
|
1616
2205
|
rmSync(p, { force: true });
|
|
1617
2206
|
}
|
|
@@ -1621,81 +2210,6 @@ var cancelRevert = (d, kind) => {
|
|
|
1621
2210
|
}
|
|
1622
2211
|
};
|
|
1623
2212
|
|
|
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
2213
|
// src/commands/off.ts
|
|
1700
2214
|
var off = (o) => {
|
|
1701
2215
|
const log = o.log ?? console.log;
|
|
@@ -1852,15 +2366,15 @@ var seatEvent = async (o) => {
|
|
|
1852
2366
|
};
|
|
1853
2367
|
|
|
1854
2368
|
// src/commands/seatSync.ts
|
|
1855
|
-
import { execFileSync as
|
|
1856
|
-
import { existsSync as
|
|
2369
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
2370
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
1857
2371
|
import { platform } from "node:os";
|
|
1858
|
-
import { join as
|
|
2372
|
+
import { join as join10 } from "node:path";
|
|
1859
2373
|
|
|
1860
2374
|
// src/cookieDb.ts
|
|
1861
2375
|
import { copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
1862
2376
|
import { tmpdir } from "node:os";
|
|
1863
|
-
import { join as
|
|
2377
|
+
import { join as join9 } from "node:path";
|
|
1864
2378
|
var QUERY = "SELECT name, hex(encrypted_value) AS ev FROM cookies WHERE host_key LIKE '%claude.ai%'";
|
|
1865
2379
|
var isBun = () => typeof globalThis.Bun !== "undefined";
|
|
1866
2380
|
async function queryRows(dbPath) {
|
|
@@ -1889,7 +2403,7 @@ function nodeSqlite() {
|
|
|
1889
2403
|
return mod;
|
|
1890
2404
|
}
|
|
1891
2405
|
async function readCookieRows(dbPath) {
|
|
1892
|
-
const copy =
|
|
2406
|
+
const copy = join9(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
|
|
1893
2407
|
copyFileSync(dbPath, copy);
|
|
1894
2408
|
try {
|
|
1895
2409
|
const rows = await queryRows(copy);
|
|
@@ -2162,16 +2676,16 @@ function findFile(root, name) {
|
|
|
2162
2676
|
const stack = [root];
|
|
2163
2677
|
while (stack.length > 0) {
|
|
2164
2678
|
const dir = stack.pop();
|
|
2165
|
-
if (!dir || !
|
|
2679
|
+
if (!dir || !existsSync8(dir))
|
|
2166
2680
|
continue;
|
|
2167
2681
|
let entries;
|
|
2168
2682
|
try {
|
|
2169
|
-
entries =
|
|
2683
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
2170
2684
|
} catch {
|
|
2171
2685
|
continue;
|
|
2172
2686
|
}
|
|
2173
2687
|
for (const e of entries) {
|
|
2174
|
-
const p =
|
|
2688
|
+
const p = join10(dir, e.name);
|
|
2175
2689
|
if (e.isDirectory())
|
|
2176
2690
|
stack.push(p);
|
|
2177
2691
|
else if (e.name === name)
|
|
@@ -2182,8 +2696,8 @@ function findFile(root, name) {
|
|
|
2182
2696
|
}
|
|
2183
2697
|
function findCookieDb(home, source) {
|
|
2184
2698
|
const b = BROWSERS[source.browser];
|
|
2185
|
-
const root =
|
|
2186
|
-
if (!
|
|
2699
|
+
const root = join10(home, APP_SUPPORT, b.appDir);
|
|
2700
|
+
if (!existsSync8(root))
|
|
2187
2701
|
throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
|
|
2188
2702
|
if (!b.profiled) {
|
|
2189
2703
|
const db2 = findFile(root, "Cookies");
|
|
@@ -2191,15 +2705,15 @@ function findCookieDb(home, source) {
|
|
|
2191
2705
|
throw new Error(`no cookie store under ${root}`);
|
|
2192
2706
|
return db2;
|
|
2193
2707
|
}
|
|
2194
|
-
const localStatePath =
|
|
2195
|
-
const localState =
|
|
2708
|
+
const localStatePath = join10(root, "Local State");
|
|
2709
|
+
const localState = existsSync8(localStatePath) ? readFileSync6(localStatePath, "utf8") : null;
|
|
2196
2710
|
const wanted = source.profile ?? "Default";
|
|
2197
2711
|
const { dir, ambiguous } = resolveProfileDir(localState, wanted);
|
|
2198
2712
|
if (ambiguous)
|
|
2199
2713
|
throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
|
|
2200
2714
|
if (!dir)
|
|
2201
2715
|
throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
|
|
2202
|
-
const db = cookieDbCandidates(
|
|
2716
|
+
const db = cookieDbCandidates(join10(root, dir)).find((p) => existsSync8(p));
|
|
2203
2717
|
if (!db)
|
|
2204
2718
|
throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
|
|
2205
2719
|
return db;
|
|
@@ -2209,7 +2723,7 @@ async function readCookieRowsBun(home, source) {
|
|
|
2209
2723
|
}
|
|
2210
2724
|
function keychainKey(service) {
|
|
2211
2725
|
try {
|
|
2212
|
-
return
|
|
2726
|
+
return execFileSync4("security", ["find-generic-password", "-w", "-s", service], {
|
|
2213
2727
|
encoding: "utf8"
|
|
2214
2728
|
}).trim();
|
|
2215
2729
|
} catch {
|
|
@@ -2221,7 +2735,7 @@ function decryptJar(rows, safeStoragePw) {
|
|
|
2221
2735
|
}
|
|
2222
2736
|
var curlUsage = (org, cookie) => {
|
|
2223
2737
|
try {
|
|
2224
|
-
const out =
|
|
2738
|
+
const out = execFileSync4("curl", usageCurlArgs(org, cookie), {
|
|
2225
2739
|
encoding: "utf8",
|
|
2226
2740
|
maxBuffer: 8000000
|
|
2227
2741
|
});
|
|
@@ -2242,7 +2756,7 @@ function fetchSeatUsage(jar, org, now, run) {
|
|
|
2242
2756
|
}
|
|
2243
2757
|
function launchAgentPlist(bun, script, home, source) {
|
|
2244
2758
|
const label = launchLabel(source);
|
|
2245
|
-
const log =
|
|
2759
|
+
const log = join10(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
|
|
2246
2760
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2247
2761
|
const args = [bun, script, "seat-sync", ...sourceArgs(source)].map((a) => ` <string>${esc(a)}</string>`).join(`
|
|
2248
2762
|
`);
|
|
@@ -2264,7 +2778,7 @@ ${args}
|
|
|
2264
2778
|
`;
|
|
2265
2779
|
}
|
|
2266
2780
|
function plistPath2(home, source) {
|
|
2267
|
-
return
|
|
2781
|
+
return join10(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
|
|
2268
2782
|
}
|
|
2269
2783
|
function install2(home, argv, source, log) {
|
|
2270
2784
|
const bun = argv[0] ?? process.execPath;
|
|
@@ -2273,23 +2787,23 @@ function install2(home, argv, source, log) {
|
|
|
2273
2787
|
log("cannot resolve the sleds script path to install the timer");
|
|
2274
2788
|
return 1;
|
|
2275
2789
|
}
|
|
2276
|
-
const dir =
|
|
2790
|
+
const dir = join10(home, "Library", "LaunchAgents");
|
|
2277
2791
|
mkdirSync8(dir, { recursive: true });
|
|
2278
2792
|
const path = plistPath2(home, source);
|
|
2279
2793
|
writeFileSync8(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
|
|
2280
2794
|
try {
|
|
2281
|
-
|
|
2795
|
+
execFileSync4("launchctl", ["unload", path], { stdio: "ignore" });
|
|
2282
2796
|
} catch {}
|
|
2283
|
-
|
|
2797
|
+
execFileSync4("launchctl", ["load", "-w", path]);
|
|
2284
2798
|
log(`seat-sync timer installed (every ${SYNC_INTERVAL_S / 60} min) → ${path}`);
|
|
2285
2799
|
return 0;
|
|
2286
2800
|
}
|
|
2287
2801
|
function uninstall2(home, source, log) {
|
|
2288
2802
|
const path = plistPath2(home, source);
|
|
2289
2803
|
try {
|
|
2290
|
-
|
|
2804
|
+
execFileSync4("launchctl", ["unload", "-w", path], { stdio: "ignore" });
|
|
2291
2805
|
} catch {}
|
|
2292
|
-
log(
|
|
2806
|
+
log(existsSync8(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
|
|
2293
2807
|
return 0;
|
|
2294
2808
|
}
|
|
2295
2809
|
var seatSync = async (o) => {
|
|
@@ -2384,7 +2898,7 @@ async function postSeat(proxyUrl, token, body, fetchFn) {
|
|
|
2384
2898
|
}
|
|
2385
2899
|
|
|
2386
2900
|
// src/commands/specEvent.ts
|
|
2387
|
-
import { readFileSync as
|
|
2901
|
+
import { readFileSync as readFileSync7, statSync } from "node:fs";
|
|
2388
2902
|
import { basename, isAbsolute, relative } from "node:path";
|
|
2389
2903
|
var HOOK_STDIN_TIMEOUT_MS2 = 1000;
|
|
2390
2904
|
var ESTIMATE_TIMEOUT_MS = 4000;
|
|
@@ -2440,7 +2954,7 @@ function readFileOrNull(path) {
|
|
|
2440
2954
|
try {
|
|
2441
2955
|
if (statSync(path).size > 2000000)
|
|
2442
2956
|
return null;
|
|
2443
|
-
return
|
|
2957
|
+
return readFileSync7(path, "utf8");
|
|
2444
2958
|
} catch {
|
|
2445
2959
|
return null;
|
|
2446
2960
|
}
|
|
@@ -2483,8 +2997,8 @@ var specEvent = async (o) => {
|
|
|
2483
2997
|
};
|
|
2484
2998
|
|
|
2485
2999
|
// src/commands/status.ts
|
|
2486
|
-
import { existsSync as
|
|
2487
|
-
var
|
|
3000
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
3001
|
+
var checkHealth2 = async (url, fetchFn = fetch, timeoutMs = 1000) => {
|
|
2488
3002
|
try {
|
|
2489
3003
|
const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2490
3004
|
return res.ok ? "ok" : "unreachable";
|
|
@@ -2551,7 +3065,7 @@ var status = async (o) => {
|
|
|
2551
3065
|
const log = o.log ?? console.log;
|
|
2552
3066
|
const nowMs = (o.now ?? Date.now)();
|
|
2553
3067
|
const cfg = readConfig(o.home);
|
|
2554
|
-
const settings =
|
|
3068
|
+
const settings = existsSync9(settingsPath(o.home)) ? readSettings(o.home) : {};
|
|
2555
3069
|
const inSettings = readProxy(settings);
|
|
2556
3070
|
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
2557
3071
|
const token = inSettings?.token ?? cfg.token;
|
|
@@ -2585,7 +3099,7 @@ var status = async (o) => {
|
|
|
2585
3099
|
info.untilResetMs = until;
|
|
2586
3100
|
if (proxyUrl) {
|
|
2587
3101
|
const [health, proxy] = await Promise.all([
|
|
2588
|
-
|
|
3102
|
+
checkHealth2(proxyUrl, o.fetchFn, o.timeoutMs),
|
|
2589
3103
|
fetchProxyStatus(proxyUrl, o.fetchFn, o.timeoutMs)
|
|
2590
3104
|
]);
|
|
2591
3105
|
info.health = health;
|
|
@@ -2649,11 +3163,11 @@ var rememberSeat = (o, window, now) => {
|
|
|
2649
3163
|
};
|
|
2650
3164
|
|
|
2651
3165
|
// src/commands/uninstall.ts
|
|
2652
|
-
import { existsSync as
|
|
3166
|
+
import { existsSync as existsSync10, rmSync as rmSync3 } from "node:fs";
|
|
2653
3167
|
var uninstall3 = (o) => {
|
|
2654
3168
|
const log = o.log ?? console.log;
|
|
2655
3169
|
const removed = [];
|
|
2656
|
-
if (
|
|
3170
|
+
if (existsSync10(settingsPath(o.home))) {
|
|
2657
3171
|
let settings = readSettings(o.home);
|
|
2658
3172
|
let changed = false;
|
|
2659
3173
|
if (hasEitherKey(settings)) {
|
|
@@ -2685,13 +3199,13 @@ var uninstall3 = (o) => {
|
|
|
2685
3199
|
writeSettings(o.home, settings);
|
|
2686
3200
|
}
|
|
2687
3201
|
for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
|
|
2688
|
-
if (
|
|
3202
|
+
if (existsSync10(file)) {
|
|
2689
3203
|
rmSync3(file, { force: true });
|
|
2690
3204
|
removed.push(file);
|
|
2691
3205
|
}
|
|
2692
3206
|
}
|
|
2693
3207
|
const skill = skillDir(o.home);
|
|
2694
|
-
if (
|
|
3208
|
+
if (existsSync10(skill)) {
|
|
2695
3209
|
rmSync3(skill, { recursive: true, force: true });
|
|
2696
3210
|
removed.push(skill);
|
|
2697
3211
|
}
|
|
@@ -2728,6 +3242,7 @@ commands:
|
|
|
2728
3242
|
init Point Claude Code at a SLEDS proxy, install the /sled skill and the status line.
|
|
2729
3243
|
--token <sled_\u2026> seat token (else SLEDS_TOKEN, ~/.sleds-token, or prompt)
|
|
2730
3244
|
--url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
|
|
3245
|
+
--no-probe skip the /health check of the proxy URL
|
|
2731
3246
|
--non-interactive never prompt; exit 2 if no token is available
|
|
2732
3247
|
--signup no org? create an individual seat: your email + your own
|
|
2733
3248
|
Anthropic API key (validated, stored encrypted, the only key it spends)
|
|
@@ -2738,8 +3253,11 @@ commands:
|
|
|
2738
3253
|
off Switch Claude Code back to your seat. Restores whatever \`on\` replaced.
|
|
2739
3254
|
uninstall Remove the proxy settings, skill and status line. Keeps ~/.sleds-token.
|
|
2740
3255
|
status Show the switch state, deadline, proxy health and wall mode.
|
|
3256
|
+
doctor Check the whole install: which seat this shell is, Claude settings, proxy, hooks,
|
|
3257
|
+
Node version, stale links, the SwiftBar plugin, the seat-sync timer.
|
|
2741
3258
|
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
|
|
3259
|
+
bar Print the menu-bar meter in SwiftBar format; --install links it into SwiftBar
|
|
3260
|
+
(--home/--claude-config pick the seat it tracks; --name <x> adds a second item).
|
|
2743
3261
|
mcp Run the MCP server on stdio: tools usage, estimate, overnight, jobs.
|
|
2744
3262
|
Register with \`claude mcp add sleds -- sleds mcp\` (Cowork sees the same server).
|
|
2745
3263
|
seat-sync Read the seat's subscription usage (weekly/5h limits, per-model, credits) from a
|
|
@@ -2754,6 +3272,7 @@ commands:
|
|
|
2754
3272
|
seat-event Claude Code hook: record a seat wall and say how to continue. Reads hook JSON on stdin.
|
|
2755
3273
|
spec-event Claude Code PostToolUse hook: when a spec-shaped markdown file is written, size it
|
|
2756
3274
|
(POST /proxy/estimate with the file path as the project). Silent unless it estimates.
|
|
3275
|
+
--version Print the CLI version.
|
|
2757
3276
|
--help Show this message.
|
|
2758
3277
|
|
|
2759
3278
|
env:
|
|
@@ -2799,7 +3318,7 @@ var seatEventCommand = (argv) => [...argv, "seat-event"].map(shellQuote2).join("
|
|
|
2799
3318
|
var specEventCommand = (argv) => [...argv, "spec-event"].map(shellQuote2).join(" ");
|
|
2800
3319
|
var main = async (argv) => {
|
|
2801
3320
|
const { cmd, flags } = parseArgs(argv);
|
|
2802
|
-
const home =
|
|
3321
|
+
const home = homedir3();
|
|
2803
3322
|
const scheduler = {
|
|
2804
3323
|
platform: platform2(),
|
|
2805
3324
|
home,
|
|
@@ -2814,6 +3333,7 @@ var main = async (argv) => {
|
|
|
2814
3333
|
url: stringFlag(flags, "url"),
|
|
2815
3334
|
nonInteractive: flags["non-interactive"] === true,
|
|
2816
3335
|
signup: flags.signup === true,
|
|
3336
|
+
noProbe: flags["no-probe"] === true,
|
|
2817
3337
|
prompt: promptStdin,
|
|
2818
3338
|
statusLineCommand: statusLineCommand(sledsArgv()),
|
|
2819
3339
|
seatEventCommand: seatEventCommand(sledsArgv()),
|
|
@@ -2842,6 +3362,8 @@ var main = async (argv) => {
|
|
|
2842
3362
|
return off({ home, scheduler });
|
|
2843
3363
|
case "uninstall":
|
|
2844
3364
|
return uninstall3({ home });
|
|
3365
|
+
case "doctor":
|
|
3366
|
+
return doctor({ home });
|
|
2845
3367
|
case "status":
|
|
2846
3368
|
return status({ home });
|
|
2847
3369
|
case "meter":
|
|
@@ -2859,6 +3381,11 @@ var main = async (argv) => {
|
|
|
2859
3381
|
case "-h":
|
|
2860
3382
|
console.log(USAGE);
|
|
2861
3383
|
return 0;
|
|
3384
|
+
case "version":
|
|
3385
|
+
case "--version":
|
|
3386
|
+
case "-v":
|
|
3387
|
+
console.log(`sleds ${"0.1.5"}`);
|
|
3388
|
+
return 0;
|
|
2862
3389
|
default:
|
|
2863
3390
|
console.error(`unknown command: ${cmd}
|
|
2864
3391
|
`);
|