@sleds/cli 0.1.16 → 0.1.18
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/dist/sleds.mjs +1733 -875
- package/package.json +1 -1
package/dist/sleds.mjs
CHANGED
|
@@ -240,6 +240,8 @@ var configDir = (home, env = process.env) => {
|
|
|
240
240
|
cfg.priorStatusLine = raw.priorStatusLine;
|
|
241
241
|
if (raw.wiring === "seat-first" || raw.wiring === "proxy-always")
|
|
242
242
|
cfg.wiring = raw.wiring;
|
|
243
|
+
if (raw.agentKind === "claude-code" || raw.agentKind === "codex")
|
|
244
|
+
cfg.agentKind = raw.agentKind;
|
|
243
245
|
const org = parseOrgInfo(raw.org);
|
|
244
246
|
if (org)
|
|
245
247
|
cfg.org = org;
|
|
@@ -440,14 +442,14 @@ var init_format = __esm(() => {
|
|
|
440
442
|
});
|
|
441
443
|
|
|
442
444
|
// src/meter.ts
|
|
443
|
-
import { existsSync as
|
|
444
|
-
import { join as
|
|
445
|
-
var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, meterCachePath = (home) =>
|
|
445
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
446
|
+
import { join as join4 } from "node:path";
|
|
447
|
+
var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, meterCachePath = (home) => join4(configDir(home), "meter.json"), readMeterCache = (home, now) => {
|
|
446
448
|
const p = meterCachePath(home);
|
|
447
|
-
if (!
|
|
449
|
+
if (!existsSync4(p))
|
|
448
450
|
return null;
|
|
449
451
|
try {
|
|
450
|
-
const raw = JSON.parse(
|
|
452
|
+
const raw = JSON.parse(readFileSync4(p, "utf8"));
|
|
451
453
|
if (!isObject4(raw))
|
|
452
454
|
return null;
|
|
453
455
|
const fetchedAt = num2(raw.fetchedAt);
|
|
@@ -459,11 +461,11 @@ var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof
|
|
|
459
461
|
}
|
|
460
462
|
}, writeMeterCache = (home, meter, now) => {
|
|
461
463
|
const dir = configDir(home);
|
|
462
|
-
|
|
464
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
463
465
|
const target = meterCachePath(home);
|
|
464
466
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
465
467
|
const file = { fetchedAt: now, meter };
|
|
466
|
-
|
|
468
|
+
writeFileSync4(tmp, `${JSON.stringify(file)}
|
|
467
469
|
`, { mode: 384 });
|
|
468
470
|
renameSync3(tmp, target);
|
|
469
471
|
}, fetchMeter = async (o) => {
|
|
@@ -481,7 +483,7 @@ var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject4 = (v) => typeof
|
|
|
481
483
|
}
|
|
482
484
|
}, meterSource = (home) => {
|
|
483
485
|
const cfg = readConfig(home);
|
|
484
|
-
const settings =
|
|
486
|
+
const settings = existsSync4(settingsPath(home)) ? readSettings(home) : {};
|
|
485
487
|
const inSettings = readProxy(settings);
|
|
486
488
|
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
487
489
|
const token = inSettings?.token ?? cfg.token;
|
|
@@ -512,6 +514,8 @@ var init_meter = __esm(() => {
|
|
|
512
514
|
// src/seat.ts
|
|
513
515
|
var exports_seat = {};
|
|
514
516
|
__export(exports_seat, {
|
|
517
|
+
APPROACHING_PCT: () => APPROACHING_PCT2,
|
|
518
|
+
CREDITS_PATTERN: () => CREDITS_PATTERN2,
|
|
515
519
|
SEAT_POST_MIN_DELTA_PCT: () => SEAT_POST_MIN_DELTA_PCT2,
|
|
516
520
|
SEAT_POST_MIN_INTERVAL_MS: () => SEAT_POST_MIN_INTERVAL_MS2,
|
|
517
521
|
SEAT_POST_TIMEOUT_MS: () => SEAT_POST_TIMEOUT_MS2,
|
|
@@ -519,8 +523,10 @@ __export(exports_seat, {
|
|
|
519
523
|
SEAT_WALL_LINE_NO_KEY: () => SEAT_WALL_LINE_NO_KEY2,
|
|
520
524
|
STDIN_TIMEOUT_MS: () => STDIN_TIMEOUT_MS2,
|
|
521
525
|
bindingResetAt: () => bindingResetAt2,
|
|
526
|
+
creditsHitIn: () => creditsHitIn2,
|
|
522
527
|
formatSeatFragment: () => formatSeatFragment2,
|
|
523
528
|
markPosted: () => markPosted2,
|
|
529
|
+
markWarned: () => markWarned2,
|
|
524
530
|
parseResetsAt: () => parseResetsAt2,
|
|
525
531
|
parseSeatState: () => parseSeatState2,
|
|
526
532
|
parseSeatWindow: () => parseSeatWindow2,
|
|
@@ -528,18 +534,32 @@ __export(exports_seat, {
|
|
|
528
534
|
postSeatWindow: () => postSeatWindow2,
|
|
529
535
|
readSeatState: () => readSeatState2,
|
|
530
536
|
readStdin: () => readStdin2,
|
|
537
|
+
scanTranscriptForCredits: () => scanTranscriptForCredits2,
|
|
531
538
|
seatSessionFromStdin: () => seatSessionFromStdin2,
|
|
532
539
|
seatStatePath: () => seatStatePath2,
|
|
533
|
-
|
|
540
|
+
seatWallBanner: () => seatWallBanner2,
|
|
541
|
+
seatWallDue: () => seatWallDue,
|
|
542
|
+
seatWallEvents: () => seatWallEvents2,
|
|
534
543
|
seatWallLine: () => seatWallLine2,
|
|
535
544
|
seatWindowBody: () => seatWindowBody2,
|
|
536
545
|
seatWindowFromStdin: () => seatWindowFromStdin2,
|
|
537
546
|
shouldPostSeat: () => shouldPostSeat2,
|
|
547
|
+
transcriptPathFromStdin: () => transcriptPathFromStdin2,
|
|
538
548
|
writeSeatState: () => writeSeatState2
|
|
539
549
|
});
|
|
540
|
-
import {
|
|
541
|
-
|
|
542
|
-
|
|
550
|
+
import {
|
|
551
|
+
closeSync as closeSync2,
|
|
552
|
+
existsSync as existsSync16,
|
|
553
|
+
mkdirSync as mkdirSync13,
|
|
554
|
+
openSync as openSync2,
|
|
555
|
+
readFileSync as readFileSync13,
|
|
556
|
+
readSync as readSync2,
|
|
557
|
+
renameSync as renameSync6,
|
|
558
|
+
statSync as statSync3,
|
|
559
|
+
writeFileSync as writeFileSync13
|
|
560
|
+
} from "node:fs";
|
|
561
|
+
import { join as join14 } from "node:path";
|
|
562
|
+
var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_MS2 = 1500, STDIN_TIMEOUT_MS2 = 300, isObject11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num6 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, EPOCH_MS_THRESHOLD2 = 100000000000, parseResetsAt2 = (v) => {
|
|
543
563
|
const n = num6(v);
|
|
544
564
|
if (n !== undefined) {
|
|
545
565
|
if (n <= 0)
|
|
@@ -552,7 +572,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
552
572
|
}
|
|
553
573
|
return;
|
|
554
574
|
}, parseBar2 = (v) => {
|
|
555
|
-
if (!
|
|
575
|
+
if (!isObject11(v))
|
|
556
576
|
return;
|
|
557
577
|
const usedPct = num6(v.used_percentage) ?? num6(v.usedPct);
|
|
558
578
|
const resetsAt = parseResetsAt2(v.resets_at ?? v.resetsAt);
|
|
@@ -560,14 +580,14 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
560
580
|
return;
|
|
561
581
|
return { usedPct, resetsAt };
|
|
562
582
|
}, parseSeatWindow2 = (raw, capturedAt) => {
|
|
563
|
-
if (!
|
|
583
|
+
if (!isObject11(raw))
|
|
564
584
|
return null;
|
|
565
|
-
const limits =
|
|
585
|
+
const limits = isObject11(raw.rate_limits) ? raw.rate_limits : raw;
|
|
566
586
|
const fiveHour = parseBar2(limits.five_hour ?? limits.fiveHour);
|
|
567
587
|
const sevenDay = parseBar2(limits.seven_day ?? limits.sevenDay);
|
|
568
588
|
const spendLimit = parseBar2(limits.spend_limit ?? limits.spendLimit);
|
|
569
589
|
const models = {};
|
|
570
|
-
if (
|
|
590
|
+
if (isObject11(limits.models))
|
|
571
591
|
for (const [k, v] of Object.entries(limits.models)) {
|
|
572
592
|
const bar = parseBar2(v);
|
|
573
593
|
if (bar)
|
|
@@ -611,15 +631,15 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
611
631
|
if (parts.length === 0)
|
|
612
632
|
return null;
|
|
613
633
|
return `seat ${parts.join(SEP)}`;
|
|
614
|
-
}, seatStatePath2 = (home) =>
|
|
615
|
-
if (!
|
|
634
|
+
}, APPROACHING_PCT2 = 90, seatStatePath2 = (home) => join14(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
|
|
635
|
+
if (!isObject11(v))
|
|
616
636
|
return;
|
|
617
637
|
const capturedAt = num6(v.capturedAt);
|
|
618
638
|
if (capturedAt === undefined)
|
|
619
639
|
return;
|
|
620
640
|
return parseSeatWindow2(v, capturedAt) ?? undefined;
|
|
621
641
|
}, parseSeatState2 = (raw) => {
|
|
622
|
-
if (!
|
|
642
|
+
if (!isObject11(raw))
|
|
623
643
|
return {};
|
|
624
644
|
const out = {};
|
|
625
645
|
const window = parseWindowStored2(raw.window);
|
|
@@ -628,7 +648,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
628
648
|
const lastPostedAt = num6(raw.lastPostedAt);
|
|
629
649
|
if (lastPostedAt !== undefined)
|
|
630
650
|
out.lastPostedAt = lastPostedAt;
|
|
631
|
-
if (
|
|
651
|
+
if (isObject11(raw.lastPosted)) {
|
|
632
652
|
const lp = {};
|
|
633
653
|
const f = num6(raw.lastPosted.fiveHour);
|
|
634
654
|
const s = num6(raw.lastPosted.sevenDay);
|
|
@@ -644,12 +664,38 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
644
664
|
const lastWallResetsAt = num6(raw.lastWallResetsAt);
|
|
645
665
|
if (lastWallResetsAt !== undefined)
|
|
646
666
|
out.lastWallResetsAt = lastWallResetsAt;
|
|
667
|
+
if (isObject11(raw.warned)) {
|
|
668
|
+
const warned = {};
|
|
669
|
+
for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
|
|
670
|
+
const w = raw.warned[bar];
|
|
671
|
+
if (!isObject11(w))
|
|
672
|
+
continue;
|
|
673
|
+
const entry = {};
|
|
674
|
+
const ap = num6(w.approaching);
|
|
675
|
+
const hit = num6(w.hit);
|
|
676
|
+
if (ap !== undefined)
|
|
677
|
+
entry.approaching = ap;
|
|
678
|
+
if (hit !== undefined)
|
|
679
|
+
entry.hit = hit;
|
|
680
|
+
warned[bar] = entry;
|
|
681
|
+
}
|
|
682
|
+
out.warned = warned;
|
|
683
|
+
}
|
|
684
|
+
if (isObject11(raw.transcriptScan) && typeof raw.transcriptScan.path === "string") {
|
|
685
|
+
const offset = num6(raw.transcriptScan.offset);
|
|
686
|
+
const hitAt = num6(raw.transcriptScan.hitAt);
|
|
687
|
+
out.transcriptScan = {
|
|
688
|
+
path: raw.transcriptScan.path,
|
|
689
|
+
offset: offset ?? 0,
|
|
690
|
+
...hitAt !== undefined ? { hitAt } : {}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
647
693
|
const session = parseSeatSession2(raw.session);
|
|
648
694
|
if (session)
|
|
649
695
|
out.session = session;
|
|
650
696
|
return out;
|
|
651
697
|
}, parseSeatSession2 = (v) => {
|
|
652
|
-
if (!
|
|
698
|
+
if (!isObject11(v) || typeof v.id !== "string" || !v.id)
|
|
653
699
|
return;
|
|
654
700
|
const at = num6(v.at);
|
|
655
701
|
if (at === undefined)
|
|
@@ -666,12 +712,12 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
666
712
|
return null;
|
|
667
713
|
try {
|
|
668
714
|
const raw = JSON.parse(trimmed);
|
|
669
|
-
if (!
|
|
715
|
+
if (!isObject11(raw) || typeof raw.session_id !== "string" || !raw.session_id)
|
|
670
716
|
return null;
|
|
671
717
|
const out = { id: raw.session_id, at: now };
|
|
672
718
|
if (typeof raw.cwd === "string" && raw.cwd)
|
|
673
719
|
out.cwd = raw.cwd;
|
|
674
|
-
const model =
|
|
720
|
+
const model = isObject11(raw.model) ? raw.model.id : raw.model;
|
|
675
721
|
if (typeof model === "string" && model)
|
|
676
722
|
out.model = model;
|
|
677
723
|
return out;
|
|
@@ -692,21 +738,21 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
692
738
|
return pick.length ? Math.min(...pick) : undefined;
|
|
693
739
|
}, readSeatState2 = (home) => {
|
|
694
740
|
const p = seatStatePath2(home);
|
|
695
|
-
if (!
|
|
741
|
+
if (!existsSync16(p))
|
|
696
742
|
return {};
|
|
697
743
|
try {
|
|
698
|
-
return parseSeatState2(JSON.parse(
|
|
744
|
+
return parseSeatState2(JSON.parse(readFileSync13(p, "utf8")));
|
|
699
745
|
} catch {
|
|
700
746
|
return {};
|
|
701
747
|
}
|
|
702
748
|
}, writeSeatState2 = (home, state) => {
|
|
703
749
|
const dir = configDir(home);
|
|
704
|
-
|
|
750
|
+
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
705
751
|
const target = seatStatePath2(home);
|
|
706
752
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
707
|
-
|
|
753
|
+
writeFileSync13(tmp, `${JSON.stringify(state)}
|
|
708
754
|
`, { mode: 384 });
|
|
709
|
-
|
|
755
|
+
renameSync6(tmp, target);
|
|
710
756
|
}, pctOf2 = (b) => b?.usedPct, shouldPostSeat2 = (state, next, now) => {
|
|
711
757
|
if (state.lastPostedAt === undefined || !state.lastPosted)
|
|
712
758
|
return true;
|
|
@@ -735,10 +781,92 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
735
781
|
if (w.spendLimit)
|
|
736
782
|
lastPosted.spendLimit = w.spendLimit.usedPct;
|
|
737
783
|
return { ...state, window: w, lastPostedAt: now, lastPosted };
|
|
738
|
-
},
|
|
784
|
+
}, seatWallDue = (state, w) => {
|
|
739
785
|
if (!w?.sevenDay || w.sevenDay.usedPct < 100)
|
|
740
786
|
return false;
|
|
741
787
|
return state.lastWallResetsAt === undefined || state.lastWallResetsAt < w.sevenDay.resetsAt;
|
|
788
|
+
}, seatWallEvents2 = (state, w) => {
|
|
789
|
+
if (!w)
|
|
790
|
+
return [];
|
|
791
|
+
const out = [];
|
|
792
|
+
const bars = [
|
|
793
|
+
["fiveHour", w.fiveHour],
|
|
794
|
+
["sevenDay", w.sevenDay],
|
|
795
|
+
["spendLimit", w.spendLimit]
|
|
796
|
+
];
|
|
797
|
+
for (const [bar, b] of bars) {
|
|
798
|
+
if (!b)
|
|
799
|
+
continue;
|
|
800
|
+
const seen = state.warned?.[bar] ?? {};
|
|
801
|
+
const weeklyOld = bar === "sevenDay" && state.lastWallResetsAt !== undefined && state.lastWallResetsAt >= b.resetsAt;
|
|
802
|
+
if (b.usedPct >= APPROACHING_PCT2 && (seen.approaching ?? -1) < b.resetsAt && !weeklyOld)
|
|
803
|
+
out.push({ bar, stage: "approaching", usedPct: b.usedPct, resetsAt: b.resetsAt });
|
|
804
|
+
if (b.usedPct >= 100 && (seen.hit ?? -1) < b.resetsAt && !weeklyOld)
|
|
805
|
+
out.push({ bar, stage: "hit", usedPct: b.usedPct, resetsAt: b.resetsAt });
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
}, markWarned2 = (state, events) => {
|
|
809
|
+
if (events.length === 0)
|
|
810
|
+
return state;
|
|
811
|
+
const warned = { ...state.warned ?? {} };
|
|
812
|
+
let lastWallResetsAt = state.lastWallResetsAt;
|
|
813
|
+
for (const e of events) {
|
|
814
|
+
warned[e.bar] = { ...warned[e.bar] ?? {}, [e.stage]: e.resetsAt };
|
|
815
|
+
if (e.bar === "sevenDay" && e.stage === "hit")
|
|
816
|
+
lastWallResetsAt = e.resetsAt;
|
|
817
|
+
}
|
|
818
|
+
return { ...state, warned, ...lastWallResetsAt !== undefined ? { lastWallResetsAt } : {} };
|
|
819
|
+
}, CREDITS_PATTERN2, creditsHitIn2 = (text) => CREDITS_PATTERN2.test(text), transcriptPathFromStdin2 = (text) => {
|
|
820
|
+
try {
|
|
821
|
+
const raw = JSON.parse(text.trim());
|
|
822
|
+
return isObject11(raw) && typeof raw.transcript_path === "string" && raw.transcript_path ? raw.transcript_path : null;
|
|
823
|
+
} catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}, scanTranscriptForCredits2 = (path, prior) => {
|
|
827
|
+
const from = prior && prior.path === path ? prior.offset : 0;
|
|
828
|
+
try {
|
|
829
|
+
const size = statSync3(path).size;
|
|
830
|
+
if (size <= from)
|
|
831
|
+
return {
|
|
832
|
+
hit: false,
|
|
833
|
+
next: { path, offset: size, ...prior?.hitAt ? { hitAt: prior.hitAt } : {} }
|
|
834
|
+
};
|
|
835
|
+
const start = Math.max(from, size - 256 * 1024);
|
|
836
|
+
const fd = openSync2(path, "r");
|
|
837
|
+
try {
|
|
838
|
+
const buf = Buffer.alloc(size - start);
|
|
839
|
+
readSync2(fd, buf, 0, buf.length, start);
|
|
840
|
+
const hit = creditsHitIn2(buf.toString("utf8"));
|
|
841
|
+
return {
|
|
842
|
+
hit,
|
|
843
|
+
next: {
|
|
844
|
+
path,
|
|
845
|
+
offset: size,
|
|
846
|
+
...hit ? { hitAt: Date.now() } : prior?.hitAt ? { hitAt: prior.hitAt } : {}
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
} finally {
|
|
850
|
+
closeSync2(fd);
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
return { hit: false, next: prior && prior.path === path ? prior : { path, offset: 0 } };
|
|
854
|
+
}
|
|
855
|
+
}, seatWallBanner2 = (e, hasUpstreamKey, now) => {
|
|
856
|
+
const label = {
|
|
857
|
+
fiveHour: "5-hour",
|
|
858
|
+
sevenDay: "weekly",
|
|
859
|
+
spendLimit: "spend",
|
|
860
|
+
credits: "usage credits"
|
|
861
|
+
}[e.bar];
|
|
862
|
+
const mins = Math.max(1, Math.round((e.resetsAt - now) / 60000));
|
|
863
|
+
const reset = e.bar === "credits" ? "" : mins < 60 ? ` · resets in ${mins} min` : ` · resets in ${Math.floor(mins / 60)} h ${mins % 60 ? `${mins % 60} min` : ""}`.trimEnd();
|
|
864
|
+
const exits = hasUpstreamKey ? "/sled handoff · sleds defer · sleds on" : "/sled handoff · sleds defer (no platform key here)";
|
|
865
|
+
if (e.stage === "approaching")
|
|
866
|
+
return `Seat ${label} at ${Math.round(e.usedPct)}%${reset}. Plan the exit now: ${exits}`;
|
|
867
|
+
if (e.bar === "credits")
|
|
868
|
+
return `Out of usage credits. ${exits}`;
|
|
869
|
+
return `Seat ${label} limit hit${reset}. ${exits}`;
|
|
742
870
|
}, seatWindowBody2 = (w) => ({
|
|
743
871
|
capturedAt: w.capturedAt,
|
|
744
872
|
...w.fiveHour ? { fiveHour: w.fiveHour } : {},
|
|
@@ -757,9 +885,9 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
757
885
|
} catch {
|
|
758
886
|
return false;
|
|
759
887
|
}
|
|
760
|
-
}, postSeatWindow2 = (o, w) => postJson2(o, "/proxy/seat", seatWindowBody2(w)), postSeatWall2 = (o, input) => postJson2(o, "/proxy/seat-wall", input), SEAT_WALL_LINE2 = "Seat limit hit. sleds on --until-reset to continue on the
|
|
888
|
+
}, postSeatWindow2 = (o, w) => postJson2(o, "/proxy/seat", seatWindowBody2(w)), postSeatWall2 = (o, input) => postJson2(o, "/proxy/seat-wall", input), SEAT_WALL_LINE2 = "Seat limit hit. sleds on --until-reset to continue on the platform key, or sleds defer to resume here at the reset.", SEAT_WALL_LINE_NO_KEY2 = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.", seatWallLine2 = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE2 : SEAT_WALL_LINE_NO_KEY2, readStdin2 = (timeoutMs, stdin = process.stdin) => new Promise((resolve6) => {
|
|
761
889
|
if (stdin.isTTY) {
|
|
762
|
-
|
|
890
|
+
resolve6("");
|
|
763
891
|
return;
|
|
764
892
|
}
|
|
765
893
|
let data = "";
|
|
@@ -769,7 +897,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
769
897
|
return;
|
|
770
898
|
settled = true;
|
|
771
899
|
clearTimeout(timer);
|
|
772
|
-
|
|
900
|
+
resolve6(data);
|
|
773
901
|
};
|
|
774
902
|
const timer = setTimeout(finish, timeoutMs);
|
|
775
903
|
stdin.setEncoding("utf8");
|
|
@@ -784,6 +912,7 @@ var init_seat = __esm(() => {
|
|
|
784
912
|
init_config();
|
|
785
913
|
init_meter();
|
|
786
914
|
SEAT_POST_MIN_INTERVAL_MS2 = 5 * 60000;
|
|
915
|
+
CREDITS_PATTERN2 = /out of usage credits|cc_cli_limit_message/i;
|
|
787
916
|
});
|
|
788
917
|
|
|
789
918
|
// src/index.ts
|
|
@@ -791,16 +920,124 @@ import { realpathSync } from "fs";
|
|
|
791
920
|
import { homedir, platform as platform2 } from "os";
|
|
792
921
|
import { fileURLToPath } from "url";
|
|
793
922
|
|
|
923
|
+
// src/codex.ts
|
|
924
|
+
init_config();
|
|
925
|
+
import { spawnSync } from "node:child_process";
|
|
926
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
927
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
928
|
+
var PROVIDER_ID = "sleds";
|
|
929
|
+
var TOKEN_ENV = "SLEDS_TOKEN";
|
|
930
|
+
var codexDir = (home) => join3(home, ".codex");
|
|
931
|
+
var codexConfigPath = (home) => join3(codexDir(home), "config.toml");
|
|
932
|
+
function providerBlock(proxyUrl) {
|
|
933
|
+
const base = `${proxyUrl.replace(/\/+$/, "")}/v1`;
|
|
934
|
+
return [
|
|
935
|
+
`[model_providers.${PROVIDER_ID}]`,
|
|
936
|
+
'name = "SLEDS"',
|
|
937
|
+
`base_url = "${base}"`,
|
|
938
|
+
`env_key = "${TOKEN_ENV}"`,
|
|
939
|
+
'wire_api = "responses"'
|
|
940
|
+
].join(`
|
|
941
|
+
`);
|
|
942
|
+
}
|
|
943
|
+
function applyCodexProvider(toml, proxyUrl) {
|
|
944
|
+
let out = removeCodexProvider(toml, { keepPrior: true });
|
|
945
|
+
const top = topLevel(out);
|
|
946
|
+
const m = top.match(/^model_provider[ \t]*=[ \t]*"([^"]*)"[ \t]*$/m);
|
|
947
|
+
if (m && m[1] !== PROVIDER_ID && !/^# sleds-prior-model-provider/m.test(out)) {
|
|
948
|
+
out = out.replace(m[0], `# sleds-prior-model-provider = "${m[1]}"
|
|
949
|
+
model_provider = "${PROVIDER_ID}"`);
|
|
950
|
+
} else if (m) {
|
|
951
|
+
out = out.replace(m[0], `model_provider = "${PROVIDER_ID}"`);
|
|
952
|
+
} else {
|
|
953
|
+
out = `model_provider = "${PROVIDER_ID}"
|
|
954
|
+
${out.startsWith(`
|
|
955
|
+
`) || out === "" ? "" : `
|
|
956
|
+
`}${out}`;
|
|
957
|
+
}
|
|
958
|
+
return `${out.replace(/\s+$/, "")}
|
|
959
|
+
|
|
960
|
+
${providerBlock(proxyUrl)}
|
|
961
|
+
`;
|
|
962
|
+
}
|
|
963
|
+
function removeCodexProvider(toml, o = {}) {
|
|
964
|
+
const lines = toml.split(`
|
|
965
|
+
`);
|
|
966
|
+
const out = [];
|
|
967
|
+
let skipping = false;
|
|
968
|
+
for (const line of lines) {
|
|
969
|
+
if (/^\s*\[/.test(line))
|
|
970
|
+
skipping = line.trim() === `[model_providers.${PROVIDER_ID}]`;
|
|
971
|
+
if (!skipping)
|
|
972
|
+
out.push(line);
|
|
973
|
+
}
|
|
974
|
+
let text = out.join(`
|
|
975
|
+
`);
|
|
976
|
+
if (!o.keepPrior) {
|
|
977
|
+
const prior = text.match(/^# sleds-prior-model-provider = "([^"]*)"\s*\n/m);
|
|
978
|
+
if (prior) {
|
|
979
|
+
text = text.replace(prior[0], "");
|
|
980
|
+
text = text.replace(/^model_provider\s*=\s*"sleds"\s*$/m, `model_provider = "${prior[1]}"`);
|
|
981
|
+
} else {
|
|
982
|
+
text = text.replace(/^model_provider\s*=\s*"sleds"\s*\n?/m, "");
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return text.replace(/\n{3,}/g, `
|
|
986
|
+
|
|
987
|
+
`);
|
|
988
|
+
}
|
|
989
|
+
function topLevel(toml) {
|
|
990
|
+
const i = toml.search(/^\s*\[/m);
|
|
991
|
+
return i === -1 ? toml : toml.slice(0, i);
|
|
992
|
+
}
|
|
993
|
+
function initCodex(o) {
|
|
994
|
+
const wrote = [];
|
|
995
|
+
const path = codexConfigPath(o.home);
|
|
996
|
+
const prior = existsSync3(path) ? readFileSync3(path, "utf8") : "";
|
|
997
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
998
|
+
writeFileSync3(path, applyCodexProvider(prior, o.proxyUrl), { mode: 384 });
|
|
999
|
+
wrote.push(`${prior ? "updated" : "created"} ${path}: provider "${PROVIDER_ID}" → ${o.proxyUrl}/v1 (key from $${TOKEN_ENV}), model_provider = "${PROVIDER_ID}"`);
|
|
1000
|
+
const cfg = readConfig(o.home);
|
|
1001
|
+
writeConfig(o.home, {
|
|
1002
|
+
...cfg,
|
|
1003
|
+
token: o.token,
|
|
1004
|
+
proxyUrl: o.proxyUrl,
|
|
1005
|
+
agentKind: "codex",
|
|
1006
|
+
...o.email ? { email: o.email } : {}
|
|
1007
|
+
});
|
|
1008
|
+
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
1009
|
+
return wrote;
|
|
1010
|
+
}
|
|
1011
|
+
function runCodex(home, args, deps = {}) {
|
|
1012
|
+
const cfg = readConfig(home);
|
|
1013
|
+
const error = deps.error ?? console.error;
|
|
1014
|
+
if (!cfg.token || !cfg.proxyUrl) {
|
|
1015
|
+
error("No seat here. Run `sleds init --agent codex --token <sled_…> --url https://proxy.sleds.ai` first.");
|
|
1016
|
+
return 2;
|
|
1017
|
+
}
|
|
1018
|
+
const env = {
|
|
1019
|
+
...deps.env ?? process.env,
|
|
1020
|
+
[TOKEN_ENV]: cfg.token,
|
|
1021
|
+
OPENAI_BASE_URL: `${cfg.proxyUrl.replace(/\/+$/, "")}/v1`
|
|
1022
|
+
};
|
|
1023
|
+
const r = (deps.spawnSync ?? spawnSync)("codex", args, { stdio: "inherit", env });
|
|
1024
|
+
if (r.error) {
|
|
1025
|
+
error(`could not start codex: ${r.error.message}. Is Codex CLI installed (npm i -g @openai/codex)?`);
|
|
1026
|
+
return 127;
|
|
1027
|
+
}
|
|
1028
|
+
return r.status ?? 1;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
794
1031
|
// src/commands/defer.ts
|
|
795
1032
|
init_config();
|
|
796
1033
|
init_meter();
|
|
797
|
-
import { existsSync as
|
|
1034
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
798
1035
|
import { hostname } from "node:os";
|
|
799
1036
|
|
|
800
1037
|
// src/musher.ts
|
|
801
|
-
import { spawnSync } from "node:child_process";
|
|
1038
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
802
1039
|
import { createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
803
|
-
import { basename, dirname as
|
|
1040
|
+
import { basename, dirname as dirname3, join as join5 } from "node:path";
|
|
804
1041
|
var MUSHER_LABEL = "ai.sleds.musher";
|
|
805
1042
|
var MUSHER_INTERVAL_S = 300;
|
|
806
1043
|
var DEFAULT_PERMISSION_MODE = "acceptEdits";
|
|
@@ -813,15 +1050,15 @@ var musherLabel = (env = process.env) => {
|
|
|
813
1050
|
const tag = basename(sh).replace(/^\.+/, "").replace(/[^A-Za-z0-9_-]+/g, "-") || "seat";
|
|
814
1051
|
return `${MUSHER_LABEL}.${tag}`;
|
|
815
1052
|
};
|
|
816
|
-
var musherPlistPath = (home, env = process.env) =>
|
|
817
|
-
var musherLogPath = (configDir2) =>
|
|
1053
|
+
var musherPlistPath = (home, env = process.env) => join5(home, "Library", "LaunchAgents", `${musherLabel(env)}.plist`);
|
|
1054
|
+
var musherLogPath = (configDir2) => join5(configDir2, "musher.log");
|
|
818
1055
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
819
1056
|
var musherPath = (home, execPath, currentPath = "") => {
|
|
820
1057
|
const parts = [
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1058
|
+
dirname3(execPath),
|
|
1059
|
+
join5(home, ".sleds", "bin"),
|
|
1060
|
+
join5(home, ".local", "bin"),
|
|
1061
|
+
join5(home, ".bun", "bin"),
|
|
825
1062
|
"/opt/homebrew/bin",
|
|
826
1063
|
"/usr/local/bin",
|
|
827
1064
|
"/usr/bin",
|
|
@@ -914,7 +1151,7 @@ var parseClaudeOutput = (stdout, exitCode) => {
|
|
|
914
1151
|
return { ok: false, summary: tail || `claude exited ${exitCode ?? "without a code"}` };
|
|
915
1152
|
};
|
|
916
1153
|
var defaultExecCapture = (cmd, args, o) => {
|
|
917
|
-
const r =
|
|
1154
|
+
const r = spawnSync2(cmd, [...args], {
|
|
918
1155
|
cwd: o.cwd,
|
|
919
1156
|
encoding: "utf8",
|
|
920
1157
|
timeout: o.timeoutMs,
|
|
@@ -1045,8 +1282,18 @@ var parsePayload = (payload) => {
|
|
|
1045
1282
|
// src/seat.ts
|
|
1046
1283
|
init_config();
|
|
1047
1284
|
init_meter();
|
|
1048
|
-
import {
|
|
1049
|
-
|
|
1285
|
+
import {
|
|
1286
|
+
closeSync,
|
|
1287
|
+
existsSync as existsSync5,
|
|
1288
|
+
mkdirSync as mkdirSync5,
|
|
1289
|
+
openSync,
|
|
1290
|
+
readFileSync as readFileSync5,
|
|
1291
|
+
readSync,
|
|
1292
|
+
renameSync as renameSync4,
|
|
1293
|
+
statSync,
|
|
1294
|
+
writeFileSync as writeFileSync5
|
|
1295
|
+
} from "node:fs";
|
|
1296
|
+
import { join as join6 } from "node:path";
|
|
1050
1297
|
var SEAT_POST_MIN_DELTA_PCT = 1;
|
|
1051
1298
|
var SEAT_POST_MIN_INTERVAL_MS = 5 * 60000;
|
|
1052
1299
|
var SEAT_POST_TIMEOUT_MS = 1500;
|
|
@@ -1131,7 +1378,8 @@ var formatSeatFragment = (w) => {
|
|
|
1131
1378
|
return null;
|
|
1132
1379
|
return `seat ${parts.join(SEP)}`;
|
|
1133
1380
|
};
|
|
1134
|
-
var
|
|
1381
|
+
var APPROACHING_PCT = 90;
|
|
1382
|
+
var seatStatePath = (home) => join6(configDir(home), "seat.json");
|
|
1135
1383
|
var parseWindowStored = (v) => {
|
|
1136
1384
|
if (!isObject5(v))
|
|
1137
1385
|
return;
|
|
@@ -1166,6 +1414,32 @@ var parseSeatState = (raw) => {
|
|
|
1166
1414
|
const lastWallResetsAt = num3(raw.lastWallResetsAt);
|
|
1167
1415
|
if (lastWallResetsAt !== undefined)
|
|
1168
1416
|
out.lastWallResetsAt = lastWallResetsAt;
|
|
1417
|
+
if (isObject5(raw.warned)) {
|
|
1418
|
+
const warned = {};
|
|
1419
|
+
for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
|
|
1420
|
+
const w = raw.warned[bar];
|
|
1421
|
+
if (!isObject5(w))
|
|
1422
|
+
continue;
|
|
1423
|
+
const entry = {};
|
|
1424
|
+
const ap = num3(w.approaching);
|
|
1425
|
+
const hit = num3(w.hit);
|
|
1426
|
+
if (ap !== undefined)
|
|
1427
|
+
entry.approaching = ap;
|
|
1428
|
+
if (hit !== undefined)
|
|
1429
|
+
entry.hit = hit;
|
|
1430
|
+
warned[bar] = entry;
|
|
1431
|
+
}
|
|
1432
|
+
out.warned = warned;
|
|
1433
|
+
}
|
|
1434
|
+
if (isObject5(raw.transcriptScan) && typeof raw.transcriptScan.path === "string") {
|
|
1435
|
+
const offset = num3(raw.transcriptScan.offset);
|
|
1436
|
+
const hitAt = num3(raw.transcriptScan.hitAt);
|
|
1437
|
+
out.transcriptScan = {
|
|
1438
|
+
path: raw.transcriptScan.path,
|
|
1439
|
+
offset: offset ?? 0,
|
|
1440
|
+
...hitAt !== undefined ? { hitAt } : {}
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1169
1443
|
const session = parseSeatSession(raw.session);
|
|
1170
1444
|
if (session)
|
|
1171
1445
|
out.session = session;
|
|
@@ -1218,20 +1492,20 @@ var bindingResetAt = (w, now) => {
|
|
|
1218
1492
|
};
|
|
1219
1493
|
var readSeatState = (home) => {
|
|
1220
1494
|
const p = seatStatePath(home);
|
|
1221
|
-
if (!
|
|
1495
|
+
if (!existsSync5(p))
|
|
1222
1496
|
return {};
|
|
1223
1497
|
try {
|
|
1224
|
-
return parseSeatState(JSON.parse(
|
|
1498
|
+
return parseSeatState(JSON.parse(readFileSync5(p, "utf8")));
|
|
1225
1499
|
} catch {
|
|
1226
1500
|
return {};
|
|
1227
1501
|
}
|
|
1228
1502
|
};
|
|
1229
1503
|
var writeSeatState = (home, state) => {
|
|
1230
1504
|
const dir = configDir(home);
|
|
1231
|
-
|
|
1505
|
+
mkdirSync5(dir, { recursive: true, mode: 448 });
|
|
1232
1506
|
const target = seatStatePath(home);
|
|
1233
1507
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
1234
|
-
|
|
1508
|
+
writeFileSync5(tmp, `${JSON.stringify(state)}
|
|
1235
1509
|
`, { mode: 384 });
|
|
1236
1510
|
renameSync4(tmp, target);
|
|
1237
1511
|
};
|
|
@@ -1266,10 +1540,94 @@ var markPosted = (state, w, now) => {
|
|
|
1266
1540
|
lastPosted.spendLimit = w.spendLimit.usedPct;
|
|
1267
1541
|
return { ...state, window: w, lastPostedAt: now, lastPosted };
|
|
1268
1542
|
};
|
|
1269
|
-
var
|
|
1270
|
-
if (!w
|
|
1271
|
-
return
|
|
1272
|
-
|
|
1543
|
+
var seatWallEvents = (state, w) => {
|
|
1544
|
+
if (!w)
|
|
1545
|
+
return [];
|
|
1546
|
+
const out = [];
|
|
1547
|
+
const bars = [
|
|
1548
|
+
["fiveHour", w.fiveHour],
|
|
1549
|
+
["sevenDay", w.sevenDay],
|
|
1550
|
+
["spendLimit", w.spendLimit]
|
|
1551
|
+
];
|
|
1552
|
+
for (const [bar, b] of bars) {
|
|
1553
|
+
if (!b)
|
|
1554
|
+
continue;
|
|
1555
|
+
const seen = state.warned?.[bar] ?? {};
|
|
1556
|
+
const weeklyOld = bar === "sevenDay" && state.lastWallResetsAt !== undefined && state.lastWallResetsAt >= b.resetsAt;
|
|
1557
|
+
if (b.usedPct >= APPROACHING_PCT && (seen.approaching ?? -1) < b.resetsAt && !weeklyOld)
|
|
1558
|
+
out.push({ bar, stage: "approaching", usedPct: b.usedPct, resetsAt: b.resetsAt });
|
|
1559
|
+
if (b.usedPct >= 100 && (seen.hit ?? -1) < b.resetsAt && !weeklyOld)
|
|
1560
|
+
out.push({ bar, stage: "hit", usedPct: b.usedPct, resetsAt: b.resetsAt });
|
|
1561
|
+
}
|
|
1562
|
+
return out;
|
|
1563
|
+
};
|
|
1564
|
+
var markWarned = (state, events) => {
|
|
1565
|
+
if (events.length === 0)
|
|
1566
|
+
return state;
|
|
1567
|
+
const warned = { ...state.warned ?? {} };
|
|
1568
|
+
let lastWallResetsAt = state.lastWallResetsAt;
|
|
1569
|
+
for (const e of events) {
|
|
1570
|
+
warned[e.bar] = { ...warned[e.bar] ?? {}, [e.stage]: e.resetsAt };
|
|
1571
|
+
if (e.bar === "sevenDay" && e.stage === "hit")
|
|
1572
|
+
lastWallResetsAt = e.resetsAt;
|
|
1573
|
+
}
|
|
1574
|
+
return { ...state, warned, ...lastWallResetsAt !== undefined ? { lastWallResetsAt } : {} };
|
|
1575
|
+
};
|
|
1576
|
+
var CREDITS_PATTERN = /out of usage credits|cc_cli_limit_message/i;
|
|
1577
|
+
var creditsHitIn = (text) => CREDITS_PATTERN.test(text);
|
|
1578
|
+
var transcriptPathFromStdin = (text) => {
|
|
1579
|
+
try {
|
|
1580
|
+
const raw = JSON.parse(text.trim());
|
|
1581
|
+
return isObject5(raw) && typeof raw.transcript_path === "string" && raw.transcript_path ? raw.transcript_path : null;
|
|
1582
|
+
} catch {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
var scanTranscriptForCredits = (path, prior) => {
|
|
1587
|
+
const from = prior && prior.path === path ? prior.offset : 0;
|
|
1588
|
+
try {
|
|
1589
|
+
const size = statSync(path).size;
|
|
1590
|
+
if (size <= from)
|
|
1591
|
+
return {
|
|
1592
|
+
hit: false,
|
|
1593
|
+
next: { path, offset: size, ...prior?.hitAt ? { hitAt: prior.hitAt } : {} }
|
|
1594
|
+
};
|
|
1595
|
+
const start = Math.max(from, size - 256 * 1024);
|
|
1596
|
+
const fd = openSync(path, "r");
|
|
1597
|
+
try {
|
|
1598
|
+
const buf = Buffer.alloc(size - start);
|
|
1599
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
1600
|
+
const hit = creditsHitIn(buf.toString("utf8"));
|
|
1601
|
+
return {
|
|
1602
|
+
hit,
|
|
1603
|
+
next: {
|
|
1604
|
+
path,
|
|
1605
|
+
offset: size,
|
|
1606
|
+
...hit ? { hitAt: Date.now() } : prior?.hitAt ? { hitAt: prior.hitAt } : {}
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
} finally {
|
|
1610
|
+
closeSync(fd);
|
|
1611
|
+
}
|
|
1612
|
+
} catch {
|
|
1613
|
+
return { hit: false, next: prior && prior.path === path ? prior : { path, offset: 0 } };
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
var seatWallBanner = (e, hasUpstreamKey, now) => {
|
|
1617
|
+
const label = {
|
|
1618
|
+
fiveHour: "5-hour",
|
|
1619
|
+
sevenDay: "weekly",
|
|
1620
|
+
spendLimit: "spend",
|
|
1621
|
+
credits: "usage credits"
|
|
1622
|
+
}[e.bar];
|
|
1623
|
+
const mins = Math.max(1, Math.round((e.resetsAt - now) / 60000));
|
|
1624
|
+
const reset = e.bar === "credits" ? "" : mins < 60 ? ` · resets in ${mins} min` : ` · resets in ${Math.floor(mins / 60)} h ${mins % 60 ? `${mins % 60} min` : ""}`.trimEnd();
|
|
1625
|
+
const exits = hasUpstreamKey ? "/sled handoff · sleds defer · sleds on" : "/sled handoff · sleds defer (no platform key here)";
|
|
1626
|
+
if (e.stage === "approaching")
|
|
1627
|
+
return `Seat ${label} at ${Math.round(e.usedPct)}%${reset}. Plan the exit now: ${exits}`;
|
|
1628
|
+
if (e.bar === "credits")
|
|
1629
|
+
return `Out of usage credits. ${exits}`;
|
|
1630
|
+
return `Seat ${label} limit hit${reset}. ${exits}`;
|
|
1273
1631
|
};
|
|
1274
1632
|
var seatWindowBody = (w) => ({
|
|
1275
1633
|
capturedAt: w.capturedAt,
|
|
@@ -1293,7 +1651,7 @@ var postJson = async (o, path, body) => {
|
|
|
1293
1651
|
};
|
|
1294
1652
|
var postSeatWindow = (o, w) => postJson(o, "/proxy/seat", seatWindowBody(w));
|
|
1295
1653
|
var postSeatWall = (o, input) => postJson(o, "/proxy/seat-wall", input);
|
|
1296
|
-
var SEAT_WALL_LINE = "Seat limit hit. sleds on --until-reset to continue on the
|
|
1654
|
+
var SEAT_WALL_LINE = "Seat limit hit. sleds on --until-reset to continue on the platform key, or sleds defer to resume here at the reset.";
|
|
1297
1655
|
var SEAT_WALL_LINE_NO_KEY = "Seat limit hit. sleds defer resumes this session on this Mac at the reset; /sled 5 to hand off — this org has no platform key to switch to.";
|
|
1298
1656
|
var seatWallLine = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE : SEAT_WALL_LINE_NO_KEY;
|
|
1299
1657
|
var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) => {
|
|
@@ -1471,7 +1829,7 @@ var defer = async (o) => {
|
|
|
1471
1829
|
log(`Deferred. This Mac resumes ${sessionId ? `session ${sessionId.slice(0, 8)}…` : "the work"}${model ? ` on ${model}` : ""} ${when}.`);
|
|
1472
1830
|
log(` job ${r.jobId}${cwd ? ` · ${cwd}` : ""} · runs on your seat, no key · sleds musher --status to watch`);
|
|
1473
1831
|
const cfg = readConfig(o.home);
|
|
1474
|
-
if (process.platform === "darwin" && !
|
|
1832
|
+
if (process.platform === "darwin" && !existsSync6(musherPlistPath(o.home)) && cfg.musher?.agent !== "launchd")
|
|
1475
1833
|
log(` The musher agent is not installed on this Mac: run \`sleds musher --install\` once, or \`sleds run ${r.jobId}\` by hand at the reset.`);
|
|
1476
1834
|
log(" Keep this Mac awake at that time (System Settings → Battery → prevent sleeping on power adapter).");
|
|
1477
1835
|
return 0;
|
|
@@ -1481,25 +1839,25 @@ var defer = async (o) => {
|
|
|
1481
1839
|
init_config();
|
|
1482
1840
|
init_meter();
|
|
1483
1841
|
import { execFileSync } from "node:child_process";
|
|
1484
|
-
import { existsSync as
|
|
1842
|
+
import { existsSync as existsSync8, lstatSync, readlinkSync } from "node:fs";
|
|
1485
1843
|
import { resolve as resolve3 } from "node:path";
|
|
1486
1844
|
init_settings();
|
|
1487
1845
|
|
|
1488
1846
|
// src/commands/musher.ts
|
|
1489
|
-
import { existsSync as
|
|
1847
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1490
1848
|
init_config();
|
|
1491
1849
|
init_meter();
|
|
1492
1850
|
var defaultWhich = (cmd) => {
|
|
1493
1851
|
const path = process.env.PATH ?? "";
|
|
1494
1852
|
for (const dir of path.split(":").filter(Boolean)) {
|
|
1495
1853
|
const p = `${dir}/${cmd}`;
|
|
1496
|
-
if (
|
|
1854
|
+
if (existsSync7(p))
|
|
1497
1855
|
return p;
|
|
1498
1856
|
}
|
|
1499
1857
|
return null;
|
|
1500
1858
|
};
|
|
1501
1859
|
var findClaude = (home, configured, which) => {
|
|
1502
|
-
if (configured &&
|
|
1860
|
+
if (configured && existsSync7(configured))
|
|
1503
1861
|
return configured;
|
|
1504
1862
|
const found = which("claude");
|
|
1505
1863
|
if (found)
|
|
@@ -1510,16 +1868,16 @@ var findClaude = (home, configured, which) => {
|
|
|
1510
1868
|
"/usr/local/bin/claude",
|
|
1511
1869
|
`${home}/.npm-global/bin/claude`
|
|
1512
1870
|
])
|
|
1513
|
-
if (
|
|
1871
|
+
if (existsSync7(p))
|
|
1514
1872
|
return p;
|
|
1515
1873
|
return null;
|
|
1516
1874
|
};
|
|
1517
1875
|
var musherAgentState = (home, env = process.env) => {
|
|
1518
1876
|
const plist = musherPlistPath(home, env);
|
|
1519
|
-
if (!
|
|
1877
|
+
if (!existsSync7(plist))
|
|
1520
1878
|
return { installed: false, thisSeat: false };
|
|
1521
1879
|
try {
|
|
1522
|
-
const text =
|
|
1880
|
+
const text = readFileSync6(plist, "utf8");
|
|
1523
1881
|
const want = env.SLEDS_HOME?.trim();
|
|
1524
1882
|
const has = /<key>SLEDS_HOME<\/key>\s*<string>([^<]*)<\/string>/.exec(text)?.[1];
|
|
1525
1883
|
return { installed: true, thisSeat: (want ?? undefined) === (has ?? undefined) };
|
|
@@ -1584,7 +1942,7 @@ async function runDoctor(o) {
|
|
|
1584
1942
|
].filter(Boolean);
|
|
1585
1943
|
const cfgDir = configDir(home, env);
|
|
1586
1944
|
const cfg = readConfig(home);
|
|
1587
|
-
const cfgExists =
|
|
1945
|
+
const cfgExists = existsSync8(configPath(home));
|
|
1588
1946
|
if (!cfgExists) {
|
|
1589
1947
|
checks.push({
|
|
1590
1948
|
level: "fail",
|
|
@@ -1600,7 +1958,7 @@ async function runDoctor(o) {
|
|
|
1600
1958
|
});
|
|
1601
1959
|
}
|
|
1602
1960
|
const sPath = settingsPath(home);
|
|
1603
|
-
const settings =
|
|
1961
|
+
const settings = existsSync8(sPath) ? readSettings(home) : null;
|
|
1604
1962
|
const pointed = settings ? readProxy(settings) : null;
|
|
1605
1963
|
if (!settings) {
|
|
1606
1964
|
checks.push({
|
|
@@ -1658,7 +2016,7 @@ async function runDoctor(o) {
|
|
|
1658
2016
|
fix: wrong.length === 0 ? undefined : `${overrides.length ? `${overrides.join(" ")} ` : ""}sleds hooks (re-writes the status line + hooks with this seat baked in)`
|
|
1659
2017
|
});
|
|
1660
2018
|
}
|
|
1661
|
-
const skill =
|
|
2019
|
+
const skill = existsSync8(skillPath(home));
|
|
1662
2020
|
checks.push({
|
|
1663
2021
|
level: skill ? "ok" : "warn",
|
|
1664
2022
|
name: "skill",
|
|
@@ -1761,511 +2119,79 @@ var doctor = async (o) => {
|
|
|
1761
2119
|
return checks.some((c) => c.level === "fail") ? 1 : 0;
|
|
1762
2120
|
};
|
|
1763
2121
|
|
|
1764
|
-
// src/
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
then restart \`claude\`. Settings go back to exactly what they were.
|
|
1839
|
-
|
|
1840
|
-
## Check
|
|
1841
|
-
|
|
1842
|
-
\`sleds status\` — on/off, since, reverts at, proxy health, wall mode, and the org's shape (\`Org:\` / \`Wiring:\`). An org with no platform key runs seat-first and its walls offer hold (\`/sled 2\`), \`sleds defer\` (this Mac resumes at the reset) and hand-off (\`/sled 5\`) — continue, overnight and switch need a key an admin adds on the dashboard.
|
|
1843
|
-
|
|
1844
|
-
The status line's \`seat wk 52% · 5h 12%\` is the user's own Pro/Max window as Claude Code reports it (weekly, then 5-hour); \`window\` is the org key's headroom through the proxy. At \`wk 100%\` the answer is \`sleds on --until-reset\` (org key) or \`sleds defer\` (this Mac resumes at the reset, no key).
|
|
1845
|
-
|
|
1846
|
-
## /sled — estimate the task, offer overnight
|
|
1847
|
-
|
|
1848
|
-
Bare \`/sled\` (or \`/sled <spec>\`) means: size the current task and offer to hand it to the night shift. Do this in order.
|
|
1849
|
-
|
|
1850
|
-
1. Read the task. The spec is the argument if given, otherwise the current conversation's open task (the todo list, the last user request).
|
|
1851
|
-
2. Estimate with \`/sled estimate <spec>\` — the proxy answers with the calibrated band and cap, and the estimate is recorded so its actuals feed the next one. Quote that band, never a point and never your own guess.
|
|
1852
|
-
3. Offer the window when any of these hold: the high end of the cost band is above the org's approval threshold, the wall-clock is over 30 minutes, or the result is not needed before end of day. The offer is one line: \`Run this overnight from 18:00? Estimated $40–120 at half price.\`
|
|
1853
|
-
4. On yes, write the hand-off the Musher will work from: the spec, the acceptance test, the org conventions that apply, and every decision already made in this session as \`question -> answer\` pairs. Ask one thing before queueing: the autonomy for this job.
|
|
1854
|
-
- \`ask\`: every question pages on-call.
|
|
1855
|
-
- \`decide_and_flag\` (default): reversible defaults are taken and listed in the morning.
|
|
1856
|
-
- \`decide\`: reversible defaults are taken quietly.
|
|
1857
|
-
Irreversible questions always page, whatever the autonomy. No answer in 30 minutes parks the job and the Musher moves to the next one.
|
|
1858
|
-
5. Queue it with \`/sled overnight <spec>\` (the hand-off from step 4 is the spec). The proxy runs the real estimator (its cap bounds the job's spend), schedules the job for the org's overnight window, and snapshots this session so the batch runner has the context. Confirm with the job id and the band from the reply — not your own guess.
|
|
1859
|
-
|
|
1860
|
-
## /sled review — the morning queue
|
|
1861
|
-
|
|
1862
|
-
Morning ritual. List what the night shift did, grouped:
|
|
1863
|
-
|
|
1864
|
-
- Delivered: jobs with a PR or a result, with actual cost against the estimate.
|
|
1865
|
-
- Flagged: decisions the Musher took on a \`decide_and_flag\` job, each as \`question -> answer\` with the rung that answered it (spec, convention, or default). These need a human eye, not a redo.
|
|
1866
|
-
- Parked: questions nobody answered in 30 minutes. Each shows the option set; answering resumes the job.
|
|
1867
|
-
|
|
1868
|
-
Read the Musher log for every delivered job before summarizing, and put each decision into the PR body with its reasoning. Then offer the next step for each: merge, kick back with a note, or \`/sled resume <id>\`. On Monday the review covers the weekend.
|
|
1869
|
-
|
|
1870
|
-
## /sled weekend — Friday sweep
|
|
1871
|
-
|
|
1872
|
-
Friday afternoon. Sweep the open task list into a weekend batch: everything the user has queued or labeled for the weekend becomes overnight jobs with the org's default autonomy, and the Monday review covers all of it. For now, explain that the sweep queues the current task list as overnight jobs; sweeping issues by GitHub label is a later ticket. Confirm the count and the total estimate band before queueing.
|
|
1873
|
-
|
|
1874
|
-
## /sled overnight [spec]
|
|
1875
|
-
|
|
1876
|
-
Queue a batch job without waiting for a wall. \`/sled overnight <spec>\` runs the spec; bare \`/sled overnight\` runs the session's original ask. The reply carries the band, the cap, when it runs, and \`/sled resume <id>\` for the morning. Half price on the batch tier.
|
|
1877
|
-
|
|
1878
|
-
## /sled jobs
|
|
1879
|
-
|
|
1880
|
-
List the org's jobs: queued, running, delivered, parked, with the estimate band, the actual so far, and the autonomy. Parked jobs show the unanswered question. Keep it to one line per job.
|
|
1881
|
-
|
|
1882
|
-
## /sled resume <id>
|
|
1883
|
-
|
|
1884
|
-
Resume a parked job. Show the unanswered question and its options, take the answer, and confirm the job is back in the queue. The answer is logged with the user's name, and it becomes a prior decision the Musher can reuse on the next question like it.
|
|
1885
|
-
|
|
1886
|
-
## /sled sync-backlog — price the backlog
|
|
1887
|
-
|
|
1888
|
-
With the \`sleds\` MCP server available, size every open issue so the dashboard's Projects page shows the backlog in dollars and hours, not T-shirt sizes or points. SLEDS is not a tracker and never the system of record: Linear and GitHub keep the issues; SLEDS mirrors their words (cycle, labels, priority, assignee, state) and adds the price.
|
|
1889
|
-
|
|
1890
|
-
1. Find the issues. In Cowork with a Claude Project attached: each spec doc, each phase or slice inside a spec, and each line of a backlog list (a numbered or bulleted list under a heading like Backlog, Next, To do, Roadmap). With a Linear or GitHub MCP available: the open issues of the team / repo the user names, with their cycle or milestone, labels, priority, assignee and state. Skip anything done, closed or shipped.
|
|
1891
|
-
2. For each issue call the \`estimate\` tool with \`spec\` = the title on the first line, then the body (quote the issue or doc, do not paraphrase), and \`source\` = \`{ kind: "linear" | "github" | "claude-project", project: <team, repo or Project name>, doc: <issue key like ENG-42 or #123, or the doc path>, item: <heading or line, only when a doc holds many>, url, cycle, labels, priority, assignee, state }\` — the tracker's values verbatim. The estimator dedupes: an unchanged issue returns its existing estimate, so re-running is safe.
|
|
1892
|
-
3. Report one line per issue — \`ENG-42 · Cycle 34 · high · priya · $6–18 · 60–150 min · 55%\` — then the total of what is open, and the Projects page address (\`/o/<slug>/projects\`). Never invent a number: the band is the estimator's. Do not change anything in the tracker from this command; the page's verbs (defer, prioritize, hand off) do that, and write back in the tracker's terms.
|
|
1893
|
-
|
|
1894
|
-
To keep the backlog priced as issues change, set a Cowork scheduled task: "Every weekday at 7:00, run /sled sync-backlog for <team or Project> and post the open total." An issue whose text changed gets a fresh band; the page counts how many times each was re-sized.
|
|
1895
|
-
|
|
1896
|
-
## Voice
|
|
1897
|
-
|
|
1898
|
-
Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what happens next, not what could.
|
|
1899
|
-
|
|
1900
|
-
## Overnight jobs (how it runs)
|
|
1901
|
-
|
|
1902
|
-
At a wall the proxy prints its options; \`/sled 3\` queues the session for the org's overnight window on the batch tier at half price. The next morning:
|
|
1903
|
-
|
|
1904
|
-
- \`/sled jobs\` — this seat's jobs: id, status, route, cost, when — then the team's open hand-offs waiting for pickup (yours first).
|
|
1905
|
-
- \`/sled resume <id>\` — pull a delivered job's output into the session as the assistant's reply and keep going.
|
|
1906
|
-
|
|
1907
|
-
A job that is \`parked\` or \`failed\` shows its reason in \`/sled jobs\`; the admin can see and cancel jobs on the dashboard's Jobs page.
|
|
1908
|
-
|
|
1909
|
-
## Team hand-off
|
|
1910
|
-
|
|
1911
|
-
\`/sled 5\` at a wall hands the snapshotted session to the most idle coworker (or parks it for anyone on the team). The team hears on Slack and the dashboard's Jobs page lists open hand-offs with the pickup command.
|
|
1912
|
-
|
|
1913
|
-
- \`/sled pickup <id>\` — in the coworker's Claude Code session: the handed-off session resumes there on the org key, metered to them. A task parked from Cowork (no session) starts this turn from the task text. A hand-off meant for someone else or already taken gets one line saying so.
|
|
1914
|
-
|
|
1915
|
-
From Cowork or any MCP client (\`sleds mcp\`): the \`handoffs\` tool lists what is waiting, \`handoff\` parks the current task for a teammate (give the task in full — the picker starts from that text), and \`pickup\` takes one and returns its context to continue there.
|
|
1916
|
-
`;
|
|
1917
|
-
|
|
1918
|
-
// src/commands/hooks.ts
|
|
1919
|
-
init_settings();
|
|
1920
|
-
var hooks2 = (o) => {
|
|
1921
|
-
const log = o.log ?? console.log;
|
|
1922
|
-
const error = o.error ?? console.error;
|
|
1923
|
-
if (!hasClaudeCode(o.home)) {
|
|
1924
|
-
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
1925
|
-
return 1;
|
|
1926
|
-
}
|
|
1927
|
-
const settings = readSettings(o.home);
|
|
1928
|
-
const had = hasSeatHook(settings);
|
|
1929
|
-
const hadSpec = hasSpecHook(settings);
|
|
1930
|
-
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
1931
|
-
const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
|
|
1932
|
-
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
1933
|
-
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
1934
|
-
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
1935
|
-
if (hadLine)
|
|
1936
|
-
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
1937
|
-
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
1938
|
-
return 0;
|
|
1939
|
-
};
|
|
1940
|
-
|
|
1941
|
-
// src/commands/init.ts
|
|
1942
|
-
var DEFAULT_PROXY_URL = "http://localhost:8787";
|
|
1943
|
-
var promptStdin = (question) => new Promise((resolve4) => {
|
|
1944
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1945
|
-
rl.question(question, (answer) => {
|
|
1946
|
-
rl.close();
|
|
1947
|
-
resolve4(answer.trim());
|
|
1948
|
-
});
|
|
1949
|
-
});
|
|
1950
|
-
async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
|
|
1951
|
-
return signupRequest(proxyUrl, { email, apiKey: credential }, fetchFn);
|
|
1952
|
-
}
|
|
1953
|
-
async function signupTeam(proxyUrl, email, team, credential, fetchFn = fetch) {
|
|
1954
|
-
return signupRequest(proxyUrl, { email, team, ...credential ? { apiKey: credential } : {} }, fetchFn);
|
|
1955
|
-
}
|
|
1956
|
-
async function signupRequest(proxyUrl, payload, fetchFn) {
|
|
1957
|
-
try {
|
|
1958
|
-
const res = await fetchFn(`${proxyUrl}/signup`, {
|
|
1959
|
-
method: "POST",
|
|
1960
|
-
headers: { "content-type": "application/json" },
|
|
1961
|
-
body: JSON.stringify(payload),
|
|
1962
|
-
signal: AbortSignal.timeout(20000)
|
|
1963
|
-
});
|
|
1964
|
-
const body = await res.json().catch(() => ({}));
|
|
1965
|
-
if (res.ok && typeof body.token === "string")
|
|
1966
|
-
return {
|
|
1967
|
-
ok: true,
|
|
1968
|
-
token: body.token,
|
|
1969
|
-
created: body.created === true,
|
|
1970
|
-
verifyPending: body.verifyPending === true
|
|
1971
|
-
};
|
|
1972
|
-
return {
|
|
1973
|
-
ok: false,
|
|
1974
|
-
reason: typeof body.reason === "string" ? body.reason : `http_${res.status}`,
|
|
1975
|
-
status: res.status
|
|
1976
|
-
};
|
|
1977
|
-
} catch {
|
|
1978
|
-
return { ok: false, reason: "unreachable", status: 0 };
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
var SIGNUP_REASONS = {
|
|
1982
|
-
email: "that does not look like an email address.",
|
|
1983
|
-
key: "that does not look like an Anthropic API key (sk-ant-…).",
|
|
1984
|
-
key_rejected: "Anthropic did not accept that API key. Check it at console.anthropic.com → API keys.",
|
|
1985
|
-
rate_limited: "too many signups from this network in the last hour. Try again later.",
|
|
1986
|
-
not_configured: "the proxy is not set up for self-serve signup yet.",
|
|
1987
|
-
team: "the team needs a name (2–80 characters).",
|
|
1988
|
-
personal_email: "a team needs a work email — that domain is a personal mailbox. Sign up as an individual instead, or use your company address.",
|
|
1989
|
-
org_exists: "someone at your company already set up SLEDS. Ask them for a seat (Members → invite, Admin → Mint a seat) and run `sleds init --token …`.",
|
|
1990
|
-
unavailable: "the proxy could not reach its control plane. Try again in a moment.",
|
|
1991
|
-
unreachable: "could not reach the proxy."
|
|
1992
|
-
};
|
|
1993
|
-
function signupFailureText(r) {
|
|
1994
|
-
return `Signup failed: ${SIGNUP_REASONS[r.reason] ?? `unexpected answer (${r.reason}, HTTP ${r.status}).`}`;
|
|
1995
|
-
}
|
|
1996
|
-
function normalizeProxyUrl(raw) {
|
|
1997
|
-
let t = raw.trim();
|
|
1998
|
-
if (!t)
|
|
1999
|
-
return null;
|
|
2000
|
-
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(t))
|
|
2001
|
-
t = `https://${t}`;
|
|
2002
|
-
t = t.replace(/[./]+$/, "");
|
|
2003
|
-
try {
|
|
2004
|
-
const u = new URL(t);
|
|
2005
|
-
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
2006
|
-
return null;
|
|
2007
|
-
if (!u.hostname)
|
|
2008
|
-
return null;
|
|
2009
|
-
u.hostname = u.hostname.replace(/\.+$/, "");
|
|
2010
|
-
return `${u.protocol}//${u.host}${u.pathname === "/" ? "" : u.pathname.replace(/\/+$/, "")}`;
|
|
2011
|
-
} catch {
|
|
2012
|
-
return null;
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
async function fetchOrgInfo(url, token, fetchFn = fetch) {
|
|
2016
|
-
try {
|
|
2017
|
-
const res = await fetchFn(`${url}/proxy/meter`, {
|
|
2018
|
-
headers: { authorization: `Bearer ${token}` },
|
|
2019
|
-
signal: AbortSignal.timeout(1e4)
|
|
2020
|
-
});
|
|
2021
|
-
if (!res.ok)
|
|
2022
|
-
return null;
|
|
2023
|
-
const body = await res.json();
|
|
2024
|
-
return body.org?.capabilities ? body.org : null;
|
|
2025
|
-
} catch {
|
|
2026
|
-
return null;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
async function probeProxy(url, fetchFn = fetch, opts = {}) {
|
|
2030
|
-
const tries = opts.tries ?? 2;
|
|
2031
|
-
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
2032
|
-
for (let i = 0;i < tries; i++) {
|
|
2033
|
-
try {
|
|
2034
|
-
const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2035
|
-
if (res.ok)
|
|
2036
|
-
return true;
|
|
2037
|
-
} catch {}
|
|
2038
|
-
}
|
|
2039
|
-
return false;
|
|
2040
|
-
}
|
|
2041
|
-
var resolveToken = async (o) => {
|
|
2042
|
-
const env = o.env ?? process.env;
|
|
2043
|
-
if (o.token)
|
|
2044
|
-
return { token: o.token.trim(), source: "flag" };
|
|
2045
|
-
if (env.SLEDS_TOKEN)
|
|
2046
|
-
return { token: env.SLEDS_TOKEN.trim(), source: "env" };
|
|
2047
|
-
const fromFile = readTokenFile(o.home);
|
|
2048
|
-
if (fromFile)
|
|
2049
|
-
return { token: fromFile, source: "file" };
|
|
2050
|
-
if (o.nonInteractive || !o.prompt)
|
|
2051
|
-
return null;
|
|
2052
|
-
if (!o.signup) {
|
|
2053
|
-
const typed = (await o.prompt("Paste your SLEDS seat token (sled_…), or press enter to sign up as an individual: ")).trim();
|
|
2054
|
-
if (typed)
|
|
2055
|
-
return { token: typed, source: "prompt" };
|
|
2056
|
-
}
|
|
2057
|
-
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
2058
|
-
if (!url)
|
|
2059
|
-
return null;
|
|
2060
|
-
const log = o.log ?? console.log;
|
|
2061
|
-
const err = o.error ?? console.error;
|
|
2062
|
-
let team = o.team?.trim() || null;
|
|
2063
|
-
if (team === null && !o.email) {
|
|
2064
|
-
const which = (await o.prompt("Set up SLEDS for (1) just you, or (2) your team? [1/2]: ")).trim();
|
|
2065
|
-
if (which === "2") {
|
|
2066
|
-
team = (await o.prompt("Team name (e.g. your company): ")).trim();
|
|
2067
|
-
if (!team)
|
|
2068
|
-
return null;
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
const email = (o.email ?? await o.prompt(team ? "Your work email: " : "Your email: ")).trim();
|
|
2072
|
-
if (!email.includes("@"))
|
|
2073
|
-
return null;
|
|
2074
|
-
if (team) {
|
|
2075
|
-
const credential2 = (await o.prompt("Your organization's Anthropic API key, if you have one (sk-ant-…; press enter to skip — a Team plan without one still works, walls offer hold and hand-off): ")).trim();
|
|
2076
|
-
const r2 = await signupTeam(url, email, team, credential2 || null, o.fetchFn);
|
|
2077
|
-
if (!r2.ok) {
|
|
2078
|
-
err(signupFailureText(r2));
|
|
2079
|
-
return null;
|
|
2080
|
-
}
|
|
2081
|
-
log(`Created ${team} with you as owner (${email}).${credential2 ? " Your platform key is stored encrypted as the org's upstream key." : " No platform key yet — add one on the dashboard (Admin → Keys) to unlock continue / overnight / switch at a wall."}`);
|
|
2082
|
-
if (r2.verifyPending)
|
|
2083
|
-
log(`Check ${email} for a verification link and click it — inviting members unlocks then (an unverified team is removed after 48 h; your own seat works meanwhile).`);
|
|
2084
|
-
log("Invite your team on the dashboard (Members → invite by email, Admin → Mint a seat); each person runs `sleds init --token …`.");
|
|
2085
|
-
return { token: r2.token, source: "signup" };
|
|
2086
|
-
}
|
|
2087
|
-
const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
|
|
2088
|
-
if (!credential)
|
|
2089
|
-
return null;
|
|
2090
|
-
const r = await signupIndividual(url, email, credential, o.fetchFn);
|
|
2091
|
-
if (!r.ok) {
|
|
2092
|
-
err(signupFailureText(r));
|
|
2093
|
-
return null;
|
|
2094
|
-
}
|
|
2095
|
-
log(r.created ? `Created your individual seat (${email}). Your key is stored encrypted and is the only key this seat spends.` : `Re-issued your individual seat (${email}); the previous token is revoked.`);
|
|
2096
|
-
return { token: r.token, source: "signup" };
|
|
2097
|
-
};
|
|
2098
|
-
var init = async (o) => {
|
|
2099
|
-
const log = o.log ?? console.log;
|
|
2100
|
-
const error = o.error ?? console.error;
|
|
2101
|
-
const env = o.env ?? process.env;
|
|
2102
|
-
if (!hasClaudeCode(o.home)) {
|
|
2103
|
-
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
2104
|
-
return 1;
|
|
2105
|
-
}
|
|
2106
|
-
const resolved = await resolveToken(o);
|
|
2107
|
-
if (!resolved) {
|
|
2108
|
-
error(o.signup ? "Signup did not complete. Nothing was changed." : "No seat token. Pass --token, set SLEDS_TOKEN, run without --non-interactive, or use --signup.");
|
|
2109
|
-
return 2;
|
|
2110
|
-
}
|
|
2111
|
-
if (!isSeatToken(resolved.token)) {
|
|
2112
|
-
error("Invalid seat token: expected something like sled_xxxxxxxx.");
|
|
2113
|
-
return 2;
|
|
2114
|
-
}
|
|
2115
|
-
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
2116
|
-
if (!url) {
|
|
2117
|
-
error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
|
|
2118
|
-
return 2;
|
|
2119
|
-
}
|
|
2120
|
-
if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
|
|
2121
|
-
error(`No SLEDS proxy answered at ${url}/healthz. Check the URL (or pass --no-probe to save it anyway).`);
|
|
2122
|
-
return 2;
|
|
2123
|
-
}
|
|
2124
|
-
const wrote = [];
|
|
2125
|
-
const org = await fetchOrgInfo(url, resolved.token, o.fetchFn);
|
|
2126
|
-
const wiring = o.proxyAlways ? "proxy-always" : o.seatFirst ? "seat-first" : org?.mode ?? "proxy-always";
|
|
2127
|
-
if (wiring === "proxy-always" && org && !org.capabilities.upstreamKey) {
|
|
2128
|
-
error(`${org.name} has no platform key for the proxy to forward with; --proxy-always would break every request. Using seat-first.`);
|
|
2129
|
-
}
|
|
2130
|
-
const finalWiring = wiring === "proxy-always" && org && !org.capabilities.upstreamKey ? "seat-first" : wiring;
|
|
2131
|
-
if (backupSettings(o.home))
|
|
2132
|
-
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
2133
|
-
const current = readSettings(o.home);
|
|
2134
|
-
const wired = finalWiring === "proxy-always" ? applyProxy(current, { url, token: resolved.token }) : removeProxy(current);
|
|
2135
|
-
const statusLine = applyStatusLine(wired, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
|
|
2136
|
-
writeSettings(o.home, statusLine.settings);
|
|
2137
|
-
wrote.push(finalWiring === "proxy-always" ? `set env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN in ${settingsPath(o.home)} (everything through the proxy)` : `left Claude Code on your seat (no ANTHROPIC_BASE_URL in ${settingsPath(o.home)}); \`sleds on\` switches to the proxy at a wall`);
|
|
2138
|
-
wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
|
|
2139
|
-
hooks2({
|
|
2140
|
-
home: o.home,
|
|
2141
|
-
env: o.env,
|
|
2142
|
-
refreshStatusLine: false,
|
|
2143
|
-
seatEventCommand: o.seatEventCommand,
|
|
2144
|
-
specEventCommand: o.specEventCommand,
|
|
2145
|
-
log: (line) => wrote.push(line),
|
|
2146
|
-
error
|
|
2147
|
-
});
|
|
2148
|
-
if (resolved.source !== "file") {
|
|
2149
|
-
writeTokenFile(o.home, resolved.token);
|
|
2150
|
-
wrote.push(`saved token to ${tokenPath(o.home)} (0600)`);
|
|
2151
|
-
}
|
|
2152
|
-
const prior = readConfig(o.home);
|
|
2153
|
-
writeConfig(o.home, {
|
|
2154
|
-
...prior,
|
|
2155
|
-
token: resolved.token,
|
|
2156
|
-
proxyUrl: url,
|
|
2157
|
-
wiring: finalWiring,
|
|
2158
|
-
...org ? { org } : {},
|
|
2159
|
-
...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
|
|
2160
|
-
});
|
|
2161
|
-
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
2162
|
-
mkdirSync6(skillDir(o.home), { recursive: true });
|
|
2163
|
-
writeFileSync6(skillPath(o.home), SKILL_MD);
|
|
2164
|
-
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
2165
|
-
if (o.installMusher) {
|
|
2166
|
-
const r = o.installMusher();
|
|
2167
|
-
if (r)
|
|
2168
|
-
wrote.push(r);
|
|
2169
|
-
}
|
|
2170
|
-
const orgLine = org ? `${org.name} · ${org.plan}${org.capabilities.upstreamKey ? ` · ${org.capabilities.upstreamSource === "own" ? "org key" : "shared key"}` : " · no platform key"}` : "org unknown (proxy did not answer /proxy/meter)";
|
|
2171
|
-
if (finalWiring === "proxy-always") {
|
|
2172
|
-
log(`Claude Code now runs through SLEDS on the org key. (${orgLine})`);
|
|
2173
|
-
log("");
|
|
2174
|
-
for (const w of wrote)
|
|
2175
|
-
log(` ${w}`);
|
|
2176
|
-
log("");
|
|
2177
|
-
log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
|
|
2178
|
-
log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
|
|
2179
|
-
log("Prefer your seat first? `sleds init --seat-first` leaves Claude Code on it and switches only at a wall.");
|
|
2180
|
-
} else {
|
|
2181
|
-
log(`SLEDS is watching your seat. Nothing changes until a wall. (${orgLine})`);
|
|
2182
|
-
log("");
|
|
2183
|
-
for (const w of wrote)
|
|
2184
|
-
log(` ${w}`);
|
|
2185
|
-
log("");
|
|
2186
|
-
log("Claude Code keeps running on your seat; the status line reports its quota to SLEDS on every turn.");
|
|
2187
|
-
if (org && !org.capabilities.upstreamKey) {
|
|
2188
|
-
log("This org has no platform key, so at a wall the choices are hold until reset, `sleds defer` (this Mac resumes the session itself at the reset), or hand off to a teammate. An admin can add a key on the dashboard to unlock continue / overnight / switch.");
|
|
2189
|
-
} else {
|
|
2190
|
-
log("At a wall: `sleds on --until-reset +5h` (or /sled 1) switches to the org key through the proxy until the reset; `sleds off` puts you back.");
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
return 0;
|
|
2194
|
-
};
|
|
2195
|
-
|
|
2196
|
-
// ../../packages/shared/src/mcp/server.ts
|
|
2197
|
-
init_format();
|
|
2198
|
-
function fetchClient(o) {
|
|
2199
|
-
const fetchFn = o.fetchFn ?? fetch;
|
|
2200
|
-
const base = o.proxyUrl.replace(/\/+$/, "");
|
|
2201
|
-
const headers = { "content-type": "application/json", authorization: `Bearer ${o.token}` };
|
|
2202
|
-
const timeout = () => AbortSignal.timeout(o.timeoutMs ?? MCP_TIMEOUT_MS);
|
|
2203
|
-
return {
|
|
2204
|
-
async post(path, body) {
|
|
2205
|
-
try {
|
|
2206
|
-
const res = await fetchFn(`${base}${path}`, {
|
|
2207
|
-
method: "POST",
|
|
2208
|
-
headers,
|
|
2209
|
-
body: JSON.stringify(body),
|
|
2210
|
-
signal: timeout()
|
|
2211
|
-
});
|
|
2212
|
-
if (!res.ok)
|
|
2213
|
-
return null;
|
|
2214
|
-
const parsed = await res.json();
|
|
2215
|
-
return isObject7(parsed) ? parsed : null;
|
|
2216
|
-
} catch {
|
|
2217
|
-
return null;
|
|
2218
|
-
}
|
|
2219
|
-
},
|
|
2220
|
-
async postFull(path, body) {
|
|
2221
|
-
try {
|
|
2222
|
-
const res = await fetchFn(`${base}${path}`, {
|
|
2223
|
-
method: "POST",
|
|
2224
|
-
headers,
|
|
2225
|
-
body: JSON.stringify(body),
|
|
2226
|
-
signal: timeout()
|
|
2227
|
-
});
|
|
2228
|
-
let json = null;
|
|
2229
|
-
try {
|
|
2230
|
-
json = await res.json();
|
|
2231
|
-
} catch {
|
|
2232
|
-
json = null;
|
|
2233
|
-
}
|
|
2234
|
-
return { status: res.status, json: isObject7(json) ? json : null };
|
|
2235
|
-
} catch {
|
|
2236
|
-
return null;
|
|
2237
|
-
}
|
|
2238
|
-
},
|
|
2239
|
-
async get(path) {
|
|
2240
|
-
try {
|
|
2241
|
-
const res = await fetchFn(`${base}${path}`, { headers, signal: timeout() });
|
|
2242
|
-
if (!res.ok)
|
|
2243
|
-
return null;
|
|
2244
|
-
const parsed = await res.json();
|
|
2245
|
-
return isObject7(parsed) ? parsed : null;
|
|
2246
|
-
} catch {
|
|
2247
|
-
return null;
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
var PROTOCOL_VERSION = "2025-06-18";
|
|
2253
|
-
var SERVER_INFO = { name: "sleds", version: "0.0.1" };
|
|
2254
|
-
var MCP_TIMEOUT_MS = 5000;
|
|
2255
|
-
var sourceSchema = {
|
|
2256
|
-
type: "object",
|
|
2257
|
-
description: "Where this item lives, so the Projects page groups it: a Claude Project doc, a repo spec, a Linear/GitHub issue. Same source + same text on a later call returns the same estimate (idempotent sync).",
|
|
2258
|
-
properties: {
|
|
2259
|
-
kind: { type: "string", enum: ["claude-project", "repo", "linear", "github", "manual"] },
|
|
2260
|
-
project: { type: "string", description: "Project / repo / board name." },
|
|
2261
|
-
doc: { type: "string", description: "Doc path or issue key inside it." },
|
|
2262
|
-
item: {
|
|
2263
|
-
type: "string",
|
|
2264
|
-
description: "The line or phase inside the doc, when a doc holds many."
|
|
2265
|
-
},
|
|
2266
|
-
url: { type: "string", description: "Link to open it, if any." }
|
|
2267
|
-
},
|
|
2268
|
-
required: ["kind", "project"]
|
|
2122
|
+
// ../../packages/shared/src/mcp/server.ts
|
|
2123
|
+
init_format();
|
|
2124
|
+
function fetchClient(o) {
|
|
2125
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
2126
|
+
const base = o.proxyUrl.replace(/\/+$/, "");
|
|
2127
|
+
const headers = { "content-type": "application/json", authorization: `Bearer ${o.token}` };
|
|
2128
|
+
const timeout = () => AbortSignal.timeout(o.timeoutMs ?? MCP_TIMEOUT_MS);
|
|
2129
|
+
return {
|
|
2130
|
+
async post(path, body) {
|
|
2131
|
+
try {
|
|
2132
|
+
const res = await fetchFn(`${base}${path}`, {
|
|
2133
|
+
method: "POST",
|
|
2134
|
+
headers,
|
|
2135
|
+
body: JSON.stringify(body),
|
|
2136
|
+
signal: timeout()
|
|
2137
|
+
});
|
|
2138
|
+
if (!res.ok)
|
|
2139
|
+
return null;
|
|
2140
|
+
const parsed = await res.json();
|
|
2141
|
+
return isObject7(parsed) ? parsed : null;
|
|
2142
|
+
} catch {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
},
|
|
2146
|
+
async postFull(path, body) {
|
|
2147
|
+
try {
|
|
2148
|
+
const res = await fetchFn(`${base}${path}`, {
|
|
2149
|
+
method: "POST",
|
|
2150
|
+
headers,
|
|
2151
|
+
body: JSON.stringify(body),
|
|
2152
|
+
signal: timeout()
|
|
2153
|
+
});
|
|
2154
|
+
let json = null;
|
|
2155
|
+
try {
|
|
2156
|
+
json = await res.json();
|
|
2157
|
+
} catch {
|
|
2158
|
+
json = null;
|
|
2159
|
+
}
|
|
2160
|
+
return { status: res.status, json: isObject7(json) ? json : null };
|
|
2161
|
+
} catch {
|
|
2162
|
+
return null;
|
|
2163
|
+
}
|
|
2164
|
+
},
|
|
2165
|
+
async get(path) {
|
|
2166
|
+
try {
|
|
2167
|
+
const res = await fetchFn(`${base}${path}`, { headers, signal: timeout() });
|
|
2168
|
+
if (!res.ok)
|
|
2169
|
+
return null;
|
|
2170
|
+
const parsed = await res.json();
|
|
2171
|
+
return isObject7(parsed) ? parsed : null;
|
|
2172
|
+
} catch {
|
|
2173
|
+
return null;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
2179
|
+
var SERVER_INFO = { name: "sleds", version: "0.0.1" };
|
|
2180
|
+
var MCP_TIMEOUT_MS = 5000;
|
|
2181
|
+
var sourceSchema = {
|
|
2182
|
+
type: "object",
|
|
2183
|
+
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).",
|
|
2184
|
+
properties: {
|
|
2185
|
+
kind: { type: "string", enum: ["claude-project", "repo", "linear", "github", "manual"] },
|
|
2186
|
+
project: { type: "string", description: "Project / repo / board name." },
|
|
2187
|
+
doc: { type: "string", description: "Doc path or issue key inside it." },
|
|
2188
|
+
item: {
|
|
2189
|
+
type: "string",
|
|
2190
|
+
description: "The line or phase inside the doc, when a doc holds many."
|
|
2191
|
+
},
|
|
2192
|
+
url: { type: "string", description: "Link to open it, if any." }
|
|
2193
|
+
},
|
|
2194
|
+
required: ["kind", "project"]
|
|
2269
2195
|
};
|
|
2270
2196
|
var specSchema = {
|
|
2271
2197
|
type: "object",
|
|
@@ -2315,28 +2241,106 @@ var TOOLS = [
|
|
|
2315
2241
|
type: "string",
|
|
2316
2242
|
description: "The task to hand off, in full. First line becomes the title."
|
|
2317
2243
|
},
|
|
2318
|
-
to: {
|
|
2244
|
+
to: {
|
|
2245
|
+
type: "string",
|
|
2246
|
+
description: "Coworker's email — a member of your org, or a buddy in another org (see the buddy tool). Omit to park for anyone in your org."
|
|
2247
|
+
},
|
|
2319
2248
|
note: { type: "string", description: "One line on where things stand (≤280 chars)." }
|
|
2320
2249
|
},
|
|
2321
2250
|
required: ["task"]
|
|
2322
2251
|
}
|
|
2323
2252
|
},
|
|
2324
2253
|
{
|
|
2325
|
-
name: "defer",
|
|
2326
|
-
description: "Hit a limit? Defer the work to the reset — the musher runs it then. With an
|
|
2254
|
+
name: "defer",
|
|
2255
|
+
description: "Hit a limit? Defer the work to the reset — the musher runs it then. With an platform key it runs in the cloud on the pinned model; without one, the seat's own Mac resumes the last Claude Code session at the reset (sleds musher). Give `at` as +2h, +90m or an ISO time to override the binding reset.",
|
|
2256
|
+
inputSchema: {
|
|
2257
|
+
type: "object",
|
|
2258
|
+
properties: {
|
|
2259
|
+
at: {
|
|
2260
|
+
type: "string",
|
|
2261
|
+
description: "When to start: +2h, +90m, or an ISO timestamp. Omit for the binding reset."
|
|
2262
|
+
},
|
|
2263
|
+
model: { type: "string", description: "Pin the model the deferred run must use." },
|
|
2264
|
+
task: {
|
|
2265
|
+
type: "string",
|
|
2266
|
+
description: "Optional note on what the deferred run should finish (kept with the job)."
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
name: "actual",
|
|
2273
|
+
description: "Close the loop on an estimated item: record what actually happened — minutes, dollars, tokens, model, who did it, when it started and finished, where the output lives (PR url), time by phase (discuss/build/deploy/test), and its state (started · done · merged · shipped). Same `source` as the estimate; Projects shows estimate vs actual and calibrates future bands.",
|
|
2274
|
+
inputSchema: {
|
|
2275
|
+
type: "object",
|
|
2276
|
+
properties: {
|
|
2277
|
+
source: {
|
|
2278
|
+
type: "object",
|
|
2279
|
+
description: "The same source the estimate was filed under.",
|
|
2280
|
+
properties: {
|
|
2281
|
+
kind: {
|
|
2282
|
+
type: "string",
|
|
2283
|
+
enum: ["claude-project", "repo", "linear", "github", "manual"]
|
|
2284
|
+
},
|
|
2285
|
+
project: { type: "string" },
|
|
2286
|
+
doc: { type: "string" },
|
|
2287
|
+
item: { type: "string" },
|
|
2288
|
+
url: { type: "string" }
|
|
2289
|
+
},
|
|
2290
|
+
required: ["kind", "project"]
|
|
2291
|
+
},
|
|
2292
|
+
minutes: { type: "number", description: "Total wall-clock minutes of work." },
|
|
2293
|
+
usd: { type: "number", description: "Dollars spent, when known." },
|
|
2294
|
+
seatCreditsUsd: {
|
|
2295
|
+
type: "number",
|
|
2296
|
+
description: "Seat credits ($, Cowork/Claude Code overage) this item burned — the rail the proxy never sees."
|
|
2297
|
+
},
|
|
2298
|
+
tokensIn: { type: "number" },
|
|
2299
|
+
tokensOut: { type: "number" },
|
|
2300
|
+
model: { type: "string", description: "Model that did the work, e.g. claude-fable-5-1." },
|
|
2301
|
+
by: {
|
|
2302
|
+
type: "string",
|
|
2303
|
+
description: "Who or what: an email, 'claude-fable-5-1 · cowork', 'musher local'."
|
|
2304
|
+
},
|
|
2305
|
+
ref: { type: "string", description: "Where the output lives: PR url, commit, doc." },
|
|
2306
|
+
startedAt: { type: "string", description: "ISO time work began." },
|
|
2307
|
+
finishedAt: { type: "string", description: "ISO time work finished." },
|
|
2308
|
+
state: {
|
|
2309
|
+
type: "string",
|
|
2310
|
+
description: "started · done · merged · shipped · parked (default: done when minutes or finishedAt are given)."
|
|
2311
|
+
},
|
|
2312
|
+
phases: {
|
|
2313
|
+
type: "object",
|
|
2314
|
+
description: "Minutes by phase; total defaults to their sum when `minutes` is absent.",
|
|
2315
|
+
properties: {
|
|
2316
|
+
discussMin: { type: "number" },
|
|
2317
|
+
buildMin: { type: "number" },
|
|
2318
|
+
deployMin: { type: "number" },
|
|
2319
|
+
testMin: { type: "number" }
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
},
|
|
2323
|
+
required: ["source"]
|
|
2324
|
+
}
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
name: "buddy",
|
|
2328
|
+
description: "Pair with one person outside your org so hand-offs can cross to them (spec 04 §4): they get an email, click Accept, and from then on `handoff {to: their email}` lands in their Waiting for you and runs on their seat. Nothing else crosses — no keys, usage or membership. With no `email`, lists your buddies. `remove: true` unpairs.",
|
|
2329
|
+
inputSchema: {
|
|
2330
|
+
type: "object",
|
|
2331
|
+
properties: {
|
|
2332
|
+
email: { type: "string", description: "Their email. Omit to list." },
|
|
2333
|
+
remove: { type: "boolean", description: "Unpair instead of inviting." }
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
name: "withdraw",
|
|
2339
|
+
description: "Take back a hand-off you parked before anyone picks it up (the id from the handoffs tool). Only the seat that parked it can withdraw it.",
|
|
2327
2340
|
inputSchema: {
|
|
2328
2341
|
type: "object",
|
|
2329
|
-
properties: {
|
|
2330
|
-
|
|
2331
|
-
type: "string",
|
|
2332
|
-
description: "When to start: +2h, +90m, or an ISO timestamp. Omit for the binding reset."
|
|
2333
|
-
},
|
|
2334
|
-
model: { type: "string", description: "Pin the model the deferred run must use." },
|
|
2335
|
-
task: {
|
|
2336
|
-
type: "string",
|
|
2337
|
-
description: "Optional note on what the deferred run should finish (kept with the job)."
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2342
|
+
properties: { handoffId: { type: "string", description: "The hand-off id." } },
|
|
2343
|
+
required: ["handoffId"]
|
|
2340
2344
|
}
|
|
2341
2345
|
},
|
|
2342
2346
|
{
|
|
@@ -2497,11 +2501,12 @@ async function jobsTool(ctx) {
|
|
|
2497
2501
|
return UNREACHABLE;
|
|
2498
2502
|
const rows = Array.isArray(r.jobs) ? r.jobs.filter(isObject7) : [];
|
|
2499
2503
|
const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject7) : [];
|
|
2504
|
+
const me = isObject7(r.seat) ? str2(r.seat.email) : undefined;
|
|
2500
2505
|
const now = (ctx.now ?? Date.now)();
|
|
2501
2506
|
if (rows.length === 0 && handoffs.length === 0)
|
|
2502
2507
|
return { text: "No jobs for this seat. Queue one with the overnight tool." };
|
|
2503
2508
|
if (rows.length === 0)
|
|
2504
|
-
return { text: ["No jobs for this seat.", ...handoffLines(handoffs, now)].join(`
|
|
2509
|
+
return { text: ["No jobs for this seat.", ...handoffLines(handoffs, now, me)].join(`
|
|
2505
2510
|
`) };
|
|
2506
2511
|
const lines = rows.map((j) => {
|
|
2507
2512
|
const when = (ts) => num4(ts) !== undefined ? shortDuration(Math.abs(now - num4(ts))) : "";
|
|
@@ -2517,18 +2522,19 @@ async function jobsTool(ctx) {
|
|
|
2517
2522
|
].join(" ").trimEnd();
|
|
2518
2523
|
});
|
|
2519
2524
|
lines.push("Resume a delivered job in a session with /sled resume <id>.");
|
|
2520
|
-
lines.push(...handoffLines(handoffs, now));
|
|
2525
|
+
lines.push(...handoffLines(handoffs, now, me));
|
|
2521
2526
|
return { text: lines.join(`
|
|
2522
2527
|
`) };
|
|
2523
2528
|
}
|
|
2524
|
-
function handoffLines(handoffs, now) {
|
|
2529
|
+
function handoffLines(handoffs, now, me) {
|
|
2525
2530
|
if (handoffs.length === 0)
|
|
2526
2531
|
return [];
|
|
2527
2532
|
const out = ["", `Hand-offs waiting (${handoffs.length}):`];
|
|
2528
2533
|
for (const h of handoffs) {
|
|
2529
2534
|
const forMe = h.forMe === true;
|
|
2530
2535
|
const to = str2(h.to);
|
|
2531
|
-
const
|
|
2536
|
+
const mine = me !== undefined && str2(h.from)?.toLowerCase() === me.toLowerCase();
|
|
2537
|
+
const who = mine ? `yours · for ${to ?? "anyone"}` : forMe ? to ? "for you" : "parked for anyone" : `for ${to ?? "anyone"}`;
|
|
2532
2538
|
const created = num4(h.createdAt);
|
|
2533
2539
|
const age = created !== undefined ? `${shortDuration(Math.max(0, now - created))} ago` : "";
|
|
2534
2540
|
const note = str2(h.note);
|
|
@@ -2537,230 +2543,813 @@ function handoffLines(handoffs, now) {
|
|
|
2537
2543
|
parts.push("note only");
|
|
2538
2544
|
out.push(`${str2(h.handoffId) ?? "?"} ${parts.join(" · ")}${note ? ` «${note.length > 60 ? `${note.slice(0, 57)}…` : note}»` : ""}`);
|
|
2539
2545
|
}
|
|
2540
|
-
out.push("Take one in Claude Code with /sled pickup <id>.");
|
|
2541
|
-
return out;
|
|
2546
|
+
out.push(me !== undefined && handoffs.some((h) => str2(h.from)?.toLowerCase() === me.toLowerCase()) ? "Take one with /sled pickup <id>; take back one of yours with the withdraw tool." : "Take one in Claude Code with /sled pickup <id>.");
|
|
2547
|
+
return out;
|
|
2548
|
+
}
|
|
2549
|
+
async function handoffsTool(ctx) {
|
|
2550
|
+
if (!ctx.client)
|
|
2551
|
+
return NOT_SET_UP;
|
|
2552
|
+
const r = await ctx.client.get("/proxy/jobs");
|
|
2553
|
+
if (!r)
|
|
2554
|
+
return UNREACHABLE;
|
|
2555
|
+
const handoffs = Array.isArray(r.handoffs) ? r.handoffs.filter(isObject7) : [];
|
|
2556
|
+
if (handoffs.length === 0)
|
|
2557
|
+
return { text: "No open hand-offs on your team. Park one with the handoff tool." };
|
|
2558
|
+
const now = (ctx.now ?? Date.now)();
|
|
2559
|
+
const packets = handoffs.map((h) => str2(h.packet)).filter((p) => Boolean(p)).join(`
|
|
2560
|
+
|
|
2561
|
+
---
|
|
2562
|
+
|
|
2563
|
+
`);
|
|
2564
|
+
const me = isObject7(r.seat) ? str2(r.seat.email) : undefined;
|
|
2565
|
+
return {
|
|
2566
|
+
text: [handoffLines(handoffs, now, me).slice(1).join(`
|
|
2567
|
+
`), packets ? `
|
|
2568
|
+
${packets}` : ""].join(`
|
|
2569
|
+
`)
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
async function withdrawTool(ctx, args) {
|
|
2573
|
+
if (!ctx.client)
|
|
2574
|
+
return NOT_SET_UP;
|
|
2575
|
+
const handoffId = str2(args.handoffId)?.trim();
|
|
2576
|
+
if (!handoffId)
|
|
2577
|
+
return { text: "Pass handoffId: the id from the handoffs tool.", isError: true };
|
|
2578
|
+
const r = await ctx.client.postFull("/proxy/handoff/withdraw", { handoffId });
|
|
2579
|
+
if (!r)
|
|
2580
|
+
return { text: `Nothing withdrawn. ${UNREACHABLE.text}`, isError: true };
|
|
2581
|
+
if (r.status < 200 || r.status >= 300) {
|
|
2582
|
+
const msg = isObject7(r.json) && isObject7(r.json.error) ? str2(r.json.error.message) : undefined;
|
|
2583
|
+
return { text: msg ?? `Could not withdraw ${handoffId} (${r.status}).`, isError: true };
|
|
2584
|
+
}
|
|
2585
|
+
return { text: `Withdrawn ${handoffId}. It is off the team's list; nobody can pick it up.` };
|
|
2586
|
+
}
|
|
2587
|
+
async function handoffTool(ctx, args) {
|
|
2588
|
+
if (!ctx.client)
|
|
2589
|
+
return NOT_SET_UP;
|
|
2590
|
+
const task = str2(args.task)?.trim();
|
|
2591
|
+
if (!task)
|
|
2592
|
+
return { text: "Nothing to hand off. Pass task: what the picker should do.", isError: true };
|
|
2593
|
+
const to = str2(args.to)?.trim();
|
|
2594
|
+
const r = await ctx.client.postFull("/proxy/handoff/park", {
|
|
2595
|
+
task,
|
|
2596
|
+
to: to || undefined,
|
|
2597
|
+
note: str2(args.note)?.trim() || undefined
|
|
2598
|
+
});
|
|
2599
|
+
if (!r)
|
|
2600
|
+
return { text: `Nothing was parked. ${UNREACHABLE.text}`, isError: true };
|
|
2601
|
+
if (r.status === 404 && to)
|
|
2602
|
+
return {
|
|
2603
|
+
text: `${to} is not a member of your org and not a buddy. Nothing was parked. Pair first with the buddy tool (they accept by email), then hand off again.`,
|
|
2604
|
+
isError: true
|
|
2605
|
+
};
|
|
2606
|
+
if (r.status !== 200)
|
|
2607
|
+
return {
|
|
2608
|
+
text: errorMessage(r.json) ?? `Nothing was parked (HTTP ${r.status}).`,
|
|
2609
|
+
isError: true
|
|
2610
|
+
};
|
|
2611
|
+
const id = str2(r.json?.handoffId) ?? "unknown";
|
|
2612
|
+
return {
|
|
2613
|
+
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.`
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
async function pickupTool(ctx, args) {
|
|
2617
|
+
if (!ctx.client)
|
|
2618
|
+
return NOT_SET_UP;
|
|
2619
|
+
const handoffId = str2(args.handoffId)?.trim();
|
|
2620
|
+
if (!handoffId)
|
|
2621
|
+
return { text: "Pass handoffId, from the handoffs tool.", isError: true };
|
|
2622
|
+
const r = await ctx.client.postFull("/proxy/handoff/pickup", { handoffId });
|
|
2623
|
+
if (!r)
|
|
2624
|
+
return { text: `Nothing was picked up. ${UNREACHABLE.text}`, isError: true };
|
|
2625
|
+
if (r.status !== 200)
|
|
2626
|
+
return {
|
|
2627
|
+
text: errorMessage(r.json) ?? `Could not pick it up (HTTP ${r.status}).`,
|
|
2628
|
+
isError: true
|
|
2629
|
+
};
|
|
2630
|
+
const j = r.json ?? {};
|
|
2631
|
+
const from = str2(j.from) ?? "a teammate";
|
|
2632
|
+
const note = str2(j.note);
|
|
2633
|
+
const head = `Picked up from ${from}.${note ? ` Where it stands: ${note}` : ""}`;
|
|
2634
|
+
if (j.kind === "task")
|
|
2635
|
+
return { text: `${head}
|
|
2636
|
+
|
|
2637
|
+
The task:
|
|
2638
|
+
${str2(j.task) ?? ""}
|
|
2639
|
+
|
|
2640
|
+
Carry it through from here.` };
|
|
2641
|
+
const first = str2(j.firstUserText) ?? "";
|
|
2642
|
+
const last = str2(j.lastUserText) ?? "";
|
|
2643
|
+
const n = num4(j.messageCount);
|
|
2644
|
+
return {
|
|
2645
|
+
text: [
|
|
2646
|
+
`${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.`,
|
|
2647
|
+
"",
|
|
2648
|
+
`It started with:
|
|
2649
|
+
${first}`,
|
|
2650
|
+
...last && last !== first ? ["", `The last ask was:
|
|
2651
|
+
${last}`] : [],
|
|
2652
|
+
"",
|
|
2653
|
+
"Continue the work from here."
|
|
2654
|
+
].join(`
|
|
2655
|
+
`)
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
function parseAt2(v, now) {
|
|
2659
|
+
const rel = /^\+(\d+)([hm])$/.exec(v.trim());
|
|
2660
|
+
if (rel)
|
|
2661
|
+
return now + Number.parseInt(rel[1] ?? "0", 10) * (rel[2] === "h" ? 3600000 : 60000);
|
|
2662
|
+
const t = Date.parse(v);
|
|
2663
|
+
return Number.isFinite(t) ? t : undefined;
|
|
2664
|
+
}
|
|
2665
|
+
async function deferTool(ctx, args) {
|
|
2666
|
+
if (!ctx.client)
|
|
2667
|
+
return NOT_SET_UP;
|
|
2668
|
+
const now = (ctx.now ?? Date.now)();
|
|
2669
|
+
const atRaw = str2(args.at)?.trim();
|
|
2670
|
+
const at = atRaw ? parseAt2(atRaw, now) : undefined;
|
|
2671
|
+
if (atRaw && at === undefined)
|
|
2672
|
+
return { text: "`at` takes +2h, +90m or an ISO time.", isError: true };
|
|
2673
|
+
const r = await ctx.client.post("/proxy/defer", {
|
|
2674
|
+
...at !== undefined ? { resetAt: at } : {},
|
|
2675
|
+
...str2(args.model) ? { model: str2(args.model) } : {},
|
|
2676
|
+
host: "mcp"
|
|
2677
|
+
});
|
|
2678
|
+
if (!r)
|
|
2679
|
+
return { text: `Nothing was deferred. ${UNREACHABLE.text}`, isError: true };
|
|
2680
|
+
const jobId = str2(r.jobId) ?? "unknown";
|
|
2681
|
+
const when = num4(r.scheduledAt);
|
|
2682
|
+
const startsIn = when !== undefined && when > now + 60000 ? `starts in ${shortDuration(when - now)}` : "starts at the next musher tick (within 5 min)";
|
|
2683
|
+
return {
|
|
2684
|
+
text: `Deferred. Job ${jobId} ${startsIn}. With no platform key it runs on the seat's own Mac (sleds musher must be installed there — sleds doctor checks); with a key, in the cloud. Watch it with the jobs tool or /sled jobs.${str2(args.task) ? ` Note kept: ${str2(args.task)}` : ""}`
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
async function actualTool(ctx, args) {
|
|
2688
|
+
if (!ctx.client)
|
|
2689
|
+
return NOT_SET_UP;
|
|
2690
|
+
const source = args.source;
|
|
2691
|
+
if (typeof source !== "object" || source === null)
|
|
2692
|
+
return {
|
|
2693
|
+
text: "`source` is required — the same one the estimate was filed under.",
|
|
2694
|
+
isError: true
|
|
2695
|
+
};
|
|
2696
|
+
const iso = (v) => {
|
|
2697
|
+
const t = str2(v) ? Date.parse(str2(v)) : Number.NaN;
|
|
2698
|
+
return Number.isFinite(t) ? t : undefined;
|
|
2699
|
+
};
|
|
2700
|
+
const phasesRaw = typeof args.phases === "object" && args.phases !== null ? args.phases : undefined;
|
|
2701
|
+
const phases = phasesRaw ? Object.fromEntries(["discussMin", "buildMin", "deployMin", "testMin"].map((k) => [k, num4(phasesRaw[k])]).filter(([, v]) => v !== undefined)) : undefined;
|
|
2702
|
+
const work = {
|
|
2703
|
+
...iso(args.startedAt) !== undefined ? { startedAt: iso(args.startedAt) } : {},
|
|
2704
|
+
...iso(args.finishedAt) !== undefined ? { finishedAt: iso(args.finishedAt) } : {},
|
|
2705
|
+
...str2(args.model) ? { model: str2(args.model) } : {},
|
|
2706
|
+
...num4(args.tokensIn) !== undefined ? { tokensIn: num4(args.tokensIn) } : {},
|
|
2707
|
+
...num4(args.tokensOut) !== undefined ? { tokensOut: num4(args.tokensOut) } : {},
|
|
2708
|
+
...str2(args.by) ? { by: str2(args.by) } : {},
|
|
2709
|
+
...str2(args.ref) ? { ref: str2(args.ref) } : {},
|
|
2710
|
+
...phases && Object.keys(phases).length > 0 ? { phases } : {},
|
|
2711
|
+
...num4(args.seatCreditsUsd) !== undefined ? { seatCreditsUsd: num4(args.seatCreditsUsd) } : {}
|
|
2712
|
+
};
|
|
2713
|
+
const r = await ctx.client.post("/proxy/estimate/actual", {
|
|
2714
|
+
source,
|
|
2715
|
+
...num4(args.minutes) !== undefined ? { actualMinutes: num4(args.minutes) } : {},
|
|
2716
|
+
...num4(args.usd) !== undefined ? { actualUsd: num4(args.usd) } : {},
|
|
2717
|
+
...str2(args.state) ? { state: str2(args.state) } : {},
|
|
2718
|
+
...Object.keys(work).length > 0 ? { work } : {}
|
|
2719
|
+
});
|
|
2720
|
+
if (!r)
|
|
2721
|
+
return { text: `Nothing recorded. ${UNREACHABLE.text}`, isError: true };
|
|
2722
|
+
if (r.ok === false)
|
|
2723
|
+
return {
|
|
2724
|
+
text: `No estimate on file for that source (${str2(r.reason) ?? "unknown"}). Estimate it first.`,
|
|
2725
|
+
isError: true
|
|
2726
|
+
};
|
|
2727
|
+
const bits = [
|
|
2728
|
+
num4(args.minutes) !== undefined ? `${num4(args.minutes)} min` : phases ? "phases recorded" : null,
|
|
2729
|
+
num4(args.usd) !== undefined ? usd(num4(args.usd)) : null,
|
|
2730
|
+
num4(args.tokensIn) !== undefined || num4(args.tokensOut) !== undefined ? `${(num4(args.tokensIn) ?? 0) + (num4(args.tokensOut) ?? 0)} tokens` : null,
|
|
2731
|
+
str2(args.model),
|
|
2732
|
+
str2(args.state) ?? "done"
|
|
2733
|
+
].filter(Boolean);
|
|
2734
|
+
return { text: `Recorded: ${bits.join(" · ")}. Projects shows it beside the estimate.` };
|
|
2735
|
+
}
|
|
2736
|
+
async function buddyTool(ctx, args) {
|
|
2737
|
+
if (!ctx.client)
|
|
2738
|
+
return NOT_SET_UP;
|
|
2739
|
+
const email = str2(args.email)?.trim();
|
|
2740
|
+
if (!email) {
|
|
2741
|
+
const r2 = await ctx.client.get("/proxy/buddy");
|
|
2742
|
+
if (!r2)
|
|
2743
|
+
return UNREACHABLE;
|
|
2744
|
+
const list = Array.isArray(r2.buddies) ? r2.buddies : [];
|
|
2745
|
+
if (list.length === 0)
|
|
2746
|
+
return {
|
|
2747
|
+
text: "No buddies yet. `buddy {email}` sends someone an invite; once they accept, `handoff {to: email}` reaches them in their own org."
|
|
2748
|
+
};
|
|
2749
|
+
const lines = list.map((b) => `${str2(b.email)} ${b.status === "active" ? "paired" : b.mine ? "invited · waiting for them" : "invited you · accept from the email"}`);
|
|
2750
|
+
return { text: `Buddies:
|
|
2751
|
+
${lines.join(`
|
|
2752
|
+
`)}` };
|
|
2753
|
+
}
|
|
2754
|
+
if (args.remove === true) {
|
|
2755
|
+
const r2 = await ctx.client.postFull("/proxy/buddy", { email, remove: true });
|
|
2756
|
+
if (!r2)
|
|
2757
|
+
return UNREACHABLE;
|
|
2758
|
+
return { text: r2.json?.ok ? `Unpaired from ${email}.` : `${email} was not a buddy.` };
|
|
2759
|
+
}
|
|
2760
|
+
const r = await ctx.client.postFull("/proxy/buddy", { email });
|
|
2761
|
+
if (!r)
|
|
2762
|
+
return UNREACHABLE;
|
|
2763
|
+
if (r.status !== 200) {
|
|
2764
|
+
const reason = str2(r.json?.reason);
|
|
2765
|
+
const why = reason === "teammate" ? `${email} is already in your org — hand off to them directly.` : reason === "self" ? "That is your own address." : reason === "bad_email" ? "That does not look like an email." : errorMessage(r.json) ?? `HTTP ${r.status}`;
|
|
2766
|
+
return { text: `Not paired. ${why}`, isError: true };
|
|
2767
|
+
}
|
|
2768
|
+
return {
|
|
2769
|
+
text: r.json?.status === "active" ? `Already paired with ${email}.` : `Invite sent to ${email}. When they click Accept, hand off with \`handoff {to: "${email}"}\` — it shows in their Waiting for you and runs on their seat.`
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
async function callTool(name, args, ctx) {
|
|
2773
|
+
switch (name) {
|
|
2774
|
+
case "usage":
|
|
2775
|
+
return usageTool(ctx);
|
|
2776
|
+
case "estimate":
|
|
2777
|
+
return estimateTool(ctx, args);
|
|
2778
|
+
case "overnight":
|
|
2779
|
+
return overnightTool(ctx, args);
|
|
2780
|
+
case "jobs":
|
|
2781
|
+
return jobsTool(ctx);
|
|
2782
|
+
case "handoffs":
|
|
2783
|
+
return handoffsTool(ctx);
|
|
2784
|
+
case "handoff":
|
|
2785
|
+
return handoffTool(ctx, args);
|
|
2786
|
+
case "pickup":
|
|
2787
|
+
return pickupTool(ctx, args);
|
|
2788
|
+
case "defer":
|
|
2789
|
+
return deferTool(ctx, args);
|
|
2790
|
+
case "actual":
|
|
2791
|
+
return actualTool(ctx, args);
|
|
2792
|
+
case "buddy":
|
|
2793
|
+
return buddyTool(ctx, args);
|
|
2794
|
+
case "withdraw":
|
|
2795
|
+
return withdrawTool(ctx, args);
|
|
2796
|
+
default:
|
|
2797
|
+
return { text: `Unknown tool: ${name}`, isError: true };
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
var response = (id, result) => JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
2801
|
+
var errorResponse = (id, code, message) => JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
2802
|
+
async function handleLine(line, ctx) {
|
|
2803
|
+
const trimmed = line.trim();
|
|
2804
|
+
if (trimmed.length === 0)
|
|
2805
|
+
return null;
|
|
2806
|
+
let msg;
|
|
2807
|
+
try {
|
|
2808
|
+
msg = JSON.parse(trimmed);
|
|
2809
|
+
} catch {
|
|
2810
|
+
return errorResponse(null, -32700, "parse error");
|
|
2811
|
+
}
|
|
2812
|
+
if (!isObject7(msg))
|
|
2813
|
+
return errorResponse(null, -32600, "invalid request");
|
|
2814
|
+
const id = typeof msg.id === "string" || typeof msg.id === "number" ? msg.id : null;
|
|
2815
|
+
const method = str2(msg.method);
|
|
2816
|
+
const isNotification = msg.id === undefined;
|
|
2817
|
+
if (!method)
|
|
2818
|
+
return isNotification ? null : errorResponse(id, -32600, "invalid request");
|
|
2819
|
+
if (method.startsWith("notifications/"))
|
|
2820
|
+
return null;
|
|
2821
|
+
const params = isObject7(msg.params) ? msg.params : {};
|
|
2822
|
+
switch (method) {
|
|
2823
|
+
case "initialize": {
|
|
2824
|
+
const requested = str2(params.protocolVersion);
|
|
2825
|
+
return response(id, {
|
|
2826
|
+
protocolVersion: requested ?? PROTOCOL_VERSION,
|
|
2827
|
+
capabilities: { tools: {} },
|
|
2828
|
+
serverInfo: SERVER_INFO
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
case "ping":
|
|
2832
|
+
return response(id, {});
|
|
2833
|
+
case "tools/list":
|
|
2834
|
+
return response(id, { tools: TOOLS });
|
|
2835
|
+
case "tools/call": {
|
|
2836
|
+
const name = str2(params.name) ?? "";
|
|
2837
|
+
const args = isObject7(params.arguments) ? params.arguments : {};
|
|
2838
|
+
const r = await callTool(name, args, ctx);
|
|
2839
|
+
return response(id, {
|
|
2840
|
+
content: [{ type: "text", text: r.text }],
|
|
2841
|
+
...r.isError ? { isError: true } : {}
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
default:
|
|
2845
|
+
return isNotification ? null : errorResponse(id, -32601, `method not found: ${method}`);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// src/mcp.ts
|
|
2850
|
+
init_meter();
|
|
2851
|
+
async function usageTool2(deps) {
|
|
2852
|
+
if (!meterSource(deps.home))
|
|
2853
|
+
return { text: "SLEDS is not set up on this machine. Run `sleds init` first.", isError: true };
|
|
2854
|
+
const now = (deps.now ?? Date.now)();
|
|
2855
|
+
const m = await loadMeter({
|
|
2856
|
+
home: deps.home,
|
|
2857
|
+
now,
|
|
2858
|
+
fetchFn: deps.fetchFn,
|
|
2859
|
+
timeoutMs: deps.timeoutMs
|
|
2860
|
+
});
|
|
2861
|
+
const frag = formatSeatFragment(readSeatState(deps.home).window ?? null);
|
|
2862
|
+
if (!m) {
|
|
2863
|
+
return {
|
|
2864
|
+
text: frag ? `sleds offline (proxy did not answer) · ${frag}` : "sleds offline (proxy did not answer).",
|
|
2865
|
+
isError: true
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
const lines = formatMeter(m, now);
|
|
2869
|
+
if (frag)
|
|
2870
|
+
lines.push(`${"Limits:".padEnd(12)}${frag.replace(/^seat /, "")}`);
|
|
2871
|
+
return { text: lines.join(`
|
|
2872
|
+
`) };
|
|
2542
2873
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
const
|
|
2874
|
+
function contextFor(deps) {
|
|
2875
|
+
const src = meterSource(deps.home);
|
|
2876
|
+
const client = src ? fetchClient({ ...src, fetchFn: deps.fetchFn, timeoutMs: deps.timeoutMs }) : null;
|
|
2877
|
+
return { client, now: deps.now, usage: () => usageTool2(deps) };
|
|
2878
|
+
}
|
|
2879
|
+
var callTool2 = (name, args, deps) => callTool(name, args, contextFor(deps));
|
|
2880
|
+
var handleLine2 = (line, deps) => handleLine(line, contextFor(deps));
|
|
2881
|
+
|
|
2882
|
+
// src/commands/handoffs.ts
|
|
2883
|
+
var handoffsCmd = async (o) => {
|
|
2884
|
+
const r = await callTool2("handoffs", {}, { home: o.home });
|
|
2885
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
2886
|
+
return r.isError ? 1 : 0;
|
|
2887
|
+
};
|
|
2888
|
+
var pickupCmd = async (o) => {
|
|
2889
|
+
const id = o.argv[0];
|
|
2890
|
+
if (!id) {
|
|
2891
|
+
(o.error ?? console.error)("usage: sleds pickup <handoffId> (ids from `sleds handoffs`)");
|
|
2892
|
+
return 2;
|
|
2893
|
+
}
|
|
2894
|
+
const r = await callTool2("pickup", { handoffId: id }, { home: o.home });
|
|
2895
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
2896
|
+
return r.isError ? 1 : 0;
|
|
2897
|
+
};
|
|
2898
|
+
var withdrawCmd = async (o) => {
|
|
2899
|
+
const id = o.argv[0];
|
|
2900
|
+
if (!id) {
|
|
2901
|
+
(o.error ?? console.error)("usage: sleds withdraw <handoffId> (one you parked)");
|
|
2902
|
+
return 2;
|
|
2903
|
+
}
|
|
2904
|
+
const r = await callTool2("withdraw", { handoffId: id }, { home: o.home });
|
|
2905
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
2906
|
+
return r.isError ? 1 : 0;
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
// src/commands/hooks.ts
|
|
2910
|
+
init_settings();
|
|
2911
|
+
var hooks = (o) => {
|
|
2912
|
+
const log = o.log ?? console.log;
|
|
2913
|
+
const error = o.error ?? console.error;
|
|
2914
|
+
if (!hasClaudeCode(o.home)) {
|
|
2915
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
2916
|
+
return 1;
|
|
2917
|
+
}
|
|
2918
|
+
const settings = readSettings(o.home);
|
|
2919
|
+
const had = hasSeatHook(settings);
|
|
2920
|
+
const hadSpec = hasSpecHook(settings);
|
|
2921
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
2922
|
+
const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
|
|
2923
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
2924
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
2925
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
2926
|
+
if (hadLine)
|
|
2927
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
2928
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
2929
|
+
return 0;
|
|
2930
|
+
};
|
|
2931
|
+
|
|
2932
|
+
// src/commands/init.ts
|
|
2933
|
+
init_config();
|
|
2934
|
+
init_settings();
|
|
2935
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2936
|
+
import { createInterface } from "node:readline";
|
|
2554
2937
|
|
|
2938
|
+
// src/skill.ts
|
|
2939
|
+
var SKILL_MD = `---
|
|
2940
|
+
name: sled
|
|
2941
|
+
description: Hand work to SLEDS — continue past a rate limit or credit wall on the org's key, hold until reset, or run overnight at half price. Use when the user says /sled, hits a usage limit, mentions their window reset, asks what ran overnight, or asks to defer or schedule agent work.
|
|
2555
2942
|
---
|
|
2556
2943
|
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2944
|
+
# SLEDS
|
|
2945
|
+
|
|
2946
|
+
Nine to five is 40 of 168 hours. SLEDS puts the other 128 to work: it carries a session past a wall, and it runs whole tasks overnight with the Musher, a night-shift orchestrator that answers the questions an agent would otherwise stop on. Nothing changes until a wall or a \`/sled\`.
|
|
2947
|
+
|
|
2948
|
+
## Hit a limit on your seat
|
|
2949
|
+
|
|
2950
|
+
A Team or Max seat caps on Anthropic's side ("You've hit your limit…"). SLEDS is what you switch to. When the user reports a limit, a cap, or that Claude stopped for the day, tell them exactly this:
|
|
2951
|
+
|
|
2952
|
+
\`\`\`
|
|
2953
|
+
sleds on --until-reset +5h
|
|
2954
|
+
\`\`\`
|
|
2955
|
+
|
|
2956
|
+
then restart \`claude\`. Claude Code runs on the platform key through the proxy; the seat is untouched. \`--until-reset\` takes \`+5h\`, \`+90m\`, or an ISO time; \`--reason "<text>"\` notes why. The switch reverts itself at the deadline where a scheduler exists.
|
|
2957
|
+
|
|
2958
|
+
## At the wall
|
|
2959
|
+
|
|
2960
|
+
When the proxy is active it answers a walled request with its options. The user picks one by number:
|
|
2961
|
+
|
|
2962
|
+
\`\`\`
|
|
2963
|
+
/sled 1 continue now on the platform key
|
|
2964
|
+
/sled 2 hold; resume when the window resets
|
|
2965
|
+
/sled 3 run overnight at half price
|
|
2966
|
+
/sled 4 switch to a cheaper model for the rest of the session (offered when one saves ≥15%)
|
|
2967
|
+
/sled 5 hand off to an idle coworker, or park for the team (offered on a team of two or more)
|
|
2968
|
+
/sled 6 defer to reset: the job runs itself when the seat resets — same model, done in minutes, full price (offered when the wall has a reset and the org has a key)
|
|
2969
|
+
\`\`\`
|
|
2970
|
+
|
|
2971
|
+
No key (Team plan, seat-first)? The musher still runs: \`sleds defer\` in a terminal schedules this Mac to resume the current session itself at the reset — \`claude --resume\` on the seat's own quota, the model pinned. \`sleds musher --status\` lists what is waiting; \`sleds run <jobId>\` runs one now; the Mac must be awake at the reset (\`sleds doctor\` checks). When the user says "defer this" or "pick it up at the reset" and the org has no key, tell them to run \`sleds defer\`.
|
|
2972
|
+
|
|
2973
|
+
A hand-off carries a brief (the packet): what, note, cost so far, how to pick it up. The \`handoffs\` MCP tool and the dashboard's Jobs page show it; read it before taking one.
|
|
2974
|
+
|
|
2975
|
+
## Window reset
|
|
2976
|
+
|
|
2977
|
+
When the user mentions their window has reset, or the deadline shown by \`sleds status\` has passed:
|
|
2978
|
+
|
|
2979
|
+
\`\`\`
|
|
2980
|
+
sleds off
|
|
2981
|
+
\`\`\`
|
|
2982
|
+
|
|
2983
|
+
then restart \`claude\`. Settings go back to exactly what they were.
|
|
2984
|
+
|
|
2985
|
+
## Check
|
|
2986
|
+
|
|
2987
|
+
\`sleds status\` — on/off, since, reverts at, proxy health, wall mode, and the org's shape (\`Org:\` / \`Wiring:\`). An org with no platform key runs seat-first and its walls offer hold (\`/sled 2\`), \`sleds defer\` (this Mac resumes at the reset) and hand-off (\`/sled 5\`) — continue, overnight and switch need a key an admin adds on the dashboard.
|
|
2988
|
+
|
|
2989
|
+
The status line's \`seat wk 52% · 5h 12%\` is the user's own Pro/Max window as Claude Code reports it (weekly, then 5-hour); \`window\` is the platform key's headroom through the proxy. At \`wk 100%\` the answer is \`sleds on --until-reset\` (platform key) or \`sleds defer\` (this Mac resumes at the reset, no key).
|
|
2990
|
+
|
|
2991
|
+
## /sled — estimate the task, offer overnight
|
|
2992
|
+
|
|
2993
|
+
Bare \`/sled\` (or \`/sled <spec>\`) means: size the current task and offer to hand it to the night shift. Do this in order.
|
|
2994
|
+
|
|
2995
|
+
1. Read the task. The spec is the argument if given, otherwise the current conversation's open task (the todo list, the last user request).
|
|
2996
|
+
2. Estimate with \`/sled estimate <spec>\` — the proxy answers with the calibrated band and cap, and the estimate is recorded so its actuals feed the next one. Quote that band, never a point and never your own guess.
|
|
2997
|
+
3. Offer the window when any of these hold: the high end of the cost band is above the org's approval threshold, the wall-clock is over 30 minutes, or the result is not needed before end of day. The offer is one line: \`Run this overnight from 18:00? Estimated $40–120 at half price.\`
|
|
2998
|
+
4. On yes, write the hand-off the Musher will work from: the spec, the acceptance test, the org conventions that apply, and every decision already made in this session as \`question -> answer\` pairs. Ask one thing before queueing: the autonomy for this job.
|
|
2999
|
+
- \`ask\`: every question pages on-call.
|
|
3000
|
+
- \`decide_and_flag\` (default): reversible defaults are taken and listed in the morning.
|
|
3001
|
+
- \`decide\`: reversible defaults are taken quietly.
|
|
3002
|
+
Irreversible questions always page, whatever the autonomy. No answer in 30 minutes parks the job and the Musher moves to the next one.
|
|
3003
|
+
5. Queue it with \`/sled overnight <spec>\` (the hand-off from step 4 is the spec). The proxy runs the real estimator (its cap bounds the job's spend), schedules the job for the org's overnight window, and snapshots this session so the batch runner has the context. Confirm with the job id and the band from the reply — not your own guess.
|
|
3004
|
+
|
|
3005
|
+
## /sled review — the morning queue
|
|
3006
|
+
|
|
3007
|
+
Morning ritual. List what the night shift did, grouped:
|
|
3008
|
+
|
|
3009
|
+
- Delivered: jobs with a PR or a result, with actual cost against the estimate.
|
|
3010
|
+
- Flagged: decisions the Musher took on a \`decide_and_flag\` job, each as \`question -> answer\` with the rung that answered it (spec, convention, or default). These need a human eye, not a redo.
|
|
3011
|
+
- Parked: questions nobody answered in 30 minutes. Each shows the option set; answering resumes the job.
|
|
3012
|
+
|
|
3013
|
+
Read the Musher log for every delivered job before summarizing, and put each decision into the PR body with its reasoning. Then offer the next step for each: merge, kick back with a note, or \`/sled resume <id>\`. On Monday the review covers the weekend.
|
|
3014
|
+
|
|
3015
|
+
## /sled weekend — Friday sweep
|
|
3016
|
+
|
|
3017
|
+
Friday afternoon. Sweep the open task list into a weekend batch: everything the user has queued or labeled for the weekend becomes overnight jobs with the org's default autonomy, and the Monday review covers all of it. For now, explain that the sweep queues the current task list as overnight jobs; sweeping issues by GitHub label is a later ticket. Confirm the count and the total estimate band before queueing.
|
|
3018
|
+
|
|
3019
|
+
## /sled overnight [spec]
|
|
3020
|
+
|
|
3021
|
+
Queue a batch job without waiting for a wall. \`/sled overnight <spec>\` runs the spec; bare \`/sled overnight\` runs the session's original ask. The reply carries the band, the cap, when it runs, and \`/sled resume <id>\` for the morning. Half price on the batch tier.
|
|
3022
|
+
|
|
3023
|
+
## /sled jobs
|
|
3024
|
+
|
|
3025
|
+
List the org's jobs: queued, running, delivered, parked, with the estimate band, the actual so far, and the autonomy. Parked jobs show the unanswered question. Keep it to one line per job.
|
|
3026
|
+
|
|
3027
|
+
## /sled resume <id>
|
|
3028
|
+
|
|
3029
|
+
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.
|
|
3030
|
+
|
|
3031
|
+
## /sled sync-backlog — price the backlog
|
|
3032
|
+
|
|
3033
|
+
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.
|
|
3034
|
+
|
|
3035
|
+
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.
|
|
3036
|
+
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.
|
|
3037
|
+
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.
|
|
3038
|
+
|
|
3039
|
+
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.
|
|
3040
|
+
|
|
3041
|
+
## Voice
|
|
3042
|
+
|
|
3043
|
+
Plain sentences, sentence case, no emoji. Numbers with units and bands. Say what happens next, not what could.
|
|
3044
|
+
|
|
3045
|
+
## Overnight jobs (how it runs)
|
|
3046
|
+
|
|
3047
|
+
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:
|
|
3048
|
+
|
|
3049
|
+
- \`/sled jobs\` — this seat's jobs: id, status, route, cost, when — then the team's open hand-offs waiting for pickup (yours first).
|
|
3050
|
+
- \`/sled resume <id>\` — pull a delivered job's output into the session as the assistant's reply and keep going.
|
|
3051
|
+
|
|
3052
|
+
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.
|
|
3053
|
+
|
|
3054
|
+
## Team hand-off
|
|
3055
|
+
|
|
3056
|
+
\`/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.
|
|
3057
|
+
|
|
3058
|
+
- \`/sled pickup <id>\` — in the coworker's Claude Code session: the handed-off session resumes there on the platform key, metered to them. A task parked from Cowork (no session) starts this turn from the task text. A hand-off meant for someone else or already taken gets one line saying so.
|
|
3059
|
+
|
|
3060
|
+
- \`/sled handoffs\` — the team's open hand-offs, yours marked \`yours · for <who>\`; \`/sled withdraw <id>\` takes back one you parked before anyone picks it up.
|
|
3061
|
+
|
|
3062
|
+
From Cowork or any MCP client (\`sleds mcp\`): the \`handoffs\` tool lists what is waiting, \`handoff\` parks the current task for a teammate (give the task in full — the picker starts from that text), \`pickup\` takes one and returns its context to continue there, and \`withdraw\` takes back one of yours. From a plain terminal (a Codex seat, or Claude Code closed): \`sleds handoffs\`, \`sleds pickup <id>\`, \`sleds withdraw <id>\` — the same words.
|
|
3063
|
+
|
|
3064
|
+
|
|
3065
|
+
## Buddies (spec 04 §4)
|
|
3066
|
+
|
|
3067
|
+
A buddy is one person outside your org you can hand work to and from. Pair once (MCP \`buddy {email}\` — they click Accept in the mail); then \`handoff {task, to: "<their email>"}\` lands in their Waiting for you, in whatever org their seat is, and \`pickup\` there runs it on their seat, metered to them. Only the packet crosses — never keys, usage or membership. \`buddy\` with no email lists your pairs; \`buddy {email, remove: true}\` unpairs. A teammate is never a buddy: hand off to them directly.
|
|
3068
|
+
`;
|
|
3069
|
+
|
|
3070
|
+
// src/commands/hooks.ts
|
|
3071
|
+
init_settings();
|
|
3072
|
+
var hooks2 = (o) => {
|
|
3073
|
+
const log = o.log ?? console.log;
|
|
3074
|
+
const error = o.error ?? console.error;
|
|
3075
|
+
if (!hasClaudeCode(o.home)) {
|
|
3076
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
3077
|
+
return 1;
|
|
3078
|
+
}
|
|
3079
|
+
const settings = readSettings(o.home);
|
|
3080
|
+
const had = hasSeatHook(settings);
|
|
3081
|
+
const hadSpec = hasSpecHook(settings);
|
|
3082
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
3083
|
+
const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
|
|
3084
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
3085
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
3086
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
3087
|
+
if (hadLine)
|
|
3088
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
3089
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
3090
|
+
return 0;
|
|
3091
|
+
};
|
|
3092
|
+
|
|
3093
|
+
// src/commands/init.ts
|
|
3094
|
+
var DEFAULT_PROXY_URL = "http://localhost:8787";
|
|
3095
|
+
var promptStdin = (question) => new Promise((resolve4) => {
|
|
3096
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3097
|
+
rl.question(question, (answer) => {
|
|
3098
|
+
rl.close();
|
|
3099
|
+
resolve4(answer.trim());
|
|
2576
3100
|
});
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
3101
|
+
});
|
|
3102
|
+
async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
|
|
3103
|
+
return signupRequest(proxyUrl, { email, apiKey: credential }, fetchFn);
|
|
3104
|
+
}
|
|
3105
|
+
async function signupTeam(proxyUrl, email, team, credential, fetchFn = fetch) {
|
|
3106
|
+
return signupRequest(proxyUrl, { email, team, ...credential ? { apiKey: credential } : {} }, fetchFn);
|
|
3107
|
+
}
|
|
3108
|
+
async function signupRequest(proxyUrl, payload, fetchFn) {
|
|
3109
|
+
try {
|
|
3110
|
+
const res = await fetchFn(`${proxyUrl}/signup`, {
|
|
3111
|
+
method: "POST",
|
|
3112
|
+
headers: { "content-type": "application/json" },
|
|
3113
|
+
body: JSON.stringify(payload),
|
|
3114
|
+
signal: AbortSignal.timeout(20000)
|
|
3115
|
+
});
|
|
3116
|
+
const body = await res.json().catch(() => ({}));
|
|
3117
|
+
if (res.ok && typeof body.token === "string")
|
|
3118
|
+
return {
|
|
3119
|
+
ok: true,
|
|
3120
|
+
token: body.token,
|
|
3121
|
+
created: body.created === true,
|
|
3122
|
+
verifyPending: body.verifyPending === true
|
|
3123
|
+
};
|
|
2582
3124
|
return {
|
|
2583
|
-
|
|
2584
|
-
|
|
3125
|
+
ok: false,
|
|
3126
|
+
reason: typeof body.reason === "string" ? body.reason : `http_${res.status}`,
|
|
3127
|
+
status: res.status
|
|
2585
3128
|
};
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
};
|
|
3129
|
+
} catch {
|
|
3130
|
+
return { ok: false, reason: "unreachable", status: 0 };
|
|
3131
|
+
}
|
|
2590
3132
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3133
|
+
var SIGNUP_REASONS = {
|
|
3134
|
+
email: "that does not look like an email address.",
|
|
3135
|
+
key: "that does not look like an Anthropic API key (sk-ant-…).",
|
|
3136
|
+
key_rejected: "Anthropic did not accept that API key. Check it at console.anthropic.com → API keys.",
|
|
3137
|
+
rate_limited: "too many signups from this network in the last hour. Try again later.",
|
|
3138
|
+
not_configured: "the proxy is not set up for self-serve signup yet.",
|
|
3139
|
+
team: "the team needs a name (2–80 characters).",
|
|
3140
|
+
personal_email: "a team needs a work email — that domain is a personal mailbox. Sign up as an individual instead, or use your company address.",
|
|
3141
|
+
org_exists: "someone at your company already set up SLEDS. Ask them for a seat (Members → invite, Admin → Mint a seat) and run `sleds init --token …`.",
|
|
3142
|
+
unavailable: "the proxy could not reach its control plane. Try again in a moment.",
|
|
3143
|
+
unreachable: "could not reach the proxy."
|
|
3144
|
+
};
|
|
3145
|
+
function signupFailureText(r) {
|
|
3146
|
+
return `Signup failed: ${SIGNUP_REASONS[r.reason] ?? `unexpected answer (${r.reason}, HTTP ${r.status}).`}`;
|
|
3147
|
+
}
|
|
3148
|
+
function normalizeProxyUrl(raw) {
|
|
3149
|
+
let t = raw.trim();
|
|
3150
|
+
if (!t)
|
|
3151
|
+
return null;
|
|
3152
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(t))
|
|
3153
|
+
t = `https://${t}`;
|
|
3154
|
+
t = t.replace(/[./]+$/, "");
|
|
3155
|
+
try {
|
|
3156
|
+
const u = new URL(t);
|
|
3157
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
3158
|
+
return null;
|
|
3159
|
+
if (!u.hostname)
|
|
3160
|
+
return null;
|
|
3161
|
+
u.hostname = u.hostname.replace(/\.+$/, "");
|
|
3162
|
+
return `${u.protocol}//${u.host}${u.pathname === "/" ? "" : u.pathname.replace(/\/+$/, "")}`;
|
|
3163
|
+
} catch {
|
|
3164
|
+
return null;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
async function fetchSeatInfo(url, token, fetchFn = fetch) {
|
|
3168
|
+
try {
|
|
3169
|
+
const res = await fetchFn(`${url}/proxy/meter`, {
|
|
3170
|
+
headers: { authorization: `Bearer ${token}` },
|
|
3171
|
+
signal: AbortSignal.timeout(1e4)
|
|
3172
|
+
});
|
|
3173
|
+
if (!res.ok)
|
|
3174
|
+
return { org: null, email: null };
|
|
3175
|
+
const body = await res.json();
|
|
2601
3176
|
return {
|
|
2602
|
-
|
|
2603
|
-
|
|
3177
|
+
org: body.org?.capabilities ? body.org : null,
|
|
3178
|
+
email: typeof body.seat?.email === "string" ? body.seat.email : null
|
|
2604
3179
|
};
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
const head = `Picked up from ${from}.${note ? ` Where it stands: ${note}` : ""}`;
|
|
2609
|
-
if (j.kind === "task")
|
|
2610
|
-
return { text: `${head}
|
|
2611
|
-
|
|
2612
|
-
The task:
|
|
2613
|
-
${str2(j.task) ?? ""}
|
|
2614
|
-
|
|
2615
|
-
Carry it through from here.` };
|
|
2616
|
-
const first = str2(j.firstUserText) ?? "";
|
|
2617
|
-
const last = str2(j.lastUserText) ?? "";
|
|
2618
|
-
const n = num4(j.messageCount);
|
|
2619
|
-
return {
|
|
2620
|
-
text: [
|
|
2621
|
-
`${head} This was a Claude Code session${n !== undefined ? ` (${n} messages)` : ""} that hit a wall; its snapshot can also be replayed in Claude Code with /sled pickup ${handoffId} — but it is now marked picked by you.`,
|
|
2622
|
-
"",
|
|
2623
|
-
`It started with:
|
|
2624
|
-
${first}`,
|
|
2625
|
-
...last && last !== first ? ["", `The last ask was:
|
|
2626
|
-
${last}`] : [],
|
|
2627
|
-
"",
|
|
2628
|
-
"Continue the work from here."
|
|
2629
|
-
].join(`
|
|
2630
|
-
`)
|
|
2631
|
-
};
|
|
2632
|
-
}
|
|
2633
|
-
function parseAt2(v, now) {
|
|
2634
|
-
const rel = /^\+(\d+)([hm])$/.exec(v.trim());
|
|
2635
|
-
if (rel)
|
|
2636
|
-
return now + Number.parseInt(rel[1] ?? "0", 10) * (rel[2] === "h" ? 3600000 : 60000);
|
|
2637
|
-
const t = Date.parse(v);
|
|
2638
|
-
return Number.isFinite(t) ? t : undefined;
|
|
3180
|
+
} catch {
|
|
3181
|
+
return { org: null, email: null };
|
|
3182
|
+
}
|
|
2639
3183
|
}
|
|
2640
|
-
async function
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
host: "mcp"
|
|
2652
|
-
});
|
|
2653
|
-
if (!r)
|
|
2654
|
-
return { text: `Nothing was deferred. ${UNREACHABLE.text}`, isError: true };
|
|
2655
|
-
const jobId = str2(r.jobId) ?? "unknown";
|
|
2656
|
-
const when = num4(r.scheduledAt);
|
|
2657
|
-
const startsIn = when !== undefined && when > now + 60000 ? `starts in ${shortDuration(when - now)}` : "starts at the next musher tick (within 5 min)";
|
|
2658
|
-
return {
|
|
2659
|
-
text: `Deferred. Job ${jobId} ${startsIn}. With no org key it runs on the seat's own Mac (sleds musher must be installed there — sleds doctor checks); with a key, in the cloud. Watch it with the jobs tool or /sled jobs.${str2(args.task) ? ` Note kept: ${str2(args.task)}` : ""}`
|
|
2660
|
-
};
|
|
3184
|
+
async function probeProxy(url, fetchFn = fetch, opts = {}) {
|
|
3185
|
+
const tries = opts.tries ?? 2;
|
|
3186
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
3187
|
+
for (let i = 0;i < tries; i++) {
|
|
3188
|
+
try {
|
|
3189
|
+
const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
3190
|
+
if (res.ok)
|
|
3191
|
+
return true;
|
|
3192
|
+
} catch {}
|
|
3193
|
+
}
|
|
3194
|
+
return false;
|
|
2661
3195
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
3196
|
+
var resolveToken = async (o) => {
|
|
3197
|
+
const env = o.env ?? process.env;
|
|
3198
|
+
if (o.token)
|
|
3199
|
+
return { token: o.token.trim(), source: "flag" };
|
|
3200
|
+
if (env.SLEDS_TOKEN)
|
|
3201
|
+
return { token: env.SLEDS_TOKEN.trim(), source: "env" };
|
|
3202
|
+
const fromFile = readTokenFile(o.home);
|
|
3203
|
+
if (fromFile)
|
|
3204
|
+
return { token: fromFile, source: "file" };
|
|
3205
|
+
if (o.nonInteractive || !o.prompt)
|
|
3206
|
+
return null;
|
|
3207
|
+
if (!o.signup) {
|
|
3208
|
+
const typed = (await o.prompt("Paste your SLEDS seat token (sled_…), or press enter to sign up as an individual: ")).trim();
|
|
3209
|
+
if (typed)
|
|
3210
|
+
return { token: typed, source: "prompt" };
|
|
3211
|
+
}
|
|
3212
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
3213
|
+
if (!url)
|
|
3214
|
+
return null;
|
|
3215
|
+
const log = o.log ?? console.log;
|
|
3216
|
+
const err = o.error ?? console.error;
|
|
3217
|
+
let team = o.team?.trim() || null;
|
|
3218
|
+
if (team === null && !o.email) {
|
|
3219
|
+
const which = (await o.prompt("Set up SLEDS for (1) just you, or (2) your team? [1/2]: ")).trim();
|
|
3220
|
+
if (which === "2") {
|
|
3221
|
+
team = (await o.prompt("Team name (e.g. your company): ")).trim();
|
|
3222
|
+
if (!team)
|
|
3223
|
+
return null;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
const email = (o.email ?? await o.prompt(team ? "Your work email: " : "Your email: ")).trim();
|
|
3227
|
+
if (!email.includes("@"))
|
|
3228
|
+
return null;
|
|
3229
|
+
if (team) {
|
|
3230
|
+
const credential2 = (await o.prompt("Your organization's Anthropic API key, if you have one (sk-ant-…; press enter to skip — a Team plan without one still works, walls offer hold and hand-off): ")).trim();
|
|
3231
|
+
const r2 = await signupTeam(url, email, team, credential2 || null, o.fetchFn);
|
|
3232
|
+
if (!r2.ok) {
|
|
3233
|
+
err(signupFailureText(r2));
|
|
3234
|
+
return null;
|
|
3235
|
+
}
|
|
3236
|
+
log(`Created ${team} with you as owner (${email}).${credential2 ? " Your platform key is stored encrypted as the org's upstream key." : " No platform key yet — add one on the dashboard (Admin → Keys) to unlock continue / overnight / switch at a wall."}`);
|
|
3237
|
+
if (r2.verifyPending)
|
|
3238
|
+
log(`Check ${email} for a verification link and click it — inviting members unlocks then (an unverified team is removed after 48 h; your own seat works meanwhile).`);
|
|
3239
|
+
log("Invite your team on the dashboard (Members → invite by email, Admin → Mint a seat); each person runs `sleds init --token …`.");
|
|
3240
|
+
return { token: r2.token, source: "signup" };
|
|
3241
|
+
}
|
|
3242
|
+
const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
|
|
3243
|
+
if (!credential)
|
|
3244
|
+
return null;
|
|
3245
|
+
const r = await signupIndividual(url, email, credential, o.fetchFn);
|
|
3246
|
+
if (!r.ok) {
|
|
3247
|
+
err(signupFailureText(r));
|
|
3248
|
+
return null;
|
|
3249
|
+
}
|
|
3250
|
+
log(r.created ? `Created your individual seat (${email}). Your key is stored encrypted and is the only key this seat spends.` : `Re-issued your individual seat (${email}); the previous token is revoked.`);
|
|
3251
|
+
return { token: r.token, source: "signup" };
|
|
3252
|
+
};
|
|
3253
|
+
var init = async (o) => {
|
|
3254
|
+
const log = o.log ?? console.log;
|
|
3255
|
+
const error = o.error ?? console.error;
|
|
3256
|
+
const env = o.env ?? process.env;
|
|
3257
|
+
if (!hasClaudeCode(o.home)) {
|
|
3258
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
3259
|
+
return 1;
|
|
3260
|
+
}
|
|
3261
|
+
const resolved = await resolveToken(o);
|
|
3262
|
+
if (!resolved) {
|
|
3263
|
+
error(o.signup ? "Signup did not complete. Nothing was changed." : "No seat token. Pass --token, set SLEDS_TOKEN, run without --non-interactive, or use --signup.");
|
|
3264
|
+
return 2;
|
|
3265
|
+
}
|
|
3266
|
+
if (!isSeatToken(resolved.token)) {
|
|
3267
|
+
error("Invalid seat token: expected something like sled_xxxxxxxx.");
|
|
3268
|
+
return 2;
|
|
3269
|
+
}
|
|
3270
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
3271
|
+
if (!url) {
|
|
3272
|
+
error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
|
|
3273
|
+
return 2;
|
|
3274
|
+
}
|
|
3275
|
+
if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
|
|
3276
|
+
error(`No SLEDS proxy answered at ${url}/healthz. Check the URL (or pass --no-probe to save it anyway).`);
|
|
3277
|
+
return 2;
|
|
3278
|
+
}
|
|
3279
|
+
const wrote = [];
|
|
3280
|
+
const seatInfo = await fetchSeatInfo(url, resolved.token, o.fetchFn);
|
|
3281
|
+
const org = seatInfo.org;
|
|
3282
|
+
const wiring = o.proxyAlways ? "proxy-always" : o.seatFirst ? "seat-first" : org?.mode ?? "proxy-always";
|
|
3283
|
+
if (wiring === "proxy-always" && org && !org.capabilities.upstreamKey) {
|
|
3284
|
+
error(`${org.name} has no platform key for the proxy to forward with; --proxy-always would break every request. Using seat-first.`);
|
|
3285
|
+
}
|
|
3286
|
+
const finalWiring = wiring === "proxy-always" && org && !org.capabilities.upstreamKey ? "seat-first" : wiring;
|
|
3287
|
+
if (backupSettings(o.home))
|
|
3288
|
+
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
3289
|
+
const current = readSettings(o.home);
|
|
3290
|
+
const wired = finalWiring === "proxy-always" ? applyProxy(current, { url, token: resolved.token }) : removeProxy(current);
|
|
3291
|
+
const statusLine = applyStatusLine(wired, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
|
|
3292
|
+
writeSettings(o.home, statusLine.settings);
|
|
3293
|
+
wrote.push(finalWiring === "proxy-always" ? `set env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN in ${settingsPath(o.home)} (everything through the proxy)` : `left Claude Code on your seat (no ANTHROPIC_BASE_URL in ${settingsPath(o.home)}); \`sleds on\` switches to the proxy at a wall`);
|
|
3294
|
+
wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
|
|
3295
|
+
hooks2({
|
|
3296
|
+
home: o.home,
|
|
3297
|
+
env: o.env,
|
|
3298
|
+
refreshStatusLine: false,
|
|
3299
|
+
seatEventCommand: o.seatEventCommand,
|
|
3300
|
+
specEventCommand: o.specEventCommand,
|
|
3301
|
+
log: (line) => wrote.push(line),
|
|
3302
|
+
error
|
|
3303
|
+
});
|
|
3304
|
+
if (resolved.source !== "file") {
|
|
3305
|
+
writeTokenFile(o.home, resolved.token);
|
|
3306
|
+
wrote.push(`saved token to ${tokenPath(o.home)} (0600)`);
|
|
2682
3307
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
}
|
|
2694
|
-
|
|
3308
|
+
const prior = readConfig(o.home);
|
|
3309
|
+
writeConfig(o.home, {
|
|
3310
|
+
...prior,
|
|
3311
|
+
token: resolved.token,
|
|
3312
|
+
proxyUrl: url,
|
|
3313
|
+
wiring: finalWiring,
|
|
3314
|
+
agentKind: "claude-code",
|
|
3315
|
+
...seatInfo.email ? { email: seatInfo.email } : {},
|
|
3316
|
+
...org ? { org } : {},
|
|
3317
|
+
...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
|
|
3318
|
+
});
|
|
3319
|
+
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
3320
|
+
mkdirSync7(skillDir(o.home), { recursive: true });
|
|
3321
|
+
writeFileSync7(skillPath(o.home), SKILL_MD);
|
|
3322
|
+
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
3323
|
+
if (o.installMusher) {
|
|
3324
|
+
const r = o.installMusher();
|
|
3325
|
+
if (r)
|
|
3326
|
+
wrote.push(r);
|
|
2695
3327
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
case "tools/list":
|
|
2718
|
-
return response(id, { tools: TOOLS });
|
|
2719
|
-
case "tools/call": {
|
|
2720
|
-
const name = str2(params.name) ?? "";
|
|
2721
|
-
const args = isObject7(params.arguments) ? params.arguments : {};
|
|
2722
|
-
const r = await callTool(name, args, ctx);
|
|
2723
|
-
return response(id, {
|
|
2724
|
-
content: [{ type: "text", text: r.text }],
|
|
2725
|
-
...r.isError ? { isError: true } : {}
|
|
2726
|
-
});
|
|
3328
|
+
const orgLine = org ? `${org.name} · ${org.plan}${org.capabilities.upstreamKey ? ` · ${org.capabilities.upstreamSource === "own" ? "platform key" : "shared key"}` : " · no platform key"}` : "org unknown (proxy did not answer /proxy/meter)";
|
|
3329
|
+
if (finalWiring === "proxy-always") {
|
|
3330
|
+
log(`Claude Code now runs through SLEDS on the platform key. (${orgLine})`);
|
|
3331
|
+
log("");
|
|
3332
|
+
for (const w of wrote)
|
|
3333
|
+
log(` ${w}`);
|
|
3334
|
+
log("");
|
|
3335
|
+
log(`Every request goes to ${url} and is metered to this seat. Run \`sleds status\` to check.`);
|
|
3336
|
+
log("The status line shows your meter: spend today, seat limits, window, allowance, queued jobs.");
|
|
3337
|
+
log("Prefer your seat first? `sleds init --seat-first` leaves Claude Code on it and switches only at a wall.");
|
|
3338
|
+
} else {
|
|
3339
|
+
log(`SLEDS is watching your seat. Nothing changes until a wall. (${orgLine})`);
|
|
3340
|
+
log("");
|
|
3341
|
+
for (const w of wrote)
|
|
3342
|
+
log(` ${w}`);
|
|
3343
|
+
log("");
|
|
3344
|
+
log("Claude Code keeps running on your seat; the status line reports its quota to SLEDS on every turn.");
|
|
3345
|
+
if (org && !org.capabilities.upstreamKey) {
|
|
3346
|
+
log("This org has no platform key, so at a wall the choices are hold until reset, `sleds defer` (this Mac resumes the session itself at the reset), or hand off to a teammate. An admin can add a key on the dashboard to unlock continue / overnight / switch.");
|
|
3347
|
+
} else {
|
|
3348
|
+
log("At a wall: `sleds on --until-reset +5h` (or /sled 1) switches to the platform key through the proxy until the reset; `sleds off` puts you back.");
|
|
2727
3349
|
}
|
|
2728
|
-
default:
|
|
2729
|
-
return isNotification ? null : errorResponse(id, -32601, `method not found: ${method}`);
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
|
|
2733
|
-
// src/mcp.ts
|
|
2734
|
-
init_meter();
|
|
2735
|
-
async function usageTool2(deps) {
|
|
2736
|
-
if (!meterSource(deps.home))
|
|
2737
|
-
return { text: "SLEDS is not set up on this machine. Run `sleds init` first.", isError: true };
|
|
2738
|
-
const now = (deps.now ?? Date.now)();
|
|
2739
|
-
const m = await loadMeter({
|
|
2740
|
-
home: deps.home,
|
|
2741
|
-
now,
|
|
2742
|
-
fetchFn: deps.fetchFn,
|
|
2743
|
-
timeoutMs: deps.timeoutMs
|
|
2744
|
-
});
|
|
2745
|
-
const frag = formatSeatFragment(readSeatState(deps.home).window ?? null);
|
|
2746
|
-
if (!m) {
|
|
2747
|
-
return {
|
|
2748
|
-
text: frag ? `sleds offline (proxy did not answer) · ${frag}` : "sleds offline (proxy did not answer).",
|
|
2749
|
-
isError: true
|
|
2750
|
-
};
|
|
2751
3350
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
lines.push(`${"Limits:".padEnd(12)}${frag.replace(/^seat /, "")}`);
|
|
2755
|
-
return { text: lines.join(`
|
|
2756
|
-
`) };
|
|
2757
|
-
}
|
|
2758
|
-
function contextFor(deps) {
|
|
2759
|
-
const src = meterSource(deps.home);
|
|
2760
|
-
const client = src ? fetchClient({ ...src, fetchFn: deps.fetchFn, timeoutMs: deps.timeoutMs }) : null;
|
|
2761
|
-
return { client, now: deps.now, usage: () => usageTool2(deps) };
|
|
2762
|
-
}
|
|
2763
|
-
var handleLine2 = (line, deps) => handleLine(line, contextFor(deps));
|
|
3351
|
+
return 0;
|
|
3352
|
+
};
|
|
2764
3353
|
|
|
2765
3354
|
// src/commands/mcp.ts
|
|
2766
3355
|
var mcp = async (o) => {
|
|
@@ -2814,21 +3403,21 @@ var meter = async (o) => {
|
|
|
2814
3403
|
// src/commands/musher.ts
|
|
2815
3404
|
init_config();
|
|
2816
3405
|
init_meter();
|
|
2817
|
-
import { spawnSync as
|
|
2818
|
-
import { existsSync as
|
|
3406
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
3407
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
2819
3408
|
import { hostname as hostname2 } from "node:os";
|
|
2820
|
-
import { dirname as
|
|
3409
|
+
import { dirname as dirname4 } from "node:path";
|
|
2821
3410
|
var defaultWhich3 = (cmd) => {
|
|
2822
3411
|
const path = process.env.PATH ?? "";
|
|
2823
3412
|
for (const dir of path.split(":").filter(Boolean)) {
|
|
2824
3413
|
const p = `${dir}/${cmd}`;
|
|
2825
|
-
if (
|
|
3414
|
+
if (existsSync9(p))
|
|
2826
3415
|
return p;
|
|
2827
3416
|
}
|
|
2828
3417
|
return null;
|
|
2829
3418
|
};
|
|
2830
3419
|
var findClaude2 = (home, configured, which) => {
|
|
2831
|
-
if (configured &&
|
|
3420
|
+
if (configured && existsSync9(configured))
|
|
2832
3421
|
return configured;
|
|
2833
3422
|
const found = which("claude");
|
|
2834
3423
|
if (found)
|
|
@@ -2839,7 +3428,7 @@ var findClaude2 = (home, configured, which) => {
|
|
|
2839
3428
|
"/usr/local/bin/claude",
|
|
2840
3429
|
`${home}/.npm-global/bin/claude`
|
|
2841
3430
|
])
|
|
2842
|
-
if (
|
|
3431
|
+
if (existsSync9(p))
|
|
2843
3432
|
return p;
|
|
2844
3433
|
return null;
|
|
2845
3434
|
};
|
|
@@ -2855,8 +3444,8 @@ var musherInstall = (o) => {
|
|
|
2855
3444
|
const plist = musherPlistPath(o.home, env);
|
|
2856
3445
|
const label = musherLabel(env);
|
|
2857
3446
|
const dir = configDir(o.home, env);
|
|
2858
|
-
|
|
2859
|
-
|
|
3447
|
+
mkdirSync8(dirname4(plist), { recursive: true });
|
|
3448
|
+
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
2860
3449
|
const execPath = o.sledsArgv[0] ?? process.execPath;
|
|
2861
3450
|
const agentEnv = {
|
|
2862
3451
|
PATH: musherPath(o.home, execPath, env.PATH ?? ""),
|
|
@@ -2865,9 +3454,9 @@ var musherInstall = (o) => {
|
|
|
2865
3454
|
...env.CLAUDE_CONFIG_DIR ? { CLAUDE_CONFIG_DIR: env.CLAUDE_CONFIG_DIR } : {}
|
|
2866
3455
|
};
|
|
2867
3456
|
const exec = o.exec ?? defaultExecSync;
|
|
2868
|
-
if (
|
|
3457
|
+
if (existsSync9(plist))
|
|
2869
3458
|
exec("launchctl", ["unload", plist]);
|
|
2870
|
-
|
|
3459
|
+
writeFileSync8(plist, buildMusherPlist({ sledsArgv: o.sledsArgv, env: agentEnv, logPath: musherLogPath(dir), label }));
|
|
2871
3460
|
const r = exec("launchctl", ["load", plist]);
|
|
2872
3461
|
if (!r.ok) {
|
|
2873
3462
|
error(`launchctl load failed: ${r.stderr.trim() || "unknown"}`);
|
|
@@ -2886,7 +3475,7 @@ var musherUninstall = (o) => {
|
|
|
2886
3475
|
const env = o.env ?? process.env;
|
|
2887
3476
|
const plist = musherPlistPath(o.home, env);
|
|
2888
3477
|
const exec = o.exec ?? defaultExecSync;
|
|
2889
|
-
if (
|
|
3478
|
+
if (existsSync9(plist)) {
|
|
2890
3479
|
exec("launchctl", ["unload", plist]);
|
|
2891
3480
|
rmSync2(plist, { force: true });
|
|
2892
3481
|
log(`musher removed: ${musherLabel(env)}`);
|
|
@@ -2921,7 +3510,7 @@ var musherStatus = async (o) => {
|
|
|
2921
3510
|
const env = o.env ?? process.env;
|
|
2922
3511
|
const plist = musherPlistPath(o.home, env);
|
|
2923
3512
|
const cfg = readConfig(o.home);
|
|
2924
|
-
log(
|
|
3513
|
+
log(existsSync9(plist) ? `agent ${musherLabel(env)} installed (${plist})` : "agent not installed — sleds musher --install");
|
|
2925
3514
|
const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3);
|
|
2926
3515
|
log(`claude ${claude ?? "not found — sleds musher --claude /path/to/claude"}`);
|
|
2927
3516
|
log(`mode ${cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE} · timeout ${cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES} min`);
|
|
@@ -2994,7 +3583,7 @@ var musherConfigure = (o, flags) => {
|
|
|
2994
3583
|
return 0;
|
|
2995
3584
|
};
|
|
2996
3585
|
var defaultExecSync = (cmd, args) => {
|
|
2997
|
-
const r =
|
|
3586
|
+
const r = spawnSync3(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
2998
3587
|
if (r.error)
|
|
2999
3588
|
return { ok: false, stderr: r.error.message };
|
|
3000
3589
|
return { ok: r.status === 0, stderr: r.stderr ?? "" };
|
|
@@ -3033,10 +3622,10 @@ var musher = async (o) => {
|
|
|
3033
3622
|
init_config();
|
|
3034
3623
|
|
|
3035
3624
|
// src/schedule.ts
|
|
3036
|
-
import { existsSync as
|
|
3037
|
-
import { join as
|
|
3625
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync9, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "node:fs";
|
|
3626
|
+
import { join as join8 } from "node:path";
|
|
3038
3627
|
var REVERT_LABEL = "ai.sleds.revert";
|
|
3039
|
-
var plistPath = (home) =>
|
|
3628
|
+
var plistPath = (home) => join8(home, "Library", "LaunchAgents", `${REVERT_LABEL}.plist`);
|
|
3040
3629
|
var xmlEscape2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3041
3630
|
var buildLaunchdPlist = (sledsArgv, deadline) => {
|
|
3042
3631
|
const args = [...sledsArgv, "off"].map((a) => ` <string>${xmlEscape2(a)}</string>`).join(`
|
|
@@ -3075,9 +3664,9 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
3075
3664
|
if (d.platform === "darwin") {
|
|
3076
3665
|
const p = plistPath(d.home);
|
|
3077
3666
|
try {
|
|
3078
|
-
|
|
3667
|
+
mkdirSync9(join8(d.home, "Library", "LaunchAgents"), { recursive: true });
|
|
3079
3668
|
d.exec("launchctl", ["unload", p]);
|
|
3080
|
-
|
|
3669
|
+
writeFileSync9(p, buildLaunchdPlist(d.sledsArgv, deadline));
|
|
3081
3670
|
const r = d.exec("launchctl", ["load", p]);
|
|
3082
3671
|
if (r.ok)
|
|
3083
3672
|
return { kind: "launchd", detail: `launchd agent ${REVERT_LABEL}` };
|
|
@@ -3104,7 +3693,7 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
3104
3693
|
var cancelRevert = (d, kind) => {
|
|
3105
3694
|
if (kind === "launchd" || d.platform === "darwin") {
|
|
3106
3695
|
const p = plistPath(d.home);
|
|
3107
|
-
if (
|
|
3696
|
+
if (existsSync10(p)) {
|
|
3108
3697
|
d.exec("launchctl", ["unload", p]);
|
|
3109
3698
|
rmSync3(p, { force: true });
|
|
3110
3699
|
}
|
|
@@ -3173,7 +3762,7 @@ var on = (o) => {
|
|
|
3173
3762
|
return 2;
|
|
3174
3763
|
}
|
|
3175
3764
|
if (cfg.org && !cfg.org.capabilities.upstreamKey) {
|
|
3176
|
-
error(`${cfg.org.name} has no platform key, so there is no
|
|
3765
|
+
error(`${cfg.org.name} has no platform key, so there is no platform key to switch to. Hold until the reset, or hand off with /sled 5. An admin can add a key on the dashboard.`);
|
|
3177
3766
|
return 2;
|
|
3178
3767
|
}
|
|
3179
3768
|
const url = (o.url ?? cfg.proxyUrl ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL2).replace(/\/+$/, "");
|
|
@@ -3216,7 +3805,7 @@ var on = (o) => {
|
|
|
3216
3805
|
next.reason = o.reason?.trim() || undefined;
|
|
3217
3806
|
next.schedule = schedule;
|
|
3218
3807
|
writeConfig(o.home, next);
|
|
3219
|
-
log(`Sleds on. Claude Code now runs on the
|
|
3808
|
+
log(`Sleds on. Claude Code now runs on the platform key through ${url}. Restart claude to apply.`);
|
|
3220
3809
|
if (untilResetMs !== undefined) {
|
|
3221
3810
|
log(`Reverts at ${formatLocal(untilResetMs, o.tz)} — run \`sleds off\` earlier if you want.`);
|
|
3222
3811
|
if (schedule === undefined || schedule === "manual") {
|
|
@@ -3346,14 +3935,14 @@ var seatEvent = async (o) => {
|
|
|
3346
3935
|
|
|
3347
3936
|
// src/commands/seatSync.ts
|
|
3348
3937
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3349
|
-
import { existsSync as
|
|
3938
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync as readdirSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
3350
3939
|
import { platform } from "node:os";
|
|
3351
|
-
import { join as
|
|
3940
|
+
import { join as join10 } from "node:path";
|
|
3352
3941
|
|
|
3353
3942
|
// src/cookieDb.ts
|
|
3354
3943
|
import { copyFileSync, rmSync as rmSync4 } from "node:fs";
|
|
3355
3944
|
import { tmpdir } from "node:os";
|
|
3356
|
-
import { join as
|
|
3945
|
+
import { join as join9 } from "node:path";
|
|
3357
3946
|
var QUERY = "SELECT name, hex(encrypted_value) AS ev FROM cookies WHERE host_key LIKE '%claude.ai%'";
|
|
3358
3947
|
var isBun = () => typeof globalThis.Bun !== "undefined";
|
|
3359
3948
|
async function queryRows(dbPath) {
|
|
@@ -3382,7 +3971,7 @@ function nodeSqlite() {
|
|
|
3382
3971
|
return mod;
|
|
3383
3972
|
}
|
|
3384
3973
|
async function readCookieRows(dbPath) {
|
|
3385
|
-
const copy =
|
|
3974
|
+
const copy = join9(tmpdir(), `sleds-cookies-${process.pid}-${Date.now()}.sqlite`);
|
|
3386
3975
|
copyFileSync(dbPath, copy);
|
|
3387
3976
|
try {
|
|
3388
3977
|
const rows = await queryRows(copy);
|
|
@@ -3658,7 +4247,7 @@ function findFile(root, name) {
|
|
|
3658
4247
|
const stack = [root];
|
|
3659
4248
|
while (stack.length > 0) {
|
|
3660
4249
|
const dir = stack.pop();
|
|
3661
|
-
if (!dir || !
|
|
4250
|
+
if (!dir || !existsSync11(dir))
|
|
3662
4251
|
continue;
|
|
3663
4252
|
let entries;
|
|
3664
4253
|
try {
|
|
@@ -3667,7 +4256,7 @@ function findFile(root, name) {
|
|
|
3667
4256
|
continue;
|
|
3668
4257
|
}
|
|
3669
4258
|
for (const e of entries) {
|
|
3670
|
-
const p =
|
|
4259
|
+
const p = join10(dir, e.name);
|
|
3671
4260
|
if (e.isDirectory())
|
|
3672
4261
|
stack.push(p);
|
|
3673
4262
|
else if (e.name === name)
|
|
@@ -3678,8 +4267,8 @@ function findFile(root, name) {
|
|
|
3678
4267
|
}
|
|
3679
4268
|
function findCookieDb(home, source) {
|
|
3680
4269
|
const b = BROWSERS[source.browser];
|
|
3681
|
-
const root =
|
|
3682
|
-
if (!
|
|
4270
|
+
const root = join10(home, APP_SUPPORT, b.appDir);
|
|
4271
|
+
if (!existsSync11(root))
|
|
3683
4272
|
throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
|
|
3684
4273
|
if (!b.profiled) {
|
|
3685
4274
|
const db2 = findFile(root, "Cookies");
|
|
@@ -3687,15 +4276,15 @@ function findCookieDb(home, source) {
|
|
|
3687
4276
|
throw new Error(`no cookie store under ${root}`);
|
|
3688
4277
|
return db2;
|
|
3689
4278
|
}
|
|
3690
|
-
const localStatePath =
|
|
3691
|
-
const localState =
|
|
4279
|
+
const localStatePath = join10(root, "Local State");
|
|
4280
|
+
const localState = existsSync11(localStatePath) ? readFileSync9(localStatePath, "utf8") : null;
|
|
3692
4281
|
const wanted = source.profile ?? "Default";
|
|
3693
4282
|
const { dir, ambiguous } = resolveProfileDir(localState, wanted);
|
|
3694
4283
|
if (ambiguous)
|
|
3695
4284
|
throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
|
|
3696
4285
|
if (!dir)
|
|
3697
4286
|
throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
|
|
3698
|
-
const db = cookieDbCandidates(
|
|
4287
|
+
const db = cookieDbCandidates(join10(root, dir)).find((p) => existsSync11(p));
|
|
3699
4288
|
if (!db)
|
|
3700
4289
|
throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
|
|
3701
4290
|
return db;
|
|
@@ -3738,7 +4327,7 @@ function fetchSeatUsage(jar, org, now, run2) {
|
|
|
3738
4327
|
}
|
|
3739
4328
|
function launchAgentPlist(bun, script, home, source) {
|
|
3740
4329
|
const label = launchLabel(source);
|
|
3741
|
-
const log =
|
|
4330
|
+
const log = join10(home, ".config", "sleds", `${label.slice("ai.sleds.".length)}.log`);
|
|
3742
4331
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3743
4332
|
const args = [bun, script, "seat-sync", ...sourceArgs(source)].map((a) => ` <string>${esc(a)}</string>`).join(`
|
|
3744
4333
|
`);
|
|
@@ -3760,7 +4349,7 @@ ${args}
|
|
|
3760
4349
|
`;
|
|
3761
4350
|
}
|
|
3762
4351
|
function plistPath2(home, source) {
|
|
3763
|
-
return
|
|
4352
|
+
return join10(home, "Library", "LaunchAgents", `${launchLabel(source)}.plist`);
|
|
3764
4353
|
}
|
|
3765
4354
|
function install(home, argv, source, log) {
|
|
3766
4355
|
const bun = argv[0] ?? process.execPath;
|
|
@@ -3769,10 +4358,10 @@ function install(home, argv, source, log) {
|
|
|
3769
4358
|
log("cannot resolve the sleds script path to install the timer");
|
|
3770
4359
|
return 1;
|
|
3771
4360
|
}
|
|
3772
|
-
const dir =
|
|
3773
|
-
|
|
4361
|
+
const dir = join10(home, "Library", "LaunchAgents");
|
|
4362
|
+
mkdirSync10(dir, { recursive: true });
|
|
3774
4363
|
const path = plistPath2(home, source);
|
|
3775
|
-
|
|
4364
|
+
writeFileSync10(path, launchAgentPlist(bun, script, home, source), { mode: 384 });
|
|
3776
4365
|
try {
|
|
3777
4366
|
execFileSync2("launchctl", ["unload", path], { stdio: "ignore" });
|
|
3778
4367
|
} catch {}
|
|
@@ -3785,7 +4374,7 @@ function uninstall(home, source, log) {
|
|
|
3785
4374
|
try {
|
|
3786
4375
|
execFileSync2("launchctl", ["unload", "-w", path], { stdio: "ignore" });
|
|
3787
4376
|
} catch {}
|
|
3788
|
-
log(
|
|
4377
|
+
log(existsSync11(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
|
|
3789
4378
|
return 0;
|
|
3790
4379
|
}
|
|
3791
4380
|
var seatSync = async (o) => {
|
|
@@ -3881,7 +4470,7 @@ async function postSeat(proxyUrl, token, body, fetchFn) {
|
|
|
3881
4470
|
|
|
3882
4471
|
// src/commands/specEvent.ts
|
|
3883
4472
|
init_meter();
|
|
3884
|
-
import { readFileSync as
|
|
4473
|
+
import { readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
|
|
3885
4474
|
import { basename as basename2, isAbsolute, relative } from "node:path";
|
|
3886
4475
|
var HOOK_STDIN_TIMEOUT_MS2 = 1000;
|
|
3887
4476
|
var ESTIMATE_TIMEOUT_MS = 4000;
|
|
@@ -3935,9 +4524,9 @@ function projectKey(filePath, cwd) {
|
|
|
3935
4524
|
}
|
|
3936
4525
|
function readFileOrNull(path) {
|
|
3937
4526
|
try {
|
|
3938
|
-
if (
|
|
4527
|
+
if (statSync2(path).size > 2000000)
|
|
3939
4528
|
return null;
|
|
3940
|
-
return
|
|
4529
|
+
return readFileSync10(path, "utf8");
|
|
3941
4530
|
} catch {
|
|
3942
4531
|
return null;
|
|
3943
4532
|
}
|
|
@@ -3982,7 +4571,7 @@ var specEvent = async (o) => {
|
|
|
3982
4571
|
// src/commands/status.ts
|
|
3983
4572
|
init_config();
|
|
3984
4573
|
init_settings();
|
|
3985
|
-
import { existsSync as
|
|
4574
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
3986
4575
|
var checkHealth2 = async (url, fetchFn = fetch, timeoutMs = 1000) => {
|
|
3987
4576
|
try {
|
|
3988
4577
|
const res = await fetchFn(`${url}/healthz`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -4040,9 +4629,11 @@ var formatStatus = (s) => {
|
|
|
4040
4629
|
}
|
|
4041
4630
|
}
|
|
4042
4631
|
if (s.email)
|
|
4043
|
-
lines.push(row2("Seat:", s.email));
|
|
4632
|
+
lines.push(row2("Seat:", `${s.email}${s.agentKind ? ` · ${s.agentKind}` : ""}`));
|
|
4633
|
+
else if (s.agentKind)
|
|
4634
|
+
lines.push(row2("Agent:", s.agentKind));
|
|
4044
4635
|
if (s.org)
|
|
4045
|
-
lines.push(row2("Org:", `${s.org.name} · ${s.org.plan} · ${s.org.upstream === "none" ? "no platform key (walls: hold or hand off)" : s.org.upstream === "own" ? "
|
|
4636
|
+
lines.push(row2("Org:", `${s.org.name} · ${s.org.plan} · ${s.org.upstream === "none" ? "no platform key (walls: hold or hand off)" : s.org.upstream === "own" ? "platform key" : "shared key"}`));
|
|
4046
4637
|
if (s.wiring)
|
|
4047
4638
|
lines.push(row2("Wiring:", s.wiring === "seat-first" ? "seat-first — Claude Code on your seat; the proxy only after `sleds on`" : "proxy-always — every request through the proxy"));
|
|
4048
4639
|
if (s.mode === "off") {
|
|
@@ -4054,7 +4645,7 @@ var status = async (o) => {
|
|
|
4054
4645
|
const log = o.log ?? console.log;
|
|
4055
4646
|
const nowMs = (o.now ?? Date.now)();
|
|
4056
4647
|
const cfg = readConfig(o.home);
|
|
4057
|
-
const settings =
|
|
4648
|
+
const settings = existsSync12(settingsPath(o.home)) ? readSettings(o.home) : {};
|
|
4058
4649
|
const inSettings = readProxy(settings);
|
|
4059
4650
|
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
4060
4651
|
const token = inSettings?.token ?? cfg.token;
|
|
@@ -4076,6 +4667,8 @@ var status = async (o) => {
|
|
|
4076
4667
|
info.tokenMasked = maskToken(token);
|
|
4077
4668
|
if (cfg.email)
|
|
4078
4669
|
info.email = cfg.email;
|
|
4670
|
+
if (cfg.agentKind)
|
|
4671
|
+
info.agentKind = cfg.agentKind;
|
|
4079
4672
|
if (cfg.wiring)
|
|
4080
4673
|
info.wiring = cfg.wiring;
|
|
4081
4674
|
if (cfg.org)
|
|
@@ -4108,7 +4701,25 @@ var status = async (o) => {
|
|
|
4108
4701
|
};
|
|
4109
4702
|
|
|
4110
4703
|
// src/commands/statusline.ts
|
|
4704
|
+
init_config();
|
|
4705
|
+
init_meter();
|
|
4706
|
+
|
|
4707
|
+
// src/commands/seatEvent.ts
|
|
4708
|
+
init_config();
|
|
4111
4709
|
init_meter();
|
|
4710
|
+
var NOTIFICATION_TITLE2 = "sleds";
|
|
4711
|
+
var osascriptArgs2 = (title, body) => [
|
|
4712
|
+
"-e",
|
|
4713
|
+
"on run argv",
|
|
4714
|
+
"-e",
|
|
4715
|
+
"display notification (item 2 of argv) with title (item 1 of argv)",
|
|
4716
|
+
"-e",
|
|
4717
|
+
"end run",
|
|
4718
|
+
title,
|
|
4719
|
+
body
|
|
4720
|
+
];
|
|
4721
|
+
|
|
4722
|
+
// src/commands/statusline.ts
|
|
4112
4723
|
var statusline = async (o) => {
|
|
4113
4724
|
const log = o.log ?? console.log;
|
|
4114
4725
|
const now = (o.now ?? Date.now)();
|
|
@@ -4118,9 +4729,21 @@ var statusline = async (o) => {
|
|
|
4118
4729
|
let posts = [];
|
|
4119
4730
|
try {
|
|
4120
4731
|
if (window)
|
|
4121
|
-
posts = rememberSeat(o, window, session, now);
|
|
4122
|
-
else
|
|
4123
|
-
|
|
4732
|
+
posts = rememberSeat(o, window, session, now, input);
|
|
4733
|
+
else {
|
|
4734
|
+
const st = readSeatState(o.home);
|
|
4735
|
+
const credits = creditsEvent(o, st, input, now);
|
|
4736
|
+
if (session || credits.scan) {
|
|
4737
|
+
const next = {
|
|
4738
|
+
...st,
|
|
4739
|
+
...session ? { session } : {},
|
|
4740
|
+
...credits.scan ? { transcriptScan: credits.scan } : {}
|
|
4741
|
+
};
|
|
4742
|
+
writeSeatState(o.home, credits.event ? markWarned(next, [credits.event]) : next);
|
|
4743
|
+
}
|
|
4744
|
+
if (credits.event)
|
|
4745
|
+
posts = announce(o, [credits.event], now);
|
|
4746
|
+
}
|
|
4124
4747
|
} catch {}
|
|
4125
4748
|
const seatFragment = formatSeatFragment(window);
|
|
4126
4749
|
let line;
|
|
@@ -4139,43 +4762,124 @@ var statusline = async (o) => {
|
|
|
4139
4762
|
await Promise.allSettled(posts);
|
|
4140
4763
|
return 0;
|
|
4141
4764
|
};
|
|
4142
|
-
var rememberSeat = (o, window, session, now) => {
|
|
4765
|
+
var rememberSeat = (o, window, session, now, input) => {
|
|
4143
4766
|
const prior = readSeatState(o.home);
|
|
4144
4767
|
let state = { ...prior, window, ...session ? { session } : {} };
|
|
4145
4768
|
const src = meterSource(o.home);
|
|
4146
4769
|
const posts = [];
|
|
4147
|
-
if (src) {
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4770
|
+
if (src && shouldPostSeat(prior, window, now)) {
|
|
4771
|
+
state = markPosted(state, window, now);
|
|
4772
|
+
posts.push(postSeatWindow({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, window));
|
|
4773
|
+
}
|
|
4774
|
+
const events = seatWallEvents(prior, window);
|
|
4775
|
+
const credits = creditsEvent(o, prior, input, now);
|
|
4776
|
+
if (credits.scan)
|
|
4777
|
+
state = { ...state, transcriptScan: credits.scan };
|
|
4778
|
+
if (credits.event)
|
|
4779
|
+
events.push(credits.event);
|
|
4780
|
+
state = markWarned(state, events);
|
|
4781
|
+
posts.push(...announce(o, events, now));
|
|
4782
|
+
writeSeatState(o.home, state);
|
|
4783
|
+
return posts;
|
|
4784
|
+
};
|
|
4785
|
+
var creditsEvent = (o, prior, input, now) => {
|
|
4786
|
+
const path = transcriptPathFromStdin(input);
|
|
4787
|
+
if (!path)
|
|
4788
|
+
return { event: null, scan: undefined };
|
|
4789
|
+
const { hit, next } = scanTranscriptForCredits(path, prior.transcriptScan);
|
|
4790
|
+
const recent = prior.warned?.credits?.hit !== undefined && now - prior.warned.credits.hit < 6 * 3600000;
|
|
4791
|
+
if (!hit || recent)
|
|
4792
|
+
return { event: null, scan: next };
|
|
4793
|
+
return { event: { bar: "credits", stage: "hit", usedPct: 100, resetsAt: now }, scan: next };
|
|
4794
|
+
};
|
|
4795
|
+
var announce = (o, events, now) => {
|
|
4796
|
+
if (events.length === 0)
|
|
4797
|
+
return [];
|
|
4798
|
+
const src = meterSource(o.home);
|
|
4799
|
+
const hasKey = readConfig(o.home).org?.capabilities.upstreamKey !== false;
|
|
4800
|
+
const platform2 = o.platform ?? process.platform;
|
|
4801
|
+
const posts = [];
|
|
4802
|
+
for (const e of events) {
|
|
4803
|
+
if (platform2 === "darwin" && o.exec) {
|
|
4804
|
+
try {
|
|
4805
|
+
o.exec("osascript", osascriptArgs2(NOTIFICATION_TITLE2, seatWallBanner(e, hasKey, now)));
|
|
4806
|
+
} catch {}
|
|
4152
4807
|
}
|
|
4153
|
-
if (
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
usedPct:
|
|
4158
|
-
|
|
4808
|
+
if (src)
|
|
4809
|
+
posts.push(postSeatWall({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, {
|
|
4810
|
+
bar: e.bar,
|
|
4811
|
+
stage: e.stage,
|
|
4812
|
+
usedPct: e.usedPct,
|
|
4813
|
+
...e.bar === "credits" ? {} : { resetsAt: e.resetsAt },
|
|
4814
|
+
notification: e.bar === "credits" ? "transcript" : "statusline"
|
|
4159
4815
|
}));
|
|
4160
|
-
}
|
|
4161
4816
|
}
|
|
4162
|
-
writeSeatState(o.home, state);
|
|
4163
4817
|
return posts;
|
|
4164
4818
|
};
|
|
4165
4819
|
|
|
4820
|
+
// src/commands/uninstall.ts
|
|
4821
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
4822
|
+
import { existsSync as existsSync14, rmSync as rmSync5 } from "node:fs";
|
|
4823
|
+
|
|
4824
|
+
// src/codex.ts
|
|
4825
|
+
init_config();
|
|
4826
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
4827
|
+
import { dirname as dirname5, join as join11 } from "node:path";
|
|
4828
|
+
var PROVIDER_ID2 = "sleds";
|
|
4829
|
+
var codexDir2 = (home) => join11(home, ".codex");
|
|
4830
|
+
var codexConfigPath2 = (home) => join11(codexDir2(home), "config.toml");
|
|
4831
|
+
function removeCodexProvider2(toml, o = {}) {
|
|
4832
|
+
const lines = toml.split(`
|
|
4833
|
+
`);
|
|
4834
|
+
const out = [];
|
|
4835
|
+
let skipping = false;
|
|
4836
|
+
for (const line of lines) {
|
|
4837
|
+
if (/^\s*\[/.test(line))
|
|
4838
|
+
skipping = line.trim() === `[model_providers.${PROVIDER_ID2}]`;
|
|
4839
|
+
if (!skipping)
|
|
4840
|
+
out.push(line);
|
|
4841
|
+
}
|
|
4842
|
+
let text = out.join(`
|
|
4843
|
+
`);
|
|
4844
|
+
if (!o.keepPrior) {
|
|
4845
|
+
const prior = text.match(/^# sleds-prior-model-provider = "([^"]*)"\s*\n/m);
|
|
4846
|
+
if (prior) {
|
|
4847
|
+
text = text.replace(prior[0], "");
|
|
4848
|
+
text = text.replace(/^model_provider\s*=\s*"sleds"\s*$/m, `model_provider = "${prior[1]}"`);
|
|
4849
|
+
} else {
|
|
4850
|
+
text = text.replace(/^model_provider\s*=\s*"sleds"\s*\n?/m, "");
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
return text.replace(/\n{3,}/g, `
|
|
4854
|
+
|
|
4855
|
+
`);
|
|
4856
|
+
}
|
|
4857
|
+
function uninstallCodex(home) {
|
|
4858
|
+
const path = codexConfigPath2(home);
|
|
4859
|
+
if (!existsSync13(path))
|
|
4860
|
+
return null;
|
|
4861
|
+
const prior = readFileSync11(path, "utf8");
|
|
4862
|
+
const next = removeCodexProvider2(prior);
|
|
4863
|
+
if (next === prior)
|
|
4864
|
+
return null;
|
|
4865
|
+
writeFileSync11(path, next, { mode: 384 });
|
|
4866
|
+
return `removed the "${PROVIDER_ID2}" provider from ${path}`;
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4166
4869
|
// src/commands/uninstall.ts
|
|
4167
4870
|
init_config();
|
|
4168
4871
|
init_meter();
|
|
4169
|
-
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
4170
|
-
import { existsSync as existsSync12, rmSync as rmSync5 } from "node:fs";
|
|
4171
4872
|
init_settings();
|
|
4172
4873
|
var defaultUnload = (plist) => {
|
|
4173
|
-
|
|
4874
|
+
spawnSync4("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
4174
4875
|
};
|
|
4175
4876
|
var uninstall2 = (o) => {
|
|
4877
|
+
const codexLine = uninstallCodex(o.home);
|
|
4878
|
+
if (codexLine)
|
|
4879
|
+
(o.log ?? console.log)(` ${codexLine}`);
|
|
4176
4880
|
const log = o.log ?? console.log;
|
|
4177
4881
|
const removed = [];
|
|
4178
|
-
if (
|
|
4882
|
+
if (existsSync14(settingsPath(o.home))) {
|
|
4179
4883
|
let settings = readSettings(o.home);
|
|
4180
4884
|
let changed = false;
|
|
4181
4885
|
if (hasEitherKey(settings)) {
|
|
@@ -4207,18 +4911,18 @@ var uninstall2 = (o) => {
|
|
|
4207
4911
|
writeSettings(o.home, settings);
|
|
4208
4912
|
}
|
|
4209
4913
|
for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
|
|
4210
|
-
if (
|
|
4914
|
+
if (existsSync14(file)) {
|
|
4211
4915
|
rmSync5(file, { force: true });
|
|
4212
4916
|
removed.push(file);
|
|
4213
4917
|
}
|
|
4214
4918
|
}
|
|
4215
4919
|
const skill = skillDir(o.home);
|
|
4216
|
-
if (
|
|
4920
|
+
if (existsSync14(skill)) {
|
|
4217
4921
|
rmSync5(skill, { recursive: true, force: true });
|
|
4218
4922
|
removed.push(skill);
|
|
4219
4923
|
}
|
|
4220
4924
|
const plist = musherPlistPath(o.home);
|
|
4221
|
-
if (
|
|
4925
|
+
if (existsSync14(plist)) {
|
|
4222
4926
|
(o.unloadAgent ?? defaultUnload)(plist);
|
|
4223
4927
|
rmSync5(plist, { force: true });
|
|
4224
4928
|
removed.push(`launchd agent ${musherLabel()}`);
|
|
@@ -4288,25 +4992,141 @@ var verify = async (o) => {
|
|
|
4288
4992
|
return ok ? 0 : 1;
|
|
4289
4993
|
};
|
|
4290
4994
|
|
|
4995
|
+
// src/config.ts
|
|
4996
|
+
init_settings();
|
|
4997
|
+
import { chmodSync as chmodSync2, existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync12, renameSync as renameSync5, writeFileSync as writeFileSync12 } from "node:fs";
|
|
4998
|
+
import { join as join12, resolve as resolve4 } from "node:path";
|
|
4999
|
+
var configDir2 = (home, env = process.env) => {
|
|
5000
|
+
const o = env.SLEDS_HOME?.trim();
|
|
5001
|
+
return o ? resolve4(home, o) : join12(home, ".config", "sleds");
|
|
5002
|
+
};
|
|
5003
|
+
var configPath2 = (home) => join12(configDir2(home), "config.json");
|
|
5004
|
+
var isObject10 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5005
|
+
var optString2 = (v) => typeof v === "string" ? v : undefined;
|
|
5006
|
+
var parsePriorEnv2 = (v) => {
|
|
5007
|
+
if (!isObject10(v))
|
|
5008
|
+
return;
|
|
5009
|
+
const out = {};
|
|
5010
|
+
const url = optString2(v[BASE_URL_KEY]);
|
|
5011
|
+
const token = optString2(v[AUTH_TOKEN_KEY]);
|
|
5012
|
+
if (url !== undefined)
|
|
5013
|
+
out[BASE_URL_KEY] = url;
|
|
5014
|
+
if (token !== undefined)
|
|
5015
|
+
out[AUTH_TOKEN_KEY] = token;
|
|
5016
|
+
if (typeof v.hadEnv === "boolean")
|
|
5017
|
+
out.hadEnv = v.hadEnv;
|
|
5018
|
+
return out;
|
|
5019
|
+
};
|
|
5020
|
+
var parseSchedule2 = (v) => v === "launchd" || v === "systemd" || v === "manual" ? v : undefined;
|
|
5021
|
+
var parseConfig2 = (raw) => {
|
|
5022
|
+
if (!isObject10(raw))
|
|
5023
|
+
return {};
|
|
5024
|
+
const cfg = {};
|
|
5025
|
+
const token = optString2(raw.token);
|
|
5026
|
+
const proxyUrl = optString2(raw.proxyUrl);
|
|
5027
|
+
const email = optString2(raw.email);
|
|
5028
|
+
const since = optString2(raw.since);
|
|
5029
|
+
const untilReset = optString2(raw.untilReset);
|
|
5030
|
+
const reason = optString2(raw.reason);
|
|
5031
|
+
const priorEnv = parsePriorEnv2(raw.priorEnv);
|
|
5032
|
+
const schedule = parseSchedule2(raw.schedule);
|
|
5033
|
+
if (token !== undefined)
|
|
5034
|
+
cfg.token = token;
|
|
5035
|
+
if (proxyUrl !== undefined)
|
|
5036
|
+
cfg.proxyUrl = proxyUrl;
|
|
5037
|
+
if (email !== undefined)
|
|
5038
|
+
cfg.email = email;
|
|
5039
|
+
if (raw.mode === "on" || raw.mode === "off")
|
|
5040
|
+
cfg.mode = raw.mode;
|
|
5041
|
+
if (since !== undefined)
|
|
5042
|
+
cfg.since = since;
|
|
5043
|
+
if (untilReset !== undefined)
|
|
5044
|
+
cfg.untilReset = untilReset;
|
|
5045
|
+
if (reason !== undefined)
|
|
5046
|
+
cfg.reason = reason;
|
|
5047
|
+
if (priorEnv !== undefined)
|
|
5048
|
+
cfg.priorEnv = priorEnv;
|
|
5049
|
+
if (schedule !== undefined)
|
|
5050
|
+
cfg.schedule = schedule;
|
|
5051
|
+
if (raw.priorStatusLine !== undefined)
|
|
5052
|
+
cfg.priorStatusLine = raw.priorStatusLine;
|
|
5053
|
+
if (raw.wiring === "seat-first" || raw.wiring === "proxy-always")
|
|
5054
|
+
cfg.wiring = raw.wiring;
|
|
5055
|
+
if (raw.agentKind === "claude-code" || raw.agentKind === "codex")
|
|
5056
|
+
cfg.agentKind = raw.agentKind;
|
|
5057
|
+
const org = parseOrgInfo2(raw.org);
|
|
5058
|
+
if (org)
|
|
5059
|
+
cfg.org = org;
|
|
5060
|
+
const musher2 = parseMusher2(raw.musher);
|
|
5061
|
+
if (musher2)
|
|
5062
|
+
cfg.musher = musher2;
|
|
5063
|
+
return cfg;
|
|
5064
|
+
};
|
|
5065
|
+
var parseMusher2 = (raw) => {
|
|
5066
|
+
if (!isObject10(raw))
|
|
5067
|
+
return;
|
|
5068
|
+
const out = {};
|
|
5069
|
+
const pm = optString2(raw.permissionMode);
|
|
5070
|
+
const cp = optString2(raw.claudePath);
|
|
5071
|
+
if (pm)
|
|
5072
|
+
out.permissionMode = pm;
|
|
5073
|
+
if (cp)
|
|
5074
|
+
out.claudePath = cp;
|
|
5075
|
+
if (typeof raw.timeoutMinutes === "number" && raw.timeoutMinutes > 0)
|
|
5076
|
+
out.timeoutMinutes = raw.timeoutMinutes;
|
|
5077
|
+
if (raw.agent === "launchd" || raw.agent === "none")
|
|
5078
|
+
out.agent = raw.agent;
|
|
5079
|
+
return out;
|
|
5080
|
+
};
|
|
5081
|
+
var parseOrgInfo2 = (raw) => {
|
|
5082
|
+
if (!isObject10(raw) || !isObject10(raw.capabilities))
|
|
5083
|
+
return;
|
|
5084
|
+
const name = optString2(raw.name);
|
|
5085
|
+
const slug2 = optString2(raw.slug);
|
|
5086
|
+
const plan = raw.plan;
|
|
5087
|
+
const c = raw.capabilities;
|
|
5088
|
+
const src = c.upstreamSource;
|
|
5089
|
+
if (name === undefined || slug2 === undefined || plan !== "team" && plan !== "enterprise" && plan !== "api" && plan !== "individual" || src !== "own" && src !== "shared" && src !== "none")
|
|
5090
|
+
return;
|
|
5091
|
+
return {
|
|
5092
|
+
name,
|
|
5093
|
+
slug: slug2,
|
|
5094
|
+
plan,
|
|
5095
|
+
capabilities: {
|
|
5096
|
+
upstreamKey: c.upstreamKey === true,
|
|
5097
|
+
adminKey: c.adminKey === true,
|
|
5098
|
+
seats: c.seats !== false,
|
|
5099
|
+
upstreamSource: src
|
|
5100
|
+
},
|
|
5101
|
+
mode: raw.mode === "proxy-always" ? "proxy-always" : "seat-first"
|
|
5102
|
+
};
|
|
5103
|
+
};
|
|
5104
|
+
var readConfig2 = (home) => {
|
|
5105
|
+
const p = configPath2(home);
|
|
5106
|
+
if (!existsSync15(p))
|
|
5107
|
+
return {};
|
|
5108
|
+
return parseConfig2(JSON.parse(readFileSync12(p, "utf8")));
|
|
5109
|
+
};
|
|
5110
|
+
|
|
4291
5111
|
// src/schedule.ts
|
|
4292
|
-
import { spawnSync as
|
|
5112
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
4293
5113
|
var defaultExec = (cmd, args) => {
|
|
4294
|
-
const r =
|
|
5114
|
+
const r = spawnSync5(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
4295
5115
|
if (r.error)
|
|
4296
5116
|
return { ok: false, stderr: r.error.message };
|
|
4297
5117
|
return { ok: r.status === 0, stderr: r.stderr ?? "" };
|
|
4298
5118
|
};
|
|
4299
5119
|
|
|
4300
5120
|
// src/settings.ts
|
|
4301
|
-
import { dirname as
|
|
5121
|
+
import { dirname as dirname6, join as join13, resolve as resolve5 } from "node:path";
|
|
4302
5122
|
var withSeatEnv2 = (command, home, env = process.env) => {
|
|
4303
5123
|
const parts = [];
|
|
4304
5124
|
const sh = env.SLEDS_HOME?.trim();
|
|
4305
5125
|
const cc = env.CLAUDE_CONFIG_DIR?.trim();
|
|
4306
5126
|
if (sh)
|
|
4307
|
-
parts.push(`SLEDS_HOME=${shellWord2(
|
|
5127
|
+
parts.push(`SLEDS_HOME=${shellWord2(resolve5(home, sh))}`);
|
|
4308
5128
|
if (cc)
|
|
4309
|
-
parts.push(`CLAUDE_CONFIG_DIR=${shellWord2(
|
|
5129
|
+
parts.push(`CLAUDE_CONFIG_DIR=${shellWord2(resolve5(home, cc))}`);
|
|
4310
5130
|
parts.push(command);
|
|
4311
5131
|
return parts.join(" ");
|
|
4312
5132
|
};
|
|
@@ -4321,14 +5141,16 @@ commands:
|
|
|
4321
5141
|
--url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
|
|
4322
5142
|
--no-probe skip the /healthz check of the proxy URL
|
|
4323
5143
|
--seat-first keep Claude Code on your seat; the proxy only at a wall (default for seat plans)
|
|
4324
|
-
--proxy-always every request through the proxy on the
|
|
5144
|
+
--proxy-always every request through the proxy on the platform key (default for API-plan orgs)
|
|
4325
5145
|
--non-interactive never prompt; exit 2 if no token is available
|
|
5146
|
+
--agent codex Codex CLI instead of Claude Code: writes ~/.codex/config.toml
|
|
5147
|
+
(provider "sleds" \u2192 the proxy, key from $SLEDS_TOKEN); no hooks/status line
|
|
4326
5148
|
--signup no org? create one: (1) just you \u2014 your email + your own Anthropic
|
|
4327
5149
|
API key (validated, stored encrypted, the only key it spends), or
|
|
4328
5150
|
(2) your team \u2014 work email + team name, platform key optional
|
|
4329
5151
|
--team "<name>" with --signup: create the team org (you become its owner)
|
|
4330
5152
|
--email <you@work.com> with --signup: skip the email prompt
|
|
4331
|
-
on Hit a limit on your seat? Switch Claude Code to the
|
|
5153
|
+
on Hit a limit on your seat? Switch Claude Code to the platform key through the proxy.
|
|
4332
5154
|
--until-reset <when> revert at this time: +5h, +90m, or an ISO timestamp
|
|
4333
5155
|
--reason "<text>" note shown by \`sleds status\`
|
|
4334
5156
|
--no-schedule record the deadline but do not install a launchd/systemd revert
|
|
@@ -4339,6 +5161,9 @@ commands:
|
|
|
4339
5161
|
musher The agent that runs deferred jobs here: --install / --uninstall / --status,
|
|
4340
5162
|
--claude <path>, --mode <permission-mode> (default acceptEdits), --timeout <min>.
|
|
4341
5163
|
run <jobId> Run one deferred job on this Mac now (the manual musher).
|
|
5164
|
+
codex [...] Run Codex CLI through this seat (sets SLEDS_TOKEN + OPENAI_BASE_URL for the run).
|
|
5165
|
+
handoffs The team's open hand-offs (yours marked); pickup <id> takes one here; withdraw <id>
|
|
5166
|
+
takes back one you parked.
|
|
4342
5167
|
verify <jobId | SLEDS-Attest line> Check a finished job's attestation: which model built it,
|
|
4343
5168
|
signed by the org's key, verified locally.
|
|
4344
5169
|
off Switch Claude Code back to your seat. Restores whatever \`on\` replaced.
|
|
@@ -4422,7 +5247,39 @@ var main = async (argv) => {
|
|
|
4422
5247
|
exec: defaultExec
|
|
4423
5248
|
};
|
|
4424
5249
|
switch (cmd) {
|
|
4425
|
-
case "
|
|
5250
|
+
case "handoffs":
|
|
5251
|
+
return handoffsCmd({ home, argv: argv.slice(1) });
|
|
5252
|
+
case "pickup":
|
|
5253
|
+
return pickupCmd({ home, argv: argv.slice(1) });
|
|
5254
|
+
case "withdraw":
|
|
5255
|
+
return withdrawCmd({ home, argv: argv.slice(1) });
|
|
5256
|
+
case "codex":
|
|
5257
|
+
return runCodex(home, [...argv.slice(1)]);
|
|
5258
|
+
case "init": {
|
|
5259
|
+
if (stringFlag(flags, "agent") === "codex") {
|
|
5260
|
+
const token = stringFlag(flags, "token") ?? process.env.SLEDS_TOKEN;
|
|
5261
|
+
const url = stringFlag(flags, "url") ?? process.env.SLEDS_PROXY_URL ?? readConfig2(home).proxyUrl;
|
|
5262
|
+
if (!token || !token.startsWith("sled_")) {
|
|
5263
|
+
console.error("sleds init --agent codex needs --token <sled_\u2026> (from Settings \u203A Your seat & tools).");
|
|
5264
|
+
return 2;
|
|
5265
|
+
}
|
|
5266
|
+
if (!url) {
|
|
5267
|
+
console.error("sleds init --agent codex needs --url https://proxy.sleds.ai");
|
|
5268
|
+
return 2;
|
|
5269
|
+
}
|
|
5270
|
+
const proxyUrl = url.replace(/\/+$/, "");
|
|
5271
|
+
const info = await fetchSeatInfo(proxyUrl, token);
|
|
5272
|
+
const wrote = initCodex({ home, token, proxyUrl, email: info.email });
|
|
5273
|
+
if (info.org)
|
|
5274
|
+
console.log(`Seat ${info.email ?? "?"} \xB7 ${info.org.name} \xB7 ${info.org.plan}. Codex requests go to ${proxyUrl}/v1 and are metered to this seat.`);
|
|
5275
|
+
console.log("Codex now runs through SLEDS on this seat.");
|
|
5276
|
+
console.log("");
|
|
5277
|
+
for (const w of wrote)
|
|
5278
|
+
console.log(` ${w}`);
|
|
5279
|
+
console.log("");
|
|
5280
|
+
console.log("Start it with `sleds codex` (sets SLEDS_TOKEN for the run), or export SLEDS_TOKEN yourself and run `codex`.");
|
|
5281
|
+
return 0;
|
|
5282
|
+
}
|
|
4426
5283
|
return init({
|
|
4427
5284
|
home,
|
|
4428
5285
|
token: stringFlag(flags, "token"),
|
|
@@ -4452,6 +5309,7 @@ var main = async (argv) => {
|
|
|
4452
5309
|
seatEventCommand: seatEventCommand(sledsArgv()),
|
|
4453
5310
|
specEventCommand: specEventCommand(sledsArgv())
|
|
4454
5311
|
});
|
|
5312
|
+
}
|
|
4455
5313
|
case "hooks":
|
|
4456
5314
|
return hooks({
|
|
4457
5315
|
home,
|
|
@@ -4494,7 +5352,7 @@ var main = async (argv) => {
|
|
|
4494
5352
|
case "seat-sync":
|
|
4495
5353
|
return seatSync({ home, argv: process.argv.slice(3) });
|
|
4496
5354
|
case "statusline":
|
|
4497
|
-
return statusline({ home });
|
|
5355
|
+
return statusline({ home, platform: platform2(), exec: defaultExec });
|
|
4498
5356
|
case "help":
|
|
4499
5357
|
case "--help":
|
|
4500
5358
|
case "-h":
|
|
@@ -4503,7 +5361,7 @@ var main = async (argv) => {
|
|
|
4503
5361
|
case "version":
|
|
4504
5362
|
case "--version":
|
|
4505
5363
|
case "-v":
|
|
4506
|
-
console.log("sleds 0.1.
|
|
5364
|
+
console.log("sleds 0.1.18");
|
|
4507
5365
|
return 0;
|
|
4508
5366
|
default:
|
|
4509
5367
|
console.error(`unknown command: ${cmd}
|