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