@sleds/cli 0.1.0 → 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.
- package/README.md +2 -1
- package/dist/sleds.mjs +881 -193
- package/package.json +2 -2
package/dist/sleds.mjs
CHANGED
|
@@ -5,9 +5,23 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
import { realpathSync } from "fs";
|
|
8
|
-
import { homedir, platform as platform2 } from "os";
|
|
8
|
+
import { homedir as homedir3, platform as platform2 } from "os";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
|
|
11
|
+
// src/commands/bar.ts
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import {
|
|
14
|
+
chmodSync as chmodSync2,
|
|
15
|
+
existsSync as existsSync5,
|
|
16
|
+
lstatSync,
|
|
17
|
+
mkdirSync as mkdirSync5,
|
|
18
|
+
symlinkSync,
|
|
19
|
+
unlinkSync,
|
|
20
|
+
writeFileSync as writeFileSync5
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join as join5, resolve as resolve3 } from "node:path";
|
|
24
|
+
|
|
11
25
|
// src/meter.ts
|
|
12
26
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
13
27
|
import { join as join3 } from "node:path";
|
|
@@ -18,7 +32,7 @@ import { join as join2, resolve as resolve2 } from "node:path";
|
|
|
18
32
|
|
|
19
33
|
// src/settings.ts
|
|
20
34
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { join, resolve } from "node:path";
|
|
35
|
+
import { dirname, join, resolve } from "node:path";
|
|
22
36
|
var BASE_URL_KEY = "ANTHROPIC_BASE_URL";
|
|
23
37
|
var AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN";
|
|
24
38
|
var claudeDir = (home, env = process.env) => {
|
|
@@ -28,7 +42,10 @@ var claudeDir = (home, env = process.env) => {
|
|
|
28
42
|
var hasHomeOverride = (env = process.env) => Boolean(env.SLEDS_HOME?.trim() || env.CLAUDE_CONFIG_DIR?.trim());
|
|
29
43
|
var settingsPath = (home) => join(claudeDir(home), "settings.json");
|
|
30
44
|
var backupPath = (home) => join(claudeDir(home), "settings.json.sleds-backup");
|
|
31
|
-
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
|
+
};
|
|
32
49
|
var skillDir = (home) => join(claudeDir(home), "skills", "sled");
|
|
33
50
|
var skillPath = (home) => join(skillDir(home), "SKILL.md");
|
|
34
51
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -196,6 +213,7 @@ var readTokenFile = (home) => {
|
|
|
196
213
|
return t.length > 0 ? t : null;
|
|
197
214
|
};
|
|
198
215
|
var writeTokenFile = (home, token) => {
|
|
216
|
+
mkdirSync(dirname(tokenPath(home)), { recursive: true, mode: 448 });
|
|
199
217
|
writeFileSync(tokenPath(home), `${token}
|
|
200
218
|
`, { mode: 384 });
|
|
201
219
|
};
|
|
@@ -489,6 +507,95 @@ var loadMeter = async (o) => {
|
|
|
489
507
|
return fresh;
|
|
490
508
|
};
|
|
491
509
|
|
|
510
|
+
// src/bar.ts
|
|
511
|
+
var SEAT_BAR_FRESH_MS = 6 * 60 * 60 * 1000;
|
|
512
|
+
var DASHBOARD_URL = "https://app.sleds.ai";
|
|
513
|
+
var WARM_PCT = 75;
|
|
514
|
+
var HOT_PCT = 90;
|
|
515
|
+
var MONO = "font=Menlo size=12";
|
|
516
|
+
var HOT_COLOR = "#ff453a";
|
|
517
|
+
var WARM_COLOR = "#ff9f0a";
|
|
518
|
+
var freshSeat = (w, now) => {
|
|
519
|
+
if (!w)
|
|
520
|
+
return null;
|
|
521
|
+
return now - w.capturedAt < SEAT_BAR_FRESH_MS ? w : null;
|
|
522
|
+
};
|
|
523
|
+
var modelLabel = (bucket) => bucket.replace(/^(seven_day|five_hour|weekly|daily)_/, "").toLowerCase();
|
|
524
|
+
var seatBars = (w) => {
|
|
525
|
+
if (!w)
|
|
526
|
+
return [];
|
|
527
|
+
const out = [];
|
|
528
|
+
if (w.sevenDay)
|
|
529
|
+
out.push({ label: "wk", bar: w.sevenDay });
|
|
530
|
+
if (w.fiveHour)
|
|
531
|
+
out.push({ label: "5h", bar: w.fiveHour });
|
|
532
|
+
if (w.spendLimit)
|
|
533
|
+
out.push({ label: "spend", bar: w.spendLimit });
|
|
534
|
+
for (const k of Object.keys(w.models ?? {}).sort()) {
|
|
535
|
+
const bar = w.models?.[k];
|
|
536
|
+
if (bar)
|
|
537
|
+
out.push({ label: modelLabel(k), bar });
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
};
|
|
541
|
+
var hottest = (w) => seatBars(w).reduce((best, cur) => !best || cur.bar.usedPct > best.bar.usedPct ? cur : best, null);
|
|
542
|
+
var toneOf = (pct) => pct === undefined ? null : pct >= HOT_PCT ? "hot" : pct >= WARM_PCT ? "warm" : null;
|
|
543
|
+
var colorParam = (tone) => tone === "hot" ? ` color=${HOT_COLOR}` : tone === "warm" ? ` color=${WARM_COLOR}` : "";
|
|
544
|
+
var barTitle = (m, seat) => {
|
|
545
|
+
const head = m ? usd(m.today.spentUsd) : "offline";
|
|
546
|
+
const wk = seat?.sevenDay ? `wk ${Math.round(seat.sevenDay.usedPct)}%` : null;
|
|
547
|
+
const top = hottest(seat);
|
|
548
|
+
const risk = top && top.label !== "wk" && toneOf(top.bar.usedPct) ? `${top.label} ${Math.round(top.bar.usedPct)}%` : null;
|
|
549
|
+
return [`sleds ${head}`, ...wk ? [wk] : [], ...risk ? [risk] : []].join(SEP);
|
|
550
|
+
};
|
|
551
|
+
var resetIn = (bar, now) => bar.resetsAt > now ? `${SEP}resets in ${shortDuration(bar.resetsAt - now)}` : "";
|
|
552
|
+
var formatBar = (m, seat, now) => {
|
|
553
|
+
const tone = toneOf(hottest(seat)?.bar.usedPct);
|
|
554
|
+
const lines = [`${barTitle(m, seat)}${colorParam(tone)}`, "---"];
|
|
555
|
+
if (m) {
|
|
556
|
+
for (const row2 of formatMeter(m, now))
|
|
557
|
+
lines.push(`${row2} | ${MONO}`);
|
|
558
|
+
} else {
|
|
559
|
+
lines.push(`offline${SEP}proxy did not answer | ${MONO}`);
|
|
560
|
+
}
|
|
561
|
+
const bars = seatBars(seat);
|
|
562
|
+
if (bars.length > 0 || seat?.usedCreditsUsd !== undefined) {
|
|
563
|
+
lines.push("---");
|
|
564
|
+
lines.push(`Seat limits${seat ? `${SEP}${shortDuration(now - seat.capturedAt)} ago` : ""} | ${MONO}`);
|
|
565
|
+
for (const { label, bar } of bars) {
|
|
566
|
+
const pct = Math.round(bar.usedPct);
|
|
567
|
+
lines.push(`${label.padEnd(12)}${String(pct).padStart(3)}%${resetIn(bar, now)} | ${MONO}${colorParam(toneOf(bar.usedPct))}`);
|
|
568
|
+
}
|
|
569
|
+
if (seat?.usedCreditsUsd !== undefined)
|
|
570
|
+
lines.push(`${"credits".padEnd(12)}${usd(seat.usedCreditsUsd)} spent | ${MONO}`);
|
|
571
|
+
}
|
|
572
|
+
lines.push("---");
|
|
573
|
+
lines.push(`Open dashboard | href=${DASHBOARD_URL}`);
|
|
574
|
+
lines.push("Refresh | refresh=true");
|
|
575
|
+
return lines;
|
|
576
|
+
};
|
|
577
|
+
var pluginScript = (nodePath, cliPath, env = {}) => [
|
|
578
|
+
"#!/bin/bash",
|
|
579
|
+
"# SLEDS menu-bar meter for SwiftBar — written by `sleds bar --install`; re-run it after moving the CLI.",
|
|
580
|
+
"# <swiftbar.title>SLEDS</swiftbar.title>",
|
|
581
|
+
"# <swiftbar.hideAbout>true</swiftbar.hideAbout>",
|
|
582
|
+
"# <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal>",
|
|
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"],
|
|
586
|
+
'export PATH="$HOME/.bun/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"',
|
|
587
|
+
`if [ -x ${shellQuote(nodePath)} ] && [ -f ${shellQuote(cliPath)} ]; then`,
|
|
588
|
+
` exec ${shellQuote(nodePath)} ${shellQuote(cliPath)} bar`,
|
|
589
|
+
"fi",
|
|
590
|
+
"if command -v sleds >/dev/null 2>&1; then exec sleds bar; fi",
|
|
591
|
+
'echo "sleds not found"',
|
|
592
|
+
'echo "---"',
|
|
593
|
+
'echo "Run \\`npx @sleds/cli bar --install\\` again. | font=Menlo size=12"',
|
|
594
|
+
""
|
|
595
|
+
].join(`
|
|
596
|
+
`);
|
|
597
|
+
var shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
598
|
+
|
|
492
599
|
// src/seat.ts
|
|
493
600
|
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
494
601
|
import { join as join4 } from "node:path";
|
|
@@ -528,7 +635,16 @@ var parseSeatWindow = (raw, capturedAt) => {
|
|
|
528
635
|
const fiveHour = parseBar(limits.five_hour ?? limits.fiveHour);
|
|
529
636
|
const sevenDay = parseBar(limits.seven_day ?? limits.sevenDay);
|
|
530
637
|
const spendLimit = parseBar(limits.spend_limit ?? limits.spendLimit);
|
|
531
|
-
|
|
638
|
+
const models = {};
|
|
639
|
+
if (isObject4(limits.models))
|
|
640
|
+
for (const [k, v] of Object.entries(limits.models)) {
|
|
641
|
+
const bar = parseBar(v);
|
|
642
|
+
if (bar)
|
|
643
|
+
models[k] = bar;
|
|
644
|
+
}
|
|
645
|
+
const usedCreditsUsd = num2(limits.usedCreditsUsd);
|
|
646
|
+
const hasModels = Object.keys(models).length > 0;
|
|
647
|
+
if (!fiveHour && !sevenDay && !spendLimit && !hasModels && usedCreditsUsd === undefined)
|
|
532
648
|
return null;
|
|
533
649
|
const out = { capturedAt };
|
|
534
650
|
if (fiveHour)
|
|
@@ -537,6 +653,10 @@ var parseSeatWindow = (raw, capturedAt) => {
|
|
|
537
653
|
out.sevenDay = sevenDay;
|
|
538
654
|
if (spendLimit)
|
|
539
655
|
out.spendLimit = spendLimit;
|
|
656
|
+
if (hasModels)
|
|
657
|
+
out.models = models;
|
|
658
|
+
if (usedCreditsUsd !== undefined)
|
|
659
|
+
out.usedCreditsUsd = usedCreditsUsd;
|
|
540
660
|
return out;
|
|
541
661
|
};
|
|
542
662
|
var seatWindowFromStdin = (text, capturedAt) => {
|
|
@@ -702,40 +822,31 @@ var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) =>
|
|
|
702
822
|
stdin.resume();
|
|
703
823
|
});
|
|
704
824
|
|
|
705
|
-
// src/bar.ts
|
|
706
|
-
var SEAT_BAR_FRESH_MS = 6 * 60 * 60 * 1000;
|
|
707
|
-
var DASHBOARD_URL = "https://app.sleds.ai";
|
|
708
|
-
var MONO = "font=Menlo size=12";
|
|
709
|
-
var freshSeat = (w, now) => {
|
|
710
|
-
if (!w)
|
|
711
|
-
return null;
|
|
712
|
-
return now - w.capturedAt < SEAT_BAR_FRESH_MS ? w : null;
|
|
713
|
-
};
|
|
714
|
-
var barTitle = (m, seat) => {
|
|
715
|
-
const head = m ? usd(m.today.spentUsd) : "offline";
|
|
716
|
-
const wk = seat?.sevenDay ? `wk ${Math.round(seat.sevenDay.usedPct)}%` : null;
|
|
717
|
-
return [`sleds ${head}`, ...wk ? [wk] : []].join(SEP);
|
|
718
|
-
};
|
|
719
|
-
var formatBar = (m, seat, now) => {
|
|
720
|
-
const lines = [barTitle(m, seat), "---"];
|
|
721
|
-
if (m) {
|
|
722
|
-
for (const row2 of formatMeter(m, now))
|
|
723
|
-
lines.push(`${row2} | ${MONO}`);
|
|
724
|
-
} else {
|
|
725
|
-
lines.push(`offline${SEP}proxy did not answer | ${MONO}`);
|
|
726
|
-
}
|
|
727
|
-
const frag = formatSeatFragment(seat);
|
|
728
|
-
if (frag)
|
|
729
|
-
lines.push(`${"Limits:".padEnd(12)}${frag.replace(/^seat /, "")} | ${MONO}`);
|
|
730
|
-
lines.push("---");
|
|
731
|
-
lines.push(`Open dashboard | href=${DASHBOARD_URL}`);
|
|
732
|
-
lines.push("Refresh | refresh=true");
|
|
733
|
-
return lines;
|
|
734
|
-
};
|
|
735
|
-
|
|
736
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
|
+
}
|
|
841
|
+
var PLUGIN_NAME = "sleds.1m.sh";
|
|
842
|
+
var pluginName = (name) => name ? `sleds-${name}.1m.sh` : PLUGIN_NAME;
|
|
737
843
|
var bar = async (o) => {
|
|
738
844
|
const log = o.log ?? console.log;
|
|
845
|
+
const argv = o.argv ?? [];
|
|
846
|
+
if (argv.includes("--install"))
|
|
847
|
+
return install(o, log);
|
|
848
|
+
if (argv.includes("--uninstall"))
|
|
849
|
+
return uninstall(o, log);
|
|
739
850
|
const now = (o.now ?? Date.now)();
|
|
740
851
|
const seat = freshSeat(readSeatState(o.home).window, now);
|
|
741
852
|
if (!meterSource(o.home)) {
|
|
@@ -749,6 +860,393 @@ var bar = async (o) => {
|
|
|
749
860
|
log(line);
|
|
750
861
|
return 0;
|
|
751
862
|
};
|
|
863
|
+
function swiftBarPluginDir() {
|
|
864
|
+
try {
|
|
865
|
+
const out = execFileSync("defaults", ["read", "com.ameba.SwiftBar", "PluginDirectory"], {
|
|
866
|
+
encoding: "utf8",
|
|
867
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
868
|
+
}).trim();
|
|
869
|
+
return out ? out.replace(/^~(?=$|\/)/, homedir()) : null;
|
|
870
|
+
} catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
var pluginPath = (home, name) => join5(configDir(home, {}), "swiftbar", pluginName(name));
|
|
875
|
+
function install(o, log) {
|
|
876
|
+
const opts = parseInstallArgs(o.argv ?? [], o.env ?? process.env);
|
|
877
|
+
const script = pluginPath(o.home, opts.name);
|
|
878
|
+
mkdirSync5(join5(script, ".."), { recursive: true, mode: 448 });
|
|
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
|
+
}), {
|
|
883
|
+
mode: 493
|
|
884
|
+
});
|
|
885
|
+
chmodSync2(script, 493);
|
|
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(" ")}`);
|
|
889
|
+
const dir = (o.swiftBarPluginDir ?? swiftBarPluginDir)();
|
|
890
|
+
if (!dir) {
|
|
891
|
+
log("SwiftBar is not set up yet: `brew install --cask swiftbar`, launch it once and pick a plugin");
|
|
892
|
+
log("folder, then run `sleds bar --install` again — or symlink the file above into that folder.");
|
|
893
|
+
return 0;
|
|
894
|
+
}
|
|
895
|
+
const link = join5(dir, pluginName(opts.name));
|
|
896
|
+
try {
|
|
897
|
+
mkdirSync5(dir, { recursive: true });
|
|
898
|
+
if (existsSync5(link) || isDanglingLink(link))
|
|
899
|
+
unlinkSync(link);
|
|
900
|
+
symlinkSync(script, link);
|
|
901
|
+
log(`linked ${link}`);
|
|
902
|
+
log("SwiftBar picks it up within a minute; the item reads `sleds $… · wk …%`.");
|
|
903
|
+
return 0;
|
|
904
|
+
} catch (e) {
|
|
905
|
+
log(`could not link into ${dir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
906
|
+
log(`symlink it yourself: ln -sf ${script} ${link}`);
|
|
907
|
+
return 1;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function uninstall(o, log) {
|
|
911
|
+
const { name } = parseInstallArgs(o.argv ?? [], {});
|
|
912
|
+
const dir = (o.swiftBarPluginDir ?? swiftBarPluginDir)();
|
|
913
|
+
const link = dir ? join5(dir, pluginName(name)) : null;
|
|
914
|
+
if (link && (existsSync5(link) || isDanglingLink(link))) {
|
|
915
|
+
unlinkSync(link);
|
|
916
|
+
log(`removed ${link}`);
|
|
917
|
+
} else
|
|
918
|
+
log("no SwiftBar plugin link to remove");
|
|
919
|
+
const script = pluginPath(o.home, name);
|
|
920
|
+
if (existsSync5(script)) {
|
|
921
|
+
unlinkSync(script);
|
|
922
|
+
log(`removed ${script}`);
|
|
923
|
+
}
|
|
924
|
+
return 0;
|
|
925
|
+
}
|
|
926
|
+
var isDanglingLink = (p) => {
|
|
927
|
+
try {
|
|
928
|
+
return lstatSync(p).isSymbolicLink();
|
|
929
|
+
} catch {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
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
|
+
};
|
|
752
1250
|
|
|
753
1251
|
// src/commands/hooks.ts
|
|
754
1252
|
var DEFAULT_SEAT_EVENT_COMMAND = "sleds seat-event";
|
|
@@ -771,7 +1269,7 @@ var hooks = (o) => {
|
|
|
771
1269
|
};
|
|
772
1270
|
|
|
773
1271
|
// src/commands/init.ts
|
|
774
|
-
import { mkdirSync as
|
|
1272
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
775
1273
|
import { createInterface } from "node:readline";
|
|
776
1274
|
|
|
777
1275
|
// src/skill.ts
|
|
@@ -803,6 +1301,7 @@ When the proxy is active it answers a walled request with its options. The user
|
|
|
803
1301
|
/sled 2 hold; resume when the window resets
|
|
804
1302
|
/sled 3 run overnight at half price
|
|
805
1303
|
/sled 4 switch to a cheaper model for the rest of the session (offered when one saves ≥15%)
|
|
1304
|
+
/sled 5 hand off to an idle coworker, or park for the team (offered on a team of two or more)
|
|
806
1305
|
\`\`\`
|
|
807
1306
|
|
|
808
1307
|
## Window reset
|
|
@@ -861,6 +1360,16 @@ List the org's jobs: queued, running, delivered, parked, with the estimate band,
|
|
|
861
1360
|
|
|
862
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.
|
|
863
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
|
+
|
|
864
1373
|
## Voice
|
|
865
1374
|
|
|
866
1375
|
Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what happens next, not what could.
|
|
@@ -869,10 +1378,18 @@ Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what
|
|
|
869
1378
|
|
|
870
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:
|
|
871
1380
|
|
|
872
|
-
- \`/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).
|
|
873
1382
|
- \`/sled resume <id>\` — pull a delivered job's output into the session as the assistant's reply and keep going.
|
|
874
1383
|
|
|
875
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.
|
|
1385
|
+
|
|
1386
|
+
## Team hand-off
|
|
1387
|
+
|
|
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.
|
|
1389
|
+
|
|
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.
|
|
876
1393
|
`;
|
|
877
1394
|
|
|
878
1395
|
// src/commands/hooks.ts
|
|
@@ -898,11 +1415,11 @@ var hooks2 = (o) => {
|
|
|
898
1415
|
// src/commands/init.ts
|
|
899
1416
|
var DEFAULT_PROXY_URL = "http://localhost:8787";
|
|
900
1417
|
var DEFAULT_STATUS_LINE_COMMAND = "sleds statusline";
|
|
901
|
-
var promptStdin = (question) => new Promise((
|
|
1418
|
+
var promptStdin = (question) => new Promise((resolve5) => {
|
|
902
1419
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
903
1420
|
rl.question(question, (answer) => {
|
|
904
1421
|
rl.close();
|
|
905
|
-
|
|
1422
|
+
resolve5(answer.trim());
|
|
906
1423
|
});
|
|
907
1424
|
});
|
|
908
1425
|
async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
|
|
@@ -937,6 +1454,33 @@ var SIGNUP_REASONS = {
|
|
|
937
1454
|
function signupFailureText(r) {
|
|
938
1455
|
return `Signup failed: ${SIGNUP_REASONS[r.reason] ?? `unexpected answer (${r.reason}, HTTP ${r.status}).`}`;
|
|
939
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
|
+
}
|
|
940
1484
|
var resolveToken = async (o) => {
|
|
941
1485
|
const env = o.env ?? process.env;
|
|
942
1486
|
if (o.token)
|
|
@@ -953,7 +1497,9 @@ var resolveToken = async (o) => {
|
|
|
953
1497
|
if (typed)
|
|
954
1498
|
return { token: typed, source: "prompt" };
|
|
955
1499
|
}
|
|
956
|
-
const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL)
|
|
1500
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL);
|
|
1501
|
+
if (!url)
|
|
1502
|
+
return null;
|
|
957
1503
|
const email = (await o.prompt("Your email: ")).trim();
|
|
958
1504
|
if (!email.includes("@"))
|
|
959
1505
|
return null;
|
|
@@ -985,7 +1531,15 @@ var init = async (o) => {
|
|
|
985
1531
|
error("Invalid seat token: expected something like sled_xxxxxxxx.");
|
|
986
1532
|
return 2;
|
|
987
1533
|
}
|
|
988
|
-
const url = (o.url ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL)
|
|
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
|
+
}
|
|
989
1543
|
const wrote = [];
|
|
990
1544
|
if (backupSettings(o.home))
|
|
991
1545
|
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
@@ -1013,17 +1567,17 @@ var init = async (o) => {
|
|
|
1013
1567
|
...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
|
|
1014
1568
|
});
|
|
1015
1569
|
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
1016
|
-
|
|
1017
|
-
|
|
1570
|
+
mkdirSync6(skillDir(o.home), { recursive: true });
|
|
1571
|
+
writeFileSync6(skillPath(o.home), SKILL_MD);
|
|
1018
1572
|
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
1019
|
-
log("
|
|
1573
|
+
log("Claude Code now runs through SLEDS on the org key.");
|
|
1020
1574
|
log("");
|
|
1021
1575
|
for (const w of wrote)
|
|
1022
1576
|
log(` ${w}`);
|
|
1023
1577
|
log("");
|
|
1024
|
-
log(`
|
|
1578
|
+
log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
|
|
1025
1579
|
log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
|
|
1026
|
-
log("
|
|
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.");
|
|
1027
1581
|
return 0;
|
|
1028
1582
|
};
|
|
1029
1583
|
|
|
@@ -1031,6 +1585,21 @@ var init = async (o) => {
|
|
|
1031
1585
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
1032
1586
|
var SERVER_INFO = { name: "sleds", version: "0.0.1" };
|
|
1033
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
|
+
};
|
|
1034
1603
|
var specSchema = {
|
|
1035
1604
|
type: "object",
|
|
1036
1605
|
properties: {
|
|
@@ -1038,7 +1607,8 @@ var specSchema = {
|
|
|
1038
1607
|
type: "string",
|
|
1039
1608
|
description: "What the job should do. The first line becomes the title."
|
|
1040
1609
|
},
|
|
1041
|
-
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
|
|
1042
1612
|
},
|
|
1043
1613
|
required: ["spec"]
|
|
1044
1614
|
};
|
|
@@ -1060,8 +1630,38 @@ var TOOLS = [
|
|
|
1060
1630
|
},
|
|
1061
1631
|
{
|
|
1062
1632
|
name: "jobs",
|
|
1063
|
-
description: "This seat's overnight jobs, newest first
|
|
1633
|
+
description: "This seat's overnight jobs, newest first (id, status, route, cost, when), then the team's open hand-offs waiting for pickup.",
|
|
1064
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
|
+
}
|
|
1065
1665
|
}
|
|
1066
1666
|
];
|
|
1067
1667
|
var NOT_SET_UP = {
|
|
@@ -1072,7 +1672,7 @@ var UNREACHABLE = {
|
|
|
1072
1672
|
text: "Control plane unreachable. Nothing was recorded → try again in a moment.",
|
|
1073
1673
|
isError: true
|
|
1074
1674
|
};
|
|
1075
|
-
var
|
|
1675
|
+
var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1076
1676
|
var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
1077
1677
|
var str2 = (v) => typeof v === "string" ? v : undefined;
|
|
1078
1678
|
async function proxyPost(deps, path, body) {
|
|
@@ -1090,11 +1690,35 @@ async function proxyPost(deps, path, body) {
|
|
|
1090
1690
|
if (!res.ok)
|
|
1091
1691
|
return null;
|
|
1092
1692
|
const parsed = await res.json();
|
|
1093
|
-
return
|
|
1693
|
+
return isObject6(parsed) ? parsed : null;
|
|
1694
|
+
} catch {
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
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 };
|
|
1094
1717
|
} catch {
|
|
1095
1718
|
return null;
|
|
1096
1719
|
}
|
|
1097
1720
|
}
|
|
1721
|
+
var errorMessage = (j) => isObject6(j?.error) ? str2(j.error.message) : undefined;
|
|
1098
1722
|
async function proxyGet(deps, path) {
|
|
1099
1723
|
const src = meterSource(deps.home);
|
|
1100
1724
|
if (!src)
|
|
@@ -1108,7 +1732,7 @@ async function proxyGet(deps, path) {
|
|
|
1108
1732
|
if (!res.ok)
|
|
1109
1733
|
return null;
|
|
1110
1734
|
const parsed = await res.json();
|
|
1111
|
-
return
|
|
1735
|
+
return isObject6(parsed) ? parsed : null;
|
|
1112
1736
|
} catch {
|
|
1113
1737
|
return null;
|
|
1114
1738
|
}
|
|
@@ -1137,7 +1761,7 @@ async function usageTool(deps) {
|
|
|
1137
1761
|
`) };
|
|
1138
1762
|
}
|
|
1139
1763
|
function bandLine(band) {
|
|
1140
|
-
if (!
|
|
1764
|
+
if (!isObject6(band))
|
|
1141
1765
|
return null;
|
|
1142
1766
|
const low = num3(band.lowUsd);
|
|
1143
1767
|
const high = num3(band.highUsd);
|
|
@@ -1157,10 +1781,15 @@ async function estimateTool(deps, args) {
|
|
|
1157
1781
|
const spec = str2(args.spec)?.trim();
|
|
1158
1782
|
if (!spec)
|
|
1159
1783
|
return { text: "Nothing to estimate. Pass spec: what the job should do.", isError: true };
|
|
1160
|
-
const
|
|
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
|
+
});
|
|
1161
1790
|
if (!r)
|
|
1162
1791
|
return UNREACHABLE;
|
|
1163
|
-
const band =
|
|
1792
|
+
const band = isObject6(r.band) ? r.band : {};
|
|
1164
1793
|
const line = bandLine(band) ?? "band unreadable";
|
|
1165
1794
|
const method = str2(band.method) ?? "plan";
|
|
1166
1795
|
const cap = num3(band.capUsd);
|
|
@@ -1172,14 +1801,18 @@ Assumes: ${assumptions.join("; ")}.` : "";
|
|
|
1172
1801
|
const capText = cap !== undefined ? ` Cap ${usd(cap)}.` : "";
|
|
1173
1802
|
const models = byModelText(r.byModel);
|
|
1174
1803
|
const routing = routingText(r.routing);
|
|
1175
|
-
|
|
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
|
+
};
|
|
1176
1809
|
}
|
|
1177
1810
|
function byModelText(raw) {
|
|
1178
1811
|
if (!Array.isArray(raw))
|
|
1179
1812
|
return "";
|
|
1180
1813
|
const parts = [];
|
|
1181
1814
|
for (const row2 of raw) {
|
|
1182
|
-
if (!
|
|
1815
|
+
if (!isObject6(row2) || !isObject6(row2.standard))
|
|
1183
1816
|
continue;
|
|
1184
1817
|
const model = str2(row2.model);
|
|
1185
1818
|
const low = num3(row2.standard.lowUsd);
|
|
@@ -1194,7 +1827,7 @@ function byModelText(raw) {
|
|
|
1194
1827
|
By model: ${parts.join(" · ")} (overnight batch ½).` : "";
|
|
1195
1828
|
}
|
|
1196
1829
|
function routingText(raw) {
|
|
1197
|
-
if (!
|
|
1830
|
+
if (!isObject6(raw) || !Array.isArray(raw.segments) || !isObject6(raw.cost))
|
|
1198
1831
|
return "";
|
|
1199
1832
|
const short = (model) => {
|
|
1200
1833
|
const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?/.exec(model);
|
|
@@ -1202,7 +1835,7 @@ function routingText(raw) {
|
|
|
1202
1835
|
};
|
|
1203
1836
|
const parts = [];
|
|
1204
1837
|
for (const s of raw.segments) {
|
|
1205
|
-
if (!
|
|
1838
|
+
if (!isObject6(s))
|
|
1206
1839
|
continue;
|
|
1207
1840
|
const from = num3(s.from);
|
|
1208
1841
|
const to = num3(s.to);
|
|
@@ -1237,7 +1870,7 @@ async function overnightTool(deps, args) {
|
|
|
1237
1870
|
const at = num3(r.scheduledAt);
|
|
1238
1871
|
const when = at !== undefined && at > now ? `Runs in ${shortDuration(at - now)}` : "Runs at the next tick";
|
|
1239
1872
|
const band = bandLine(r.band);
|
|
1240
|
-
const cap =
|
|
1873
|
+
const cap = isObject6(r.band) ? num3(r.band.capUsd) : undefined;
|
|
1241
1874
|
const sized = band ? ` ${band}${cap !== undefined ? ` · cap ${usd(cap)}` : ""}.` : "";
|
|
1242
1875
|
return {
|
|
1243
1876
|
text: `Queued overnight. Batch tier · ½ price.${sized} ${when}. Check with the jobs tool; pull the result into a session with /sled resume ${jobId}.`
|
|
@@ -1249,10 +1882,14 @@ async function jobsTool(deps) {
|
|
|
1249
1882
|
const r = await proxyGet(deps, "/proxy/jobs");
|
|
1250
1883
|
if (!r)
|
|
1251
1884
|
return UNREACHABLE;
|
|
1252
|
-
const rows = Array.isArray(r.jobs) ? r.jobs.filter(
|
|
1253
|
-
|
|
1254
|
-
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) : [];
|
|
1255
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
|
+
`) };
|
|
1256
1893
|
const lines = rows.map((j) => {
|
|
1257
1894
|
const when = (ts) => num3(ts) !== undefined ? shortDuration(Math.abs(now - num3(ts))) : "";
|
|
1258
1895
|
const status = str2(j.status) ?? "unknown";
|
|
@@ -1267,9 +1904,110 @@ async function jobsTool(deps) {
|
|
|
1267
1904
|
].join(" ").trimEnd();
|
|
1268
1905
|
});
|
|
1269
1906
|
lines.push("Resume a delivered job in a session with /sled resume <id>.");
|
|
1907
|
+
lines.push(...handoffLines(handoffs, now));
|
|
1270
1908
|
return { text: lines.join(`
|
|
1271
1909
|
`) };
|
|
1272
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
|
+
}
|
|
1273
2011
|
async function callTool(name, args, deps) {
|
|
1274
2012
|
switch (name) {
|
|
1275
2013
|
case "usage":
|
|
@@ -1280,6 +2018,12 @@ async function callTool(name, args, deps) {
|
|
|
1280
2018
|
return overnightTool(deps, args);
|
|
1281
2019
|
case "jobs":
|
|
1282
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);
|
|
1283
2027
|
default:
|
|
1284
2028
|
return { text: `Unknown tool: ${name}`, isError: true };
|
|
1285
2029
|
}
|
|
@@ -1296,7 +2040,7 @@ async function handleLine(line, deps) {
|
|
|
1296
2040
|
} catch {
|
|
1297
2041
|
return errorResponse(null, -32700, "parse error");
|
|
1298
2042
|
}
|
|
1299
|
-
if (!
|
|
2043
|
+
if (!isObject6(msg))
|
|
1300
2044
|
return errorResponse(null, -32600, "invalid request");
|
|
1301
2045
|
const id = typeof msg.id === "string" || typeof msg.id === "number" ? msg.id : null;
|
|
1302
2046
|
const method = str2(msg.method);
|
|
@@ -1305,7 +2049,7 @@ async function handleLine(line, deps) {
|
|
|
1305
2049
|
return isNotification ? null : errorResponse(id, -32600, "invalid request");
|
|
1306
2050
|
if (method.startsWith("notifications/"))
|
|
1307
2051
|
return null;
|
|
1308
|
-
const params =
|
|
2052
|
+
const params = isObject6(msg.params) ? msg.params : {};
|
|
1309
2053
|
switch (method) {
|
|
1310
2054
|
case "initialize": {
|
|
1311
2055
|
const requested = str2(params.protocolVersion);
|
|
@@ -1321,7 +2065,7 @@ async function handleLine(line, deps) {
|
|
|
1321
2065
|
return response(id, { tools: TOOLS });
|
|
1322
2066
|
case "tools/call": {
|
|
1323
2067
|
const name = str2(params.name) ?? "";
|
|
1324
|
-
const args =
|
|
2068
|
+
const args = isObject6(params.arguments) ? params.arguments : {};
|
|
1325
2069
|
const r = await callTool(name, args, deps);
|
|
1326
2070
|
return response(id, {
|
|
1327
2071
|
content: [{ type: "text", text: r.text }],
|
|
@@ -1381,10 +2125,10 @@ var meter = async (o) => {
|
|
|
1381
2125
|
};
|
|
1382
2126
|
|
|
1383
2127
|
// src/schedule.ts
|
|
1384
|
-
import { existsSync as
|
|
1385
|
-
import { join as
|
|
2128
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, rmSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2129
|
+
import { join as join8 } from "node:path";
|
|
1386
2130
|
var REVERT_LABEL = "ai.sleds.revert";
|
|
1387
|
-
var plistPath = (home) =>
|
|
2131
|
+
var plistPath = (home) => join8(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
|
|
1388
2132
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1389
2133
|
var buildLaunchdPlist = (sledsArgv, deadline) => {
|
|
1390
2134
|
const args = [...sledsArgv, "off"].map((a) => ` <string>${xmlEscape(a)}</string>`).join(`
|
|
@@ -1423,9 +2167,9 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
1423
2167
|
if (d.platform === "darwin") {
|
|
1424
2168
|
const p = plistPath(d.home);
|
|
1425
2169
|
try {
|
|
1426
|
-
|
|
2170
|
+
mkdirSync7(join8(d.home, "Library", "LaunchAgents"), { recursive: true });
|
|
1427
2171
|
d.exec("launchctl", ["unload", p]);
|
|
1428
|
-
|
|
2172
|
+
writeFileSync7(p, buildLaunchdPlist(d.sledsArgv, deadline));
|
|
1429
2173
|
const r = d.exec("launchctl", ["load", p]);
|
|
1430
2174
|
if (r.ok)
|
|
1431
2175
|
return { kind: "launchd", detail: `launchd agent ${REVERT_LABEL}` };
|
|
@@ -1452,7 +2196,7 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
1452
2196
|
var cancelRevert = (d, kind) => {
|
|
1453
2197
|
if (kind === "launchd" || d.platform === "darwin") {
|
|
1454
2198
|
const p = plistPath(d.home);
|
|
1455
|
-
if (
|
|
2199
|
+
if (existsSync7(p)) {
|
|
1456
2200
|
d.exec("launchctl", ["unload", p]);
|
|
1457
2201
|
rmSync(p, { force: true });
|
|
1458
2202
|
}
|
|
@@ -1462,81 +2206,6 @@ var cancelRevert = (d, kind) => {
|
|
|
1462
2206
|
}
|
|
1463
2207
|
};
|
|
1464
2208
|
|
|
1465
|
-
// src/switch.ts
|
|
1466
|
-
var isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1467
|
-
var envOf2 = (settings) => isObject6(settings.env) ? settings.env : {};
|
|
1468
|
-
var capturePriorEnv = (settings) => {
|
|
1469
|
-
const env = envOf2(settings);
|
|
1470
|
-
const prior = { hadEnv: isObject6(settings.env) };
|
|
1471
|
-
const url = env[BASE_URL_KEY];
|
|
1472
|
-
const token = env[AUTH_TOKEN_KEY];
|
|
1473
|
-
if (typeof url === "string")
|
|
1474
|
-
prior[BASE_URL_KEY] = url;
|
|
1475
|
-
if (typeof token === "string")
|
|
1476
|
-
prior[AUTH_TOKEN_KEY] = token;
|
|
1477
|
-
return prior;
|
|
1478
|
-
};
|
|
1479
|
-
var switchOn = (settings, cfg) => applyProxy(settings, cfg);
|
|
1480
|
-
var switchOff = (settings, prior) => {
|
|
1481
|
-
const hadEnv = prior.hadEnv === true;
|
|
1482
|
-
const stripped = removeProxy(settings);
|
|
1483
|
-
const env = { ...envOf2(stripped) };
|
|
1484
|
-
const url = prior[BASE_URL_KEY];
|
|
1485
|
-
const token = prior[AUTH_TOKEN_KEY];
|
|
1486
|
-
if (url !== undefined)
|
|
1487
|
-
env[BASE_URL_KEY] = url;
|
|
1488
|
-
if (token !== undefined)
|
|
1489
|
-
env[AUTH_TOKEN_KEY] = token;
|
|
1490
|
-
if (Object.keys(env).length === 0 && !hadEnv) {
|
|
1491
|
-
const { env: _env, ...rest } = stripped;
|
|
1492
|
-
return rest;
|
|
1493
|
-
}
|
|
1494
|
-
return { ...stripped, env };
|
|
1495
|
-
};
|
|
1496
|
-
var isPointedAtProxy = (settings) => {
|
|
1497
|
-
const env = envOf2(settings);
|
|
1498
|
-
return BASE_URL_KEY in env || AUTH_TOKEN_KEY in env;
|
|
1499
|
-
};
|
|
1500
|
-
var RELATIVE = /^\+(?:(\d+)h)?(?:(\d+)m)?$/;
|
|
1501
|
-
var parseUntilReset = (input, nowMs) => {
|
|
1502
|
-
const trimmed = input.trim();
|
|
1503
|
-
const rel = RELATIVE.exec(trimmed);
|
|
1504
|
-
let deadline;
|
|
1505
|
-
if (rel) {
|
|
1506
|
-
const hours = Number(rel[1] ?? 0);
|
|
1507
|
-
const minutes = Number(rel[2] ?? 0);
|
|
1508
|
-
if (hours === 0 && minutes === 0) {
|
|
1509
|
-
throw new Error(`--until-reset: "${input}" is zero; use something like +5h or +90m`);
|
|
1510
|
-
}
|
|
1511
|
-
deadline = nowMs + (hours * 60 + minutes) * 60000;
|
|
1512
|
-
} else {
|
|
1513
|
-
deadline = Date.parse(trimmed);
|
|
1514
|
-
if (Number.isNaN(deadline)) {
|
|
1515
|
-
throw new Error(`--until-reset: "${input}" is not +<n>h[<n>m] or an ISO timestamp`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
if (deadline <= nowMs) {
|
|
1519
|
-
throw new Error(`--until-reset: ${trimmed} is in the past`);
|
|
1520
|
-
}
|
|
1521
|
-
return deadline;
|
|
1522
|
-
};
|
|
1523
|
-
var formatLocal = (ms, tz) => new Intl.DateTimeFormat("en-US", {
|
|
1524
|
-
month: "short",
|
|
1525
|
-
day: "numeric",
|
|
1526
|
-
hour: "numeric",
|
|
1527
|
-
minute: "2-digit",
|
|
1528
|
-
timeZoneName: "short",
|
|
1529
|
-
...tz ? { timeZone: tz } : {}
|
|
1530
|
-
}).format(new Date(ms));
|
|
1531
|
-
var formatDuration = (ms) => {
|
|
1532
|
-
const total = Math.max(0, Math.round(ms / 60000));
|
|
1533
|
-
const h = Math.floor(total / 60);
|
|
1534
|
-
const m = total % 60;
|
|
1535
|
-
if (h === 0)
|
|
1536
|
-
return `${m}m`;
|
|
1537
|
-
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
1538
|
-
};
|
|
1539
|
-
|
|
1540
2209
|
// src/commands/off.ts
|
|
1541
2210
|
var off = (o) => {
|
|
1542
2211
|
const log = o.log ?? console.log;
|
|
@@ -1693,15 +2362,15 @@ var seatEvent = async (o) => {
|
|
|
1693
2362
|
};
|
|
1694
2363
|
|
|
1695
2364
|
// src/commands/seatSync.ts
|
|
1696
|
-
import { execFileSync } from "node:child_process";
|
|
1697
|
-
import { existsSync as
|
|
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";
|
|
1698
2367
|
import { platform } from "node:os";
|
|
1699
|
-
import { join as
|
|
2368
|
+
import { join as join10 } from "node:path";
|
|
1700
2369
|
|
|
1701
2370
|
// src/cookieDb.ts
|
|
1702
2371
|
import { copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
1703
2372
|
import { tmpdir } from "node:os";
|
|
1704
|
-
import { join as
|
|
2373
|
+
import { join as join9 } from "node:path";
|
|
1705
2374
|
var QUERY = "SELECT name, hex(encrypted_value) AS ev FROM cookies WHERE host_key LIKE '%claude.ai%'";
|
|
1706
2375
|
var isBun = () => typeof globalThis.Bun !== "undefined";
|
|
1707
2376
|
async function queryRows(dbPath) {
|
|
@@ -1730,7 +2399,7 @@ function nodeSqlite() {
|
|
|
1730
2399
|
return mod;
|
|
1731
2400
|
}
|
|
1732
2401
|
async function readCookieRows(dbPath) {
|
|
1733
|
-
const copy =
|
|
2402
|
+
const copy = join9(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
|
|
1734
2403
|
copyFileSync(dbPath, copy);
|
|
1735
2404
|
try {
|
|
1736
2405
|
const rows = await queryRows(copy);
|
|
@@ -2003,16 +2672,16 @@ function findFile(root, name) {
|
|
|
2003
2672
|
const stack = [root];
|
|
2004
2673
|
while (stack.length > 0) {
|
|
2005
2674
|
const dir = stack.pop();
|
|
2006
|
-
if (!dir || !
|
|
2675
|
+
if (!dir || !existsSync8(dir))
|
|
2007
2676
|
continue;
|
|
2008
2677
|
let entries;
|
|
2009
2678
|
try {
|
|
2010
|
-
entries =
|
|
2679
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
2011
2680
|
} catch {
|
|
2012
2681
|
continue;
|
|
2013
2682
|
}
|
|
2014
2683
|
for (const e of entries) {
|
|
2015
|
-
const p =
|
|
2684
|
+
const p = join10(dir, e.name);
|
|
2016
2685
|
if (e.isDirectory())
|
|
2017
2686
|
stack.push(p);
|
|
2018
2687
|
else if (e.name === name)
|
|
@@ -2023,8 +2692,8 @@ function findFile(root, name) {
|
|
|
2023
2692
|
}
|
|
2024
2693
|
function findCookieDb(home, source) {
|
|
2025
2694
|
const b = BROWSERS[source.browser];
|
|
2026
|
-
const root =
|
|
2027
|
-
if (!
|
|
2695
|
+
const root = join10(home, APP_SUPPORT, b.appDir);
|
|
2696
|
+
if (!existsSync8(root))
|
|
2028
2697
|
throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
|
|
2029
2698
|
if (!b.profiled) {
|
|
2030
2699
|
const db2 = findFile(root, "Cookies");
|
|
@@ -2032,15 +2701,15 @@ function findCookieDb(home, source) {
|
|
|
2032
2701
|
throw new Error(`no cookie store under ${root}`);
|
|
2033
2702
|
return db2;
|
|
2034
2703
|
}
|
|
2035
|
-
const localStatePath =
|
|
2036
|
-
const localState =
|
|
2704
|
+
const localStatePath = join10(root, "Local State");
|
|
2705
|
+
const localState = existsSync8(localStatePath) ? readFileSync6(localStatePath, "utf8") : null;
|
|
2037
2706
|
const wanted = source.profile ?? "Default";
|
|
2038
2707
|
const { dir, ambiguous } = resolveProfileDir(localState, wanted);
|
|
2039
2708
|
if (ambiguous)
|
|
2040
2709
|
throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
|
|
2041
2710
|
if (!dir)
|
|
2042
2711
|
throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
|
|
2043
|
-
const db = cookieDbCandidates(
|
|
2712
|
+
const db = cookieDbCandidates(join10(root, dir)).find((p) => existsSync8(p));
|
|
2044
2713
|
if (!db)
|
|
2045
2714
|
throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
|
|
2046
2715
|
return db;
|
|
@@ -2050,7 +2719,7 @@ async function readCookieRowsBun(home, source) {
|
|
|
2050
2719
|
}
|
|
2051
2720
|
function keychainKey(service) {
|
|
2052
2721
|
try {
|
|
2053
|
-
return
|
|
2722
|
+
return execFileSync4("security", ["find-generic-password", "-w", "-s", service], {
|
|
2054
2723
|
encoding: "utf8"
|
|
2055
2724
|
}).trim();
|
|
2056
2725
|
} catch {
|
|
@@ -2062,7 +2731,7 @@ function decryptJar(rows, safeStoragePw) {
|
|
|
2062
2731
|
}
|
|
2063
2732
|
var curlUsage = (org, cookie) => {
|
|
2064
2733
|
try {
|
|
2065
|
-
const out =
|
|
2734
|
+
const out = execFileSync4("curl", usageCurlArgs(org, cookie), {
|
|
2066
2735
|
encoding: "utf8",
|
|
2067
2736
|
maxBuffer: 8000000
|
|
2068
2737
|
});
|
|
@@ -2083,7 +2752,7 @@ function fetchSeatUsage(jar, org, now, run) {
|
|
|
2083
2752
|
}
|
|
2084
2753
|
function launchAgentPlist(bun, script, home, source) {
|
|
2085
2754
|
const label = launchLabel(source);
|
|
2086
|
-
const log =
|
|
2755
|
+
const log = join10(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
|
|
2087
2756
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2088
2757
|
const args = [bun, script, "seat-sync", ...sourceArgs(source)].map((a) => ` <string>${esc(a)}</string>`).join(`
|
|
2089
2758
|
`);
|
|
@@ -2105,32 +2774,32 @@ ${args}
|
|
|
2105
2774
|
`;
|
|
2106
2775
|
}
|
|
2107
2776
|
function plistPath2(home, source) {
|
|
2108
|
-
return
|
|
2777
|
+
return join10(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
|
|
2109
2778
|
}
|
|
2110
|
-
function
|
|
2779
|
+
function install2(home, argv, source, log) {
|
|
2111
2780
|
const bun = argv[0] ?? process.execPath;
|
|
2112
2781
|
const script = argv[1] ?? "";
|
|
2113
2782
|
if (!script) {
|
|
2114
2783
|
log("cannot resolve the sleds script path to install the timer");
|
|
2115
2784
|
return 1;
|
|
2116
2785
|
}
|
|
2117
|
-
const dir =
|
|
2118
|
-
|
|
2786
|
+
const dir = join10(home, "Library", "LaunchAgents");
|
|
2787
|
+
mkdirSync8(dir, { recursive: true });
|
|
2119
2788
|
const path = plistPath2(home, source);
|
|
2120
|
-
|
|
2789
|
+
writeFileSync8(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
|
|
2121
2790
|
try {
|
|
2122
|
-
|
|
2791
|
+
execFileSync4("launchctl", ["unload", path], { stdio: "ignore" });
|
|
2123
2792
|
} catch {}
|
|
2124
|
-
|
|
2793
|
+
execFileSync4("launchctl", ["load", "-w", path]);
|
|
2125
2794
|
log(`seat-sync timer installed (every ${SYNC_INTERVAL_S / 60} min) → ${path}`);
|
|
2126
2795
|
return 0;
|
|
2127
2796
|
}
|
|
2128
|
-
function
|
|
2797
|
+
function uninstall2(home, source, log) {
|
|
2129
2798
|
const path = plistPath2(home, source);
|
|
2130
2799
|
try {
|
|
2131
|
-
|
|
2800
|
+
execFileSync4("launchctl", ["unload", "-w", path], { stdio: "ignore" });
|
|
2132
2801
|
} catch {}
|
|
2133
|
-
log(
|
|
2802
|
+
log(existsSync8(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
|
|
2134
2803
|
return 0;
|
|
2135
2804
|
}
|
|
2136
2805
|
var seatSync = async (o) => {
|
|
@@ -2143,9 +2812,9 @@ var seatSync = async (o) => {
|
|
|
2143
2812
|
}
|
|
2144
2813
|
const { source } = parsed;
|
|
2145
2814
|
if (source.uninstall)
|
|
2146
|
-
return
|
|
2815
|
+
return uninstall2(o.home, source, log);
|
|
2147
2816
|
if (source.install)
|
|
2148
|
-
return
|
|
2817
|
+
return install2(o.home, [process.execPath, process.argv[1] ?? ""], source, log);
|
|
2149
2818
|
if (platform() !== "darwin" && !o.readCookieRows) {
|
|
2150
2819
|
log("seat-sync is macOS-only for now.");
|
|
2151
2820
|
return 2;
|
|
@@ -2191,6 +2860,9 @@ var seatSync = async (o) => {
|
|
|
2191
2860
|
log(`seat-sync: usage read failed — ${why}`);
|
|
2192
2861
|
return 2;
|
|
2193
2862
|
}
|
|
2863
|
+
try {
|
|
2864
|
+
writeSeatState(o.home, { ...readSeatState(o.home), window: usageAsWindow(usage) });
|
|
2865
|
+
} catch {}
|
|
2194
2866
|
const posted = await postSeat(proxyUrl, token, seatSyncBody(usage), o.fetchFn ?? fetch);
|
|
2195
2867
|
const wk = usage.sevenDay ? `wk ${Math.round(usage.sevenDay.usedPct)}%` : "wk —";
|
|
2196
2868
|
const models = Object.keys(usage.models).length;
|
|
@@ -2198,6 +2870,15 @@ var seatSync = async (o) => {
|
|
|
2198
2870
|
log(posted ? `seat-sync: posted ${wk} · ${models} model bucket${models === 1 ? "" : "s"}${credits}` : `seat-sync: read ${wk} but the proxy did not accept it`);
|
|
2199
2871
|
return posted ? 0 : 2;
|
|
2200
2872
|
};
|
|
2873
|
+
function usageAsWindow(u) {
|
|
2874
|
+
return {
|
|
2875
|
+
capturedAt: u.capturedAt,
|
|
2876
|
+
...u.fiveHour ? { fiveHour: u.fiveHour } : {},
|
|
2877
|
+
...u.sevenDay ? { sevenDay: u.sevenDay } : {},
|
|
2878
|
+
...Object.keys(u.models).length > 0 ? { models: u.models } : {},
|
|
2879
|
+
...u.usedCreditsUsd !== undefined ? { usedCreditsUsd: u.usedCreditsUsd } : {}
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2201
2882
|
async function postSeat(proxyUrl, token, body, fetchFn) {
|
|
2202
2883
|
try {
|
|
2203
2884
|
const res = await fetchFn(`${proxyUrl}/proxy/seat`, {
|
|
@@ -2213,7 +2894,7 @@ async function postSeat(proxyUrl, token, body, fetchFn) {
|
|
|
2213
2894
|
}
|
|
2214
2895
|
|
|
2215
2896
|
// src/commands/specEvent.ts
|
|
2216
|
-
import { readFileSync as
|
|
2897
|
+
import { readFileSync as readFileSync7, statSync } from "node:fs";
|
|
2217
2898
|
import { basename, isAbsolute, relative } from "node:path";
|
|
2218
2899
|
var HOOK_STDIN_TIMEOUT_MS2 = 1000;
|
|
2219
2900
|
var ESTIMATE_TIMEOUT_MS = 4000;
|
|
@@ -2269,7 +2950,7 @@ function readFileOrNull(path) {
|
|
|
2269
2950
|
try {
|
|
2270
2951
|
if (statSync(path).size > 2000000)
|
|
2271
2952
|
return null;
|
|
2272
|
-
return
|
|
2953
|
+
return readFileSync7(path, "utf8");
|
|
2273
2954
|
} catch {
|
|
2274
2955
|
return null;
|
|
2275
2956
|
}
|
|
@@ -2312,8 +2993,8 @@ var specEvent = async (o) => {
|
|
|
2312
2993
|
};
|
|
2313
2994
|
|
|
2314
2995
|
// src/commands/status.ts
|
|
2315
|
-
import { existsSync as
|
|
2316
|
-
var
|
|
2996
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
2997
|
+
var checkHealth2 = async (url, fetchFn = fetch, timeoutMs = 1000) => {
|
|
2317
2998
|
try {
|
|
2318
2999
|
const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2319
3000
|
return res.ok ? "ok" : "unreachable";
|
|
@@ -2380,7 +3061,7 @@ var status = async (o) => {
|
|
|
2380
3061
|
const log = o.log ?? console.log;
|
|
2381
3062
|
const nowMs = (o.now ?? Date.now)();
|
|
2382
3063
|
const cfg = readConfig(o.home);
|
|
2383
|
-
const settings =
|
|
3064
|
+
const settings = existsSync9(settingsPath(o.home)) ? readSettings(o.home) : {};
|
|
2384
3065
|
const inSettings = readProxy(settings);
|
|
2385
3066
|
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
2386
3067
|
const token = inSettings?.token ?? cfg.token;
|
|
@@ -2414,7 +3095,7 @@ var status = async (o) => {
|
|
|
2414
3095
|
info.untilResetMs = until;
|
|
2415
3096
|
if (proxyUrl) {
|
|
2416
3097
|
const [health, proxy] = await Promise.all([
|
|
2417
|
-
|
|
3098
|
+
checkHealth2(proxyUrl, o.fetchFn, o.timeoutMs),
|
|
2418
3099
|
fetchProxyStatus(proxyUrl, o.fetchFn, o.timeoutMs)
|
|
2419
3100
|
]);
|
|
2420
3101
|
info.health = health;
|
|
@@ -2478,11 +3159,11 @@ var rememberSeat = (o, window, now) => {
|
|
|
2478
3159
|
};
|
|
2479
3160
|
|
|
2480
3161
|
// src/commands/uninstall.ts
|
|
2481
|
-
import { existsSync as
|
|
2482
|
-
var
|
|
3162
|
+
import { existsSync as existsSync10, rmSync as rmSync3 } from "node:fs";
|
|
3163
|
+
var uninstall3 = (o) => {
|
|
2483
3164
|
const log = o.log ?? console.log;
|
|
2484
3165
|
const removed = [];
|
|
2485
|
-
if (
|
|
3166
|
+
if (existsSync10(settingsPath(o.home))) {
|
|
2486
3167
|
let settings = readSettings(o.home);
|
|
2487
3168
|
let changed = false;
|
|
2488
3169
|
if (hasEitherKey(settings)) {
|
|
@@ -2514,13 +3195,13 @@ var uninstall2 = (o) => {
|
|
|
2514
3195
|
writeSettings(o.home, settings);
|
|
2515
3196
|
}
|
|
2516
3197
|
for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
|
|
2517
|
-
if (
|
|
3198
|
+
if (existsSync10(file)) {
|
|
2518
3199
|
rmSync3(file, { force: true });
|
|
2519
3200
|
removed.push(file);
|
|
2520
3201
|
}
|
|
2521
3202
|
}
|
|
2522
3203
|
const skill = skillDir(o.home);
|
|
2523
|
-
if (
|
|
3204
|
+
if (existsSync10(skill)) {
|
|
2524
3205
|
rmSync3(skill, { recursive: true, force: true });
|
|
2525
3206
|
removed.push(skill);
|
|
2526
3207
|
}
|
|
@@ -2557,6 +3238,7 @@ commands:
|
|
|
2557
3238
|
init Point Claude Code at a SLEDS proxy, install the /sled skill and the status line.
|
|
2558
3239
|
--token <sled_\u2026> seat token (else SLEDS_TOKEN, ~/.sleds-token, or prompt)
|
|
2559
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
|
|
2560
3242
|
--non-interactive never prompt; exit 2 if no token is available
|
|
2561
3243
|
--signup no org? create an individual seat: your email + your own
|
|
2562
3244
|
Anthropic API key (validated, stored encrypted, the only key it spends)
|
|
@@ -2567,8 +3249,11 @@ commands:
|
|
|
2567
3249
|
off Switch Claude Code back to your seat. Restores whatever \`on\` replaced.
|
|
2568
3250
|
uninstall Remove the proxy settings, skill and status line. Keeps ~/.sleds-token.
|
|
2569
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.
|
|
2570
3254
|
meter Show the pipeline: built today, queued with its band, delivered overnight.
|
|
2571
|
-
bar Print the menu-bar meter in SwiftBar format
|
|
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).
|
|
2572
3257
|
mcp Run the MCP server on stdio: tools usage, estimate, overnight, jobs.
|
|
2573
3258
|
Register with \`claude mcp add sleds -- sleds mcp\` (Cowork sees the same server).
|
|
2574
3259
|
seat-sync Read the seat's subscription usage (weekly/5h limits, per-model, credits) from a
|
|
@@ -2622,13 +3307,13 @@ var sledsArgv = () => {
|
|
|
2622
3307
|
const script = process.argv[1];
|
|
2623
3308
|
return script ? [process.execPath, script] : [process.execPath];
|
|
2624
3309
|
};
|
|
2625
|
-
var
|
|
2626
|
-
var statusLineCommand = (argv) => [...argv, "statusline"].map(
|
|
2627
|
-
var seatEventCommand = (argv) => [...argv, "seat-event"].map(
|
|
2628
|
-
var specEventCommand = (argv) => [...argv, "spec-event"].map(
|
|
3310
|
+
var shellQuote2 = (s) => /^[\w./-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
|
|
3311
|
+
var statusLineCommand = (argv) => [...argv, "statusline"].map(shellQuote2).join(" ");
|
|
3312
|
+
var seatEventCommand = (argv) => [...argv, "seat-event"].map(shellQuote2).join(" ");
|
|
3313
|
+
var specEventCommand = (argv) => [...argv, "spec-event"].map(shellQuote2).join(" ");
|
|
2629
3314
|
var main = async (argv) => {
|
|
2630
3315
|
const { cmd, flags } = parseArgs(argv);
|
|
2631
|
-
const home =
|
|
3316
|
+
const home = homedir3();
|
|
2632
3317
|
const scheduler = {
|
|
2633
3318
|
platform: platform2(),
|
|
2634
3319
|
home,
|
|
@@ -2643,6 +3328,7 @@ var main = async (argv) => {
|
|
|
2643
3328
|
url: stringFlag(flags, "url"),
|
|
2644
3329
|
nonInteractive: flags["non-interactive"] === true,
|
|
2645
3330
|
signup: flags.signup === true,
|
|
3331
|
+
noProbe: flags["no-probe"] === true,
|
|
2646
3332
|
prompt: promptStdin,
|
|
2647
3333
|
statusLineCommand: statusLineCommand(sledsArgv()),
|
|
2648
3334
|
seatEventCommand: seatEventCommand(sledsArgv()),
|
|
@@ -2670,13 +3356,15 @@ var main = async (argv) => {
|
|
|
2670
3356
|
case "off":
|
|
2671
3357
|
return off({ home, scheduler });
|
|
2672
3358
|
case "uninstall":
|
|
2673
|
-
return
|
|
3359
|
+
return uninstall3({ home });
|
|
3360
|
+
case "doctor":
|
|
3361
|
+
return doctor({ home });
|
|
2674
3362
|
case "status":
|
|
2675
3363
|
return status({ home });
|
|
2676
3364
|
case "meter":
|
|
2677
3365
|
return meter({ home });
|
|
2678
3366
|
case "bar":
|
|
2679
|
-
return bar({ home });
|
|
3367
|
+
return bar({ home, argv: process.argv.slice(3) });
|
|
2680
3368
|
case "mcp":
|
|
2681
3369
|
return mcp({ home });
|
|
2682
3370
|
case "seat-sync":
|