@sleds/cli 0.1.23 → 0.1.24
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 +1609 -1134
- package/package.json +1 -1
package/dist/sleds.mjs
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @bun
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
enumerable: true,
|
|
14
|
-
configurable: true,
|
|
15
|
-
set: __exportSetter.bind(all, name)
|
|
16
|
-
});
|
|
3
|
+
var __esm = (fn, res, err) => () => {
|
|
4
|
+
if (fn)
|
|
5
|
+
try {
|
|
6
|
+
res = fn(fn = 0);
|
|
7
|
+
} catch (e) {
|
|
8
|
+
err = [e];
|
|
9
|
+
}
|
|
10
|
+
if (err)
|
|
11
|
+
throw err[0];
|
|
12
|
+
return res;
|
|
17
13
|
};
|
|
18
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
14
|
|
|
21
15
|
// ../../packages/shared/src/mcp/format.ts
|
|
22
16
|
var isObject = (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) => {
|
|
@@ -158,8 +152,8 @@ var init_format = __esm(() => {
|
|
|
158
152
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
159
153
|
import { dirname, join, resolve } from "node:path";
|
|
160
154
|
var BASE_URL_KEY = "ANTHROPIC_BASE_URL", AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN", claudeDir = (home, env = process.env) => {
|
|
161
|
-
const
|
|
162
|
-
return
|
|
155
|
+
const o = env.CLAUDE_CONFIG_DIR?.trim();
|
|
156
|
+
return o ? resolve(home, o) : join(home, ".claude");
|
|
163
157
|
}, hasHomeOverride = (env = process.env) => Boolean(env.SLEDS_HOME?.trim() || env.CLAUDE_CONFIG_DIR?.trim()), settingsPath = (home) => join(claudeDir(home), "settings.json"), sledsCommand = (sub, home, env = process.env) => withSeatEnv(`sleds ${sub}`, home, env), withSeatEnv = (command, home, env = process.env) => {
|
|
164
158
|
const parts = [];
|
|
165
159
|
const sh = env.SLEDS_HOME?.trim();
|
|
@@ -189,8 +183,8 @@ var BASE_URL_KEY = "ANTHROPIC_BASE_URL", AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN"
|
|
|
189
183
|
visit(settings.hooks);
|
|
190
184
|
return out;
|
|
191
185
|
}, bakedHome = (command) => command.match(/(?:^|\s)SLEDS_HOME=('([^']*)'|(\S+))/)?.[2] ?? command.match(/(?:^|\s)SLEDS_HOME=(\S+)/)?.[1], shellWord = (v) => /^[\w./~-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`, backupPath = (home) => join(claudeDir(home), "settings.json.sleds-backup"), tokenPath = (home, env = process.env) => {
|
|
192
|
-
const
|
|
193
|
-
return
|
|
186
|
+
const o = env.SLEDS_HOME?.trim();
|
|
187
|
+
return o ? join(resolve(home, o), ".sleds-token") : join(home, ".sleds-token");
|
|
194
188
|
}, skillDir = (home) => join(claudeDir(home), "skills", "sled"), skillPath = (home) => join(skillDir(home), "SKILL.md"), isObject5 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), envOf = (settings) => {
|
|
195
189
|
const env = settings.env;
|
|
196
190
|
return isObject5(env) ? env : {};
|
|
@@ -326,12 +320,12 @@ var BASE_URL_KEY = "ANTHROPIC_BASE_URL", AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN"
|
|
|
326
320
|
return withEventHooks(settings, event, list.filter((e) => !ours(e)));
|
|
327
321
|
}, applySessionHook = (settings, command) => applyOnce(settings, SESSION_EVENT, isSledsSessionHook, sessionHookEntry(command)), removeSessionHook = (settings) => removeOurs(settings, SESSION_EVENT, isSledsSessionHook), hasSessionHook = (settings) => eventHooks(settings, SESSION_EVENT).some(isSledsSessionHook), applyStopHook = (settings, command) => applyOnce(settings, STOP_EVENT, isSledsStopHook, stopHookEntry(command)), removeStopHook = (settings) => removeOurs(settings, STOP_EVENT, isSledsStopHook), hasStopHook = (settings) => eventHooks(settings, STOP_EVENT).some(isSledsStopHook), isSeatToken = (token) => /^sled_[A-Za-z0-9_-]+$/.test(token), maskToken = (token) => token.length <= 9 ? `${token.slice(0, 5)}…` : `${token.slice(0, 9)}…`, serialize = (settings) => `${JSON.stringify(settings, null, 2)}
|
|
328
322
|
`, hasClaudeCode = (home) => existsSync(claudeDir(home)), readSettings = (home) => {
|
|
329
|
-
const
|
|
330
|
-
if (!existsSync(
|
|
323
|
+
const p = settingsPath(home);
|
|
324
|
+
if (!existsSync(p))
|
|
331
325
|
return {};
|
|
332
|
-
const parsed = JSON.parse(readFileSync(
|
|
326
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
333
327
|
if (!isObject5(parsed))
|
|
334
|
-
throw new Error(`${
|
|
328
|
+
throw new Error(`${p} is not a JSON object`);
|
|
335
329
|
return parsed;
|
|
336
330
|
}, writeSettings = (home, settings) => {
|
|
337
331
|
mkdirSync(claudeDir(home), { recursive: true });
|
|
@@ -347,10 +341,10 @@ var BASE_URL_KEY = "ANTHROPIC_BASE_URL", AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN"
|
|
|
347
341
|
writeFileSync(dst, readFileSync(src));
|
|
348
342
|
return true;
|
|
349
343
|
}, readTokenFile = (home) => {
|
|
350
|
-
const
|
|
351
|
-
if (!existsSync(
|
|
344
|
+
const p = tokenPath(home);
|
|
345
|
+
if (!existsSync(p))
|
|
352
346
|
return null;
|
|
353
|
-
const t = readFileSync(
|
|
347
|
+
const t = readFileSync(p, "utf8").trim();
|
|
354
348
|
return t.length > 0 ? t : null;
|
|
355
349
|
}, writeTokenFile = (home, token) => {
|
|
356
350
|
mkdirSync(dirname(tokenPath(home)), { recursive: true, mode: 448 });
|
|
@@ -366,8 +360,8 @@ var init_settings = __esm(() => {
|
|
|
366
360
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
367
361
|
import { join as join2, resolve as resolve2 } from "node:path";
|
|
368
362
|
var configDir = (home, env = process.env) => {
|
|
369
|
-
const
|
|
370
|
-
return
|
|
363
|
+
const o = env.SLEDS_HOME?.trim();
|
|
364
|
+
return o ? resolve2(home, o) : join2(home, ".config", "sleds");
|
|
371
365
|
}, configPath = (home) => join2(configDir(home), "config.json"), isObject6 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), optString = (v) => typeof v === "string" ? v : undefined, parsePriorEnv = (v) => {
|
|
372
366
|
if (!isObject6(v))
|
|
373
367
|
return;
|
|
@@ -444,15 +438,15 @@ var configDir = (home, env = process.env) => {
|
|
|
444
438
|
return;
|
|
445
439
|
const name = optString(raw.name);
|
|
446
440
|
const slug = optString(raw.slug);
|
|
447
|
-
const
|
|
441
|
+
const plan = raw.plan;
|
|
448
442
|
const c = raw.capabilities;
|
|
449
443
|
const src = c.upstreamSource;
|
|
450
|
-
if (name === undefined || slug === undefined ||
|
|
444
|
+
if (name === undefined || slug === undefined || plan !== "team" && plan !== "enterprise" && plan !== "api" && plan !== "individual" || src !== "own" && src !== "shared" && src !== "none")
|
|
451
445
|
return;
|
|
452
446
|
return {
|
|
453
447
|
name,
|
|
454
448
|
slug,
|
|
455
|
-
plan
|
|
449
|
+
plan,
|
|
456
450
|
capabilities: {
|
|
457
451
|
upstreamKey: c.upstreamKey === true,
|
|
458
452
|
adminKey: c.adminKey === true,
|
|
@@ -462,10 +456,10 @@ var configDir = (home, env = process.env) => {
|
|
|
462
456
|
mode: raw.mode === "proxy-always" ? "proxy-always" : "seat-first"
|
|
463
457
|
};
|
|
464
458
|
}, readConfig = (home) => {
|
|
465
|
-
const
|
|
466
|
-
if (!existsSync2(
|
|
459
|
+
const p = configPath(home);
|
|
460
|
+
if (!existsSync2(p))
|
|
467
461
|
return {};
|
|
468
|
-
return parseConfig(JSON.parse(readFileSync2(
|
|
462
|
+
return parseConfig(JSON.parse(readFileSync2(p, "utf8")));
|
|
469
463
|
}, writeConfig = (home, cfg) => {
|
|
470
464
|
const dir = configDir(home);
|
|
471
465
|
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
@@ -484,11 +478,11 @@ var init_config = __esm(() => {
|
|
|
484
478
|
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
485
479
|
import { join as join4 } from "node:path";
|
|
486
480
|
var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject7 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num4 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, meterCachePath = (home) => join4(configDir(home), "meter.json"), readMeterCache = (home, now) => {
|
|
487
|
-
const
|
|
488
|
-
if (!existsSync4(
|
|
481
|
+
const p = meterCachePath(home);
|
|
482
|
+
if (!existsSync4(p))
|
|
489
483
|
return null;
|
|
490
484
|
try {
|
|
491
|
-
const raw = JSON.parse(readFileSync4(
|
|
485
|
+
const raw = JSON.parse(readFileSync4(p, "utf8"));
|
|
492
486
|
if (!isObject7(raw))
|
|
493
487
|
return null;
|
|
494
488
|
const fetchedAt = num4(raw.fetchedAt);
|
|
@@ -507,12 +501,12 @@ var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject7 = (v) => typeof
|
|
|
507
501
|
writeFileSync4(tmp, `${JSON.stringify(file)}
|
|
508
502
|
`, { mode: 384 });
|
|
509
503
|
renameSync3(tmp, target);
|
|
510
|
-
}, fetchMeter = async (
|
|
511
|
-
const fetchFn =
|
|
504
|
+
}, fetchMeter = async (o) => {
|
|
505
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
512
506
|
try {
|
|
513
|
-
const res = await fetchFn(`${
|
|
514
|
-
headers: { authorization: `Bearer ${
|
|
515
|
-
signal: AbortSignal.timeout(
|
|
507
|
+
const res = await fetchFn(`${o.proxyUrl}/proxy/meter`, {
|
|
508
|
+
headers: { authorization: `Bearer ${o.token}` },
|
|
509
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? METER_TIMEOUT_MS)
|
|
516
510
|
});
|
|
517
511
|
if (!res.ok)
|
|
518
512
|
return null;
|
|
@@ -527,18 +521,18 @@ var METER_CACHE_TTL_MS = 1e4, METER_TIMEOUT_MS = 1500, isObject7 = (v) => typeof
|
|
|
527
521
|
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
528
522
|
const token = inSettings?.token ?? cfg.token;
|
|
529
523
|
return proxyUrl && token ? { proxyUrl, token } : null;
|
|
530
|
-
}, loadMeter = async (
|
|
531
|
-
const now =
|
|
532
|
-
const cached = readMeterCache(
|
|
524
|
+
}, loadMeter = async (o) => {
|
|
525
|
+
const now = o.now ?? Date.now();
|
|
526
|
+
const cached = readMeterCache(o.home, now);
|
|
533
527
|
if (cached)
|
|
534
528
|
return cached;
|
|
535
|
-
const src = meterSource(
|
|
529
|
+
const src = meterSource(o.home);
|
|
536
530
|
if (!src)
|
|
537
531
|
return null;
|
|
538
|
-
const fresh = await fetchMeter({ ...src, fetchFn:
|
|
532
|
+
const fresh = await fetchMeter({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs });
|
|
539
533
|
if (fresh) {
|
|
540
534
|
try {
|
|
541
|
-
writeMeterCache(
|
|
535
|
+
writeMeterCache(o.home, fresh, now);
|
|
542
536
|
} catch {}
|
|
543
537
|
}
|
|
544
538
|
return fresh;
|
|
@@ -551,432 +545,9 @@ var init_meter = __esm(() => {
|
|
|
551
545
|
});
|
|
552
546
|
|
|
553
547
|
// src/seat.ts
|
|
554
|
-
var
|
|
555
|
-
__export(exports_seat, {
|
|
556
|
-
APPROACHING_PCT: () => APPROACHING_PCT2,
|
|
557
|
-
CREDITS_PATTERN: () => CREDITS_PATTERN2,
|
|
558
|
-
SEAT_POST_MIN_DELTA_PCT: () => SEAT_POST_MIN_DELTA_PCT2,
|
|
559
|
-
SEAT_POST_MIN_INTERVAL_MS: () => SEAT_POST_MIN_INTERVAL_MS2,
|
|
560
|
-
SEAT_POST_TIMEOUT_MS: () => SEAT_POST_TIMEOUT_MS2,
|
|
561
|
-
SEAT_WALL_LINE: () => SEAT_WALL_LINE2,
|
|
562
|
-
SEAT_WALL_LINE_NO_KEY: () => SEAT_WALL_LINE_NO_KEY2,
|
|
563
|
-
STDIN_TIMEOUT_MS: () => STDIN_TIMEOUT_MS2,
|
|
564
|
-
bindingResetAt: () => bindingResetAt2,
|
|
565
|
-
creditsHitIn: () => creditsHitIn2,
|
|
566
|
-
formatSeatFragment: () => formatSeatFragment2,
|
|
567
|
-
markPosted: () => markPosted2,
|
|
568
|
-
markWarned: () => markWarned2,
|
|
569
|
-
parseResetsAt: () => parseResetsAt2,
|
|
570
|
-
parseSeatState: () => parseSeatState2,
|
|
571
|
-
parseSeatWindow: () => parseSeatWindow2,
|
|
572
|
-
postSeatWall: () => postSeatWall2,
|
|
573
|
-
postSeatWindow: () => postSeatWindow2,
|
|
574
|
-
readSeatState: () => readSeatState2,
|
|
575
|
-
readStdin: () => readStdin2,
|
|
576
|
-
scanTranscriptForCredits: () => scanTranscriptForCredits2,
|
|
577
|
-
seatSessionFromStdin: () => seatSessionFromStdin2,
|
|
578
|
-
seatStatePath: () => seatStatePath2,
|
|
579
|
-
seatWallDue: () => seatWallDue,
|
|
580
|
-
seatWallEvents: () => seatWallEvents2,
|
|
581
|
-
seatWallLine: () => seatWallLine2,
|
|
582
|
-
seatWindowBody: () => seatWindowBody2,
|
|
583
|
-
seatWindowFromStdin: () => seatWindowFromStdin2,
|
|
584
|
-
shouldPostSeat: () => shouldPostSeat2,
|
|
585
|
-
transcriptPathFromStdin: () => transcriptPathFromStdin2,
|
|
586
|
-
writeSeatState: () => writeSeatState2
|
|
587
|
-
});
|
|
588
|
-
import {
|
|
589
|
-
closeSync as closeSync2,
|
|
590
|
-
existsSync as existsSync17,
|
|
591
|
-
mkdirSync as mkdirSync14,
|
|
592
|
-
openSync as openSync2,
|
|
593
|
-
readFileSync as readFileSync14,
|
|
594
|
-
readSync as readSync2,
|
|
595
|
-
renameSync as renameSync7,
|
|
596
|
-
statSync as statSync3,
|
|
597
|
-
writeFileSync as writeFileSync14
|
|
598
|
-
} from "node:fs";
|
|
599
|
-
import { join as join15 } from "node:path";
|
|
600
|
-
var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_MS2 = 1500, STDIN_TIMEOUT_MS2 = 300, isObject14 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num7 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, EPOCH_MS_THRESHOLD2 = 100000000000, parseResetsAt2 = (v) => {
|
|
601
|
-
const n = num7(v);
|
|
602
|
-
if (n !== undefined) {
|
|
603
|
-
if (n <= 0)
|
|
604
|
-
return;
|
|
605
|
-
return n >= EPOCH_MS_THRESHOLD2 ? Math.round(n) : Math.round(n * 1000);
|
|
606
|
-
}
|
|
607
|
-
if (typeof v === "string" && v.trim() !== "") {
|
|
608
|
-
const parsed = Date.parse(v);
|
|
609
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
610
|
-
}
|
|
611
|
-
return;
|
|
612
|
-
}, parseBar2 = (v) => {
|
|
613
|
-
if (!isObject14(v))
|
|
614
|
-
return;
|
|
615
|
-
const usedPct = num7(v.used_percentage) ?? num7(v.usedPct);
|
|
616
|
-
const resetsAt = parseResetsAt2(v.resets_at ?? v.resetsAt);
|
|
617
|
-
if (usedPct === undefined || usedPct < 0 || resetsAt === undefined)
|
|
618
|
-
return;
|
|
619
|
-
return { usedPct, resetsAt };
|
|
620
|
-
}, parseSeatWindow2 = (raw, capturedAt) => {
|
|
621
|
-
if (!isObject14(raw))
|
|
622
|
-
return null;
|
|
623
|
-
const limits = isObject14(raw.rate_limits) ? raw.rate_limits : raw;
|
|
624
|
-
const fiveHour = parseBar2(limits.five_hour ?? limits.fiveHour);
|
|
625
|
-
const sevenDay = parseBar2(limits.seven_day ?? limits.sevenDay);
|
|
626
|
-
const spendLimit = parseBar2(limits.spend_limit ?? limits.spendLimit);
|
|
627
|
-
const models = {};
|
|
628
|
-
if (isObject14(limits.models))
|
|
629
|
-
for (const [k, v] of Object.entries(limits.models)) {
|
|
630
|
-
const bar = parseBar2(v);
|
|
631
|
-
if (bar)
|
|
632
|
-
models[k] = bar;
|
|
633
|
-
}
|
|
634
|
-
const usedCreditsUsd = num7(limits.usedCreditsUsd);
|
|
635
|
-
const hasModels = Object.keys(models).length > 0;
|
|
636
|
-
if (!fiveHour && !sevenDay && !spendLimit && !hasModels && usedCreditsUsd === undefined)
|
|
637
|
-
return null;
|
|
638
|
-
const out = { capturedAt };
|
|
639
|
-
if (fiveHour)
|
|
640
|
-
out.fiveHour = fiveHour;
|
|
641
|
-
if (sevenDay)
|
|
642
|
-
out.sevenDay = sevenDay;
|
|
643
|
-
if (spendLimit)
|
|
644
|
-
out.spendLimit = spendLimit;
|
|
645
|
-
if (hasModels)
|
|
646
|
-
out.models = models;
|
|
647
|
-
if (usedCreditsUsd !== undefined)
|
|
648
|
-
out.usedCreditsUsd = usedCreditsUsd;
|
|
649
|
-
return out;
|
|
650
|
-
}, seatWindowFromStdin2 = (text, capturedAt) => {
|
|
651
|
-
const trimmed = text.trim();
|
|
652
|
-
if (!trimmed)
|
|
653
|
-
return null;
|
|
654
|
-
try {
|
|
655
|
-
return parseSeatWindow2(JSON.parse(trimmed), capturedAt);
|
|
656
|
-
} catch {
|
|
657
|
-
return null;
|
|
658
|
-
}
|
|
659
|
-
}, formatSeatFragment2 = (w, now = Date.now()) => {
|
|
660
|
-
if (!w)
|
|
661
|
-
return null;
|
|
662
|
-
const parts = [];
|
|
663
|
-
if (w.sevenDay)
|
|
664
|
-
parts.push(`wk ${Math.round(w.sevenDay.usedPct)}%`);
|
|
665
|
-
if (w.fiveHour)
|
|
666
|
-
parts.push(`5h ${Math.round(w.fiveHour.usedPct)}%`);
|
|
667
|
-
if (w.spendLimit)
|
|
668
|
-
parts.push(`spend ${Math.round(w.spendLimit.usedPct)}%`);
|
|
669
|
-
if (parts.length === 0)
|
|
670
|
-
return null;
|
|
671
|
-
const hot = [
|
|
672
|
-
["wk", w.sevenDay],
|
|
673
|
-
["5h", w.fiveHour],
|
|
674
|
-
["spend", w.spendLimit]
|
|
675
|
-
].filter((x) => x[1] !== undefined && x[1].usedPct >= APPROACHING_PCT2).sort((a, b) => b[1].usedPct - a[1].usedPct)[0];
|
|
676
|
-
if (hot) {
|
|
677
|
-
const mins = Math.max(0, Math.round((hot[1].resetsAt - now) / 60000));
|
|
678
|
-
const reset = mins >= 60 ? `${Math.floor(mins / 60)}h${mins % 60 ? `${mins % 60}m` : ""}` : `${mins}m`;
|
|
679
|
-
const verb = hot[1].usedPct >= 100 ? "at the wall" : `${Math.round(hot[1].usedPct)}%`;
|
|
680
|
-
return `⚠ seat ${hot[0]} ${verb}${SEP}resets ${reset}${SEP}/sled handoff · sleds defer`;
|
|
681
|
-
}
|
|
682
|
-
return `seat ${parts.join(SEP)}`;
|
|
683
|
-
}, APPROACHING_PCT2 = 90, seatStatePath2 = (home) => join15(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
|
|
684
|
-
if (!isObject14(v))
|
|
685
|
-
return;
|
|
686
|
-
const capturedAt = num7(v.capturedAt);
|
|
687
|
-
if (capturedAt === undefined)
|
|
688
|
-
return;
|
|
689
|
-
return parseSeatWindow2(v, capturedAt) ?? undefined;
|
|
690
|
-
}, parseSeatState2 = (raw) => {
|
|
691
|
-
if (!isObject14(raw))
|
|
692
|
-
return {};
|
|
693
|
-
const out = {};
|
|
694
|
-
const window = parseWindowStored2(raw.window);
|
|
695
|
-
if (window)
|
|
696
|
-
out.window = window;
|
|
697
|
-
const lastPostedAt = num7(raw.lastPostedAt);
|
|
698
|
-
if (lastPostedAt !== undefined)
|
|
699
|
-
out.lastPostedAt = lastPostedAt;
|
|
700
|
-
if (isObject14(raw.lastPosted)) {
|
|
701
|
-
const lp = {};
|
|
702
|
-
const f = num7(raw.lastPosted.fiveHour);
|
|
703
|
-
const s = num7(raw.lastPosted.sevenDay);
|
|
704
|
-
const sp = num7(raw.lastPosted.spendLimit);
|
|
705
|
-
if (f !== undefined)
|
|
706
|
-
lp.fiveHour = f;
|
|
707
|
-
if (s !== undefined)
|
|
708
|
-
lp.sevenDay = s;
|
|
709
|
-
if (sp !== undefined)
|
|
710
|
-
lp.spendLimit = sp;
|
|
711
|
-
out.lastPosted = lp;
|
|
712
|
-
}
|
|
713
|
-
const lastWallResetsAt = num7(raw.lastWallResetsAt);
|
|
714
|
-
if (lastWallResetsAt !== undefined)
|
|
715
|
-
out.lastWallResetsAt = lastWallResetsAt;
|
|
716
|
-
if (isObject14(raw.warned)) {
|
|
717
|
-
const warned = {};
|
|
718
|
-
for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
|
|
719
|
-
const w = raw.warned[bar];
|
|
720
|
-
if (!isObject14(w))
|
|
721
|
-
continue;
|
|
722
|
-
const entry = {};
|
|
723
|
-
const ap = num7(w.approaching);
|
|
724
|
-
const hit = num7(w.hit);
|
|
725
|
-
if (ap !== undefined)
|
|
726
|
-
entry.approaching = ap;
|
|
727
|
-
if (hit !== undefined)
|
|
728
|
-
entry.hit = hit;
|
|
729
|
-
warned[bar] = entry;
|
|
730
|
-
}
|
|
731
|
-
out.warned = warned;
|
|
732
|
-
}
|
|
733
|
-
if (isObject14(raw.told)) {
|
|
734
|
-
const told = {};
|
|
735
|
-
for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
|
|
736
|
-
const w = raw.told[bar];
|
|
737
|
-
if (!isObject14(w))
|
|
738
|
-
continue;
|
|
739
|
-
const entry = {};
|
|
740
|
-
const ap = num7(w.approaching);
|
|
741
|
-
const hit = num7(w.hit);
|
|
742
|
-
if (ap !== undefined)
|
|
743
|
-
entry.approaching = ap;
|
|
744
|
-
if (hit !== undefined)
|
|
745
|
-
entry.hit = hit;
|
|
746
|
-
told[bar] = entry;
|
|
747
|
-
}
|
|
748
|
-
out.told = told;
|
|
749
|
-
}
|
|
750
|
-
if (isObject14(raw.transcriptScan) && typeof raw.transcriptScan.path === "string") {
|
|
751
|
-
const offset = num7(raw.transcriptScan.offset);
|
|
752
|
-
const hitAt = num7(raw.transcriptScan.hitAt);
|
|
753
|
-
out.transcriptScan = {
|
|
754
|
-
path: raw.transcriptScan.path,
|
|
755
|
-
offset: offset ?? 0,
|
|
756
|
-
...hitAt !== undefined ? { hitAt } : {}
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
const session = parseSeatSession2(raw.session);
|
|
760
|
-
if (session)
|
|
761
|
-
out.session = session;
|
|
762
|
-
if (isObject14(raw.cockpit) && raw.cockpit.kind === "cmux") {
|
|
763
|
-
const c = { kind: "cmux" };
|
|
764
|
-
if (isObject14(raw.cockpit.lines)) {
|
|
765
|
-
const lines = {};
|
|
766
|
-
for (const [k, v] of Object.entries(raw.cockpit.lines))
|
|
767
|
-
if (typeof v === "string")
|
|
768
|
-
lines[k] = v;
|
|
769
|
-
c.lines = lines;
|
|
770
|
-
}
|
|
771
|
-
if (raw.cockpit.progress === true)
|
|
772
|
-
c.progress = true;
|
|
773
|
-
const f = raw.cockpit.feed;
|
|
774
|
-
if (isObject14(f) && num7(f.at) !== undefined) {
|
|
775
|
-
const strs = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
776
|
-
const j = isObject14(f.jobs) ? f.jobs : {};
|
|
777
|
-
c.feed = {
|
|
778
|
-
at: num7(f.at),
|
|
779
|
-
handoffs: strs(f.handoffs),
|
|
780
|
-
jobs: {
|
|
781
|
-
scheduled: num7(j.scheduled) ?? 0,
|
|
782
|
-
running: num7(j.running) ?? 0,
|
|
783
|
-
delivered: num7(j.delivered) ?? 0,
|
|
784
|
-
failed: num7(j.failed) ?? 0
|
|
785
|
-
},
|
|
786
|
-
delivered: strs(f.delivered),
|
|
787
|
-
failed: strs(f.failed)
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
out.cockpit = c;
|
|
791
|
-
}
|
|
792
|
-
if (isObject14(raw.estimate)) {
|
|
793
|
-
const e = raw.estimate;
|
|
794
|
-
const project = typeof e.project === "string" ? e.project : undefined;
|
|
795
|
-
const [lo, hi, lm, hm, at] = [e.lowUsd, e.highUsd, e.lowMin, e.highMin, e.at].map(num7);
|
|
796
|
-
if (project && lo !== undefined && hi !== undefined && lm !== undefined && hm !== undefined && at !== undefined)
|
|
797
|
-
out.estimate = { project, lowUsd: lo, highUsd: hi, lowMin: lm, highMin: hm, at };
|
|
798
|
-
}
|
|
799
|
-
return out;
|
|
800
|
-
}, parseSeatSession2 = (v) => {
|
|
801
|
-
if (!isObject14(v) || typeof v.id !== "string" || !v.id)
|
|
802
|
-
return;
|
|
803
|
-
const at = num7(v.at);
|
|
804
|
-
if (at === undefined)
|
|
805
|
-
return;
|
|
806
|
-
const out = { id: v.id, at };
|
|
807
|
-
if (typeof v.cwd === "string" && v.cwd)
|
|
808
|
-
out.cwd = v.cwd;
|
|
809
|
-
if (typeof v.model === "string" && v.model)
|
|
810
|
-
out.model = v.model;
|
|
811
|
-
return out;
|
|
812
|
-
}, seatSessionFromStdin2 = (text, now) => {
|
|
813
|
-
const trimmed = text.trim();
|
|
814
|
-
if (!trimmed)
|
|
815
|
-
return null;
|
|
816
|
-
try {
|
|
817
|
-
const raw = JSON.parse(trimmed);
|
|
818
|
-
if (!isObject14(raw) || typeof raw.session_id !== "string" || !raw.session_id)
|
|
819
|
-
return null;
|
|
820
|
-
const out = { id: raw.session_id, at: now };
|
|
821
|
-
if (typeof raw.cwd === "string" && raw.cwd)
|
|
822
|
-
out.cwd = raw.cwd;
|
|
823
|
-
const model = isObject14(raw.model) ? raw.model.id : raw.model;
|
|
824
|
-
if (typeof model === "string" && model)
|
|
825
|
-
out.model = model;
|
|
826
|
-
return out;
|
|
827
|
-
} catch {
|
|
828
|
-
return null;
|
|
829
|
-
}
|
|
830
|
-
}, bindingResetAt2 = (w, now) => {
|
|
831
|
-
if (!w)
|
|
832
|
-
return;
|
|
833
|
-
const bars = [
|
|
834
|
-
...w.fiveHour ? [w.fiveHour] : [],
|
|
835
|
-
...w.sevenDay ? [w.sevenDay] : [],
|
|
836
|
-
...w.spendLimit ? [w.spendLimit] : [],
|
|
837
|
-
...Object.values(w.models ?? {})
|
|
838
|
-
].filter((b) => b.resetsAt > now);
|
|
839
|
-
const full = bars.filter((b) => b.usedPct >= 100);
|
|
840
|
-
const pick = (full.length ? full : bars).map((b) => b.resetsAt);
|
|
841
|
-
return pick.length ? Math.min(...pick) : undefined;
|
|
842
|
-
}, readSeatState2 = (home) => {
|
|
843
|
-
const p2 = seatStatePath2(home);
|
|
844
|
-
if (!existsSync17(p2))
|
|
845
|
-
return {};
|
|
846
|
-
try {
|
|
847
|
-
return parseSeatState2(JSON.parse(readFileSync14(p2, "utf8")));
|
|
848
|
-
} catch {
|
|
849
|
-
return {};
|
|
850
|
-
}
|
|
851
|
-
}, writeSeatState2 = (home, state) => {
|
|
852
|
-
const dir = configDir(home);
|
|
853
|
-
mkdirSync14(dir, { recursive: true, mode: 448 });
|
|
854
|
-
const target = seatStatePath2(home);
|
|
855
|
-
const tmp = `${target}.${process.pid}.tmp`;
|
|
856
|
-
writeFileSync14(tmp, `${JSON.stringify(state)}
|
|
857
|
-
`, { mode: 384 });
|
|
858
|
-
renameSync7(tmp, target);
|
|
859
|
-
}, pctOf2 = (b) => b?.usedPct, shouldPostSeat2 = (state, next, now) => {
|
|
860
|
-
if (state.lastPostedAt === undefined || !state.lastPosted)
|
|
861
|
-
return true;
|
|
862
|
-
if (now - state.lastPostedAt >= SEAT_POST_MIN_INTERVAL_MS2)
|
|
863
|
-
return true;
|
|
864
|
-
const pairs = [
|
|
865
|
-
[state.lastPosted.fiveHour, pctOf2(next.fiveHour)],
|
|
866
|
-
[state.lastPosted.sevenDay, pctOf2(next.sevenDay)],
|
|
867
|
-
[state.lastPosted.spendLimit, pctOf2(next.spendLimit)]
|
|
868
|
-
];
|
|
869
|
-
for (const [prev, cur] of pairs) {
|
|
870
|
-
if (prev === undefined && cur === undefined)
|
|
871
|
-
continue;
|
|
872
|
-
if (prev === undefined || cur === undefined)
|
|
873
|
-
return true;
|
|
874
|
-
if (Math.abs(cur - prev) >= SEAT_POST_MIN_DELTA_PCT2)
|
|
875
|
-
return true;
|
|
876
|
-
}
|
|
877
|
-
return false;
|
|
878
|
-
}, markPosted2 = (state, w, now) => {
|
|
879
|
-
const lastPosted = {};
|
|
880
|
-
if (w.fiveHour)
|
|
881
|
-
lastPosted.fiveHour = w.fiveHour.usedPct;
|
|
882
|
-
if (w.sevenDay)
|
|
883
|
-
lastPosted.sevenDay = w.sevenDay.usedPct;
|
|
884
|
-
if (w.spendLimit)
|
|
885
|
-
lastPosted.spendLimit = w.spendLimit.usedPct;
|
|
886
|
-
return { ...state, window: w, lastPostedAt: now, lastPosted };
|
|
887
|
-
}, seatWallDue = (state, w) => {
|
|
888
|
-
if (!w?.sevenDay || w.sevenDay.usedPct < 100)
|
|
889
|
-
return false;
|
|
890
|
-
return state.lastWallResetsAt === undefined || state.lastWallResetsAt < w.sevenDay.resetsAt;
|
|
891
|
-
}, seatWallEvents2 = (state, w) => {
|
|
892
|
-
if (!w)
|
|
893
|
-
return [];
|
|
894
|
-
const out = [];
|
|
895
|
-
const bars = [
|
|
896
|
-
["fiveHour", w.fiveHour],
|
|
897
|
-
["sevenDay", w.sevenDay],
|
|
898
|
-
["spendLimit", w.spendLimit]
|
|
899
|
-
];
|
|
900
|
-
for (const [bar, b] of bars) {
|
|
901
|
-
if (!b)
|
|
902
|
-
continue;
|
|
903
|
-
const seen = state.warned?.[bar] ?? {};
|
|
904
|
-
const weeklyOld = bar === "sevenDay" && state.lastWallResetsAt !== undefined && state.lastWallResetsAt >= b.resetsAt;
|
|
905
|
-
if (b.usedPct >= APPROACHING_PCT2 && (seen.approaching ?? -1) < b.resetsAt && !weeklyOld)
|
|
906
|
-
out.push({ bar, stage: "approaching", usedPct: b.usedPct, resetsAt: b.resetsAt });
|
|
907
|
-
if (b.usedPct >= 100 && (seen.hit ?? -1) < b.resetsAt && !weeklyOld)
|
|
908
|
-
out.push({ bar, stage: "hit", usedPct: b.usedPct, resetsAt: b.resetsAt });
|
|
909
|
-
}
|
|
910
|
-
return out;
|
|
911
|
-
}, markWarned2 = (state, events) => {
|
|
912
|
-
if (events.length === 0)
|
|
913
|
-
return state;
|
|
914
|
-
const warned = { ...state.warned ?? {} };
|
|
915
|
-
let lastWallResetsAt = state.lastWallResetsAt;
|
|
916
|
-
for (const e of events) {
|
|
917
|
-
warned[e.bar] = { ...warned[e.bar] ?? {}, [e.stage]: e.resetsAt };
|
|
918
|
-
if (e.bar === "sevenDay" && e.stage === "hit")
|
|
919
|
-
lastWallResetsAt = e.resetsAt;
|
|
920
|
-
}
|
|
921
|
-
return { ...state, warned, ...lastWallResetsAt !== undefined ? { lastWallResetsAt } : {} };
|
|
922
|
-
}, CREDITS_PATTERN2, creditsHitIn2 = (text) => CREDITS_PATTERN2.test(text), transcriptPathFromStdin2 = (text) => {
|
|
923
|
-
try {
|
|
924
|
-
const raw = JSON.parse(text.trim());
|
|
925
|
-
return isObject14(raw) && typeof raw.transcript_path === "string" && raw.transcript_path ? raw.transcript_path : null;
|
|
926
|
-
} catch {
|
|
927
|
-
return null;
|
|
928
|
-
}
|
|
929
|
-
}, scanTranscriptForCredits2 = (path, prior) => {
|
|
930
|
-
const from = prior && prior.path === path ? prior.offset : 0;
|
|
931
|
-
try {
|
|
932
|
-
const size = statSync3(path).size;
|
|
933
|
-
if (size <= from)
|
|
934
|
-
return {
|
|
935
|
-
hit: false,
|
|
936
|
-
next: { path, offset: size, ...prior?.hitAt ? { hitAt: prior.hitAt } : {} }
|
|
937
|
-
};
|
|
938
|
-
const start = Math.max(from, size - 256 * 1024);
|
|
939
|
-
const fd = openSync2(path, "r");
|
|
940
|
-
try {
|
|
941
|
-
const buf = Buffer.alloc(size - start);
|
|
942
|
-
readSync2(fd, buf, 0, buf.length, start);
|
|
943
|
-
const hit = creditsHitIn2(buf.toString("utf8"));
|
|
944
|
-
return {
|
|
945
|
-
hit,
|
|
946
|
-
next: {
|
|
947
|
-
path,
|
|
948
|
-
offset: size,
|
|
949
|
-
...hit ? { hitAt: Date.now() } : prior?.hitAt ? { hitAt: prior.hitAt } : {}
|
|
950
|
-
}
|
|
951
|
-
};
|
|
952
|
-
} finally {
|
|
953
|
-
closeSync2(fd);
|
|
954
|
-
}
|
|
955
|
-
} catch {
|
|
956
|
-
return { hit: false, next: prior && prior.path === path ? prior : { path, offset: 0 } };
|
|
957
|
-
}
|
|
958
|
-
}, seatWindowBody2 = (w) => ({
|
|
959
|
-
capturedAt: w.capturedAt,
|
|
960
|
-
source: "claude",
|
|
961
|
-
...w.fiveHour ? { fiveHour: w.fiveHour } : {},
|
|
962
|
-
...w.sevenDay ? { sevenDay: w.sevenDay } : {},
|
|
963
|
-
...w.spendLimit ? { spendLimit: w.spendLimit } : {}
|
|
964
|
-
}), postJson2 = async (o2, path, body) => {
|
|
965
|
-
const fetchFn = o2.fetchFn ?? fetch;
|
|
966
|
-
try {
|
|
967
|
-
const res = await fetchFn(`${o2.proxyUrl}${path}`, {
|
|
968
|
-
method: "POST",
|
|
969
|
-
headers: { authorization: `Bearer ${o2.token}`, "content-type": "application/json" },
|
|
970
|
-
body: JSON.stringify(body),
|
|
971
|
-
signal: AbortSignal.timeout(o2.timeoutMs ?? SEAT_POST_TIMEOUT_MS2)
|
|
972
|
-
});
|
|
973
|
-
return res.ok;
|
|
974
|
-
} catch {
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
}, postSeatWindow2 = (o2, w) => postJson2(o2, "/proxy/seat", seatWindowBody2(w)), postSeatWall2 = (o2, input) => postJson2(o2, "/proxy/seat-wall", { source: "claude", ...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) => {
|
|
548
|
+
var SEAT_POST_MIN_INTERVAL_MS2, readStdin2 = (timeoutMs, stdin = process.stdin) => new Promise((resolve) => {
|
|
978
549
|
if (stdin.isTTY) {
|
|
979
|
-
|
|
550
|
+
resolve("");
|
|
980
551
|
return;
|
|
981
552
|
}
|
|
982
553
|
let data = "";
|
|
@@ -986,7 +557,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
|
|
|
986
557
|
return;
|
|
987
558
|
settled = true;
|
|
988
559
|
clearTimeout(timer);
|
|
989
|
-
|
|
560
|
+
resolve(data);
|
|
990
561
|
};
|
|
991
562
|
const timer = setTimeout(finish, timeoutMs);
|
|
992
563
|
stdin.setEncoding("utf8");
|
|
@@ -1001,12 +572,11 @@ var init_seat = __esm(() => {
|
|
|
1001
572
|
init_config();
|
|
1002
573
|
init_meter();
|
|
1003
574
|
SEAT_POST_MIN_INTERVAL_MS2 = 5 * 60000;
|
|
1004
|
-
CREDITS_PATTERN2 = /out of usage credits|cc_cli_limit_message/i;
|
|
1005
575
|
});
|
|
1006
576
|
|
|
1007
577
|
// src/index.ts
|
|
1008
578
|
import { realpathSync } from "fs";
|
|
1009
|
-
import { homedir, platform as
|
|
579
|
+
import { homedir, platform as platform3 } from "os";
|
|
1010
580
|
import { fileURLToPath } from "url";
|
|
1011
581
|
// ../../packages/shared/src/pricing.ts
|
|
1012
582
|
var p = (input, output, cacheWrite, cacheWrite1h, cacheRead) => ({ input, output, cacheWrite, cacheWrite1h, cacheRead });
|
|
@@ -1183,6 +753,33 @@ var AUDIENCE_LABEL = {
|
|
|
1183
753
|
everyone: "Each person, once"
|
|
1184
754
|
};
|
|
1185
755
|
var GUIDES = [
|
|
756
|
+
{
|
|
757
|
+
id: "board",
|
|
758
|
+
title: "Read the board",
|
|
759
|
+
audience: "everyone",
|
|
760
|
+
lede: "Sleds is the board: the pool of sized work, a sled per seat with the jobs moving on it, and what landed this week.",
|
|
761
|
+
steps: [
|
|
762
|
+
{
|
|
763
|
+
do: "Open `Sleds` and read the strip",
|
|
764
|
+
where: "Sleds",
|
|
765
|
+
note: "Five lanes: waiting for product · ready · moving · landed this week · worth landed. Click a lane to narrow the page; `Mine` shows your sled, the rows proposed for you and the asks you filed."
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
do: "Read a pool row: the band, the worth, the chip",
|
|
769
|
+
note: "`proposed` names the seat the rules (and the model) suggest, with why; `waiting for product` needs a yes; an amber note says a related or duplicate row exists. `Take it` puts it on your sled; an approver can `Put it on <seat>'s sled`."
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
do: "Read a seat card: the rails, the jobs, the band spent",
|
|
773
|
+
note: "Each rail is one agent on one platform with its own wall; the bar is the window used. A job says which rail it runs on and, at a wall, `paused at a wall` with `Move to <rail>` when the seat has headroom elsewhere."
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
do: "Open a row for the whole story",
|
|
777
|
+
note: "The panel: the brief and who filed it, what the AI added, the recommendation, milestones with holders, runs, the receipt when it lands, and every verb that applies to you."
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
check: "Your own sled shows under `Mine` with the jobs you hold; a landed row shows its measured time and verdict under `Landed this week` and on Record › Receipts.",
|
|
781
|
+
scope: "A product viewer sees seats and holders, never rails, models or spend; the words on the board are the words in chat — sled, seat, rail, job, wall, receipt."
|
|
782
|
+
},
|
|
1186
783
|
{
|
|
1187
784
|
id: "workspace",
|
|
1188
785
|
title: "Set up a workspace",
|
|
@@ -1243,9 +840,14 @@ var GUIDES = [
|
|
|
1243
840
|
note: "Your seat's bars and credits come back. If it answers that it does not know you, your invite and your Claude sign-in are different addresses — an admin fixes the member row."
|
|
1244
841
|
},
|
|
1245
842
|
{
|
|
1246
|
-
do: "
|
|
843
|
+
do: "Start asks in a Claude Project that has SLEDS on",
|
|
844
|
+
where: "claude.ai › Projects › New project › Connectors: SLEDS",
|
|
845
|
+
note: 'One line of project instructions — "Work described here goes on the sled: use SLEDS intake" — and nobody has to remember the connector. Anywhere else, "put it on the sled" is enough; the brief appearing as a card is the sign SLEDS is on.'
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
do: "Claude Code and the terminal: install the CLI and sign in through the browser",
|
|
1247
849
|
where: "`curl -fsSL sleds.ai/install | sh` then `sleds init`",
|
|
1248
|
-
note: "Installs the `/sled` skill and the status line
|
|
850
|
+
note: "`sleds init` opens app.sleds.ai/connect with a code; you sign in there (the same 6-digit code to your work email), approve, and the machine is your seat — nothing to paste, and you are in the workspace on the web in the same motion. Installs the `/sled` skill and the status line; `sleds doctor` says what is wired."
|
|
1249
851
|
},
|
|
1250
852
|
{
|
|
1251
853
|
do: "Optional: SLEDS for Chrome — the meter, and the Team roster and credits when you administer the plan",
|
|
@@ -1262,18 +864,9 @@ var GUIDES = [
|
|
|
1262
864
|
lede: "One GitHub App installation on your org: SLEDS sees PRs open and merge, and — only when someone asks — files and runs work from issues.",
|
|
1263
865
|
steps: [
|
|
1264
866
|
{
|
|
1265
|
-
do: "Install
|
|
1266
|
-
where: "
|
|
1267
|
-
note: "
|
|
1268
|
-
},
|
|
1269
|
-
{
|
|
1270
|
-
do: "Copy the installation number",
|
|
1271
|
-
note: "The page you land on ends in `/installations/<number>`. That number is the installation id."
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
do: "Connect the installation to this workspace",
|
|
1275
|
-
where: "Settings › Integrations › GitHub › `Connect`",
|
|
1276
|
-
note: "`App installation id` is the number; `Default repo (owner/name)` is where an ask filed from chat would land if it named no repo. The webhook secret is set on the app, not here — leave it blank."
|
|
867
|
+
do: "Install on GitHub",
|
|
868
|
+
where: "Settings › Integrations › GitHub › `Install on GitHub`",
|
|
869
|
+
note: "GitHub's own screen opens: pick your org and `Only select repositories` → the engineering repos. Do this signed in as an owner of the org (GitHub asks a member to request it instead — if that happens, come back and click the button again once an owner has installed). You land back here with the org named on the tile; nothing to copy."
|
|
1277
870
|
},
|
|
1278
871
|
{
|
|
1279
872
|
do: "Read-only for now: skip `Send test`",
|
|
@@ -1282,7 +875,7 @@ var GUIDES = [
|
|
|
1282
875
|
{
|
|
1283
876
|
do: "To keep every PR in sync: add the ledger workflow to each repo",
|
|
1284
877
|
where: "`.github/workflows/sleds-ledger.yml` from the SLEDS repo, with a repo secret `SLEDS_TOKEN`",
|
|
1285
|
-
note: "Mint a seat for CI (Settings › People, under a service person) and store its token as the secret. Then every PR is sized when it opens and closed on merge with the `SLEDS-Actual:` line from its body — the est → actual column on
|
|
878
|
+
note: "Mint a seat for CI (Settings › People, under a service person) and store its token as the secret. Then every PR is sized when it opens and closed on merge with the `SLEDS-Actual:` line from its body — the est → actual column on Record › Receipts. It comments on the PR; leave it out while strictly read-only."
|
|
1286
879
|
},
|
|
1287
880
|
{
|
|
1288
881
|
do: "When you are ready to write: add a `sled` label to the repos",
|
|
@@ -1311,7 +904,7 @@ var GUIDES = [
|
|
|
1311
904
|
{
|
|
1312
905
|
do: "Connect the site and name the projects to mirror",
|
|
1313
906
|
where: "Settings › Integrations › Jira › `Connect`",
|
|
1314
|
-
note: "`Site` is the host (`acme.atlassian.net`). `Projects to mirror, read-only` takes project keys (`WORK, CORE`): their open issues become sized rows
|
|
907
|
+
note: "`Site` is the host (`acme.atlassian.net`). `Projects to mirror, read-only` takes project keys (`WORK, CORE`): their open issues become sized rows in the pool on Sleds, epics with their children, and an issue that reaches Done lands its row — pulled every 30 minutes or with `Sync now`. Leave `Project key intake files into` blank for read-only; set it later and intake files a Jira issue and `plan` files sub-tasks."
|
|
1315
908
|
},
|
|
1316
909
|
{
|
|
1317
910
|
do: "Prove the token with `Send test` and the issue key left blank",
|
|
@@ -1323,47 +916,48 @@ var GUIDES = [
|
|
|
1323
916
|
note: "URL `https://<your deployment>.convex.site/jira/webhook` with a secret; events `issue created`, `issue updated`. Paste the same secret in `Webhook secret` on the Connect form. It acts only on issues labelled `sled`, `sled:now` or `intake`, or moved to `Ready for Sleds`."
|
|
1324
917
|
}
|
|
1325
918
|
],
|
|
1326
|
-
check: "The Jira tile says connected with the token's last four, the blank Send test names the account the token belongs to, and after `Sync now` the
|
|
919
|
+
check: "The Jira tile says connected with the token's last four, the blank Send test names the account the token belongs to, and after `Sync now` the pool on Sleds lists the mirrored project's open issues with a band on each.",
|
|
1327
920
|
scope: "With the project key blank and no `sled` labels in use, SLEDS never creates or comments on a Jira issue. The mirror only reads; rows carry the tracker's own state, labels, priority and assignee, never edited here."
|
|
1328
921
|
},
|
|
1329
922
|
{
|
|
1330
923
|
id: "slack",
|
|
1331
924
|
title: "Connect Slack",
|
|
1332
925
|
audience: "admin",
|
|
1333
|
-
lede: "
|
|
926
|
+
lede: "One button. SLEDS posts hand-offs, walls and digests to a channel of yours and DMs the person a thing is for — with nothing to create, copy or paste.",
|
|
1334
927
|
steps: [
|
|
1335
928
|
{
|
|
1336
|
-
do: "
|
|
1337
|
-
where: "
|
|
1338
|
-
note: "
|
|
1339
|
-
},
|
|
1340
|
-
{
|
|
1341
|
-
do: "Store the webhook",
|
|
1342
|
-
where: "Settings › Platform keys › provider `slack_webhook`"
|
|
929
|
+
do: "Add to Slack",
|
|
930
|
+
where: "Settings › Integrations › Slack › `Add to Slack`",
|
|
931
|
+
note: "Slack's own screen opens: choose the channel SLEDS posts to (private channels you belong to are in the list) and allow. You are back here in a few seconds with the team and channel shown on the tile. Nobody at SLEDS is involved and no key touches a clipboard."
|
|
1343
932
|
},
|
|
1344
933
|
{
|
|
1345
|
-
do: "
|
|
1346
|
-
where: "
|
|
1347
|
-
note: "
|
|
934
|
+
do: "If the channel is private, let the bot in",
|
|
935
|
+
where: "In Slack, in that channel: `/invite @SLEDS`",
|
|
936
|
+
note: "Posting to the channel works from the moment you allowed; reading and replying in a private channel needs the bot as a member."
|
|
1348
937
|
},
|
|
1349
938
|
{
|
|
1350
939
|
do: "Send one test post",
|
|
1351
940
|
where: "Settings › Integrations › Slack › `Send test`",
|
|
1352
|
-
note: "One real post to the channel
|
|
941
|
+
note: "One real post to the channel and one DM to you. Safe to run any time. If the DM part says Slack has no user for your email, you sign in to Slack with another address — set it under Settings › People › you › `Slack email, if different`."
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
do: "Advanced: keys by hand",
|
|
945
|
+
where: "Settings › Platform keys › `slack_webhook` and `slack`",
|
|
946
|
+
note: "Only for a Slack app of your own. Add to Slack writes both of these for you."
|
|
1353
947
|
}
|
|
1354
948
|
],
|
|
1355
|
-
check: "The test post arrives in the channel. From then on a hand-off, a wall, and a product decision each post there."
|
|
949
|
+
check: "The test post arrives in the channel and the DM in your Slack. From then on a hand-off, a wall, and a product decision each post there; a hand-off to a named coworker reaches them directly."
|
|
1356
950
|
},
|
|
1357
951
|
{
|
|
1358
952
|
id: "approval",
|
|
1359
953
|
title: "Turn on product's yes",
|
|
1360
954
|
audience: "admin",
|
|
1361
|
-
lede: "
|
|
955
|
+
lede: "An approval step before engineering can take a row: it waits until someone product-side says yes, or defers it with a note.",
|
|
1362
956
|
steps: [
|
|
1363
957
|
{
|
|
1364
|
-
do: "Choose
|
|
1365
|
-
where: "Policy › Product's yes › `
|
|
1366
|
-
note: "`Asks from
|
|
958
|
+
do: "Choose who needs a yes",
|
|
959
|
+
where: "Policy › Product's yes › `Who needs a yes`",
|
|
960
|
+
note: "`Asks filed from chat` gates what the business side files and leaves tracker issues ready as they are; `Everything` gates engineers' own briefs too. `Nobody` is the default — everything filed is ready to take."
|
|
1367
961
|
},
|
|
1368
962
|
{
|
|
1369
963
|
do: "Name the approvers",
|
|
@@ -1371,8 +965,8 @@ var GUIDES = [
|
|
|
1371
965
|
note: "Emails, comma-separated. Blank means any owner or admin of the workspace."
|
|
1372
966
|
},
|
|
1373
967
|
{
|
|
1374
|
-
do: "Approvers decide from the
|
|
1375
|
-
where: "
|
|
968
|
+
do: "Approvers decide from the board or the Overview",
|
|
969
|
+
where: "Overview › `Waiting on product`, or the row on Sleds",
|
|
1376
970
|
note: "`Approve` opens the gate; `Defer` keeps it closed and carries the note back to the filer, who answers in the brief and it comes back. In chat the same is `approve {project, decision, note?}`."
|
|
1377
971
|
},
|
|
1378
972
|
{
|
|
@@ -1380,7 +974,7 @@ var GUIDES = [
|
|
|
1380
974
|
note: "An approved epic can be planned in chat (`plan`): its milestones are sized, filed under it, and inherit the yes — they do not queue again. The epic's cap is the sum."
|
|
1381
975
|
}
|
|
1382
976
|
],
|
|
1383
|
-
check: "File an ask from chat (`intake`): its row shows waiting for product
|
|
977
|
+
check: "File an ask from chat (`intake`): its row shows waiting for product in the pool on Sleds, the approvers see it under Waiting on product, and `run` on it is refused until someone approves."
|
|
1384
978
|
}
|
|
1385
979
|
];
|
|
1386
980
|
function findGuides(filter) {
|
|
@@ -1436,7 +1030,7 @@ var VERBS = [
|
|
|
1436
1030
|
want: "See my overnight jobs and the team's open hand-offs",
|
|
1437
1031
|
mcp: "jobs · handoffs",
|
|
1438
1032
|
claudeCode: "/sled jobs · `sleds handoffs`",
|
|
1439
|
-
app: "
|
|
1033
|
+
app: "Sleds › Moving (the runs on each seat's sled) · Record › Receipts once they land",
|
|
1440
1034
|
how: "Newest first: id, status, route, cost. Hand-offs meant for you are listed first."
|
|
1441
1035
|
},
|
|
1442
1036
|
{
|
|
@@ -1444,16 +1038,40 @@ var VERBS = [
|
|
|
1444
1038
|
group: "meter",
|
|
1445
1039
|
want: "Ask where a piece of work is",
|
|
1446
1040
|
mcp: "status {q?} — a tracker key or a word from the title; nothing for what you filed",
|
|
1447
|
-
app: "
|
|
1041
|
+
app: "Sleds (the strip and the row's stage: waiting for product · deferred · ready · moving · landed)",
|
|
1448
1042
|
how: "One paragraph per row: its stage, who has it, the band, the worth, the tree when it has milestones, the PR when it landed. Yours by default; a lead widens to the team."
|
|
1449
1043
|
},
|
|
1044
|
+
{
|
|
1045
|
+
id: "touches",
|
|
1046
|
+
group: "meter",
|
|
1047
|
+
want: "Know what a change reaches before I ask for it",
|
|
1048
|
+
mcp: "touches {text}",
|
|
1049
|
+
app: "Settings › Integrations › GitHub › Product map",
|
|
1050
|
+
how: "The product map — built from the workspace's repos on every merge, no model — says which areas of the product an ask reaches, which of the product's own documents it names, and what on the board collides with it. Your AI reads it back in plain words; intake reads it on its own while drafting."
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
id: "attach",
|
|
1054
|
+
group: "meter",
|
|
1055
|
+
want: "Put the spec, the prototype or a screenshot on the row so the team can open it",
|
|
1056
|
+
mcp: "attach {project, name, content, mime?, kind?} — intake takes the same as `attachments` when filing",
|
|
1057
|
+
app: "Sleds › the row › Assets",
|
|
1058
|
+
how: "The file itself lives in SLEDS, not only in the chat that made it: a spec as markdown, a prototype as one HTML page (opens and renders in its own tab), a screenshot. Up to 4 MB; the same name again replaces. Anyone on the board opens it from the row; the person who added it, or an admin, can remove it."
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
id: "answer",
|
|
1062
|
+
group: "meter",
|
|
1063
|
+
want: "Answer a question engineering asked about something I filed",
|
|
1064
|
+
mcp: "answer {answer, project?, id?} — nothing to see what is waiting on you",
|
|
1065
|
+
app: "Sleds › the row › Questions",
|
|
1066
|
+
how: "An approver asks on the row; you hear by bell, DM and email. Your reply, here or on the row, goes on the record and the asker hears at once. A worth question answered with a level or a band sets the row's worth."
|
|
1067
|
+
},
|
|
1450
1068
|
{
|
|
1451
1069
|
id: "estimate",
|
|
1452
1070
|
group: "size",
|
|
1453
1071
|
want: "Size a task before running it",
|
|
1454
1072
|
mcp: "estimate {spec, source?}",
|
|
1455
1073
|
claudeCode: "/sled estimate · `sleds spec-event` fires on save of a spec file",
|
|
1456
|
-
app: "
|
|
1074
|
+
app: "Sleds (rows appear in the pool as specs and issues are sized)",
|
|
1457
1075
|
how: "A cost and time band with confidence and a spend cap — never a point figure. Recorded; the actual calibrates the next one."
|
|
1458
1076
|
},
|
|
1459
1077
|
{
|
|
@@ -1462,7 +1080,7 @@ var VERBS = [
|
|
|
1462
1080
|
want: "Say what the work is worth to the business",
|
|
1463
1081
|
mcp: "a `Worth:` line in the spec you estimate (`Worth: high · $5k–20k · Acme renewal`)",
|
|
1464
1082
|
claudeCode: "`SLEDS-Worth:` line in the PR body",
|
|
1465
|
-
app: "
|
|
1083
|
+
app: "Sleds › the row › Worth…",
|
|
1466
1084
|
how: "A level, dollars when you know them, a note. Yours to state; the tracker's priority stands in at low confidence; blank stays blank."
|
|
1467
1085
|
},
|
|
1468
1086
|
{
|
|
@@ -1470,15 +1088,15 @@ var VERBS = [
|
|
|
1470
1088
|
group: "send",
|
|
1471
1089
|
want: "Send an ask to engineering from the conversation I already have open",
|
|
1472
1090
|
mcp: "intake {text, links?, answers?, confirm?, parent?}",
|
|
1473
|
-
app: "
|
|
1474
|
-
how: "Your AI reads what you sent and asks only what it cannot answer — who it is for, what done looks like, the smallest version, the worth, what it touches — then shows the brief. On yes the row is filed on the sled and sized. Nothing is written before yes; worth is never guessed. `parent` files it under an epic or the ask it spun off from, so the parent's tree cost is true."
|
|
1091
|
+
app: "Sleds (the row lands in the pool sized, filed by you, at waiting or ready)",
|
|
1092
|
+
how: "Your AI reads what you sent and asks only what it cannot answer — who it is for, what done looks like, the smallest version, the worth, what it touches, how many have asked — then shows the brief. On yes the row is filed on the sled and sized. Nothing is written before yes; worth is never guessed. `parent` files it under an epic or the ask it spun off from, so the parent's tree cost is true."
|
|
1475
1093
|
},
|
|
1476
1094
|
{
|
|
1477
1095
|
id: "plan",
|
|
1478
1096
|
group: "send",
|
|
1479
1097
|
want: "Turn an approved epic into milestones I can send one at a time",
|
|
1480
1098
|
mcp: "plan {q | project, milestones?, confirm?} — the epic first, then the drafted list",
|
|
1481
|
-
app: "
|
|
1099
|
+
app: "Sleds (the epic row expands into its milestones; the tree under its band) · Approve · plan it",
|
|
1482
1100
|
how: "Your AI drafts 3–7 vertical slices from the epic's brief — each something a person could use on its own — and shows them. On yes SLEDS sizes each, files it under the epic with the epic's worth and approval (a Jira sub-task when the epic lives there), and says what the tree will cost. Nothing is filed before yes."
|
|
1483
1101
|
},
|
|
1484
1102
|
{
|
|
@@ -1486,7 +1104,7 @@ var VERBS = [
|
|
|
1486
1104
|
group: "send",
|
|
1487
1105
|
want: "Give product's yes on an ask — or say not now, and why",
|
|
1488
1106
|
mcp: "approve {q, decision: approve | defer, note?}",
|
|
1489
|
-
app: "
|
|
1107
|
+
app: "Sleds › Approve / Defer on a waiting row · Overview › Waiting on product · Policy › Product's yes",
|
|
1490
1108
|
how: "Off by default. When Policy says asks from intake (or every row) wait for product, a row sits at waiting until an approver — the named approvers, else an owner or admin — approves it; nobody can run it before. Defer keeps it closed and sends the note to the filer; they answer in the brief and it comes back."
|
|
1491
1109
|
},
|
|
1492
1110
|
{
|
|
@@ -1494,7 +1112,7 @@ var VERBS = [
|
|
|
1494
1112
|
group: "send",
|
|
1495
1113
|
want: "Ask where a piece of work should go, then send it there",
|
|
1496
1114
|
mcp: "dispatch {q, run?: now | park | overnight, to?} — suggests with no run",
|
|
1497
|
-
app: "
|
|
1115
|
+
app: "Sleds › Take it (onto your own sled) · Sleds › the row › Send to… · Put it on <seat>'s sled",
|
|
1498
1116
|
how: "Now on the seat with the most weekly headroom, overnight at half price, parked for the team, or not worth it yet — with the reasons. Nothing runs until you say run."
|
|
1499
1117
|
},
|
|
1500
1118
|
{
|
|
@@ -1503,7 +1121,7 @@ var VERBS = [
|
|
|
1503
1121
|
want: "Send work to a teammate or a buddy",
|
|
1504
1122
|
mcp: "handoff {task, to?, note?}",
|
|
1505
1123
|
claudeCode: "/sled handoff · the wall popup",
|
|
1506
|
-
app: "
|
|
1124
|
+
app: "Sleds › the row › Send to… · extension › Park hand-off",
|
|
1507
1125
|
how: "Parks a self-contained brief (the picker starts cold). Named by email — a teammate, or a paired buddy in another workspace — or anyone on the team."
|
|
1508
1126
|
},
|
|
1509
1127
|
{
|
|
@@ -1512,7 +1130,7 @@ var VERBS = [
|
|
|
1512
1130
|
want: "Take back a hand-off nobody has picked up",
|
|
1513
1131
|
mcp: "withdraw {handoffId}",
|
|
1514
1132
|
claudeCode: "`sleds withdraw <id>`",
|
|
1515
|
-
app: "
|
|
1133
|
+
app: "Sleds › the row › Withdraw",
|
|
1516
1134
|
how: "Only the seat that parked it can withdraw it. Unpicked hand-offs are reminded after a day and expire after three."
|
|
1517
1135
|
},
|
|
1518
1136
|
{
|
|
@@ -1521,7 +1139,7 @@ var VERBS = [
|
|
|
1521
1139
|
want: "Pick up work someone sent me",
|
|
1522
1140
|
mcp: "handoffs → pickup {handoffId}",
|
|
1523
1141
|
claudeCode: "/sled pickup <id> · `sleds pickup <id>`",
|
|
1524
|
-
app: "
|
|
1142
|
+
app: "Sleds › Take it · extension › Take it",
|
|
1525
1143
|
how: "A parked task comes back verbatim; a walled session comes back as its first and last messages. Runs on your seat, metered to your workspace."
|
|
1526
1144
|
},
|
|
1527
1145
|
{
|
|
@@ -1531,7 +1149,7 @@ var VERBS = [
|
|
|
1531
1149
|
mcp: "defer {at?, task?}",
|
|
1532
1150
|
claudeCode: "`sleds defer` · the wall popup › Hold until reset",
|
|
1533
1151
|
app: "wall popup",
|
|
1534
|
-
how: "The
|
|
1152
|
+
how: "The night shift on your Mac resumes the session at the binding reset; no key needed. `+2h` or an ISO time overrides the reset."
|
|
1535
1153
|
},
|
|
1536
1154
|
{
|
|
1537
1155
|
id: "overnight",
|
|
@@ -1539,7 +1157,7 @@ var VERBS = [
|
|
|
1539
1157
|
want: "Run this tonight at half price",
|
|
1540
1158
|
mcp: "overnight {spec, source?}",
|
|
1541
1159
|
claudeCode: "/sled overnight",
|
|
1542
|
-
app: "wall popup › Run overnight ·
|
|
1160
|
+
app: "wall popup › Run overnight · Sleds › the Night shift seat",
|
|
1543
1161
|
how: "Sized first; the cap bounds the spend. Runs in the workspace's overnight window on the batch tier; `/sled resume <id>` pulls it back in."
|
|
1544
1162
|
},
|
|
1545
1163
|
{
|
|
@@ -1547,8 +1165,8 @@ var VERBS = [
|
|
|
1547
1165
|
group: "defer",
|
|
1548
1166
|
want: "Pull a finished overnight job into my session",
|
|
1549
1167
|
claudeCode: "/sled resume <id>",
|
|
1550
|
-
app: "
|
|
1551
|
-
how: "The delivered result, with its
|
|
1168
|
+
app: "Sleds › the Night shift seat › the row",
|
|
1169
|
+
how: "The delivered result, with its record of which model built it — which model built it, signed by the workspace key."
|
|
1552
1170
|
},
|
|
1553
1171
|
{
|
|
1554
1172
|
id: "actual",
|
|
@@ -1556,7 +1174,7 @@ var VERBS = [
|
|
|
1556
1174
|
want: "Record what a piece of work actually took",
|
|
1557
1175
|
mcp: "actual {source, minutes?, usd?, model?, ref?}",
|
|
1558
1176
|
claudeCode: "`SLEDS-Actual: 45 min · $7` in the PR body — the ledger posts it on merge",
|
|
1559
|
-
app: "
|
|
1177
|
+
app: "Record › Receipts (fills in on merge or delivery) · Sleds › Landed this week",
|
|
1560
1178
|
how: "Closes the estimate: est → actual per row, and the calibration for the next band. A delivered job closes its own."
|
|
1561
1179
|
},
|
|
1562
1180
|
{
|
|
@@ -1564,7 +1182,7 @@ var VERBS = [
|
|
|
1564
1182
|
group: "close",
|
|
1565
1183
|
want: "Check which model built something",
|
|
1566
1184
|
claudeCode: "`sleds verify <jobId | SLEDS-Attest line>`",
|
|
1567
|
-
app: "
|
|
1185
|
+
app: "Record › Receipts › the row's ref",
|
|
1568
1186
|
how: "Ed25519 over the job, model, outputs and time; verified locally against the workspace's public key."
|
|
1569
1187
|
},
|
|
1570
1188
|
{
|
|
@@ -1579,7 +1197,7 @@ var VERBS = [
|
|
|
1579
1197
|
id: "team",
|
|
1580
1198
|
group: "people",
|
|
1581
1199
|
want: "See the team's work — in flight, landed, waiting",
|
|
1582
|
-
app: "Overview ·
|
|
1200
|
+
app: "Overview · Sleds · Record at the team zoom",
|
|
1583
1201
|
how: "Same screens, wider scope, summed by role. Aggregates by default; nobody's spend to anyone else without their say-so."
|
|
1584
1202
|
},
|
|
1585
1203
|
{
|
|
@@ -1686,14 +1304,16 @@ var QUESTIONS = {
|
|
|
1686
1304
|
done: "What does done look like, in your words?",
|
|
1687
1305
|
smallest: "What is the smallest version still worth doing?",
|
|
1688
1306
|
worth: "What is it worth to the business — a customer, a number, or high / medium / low?",
|
|
1689
|
-
touches: "What does it touch — which product areas or systems?"
|
|
1307
|
+
touches: "What does it touch — which product areas or systems, and where in the product would it live?",
|
|
1308
|
+
demand: "How many people have asked for this, or how often does it come up?"
|
|
1690
1309
|
};
|
|
1691
1310
|
var REQUIRED_TO_FILE = ["why", "done"];
|
|
1692
1311
|
var HEADINGS = {
|
|
1693
1312
|
why: /^(why|context|background|problem|motivation|goal|purpose|the ask)\b/i,
|
|
1694
1313
|
done: /^(done|done when|definition of done|acceptance|acceptance criteria|success|outcome|expected)\b/i,
|
|
1695
1314
|
smallest: /^(smallest|smallest version|mvp|minimum|minimal|first cut|v1|scope|in scope)\b/i,
|
|
1696
|
-
touches: /^(touches|affects|areas?|surfaces?|systems?|components?|files)\b/i
|
|
1315
|
+
touches: /^(touches|affects|areas?|surfaces?|systems?|components?|files|where)\b/i,
|
|
1316
|
+
demand: /^(demand|requests?|requested by|how many|how often|asked for|frequency|volume)\b/i
|
|
1697
1317
|
};
|
|
1698
1318
|
var clip = (s, max) => {
|
|
1699
1319
|
const t = s.replace(/\s+/g, " ").trim();
|
|
@@ -1788,13 +1408,14 @@ function draftBrief(input) {
|
|
|
1788
1408
|
done: pick("done"),
|
|
1789
1409
|
smallest: pick("smallest"),
|
|
1790
1410
|
touches: pick("touches"),
|
|
1411
|
+
demand: pick("demand"),
|
|
1791
1412
|
...worth ? { worth } : {},
|
|
1792
1413
|
sources
|
|
1793
1414
|
};
|
|
1794
1415
|
}
|
|
1795
1416
|
function openQuestions(b) {
|
|
1796
1417
|
const out = [];
|
|
1797
|
-
const keys = ["why", "done", "smallest", "worth", "touches"];
|
|
1418
|
+
const keys = ["why", "done", "smallest", "worth", "touches", "demand"];
|
|
1798
1419
|
for (const key of keys) {
|
|
1799
1420
|
const missing = key === "worth" ? !b.worth : !b[key];
|
|
1800
1421
|
if (missing)
|
|
@@ -1805,7 +1426,7 @@ function openQuestions(b) {
|
|
|
1805
1426
|
function missingToFile(b) {
|
|
1806
1427
|
return REQUIRED_TO_FILE.filter((k) => !b[k]);
|
|
1807
1428
|
}
|
|
1808
|
-
var fromWord = (f) => f.from === "filer" ? "from you" : "from what you sent";
|
|
1429
|
+
var fromWord = (f) => f.from === "filer" ? "from you" : f.from === "product" ? "from the product map" : "from what you sent";
|
|
1809
1430
|
function renderBrief(b) {
|
|
1810
1431
|
const parts = [b.ask.text];
|
|
1811
1432
|
if (b.why)
|
|
@@ -1817,10 +1438,42 @@ function renderBrief(b) {
|
|
|
1817
1438
|
parts.push(`worth: ${b.worth ? worthLabel(b.worth) : "not stated"}`);
|
|
1818
1439
|
if (b.touches)
|
|
1819
1440
|
parts.push(`touches (${fromWord(b.touches)}): ${b.touches.text}`);
|
|
1441
|
+
if (b.demand)
|
|
1442
|
+
parts.push(`demand (${fromWord(b.demand)}): ${b.demand.text}`);
|
|
1820
1443
|
if (b.sources.length > 0)
|
|
1821
1444
|
parts.push(`sources: ${b.sources.join(", ")}`);
|
|
1822
1445
|
return `Brief: ${parts.join(" · ")}`;
|
|
1823
1446
|
}
|
|
1447
|
+
function renderBriefMarkdown(b, open, extra = {}) {
|
|
1448
|
+
const cell = (t) => t.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
1449
|
+
const rows = ["| | | |", "|---|---|---|"];
|
|
1450
|
+
const field = (label, f) => {
|
|
1451
|
+
if (f)
|
|
1452
|
+
rows.push(`| **${label}** | ${cell(f.text)} | ${fromWord(f)} |`);
|
|
1453
|
+
};
|
|
1454
|
+
rows.push(`| **Ask** | ${cell(b.ask.text)} | ${fromWord(b.ask)} |`);
|
|
1455
|
+
field("Why", b.why);
|
|
1456
|
+
field("Done", b.done);
|
|
1457
|
+
field("Smallest", b.smallest);
|
|
1458
|
+
rows.push(`| **Worth** | ${b.worth ? cell(worthLabel(b.worth)) : "_not stated_"} | ${b.worth ? "stated" : ""} |`);
|
|
1459
|
+
field("Touches", b.touches);
|
|
1460
|
+
field("Demand", b.demand);
|
|
1461
|
+
if (b.due)
|
|
1462
|
+
rows.push(`| **Due** | ${b.due} | from your words |`);
|
|
1463
|
+
const out = ["**Brief — draft**", "", ...rows];
|
|
1464
|
+
const files = [...b.sources, ...extra.files ?? []];
|
|
1465
|
+
if (files.length)
|
|
1466
|
+
out.push("", `Files: ${files.map((f) => `\`${cell(f)}\``).join(" · ")}`);
|
|
1467
|
+
if (extra.map)
|
|
1468
|
+
out.push("", `> ${cell(extra.map)}`);
|
|
1469
|
+
if (open.length) {
|
|
1470
|
+
out.push("", `**Still open** — ${open.length === 1 ? "one question" : `${open.length} questions`}:`, "");
|
|
1471
|
+
open.forEach((q, i) => out.push(`${i + 1}. ${q.question}`));
|
|
1472
|
+
} else
|
|
1473
|
+
out.push("", "Nothing open — say yes to file it.");
|
|
1474
|
+
return out.join(`
|
|
1475
|
+
`);
|
|
1476
|
+
}
|
|
1824
1477
|
function briefToSpec(b) {
|
|
1825
1478
|
const lines = [b.ask.text, ""];
|
|
1826
1479
|
if (b.why)
|
|
@@ -1831,6 +1484,8 @@ function briefToSpec(b) {
|
|
|
1831
1484
|
lines.push(`Smallest: ${b.smallest.text}`);
|
|
1832
1485
|
if (b.touches)
|
|
1833
1486
|
lines.push(`Touches: ${b.touches.text}`);
|
|
1487
|
+
if (b.demand)
|
|
1488
|
+
lines.push(`Demand: ${b.demand.text}`);
|
|
1834
1489
|
if (b.worth)
|
|
1835
1490
|
lines.push(`Worth: ${worthLabel(b.worth)}`);
|
|
1836
1491
|
if (b.sources.length > 0)
|
|
@@ -1844,7 +1499,8 @@ function intakeReply(i) {
|
|
|
1844
1499
|
const blank = i.blank.length > 0 ? ` Still blank: ${i.blank.join(", ")} — say them any time and the row updates.` : "";
|
|
1845
1500
|
const under = i.parent ? ` under ${i.parent}` : "";
|
|
1846
1501
|
const next = i.needsApproval ? "It waits for product's yes — they approve or defer it with a note; status tells you which." : "An engineer takes it from the sled; you will hear when it lands.";
|
|
1847
|
-
|
|
1502
|
+
const name = i.key ? `${i.key} · ${i.title}` : i.title;
|
|
1503
|
+
return `On the sled as ${name}${under} — filed in ${i.filedIn}. ${size}, ${worth}.${blank} ${next}`;
|
|
1848
1504
|
}
|
|
1849
1505
|
function parseWorthBand(raw) {
|
|
1850
1506
|
if (typeof raw !== "object" || raw === null)
|
|
@@ -1868,9 +1524,140 @@ function parseWorthBand(raw) {
|
|
|
1868
1524
|
...typeof w.note === "string" && w.note.trim() ? { note: clip(w.note, 140) } : {}
|
|
1869
1525
|
};
|
|
1870
1526
|
}
|
|
1527
|
+
// ../../packages/shared/src/intake/related.ts
|
|
1528
|
+
var STOP = new Set([
|
|
1529
|
+
"a",
|
|
1530
|
+
"an",
|
|
1531
|
+
"the",
|
|
1532
|
+
"and",
|
|
1533
|
+
"or",
|
|
1534
|
+
"of",
|
|
1535
|
+
"to",
|
|
1536
|
+
"in",
|
|
1537
|
+
"on",
|
|
1538
|
+
"for",
|
|
1539
|
+
"with",
|
|
1540
|
+
"by",
|
|
1541
|
+
"at",
|
|
1542
|
+
"from",
|
|
1543
|
+
"as",
|
|
1544
|
+
"is",
|
|
1545
|
+
"are",
|
|
1546
|
+
"be",
|
|
1547
|
+
"it",
|
|
1548
|
+
"its",
|
|
1549
|
+
"this",
|
|
1550
|
+
"that",
|
|
1551
|
+
"we",
|
|
1552
|
+
"our",
|
|
1553
|
+
"add",
|
|
1554
|
+
"new",
|
|
1555
|
+
"make",
|
|
1556
|
+
"fix",
|
|
1557
|
+
"update",
|
|
1558
|
+
"support",
|
|
1559
|
+
"when",
|
|
1560
|
+
"into",
|
|
1561
|
+
"up",
|
|
1562
|
+
"via"
|
|
1563
|
+
]);
|
|
1871
1564
|
// ../../packages/shared/src/dispatch/plan.ts
|
|
1872
1565
|
init_format();
|
|
1873
1566
|
|
|
1567
|
+
// ../../packages/shared/src/intake/due.ts
|
|
1568
|
+
var MONTHS = {
|
|
1569
|
+
jan: 1,
|
|
1570
|
+
january: 1,
|
|
1571
|
+
feb: 2,
|
|
1572
|
+
february: 2,
|
|
1573
|
+
mar: 3,
|
|
1574
|
+
march: 3,
|
|
1575
|
+
apr: 4,
|
|
1576
|
+
april: 4,
|
|
1577
|
+
may: 5,
|
|
1578
|
+
jun: 6,
|
|
1579
|
+
june: 6,
|
|
1580
|
+
jul: 7,
|
|
1581
|
+
july: 7,
|
|
1582
|
+
aug: 8,
|
|
1583
|
+
august: 8,
|
|
1584
|
+
sep: 9,
|
|
1585
|
+
sept: 9,
|
|
1586
|
+
september: 9,
|
|
1587
|
+
oct: 10,
|
|
1588
|
+
october: 10,
|
|
1589
|
+
nov: 11,
|
|
1590
|
+
november: 11,
|
|
1591
|
+
dec: 12,
|
|
1592
|
+
december: 12
|
|
1593
|
+
};
|
|
1594
|
+
var MONTH_RE = "(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)";
|
|
1595
|
+
var LEAD = "(?:by|before|due|until|no later than|target(?:ed)?(?: for)?|deadline(?: is)?|needed(?: by)?|end of|eo)\\s*(?:the\\s+)?";
|
|
1596
|
+
var iso = (y, m, d) => {
|
|
1597
|
+
if (m < 1 || m > 12 || d < 1 || d > 31)
|
|
1598
|
+
return null;
|
|
1599
|
+
const t = Date.UTC(y, m - 1, d);
|
|
1600
|
+
const dt = new Date(t);
|
|
1601
|
+
if (dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d)
|
|
1602
|
+
return null;
|
|
1603
|
+
return dt.toISOString().slice(0, 10);
|
|
1604
|
+
};
|
|
1605
|
+
var lastDay = (y, m) => new Date(Date.UTC(y, m, 0)).getUTCDate();
|
|
1606
|
+
function yearFor(m, d, now) {
|
|
1607
|
+
const n = new Date(now);
|
|
1608
|
+
const y = n.getUTCFullYear();
|
|
1609
|
+
const candidate = Date.UTC(y, m - 1, d);
|
|
1610
|
+
return candidate < Date.UTC(y, n.getUTCMonth(), n.getUTCDate()) - 86400000 ? y + 1 : y;
|
|
1611
|
+
}
|
|
1612
|
+
function parseDue(text, now = Date.now()) {
|
|
1613
|
+
if (!text)
|
|
1614
|
+
return null;
|
|
1615
|
+
const t = text.replace(/\s+/g, " ");
|
|
1616
|
+
const isoM = new RegExp(`(?:${LEAD})?(\\d{4})-(\\d{2})-(\\d{2})\\b`, "i").exec(t);
|
|
1617
|
+
if (isoM) {
|
|
1618
|
+
const r = iso(Number(isoM[1]), Number(isoM[2]), Number(isoM[3]));
|
|
1619
|
+
if (r)
|
|
1620
|
+
return r;
|
|
1621
|
+
}
|
|
1622
|
+
const mdy = new RegExp(`${LEAD}${MONTH_RE}\\.?\\s+(\\d{1,2})(?:st|nd|rd|th)?(?:,?\\s+(\\d{4}))?\\b`, "i").exec(t);
|
|
1623
|
+
if (mdy) {
|
|
1624
|
+
const m = MONTHS[(mdy[1] ?? "").toLowerCase()] ?? 0;
|
|
1625
|
+
const d = Number(mdy[2]);
|
|
1626
|
+
const y = mdy[3] ? Number(mdy[3]) : yearFor(m, d, now);
|
|
1627
|
+
const r = iso(y, m, d);
|
|
1628
|
+
if (r)
|
|
1629
|
+
return r;
|
|
1630
|
+
}
|
|
1631
|
+
const dmy = new RegExp(`${LEAD}(\\d{1,2})(?:st|nd|rd|th)?\\s+(?:of\\s+)?${MONTH_RE}\\.?(?:,?\\s+(\\d{4}))?\\b`, "i").exec(t);
|
|
1632
|
+
if (dmy) {
|
|
1633
|
+
const d = Number(dmy[1]);
|
|
1634
|
+
const m = MONTHS[(dmy[2] ?? "").toLowerCase()] ?? 0;
|
|
1635
|
+
const y = dmy[3] ? Number(dmy[3]) : yearFor(m, d, now);
|
|
1636
|
+
const r = iso(y, m, d);
|
|
1637
|
+
if (r)
|
|
1638
|
+
return r;
|
|
1639
|
+
}
|
|
1640
|
+
const eom = new RegExp(`(?:end of|eo)\\s*(?:the\\s+)?(?:${MONTH_RE}|month|m\\b)`, "i").exec(t);
|
|
1641
|
+
if (eom) {
|
|
1642
|
+
const n = new Date(now);
|
|
1643
|
+
const named = eom[1] ? MONTHS[eom[1].toLowerCase()] ?? 0 : 0;
|
|
1644
|
+
const m = named || n.getUTCMonth() + 1;
|
|
1645
|
+
const y = named ? yearFor(m, 1, now) : n.getUTCFullYear();
|
|
1646
|
+
return iso(y, m, lastDay(y, m));
|
|
1647
|
+
}
|
|
1648
|
+
const slash = new RegExp(`${LEAD}(\\d{1,2})/(\\d{1,2})(?:/(\\d{2,4}))?\\b`, "i").exec(t);
|
|
1649
|
+
if (slash) {
|
|
1650
|
+
const m = Number(slash[1]);
|
|
1651
|
+
const d = Number(slash[2]);
|
|
1652
|
+
const yy = slash[3] ? Number(slash[3]) : null;
|
|
1653
|
+
const y = yy === null ? yearFor(m, d, now) : yy < 100 ? 2000 + yy : yy;
|
|
1654
|
+
const r = iso(y, m, d);
|
|
1655
|
+
if (r)
|
|
1656
|
+
return r;
|
|
1657
|
+
}
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1874
1661
|
// ../../packages/shared/src/mcp/server.ts
|
|
1875
1662
|
init_format();
|
|
1876
1663
|
|
|
@@ -1879,56 +1666,112 @@ init_format();
|
|
|
1879
1666
|
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1880
1667
|
var str2 = (v) => typeof v === "string" ? v : undefined;
|
|
1881
1668
|
var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
1882
|
-
function statusName(
|
|
1883
|
-
const source = isObject2(
|
|
1669
|
+
function statusName(row) {
|
|
1670
|
+
const source = isObject2(row.source) ? row.source : {};
|
|
1884
1671
|
const kind = str2(source.kind);
|
|
1885
1672
|
const doc = str2(source.doc);
|
|
1886
|
-
const title = str2(
|
|
1887
|
-
if ((kind === "linear" || kind === "github") && doc)
|
|
1673
|
+
const title = str2(row.title) ?? str2(row.project) ?? "this row";
|
|
1674
|
+
if ((kind === "linear" || kind === "github" || kind === "jira") && doc)
|
|
1888
1675
|
return `${doc} · ${title}`;
|
|
1889
|
-
|
|
1676
|
+
const key = str2(row.key);
|
|
1677
|
+
return key ? `${key} · ${title}` : title;
|
|
1890
1678
|
}
|
|
1891
|
-
function stateTail(
|
|
1892
|
-
const retired = str2(
|
|
1679
|
+
function stateTail(row) {
|
|
1680
|
+
const retired = str2(row.retired);
|
|
1893
1681
|
if (retired)
|
|
1894
1682
|
return ` Retired: ${retired}.`;
|
|
1895
|
-
if (
|
|
1683
|
+
if (row.stale === true)
|
|
1896
1684
|
return " Stale: no movement in 14 days.";
|
|
1897
1685
|
return "";
|
|
1898
1686
|
}
|
|
1899
|
-
function statusLine(
|
|
1900
|
-
return `${statusBody(
|
|
1687
|
+
function statusLine(row, now) {
|
|
1688
|
+
return `${statusBody(row, now)}${trackerTail(row)}${stateTail(row)}${assetsTail(row)}`;
|
|
1689
|
+
}
|
|
1690
|
+
function assetsTail(row) {
|
|
1691
|
+
const assets = Array.isArray(row.assets) ? row.assets.filter(isObject2) : [];
|
|
1692
|
+
if (assets.length === 0)
|
|
1693
|
+
return "";
|
|
1694
|
+
const parts = assets.map((a) => {
|
|
1695
|
+
const name = str2(a.name) ?? "file";
|
|
1696
|
+
const kind = str2(a.kind);
|
|
1697
|
+
const url = str2(a.url);
|
|
1698
|
+
return `${name}${kind && kind !== "other" ? ` (${kind})` : ""}${url ? ` ${url}` : ""}`;
|
|
1699
|
+
});
|
|
1700
|
+
return ` On the row: ${parts.join(" · ")}.`;
|
|
1901
1701
|
}
|
|
1902
|
-
function
|
|
1903
|
-
const
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
const
|
|
1908
|
-
const
|
|
1702
|
+
function trackerTail(row) {
|
|
1703
|
+
const source = isObject2(row.source) ? row.source : {};
|
|
1704
|
+
const kind = str2(source.kind);
|
|
1705
|
+
if (kind !== "github" && kind !== "jira" && kind !== "linear")
|
|
1706
|
+
return "";
|
|
1707
|
+
const bits = [];
|
|
1708
|
+
const openedBy = str2(source.openedBy);
|
|
1709
|
+
const assignee = str2(source.assignee);
|
|
1710
|
+
if (openedBy)
|
|
1711
|
+
bits.push(`opened by ${openedBy}`);
|
|
1712
|
+
if (assignee)
|
|
1713
|
+
bits.push(`assigned to ${assignee}`);
|
|
1714
|
+
else if (openedBy)
|
|
1715
|
+
bits.push("nobody assigned");
|
|
1716
|
+
const issueType = str2(source.issueType);
|
|
1717
|
+
const parentKey = str2(source.parentKey);
|
|
1718
|
+
if (issueType)
|
|
1719
|
+
bits.push(parentKey ? `${issueType.toLowerCase()} under ${parentKey}` : issueType.toLowerCase());
|
|
1720
|
+
const mergedBy = str2(source.mergedBy);
|
|
1721
|
+
if (mergedBy)
|
|
1722
|
+
bits.push(`merged by ${mergedBy}`);
|
|
1723
|
+
const agent = str2(source.agent);
|
|
1724
|
+
if (agent)
|
|
1725
|
+
bits.push(`built with ${agent}`);
|
|
1726
|
+
const diff = isObject2(source.diff) ? source.diff : null;
|
|
1727
|
+
if (diff) {
|
|
1728
|
+
const add = num2(diff.additions) ?? 0;
|
|
1729
|
+
const del = num2(diff.deletions) ?? 0;
|
|
1730
|
+
const files = num2(diff.files) ?? 0;
|
|
1731
|
+
const commits = num2(diff.commits) ?? 0;
|
|
1732
|
+
bits.push(`+${add} −${del}${files ? ` · ${files} ${files === 1 ? "file" : "files"}` : ""}${commits ? ` · ${commits} ${commits === 1 ? "commit" : "commits"}` : ""}`);
|
|
1733
|
+
}
|
|
1734
|
+
const blockedBy = Array.isArray(source.blockedBy) ? source.blockedBy.filter((b) => typeof b === "string") : [];
|
|
1735
|
+
if (blockedBy.length)
|
|
1736
|
+
bits.push(`blocked by ${blockedBy.join(", ")}`);
|
|
1737
|
+
const held = str2(source.held);
|
|
1738
|
+
if (held)
|
|
1739
|
+
bits.push(`held: "${held}"`);
|
|
1740
|
+
if (bits.length === 0)
|
|
1741
|
+
return "";
|
|
1742
|
+
const where = kind === "github" ? "GitHub" : kind === "jira" ? "Jira" : "Linear";
|
|
1743
|
+
return ` In ${where}: ${bits.join(" · ")}.`;
|
|
1744
|
+
}
|
|
1745
|
+
function statusBody(row, now) {
|
|
1746
|
+
const name = statusName(row);
|
|
1747
|
+
const stage = str2(row.stage) ?? "ready";
|
|
1748
|
+
const handoff = isObject2(row.handoff) ? row.handoff : null;
|
|
1749
|
+
const work = isObject2(row.work) ? row.work : null;
|
|
1750
|
+
const worth = isObject2(row.worth) ? row.worth : null;
|
|
1751
|
+
const band = bandLine(row.band);
|
|
1909
1752
|
const sized = band ? `Sized ${band}` : "Not sized";
|
|
1910
1753
|
const worthText = worth ? `worth ${worthLabel(worth)}` : "worth not stated";
|
|
1911
1754
|
const ago = (ts) => num2(ts) !== undefined ? `${shortDuration(Math.max(0, now - num2(ts)))} ago` : "";
|
|
1912
1755
|
if (stage === "landed") {
|
|
1913
|
-
const mins = num2(
|
|
1914
|
-
const
|
|
1756
|
+
const mins = num2(row.actualMinutes);
|
|
1757
|
+
const dollars = num2(row.actualUsd);
|
|
1915
1758
|
const credits = work ? num2(work.seatCreditsUsd) : undefined;
|
|
1916
1759
|
const bits = [
|
|
1917
1760
|
mins !== undefined ? `${Math.round(mins)} min` : null,
|
|
1918
|
-
|
|
1761
|
+
dollars !== undefined && dollars > 0 ? usd2(dollars) : null,
|
|
1919
1762
|
credits !== undefined ? `${usd2(credits)} credits` : null,
|
|
1920
1763
|
work ? str2(work.model) : null
|
|
1921
1764
|
].filter((b) => Boolean(b));
|
|
1922
1765
|
const ref = work ? str2(work.ref) : undefined;
|
|
1923
1766
|
return `${name} — landed${bits.length ? `: ${bits.join(" · ")}` : ""}${ref ? ` · ${ref}` : ""}. ${sized}, ${worthText}.`;
|
|
1924
1767
|
}
|
|
1925
|
-
const tree = isObject2(
|
|
1768
|
+
const tree = isObject2(row.tree) ? row.tree : null;
|
|
1926
1769
|
const treeText = treeLine(tree);
|
|
1927
1770
|
if (stage === "on_sled" && handoff) {
|
|
1928
1771
|
const who = str2(handoff.pickedBy) ?? "someone";
|
|
1929
1772
|
return `${name} — on the sled: ${who} has it (picked ${ago(handoff.at)}). ${sized}, ${worthText}.${treeText} You will hear when it lands.`;
|
|
1930
1773
|
}
|
|
1931
|
-
const approval = isObject2(
|
|
1774
|
+
const approval = isObject2(row.approval) ? row.approval : null;
|
|
1932
1775
|
if (stage === "deferred") {
|
|
1933
1776
|
const by = approval ? str2(approval.by) ?? "product" : "product";
|
|
1934
1777
|
const note = approval ? str2(approval.note) : undefined;
|
|
@@ -2043,12 +1886,139 @@ function planReply(epic, rows, total) {
|
|
|
2043
1886
|
`);
|
|
2044
1887
|
}
|
|
2045
1888
|
|
|
1889
|
+
// ../../packages/shared/src/mcp/ui/intakeView.ts
|
|
1890
|
+
var INTAKE_VIEW_URI = "ui://sleds/intake";
|
|
1891
|
+
var MCP_APP_MIME = "text/html;profile=mcp-app";
|
|
1892
|
+
var INTAKE_VIEW_HTML = `<!doctype html>
|
|
1893
|
+
<html lang="en"><head><meta charset="utf-8"><title>SLEDS · intake</title>
|
|
1894
|
+
<style>
|
|
1895
|
+
:root{
|
|
1896
|
+
--bg:var(--color-background-primary,light-dark(#ffffff,#0f1116));
|
|
1897
|
+
--bg2:var(--color-background-secondary,light-dark(#f4f5f2,#14171e));
|
|
1898
|
+
--fg:var(--color-text-primary,light-dark(#171717,#f4f5f2));
|
|
1899
|
+
--fg2:var(--color-text-secondary,light-dark(#555a66,#9aa0ad));
|
|
1900
|
+
--fg3:light-dark(#7a7f8a,#6b6f7b);
|
|
1901
|
+
--line:var(--color-border-primary,light-dark(#e3e4e0,#2a2e38));
|
|
1902
|
+
--line2:light-dark(#c9cbd1,#3a4050);
|
|
1903
|
+
--volt:#d9dd45;--volt-ink:#0b0d11;--volt-dim:rgba(217,221,69,.12);
|
|
1904
|
+
--peri:#8b90ff;--peri-dim:rgba(139,144,255,.14);--warning:#ffb02e;--warning-dim:rgba(255,176,46,.12);--success:#55e08c;--success-dim:rgba(85,224,140,.12);--error:#ff5c47;
|
|
1905
|
+
--sans:var(--font-sans,system-ui,-apple-system,"Segoe UI",sans-serif);--mono:var(--font-mono,ui-monospace,SFMono-Regular,Menlo,monospace);
|
|
1906
|
+
color-scheme:light dark;
|
|
1907
|
+
}
|
|
1908
|
+
*{box-sizing:border-box}
|
|
1909
|
+
html,body{margin:0;background:transparent;color:var(--fg);font:14px/1.45 var(--sans)}
|
|
1910
|
+
.card{border:1px solid var(--line);border-radius:10px;background:var(--bg);padding:14px 16px}
|
|
1911
|
+
.h{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px}
|
|
1912
|
+
.l{font:600 11px var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--fg3)}.l b{color:var(--volt);font-weight:600}
|
|
1913
|
+
.title{font-weight:600;font-size:16px;line-height:1.3}.meta{font:11px var(--mono);color:var(--fg3);margin-top:3px}
|
|
1914
|
+
.kv{display:grid;grid-template-columns:88px 1fr;gap:6px 12px;margin:12px 0;font-size:13px}
|
|
1915
|
+
.k{font:11px var(--mono);letter-spacing:.06em;text-transform:uppercase;color:var(--fg3);padding-top:3px}
|
|
1916
|
+
.from{font:11px var(--mono);color:var(--fg3);margin-left:6px;white-space:nowrap}
|
|
1917
|
+
.v[contenteditable]{border-bottom:1px dashed var(--line2);outline:none;padding-bottom:1px}
|
|
1918
|
+
.v[contenteditable]:focus{border-bottom-color:var(--volt)}
|
|
1919
|
+
.q{border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin:8px 0}
|
|
1920
|
+
.q .t{font-weight:500}
|
|
1921
|
+
.q textarea{width:100%;margin-top:8px;background:var(--bg2);border:1px solid var(--line2);border-radius:6px;color:var(--fg);padding:8px 10px;font:14px var(--sans);resize:vertical;min-height:38px}
|
|
1922
|
+
.q textarea:focus{outline:none;border-color:var(--volt)}
|
|
1923
|
+
.opt{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
|
|
1924
|
+
.opt button{font:12px var(--sans);border:1px solid var(--line2);border-radius:14px;padding:3px 10px;color:var(--fg2);background:transparent;cursor:pointer}
|
|
1925
|
+
.opt button.on{border-color:var(--volt);color:var(--fg);background:var(--volt-dim)}
|
|
1926
|
+
.chips{display:flex;gap:6px;flex-wrap:wrap;margin:6px 0 10px}
|
|
1927
|
+
.chip{font:11px var(--mono);padding:2px 7px;border-radius:4px;border:1px solid var(--line2);color:var(--fg2)}
|
|
1928
|
+
.chip.w{border-color:var(--warning);color:var(--warning);background:var(--warning-dim)}.chip.p{border-color:var(--peri);color:var(--peri);background:var(--peri-dim)}.chip.s{border-color:var(--success);color:var(--success);background:var(--success-dim)}.chip.v{border-color:var(--volt);color:var(--volt);background:var(--volt-dim)}
|
|
1929
|
+
.map{font-size:12px;color:var(--fg2);border-left:2px solid var(--line2);padding:2px 10px;margin:8px 0}
|
|
1930
|
+
.acts{display:flex;gap:8px;align-items:center;border-top:1px solid var(--line);padding-top:12px;margin-top:6px;flex-wrap:wrap}
|
|
1931
|
+
.verb{border:1px solid var(--line2);border-radius:6px;padding:6px 12px;font:500 13px var(--sans);background:transparent;color:var(--fg);cursor:pointer}
|
|
1932
|
+
.verb.pri{background:var(--volt);color:var(--volt-ink);border-color:var(--volt);font-weight:600}
|
|
1933
|
+
.verb:disabled{opacity:.5;cursor:default}
|
|
1934
|
+
.n{font:11px var(--mono);color:var(--fg3);margin-left:auto}
|
|
1935
|
+
.err{color:var(--error);font-size:12px;margin-top:8px}
|
|
1936
|
+
a{color:var(--peri)}
|
|
1937
|
+
</style></head><body>
|
|
1938
|
+
<div id="root" class="card"><div class="l"><b>SLEDS</b> · intake</div><div class="meta">waiting for the brief…</div></div>
|
|
1939
|
+
<script>
|
|
1940
|
+
(function(){
|
|
1941
|
+
var seq=1, pending={}, args={}, state=null, busy=false, err="";
|
|
1942
|
+
var parent=window.parent;
|
|
1943
|
+
function send(method, params){ var id=seq++; return new Promise(function(res,rej){ pending[id]={res:res,rej:rej}; parent.postMessage({jsonrpc:"2.0",id:id,method:method,params:params||{}},"*"); }); }
|
|
1944
|
+
function notify(method, params){ parent.postMessage({jsonrpc:"2.0",method:method,params:params||{}},"*"); }
|
|
1945
|
+
window.addEventListener("message", function(ev){
|
|
1946
|
+
var m=ev.data; if(!m||m.jsonrpc!=="2.0") return;
|
|
1947
|
+
if(m.id!==undefined && (m.result!==undefined||m.error!==undefined) && pending[m.id]){ var p=pending[m.id]; delete pending[m.id]; m.error?p.rej(m.error):p.res(m.result); return; }
|
|
1948
|
+
if(m.method==="ui/notifications/tool-input"){ args=(m.params&&m.params.arguments)||{}; return; }
|
|
1949
|
+
if(m.method==="ui/notifications/tool-result"){ onResult(m.params||{}); return; }
|
|
1950
|
+
if(m.method==="ui/notifications/host-context-changed"){ applyStyles(m.params||{}); return; }
|
|
1951
|
+
});
|
|
1952
|
+
function applyStyles(hc){ var vars=(hc&&hc.styles&&hc.styles.variables)||{}; var root=document.documentElement; Object.keys(vars).forEach(function(k){ root.style.setProperty(k, vars[k]); }); if(hc&&hc.theme) root.style.colorScheme=hc.theme; }
|
|
1953
|
+
function onResult(r){ var s=r.structuredContent; if(s&&s.kind){ state=s; } else { state={kind:"text", text:((r.content||[]).map(function(c){return c.text||"";}).join("\\n"))}; } busy=false; err=r.isError?"":err; render(); }
|
|
1954
|
+
function esc(s){ return String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); }
|
|
1955
|
+
function fit(){ var h=document.documentElement.scrollHeight; notify("ui/notifications/size-changed",{height:h}); }
|
|
1956
|
+
var FIELDS=["ask","why","done","smallest","touches","demand"];
|
|
1957
|
+
var LABEL={ask:"Ask",why:"Why",done:"Done",smallest:"Smallest",touches:"Touches",demand:"Demand",worth:"Worth"};
|
|
1958
|
+
var WORTH=["critical","high","medium","low"];
|
|
1959
|
+
var picked={worth:null};
|
|
1960
|
+
function render(){
|
|
1961
|
+
var root=document.getElementById("root");
|
|
1962
|
+
if(!state){ return; }
|
|
1963
|
+
if(state.kind==="text"){ root.innerHTML='<div class="l"><b>SLEDS</b> · intake</div><div style="white-space:pre-wrap;margin-top:8px">'+esc(state.text)+'</div>'; fit(); return; }
|
|
1964
|
+
if(state.kind==="filed"){
|
|
1965
|
+
root.innerHTML='<div class="h"><span class="l"><b>SLEDS</b> · on the sled</span><span class="chip '+(state.needsApproval?'w':'s')+'">'+(state.needsApproval?"waits for product's yes":"ready")+'</span></div>'
|
|
1966
|
+
+'<div class="title">'+esc(state.title)+'</div><div class="meta">'+esc(state.band||"")+(state.worth?' · worth '+esc(state.worth):' · worth not stated')+' · filed in '+esc(state.filedIn||"SLEDS")+'</div>'
|
|
1967
|
+
+(state.attached&&state.attached.length?'<div class="chips">'+state.attached.map(function(a){return '<span class="chip '+(a.ok?'p':'')+'">'+esc(a.name)+(a.ok?'':' · not stored')+'</span>';}).join("")+'</div>':'')
|
|
1968
|
+
+(state.blank&&state.blank.length?'<div class="map">Still blank: '+esc(state.blank.join(", "))+' — engineering may ask.</div>':'')
|
|
1969
|
+
+'<div class="acts">'+(state.rowUrl?'<button class="verb pri" id="open">Open the row</button>':'')+'<span class="n">'+esc(state.project||"")+'</span></div>';
|
|
1970
|
+
var o=document.getElementById("open"); if(o) o.onclick=function(){ send("ui/open-link",{url:state.rowUrl}); };
|
|
1971
|
+
fit(); return;
|
|
1972
|
+
}
|
|
1973
|
+
var b=state.brief||{}; var open=state.open||[];
|
|
1974
|
+
var html='<div class="h"><span class="l"><b>SLEDS</b> · intake · draft</span>'+(open.length?'<span class="chip w">'+open.length+' question'+(open.length===1?'':'s')+' open</span>':'<span class="chip s">ready to file</span>')+'</div>';
|
|
1975
|
+
html+='<div class="title">'+esc(b.ask&&b.ask.text||"")+'</div><div class="meta">from '+esc(b.ask&&b.ask.from||"the artifact")+(state.needed&&state.needed.length?' · needs '+esc(state.needed.join(" and "))+' to file':' · nothing is filed until you confirm')+'</div>';
|
|
1976
|
+
html+='<div class="kv">';
|
|
1977
|
+
FIELDS.slice(1).forEach(function(k){ var f=b[k]; if(!f) return; html+='<div class="k">'+LABEL[k]+'</div><div><span class="v" contenteditable="true" data-k="'+k+'">'+esc(f.text)+'</span><span class="from">from the '+esc(f.from)+'</span></div>'; });
|
|
1978
|
+
if(b.worth){ html+='<div class="k">Worth</div><div><span class="chip v">'+esc(b.worth.level)+'</span>'+(b.worth.note?'<span class="from">'+esc(b.worth.note)+'</span>':'')+'</div>'; }
|
|
1979
|
+
if(b.due){ html+='<div class="k">Due</div><div>'+esc(b.due)+'<span class="from">from your words</span></div>'; }
|
|
1980
|
+
html+='</div>';
|
|
1981
|
+
if(state.map){ html+='<div class="map">What the product says: '+esc(state.map)+'</div>'; }
|
|
1982
|
+
open.forEach(function(q){
|
|
1983
|
+
html+='<div class="q"><div class="t">'+esc(q.question)+'</div>';
|
|
1984
|
+
if(q.key==="worth"){ html+='<div class="opt">'+WORTH.map(function(w){return '<button type="button" data-w="'+w+'" class="'+(picked.worth===w?'on':'')+'">'+w+'</button>';}).join("")+'</div><textarea data-q="worth" rows="1" placeholder="…or a customer, a number, a renewal"></textarea>'; }
|
|
1985
|
+
else { html+='<textarea data-q="'+esc(q.key)+'" rows="2" placeholder="In your words"></textarea>'; }
|
|
1986
|
+
html+='</div>';
|
|
1987
|
+
});
|
|
1988
|
+
var files=(state.links||[]).map(function(l){return '<span class="chip">'+esc(l)+'</span>';}).concat((state.attachments||[]).map(function(a){return '<span class="chip p">'+esc(a)+'</span>';}));
|
|
1989
|
+
if(files.length) html+='<div class="chips">'+files.join("")+'</div>';
|
|
1990
|
+
html+='<div class="acts"><button class="verb pri" id="confirm"'+(busy?' disabled':'')+'>'+(busy?"Filing…":"Put it on the sled")+'</button><span class="n">'+(state.needsApproval?"waits for product's yes":"")+'</span></div>';
|
|
1991
|
+
if(err) html+='<div class="err">'+esc(err)+'</div>';
|
|
1992
|
+
root.innerHTML=html;
|
|
1993
|
+
root.querySelectorAll("[data-w]").forEach(function(btn){ btn.onclick=function(){ picked.worth=btn.getAttribute("data-w"); root.querySelectorAll("[data-w]").forEach(function(x){x.classList.toggle("on",x===btn);}); }; });
|
|
1994
|
+
root.querySelectorAll("textarea").forEach(function(t){ t.oninput=fit; });
|
|
1995
|
+
document.getElementById("confirm").onclick=confirm;
|
|
1996
|
+
fit();
|
|
1997
|
+
}
|
|
1998
|
+
function collect(){
|
|
1999
|
+
var answers={}; var root=document.getElementById("root");
|
|
2000
|
+
var a=(args&&args.answers)||{}; Object.keys(a).forEach(function(k){ if(a[k]) answers[k]=a[k]; });
|
|
2001
|
+
root.querySelectorAll(".v[contenteditable]").forEach(function(el){ var k=el.getAttribute("data-k"); var t=el.textContent.trim(); var b=(state.brief||{})[k]; if(t && (!b || b.text!==t || b.from==="filer")) answers[k]=t; });
|
|
2002
|
+
root.querySelectorAll("textarea[data-q]").forEach(function(t){ var k=t.getAttribute("data-q"); var v=t.value.trim(); if(k==="worth"){ var w=[picked.worth,v].filter(Boolean).join(" · "); if(w) answers.worth=w; } else if(v) answers[k]=v; });
|
|
2003
|
+
return answers;
|
|
2004
|
+
}
|
|
2005
|
+
function confirm(){
|
|
2006
|
+
if(busy) return; busy=true; err=""; render();
|
|
2007
|
+
var call={ text:args.text||"", confirm:true, answers:collect() };
|
|
2008
|
+
if(args.links) call.links=args.links; if(args.attachments) call.attachments=args.attachments; if(args.added) call.added=args.added; if(args.due) call.due=args.due; if(args.parent) call.parent=args.parent;
|
|
2009
|
+
send("tools/call",{name:"intake",arguments:call}).then(function(r){ onResult(r||{}); if(r&&r.isError){ err=((r.content||[]).map(function(c){return c.text||"";}).join(" ")); busy=false; render(); } })
|
|
2010
|
+
.catch(function(e){ err=(e&&e.message)||"failed"; busy=false; render(); });
|
|
2011
|
+
}
|
|
2012
|
+
send("ui/initialize",{appCapabilities:{availableDisplayModes:["inline"]}}).then(function(r){ applyStyles((r&&r.hostContext)||{}); notify("ui/notifications/initialized",{}); }).catch(function(){ notify("ui/notifications/initialized",{}); });
|
|
2013
|
+
})();
|
|
2014
|
+
</script></body></html>`;
|
|
2015
|
+
|
|
2046
2016
|
// ../../packages/shared/src/mcp/server.ts
|
|
2047
|
-
function fetchClient(
|
|
2048
|
-
const fetchFn =
|
|
2049
|
-
const base =
|
|
2050
|
-
const headers = { "content-type": "application/json", authorization: `Bearer ${
|
|
2051
|
-
const timeout = () => AbortSignal.timeout(
|
|
2017
|
+
function fetchClient(o) {
|
|
2018
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
2019
|
+
const base = o.proxyUrl.replace(/\/+$/, "");
|
|
2020
|
+
const headers = { "content-type": "application/json", authorization: `Bearer ${o.token}` };
|
|
2021
|
+
const timeout = () => AbortSignal.timeout(o.timeoutMs ?? MCP_TIMEOUT_MS);
|
|
2052
2022
|
return {
|
|
2053
2023
|
async post(path, body) {
|
|
2054
2024
|
try {
|
|
@@ -2111,10 +2081,10 @@ var SERVER_INFO = {
|
|
|
2111
2081
|
]
|
|
2112
2082
|
};
|
|
2113
2083
|
var SERVER_INSTRUCTIONS = [
|
|
2114
|
-
"SLEDS is the ledger of engineering work: every piece is sized before it runs (a cost and time band, never a point figure) and closed with a measured actual after. You are the head; SLEDS holds the record. The person may be a PM or designer describing something they want built, or an engineer about to build — read which
|
|
2115
|
-
'When someone describes work for engineering (a feature, a fix, a spec, a PRD, "can we…"): offer once to put it on the sled. If they say yes, use `intake` — it drafts the brief, asks only what it cannot answer (who it is for and why now, what done looks like, the smallest version, the worth) two
|
|
2116
|
-
"When you are about to build something that will take more than a few minutes: `status` first if a row may
|
|
2117
|
-
"If SLEDS says there is no estimate for the source, size it first and send the actual again. If it says unreachable, try once more;
|
|
2084
|
+
"SLEDS is the ledger of engineering work: every piece is sized before it runs (a cost and time band, never a point figure) and closed with a measured actual after. You are the head; SLEDS holds the record. The person may be a PM or designer describing something they want built, or an engineer about to build — read which.",
|
|
2085
|
+
'When someone describes work for engineering (a feature, a fix, a spec, a PRD, "can we…"): offer once to put it on the sled. If they say yes, use `intake` — it drafts the brief, asks only what it cannot answer (who it is for and why now, what done looks like, the smallest version, the worth, how many have asked) two at a time, writes nothing until they confirm. Never guess worth. It also reads the product map — what the ask touches, which product docs it names, what on the board collides with it; read it back plainly, let them correct it (`touches` answers the same for any question). You judge; the map only says what is there. About a page that exists? Ask for its URL (in `links`) and a screenshot to mock the change on. Anything with a screen: offer once a clickable prototype in the workspace\'s design system when intake names one — it is the ask, not the spec. Spec, prototype, screenshot go on the row as files (`attachments` on intake, `attach` after) so anyone on the board can open them; a link only they can. Change a filed ask here, not from Slack. Say it is on the sled, the band, whether it waits for product\'s yes. They never need to learn a verb.',
|
|
2086
|
+
"When you are about to build something that will take more than a few minutes: `status` first if a row may exist for it; otherwise `estimate` with a stable `source` (kind · project · doc · item) before the first line of work, and say the band. When it lands: `actual` with the same `source`, the state, where the output lives (`ref`), the model, and your session link (`sessionId`). Do not pass `minutes` unless a person stated them — SLEDS measures the time itself. A follow-up to landed work is a new `item` under the same doc, never an overwrite. An `actual` with no estimate still records (landed unsized), but the band you skipped is the point, so estimate first.",
|
|
2087
|
+
"If SLEDS says there is no estimate for the source, size it first and send the actual again. If it says unreachable, try once more; then move on and tell the person the row is still owed. Never invent a number to get past an error."
|
|
2118
2088
|
].join(`
|
|
2119
2089
|
|
|
2120
2090
|
`);
|
|
@@ -2162,7 +2132,8 @@ var TOOLS = [
|
|
|
2162
2132
|
},
|
|
2163
2133
|
{
|
|
2164
2134
|
name: "intake",
|
|
2165
|
-
|
|
2135
|
+
_meta: { ui: { resourceUri: INTAKE_VIEW_URI } },
|
|
2136
|
+
description: 'Use this when a person describes work they want engineering to do — a feature, a fix, a spec, a PRD, "can we…" — and has said yes to putting it on the sled. Sends the ask to engineering from this conversation (spec 13). Pass what the person sent — a paragraph, a spec, a PRD — plus any links (the URL of the page it is about, a prototype you made). If it concerns a page or feature that already exists, ask for its URL and a screenshot first — the screenshot is what you mock the change on. Without `confirm` nothing is written: you get the brief drafted from it and the questions still open (who it is for and why now, what done looks like, the smallest version, the worth, what it touches and where it would live, how many have asked) — ask at most two at a time, in your own words, and skip any the person already answered. Put their answers in `answers`, show them the brief, and when they say yes call again with `confirm: true`: the row is filed on the sled, sized, with their name on it. A brief without a why and a done is a draft and will not file. Worth is never guessed.',
|
|
2166
2137
|
inputSchema: {
|
|
2167
2138
|
type: "object",
|
|
2168
2139
|
properties: {
|
|
@@ -2175,6 +2146,28 @@ var TOOLS = [
|
|
|
2175
2146
|
items: { type: "string" },
|
|
2176
2147
|
description: "Links and file names that came with it (a Figma frame, a Loom, a doc)."
|
|
2177
2148
|
},
|
|
2149
|
+
attachments: {
|
|
2150
|
+
type: "array",
|
|
2151
|
+
description: 'The files themselves, stored on the row so anyone on the board can open them: the spec or PRD as markdown, a prototype you made as a single HTML page, a screenshot. Sent only on `confirm`. Up to 4 MB each; text as is, an image as base64 with `encoding: "base64"`. The same name again replaces.',
|
|
2152
|
+
items: {
|
|
2153
|
+
type: "object",
|
|
2154
|
+
properties: {
|
|
2155
|
+
name: {
|
|
2156
|
+
type: "string",
|
|
2157
|
+
description: "File name with extension: spec.md, prototype.html, before.png."
|
|
2158
|
+
},
|
|
2159
|
+
kind: {
|
|
2160
|
+
type: "string",
|
|
2161
|
+
enum: ["spec", "prototype", "screenshot", "doc", "other"],
|
|
2162
|
+
description: "Read from the name and mime when omitted."
|
|
2163
|
+
},
|
|
2164
|
+
mime: { type: "string", description: "text/markdown, text/html, image/png…" },
|
|
2165
|
+
content: { type: "string", description: "The file: text as is, or base64." },
|
|
2166
|
+
encoding: { type: "string", enum: ["text", "base64"] }
|
|
2167
|
+
},
|
|
2168
|
+
required: ["name", "content"]
|
|
2169
|
+
}
|
|
2170
|
+
},
|
|
2178
2171
|
answers: {
|
|
2179
2172
|
type: "object",
|
|
2180
2173
|
description: "The person's answers, in their words. Each overrides what the artifact said.",
|
|
@@ -2187,9 +2180,25 @@ var TOOLS = [
|
|
|
2187
2180
|
type: "string",
|
|
2188
2181
|
description: "A level (high / medium / low), dollars, a customer — e.g. 'high · Acme renewal'."
|
|
2189
2182
|
},
|
|
2190
|
-
touches: {
|
|
2183
|
+
touches: {
|
|
2184
|
+
type: "string",
|
|
2185
|
+
description: "Areas or systems it touches, and where in the product it would live."
|
|
2186
|
+
},
|
|
2187
|
+
demand: {
|
|
2188
|
+
type: "string",
|
|
2189
|
+
description: "How many have asked for it, or how often it comes up."
|
|
2190
|
+
}
|
|
2191
2191
|
}
|
|
2192
2192
|
},
|
|
2193
|
+
added: {
|
|
2194
|
+
type: "array",
|
|
2195
|
+
items: { type: "string" },
|
|
2196
|
+
description: "What you added on the way, one line each — milestones you drafted ('M1 — the posting form only'), a decision you flagged for product, the nearest landed work you found. Shown on the row as the AI's contribution; an approver reads it before saying yes."
|
|
2197
|
+
},
|
|
2198
|
+
due: {
|
|
2199
|
+
type: "string",
|
|
2200
|
+
description: "A due day the person stated, as YYYY-MM-DD — only when they said one ('by Oct 10', 'end of the month'). Never ask for it. Read from the text on its own when you leave it out."
|
|
2201
|
+
},
|
|
2193
2202
|
confirm: {
|
|
2194
2203
|
type: "boolean",
|
|
2195
2204
|
description: "True only after the person said yes to the brief. Files the row."
|
|
@@ -2295,6 +2304,64 @@ var TOOLS = [
|
|
|
2295
2304
|
}
|
|
2296
2305
|
}
|
|
2297
2306
|
},
|
|
2307
|
+
{
|
|
2308
|
+
name: "touches",
|
|
2309
|
+
description: "What the product says an ask reaches: the areas of the product (from the workspace's repos), the product's own documents it names, and the rows on the board it collides with. Call with `text` — the ask, or a question like 'if we change the 4-hour minimum, what else is affected?'. Read it back in plain words; you judge, the map only says what is there. `intake` calls this on its own while drafting.",
|
|
2310
|
+
inputSchema: {
|
|
2311
|
+
type: "object",
|
|
2312
|
+
properties: {
|
|
2313
|
+
text: { type: "string", description: "The ask or the question, in the person's words." }
|
|
2314
|
+
},
|
|
2315
|
+
required: ["text"]
|
|
2316
|
+
}
|
|
2317
|
+
},
|
|
2318
|
+
{
|
|
2319
|
+
name: "attach",
|
|
2320
|
+
description: "Put a file on a row already on the sled so anyone on the board can open it — a spec or PRD as markdown, a prototype you made as one HTML page (it opens and renders in its own tab), a screenshot. Pass the row's project key (from status, or what intake said) and the file. Up to 4 MB; the same name again replaces the earlier one. `intake` takes the same files as `attachments` when filing.",
|
|
2321
|
+
inputSchema: {
|
|
2322
|
+
type: "object",
|
|
2323
|
+
properties: {
|
|
2324
|
+
project: {
|
|
2325
|
+
type: "string",
|
|
2326
|
+
description: "The row's project key, e.g. manual:intake/help-and-support or jira:WORK/WORK-7551."
|
|
2327
|
+
},
|
|
2328
|
+
name: {
|
|
2329
|
+
type: "string",
|
|
2330
|
+
description: "File name with extension: spec.md, prototype.html, before.png."
|
|
2331
|
+
},
|
|
2332
|
+
kind: {
|
|
2333
|
+
type: "string",
|
|
2334
|
+
enum: ["spec", "prototype", "screenshot", "doc", "other"],
|
|
2335
|
+
description: "Read from the name and mime when omitted."
|
|
2336
|
+
},
|
|
2337
|
+
mime: { type: "string", description: "text/markdown, text/html, image/png…" },
|
|
2338
|
+
content: {
|
|
2339
|
+
type: "string",
|
|
2340
|
+
description: "The file: text as is, or base64 with encoding base64."
|
|
2341
|
+
},
|
|
2342
|
+
encoding: { type: "string", enum: ["text", "base64"] }
|
|
2343
|
+
},
|
|
2344
|
+
required: ["project", "name", "content"]
|
|
2345
|
+
}
|
|
2346
|
+
},
|
|
2347
|
+
{
|
|
2348
|
+
name: "answer",
|
|
2349
|
+
description: "Answer a question engineering or product asked on a row you filed. Call with nothing to see what is waiting on you (each with its id and the row); call with `answer` (and `project` or `id` when more than one is open) to put your reply on the row — it becomes part of the record and the person who asked hears at once. When someone says 'tell them…' or 'the answer is…' about a question SLEDS relayed, this is the tool.",
|
|
2350
|
+
inputSchema: {
|
|
2351
|
+
type: "object",
|
|
2352
|
+
properties: {
|
|
2353
|
+
answer: { type: "string", description: "The reply, in the person's words." },
|
|
2354
|
+
project: {
|
|
2355
|
+
type: "string",
|
|
2356
|
+
description: "The row's project key (from status or the question list) when more than one question is open."
|
|
2357
|
+
},
|
|
2358
|
+
id: {
|
|
2359
|
+
type: "string",
|
|
2360
|
+
description: "The question's id from the list, when more than one is open on the same row."
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
},
|
|
2298
2365
|
{
|
|
2299
2366
|
name: "handoffs",
|
|
2300
2367
|
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).",
|
|
@@ -2484,6 +2551,10 @@ async function usageTool(ctx) {
|
|
|
2484
2551
|
if (!ctx.client)
|
|
2485
2552
|
return NOT_SET_UP;
|
|
2486
2553
|
const raw = await ctx.client.get("/proxy/meter");
|
|
2554
|
+
if (raw && typeof raw.note === "string" && raw.seat?.filer === true)
|
|
2555
|
+
return {
|
|
2556
|
+
text: `${raw.note}. Intake, status, answer, touches and attach are yours; taking or sizing work is a seat's.`
|
|
2557
|
+
};
|
|
2487
2558
|
const m = raw ? parseMeter(raw) : null;
|
|
2488
2559
|
if (!m)
|
|
2489
2560
|
return { text: "sleds offline (proxy did not answer).", isError: true };
|
|
@@ -2538,12 +2609,12 @@ function byModelText(raw) {
|
|
|
2538
2609
|
if (!Array.isArray(raw))
|
|
2539
2610
|
return "";
|
|
2540
2611
|
const parts = [];
|
|
2541
|
-
for (const
|
|
2542
|
-
if (!isObject4(
|
|
2612
|
+
for (const row of raw) {
|
|
2613
|
+
if (!isObject4(row) || !isObject4(row.standard))
|
|
2543
2614
|
continue;
|
|
2544
|
-
const model = str4(
|
|
2545
|
-
const low = num3(
|
|
2546
|
-
const high = num3(
|
|
2615
|
+
const model = str4(row.model);
|
|
2616
|
+
const low = num3(row.standard.lowUsd);
|
|
2617
|
+
const high = num3(row.standard.highUsd);
|
|
2547
2618
|
if (!model || low === undefined || high === undefined)
|
|
2548
2619
|
continue;
|
|
2549
2620
|
const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?/.exec(model);
|
|
@@ -2587,13 +2658,35 @@ function briefAnswers(raw) {
|
|
|
2587
2658
|
if (!isObject4(raw))
|
|
2588
2659
|
return {};
|
|
2589
2660
|
const out = {};
|
|
2590
|
-
for (const k of ["ask", "why", "done", "smallest", "worth", "touches"]) {
|
|
2661
|
+
for (const k of ["ask", "why", "done", "smallest", "worth", "touches", "demand"]) {
|
|
2591
2662
|
const v = str4(raw[k])?.trim();
|
|
2592
2663
|
if (v)
|
|
2593
2664
|
out[k] = v;
|
|
2594
2665
|
}
|
|
2595
2666
|
return out;
|
|
2596
2667
|
}
|
|
2668
|
+
async function readTouches(ctx, text) {
|
|
2669
|
+
if (!ctx.client || !text.trim())
|
|
2670
|
+
return null;
|
|
2671
|
+
const r = await ctx.client.get(`/proxy/touches?q=${encodeURIComponent(text.slice(0, 2000))}`);
|
|
2672
|
+
if (!r)
|
|
2673
|
+
return null;
|
|
2674
|
+
const mapped = r.mapped === true;
|
|
2675
|
+
const areas = Array.isArray(r.areas) ? r.areas : [];
|
|
2676
|
+
const touchesLine = areas.length ? areas.map((a) => `${str4(a.area) ?? ""}${str4(a.repo) ? ` (${str4(a.repo).split("/").pop()})` : ""}`).filter(Boolean).join(" · ") : null;
|
|
2677
|
+
return { text: str4(r.text) ?? "", touchesLine, mapped };
|
|
2678
|
+
}
|
|
2679
|
+
async function touchesTool(ctx, args) {
|
|
2680
|
+
if (!ctx.client)
|
|
2681
|
+
return NOT_SET_UP;
|
|
2682
|
+
const text = str4(args.text)?.trim();
|
|
2683
|
+
if (!text)
|
|
2684
|
+
return { text: "Say what the ask or the question is (text).", isError: true };
|
|
2685
|
+
const r = await readTouches(ctx, text);
|
|
2686
|
+
if (!r)
|
|
2687
|
+
return UNREACHABLE;
|
|
2688
|
+
return { text: r.text || "Nothing in the product map matches these words." };
|
|
2689
|
+
}
|
|
2597
2690
|
async function intakeTool(ctx, args) {
|
|
2598
2691
|
if (!ctx.client)
|
|
2599
2692
|
return NOT_SET_UP;
|
|
@@ -2607,18 +2700,35 @@ async function intakeTool(ctx, args) {
|
|
|
2607
2700
|
const brief = draftBrief({ text, links, answers: briefAnswers(args.answers) });
|
|
2608
2701
|
if (!brief)
|
|
2609
2702
|
return { text: "Nothing to file. Pass text: the ask, a spec, or a paragraph.", isError: true };
|
|
2703
|
+
const map = await readTouches(ctx, [brief.ask.text, brief.why?.text, brief.done?.text].filter(Boolean).join(`
|
|
2704
|
+
`));
|
|
2705
|
+
if (map?.touchesLine && !brief.touches)
|
|
2706
|
+
brief.touches = { text: map.touchesLine, from: "product" };
|
|
2610
2707
|
const open = openQuestions(brief);
|
|
2611
2708
|
if (args.confirm !== true) {
|
|
2612
|
-
const
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2709
|
+
const needed = missingToFile(brief);
|
|
2710
|
+
const attachments = parseAttachments(args.attachments).map((a) => a.name || "file");
|
|
2711
|
+
const structured = {
|
|
2712
|
+
kind: "draft",
|
|
2713
|
+
brief: briefForView(brief),
|
|
2714
|
+
open,
|
|
2715
|
+
needed,
|
|
2716
|
+
map: map?.text ?? null,
|
|
2717
|
+
links,
|
|
2718
|
+
attachments
|
|
2719
|
+
};
|
|
2720
|
+
const lines = [
|
|
2721
|
+
renderBriefMarkdown(brief, open, { map: map?.text ?? null, files: attachments }),
|
|
2722
|
+
"",
|
|
2723
|
+
open.length > 0 ? "Show the person the table as is, then ask at most two of the open questions at a time, in your own words; skip what they already said." : "Show the person the table as is.",
|
|
2724
|
+
needed.length > 0 ? `Nothing is filed yet. Filing needs: ${needed.join(", ")}. When they say yes to the brief, call intake again with the same text, their answers, and confirm: true.` : "Nothing is filed yet. When they say yes, call intake again with the same text, their answers, and confirm: true.",
|
|
2725
|
+
"",
|
|
2726
|
+
"```json sleds-draft",
|
|
2727
|
+
JSON.stringify(structured),
|
|
2728
|
+
"```"
|
|
2729
|
+
];
|
|
2620
2730
|
return { text: lines.join(`
|
|
2621
|
-
`) };
|
|
2731
|
+
`), structured };
|
|
2622
2732
|
}
|
|
2623
2733
|
const needed = missingToFile(brief);
|
|
2624
2734
|
if (needed.length > 0)
|
|
@@ -2627,8 +2737,14 @@ async function intakeTool(ctx, args) {
|
|
|
2627
2737
|
isError: true
|
|
2628
2738
|
};
|
|
2629
2739
|
const parent = str4(args.parent)?.trim() || undefined;
|
|
2740
|
+
const answers = briefAnswers(args.answers);
|
|
2741
|
+
const asked = Object.keys(QUESTIONS).filter((k) => answers[k]).map((k) => ({ q: QUESTIONS[k], a: answers[k] }));
|
|
2742
|
+
const added = Array.isArray(args.added) ? args.added.filter((l) => typeof l === "string" && l.trim().length > 0).map((l) => l.trim().slice(0, 200)).slice(0, 12) : [];
|
|
2743
|
+
const dueArg = typeof args.due === "string" && /^\d{4}-\d{2}-\d{2}$/.test(args.due) ? args.due : null;
|
|
2744
|
+
const due = dueArg ?? parseDue([text, ...Object.values(answers), ...added].filter(Boolean).join(`
|
|
2745
|
+
`));
|
|
2630
2746
|
const r = await ctx.client.postFull("/proxy/intake", {
|
|
2631
|
-
brief,
|
|
2747
|
+
brief: { ...brief, intake: { asked, added, related: [] }, ...due ? { due } : {} },
|
|
2632
2748
|
...parent ? { parent } : {}
|
|
2633
2749
|
});
|
|
2634
2750
|
if (!r)
|
|
@@ -2641,6 +2757,7 @@ async function intakeTool(ctx, args) {
|
|
|
2641
2757
|
const j = r.json ?? {};
|
|
2642
2758
|
const worth = parseWorthBand(j.worth) ?? brief.worth ?? null;
|
|
2643
2759
|
const blank = open.filter((q) => q.key !== "worth" || !worth).map((q) => q.key);
|
|
2760
|
+
const attached = str4(j.project) ? await sendAttachments(ctx, str4(j.project), args.attachments) : [];
|
|
2644
2761
|
return {
|
|
2645
2762
|
text: intakeReply({
|
|
2646
2763
|
title: briefToSpec(brief).title,
|
|
@@ -2649,8 +2766,132 @@ async function intakeTool(ctx, args) {
|
|
|
2649
2766
|
blank,
|
|
2650
2767
|
filedIn: str4(j.filedIn) ?? "SLEDS (no tracker connected)",
|
|
2651
2768
|
needsApproval: j.needsApproval === true,
|
|
2652
|
-
...str4(j.parent) ? { parent: str4(j.parent) } : {}
|
|
2653
|
-
|
|
2769
|
+
...str4(j.parent) ? { parent: str4(j.parent) } : {},
|
|
2770
|
+
key: str4(j.key) ?? null
|
|
2771
|
+
}) + attachedLine(attached),
|
|
2772
|
+
structured: {
|
|
2773
|
+
kind: "filed",
|
|
2774
|
+
project: str4(j.project) ?? null,
|
|
2775
|
+
key: str4(j.key) ?? null,
|
|
2776
|
+
title: briefToSpec(brief).title,
|
|
2777
|
+
band: bandLine(j.band),
|
|
2778
|
+
worth: worth ? worthLabel(worth) : null,
|
|
2779
|
+
blank,
|
|
2780
|
+
filedIn: str4(j.filedIn) ?? "SLEDS",
|
|
2781
|
+
needsApproval: j.needsApproval === true,
|
|
2782
|
+
rowUrl: str4(j.url) ?? null,
|
|
2783
|
+
attached
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function briefForView(b) {
|
|
2788
|
+
const f = (x) => x ? { text: x.text, from: x.from } : undefined;
|
|
2789
|
+
return {
|
|
2790
|
+
ask: f(b.ask),
|
|
2791
|
+
why: f(b.why),
|
|
2792
|
+
done: f(b.done),
|
|
2793
|
+
smallest: f(b.smallest),
|
|
2794
|
+
touches: f(b.touches),
|
|
2795
|
+
demand: f(b.demand),
|
|
2796
|
+
worth: b.worth ? { level: b.worth.level, note: b.worth.note ?? null } : undefined,
|
|
2797
|
+
due: b.due ?? null
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
function parseAttachments(raw) {
|
|
2801
|
+
if (!Array.isArray(raw))
|
|
2802
|
+
return [];
|
|
2803
|
+
const out = [];
|
|
2804
|
+
for (const a of raw) {
|
|
2805
|
+
if (!isObject4(a))
|
|
2806
|
+
continue;
|
|
2807
|
+
const content = str4(a.content);
|
|
2808
|
+
if (!content)
|
|
2809
|
+
continue;
|
|
2810
|
+
const mime = str4(a.mime)?.trim() || mimeFromName(str4(a.name) ?? "");
|
|
2811
|
+
out.push({
|
|
2812
|
+
name: str4(a.name)?.trim() || "",
|
|
2813
|
+
mime,
|
|
2814
|
+
content,
|
|
2815
|
+
...str4(a.kind) ? { kind: str4(a.kind) } : {},
|
|
2816
|
+
...a.encoding === "base64" ? { encoding: "base64" } : {}
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
return out.slice(0, 12);
|
|
2820
|
+
}
|
|
2821
|
+
function mimeFromName(name) {
|
|
2822
|
+
const ext = name.toLowerCase().split(".").pop() ?? "";
|
|
2823
|
+
return {
|
|
2824
|
+
html: "text/html",
|
|
2825
|
+
htm: "text/html",
|
|
2826
|
+
md: "text/markdown",
|
|
2827
|
+
markdown: "text/markdown",
|
|
2828
|
+
txt: "text/plain",
|
|
2829
|
+
csv: "text/csv",
|
|
2830
|
+
json: "application/json",
|
|
2831
|
+
pdf: "application/pdf",
|
|
2832
|
+
png: "image/png",
|
|
2833
|
+
jpg: "image/jpeg",
|
|
2834
|
+
jpeg: "image/jpeg",
|
|
2835
|
+
gif: "image/gif",
|
|
2836
|
+
webp: "image/webp",
|
|
2837
|
+
svg: "image/svg+xml"
|
|
2838
|
+
}[ext] ?? "text/plain";
|
|
2839
|
+
}
|
|
2840
|
+
async function sendAttachments(ctx, project, raw) {
|
|
2841
|
+
const files = parseAttachments(raw);
|
|
2842
|
+
const out = [];
|
|
2843
|
+
for (const f of files) {
|
|
2844
|
+
const r = await ctx.client.postFull("/proxy/asset", { project, ...f });
|
|
2845
|
+
if (!r)
|
|
2846
|
+
out.push({ name: f.name || "file", ok: false, why: "unreachable" });
|
|
2847
|
+
else if (r.status !== 200)
|
|
2848
|
+
out.push({
|
|
2849
|
+
name: f.name || "file",
|
|
2850
|
+
ok: false,
|
|
2851
|
+
why: errorMessage(r.json) ?? `HTTP ${r.status}`
|
|
2852
|
+
});
|
|
2853
|
+
else
|
|
2854
|
+
out.push({
|
|
2855
|
+
name: str4(r.json?.name) ?? f.name,
|
|
2856
|
+
ok: true,
|
|
2857
|
+
...str4(r.json?.url) ? { url: str4(r.json?.url) } : {}
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
return out;
|
|
2861
|
+
}
|
|
2862
|
+
function attachedLine(sent) {
|
|
2863
|
+
if (sent.length === 0)
|
|
2864
|
+
return "";
|
|
2865
|
+
const ok = sent.filter((s) => s.ok).map((s) => s.name);
|
|
2866
|
+
const bad = sent.filter((s) => !s.ok);
|
|
2867
|
+
const parts = [];
|
|
2868
|
+
if (ok.length)
|
|
2869
|
+
parts.push(`On the row: ${ok.join(", ")}.`);
|
|
2870
|
+
for (const b of bad)
|
|
2871
|
+
parts.push(`${b.name} was not stored (${b.why ?? "failed"}).`);
|
|
2872
|
+
return ` ${parts.join(" ")}`;
|
|
2873
|
+
}
|
|
2874
|
+
async function attachTool(ctx, args) {
|
|
2875
|
+
if (!ctx.client)
|
|
2876
|
+
return NOT_SET_UP;
|
|
2877
|
+
const project = str4(args.project)?.trim();
|
|
2878
|
+
if (!project)
|
|
2879
|
+
return {
|
|
2880
|
+
text: "Which row? Pass project: the key status shows (or what intake said).",
|
|
2881
|
+
isError: true
|
|
2882
|
+
};
|
|
2883
|
+
const files = parseAttachments([args]);
|
|
2884
|
+
if (files.length === 0)
|
|
2885
|
+
return {
|
|
2886
|
+
text: "Nothing to store. Pass name and content (text as is, or base64 with encoding base64).",
|
|
2887
|
+
isError: true
|
|
2888
|
+
};
|
|
2889
|
+
const sent = await sendAttachments(ctx, project, files);
|
|
2890
|
+
const first = sent[0];
|
|
2891
|
+
if (!first?.ok)
|
|
2892
|
+
return { text: `Not stored: ${first?.why ?? "failed"}.`, isError: true };
|
|
2893
|
+
return {
|
|
2894
|
+
text: `On the row ${project}: ${first.name}${first.url ? ` — ${first.url}` : ""}. Anyone on the board can open it from the row.`
|
|
2654
2895
|
};
|
|
2655
2896
|
}
|
|
2656
2897
|
async function overnightTool(ctx, args) {
|
|
@@ -2767,8 +3008,8 @@ async function dispatchTool(ctx, args) {
|
|
|
2767
3008
|
}
|
|
2768
3009
|
return { text: reply };
|
|
2769
3010
|
}
|
|
2770
|
-
const
|
|
2771
|
-
const title = str4(
|
|
3011
|
+
const row = isObject4(j.row) ? j.row : {};
|
|
3012
|
+
const title = str4(row.title) ?? "the row";
|
|
2772
3013
|
if (action.kind === "parked") {
|
|
2773
3014
|
const dest = str4(action.to);
|
|
2774
3015
|
return {
|
|
@@ -2811,9 +3052,9 @@ async function approveTool(ctx, args) {
|
|
|
2811
3052
|
isError: true
|
|
2812
3053
|
};
|
|
2813
3054
|
const j = r.json ?? {};
|
|
2814
|
-
const
|
|
2815
|
-
const title = str4(
|
|
2816
|
-
const filedBy = str4(
|
|
3055
|
+
const row = isObject4(j.row) ? j.row : {};
|
|
3056
|
+
const title = str4(row.title) ?? "the row";
|
|
3057
|
+
const filedBy = str4(row.filedBy);
|
|
2817
3058
|
if (decision === "approved")
|
|
2818
3059
|
return {
|
|
2819
3060
|
text: `${title}: approved — it is ready on the sled${filedBy ? `; ${filedBy} is told` : ""}. dispatch says where it should go; an engineer takes it with Take it or /sled pickup.`
|
|
@@ -2865,6 +3106,50 @@ async function statusTool(ctx, args) {
|
|
|
2865
3106
|
const rows = Array.isArray(r.rows) ? r.rows.filter(isObject4) : [];
|
|
2866
3107
|
return { text: statusText(rows, (ctx.now ?? Date.now)(), q) };
|
|
2867
3108
|
}
|
|
3109
|
+
function questionsText(open, now) {
|
|
3110
|
+
if (open.length === 0)
|
|
3111
|
+
return "Nothing is waiting on your answer.";
|
|
3112
|
+
const lines = open.map((q) => {
|
|
3113
|
+
const at = typeof q.at === "number" ? ` · ${shortDuration(Math.max(0, now - q.at))} ago` : "";
|
|
3114
|
+
return `- ${str4(q.by) ?? "someone"} asked on "${str4(q.title) ?? str4(q.project) ?? "a row"}"${at}: ${str4(q.question) ?? ""} (id ${str4(q.id) ?? "?"})`;
|
|
3115
|
+
});
|
|
3116
|
+
return `${open.length === 1 ? "One question is" : `${open.length} questions are`} waiting on you:
|
|
3117
|
+
${lines.join(`
|
|
3118
|
+
`)}
|
|
3119
|
+
Answer with the answer tool; your reply goes on the row.`;
|
|
3120
|
+
}
|
|
3121
|
+
async function answerTool(ctx, args) {
|
|
3122
|
+
if (!ctx.client)
|
|
3123
|
+
return NOT_SET_UP;
|
|
3124
|
+
const now = (ctx.now ?? Date.now)();
|
|
3125
|
+
const answer = str4(args.answer)?.trim();
|
|
3126
|
+
if (!answer) {
|
|
3127
|
+
const r = await ctx.client.get("/proxy/questions");
|
|
3128
|
+
if (!r)
|
|
3129
|
+
return UNREACHABLE;
|
|
3130
|
+
const open = Array.isArray(r.open) ? r.open.filter(isObject4) : [];
|
|
3131
|
+
return { text: questionsText(open, now) };
|
|
3132
|
+
}
|
|
3133
|
+
const project = str4(args.project)?.trim() || undefined;
|
|
3134
|
+
const questionId = str4(args.id)?.trim() || undefined;
|
|
3135
|
+
const r = await ctx.client.postFull("/proxy/answer", {
|
|
3136
|
+
answer,
|
|
3137
|
+
...project ? { project } : {},
|
|
3138
|
+
...questionId ? { questionId } : {}
|
|
3139
|
+
});
|
|
3140
|
+
if (!r)
|
|
3141
|
+
return { text: `Nothing was answered. ${UNREACHABLE.text}`, isError: true };
|
|
3142
|
+
if (r.status !== 200) {
|
|
3143
|
+
const open = Array.isArray(r.json?.open) ? (r.json?.open).filter(isObject4) : [];
|
|
3144
|
+
const msg = errorMessage(r.json) ?? `Nothing was answered (HTTP ${r.status}).`;
|
|
3145
|
+
return { text: open.length ? `${msg}
|
|
3146
|
+
${questionsText(open, now)}` : msg, isError: true };
|
|
3147
|
+
}
|
|
3148
|
+
const asker = str4(r.json?.asker) ?? "the person who asked";
|
|
3149
|
+
return {
|
|
3150
|
+
text: `On the row. ${asker} hears now: "${str4(r.json?.question) ?? "the question"}" → ${answer}`
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
2868
3153
|
async function handoffsTool(ctx) {
|
|
2869
3154
|
if (!ctx.client)
|
|
2870
3155
|
return NOT_SET_UP;
|
|
@@ -2875,7 +3160,7 @@ async function handoffsTool(ctx) {
|
|
|
2875
3160
|
if (handoffs.length === 0)
|
|
2876
3161
|
return { text: "No open hand-offs on your team. Park one with the handoff tool." };
|
|
2877
3162
|
const now = (ctx.now ?? Date.now)();
|
|
2878
|
-
const packets = handoffs.map((h) => str4(h.packet)).filter((
|
|
3163
|
+
const packets = handoffs.map((h) => str4(h.packet)).filter((p) => Boolean(p)).join(`
|
|
2879
3164
|
|
|
2880
3165
|
---
|
|
2881
3166
|
|
|
@@ -3088,10 +3373,10 @@ async function buddyTool(ctx, args) {
|
|
|
3088
3373
|
return NOT_SET_UP;
|
|
3089
3374
|
const email = str4(args.email)?.trim();
|
|
3090
3375
|
if (!email) {
|
|
3091
|
-
const
|
|
3092
|
-
if (!
|
|
3376
|
+
const r = await ctx.client.get("/proxy/buddy");
|
|
3377
|
+
if (!r)
|
|
3093
3378
|
return UNREACHABLE;
|
|
3094
|
-
const list = Array.isArray(
|
|
3379
|
+
const list = Array.isArray(r.buddies) ? r.buddies : [];
|
|
3095
3380
|
if (list.length === 0)
|
|
3096
3381
|
return {
|
|
3097
3382
|
text: "No buddies yet. `buddy {email}` sends someone an invite; once they accept, `handoff {to: email}` reaches them in their own org."
|
|
@@ -3102,10 +3387,10 @@ ${lines.join(`
|
|
|
3102
3387
|
`)}` };
|
|
3103
3388
|
}
|
|
3104
3389
|
if (args.remove === true) {
|
|
3105
|
-
const
|
|
3106
|
-
if (!
|
|
3390
|
+
const r = await ctx.client.postFull("/proxy/buddy", { email, remove: true });
|
|
3391
|
+
if (!r)
|
|
3107
3392
|
return UNREACHABLE;
|
|
3108
|
-
return { text:
|
|
3393
|
+
return { text: r.json?.ok ? `Unpaired from ${email}.` : `${email} was not a buddy.` };
|
|
3109
3394
|
}
|
|
3110
3395
|
const r = await ctx.client.postFull("/proxy/buddy", { email });
|
|
3111
3396
|
if (!r)
|
|
@@ -3133,6 +3418,12 @@ async function callTool(name, args, ctx) {
|
|
|
3133
3418
|
return jobsTool(ctx);
|
|
3134
3419
|
case "status":
|
|
3135
3420
|
return statusTool(ctx, args);
|
|
3421
|
+
case "touches":
|
|
3422
|
+
return touchesTool(ctx, args);
|
|
3423
|
+
case "attach":
|
|
3424
|
+
return attachTool(ctx, args);
|
|
3425
|
+
case "answer":
|
|
3426
|
+
return answerTool(ctx, args);
|
|
3136
3427
|
case "dispatch":
|
|
3137
3428
|
return dispatchTool(ctx, args);
|
|
3138
3429
|
case "approve":
|
|
@@ -3161,6 +3452,15 @@ async function callTool(name, args, ctx) {
|
|
|
3161
3452
|
return { text: `Unknown tool: ${name}`, isError: true };
|
|
3162
3453
|
}
|
|
3163
3454
|
}
|
|
3455
|
+
var UI_RESOURCES = [
|
|
3456
|
+
{
|
|
3457
|
+
uri: INTAKE_VIEW_URI,
|
|
3458
|
+
name: "sleds_intake",
|
|
3459
|
+
description: "The intake brief as a form: edit, answer what is open, confirm.",
|
|
3460
|
+
mimeType: MCP_APP_MIME,
|
|
3461
|
+
html: INTAKE_VIEW_HTML
|
|
3462
|
+
}
|
|
3463
|
+
];
|
|
3164
3464
|
var response = (id, result) => JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
3165
3465
|
var errorResponse = (id, code, message) => JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
3166
3466
|
async function handleLine(line, ctx) {
|
|
@@ -3188,7 +3488,7 @@ async function handleLine(line, ctx) {
|
|
|
3188
3488
|
const requested = str4(params.protocolVersion);
|
|
3189
3489
|
return response(id, {
|
|
3190
3490
|
protocolVersion: requested ?? PROTOCOL_VERSION,
|
|
3191
|
-
capabilities: { tools: {} },
|
|
3491
|
+
capabilities: { tools: {}, resources: {} },
|
|
3192
3492
|
serverInfo: SERVER_INFO,
|
|
3193
3493
|
instructions: SERVER_INSTRUCTIONS
|
|
3194
3494
|
});
|
|
@@ -3197,12 +3497,33 @@ async function handleLine(line, ctx) {
|
|
|
3197
3497
|
return response(id, {});
|
|
3198
3498
|
case "tools/list":
|
|
3199
3499
|
return response(id, { tools: TOOLS });
|
|
3500
|
+
case "resources/list":
|
|
3501
|
+
return response(id, { resources: UI_RESOURCES.map(({ html: _html, ...r }) => r) });
|
|
3502
|
+
case "resources/templates/list":
|
|
3503
|
+
return response(id, { resourceTemplates: [] });
|
|
3504
|
+
case "resources/read": {
|
|
3505
|
+
const uri = str4(params.uri) ?? "";
|
|
3506
|
+
const r = UI_RESOURCES.find((x) => x.uri === uri);
|
|
3507
|
+
if (!r)
|
|
3508
|
+
return errorResponse(id, -32002, `resource not found: ${uri}`);
|
|
3509
|
+
return response(id, { contents: [{ uri: r.uri, mimeType: r.mimeType, text: r.html }] });
|
|
3510
|
+
}
|
|
3200
3511
|
case "tools/call": {
|
|
3201
3512
|
const name = str4(params.name) ?? "";
|
|
3202
3513
|
const args = isObject4(params.arguments) ? params.arguments : {};
|
|
3514
|
+
const t0 = (ctx.now ?? Date.now)();
|
|
3203
3515
|
const r = await callTool(name, args, ctx);
|
|
3516
|
+
try {
|
|
3517
|
+
ctx.observe?.({
|
|
3518
|
+
tool: name,
|
|
3519
|
+
args,
|
|
3520
|
+
result: r,
|
|
3521
|
+
ms: Math.max(0, (ctx.now ?? Date.now)() - t0)
|
|
3522
|
+
});
|
|
3523
|
+
} catch {}
|
|
3204
3524
|
return response(id, {
|
|
3205
3525
|
content: [{ type: "text", text: r.text }],
|
|
3526
|
+
...r.structured ? { structuredContent: r.structured } : {},
|
|
3206
3527
|
...r.isError ? { isError: true } : {}
|
|
3207
3528
|
});
|
|
3208
3529
|
}
|
|
@@ -3213,6 +3534,8 @@ async function handleLine(line, ctx) {
|
|
|
3213
3534
|
|
|
3214
3535
|
// ../../packages/shared/src/dispatch/plan.ts
|
|
3215
3536
|
var FRESH_MS = 24 * 3600000;
|
|
3537
|
+
// ../../packages/shared/src/dispatch/recommend.ts
|
|
3538
|
+
var QA_TASK_TYPES = new Set(["tests", "bugfix"]);
|
|
3216
3539
|
// src/codex.ts
|
|
3217
3540
|
init_config();
|
|
3218
3541
|
import { spawnSync } from "node:child_process";
|
|
@@ -3253,7 +3576,7 @@ ${out.startsWith(`
|
|
|
3253
3576
|
${providerBlock(proxyUrl)}
|
|
3254
3577
|
`;
|
|
3255
3578
|
}
|
|
3256
|
-
function removeCodexProvider(toml,
|
|
3579
|
+
function removeCodexProvider(toml, o = {}) {
|
|
3257
3580
|
const lines = toml.split(`
|
|
3258
3581
|
`);
|
|
3259
3582
|
const out = [];
|
|
@@ -3266,7 +3589,7 @@ function removeCodexProvider(toml, o2 = {}) {
|
|
|
3266
3589
|
}
|
|
3267
3590
|
let text = out.join(`
|
|
3268
3591
|
`);
|
|
3269
|
-
if (!
|
|
3592
|
+
if (!o.keepPrior) {
|
|
3270
3593
|
const prior = text.match(/^# sleds-prior-model-provider = "([^"]*)"\s*\n/m);
|
|
3271
3594
|
if (prior) {
|
|
3272
3595
|
text = text.replace(prior[0], "");
|
|
@@ -3283,22 +3606,22 @@ function topLevel(toml) {
|
|
|
3283
3606
|
const i = toml.search(/^\s*\[/m);
|
|
3284
3607
|
return i === -1 ? toml : toml.slice(0, i);
|
|
3285
3608
|
}
|
|
3286
|
-
function initCodex(
|
|
3609
|
+
function initCodex(o) {
|
|
3287
3610
|
const wrote = [];
|
|
3288
|
-
const path = codexConfigPath(
|
|
3611
|
+
const path = codexConfigPath(o.home);
|
|
3289
3612
|
const prior = existsSync3(path) ? readFileSync3(path, "utf8") : "";
|
|
3290
3613
|
mkdirSync3(dirname2(path), { recursive: true });
|
|
3291
|
-
writeFileSync3(path, applyCodexProvider(prior,
|
|
3292
|
-
wrote.push(`${prior ? "updated" : "created"} ${path}: provider "${PROVIDER_ID}" → ${
|
|
3293
|
-
const cfg = readConfig(
|
|
3294
|
-
writeConfig(
|
|
3614
|
+
writeFileSync3(path, applyCodexProvider(prior, o.proxyUrl), { mode: 384 });
|
|
3615
|
+
wrote.push(`${prior ? "updated" : "created"} ${path}: provider "${PROVIDER_ID}" → ${o.proxyUrl}/v1 (key from $${TOKEN_ENV}), model_provider = "${PROVIDER_ID}"`);
|
|
3616
|
+
const cfg = readConfig(o.home);
|
|
3617
|
+
writeConfig(o.home, {
|
|
3295
3618
|
...cfg,
|
|
3296
|
-
token:
|
|
3297
|
-
proxyUrl:
|
|
3619
|
+
token: o.token,
|
|
3620
|
+
proxyUrl: o.proxyUrl,
|
|
3298
3621
|
agentKind: "codex",
|
|
3299
|
-
...
|
|
3622
|
+
...o.email ? { email: o.email } : {}
|
|
3300
3623
|
});
|
|
3301
|
-
wrote.push(`saved token and proxy URL to ${configPath(
|
|
3624
|
+
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
3302
3625
|
return wrote;
|
|
3303
3626
|
}
|
|
3304
3627
|
function runCodex(home, args, deps = {}) {
|
|
@@ -3344,7 +3667,7 @@ var musherLabel = (env = process.env) => {
|
|
|
3344
3667
|
return `${MUSHER_LABEL}.${tag}`;
|
|
3345
3668
|
};
|
|
3346
3669
|
var musherPlistPath = (home, env = process.env) => join5(home, "Library", "LaunchAgents", `${musherLabel(env)}.plist`);
|
|
3347
|
-
var musherLogPath = (
|
|
3670
|
+
var musherLogPath = (configDir) => join5(configDir, "musher.log");
|
|
3348
3671
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3349
3672
|
var musherPath = (home, execPath, currentPath = "") => {
|
|
3350
3673
|
const parts = [
|
|
@@ -3360,11 +3683,11 @@ var musherPath = (home, execPath, currentPath = "") => {
|
|
|
3360
3683
|
];
|
|
3361
3684
|
return [...new Set(parts)].join(":");
|
|
3362
3685
|
};
|
|
3363
|
-
var buildMusherPlist = (
|
|
3364
|
-
const label =
|
|
3365
|
-
const args = [...
|
|
3686
|
+
var buildMusherPlist = (o) => {
|
|
3687
|
+
const label = o.label ?? MUSHER_LABEL;
|
|
3688
|
+
const args = [...o.sledsArgv, "musher", "tick"].map((a) => ` <string>${xmlEscape(a)}</string>`).join(`
|
|
3366
3689
|
`);
|
|
3367
|
-
const env = Object.entries(
|
|
3690
|
+
const env = Object.entries(o.env).filter(([, v]) => v !== "").map(([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
3368
3691
|
<string>${xmlEscape(v)}</string>`).join(`
|
|
3369
3692
|
`);
|
|
3370
3693
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -3386,9 +3709,9 @@ ${env}
|
|
|
3386
3709
|
<key>RunAtLoad</key>
|
|
3387
3710
|
<true/>
|
|
3388
3711
|
<key>StandardOutPath</key>
|
|
3389
|
-
<string>${xmlEscape(
|
|
3712
|
+
<string>${xmlEscape(o.logPath)}</string>
|
|
3390
3713
|
<key>StandardErrorPath</key>
|
|
3391
|
-
<string>${xmlEscape(
|
|
3714
|
+
<string>${xmlEscape(o.logPath)}</string>
|
|
3392
3715
|
</dict>
|
|
3393
3716
|
</plist>
|
|
3394
3717
|
`;
|
|
@@ -3405,15 +3728,15 @@ var NO_SESSION_PROMPT = [
|
|
|
3405
3728
|
"SLEDS musher: a deferred job started on this seat, but no Claude Code session was recorded",
|
|
3406
3729
|
"to resume. Do not guess at work. Reply with one line saying so, and stop."
|
|
3407
3730
|
].join(" ");
|
|
3408
|
-
var claudeArgs = (
|
|
3731
|
+
var claudeArgs = (o) => [
|
|
3409
3732
|
"-p",
|
|
3410
|
-
|
|
3411
|
-
...
|
|
3412
|
-
...
|
|
3733
|
+
o.sessionId ? RESUME_PROMPT : NO_SESSION_PROMPT,
|
|
3734
|
+
...o.sessionId ? ["--resume", o.sessionId] : [],
|
|
3735
|
+
...o.model ? ["--model", o.model] : [],
|
|
3413
3736
|
"--output-format",
|
|
3414
3737
|
"json",
|
|
3415
3738
|
"--permission-mode",
|
|
3416
|
-
|
|
3739
|
+
o.permissionMode
|
|
3417
3740
|
];
|
|
3418
3741
|
var parseClaudeOutput = (stdout, exitCode) => {
|
|
3419
3742
|
const lines = stdout.split(`
|
|
@@ -3443,11 +3766,11 @@ var parseClaudeOutput = (stdout, exitCode) => {
|
|
|
3443
3766
|
const tail = lines.slice(-5).join(" ").slice(0, SUMMARY_MAX);
|
|
3444
3767
|
return { ok: false, summary: tail || `claude exited ${exitCode ?? "without a code"}` };
|
|
3445
3768
|
};
|
|
3446
|
-
var defaultExecCapture = (cmd, args,
|
|
3769
|
+
var defaultExecCapture = (cmd, args, o) => {
|
|
3447
3770
|
const r = spawnSync2(cmd, [...args], {
|
|
3448
|
-
cwd:
|
|
3771
|
+
cwd: o.cwd,
|
|
3449
3772
|
encoding: "utf8",
|
|
3450
|
-
timeout:
|
|
3773
|
+
timeout: o.timeoutMs,
|
|
3451
3774
|
maxBuffer: 64 * 1024 * 1024,
|
|
3452
3775
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3453
3776
|
env: { ...process.env, SLEDS_MUSHER: "1" }
|
|
@@ -3459,16 +3782,16 @@ var defaultExecCapture = (cmd, args, o2) => {
|
|
|
3459
3782
|
...r.error ? { error: r.error.message } : {}
|
|
3460
3783
|
};
|
|
3461
3784
|
};
|
|
3462
|
-
var musherClient = (
|
|
3463
|
-
const fetchFn =
|
|
3464
|
-
const headers = { authorization: `Bearer ${
|
|
3785
|
+
var musherClient = (o) => {
|
|
3786
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
3787
|
+
const headers = { authorization: `Bearer ${o.token}`, "content-type": "application/json" };
|
|
3465
3788
|
const call = async (path, body) => {
|
|
3466
3789
|
try {
|
|
3467
|
-
const res = await fetchFn(`${
|
|
3790
|
+
const res = await fetchFn(`${o.proxyUrl}${path}`, {
|
|
3468
3791
|
method: body === undefined ? "GET" : "POST",
|
|
3469
3792
|
headers,
|
|
3470
3793
|
...body === undefined ? {} : { body: JSON.stringify(body) },
|
|
3471
|
-
signal: AbortSignal.timeout(
|
|
3794
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? 1e4)
|
|
3472
3795
|
});
|
|
3473
3796
|
if (!res.ok)
|
|
3474
3797
|
return null;
|
|
@@ -3510,33 +3833,33 @@ var collectOutputs = (commits, summary) => {
|
|
|
3510
3833
|
out.push(`pr:${m[0]}`);
|
|
3511
3834
|
return out.slice(0, 50);
|
|
3512
3835
|
};
|
|
3513
|
-
var runJob = async (
|
|
3514
|
-
const log =
|
|
3515
|
-
const sessionId =
|
|
3516
|
-
const cwd =
|
|
3517
|
-
const started = await
|
|
3836
|
+
var runJob = async (o) => {
|
|
3837
|
+
const log = o.log ?? (() => {});
|
|
3838
|
+
const sessionId = o.job.claudeSessionId ?? o.fallbackSession?.id ?? null;
|
|
3839
|
+
const cwd = o.job.cwd ?? o.fallbackSession?.cwd ?? undefined;
|
|
3840
|
+
const started = await o.client.start(o.job.jobId, {
|
|
3518
3841
|
...sessionId ? { claudeSessionId: sessionId } : {},
|
|
3519
3842
|
...cwd ? { cwd } : {},
|
|
3520
|
-
host:
|
|
3843
|
+
host: o.host
|
|
3521
3844
|
});
|
|
3522
3845
|
if (!started)
|
|
3523
3846
|
return { kind: "skipped", reason: "control plane unreachable" };
|
|
3524
3847
|
if (!started.ok)
|
|
3525
3848
|
return { kind: "skipped", reason: started.reason };
|
|
3526
|
-
const model =
|
|
3527
|
-
const args = claudeArgs({ sessionId, model, permissionMode:
|
|
3528
|
-
log(`musher: running job ${
|
|
3529
|
-
const exec =
|
|
3849
|
+
const model = o.job.model ?? started.model ?? o.fallbackSession?.model ?? null;
|
|
3850
|
+
const args = claudeArgs({ sessionId, model, permissionMode: o.permissionMode });
|
|
3851
|
+
log(`musher: running job ${o.job.jobId} (${sessionId ? `resume ${sessionId}` : "no session"}${model ? ` · ${model}` : ""}${cwd ? ` · ${cwd}` : ""})`);
|
|
3852
|
+
const exec = o.exec ?? defaultExecCapture;
|
|
3530
3853
|
const headBefore = gitHead(cwd, exec);
|
|
3531
3854
|
const t0 = Date.now();
|
|
3532
|
-
const r = exec(
|
|
3855
|
+
const r = exec(o.claudePath, args, { cwd, timeoutMs: o.timeoutMs });
|
|
3533
3856
|
const durationMs = Date.now() - t0;
|
|
3534
|
-
const result = r.error ? { ok: false, summary: `could not run ${
|
|
3857
|
+
const result = r.error ? { ok: false, summary: `could not run ${o.claudePath}: ${r.error}` } : parseClaudeOutput(r.stdout, r.status);
|
|
3535
3858
|
if (!r.error && r.status === null)
|
|
3536
|
-
result.summary = `timed out after ${Math.round(
|
|
3859
|
+
result.summary = `timed out after ${Math.round(o.timeoutMs / 60000)} min. ${result.summary}`.trim();
|
|
3537
3860
|
const outputs = collectOutputs(gitNewCommits(cwd, headBefore, gitHead(cwd, exec), exec), result.summary);
|
|
3538
|
-
await
|
|
3539
|
-
jobId:
|
|
3861
|
+
await o.client.finish({
|
|
3862
|
+
jobId: o.job.jobId,
|
|
3540
3863
|
runId: started.runId,
|
|
3541
3864
|
ok: result.ok,
|
|
3542
3865
|
summary: result.summary,
|
|
@@ -3545,7 +3868,7 @@ var runJob = async (o2) => {
|
|
|
3545
3868
|
durationMs: result.durationMs ?? durationMs,
|
|
3546
3869
|
...result.sessionId ?? sessionId ? { claudeSessionId: result.sessionId ?? sessionId } : {}
|
|
3547
3870
|
});
|
|
3548
|
-
log(`musher: job ${
|
|
3871
|
+
log(`musher: job ${o.job.jobId} ${result.ok ? "delivered" : "failed"} in ${Math.round(durationMs / 1000)}s`);
|
|
3549
3872
|
return { kind: "ran", ok: result.ok, summary: result.summary, durationMs, outputs };
|
|
3550
3873
|
};
|
|
3551
3874
|
var verifyAttestation = (payload, sig, publicKeyB64) => {
|
|
@@ -3849,11 +4172,11 @@ var bindingResetAt = (w, now) => {
|
|
|
3849
4172
|
return pick.length ? Math.min(...pick) : undefined;
|
|
3850
4173
|
};
|
|
3851
4174
|
var readSeatState = (home) => {
|
|
3852
|
-
const
|
|
3853
|
-
if (!existsSync5(
|
|
4175
|
+
const p = seatStatePath(home);
|
|
4176
|
+
if (!existsSync5(p))
|
|
3854
4177
|
return {};
|
|
3855
4178
|
try {
|
|
3856
|
-
return parseSeatState(JSON.parse(readFileSync5(
|
|
4179
|
+
return parseSeatState(JSON.parse(readFileSync5(p, "utf8")));
|
|
3857
4180
|
} catch {
|
|
3858
4181
|
return {};
|
|
3859
4182
|
}
|
|
@@ -3978,28 +4301,28 @@ var seatWindowBody = (w) => ({
|
|
|
3978
4301
|
...w.sevenDay ? { sevenDay: w.sevenDay } : {},
|
|
3979
4302
|
...w.spendLimit ? { spendLimit: w.spendLimit } : {}
|
|
3980
4303
|
});
|
|
3981
|
-
var postJson = async (
|
|
3982
|
-
const fetchFn =
|
|
4304
|
+
var postJson = async (o, path, body) => {
|
|
4305
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
3983
4306
|
try {
|
|
3984
|
-
const res = await fetchFn(`${
|
|
4307
|
+
const res = await fetchFn(`${o.proxyUrl}${path}`, {
|
|
3985
4308
|
method: "POST",
|
|
3986
|
-
headers: { authorization: `Bearer ${
|
|
4309
|
+
headers: { authorization: `Bearer ${o.token}`, "content-type": "application/json" },
|
|
3987
4310
|
body: JSON.stringify(body),
|
|
3988
|
-
signal: AbortSignal.timeout(
|
|
4311
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? SEAT_POST_TIMEOUT_MS)
|
|
3989
4312
|
});
|
|
3990
4313
|
return res.ok;
|
|
3991
4314
|
} catch {
|
|
3992
4315
|
return false;
|
|
3993
4316
|
}
|
|
3994
4317
|
};
|
|
3995
|
-
var postSeatWindow = (
|
|
3996
|
-
var postSeatWall = (
|
|
4318
|
+
var postSeatWindow = (o, w) => postJson(o, "/proxy/seat", seatWindowBody(w));
|
|
4319
|
+
var postSeatWall = (o, input) => postJson(o, "/proxy/seat-wall", { source: "claude", ...input });
|
|
3997
4320
|
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.";
|
|
3998
4321
|
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.";
|
|
3999
4322
|
var seatWallLine = (hasUpstreamKey) => hasUpstreamKey ? SEAT_WALL_LINE : SEAT_WALL_LINE_NO_KEY;
|
|
4000
|
-
var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((
|
|
4323
|
+
var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve) => {
|
|
4001
4324
|
if (stdin.isTTY) {
|
|
4002
|
-
|
|
4325
|
+
resolve("");
|
|
4003
4326
|
return;
|
|
4004
4327
|
}
|
|
4005
4328
|
let data = "";
|
|
@@ -4009,7 +4332,7 @@ var readStdin = (timeoutMs, stdin = process.stdin) => new Promise((resolve3) =>
|
|
|
4009
4332
|
return;
|
|
4010
4333
|
settled = true;
|
|
4011
4334
|
clearTimeout(timer);
|
|
4012
|
-
|
|
4335
|
+
resolve(data);
|
|
4013
4336
|
};
|
|
4014
4337
|
const timer = setTimeout(finish, timeoutMs);
|
|
4015
4338
|
stdin.setEncoding("utf8");
|
|
@@ -4127,21 +4450,21 @@ var parseFlags = (argv) => {
|
|
|
4127
4450
|
}
|
|
4128
4451
|
return flags;
|
|
4129
4452
|
};
|
|
4130
|
-
var defer = async (
|
|
4131
|
-
const log =
|
|
4132
|
-
const error =
|
|
4133
|
-
const now = (
|
|
4134
|
-
const flags = parseFlags(
|
|
4135
|
-
const client =
|
|
4136
|
-
const src = meterSource(
|
|
4137
|
-
return src ? musherClient({ ...src, fetchFn:
|
|
4453
|
+
var defer = async (o) => {
|
|
4454
|
+
const log = o.log ?? console.log;
|
|
4455
|
+
const error = o.error ?? console.error;
|
|
4456
|
+
const now = (o.now ?? Date.now)();
|
|
4457
|
+
const flags = parseFlags(o.argv);
|
|
4458
|
+
const client = o.client ?? (() => {
|
|
4459
|
+
const src = meterSource(o.home);
|
|
4460
|
+
return src ? musherClient({ ...src, fetchFn: o.fetchFn }) : null;
|
|
4138
4461
|
})();
|
|
4139
4462
|
if (!client) {
|
|
4140
4463
|
error("No seat on this machine. Run `sleds init` first.");
|
|
4141
4464
|
return 1;
|
|
4142
4465
|
}
|
|
4143
|
-
const state = readSeatState(
|
|
4144
|
-
const piped =
|
|
4466
|
+
const state = readSeatState(o.home);
|
|
4467
|
+
const piped = o.stdin ? seatSessionFromStdin(o.stdin, now) : null;
|
|
4145
4468
|
const remembered = piped ?? state.session;
|
|
4146
4469
|
const sessionId = flags.session ?? remembered?.id;
|
|
4147
4470
|
const cwd = flags.cwd ?? remembered?.cwd;
|
|
@@ -4162,7 +4485,7 @@ var defer = async (o2) => {
|
|
|
4162
4485
|
...model ? { model } : {},
|
|
4163
4486
|
...sessionId ? { claudeSessionId: sessionId } : {},
|
|
4164
4487
|
...cwd ? { cwd } : {},
|
|
4165
|
-
host:
|
|
4488
|
+
host: o.host ?? hostname()
|
|
4166
4489
|
});
|
|
4167
4490
|
if (!r) {
|
|
4168
4491
|
error("Could not reach the SLEDS proxy; nothing was scheduled.");
|
|
@@ -4171,8 +4494,8 @@ var defer = async (o2) => {
|
|
|
4171
4494
|
const when = r.scheduledAt <= now + 60000 ? "as soon as the musher ticks (within 5 min)" : `${formatLocal(r.scheduledAt)} (in ${formatDuration(r.scheduledAt - now)})`;
|
|
4172
4495
|
log(`Deferred. This Mac resumes ${sessionId ? `session ${sessionId.slice(0, 8)}…` : "the work"}${model ? ` on ${model}` : ""} ${when}.`);
|
|
4173
4496
|
log(` job ${r.jobId}${cwd ? ` · ${cwd}` : ""} · runs on your seat, no key · sleds musher --status to watch`);
|
|
4174
|
-
const cfg = readConfig(
|
|
4175
|
-
if (process.platform === "darwin" && !existsSync6(musherPlistPath(
|
|
4497
|
+
const cfg = readConfig(o.home);
|
|
4498
|
+
if (process.platform === "darwin" && !existsSync6(musherPlistPath(o.home)) && cfg.musher?.agent !== "launchd")
|
|
4176
4499
|
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.`);
|
|
4177
4500
|
log(" Keep this Mac awake at that time (System Settings → Battery → prevent sleeping on power adapter).");
|
|
4178
4501
|
return 0;
|
|
@@ -4352,7 +4675,7 @@ var renderCockpit = (cockpit, window, events, prior, now = Date.now(), extra = {
|
|
|
4352
4675
|
`/sled pickup ${newHandoffs[0]}`,
|
|
4353
4676
|
...ws(cockpit)
|
|
4354
4677
|
]);
|
|
4355
|
-
for (const id of extra.feed.delivered.filter((
|
|
4678
|
+
for (const id of extra.feed.delivered.filter((id) => !(before?.delivered ?? []).includes(id)))
|
|
4356
4679
|
argv.push([
|
|
4357
4680
|
"log",
|
|
4358
4681
|
"--level",
|
|
@@ -4362,7 +4685,7 @@ var renderCockpit = (cockpit, window, events, prior, now = Date.now(), extra = {
|
|
|
4362
4685
|
`overnight job done · /sled resume ${id}`,
|
|
4363
4686
|
...ws(cockpit)
|
|
4364
4687
|
]);
|
|
4365
|
-
for (const id of extra.feed.failed.filter((
|
|
4688
|
+
for (const id of extra.feed.failed.filter((id) => !(before?.failed ?? []).includes(id)))
|
|
4366
4689
|
argv.push([
|
|
4367
4690
|
"notify",
|
|
4368
4691
|
"--title",
|
|
@@ -4419,15 +4742,15 @@ var feedFromJobs = (body, me, now) => {
|
|
|
4419
4742
|
}
|
|
4420
4743
|
return f;
|
|
4421
4744
|
};
|
|
4422
|
-
var fetchFeed = async (
|
|
4745
|
+
var fetchFeed = async (o, now) => {
|
|
4423
4746
|
try {
|
|
4424
|
-
const res = await (
|
|
4425
|
-
headers: { authorization: `Bearer ${
|
|
4426
|
-
signal: AbortSignal.timeout(
|
|
4747
|
+
const res = await (o.fetchFn ?? fetch)(`${o.proxyUrl}/proxy/jobs`, {
|
|
4748
|
+
headers: { authorization: `Bearer ${o.token}` },
|
|
4749
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? 1500)
|
|
4427
4750
|
});
|
|
4428
4751
|
if (!res.ok)
|
|
4429
4752
|
return null;
|
|
4430
|
-
return feedFromJobs(await res.json(),
|
|
4753
|
+
return feedFromJobs(await res.json(), o.email, now);
|
|
4431
4754
|
} catch {
|
|
4432
4755
|
return null;
|
|
4433
4756
|
}
|
|
@@ -4463,9 +4786,9 @@ init_meter();
|
|
|
4463
4786
|
var defaultWhich = (cmd) => {
|
|
4464
4787
|
const path = process.env.PATH ?? "";
|
|
4465
4788
|
for (const dir of path.split(":").filter(Boolean)) {
|
|
4466
|
-
const
|
|
4467
|
-
if (existsSync7(
|
|
4468
|
-
return
|
|
4789
|
+
const p = `${dir}/${cmd}`;
|
|
4790
|
+
if (existsSync7(p))
|
|
4791
|
+
return p;
|
|
4469
4792
|
}
|
|
4470
4793
|
return null;
|
|
4471
4794
|
};
|
|
@@ -4475,14 +4798,14 @@ var findClaude = (home, configured, which) => {
|
|
|
4475
4798
|
const found = which("claude");
|
|
4476
4799
|
if (found)
|
|
4477
4800
|
return found;
|
|
4478
|
-
for (const
|
|
4801
|
+
for (const p of [
|
|
4479
4802
|
`${home}/.local/bin/claude`,
|
|
4480
4803
|
"/opt/homebrew/bin/claude",
|
|
4481
4804
|
"/usr/local/bin/claude",
|
|
4482
4805
|
`${home}/.npm-global/bin/claude`
|
|
4483
4806
|
])
|
|
4484
|
-
if (existsSync7(
|
|
4485
|
-
return
|
|
4807
|
+
if (existsSync7(p))
|
|
4808
|
+
return p;
|
|
4486
4809
|
return null;
|
|
4487
4810
|
};
|
|
4488
4811
|
var musherAgentState = (home, env = process.env) => {
|
|
@@ -4545,10 +4868,10 @@ function defaultWhich2(cmd) {
|
|
|
4545
4868
|
return [];
|
|
4546
4869
|
}
|
|
4547
4870
|
}
|
|
4548
|
-
async function runDoctor(
|
|
4549
|
-
const env =
|
|
4871
|
+
async function runDoctor(o) {
|
|
4872
|
+
const env = o.env ?? process.env;
|
|
4550
4873
|
const checks = [];
|
|
4551
|
-
const home =
|
|
4874
|
+
const home = o.home;
|
|
4552
4875
|
const overrides = [
|
|
4553
4876
|
env.SLEDS_HOME?.trim() ? `SLEDS_HOME=${env.SLEDS_HOME.trim()}` : null,
|
|
4554
4877
|
env.CLAUDE_CONFIG_DIR?.trim() ? `CLAUDE_CONFIG_DIR=${env.CLAUDE_CONFIG_DIR.trim()}` : null
|
|
@@ -4598,7 +4921,7 @@ async function runDoctor(o2) {
|
|
|
4598
4921
|
}
|
|
4599
4922
|
const src = meterSource(home);
|
|
4600
4923
|
if (src) {
|
|
4601
|
-
const health = await checkHealth(src.proxyUrl,
|
|
4924
|
+
const health = await checkHealth(src.proxyUrl, o.fetchFn, o.timeoutMs);
|
|
4602
4925
|
checks.push({
|
|
4603
4926
|
level: health === "ok" ? "ok" : "fail",
|
|
4604
4927
|
name: "proxy",
|
|
@@ -4607,15 +4930,15 @@ async function runDoctor(o2) {
|
|
|
4607
4930
|
});
|
|
4608
4931
|
}
|
|
4609
4932
|
if (settings) {
|
|
4610
|
-
const
|
|
4933
|
+
const seat = hasSeatHook(settings);
|
|
4611
4934
|
const spec = hasSpecHook(settings);
|
|
4612
4935
|
const session = hasSessionHook(settings);
|
|
4613
4936
|
const stop = hasStopHook(settings);
|
|
4614
|
-
const all =
|
|
4937
|
+
const all = seat && spec && session && stop;
|
|
4615
4938
|
checks.push({
|
|
4616
4939
|
level: all ? "ok" : "warn",
|
|
4617
4940
|
name: "hooks",
|
|
4618
|
-
detail: `seat-event ${
|
|
4941
|
+
detail: `seat-event ${seat ? "on" : "missing"} · spec-event ${spec ? "on" : "missing"} · session-event ${session ? "on" : "missing"} · stop-event ${stop ? "on" : "missing"}`,
|
|
4619
4942
|
fix: all ? undefined : "sleds hooks"
|
|
4620
4943
|
});
|
|
4621
4944
|
const want = env.SLEDS_HOME?.trim() ? resolve3(home, env.SLEDS_HOME.trim()) : undefined;
|
|
@@ -4646,18 +4969,18 @@ async function runDoctor(o2) {
|
|
|
4646
4969
|
name: "cockpit",
|
|
4647
4970
|
detail: cockpit ? `${cockpit.kind}${cockpit.workspaceId ? ` (workspace ${cockpit.workspaceId})` : ""} — seat line in the sidebar, ring at the wall` : "none detected (run inside cmux to mirror the seat into its sidebar)"
|
|
4648
4971
|
});
|
|
4649
|
-
const nodeVersion =
|
|
4972
|
+
const nodeVersion = o.nodeVersion ?? process.version;
|
|
4650
4973
|
checks.push({
|
|
4651
4974
|
level: nodeOk(nodeVersion) ? "ok" : "warn",
|
|
4652
4975
|
name: "node",
|
|
4653
4976
|
detail: `${nodeVersion}${nodeOk(nodeVersion) ? "" : ` — seat-sync needs ≥ ${NODE_MIN} (node:sqlite)`}`,
|
|
4654
4977
|
fix: nodeOk(nodeVersion) ? undefined : `brew install node@22 && echo 'export PATH="/opt/homebrew/opt/node@22/bin:$PATH"' >> ~/.zshrc`
|
|
4655
4978
|
});
|
|
4656
|
-
const which = (
|
|
4657
|
-
const stale = which.filter((
|
|
4979
|
+
const which = (o.which ?? defaultWhich2)("sleds");
|
|
4980
|
+
const stale = which.filter((p) => {
|
|
4658
4981
|
try {
|
|
4659
|
-
const target = lstatSync(
|
|
4660
|
-
return /\/src\/index\.ts$/.test(target) || /\.bun\/bin\/sleds$/.test(
|
|
4982
|
+
const target = lstatSync(p).isSymbolicLink() ? readlinkSync(p) : p;
|
|
4983
|
+
return /\/src\/index\.ts$/.test(target) || /\.bun\/bin\/sleds$/.test(p);
|
|
4661
4984
|
} catch {
|
|
4662
4985
|
return false;
|
|
4663
4986
|
}
|
|
@@ -4679,7 +5002,7 @@ async function runDoctor(o2) {
|
|
|
4679
5002
|
detail: age === null ? "no report yet — run one Claude Code turn on this seat" : `last report ${age < 1 ? "just now" : `${age}m ago`} via the status line${seat.window?.sevenDay ? ` · wk ${Math.round(seat.window.sevenDay.usedPct)}%` : ""}`,
|
|
4680
5003
|
fix: age === null ? "open claude, say hi, exit — the status line posts the seat's quota" : undefined
|
|
4681
5004
|
});
|
|
4682
|
-
if ((
|
|
5005
|
+
if ((o.platform ?? process.platform) === "darwin") {
|
|
4683
5006
|
const agent = musherAgentState(home, env);
|
|
4684
5007
|
checks.push(!agent.installed ? {
|
|
4685
5008
|
level: "warn",
|
|
@@ -4696,14 +5019,14 @@ async function runDoctor(o2) {
|
|
|
4696
5019
|
name: "musher",
|
|
4697
5020
|
detail: `launchd agent ${musherLabel(env)}, every 5 min`
|
|
4698
5021
|
});
|
|
4699
|
-
const claude = findClaude(home, readConfig(home).musher?.claudePath, (c) => (
|
|
5022
|
+
const claude = findClaude(home, readConfig(home).musher?.claudePath, (c) => (o.which ?? defaultWhich2)(c)[0] ?? null);
|
|
4700
5023
|
checks.push(claude ? { level: "ok", name: "claude cli", detail: claude } : {
|
|
4701
5024
|
level: "warn",
|
|
4702
5025
|
name: "claude cli",
|
|
4703
5026
|
detail: "`claude` not on PATH — the musher cannot run Claude Code",
|
|
4704
5027
|
fix: 'sleds musher --claude "$(which claude)"'
|
|
4705
5028
|
});
|
|
4706
|
-
const sleep = (
|
|
5029
|
+
const sleep = (o.pmset ?? defaultPmset)();
|
|
4707
5030
|
if (sleep !== null)
|
|
4708
5031
|
checks.push(sleep.acSleepMinutes === 0 ? {
|
|
4709
5032
|
level: "ok",
|
|
@@ -4733,9 +5056,9 @@ var defaultPmset = () => {
|
|
|
4733
5056
|
return null;
|
|
4734
5057
|
}
|
|
4735
5058
|
};
|
|
4736
|
-
var doctor = async (
|
|
4737
|
-
const log =
|
|
4738
|
-
const checks = await runDoctor(
|
|
5059
|
+
var doctor = async (o) => {
|
|
5060
|
+
const log = o.log ?? console.log;
|
|
5061
|
+
const checks = await runDoctor(o);
|
|
4739
5062
|
for (const line of formatDoctor(checks))
|
|
4740
5063
|
log(line);
|
|
4741
5064
|
return checks.some((c) => c.level === "fail") ? 1 : 0;
|
|
@@ -4763,21 +5086,21 @@ function sourceKey(s) {
|
|
|
4763
5086
|
function sourceText2(s) {
|
|
4764
5087
|
return [`${str5(s.kind) ?? "?"} ${str5(s.project) ?? "?"}`, str5(s.doc), str5(s.item)].filter(Boolean).join(" › ");
|
|
4765
5088
|
}
|
|
4766
|
-
var readJson = (
|
|
4767
|
-
if (!existsSync9(
|
|
5089
|
+
var readJson = (p) => {
|
|
5090
|
+
if (!existsSync9(p))
|
|
4768
5091
|
return null;
|
|
4769
5092
|
try {
|
|
4770
|
-
return JSON.parse(readFileSync8(
|
|
5093
|
+
return JSON.parse(readFileSync8(p, "utf8"));
|
|
4771
5094
|
} catch {
|
|
4772
5095
|
return null;
|
|
4773
5096
|
}
|
|
4774
5097
|
};
|
|
4775
|
-
var writeJson = (
|
|
4776
|
-
mkdirSync7(dirname4(
|
|
4777
|
-
const tmp = `${
|
|
5098
|
+
var writeJson = (p, v) => {
|
|
5099
|
+
mkdirSync7(dirname4(p), { recursive: true, mode: 448 });
|
|
5100
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
4778
5101
|
writeFileSync7(tmp, `${JSON.stringify(v, null, 2)}
|
|
4779
5102
|
`, { mode: 384 });
|
|
4780
|
-
renameSync5(tmp,
|
|
5103
|
+
renameSync5(tmp, p);
|
|
4781
5104
|
};
|
|
4782
5105
|
var readOwed = (home, now = Date.now()) => {
|
|
4783
5106
|
const f = readJson(owedPath(home));
|
|
@@ -4792,14 +5115,14 @@ var readSession = (home) => {
|
|
|
4792
5115
|
var writeSession = (home, s) => writeJson(sessionPath(home), s);
|
|
4793
5116
|
function withEstimated(rows, a) {
|
|
4794
5117
|
const key = sourceKey(a.source);
|
|
4795
|
-
const
|
|
5118
|
+
const row = {
|
|
4796
5119
|
key,
|
|
4797
5120
|
source: a.source,
|
|
4798
5121
|
...a.title ? { title: a.title } : {},
|
|
4799
5122
|
ts: a.now,
|
|
4800
5123
|
...a.sessionId ? { sessionId: a.sessionId } : {}
|
|
4801
5124
|
};
|
|
4802
|
-
return [...rows.filter((r) => r.key !== key),
|
|
5125
|
+
return [...rows.filter((r) => r.key !== key), row];
|
|
4803
5126
|
}
|
|
4804
5127
|
function withActual(rows, a) {
|
|
4805
5128
|
if (a.state === "started")
|
|
@@ -4897,59 +5220,59 @@ var callTool2 = (name, args, deps) => callTool(name, args, contextFor(deps));
|
|
|
4897
5220
|
var handleLine2 = (line, deps) => handleLine(line, contextFor(deps));
|
|
4898
5221
|
|
|
4899
5222
|
// src/commands/handoffs.ts
|
|
4900
|
-
var handoffsCmd = async (
|
|
4901
|
-
const r = await callTool2("handoffs", {}, { home:
|
|
4902
|
-
(r.isError ?
|
|
5223
|
+
var handoffsCmd = async (o) => {
|
|
5224
|
+
const r = await callTool2("handoffs", {}, { home: o.home });
|
|
5225
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
4903
5226
|
return r.isError ? 1 : 0;
|
|
4904
5227
|
};
|
|
4905
|
-
var pickupCmd = async (
|
|
4906
|
-
const id =
|
|
5228
|
+
var pickupCmd = async (o) => {
|
|
5229
|
+
const id = o.argv[0];
|
|
4907
5230
|
if (!id) {
|
|
4908
|
-
(
|
|
5231
|
+
(o.error ?? console.error)("usage: sleds pickup <handoffId> (ids from `sleds handoffs`)");
|
|
4909
5232
|
return 2;
|
|
4910
5233
|
}
|
|
4911
|
-
const r = await callTool2("pickup", { handoffId: id }, { home:
|
|
4912
|
-
(r.isError ?
|
|
5234
|
+
const r = await callTool2("pickup", { handoffId: id }, { home: o.home });
|
|
5235
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
4913
5236
|
return r.isError ? 1 : 0;
|
|
4914
5237
|
};
|
|
4915
|
-
var withdrawCmd = async (
|
|
4916
|
-
const id =
|
|
5238
|
+
var withdrawCmd = async (o) => {
|
|
5239
|
+
const id = o.argv[0];
|
|
4917
5240
|
if (!id) {
|
|
4918
|
-
(
|
|
5241
|
+
(o.error ?? console.error)("usage: sleds withdraw <handoffId> (one you parked)");
|
|
4919
5242
|
return 2;
|
|
4920
5243
|
}
|
|
4921
|
-
const r = await callTool2("withdraw", { handoffId: id }, { home:
|
|
4922
|
-
(r.isError ?
|
|
5244
|
+
const r = await callTool2("withdraw", { handoffId: id }, { home: o.home });
|
|
5245
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
4923
5246
|
return r.isError ? 1 : 0;
|
|
4924
5247
|
};
|
|
4925
5248
|
|
|
4926
5249
|
// src/commands/hooks.ts
|
|
4927
5250
|
init_settings();
|
|
4928
|
-
var hooks = (
|
|
4929
|
-
const log =
|
|
4930
|
-
const error =
|
|
4931
|
-
if (!hasClaudeCode(
|
|
4932
|
-
error(`Claude Code not found (${claudeDir(
|
|
5251
|
+
var hooks = (o) => {
|
|
5252
|
+
const log = o.log ?? console.log;
|
|
5253
|
+
const error = o.error ?? console.error;
|
|
5254
|
+
if (!hasClaudeCode(o.home)) {
|
|
5255
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
4933
5256
|
return 1;
|
|
4934
5257
|
}
|
|
4935
|
-
const settings = readSettings(
|
|
5258
|
+
const settings = readSettings(o.home);
|
|
4936
5259
|
const had = hasSeatHook(settings);
|
|
4937
5260
|
const hadSpec = hasSpecHook(settings);
|
|
4938
|
-
const withSeat = applySeatHook(settings,
|
|
5261
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
4939
5262
|
const hadPrompt = hasPromptHook(settings);
|
|
4940
5263
|
const hadSession = hasSessionHook(settings);
|
|
4941
5264
|
const hadStop = hasStopHook(settings);
|
|
4942
|
-
const withPrompt = applyPromptHook(applySpecHook(withSeat,
|
|
4943
|
-
const withSpec = applyStopHook(applySessionHook(withPrompt,
|
|
4944
|
-
const hadLine =
|
|
4945
|
-
writeSettings(
|
|
4946
|
-
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(
|
|
5265
|
+
const withPrompt = applyPromptHook(applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env)), o.promptEventCommand ?? sledsCommand("prompt-event", o.home, o.env));
|
|
5266
|
+
const withSpec = applyStopHook(applySessionHook(withPrompt, o.sessionEventCommand ?? sledsCommand("session-event", o.home, o.env)), o.stopEventCommand ?? sledsCommand("stop-event", o.home, o.env));
|
|
5267
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
5268
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
5269
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
4947
5270
|
if (hadLine)
|
|
4948
|
-
log(`refreshed statusLine in ${settingsPath(
|
|
4949
|
-
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(
|
|
4950
|
-
log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(
|
|
4951
|
-
log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(
|
|
4952
|
-
log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(
|
|
5271
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
5272
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
5273
|
+
log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o.home)}`);
|
|
5274
|
+
log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(o.home)}`);
|
|
5275
|
+
log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(o.home)}`);
|
|
4953
5276
|
return 0;
|
|
4954
5277
|
};
|
|
4955
5278
|
|
|
@@ -5100,43 +5423,172 @@ From Cowork or any MCP client (\`sleds mcp\`): the \`handoffs\` tool lists what
|
|
|
5100
5423
|
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.
|
|
5101
5424
|
`;
|
|
5102
5425
|
|
|
5426
|
+
// src/commands/device.ts
|
|
5427
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5428
|
+
import { hostname as hostname2, platform } from "node:os";
|
|
5429
|
+
var DEVICE_REASONS = {
|
|
5430
|
+
unreachable: "could not reach the proxy.",
|
|
5431
|
+
rate_limited: "too many sign-ins are open right now. Try again in a few minutes.",
|
|
5432
|
+
denied: "that sign-in was declined on the web.",
|
|
5433
|
+
expired: "the code expired before it was approved (they last fifteen minutes). Run `sleds init` again.",
|
|
5434
|
+
bad_answer: "the proxy gave an answer this CLI does not understand — update it: curl -fsSL sleds.ai/install | sh"
|
|
5435
|
+
};
|
|
5436
|
+
function deviceFailureText(reason) {
|
|
5437
|
+
return `Sign-in did not complete: ${DEVICE_REASONS[reason]} Or mint a seat on the dashboard (Settings › Your seat & tools) and run \`sleds init --token …\`.`;
|
|
5438
|
+
}
|
|
5439
|
+
function parseStart(body) {
|
|
5440
|
+
if (typeof body !== "object" || body === null)
|
|
5441
|
+
return null;
|
|
5442
|
+
const b = body;
|
|
5443
|
+
if (typeof b.deviceCode !== "string" || typeof b.userCode !== "string")
|
|
5444
|
+
return null;
|
|
5445
|
+
if (typeof b.verifyUrl !== "string" || !/^https?:\/\//.test(b.verifyUrl))
|
|
5446
|
+
return null;
|
|
5447
|
+
const expiresIn = typeof b.expiresIn === "number" && b.expiresIn > 0 ? Math.min(b.expiresIn, 3600) : 900;
|
|
5448
|
+
const interval = typeof b.interval === "number" && b.interval >= 1 ? Math.min(b.interval, 30) : 3;
|
|
5449
|
+
return {
|
|
5450
|
+
deviceCode: b.deviceCode,
|
|
5451
|
+
userCode: b.userCode,
|
|
5452
|
+
verifyUrl: b.verifyUrl,
|
|
5453
|
+
expiresIn,
|
|
5454
|
+
interval
|
|
5455
|
+
};
|
|
5456
|
+
}
|
|
5457
|
+
function parsePoll(body) {
|
|
5458
|
+
if (typeof body !== "object" || body === null)
|
|
5459
|
+
return null;
|
|
5460
|
+
const b = body;
|
|
5461
|
+
if (b.status === "pending" || b.status === "denied" || b.status === "expired")
|
|
5462
|
+
return { status: b.status };
|
|
5463
|
+
if (b.status !== "approved")
|
|
5464
|
+
return null;
|
|
5465
|
+
const org = b.org;
|
|
5466
|
+
if (typeof b.token !== "string" || typeof b.email !== "string" || !org)
|
|
5467
|
+
return null;
|
|
5468
|
+
if (typeof org.name !== "string" || typeof org.slug !== "string")
|
|
5469
|
+
return null;
|
|
5470
|
+
return {
|
|
5471
|
+
status: "approved",
|
|
5472
|
+
approved: {
|
|
5473
|
+
token: b.token,
|
|
5474
|
+
email: b.email,
|
|
5475
|
+
org: { name: org.name, slug: org.slug, plan: typeof org.plan === "string" ? org.plan : "" },
|
|
5476
|
+
workspaceUrl: typeof b.workspaceUrl === "string" ? b.workspaceUrl : ""
|
|
5477
|
+
}
|
|
5478
|
+
};
|
|
5479
|
+
}
|
|
5480
|
+
function openInBrowser(url) {
|
|
5481
|
+
const p = platform();
|
|
5482
|
+
const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
|
|
5483
|
+
const args = p === "win32" ? ["/c", "start", "", url] : [url];
|
|
5484
|
+
try {
|
|
5485
|
+
const r = spawnSync3(cmd, args, { stdio: "ignore", timeout: 5000 });
|
|
5486
|
+
return r.error === undefined && r.status === 0;
|
|
5487
|
+
} catch {
|
|
5488
|
+
return false;
|
|
5489
|
+
}
|
|
5490
|
+
}
|
|
5491
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5492
|
+
async function deviceFlow(o) {
|
|
5493
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
5494
|
+
const log = o.log ?? console.log;
|
|
5495
|
+
const sleep = o.sleep ?? defaultSleep;
|
|
5496
|
+
const now = o.now ?? Date.now;
|
|
5497
|
+
let started = null;
|
|
5498
|
+
try {
|
|
5499
|
+
const res = await fetchFn(`${o.proxyUrl}/device/start`, {
|
|
5500
|
+
method: "POST",
|
|
5501
|
+
headers: { "content-type": "application/json" },
|
|
5502
|
+
body: JSON.stringify({ agent: o.agent ?? "claude-code", host: o.host ?? hostname2() }),
|
|
5503
|
+
signal: AbortSignal.timeout(20000)
|
|
5504
|
+
});
|
|
5505
|
+
if (res.status === 429)
|
|
5506
|
+
return { ok: false, reason: "rate_limited" };
|
|
5507
|
+
if (!res.ok)
|
|
5508
|
+
return { ok: false, reason: "unreachable" };
|
|
5509
|
+
started = parseStart(await res.json().catch(() => null));
|
|
5510
|
+
} catch {
|
|
5511
|
+
return { ok: false, reason: "unreachable" };
|
|
5512
|
+
}
|
|
5513
|
+
if (!started)
|
|
5514
|
+
return { ok: false, reason: "bad_answer" };
|
|
5515
|
+
const opened = o.noBrowser ? false : (o.openBrowser ?? openInBrowser)(started.verifyUrl);
|
|
5516
|
+
log("");
|
|
5517
|
+
log(opened ? "Opening your browser to sign in to SLEDS." : "Open this in your browser to sign in to SLEDS:");
|
|
5518
|
+
log(` ${started.verifyUrl}`);
|
|
5519
|
+
log("");
|
|
5520
|
+
log(` Your code: ${started.userCode} — the page shows the same one.`);
|
|
5521
|
+
log("");
|
|
5522
|
+
log("Waiting for the approval…");
|
|
5523
|
+
const deadline = now() + started.expiresIn * 1000;
|
|
5524
|
+
let intervalMs = started.interval * 1000;
|
|
5525
|
+
while (now() < deadline) {
|
|
5526
|
+
await sleep(intervalMs);
|
|
5527
|
+
let body;
|
|
5528
|
+
try {
|
|
5529
|
+
const res = await fetchFn(`${o.proxyUrl}/device/poll`, {
|
|
5530
|
+
method: "POST",
|
|
5531
|
+
headers: { "content-type": "application/json" },
|
|
5532
|
+
body: JSON.stringify({ deviceCode: started.deviceCode }),
|
|
5533
|
+
signal: AbortSignal.timeout(20000)
|
|
5534
|
+
});
|
|
5535
|
+
if (res.status === 429) {
|
|
5536
|
+
intervalMs = Math.min(intervalMs * 2, 30000);
|
|
5537
|
+
continue;
|
|
5538
|
+
}
|
|
5539
|
+
body = await res.json().catch(() => null);
|
|
5540
|
+
} catch {
|
|
5541
|
+
continue;
|
|
5542
|
+
}
|
|
5543
|
+
const r = parsePoll(body);
|
|
5544
|
+
if (!r)
|
|
5545
|
+
return { ok: false, reason: "bad_answer" };
|
|
5546
|
+
if (r.status === "pending")
|
|
5547
|
+
continue;
|
|
5548
|
+
if (r.status === "approved")
|
|
5549
|
+
return { ok: true, approved: r.approved };
|
|
5550
|
+
return { ok: false, reason: r.status };
|
|
5551
|
+
}
|
|
5552
|
+
return { ok: false, reason: "expired" };
|
|
5553
|
+
}
|
|
5554
|
+
|
|
5103
5555
|
// src/commands/hooks.ts
|
|
5104
5556
|
init_settings();
|
|
5105
|
-
var hooks2 = (
|
|
5106
|
-
const log =
|
|
5107
|
-
const error =
|
|
5108
|
-
if (!hasClaudeCode(
|
|
5109
|
-
error(`Claude Code not found (${claudeDir(
|
|
5557
|
+
var hooks2 = (o) => {
|
|
5558
|
+
const log = o.log ?? console.log;
|
|
5559
|
+
const error = o.error ?? console.error;
|
|
5560
|
+
if (!hasClaudeCode(o.home)) {
|
|
5561
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
5110
5562
|
return 1;
|
|
5111
5563
|
}
|
|
5112
|
-
const settings = readSettings(
|
|
5564
|
+
const settings = readSettings(o.home);
|
|
5113
5565
|
const had = hasSeatHook(settings);
|
|
5114
5566
|
const hadSpec = hasSpecHook(settings);
|
|
5115
|
-
const withSeat = applySeatHook(settings,
|
|
5567
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
5116
5568
|
const hadPrompt = hasPromptHook(settings);
|
|
5117
5569
|
const hadSession = hasSessionHook(settings);
|
|
5118
5570
|
const hadStop = hasStopHook(settings);
|
|
5119
|
-
const withPrompt = applyPromptHook(applySpecHook(withSeat,
|
|
5120
|
-
const withSpec = applyStopHook(applySessionHook(withPrompt,
|
|
5121
|
-
const hadLine =
|
|
5122
|
-
writeSettings(
|
|
5123
|
-
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(
|
|
5571
|
+
const withPrompt = applyPromptHook(applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env)), o.promptEventCommand ?? sledsCommand("prompt-event", o.home, o.env));
|
|
5572
|
+
const withSpec = applyStopHook(applySessionHook(withPrompt, o.sessionEventCommand ?? sledsCommand("session-event", o.home, o.env)), o.stopEventCommand ?? sledsCommand("stop-event", o.home, o.env));
|
|
5573
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
5574
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
5575
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
5124
5576
|
if (hadLine)
|
|
5125
|
-
log(`refreshed statusLine in ${settingsPath(
|
|
5126
|
-
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(
|
|
5127
|
-
log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(
|
|
5128
|
-
log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(
|
|
5129
|
-
log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(
|
|
5577
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
5578
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
5579
|
+
log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o.home)}`);
|
|
5580
|
+
log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(o.home)}`);
|
|
5581
|
+
log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(o.home)}`);
|
|
5130
5582
|
return 0;
|
|
5131
5583
|
};
|
|
5132
5584
|
|
|
5133
5585
|
// src/commands/init.ts
|
|
5134
5586
|
var DEFAULT_PROXY_URL = "https://proxy.sleds.ai";
|
|
5135
|
-
var promptStdin = (question) => new Promise((
|
|
5587
|
+
var promptStdin = (question) => new Promise((resolve) => {
|
|
5136
5588
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
5137
5589
|
rl.question(question, (answer) => {
|
|
5138
5590
|
rl.close();
|
|
5139
|
-
|
|
5591
|
+
resolve(answer.trim());
|
|
5140
5592
|
});
|
|
5141
5593
|
});
|
|
5142
5594
|
async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
|
|
@@ -5233,56 +5685,70 @@ async function probeProxy(url, fetchFn = fetch, opts = {}) {
|
|
|
5233
5685
|
}
|
|
5234
5686
|
return false;
|
|
5235
5687
|
}
|
|
5236
|
-
var resolveToken = async (
|
|
5237
|
-
const env =
|
|
5238
|
-
if (
|
|
5239
|
-
return { token:
|
|
5688
|
+
var resolveToken = async (o) => {
|
|
5689
|
+
const env = o.env ?? process.env;
|
|
5690
|
+
if (o.token)
|
|
5691
|
+
return { token: o.token.trim(), source: "flag" };
|
|
5240
5692
|
if (env.SLEDS_TOKEN)
|
|
5241
5693
|
return { token: env.SLEDS_TOKEN.trim(), source: "env" };
|
|
5242
|
-
const fromFile = readTokenFile(
|
|
5694
|
+
const fromFile = readTokenFile(o.home);
|
|
5243
5695
|
if (fromFile)
|
|
5244
5696
|
return { token: fromFile, source: "file" };
|
|
5245
|
-
if (
|
|
5697
|
+
if (o.nonInteractive)
|
|
5246
5698
|
return null;
|
|
5247
|
-
|
|
5248
|
-
const typed = (await o2.prompt("Paste your SLEDS seat token (sled_…), or press enter to sign up as an individual: ")).trim();
|
|
5249
|
-
if (typed)
|
|
5250
|
-
return { token: typed, source: "prompt" };
|
|
5251
|
-
}
|
|
5252
|
-
const url = normalizeProxyUrl(o2.url ?? env.SLEDS_PROXY_URL ?? readConfig(o2.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
5699
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
5253
5700
|
if (!url)
|
|
5254
5701
|
return null;
|
|
5255
|
-
const log =
|
|
5256
|
-
const err =
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5702
|
+
const log = o.log ?? console.log;
|
|
5703
|
+
const err = o.error ?? console.error;
|
|
5704
|
+
if (!o.signup) {
|
|
5705
|
+
const r = await deviceFlow({
|
|
5706
|
+
proxyUrl: url,
|
|
5707
|
+
agent: "claude-code",
|
|
5708
|
+
host: o.host,
|
|
5709
|
+
fetchFn: o.fetchFn,
|
|
5710
|
+
openBrowser: o.openBrowser ?? openInBrowser,
|
|
5711
|
+
noBrowser: o.noBrowser,
|
|
5712
|
+
sleep: o.sleep,
|
|
5713
|
+
log
|
|
5714
|
+
});
|
|
5715
|
+
if (!r.ok) {
|
|
5716
|
+
err(deviceFailureText(r.reason));
|
|
5717
|
+
return null;
|
|
5718
|
+
}
|
|
5719
|
+
return { token: r.approved.token, source: "device", approved: r.approved };
|
|
5720
|
+
}
|
|
5721
|
+
if (!o.prompt)
|
|
5722
|
+
return null;
|
|
5723
|
+
let team = o.team?.trim() || null;
|
|
5724
|
+
if (team === null && !o.email) {
|
|
5725
|
+
const which = (await o.prompt("Set up SLEDS for (1) just you, or (2) your team? [1/2]: ")).trim();
|
|
5260
5726
|
if (which === "2") {
|
|
5261
|
-
team = (await
|
|
5727
|
+
team = (await o.prompt("Team name (e.g. your company): ")).trim();
|
|
5262
5728
|
if (!team)
|
|
5263
5729
|
return null;
|
|
5264
5730
|
}
|
|
5265
5731
|
}
|
|
5266
|
-
const email = (
|
|
5732
|
+
const email = (o.email ?? await o.prompt(team ? "Your work email: " : "Your email: ")).trim();
|
|
5267
5733
|
if (!email.includes("@"))
|
|
5268
5734
|
return null;
|
|
5269
5735
|
if (team) {
|
|
5270
|
-
const
|
|
5271
|
-
const
|
|
5272
|
-
if (!
|
|
5273
|
-
err(signupFailureText(
|
|
5736
|
+
const credential = (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();
|
|
5737
|
+
const r = await signupTeam(url, email, team, credential || null, o.fetchFn);
|
|
5738
|
+
if (!r.ok) {
|
|
5739
|
+
err(signupFailureText(r));
|
|
5274
5740
|
return null;
|
|
5275
5741
|
}
|
|
5276
|
-
log(`Created ${team} with you as owner (${email}).${
|
|
5277
|
-
if (
|
|
5742
|
+
log(`Created ${team} with you as owner (${email}).${credential ? " 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."}`);
|
|
5743
|
+
if (r.verifyPending)
|
|
5278
5744
|
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).`);
|
|
5279
5745
|
log("Invite your team on the dashboard (Members → invite by email, Admin → Mint a seat); each person runs `sleds init --token …`.");
|
|
5280
|
-
return { token:
|
|
5746
|
+
return { token: r.token, source: "signup" };
|
|
5281
5747
|
}
|
|
5282
|
-
const credential = (await
|
|
5748
|
+
const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
|
|
5283
5749
|
if (!credential)
|
|
5284
5750
|
return null;
|
|
5285
|
-
const r = await signupIndividual(url, email, credential,
|
|
5751
|
+
const r = await signupIndividual(url, email, credential, o.fetchFn);
|
|
5286
5752
|
if (!r.ok) {
|
|
5287
5753
|
err(signupFailureText(r));
|
|
5288
5754
|
return null;
|
|
@@ -5290,66 +5756,66 @@ var resolveToken = async (o2) => {
|
|
|
5290
5756
|
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.`);
|
|
5291
5757
|
return { token: r.token, source: "signup" };
|
|
5292
5758
|
};
|
|
5293
|
-
var init = async (
|
|
5294
|
-
const log =
|
|
5295
|
-
const error =
|
|
5296
|
-
const env =
|
|
5297
|
-
if (!hasClaudeCode(
|
|
5298
|
-
error(`Claude Code not found (${claudeDir(
|
|
5759
|
+
var init = async (o) => {
|
|
5760
|
+
const log = o.log ?? console.log;
|
|
5761
|
+
const error = o.error ?? console.error;
|
|
5762
|
+
const env = o.env ?? process.env;
|
|
5763
|
+
if (!hasClaudeCode(o.home)) {
|
|
5764
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
5299
5765
|
return 1;
|
|
5300
5766
|
}
|
|
5301
|
-
const resolved = await resolveToken(
|
|
5767
|
+
const resolved = await resolveToken(o);
|
|
5302
5768
|
if (!resolved) {
|
|
5303
|
-
error(
|
|
5769
|
+
error(o.signup ? "Signup did not complete. Nothing was changed." : "No seat token. Run `sleds init` without --non-interactive to sign in through the browser, or pass --token / set SLEDS_TOKEN.");
|
|
5304
5770
|
return 2;
|
|
5305
5771
|
}
|
|
5306
5772
|
if (!isSeatToken(resolved.token)) {
|
|
5307
5773
|
error("Invalid seat token: expected something like sled_xxxxxxxx.");
|
|
5308
5774
|
return 2;
|
|
5309
5775
|
}
|
|
5310
|
-
const url = normalizeProxyUrl(
|
|
5776
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
5311
5777
|
if (!url) {
|
|
5312
|
-
error(`Invalid proxy URL: ${JSON.stringify(
|
|
5778
|
+
error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
|
|
5313
5779
|
return 2;
|
|
5314
5780
|
}
|
|
5315
|
-
if (!
|
|
5781
|
+
if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
|
|
5316
5782
|
error(`No SLEDS proxy answered at ${url}/healthz. Check the URL (or pass --no-probe to save it anyway).`);
|
|
5317
5783
|
return 2;
|
|
5318
5784
|
}
|
|
5319
5785
|
const wrote = [];
|
|
5320
|
-
const seatInfo = await fetchSeatInfo(url, resolved.token,
|
|
5786
|
+
const seatInfo = await fetchSeatInfo(url, resolved.token, o.fetchFn);
|
|
5321
5787
|
const org = seatInfo.org;
|
|
5322
|
-
const wiring =
|
|
5788
|
+
const wiring = o.proxyAlways ? "proxy-always" : o.seatFirst ? "seat-first" : org?.mode ?? "proxy-always";
|
|
5323
5789
|
if (wiring === "proxy-always" && org && !org.capabilities.upstreamKey) {
|
|
5324
5790
|
error(`${org.name} has no platform key for the proxy to forward with; --proxy-always would break every request. Using seat-first.`);
|
|
5325
5791
|
}
|
|
5326
5792
|
const finalWiring = wiring === "proxy-always" && org && !org.capabilities.upstreamKey ? "seat-first" : wiring;
|
|
5327
|
-
if (backupSettings(
|
|
5328
|
-
wrote.push(`backed up settings to ${backupPath(
|
|
5329
|
-
const current = readSettings(
|
|
5793
|
+
if (backupSettings(o.home))
|
|
5794
|
+
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
5795
|
+
const current = readSettings(o.home);
|
|
5330
5796
|
const wired = finalWiring === "proxy-always" ? applyProxy(current, { url, token: resolved.token }) : removeProxy(current);
|
|
5331
|
-
const
|
|
5332
|
-
writeSettings(
|
|
5333
|
-
wrote.push(finalWiring === "proxy-always" ? `set env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN in ${settingsPath(
|
|
5334
|
-
wrote.push(
|
|
5797
|
+
const statusLine = applyStatusLine(wired, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
|
|
5798
|
+
writeSettings(o.home, statusLine.settings);
|
|
5799
|
+
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`);
|
|
5800
|
+
wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
|
|
5335
5801
|
hooks2({
|
|
5336
|
-
home:
|
|
5337
|
-
env:
|
|
5802
|
+
home: o.home,
|
|
5803
|
+
env: o.env,
|
|
5338
5804
|
refreshStatusLine: false,
|
|
5339
|
-
seatEventCommand:
|
|
5340
|
-
specEventCommand:
|
|
5341
|
-
promptEventCommand:
|
|
5342
|
-
sessionEventCommand:
|
|
5343
|
-
stopEventCommand:
|
|
5805
|
+
seatEventCommand: o.seatEventCommand,
|
|
5806
|
+
specEventCommand: o.specEventCommand,
|
|
5807
|
+
promptEventCommand: o.promptEventCommand,
|
|
5808
|
+
sessionEventCommand: o.sessionEventCommand,
|
|
5809
|
+
stopEventCommand: o.stopEventCommand,
|
|
5344
5810
|
log: (line) => wrote.push(line),
|
|
5345
5811
|
error
|
|
5346
5812
|
});
|
|
5347
5813
|
if (resolved.source !== "file") {
|
|
5348
|
-
writeTokenFile(
|
|
5349
|
-
wrote.push(`saved token to ${tokenPath(
|
|
5814
|
+
writeTokenFile(o.home, resolved.token);
|
|
5815
|
+
wrote.push(`saved token to ${tokenPath(o.home)} (0600)`);
|
|
5350
5816
|
}
|
|
5351
|
-
const prior = readConfig(
|
|
5352
|
-
writeConfig(
|
|
5817
|
+
const prior = readConfig(o.home);
|
|
5818
|
+
writeConfig(o.home, {
|
|
5353
5819
|
...prior,
|
|
5354
5820
|
token: resolved.token,
|
|
5355
5821
|
proxyUrl: url,
|
|
@@ -5357,18 +5823,24 @@ var init = async (o2) => {
|
|
|
5357
5823
|
agentKind: "claude-code",
|
|
5358
5824
|
...seatInfo.email ? { email: seatInfo.email } : {},
|
|
5359
5825
|
...org ? { org } : {},
|
|
5360
|
-
...
|
|
5826
|
+
...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
|
|
5361
5827
|
});
|
|
5362
|
-
wrote.push(`saved token and proxy URL to ${configPath(
|
|
5363
|
-
mkdirSync8(skillDir(
|
|
5364
|
-
writeFileSync8(skillPath(
|
|
5365
|
-
wrote.push(`installed skill at ${skillPath(
|
|
5366
|
-
if (
|
|
5367
|
-
const r =
|
|
5828
|
+
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
5829
|
+
mkdirSync8(skillDir(o.home), { recursive: true });
|
|
5830
|
+
writeFileSync8(skillPath(o.home), SKILL_MD);
|
|
5831
|
+
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
5832
|
+
if (o.installMusher) {
|
|
5833
|
+
const r = o.installMusher();
|
|
5368
5834
|
if (r)
|
|
5369
5835
|
wrote.push(r);
|
|
5370
5836
|
}
|
|
5371
5837
|
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)";
|
|
5838
|
+
if (resolved.approved) {
|
|
5839
|
+
log("");
|
|
5840
|
+
log(`Signed in as ${resolved.approved.email} · ${resolved.approved.org.name}. This machine is now a seat there.`);
|
|
5841
|
+
if (resolved.approved.workspaceUrl)
|
|
5842
|
+
log(`Your workspace on the web: ${resolved.approved.workspaceUrl}`);
|
|
5843
|
+
}
|
|
5372
5844
|
if (finalWiring === "proxy-always") {
|
|
5373
5845
|
log(`Claude Code now runs through SLEDS on the platform key. (${orgLine})`);
|
|
5374
5846
|
log("");
|
|
@@ -5395,9 +5867,9 @@ var init = async (o2) => {
|
|
|
5395
5867
|
};
|
|
5396
5868
|
|
|
5397
5869
|
// src/commands/mcp.ts
|
|
5398
|
-
var mcp = async (
|
|
5399
|
-
const stdin =
|
|
5400
|
-
const write =
|
|
5870
|
+
var mcp = async (o) => {
|
|
5871
|
+
const stdin = o.stdin ?? process.stdin;
|
|
5872
|
+
const write = o.write ?? ((line) => process.stdout.write(`${line}
|
|
5401
5873
|
`));
|
|
5402
5874
|
let buffer = "";
|
|
5403
5875
|
for await (const chunk of stdin) {
|
|
@@ -5407,7 +5879,7 @@ var mcp = async (o2) => {
|
|
|
5407
5879
|
while (nl !== -1) {
|
|
5408
5880
|
const line = buffer.slice(0, nl);
|
|
5409
5881
|
buffer = buffer.slice(nl + 1);
|
|
5410
|
-
const out = await handleLine2(line, { home:
|
|
5882
|
+
const out = await handleLine2(line, { home: o.home });
|
|
5411
5883
|
if (out !== null)
|
|
5412
5884
|
write(out);
|
|
5413
5885
|
nl = buffer.indexOf(`
|
|
@@ -5415,7 +5887,7 @@ var mcp = async (o2) => {
|
|
|
5415
5887
|
}
|
|
5416
5888
|
}
|
|
5417
5889
|
if (buffer.trim().length > 0) {
|
|
5418
|
-
const out = await handleLine2(buffer, { home:
|
|
5890
|
+
const out = await handleLine2(buffer, { home: o.home });
|
|
5419
5891
|
if (out !== null)
|
|
5420
5892
|
write(out);
|
|
5421
5893
|
}
|
|
@@ -5425,15 +5897,15 @@ var mcp = async (o2) => {
|
|
|
5425
5897
|
// src/commands/meter.ts
|
|
5426
5898
|
init_config();
|
|
5427
5899
|
init_meter();
|
|
5428
|
-
var meter = async (
|
|
5429
|
-
const log =
|
|
5430
|
-
const now = (
|
|
5431
|
-
const src = meterSource(
|
|
5900
|
+
var meter = async (o) => {
|
|
5901
|
+
const log = o.log ?? console.log;
|
|
5902
|
+
const now = (o.now ?? Date.now)();
|
|
5903
|
+
const src = meterSource(o.home);
|
|
5432
5904
|
if (!src) {
|
|
5433
|
-
log(`SLEDS is not set up (${configPath(
|
|
5905
|
+
log(`SLEDS is not set up (${configPath(o.home)}). Run \`sleds init\` first.`);
|
|
5434
5906
|
return 1;
|
|
5435
5907
|
}
|
|
5436
|
-
const m = await loadMeter({ home:
|
|
5908
|
+
const m = await loadMeter({ home: o.home, now, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs });
|
|
5437
5909
|
if (!m) {
|
|
5438
5910
|
log(`sleds offline. ${src.proxyUrl} did not answer → \`sleds status\` for the proxy health.`);
|
|
5439
5911
|
return 2;
|
|
@@ -5446,16 +5918,16 @@ var meter = async (o2) => {
|
|
|
5446
5918
|
// src/commands/musher.ts
|
|
5447
5919
|
init_config();
|
|
5448
5920
|
init_meter();
|
|
5449
|
-
import { spawnSync as
|
|
5921
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
5450
5922
|
import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "node:fs";
|
|
5451
|
-
import { hostname as
|
|
5923
|
+
import { hostname as hostname3 } from "node:os";
|
|
5452
5924
|
import { dirname as dirname5 } from "node:path";
|
|
5453
5925
|
var defaultWhich3 = (cmd) => {
|
|
5454
5926
|
const path = process.env.PATH ?? "";
|
|
5455
5927
|
for (const dir of path.split(":").filter(Boolean)) {
|
|
5456
|
-
const
|
|
5457
|
-
if (existsSync10(
|
|
5458
|
-
return
|
|
5928
|
+
const p = `${dir}/${cmd}`;
|
|
5929
|
+
if (existsSync10(p))
|
|
5930
|
+
return p;
|
|
5459
5931
|
}
|
|
5460
5932
|
return null;
|
|
5461
5933
|
};
|
|
@@ -5465,68 +5937,68 @@ var findClaude2 = (home, configured, which) => {
|
|
|
5465
5937
|
const found = which("claude");
|
|
5466
5938
|
if (found)
|
|
5467
5939
|
return found;
|
|
5468
|
-
for (const
|
|
5940
|
+
for (const p of [
|
|
5469
5941
|
`${home}/.local/bin/claude`,
|
|
5470
5942
|
"/opt/homebrew/bin/claude",
|
|
5471
5943
|
"/usr/local/bin/claude",
|
|
5472
5944
|
`${home}/.npm-global/bin/claude`
|
|
5473
5945
|
])
|
|
5474
|
-
if (existsSync10(
|
|
5475
|
-
return
|
|
5946
|
+
if (existsSync10(p))
|
|
5947
|
+
return p;
|
|
5476
5948
|
return null;
|
|
5477
5949
|
};
|
|
5478
|
-
var musherInstall = (
|
|
5479
|
-
const log =
|
|
5480
|
-
const error =
|
|
5481
|
-
const platform =
|
|
5950
|
+
var musherInstall = (o) => {
|
|
5951
|
+
const log = o.log ?? console.log;
|
|
5952
|
+
const error = o.error ?? console.error;
|
|
5953
|
+
const platform = o.platform ?? process.platform;
|
|
5482
5954
|
if (platform !== "darwin") {
|
|
5483
5955
|
error(`sleds musher --install needs launchd (macOS). On ${platform}, run \`sleds musher tick\` from cron every 5 minutes.`);
|
|
5484
5956
|
return 1;
|
|
5485
5957
|
}
|
|
5486
|
-
const env =
|
|
5487
|
-
const plist = musherPlistPath(
|
|
5958
|
+
const env = o.env ?? process.env;
|
|
5959
|
+
const plist = musherPlistPath(o.home, env);
|
|
5488
5960
|
const label = musherLabel(env);
|
|
5489
|
-
const dir = configDir(
|
|
5961
|
+
const dir = configDir(o.home, env);
|
|
5490
5962
|
mkdirSync9(dirname5(plist), { recursive: true });
|
|
5491
5963
|
mkdirSync9(dir, { recursive: true, mode: 448 });
|
|
5492
|
-
const execPath =
|
|
5964
|
+
const execPath = o.sledsArgv[0] ?? process.execPath;
|
|
5493
5965
|
const agentEnv = {
|
|
5494
|
-
PATH: musherPath(
|
|
5495
|
-
HOME:
|
|
5966
|
+
PATH: musherPath(o.home, execPath, env.PATH ?? ""),
|
|
5967
|
+
HOME: o.home,
|
|
5496
5968
|
...env.SLEDS_HOME ? { SLEDS_HOME: env.SLEDS_HOME } : {},
|
|
5497
5969
|
...env.CLAUDE_CONFIG_DIR ? { CLAUDE_CONFIG_DIR: env.CLAUDE_CONFIG_DIR } : {}
|
|
5498
5970
|
};
|
|
5499
|
-
const exec =
|
|
5971
|
+
const exec = o.exec ?? defaultExecSync;
|
|
5500
5972
|
if (existsSync10(plist))
|
|
5501
5973
|
exec("launchctl", ["unload", plist]);
|
|
5502
|
-
writeFileSync9(plist, buildMusherPlist({ sledsArgv:
|
|
5974
|
+
writeFileSync9(plist, buildMusherPlist({ sledsArgv: o.sledsArgv, env: agentEnv, logPath: musherLogPath(dir), label }));
|
|
5503
5975
|
const r = exec("launchctl", ["load", plist]);
|
|
5504
5976
|
if (!r.ok) {
|
|
5505
5977
|
error(`launchctl load failed: ${r.stderr.trim() || "unknown"}`);
|
|
5506
5978
|
return 1;
|
|
5507
5979
|
}
|
|
5508
|
-
const cfg = readConfig(
|
|
5509
|
-
writeConfig(
|
|
5510
|
-
const claude = findClaude2(
|
|
5980
|
+
const cfg = readConfig(o.home);
|
|
5981
|
+
writeConfig(o.home, { ...cfg, musher: { ...cfg.musher ?? {}, agent: "launchd" } });
|
|
5982
|
+
const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3);
|
|
5511
5983
|
log(`musher installed: launchd agent ${label}, every 5 min · log ${musherLogPath(dir)}`);
|
|
5512
5984
|
if (!claude)
|
|
5513
5985
|
log("warn: `claude` not found on PATH — set it with `sleds musher --claude /path/to/claude`");
|
|
5514
5986
|
return 0;
|
|
5515
5987
|
};
|
|
5516
|
-
var musherUninstall = (
|
|
5517
|
-
const log =
|
|
5518
|
-
const env =
|
|
5519
|
-
const plist = musherPlistPath(
|
|
5520
|
-
const exec =
|
|
5988
|
+
var musherUninstall = (o) => {
|
|
5989
|
+
const log = o.log ?? console.log;
|
|
5990
|
+
const env = o.env ?? process.env;
|
|
5991
|
+
const plist = musherPlistPath(o.home, env);
|
|
5992
|
+
const exec = o.exec ?? defaultExecSync;
|
|
5521
5993
|
if (existsSync10(plist)) {
|
|
5522
5994
|
exec("launchctl", ["unload", plist]);
|
|
5523
5995
|
rmSync2(plist, { force: true });
|
|
5524
5996
|
log(`musher removed: ${musherLabel(env)}`);
|
|
5525
5997
|
} else
|
|
5526
5998
|
log("musher was not installed.");
|
|
5527
|
-
const cfg = readConfig(
|
|
5999
|
+
const cfg = readConfig(o.home);
|
|
5528
6000
|
if (cfg.musher)
|
|
5529
|
-
writeConfig(
|
|
6001
|
+
writeConfig(o.home, { ...cfg, musher: { ...cfg.musher, agent: "none" } });
|
|
5530
6002
|
return 0;
|
|
5531
6003
|
};
|
|
5532
6004
|
var formatMusherStatus = (list, now) => {
|
|
@@ -5541,23 +6013,23 @@ var formatMusherStatus = (list, now) => {
|
|
|
5541
6013
|
lines.push("nothing deferred on this seat.");
|
|
5542
6014
|
return lines;
|
|
5543
6015
|
};
|
|
5544
|
-
var clientFor = (
|
|
5545
|
-
if (
|
|
5546
|
-
return
|
|
5547
|
-
const src = meterSource(
|
|
5548
|
-
return src ? musherClient({ ...src, fetchFn:
|
|
5549
|
-
};
|
|
5550
|
-
var musherStatus = async (
|
|
5551
|
-
const log =
|
|
5552
|
-
const now = (
|
|
5553
|
-
const env =
|
|
5554
|
-
const plist = musherPlistPath(
|
|
5555
|
-
const cfg = readConfig(
|
|
6016
|
+
var clientFor = (o) => {
|
|
6017
|
+
if (o.client)
|
|
6018
|
+
return o.client;
|
|
6019
|
+
const src = meterSource(o.home);
|
|
6020
|
+
return src ? musherClient({ ...src, fetchFn: o.fetchFn }) : null;
|
|
6021
|
+
};
|
|
6022
|
+
var musherStatus = async (o) => {
|
|
6023
|
+
const log = o.log ?? console.log;
|
|
6024
|
+
const now = (o.now ?? Date.now)();
|
|
6025
|
+
const env = o.env ?? process.env;
|
|
6026
|
+
const plist = musherPlistPath(o.home, env);
|
|
6027
|
+
const cfg = readConfig(o.home);
|
|
5556
6028
|
log(existsSync10(plist) ? `agent ${musherLabel(env)} installed (${plist})` : "agent not installed — sleds musher --install");
|
|
5557
|
-
const claude = findClaude2(
|
|
6029
|
+
const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3);
|
|
5558
6030
|
log(`claude ${claude ?? "not found — sleds musher --claude /path/to/claude"}`);
|
|
5559
6031
|
log(`mode ${cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE} · timeout ${cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES} min`);
|
|
5560
|
-
const client = clientFor(
|
|
6032
|
+
const client = clientFor(o);
|
|
5561
6033
|
if (!client) {
|
|
5562
6034
|
log("seat not initialised (sleds init)");
|
|
5563
6035
|
return 1;
|
|
@@ -5571,9 +6043,9 @@ var musherStatus = async (o2) => {
|
|
|
5571
6043
|
log(line);
|
|
5572
6044
|
return 0;
|
|
5573
6045
|
};
|
|
5574
|
-
var musherTick = async (
|
|
5575
|
-
const log =
|
|
5576
|
-
const client = clientFor(
|
|
6046
|
+
var musherTick = async (o) => {
|
|
6047
|
+
const log = o.log ?? console.log;
|
|
6048
|
+
const client = clientFor(o);
|
|
5577
6049
|
if (!client) {
|
|
5578
6050
|
log("musher: seat not initialised; nothing to do");
|
|
5579
6051
|
return 1;
|
|
@@ -5585,19 +6057,19 @@ var musherTick = async (o2) => {
|
|
|
5585
6057
|
}
|
|
5586
6058
|
if (list.due.length === 0)
|
|
5587
6059
|
return 0;
|
|
5588
|
-
const cfg = readConfig(
|
|
5589
|
-
const claude = findClaude2(
|
|
5590
|
-
const seat = readSeatState(
|
|
6060
|
+
const cfg = readConfig(o.home);
|
|
6061
|
+
const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3) ?? "claude";
|
|
6062
|
+
const seat = readSeatState(o.home);
|
|
5591
6063
|
for (const job of list.due) {
|
|
5592
6064
|
const out = await runJob({
|
|
5593
6065
|
job,
|
|
5594
6066
|
client,
|
|
5595
6067
|
fallbackSession: seat.session,
|
|
5596
|
-
host:
|
|
6068
|
+
host: o.host ?? hostname3(),
|
|
5597
6069
|
claudePath: claude,
|
|
5598
6070
|
permissionMode: cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE,
|
|
5599
6071
|
timeoutMs: (cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES) * 60000,
|
|
5600
|
-
exec:
|
|
6072
|
+
exec: o.execCapture,
|
|
5601
6073
|
log
|
|
5602
6074
|
});
|
|
5603
6075
|
if (out.kind === "skipped")
|
|
@@ -5605,9 +6077,9 @@ var musherTick = async (o2) => {
|
|
|
5605
6077
|
}
|
|
5606
6078
|
return 0;
|
|
5607
6079
|
};
|
|
5608
|
-
var musherConfigure = (
|
|
5609
|
-
const log =
|
|
5610
|
-
const cfg = readConfig(
|
|
6080
|
+
var musherConfigure = (o, flags) => {
|
|
6081
|
+
const log = o.log ?? console.log;
|
|
6082
|
+
const cfg = readConfig(o.home);
|
|
5611
6083
|
const musher = { ...cfg.musher ?? {} };
|
|
5612
6084
|
if (flags.claude)
|
|
5613
6085
|
musher.claudePath = flags.claude;
|
|
@@ -5616,23 +6088,23 @@ var musherConfigure = (o2, flags) => {
|
|
|
5616
6088
|
if (flags.timeout) {
|
|
5617
6089
|
const n = Number.parseInt(flags.timeout, 10);
|
|
5618
6090
|
if (!Number.isFinite(n) || n <= 0) {
|
|
5619
|
-
(
|
|
6091
|
+
(o.error ?? console.error)("--timeout takes minutes, a positive number");
|
|
5620
6092
|
return 1;
|
|
5621
6093
|
}
|
|
5622
6094
|
musher.timeoutMinutes = n;
|
|
5623
6095
|
}
|
|
5624
|
-
writeConfig(
|
|
6096
|
+
writeConfig(o.home, { ...cfg, musher });
|
|
5625
6097
|
log(`musher settings: claude ${musher.claudePath ?? "(PATH)"} · mode ${musher.permissionMode ?? DEFAULT_PERMISSION_MODE} · timeout ${musher.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES} min`);
|
|
5626
6098
|
return 0;
|
|
5627
6099
|
};
|
|
5628
6100
|
var defaultExecSync = (cmd, args) => {
|
|
5629
|
-
const r =
|
|
6101
|
+
const r = spawnSync4(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
5630
6102
|
if (r.error)
|
|
5631
6103
|
return { ok: false, stderr: r.error.message };
|
|
5632
6104
|
return { ok: r.status === 0, stderr: r.stderr ?? "" };
|
|
5633
6105
|
};
|
|
5634
|
-
var musher = async (
|
|
5635
|
-
const args = [...
|
|
6106
|
+
var musher = async (o) => {
|
|
6107
|
+
const args = [...o.argv];
|
|
5636
6108
|
const flags = {};
|
|
5637
6109
|
const positional = [];
|
|
5638
6110
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -5648,16 +6120,16 @@ var musher = async (o2) => {
|
|
|
5648
6120
|
positional.push(a);
|
|
5649
6121
|
}
|
|
5650
6122
|
if (flags.install)
|
|
5651
|
-
return musherInstall(
|
|
6123
|
+
return musherInstall(o);
|
|
5652
6124
|
if (flags.uninstall)
|
|
5653
|
-
return musherUninstall(
|
|
6125
|
+
return musherUninstall(o);
|
|
5654
6126
|
if (flags.claude || flags.mode || flags.timeout)
|
|
5655
|
-
return musherConfigure(
|
|
6127
|
+
return musherConfigure(o, flags);
|
|
5656
6128
|
if (positional[0] === "tick")
|
|
5657
|
-
return musherTick(
|
|
6129
|
+
return musherTick(o);
|
|
5658
6130
|
if (flags.status || positional.length === 0)
|
|
5659
|
-
return musherStatus(
|
|
5660
|
-
(
|
|
6131
|
+
return musherStatus(o);
|
|
6132
|
+
(o.error ?? console.error)(`sleds musher: unknown argument ${positional[0]}`);
|
|
5661
6133
|
return 1;
|
|
5662
6134
|
};
|
|
5663
6135
|
|
|
@@ -5705,12 +6177,12 @@ var systemdCalendar = (deadline) => `${deadline.getFullYear()}-${pad2(deadline.g
|
|
|
5705
6177
|
var scheduleRevert = (d, deadlineMs) => {
|
|
5706
6178
|
const deadline = new Date(deadlineMs);
|
|
5707
6179
|
if (d.platform === "darwin") {
|
|
5708
|
-
const
|
|
6180
|
+
const p = plistPath(d.home);
|
|
5709
6181
|
try {
|
|
5710
6182
|
mkdirSync10(join9(d.home, "Library", "LaunchAgents"), { recursive: true });
|
|
5711
|
-
d.exec("launchctl", ["unload",
|
|
5712
|
-
writeFileSync10(
|
|
5713
|
-
const r = d.exec("launchctl", ["load",
|
|
6183
|
+
d.exec("launchctl", ["unload", p]);
|
|
6184
|
+
writeFileSync10(p, buildLaunchdPlist(d.sledsArgv, deadline));
|
|
6185
|
+
const r = d.exec("launchctl", ["load", p]);
|
|
5714
6186
|
if (r.ok)
|
|
5715
6187
|
return { kind: "launchd", detail: `launchd agent ${REVERT_LABEL}` };
|
|
5716
6188
|
return { kind: "manual", detail: `launchctl load failed: ${r.stderr.trim() || "unknown"}` };
|
|
@@ -5735,10 +6207,10 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
5735
6207
|
};
|
|
5736
6208
|
var cancelRevert = (d, kind) => {
|
|
5737
6209
|
if (kind === "launchd" || d.platform === "darwin") {
|
|
5738
|
-
const
|
|
5739
|
-
if (existsSync11(
|
|
5740
|
-
d.exec("launchctl", ["unload",
|
|
5741
|
-
rmSync3(
|
|
6210
|
+
const p = plistPath(d.home);
|
|
6211
|
+
if (existsSync11(p)) {
|
|
6212
|
+
d.exec("launchctl", ["unload", p]);
|
|
6213
|
+
rmSync3(p, { force: true });
|
|
5742
6214
|
}
|
|
5743
6215
|
}
|
|
5744
6216
|
if (kind === "systemd" || d.platform === "linux") {
|
|
@@ -5748,15 +6220,15 @@ var cancelRevert = (d, kind) => {
|
|
|
5748
6220
|
|
|
5749
6221
|
// src/commands/off.ts
|
|
5750
6222
|
init_settings();
|
|
5751
|
-
var off = (
|
|
5752
|
-
const log =
|
|
5753
|
-
const cfg = readConfig(
|
|
5754
|
-
const settings = readSettings(
|
|
6223
|
+
var off = (o) => {
|
|
6224
|
+
const log = o.log ?? console.log;
|
|
6225
|
+
const cfg = readConfig(o.home);
|
|
6226
|
+
const settings = readSettings(o.home);
|
|
5755
6227
|
const wasOn = cfg.mode === "on" || isPointedAtProxy(settings);
|
|
5756
6228
|
if (wasOn)
|
|
5757
|
-
writeSettings(
|
|
5758
|
-
if (
|
|
5759
|
-
cancelRevert(
|
|
6229
|
+
writeSettings(o.home, switchOff(settings, cfg.priorEnv ?? {}));
|
|
6230
|
+
if (o.scheduler)
|
|
6231
|
+
cancelRevert(o.scheduler, cfg.schedule);
|
|
5760
6232
|
const next = {
|
|
5761
6233
|
...cfg,
|
|
5762
6234
|
mode: "off",
|
|
@@ -5766,7 +6238,7 @@ var off = (o2) => {
|
|
|
5766
6238
|
priorEnv: undefined,
|
|
5767
6239
|
schedule: undefined
|
|
5768
6240
|
};
|
|
5769
|
-
writeConfig(
|
|
6241
|
+
writeConfig(o.home, next);
|
|
5770
6242
|
if (!wasOn) {
|
|
5771
6243
|
log("Sleds already off. Claude Code is on your seat.");
|
|
5772
6244
|
return 0;
|
|
@@ -5785,19 +6257,19 @@ init_settings();
|
|
|
5785
6257
|
var DEFAULT_PROXY_URL2 = "https://proxy.sleds.ai";
|
|
5786
6258
|
|
|
5787
6259
|
// src/commands/on.ts
|
|
5788
|
-
var on = (
|
|
5789
|
-
const log =
|
|
5790
|
-
const error =
|
|
5791
|
-
const env =
|
|
5792
|
-
const now = (
|
|
5793
|
-
if (!hasClaudeCode(
|
|
5794
|
-
error(`Claude Code not found (${claudeDir(
|
|
6260
|
+
var on = (o) => {
|
|
6261
|
+
const log = o.log ?? console.log;
|
|
6262
|
+
const error = o.error ?? console.error;
|
|
6263
|
+
const env = o.env ?? process.env;
|
|
6264
|
+
const now = (o.now ?? Date.now)();
|
|
6265
|
+
if (!hasClaudeCode(o.home)) {
|
|
6266
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
5795
6267
|
return 1;
|
|
5796
6268
|
}
|
|
5797
|
-
const cfg = readConfig(
|
|
5798
|
-
const token = cfg.token ?? readTokenFile(
|
|
6269
|
+
const cfg = readConfig(o.home);
|
|
6270
|
+
const token = cfg.token ?? readTokenFile(o.home) ?? env.SLEDS_TOKEN?.trim();
|
|
5799
6271
|
if (!token) {
|
|
5800
|
-
error(`No seat token in ${configPath(
|
|
6272
|
+
error(`No seat token in ${configPath(o.home)}. Run \`sleds init\` first.`);
|
|
5801
6273
|
return 2;
|
|
5802
6274
|
}
|
|
5803
6275
|
if (!isSeatToken(token)) {
|
|
@@ -5808,28 +6280,28 @@ var on = (o2) => {
|
|
|
5808
6280
|
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.`);
|
|
5809
6281
|
return 2;
|
|
5810
6282
|
}
|
|
5811
|
-
const url = (
|
|
6283
|
+
const url = (o.url ?? cfg.proxyUrl ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL2).replace(/\/+$/, "");
|
|
5812
6284
|
let untilResetMs;
|
|
5813
|
-
if (
|
|
6285
|
+
if (o.untilReset !== undefined) {
|
|
5814
6286
|
try {
|
|
5815
|
-
untilResetMs = parseUntilReset(
|
|
6287
|
+
untilResetMs = parseUntilReset(o.untilReset, now);
|
|
5816
6288
|
} catch (err) {
|
|
5817
6289
|
error(err instanceof Error ? err.message : String(err));
|
|
5818
6290
|
return 2;
|
|
5819
6291
|
}
|
|
5820
6292
|
}
|
|
5821
|
-
const settings = readSettings(
|
|
6293
|
+
const settings = readSettings(o.home);
|
|
5822
6294
|
const priorEnv = cfg.mode === "on" && cfg.priorEnv ? cfg.priorEnv : capturePriorEnv(settings);
|
|
5823
|
-
const backedUp = backupSettings(
|
|
5824
|
-
writeSettings(
|
|
6295
|
+
const backedUp = backupSettings(o.home);
|
|
6296
|
+
writeSettings(o.home, switchOn(settings, { url, token }));
|
|
5825
6297
|
let schedule;
|
|
5826
6298
|
let scheduleNote;
|
|
5827
|
-
if (untilResetMs !== undefined && !
|
|
6299
|
+
if (untilResetMs !== undefined && !o.noSchedule && o.scheduler && hasHomeOverride(env)) {
|
|
5828
6300
|
error("SLEDS_HOME / CLAUDE_CONFIG_DIR is set: the auto-revert timer would act on the default seat. Re-run with --no-schedule and run `sleds off` yourself at the reset.");
|
|
5829
6301
|
return 2;
|
|
5830
6302
|
}
|
|
5831
|
-
if (untilResetMs !== undefined && !
|
|
5832
|
-
const r = scheduleRevert(
|
|
6303
|
+
if (untilResetMs !== undefined && !o.noSchedule && o.scheduler) {
|
|
6304
|
+
const r = scheduleRevert(o.scheduler, untilResetMs);
|
|
5833
6305
|
schedule = r.kind;
|
|
5834
6306
|
scheduleNote = r.kind === "manual" ? r.detail : undefined;
|
|
5835
6307
|
}
|
|
@@ -5845,19 +6317,19 @@ var on = (o2) => {
|
|
|
5845
6317
|
next.untilReset = new Date(untilResetMs).toISOString();
|
|
5846
6318
|
else
|
|
5847
6319
|
next.untilReset = undefined;
|
|
5848
|
-
next.reason =
|
|
6320
|
+
next.reason = o.reason?.trim() || undefined;
|
|
5849
6321
|
next.schedule = schedule;
|
|
5850
|
-
writeConfig(
|
|
6322
|
+
writeConfig(o.home, next);
|
|
5851
6323
|
log(`Sleds on. Claude Code now runs on the platform key through ${url}. Restart claude to apply.`);
|
|
5852
6324
|
if (untilResetMs !== undefined) {
|
|
5853
|
-
log(`Reverts at ${formatLocal(untilResetMs,
|
|
6325
|
+
log(`Reverts at ${formatLocal(untilResetMs, o.tz)} — run \`sleds off\` earlier if you want.`);
|
|
5854
6326
|
if (schedule === undefined || schedule === "manual") {
|
|
5855
6327
|
const why = scheduleNote ? ` (${scheduleNote})` : "";
|
|
5856
6328
|
log(`Auto-revert not scheduled${why}. Run \`sleds off\` after the reset.`);
|
|
5857
6329
|
}
|
|
5858
6330
|
}
|
|
5859
6331
|
if (backedUp)
|
|
5860
|
-
log(`Backed up ${settingsPath(
|
|
6332
|
+
log(`Backed up ${settingsPath(o.home)} to ${backupPath(o.home)}.`);
|
|
5861
6333
|
return 0;
|
|
5862
6334
|
};
|
|
5863
6335
|
|
|
@@ -5905,22 +6377,22 @@ var exitsLine = (hasUpstreamKey) => hasUpstreamKey ? "`/sled handoff <what you w
|
|
|
5905
6377
|
var approachingContext = (bar, usedPct, resetsAt, hasUpstreamKey, now) => `[SLEDS] This seat is at ${usedPct !== undefined ? `${Math.round(usedPct)}%` : "90%+"} of its ${LABEL[bar]} limit; it resets in ${fmtIn(resetsAt - now)}. Tell the user in one short line before you answer, and offer the exits: ${exitsLine(hasUpstreamKey)} Do not mention this again unless asked.`;
|
|
5906
6378
|
var hitContext = (bar, resetsAt, hasUpstreamKey, now) => `[SLEDS] This seat hit its ${LABEL[bar]} limit; it resets in ${fmtIn(resetsAt - now)}. If this reply fails or the user is stuck, the exits are: ${exitsLine(hasUpstreamKey)} Say so in one line.`;
|
|
5907
6379
|
var creditsBlockText = (hasUpstreamKey) => `SLEDS: this seat is out of usage credits — the next reply would fail, so this prompt was not sent. ${hasUpstreamKey ? "Exits: /sled handoff <task> (a buddy picks it up), sleds on (continue on the workspace's platform key), or /model to a cheaper model." : "Exits: /sled handoff <task> (a buddy picks it up), sleds defer (resume when credits are topped up), or /model to a cheaper model."} Re-send your prompt after choosing; SLEDS will not block again.`;
|
|
5908
|
-
var promptEvent = async (
|
|
5909
|
-
const log =
|
|
5910
|
-
const error =
|
|
5911
|
-
const now = (
|
|
5912
|
-
if (
|
|
6380
|
+
var promptEvent = async (o) => {
|
|
6381
|
+
const log = o.log ?? console.log;
|
|
6382
|
+
const error = o.error ?? console.error;
|
|
6383
|
+
const now = (o.now ?? Date.now)();
|
|
6384
|
+
if (o.stdin === undefined)
|
|
5913
6385
|
await readStdin(HOOK_STDIN_TIMEOUT_MS);
|
|
5914
6386
|
let state;
|
|
5915
6387
|
try {
|
|
5916
|
-
state = readSeatState(
|
|
6388
|
+
state = readSeatState(o.home);
|
|
5917
6389
|
} catch {
|
|
5918
6390
|
return 0;
|
|
5919
6391
|
}
|
|
5920
6392
|
const pending = untold(state);
|
|
5921
6393
|
if (pending.length === 0)
|
|
5922
6394
|
return 0;
|
|
5923
|
-
const hasKey = readConfig(
|
|
6395
|
+
const hasKey = readConfig(o.home).org?.capabilities.upstreamKey !== false;
|
|
5924
6396
|
const told = { ...state.told ?? {} };
|
|
5925
6397
|
const mark = (bar, stage, at) => {
|
|
5926
6398
|
told[bar] = { ...told[bar] ?? {}, [stage]: at };
|
|
@@ -5940,7 +6412,7 @@ var promptEvent = async (o2) => {
|
|
|
5940
6412
|
lines.push(e.stage === "approaching" ? approachingContext(e.bar, pct, e.resetsAt, hasKey, now) : hitContext(e.bar, e.resetsAt, hasKey, now));
|
|
5941
6413
|
}
|
|
5942
6414
|
try {
|
|
5943
|
-
writeSeatState(
|
|
6415
|
+
writeSeatState(o.home, { ...state, told });
|
|
5944
6416
|
} catch {}
|
|
5945
6417
|
if (code === 0 && lines.length > 0)
|
|
5946
6418
|
log(lines.join(`
|
|
@@ -5951,17 +6423,17 @@ var promptEvent = async (o2) => {
|
|
|
5951
6423
|
// src/commands/run.ts
|
|
5952
6424
|
init_config();
|
|
5953
6425
|
init_meter();
|
|
5954
|
-
import { hostname as
|
|
5955
|
-
var run = async (
|
|
5956
|
-
const log =
|
|
5957
|
-
const error =
|
|
5958
|
-
if (!
|
|
6426
|
+
import { hostname as hostname4 } from "node:os";
|
|
6427
|
+
var run = async (o) => {
|
|
6428
|
+
const log = o.log ?? console.log;
|
|
6429
|
+
const error = o.error ?? console.error;
|
|
6430
|
+
if (!o.jobId) {
|
|
5959
6431
|
error("usage: sleds run <jobId>");
|
|
5960
6432
|
return 2;
|
|
5961
6433
|
}
|
|
5962
|
-
const client =
|
|
5963
|
-
const src = meterSource(
|
|
5964
|
-
return src ? musherClient({ ...src, fetchFn:
|
|
6434
|
+
const client = o.client ?? (() => {
|
|
6435
|
+
const src = meterSource(o.home);
|
|
6436
|
+
return src ? musherClient({ ...src, fetchFn: o.fetchFn }) : null;
|
|
5965
6437
|
})();
|
|
5966
6438
|
if (!client) {
|
|
5967
6439
|
error("No seat on this machine. Run `sleds init` first.");
|
|
@@ -5972,13 +6444,13 @@ var run = async (o2) => {
|
|
|
5972
6444
|
error("Could not reach the SLEDS proxy.");
|
|
5973
6445
|
return 1;
|
|
5974
6446
|
}
|
|
5975
|
-
const job = [...list.due, ...list.upcoming].find((j) => j.jobId ===
|
|
6447
|
+
const job = [...list.due, ...list.upcoming].find((j) => j.jobId === o.jobId);
|
|
5976
6448
|
if (!job) {
|
|
5977
|
-
error(`Job ${
|
|
6449
|
+
error(`Job ${o.jobId} is not a scheduled local job on this seat (sleds musher --status lists them).`);
|
|
5978
6450
|
return 1;
|
|
5979
6451
|
}
|
|
5980
|
-
const cfg = readConfig(
|
|
5981
|
-
const claude = findClaude(
|
|
6452
|
+
const cfg = readConfig(o.home);
|
|
6453
|
+
const claude = findClaude(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich);
|
|
5982
6454
|
if (!claude) {
|
|
5983
6455
|
error("`claude` not found. Install Claude Code, or `sleds musher --claude /path/to/claude`.");
|
|
5984
6456
|
return 1;
|
|
@@ -5986,12 +6458,12 @@ var run = async (o2) => {
|
|
|
5986
6458
|
const out = await runJob({
|
|
5987
6459
|
job,
|
|
5988
6460
|
client,
|
|
5989
|
-
fallbackSession: readSeatState(
|
|
5990
|
-
host:
|
|
6461
|
+
fallbackSession: readSeatState(o.home).session,
|
|
6462
|
+
host: o.host ?? hostname4(),
|
|
5991
6463
|
claudePath: claude,
|
|
5992
6464
|
permissionMode: cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE,
|
|
5993
6465
|
timeoutMs: (cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES) * 60000,
|
|
5994
|
-
exec:
|
|
6466
|
+
exec: o.execCapture,
|
|
5995
6467
|
log
|
|
5996
6468
|
});
|
|
5997
6469
|
if (out.kind === "skipped") {
|
|
@@ -6024,25 +6496,25 @@ var notificationType = (text) => {
|
|
|
6024
6496
|
return;
|
|
6025
6497
|
}
|
|
6026
6498
|
};
|
|
6027
|
-
var seatEvent = async (
|
|
6028
|
-
const log =
|
|
6029
|
-
const now = (
|
|
6030
|
-
const input =
|
|
6499
|
+
var seatEvent = async (o) => {
|
|
6500
|
+
const log = o.log ?? console.log;
|
|
6501
|
+
const now = (o.now ?? Date.now)();
|
|
6502
|
+
const input = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS2);
|
|
6031
6503
|
const kind = notificationType(input);
|
|
6032
6504
|
try {
|
|
6033
|
-
const state = readSeatState(
|
|
6505
|
+
const state = readSeatState(o.home);
|
|
6034
6506
|
const week = state.window?.sevenDay;
|
|
6035
|
-
const src = meterSource(
|
|
6507
|
+
const src = meterSource(o.home);
|
|
6036
6508
|
if (src) {
|
|
6037
|
-
await postSeatWall({ ...src, fetchFn:
|
|
6509
|
+
await postSeatWall({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, {
|
|
6038
6510
|
...week ? { resetsAt: week.resetsAt, usedPct: week.usedPct } : {},
|
|
6039
6511
|
...kind ? { notification: kind } : {}
|
|
6040
6512
|
});
|
|
6041
6513
|
}
|
|
6042
6514
|
if (week && week.resetsAt > now)
|
|
6043
|
-
writeSeatState(
|
|
6515
|
+
writeSeatState(o.home, { ...state, lastWallResetsAt: week.resetsAt });
|
|
6044
6516
|
} catch {}
|
|
6045
|
-
const line = seatWallLine(readConfig(
|
|
6517
|
+
const line = seatWallLine(readConfig(o.home).org?.capabilities.upstreamKey !== false);
|
|
6046
6518
|
log(line);
|
|
6047
6519
|
return 0;
|
|
6048
6520
|
};
|
|
@@ -6050,7 +6522,7 @@ var seatEvent = async (o2) => {
|
|
|
6050
6522
|
// src/commands/seatSync.ts
|
|
6051
6523
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
6052
6524
|
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, writeFileSync as writeFileSync11 } from "node:fs";
|
|
6053
|
-
import { platform } from "node:os";
|
|
6525
|
+
import { platform as platform2 } from "node:os";
|
|
6054
6526
|
import { join as join11 } from "node:path";
|
|
6055
6527
|
|
|
6056
6528
|
// src/cookieDb.ts
|
|
@@ -6062,11 +6534,11 @@ var isBun = () => typeof globalThis.Bun !== "undefined";
|
|
|
6062
6534
|
async function queryRows(dbPath) {
|
|
6063
6535
|
if (isBun()) {
|
|
6064
6536
|
const { Database } = await import("bun:sqlite");
|
|
6065
|
-
const
|
|
6537
|
+
const conn = new Database(dbPath, { readonly: true });
|
|
6066
6538
|
try {
|
|
6067
|
-
return
|
|
6539
|
+
return conn.query(QUERY).all();
|
|
6068
6540
|
} finally {
|
|
6069
|
-
|
|
6541
|
+
conn.close();
|
|
6070
6542
|
}
|
|
6071
6543
|
}
|
|
6072
6544
|
const mod = nodeSqlite();
|
|
@@ -6177,9 +6649,9 @@ function resolveProfileDir(localStateJson, profile) {
|
|
|
6177
6649
|
if (localStateJson) {
|
|
6178
6650
|
try {
|
|
6179
6651
|
const raw = JSON.parse(localStateJson);
|
|
6180
|
-
const
|
|
6181
|
-
if (
|
|
6182
|
-
cache =
|
|
6652
|
+
const p = isObject11(raw) && isObject11(raw.profile) ? raw.profile : null;
|
|
6653
|
+
if (p && isObject11(p.info_cache))
|
|
6654
|
+
cache = p.info_cache;
|
|
6183
6655
|
} catch {}
|
|
6184
6656
|
}
|
|
6185
6657
|
if (Object.hasOwn(cache, profile))
|
|
@@ -6370,11 +6842,11 @@ function findFile(root, name) {
|
|
|
6370
6842
|
continue;
|
|
6371
6843
|
}
|
|
6372
6844
|
for (const e of entries) {
|
|
6373
|
-
const
|
|
6845
|
+
const p = join11(dir, e.name);
|
|
6374
6846
|
if (e.isDirectory())
|
|
6375
|
-
stack.push(
|
|
6847
|
+
stack.push(p);
|
|
6376
6848
|
else if (e.name === name)
|
|
6377
|
-
return
|
|
6849
|
+
return p;
|
|
6378
6850
|
}
|
|
6379
6851
|
}
|
|
6380
6852
|
return null;
|
|
@@ -6385,10 +6857,10 @@ function findCookieDb(home, source) {
|
|
|
6385
6857
|
if (!existsSync12(root))
|
|
6386
6858
|
throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
|
|
6387
6859
|
if (!b.profiled) {
|
|
6388
|
-
const
|
|
6389
|
-
if (!
|
|
6860
|
+
const db = findFile(root, "Cookies");
|
|
6861
|
+
if (!db)
|
|
6390
6862
|
throw new Error(`no cookie store under ${root}`);
|
|
6391
|
-
return
|
|
6863
|
+
return db;
|
|
6392
6864
|
}
|
|
6393
6865
|
const localStatePath = join11(root, "Local State");
|
|
6394
6866
|
const localState = existsSync12(localStatePath) ? readFileSync10(localStatePath, "utf8") : null;
|
|
@@ -6398,7 +6870,7 @@ function findCookieDb(home, source) {
|
|
|
6398
6870
|
throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
|
|
6399
6871
|
if (!dir)
|
|
6400
6872
|
throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
|
|
6401
|
-
const db = cookieDbCandidates(join11(root, dir)).find((
|
|
6873
|
+
const db = cookieDbCandidates(join11(root, dir)).find((p) => existsSync12(p));
|
|
6402
6874
|
if (!db)
|
|
6403
6875
|
throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
|
|
6404
6876
|
return db;
|
|
@@ -6429,8 +6901,8 @@ var curlUsage = (org, cookie) => {
|
|
|
6429
6901
|
return { status: 0, body: "" };
|
|
6430
6902
|
}
|
|
6431
6903
|
};
|
|
6432
|
-
function fetchSeatUsage(jar, org, now,
|
|
6433
|
-
const { status, body } =
|
|
6904
|
+
function fetchSeatUsage(jar, org, now, run) {
|
|
6905
|
+
const { status, body } = run(org, cookieHeader(jar, org));
|
|
6434
6906
|
if (status !== 200)
|
|
6435
6907
|
return { usage: null, status };
|
|
6436
6908
|
try {
|
|
@@ -6491,24 +6963,24 @@ function uninstall(home, source, log) {
|
|
|
6491
6963
|
log(existsSync12(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
|
|
6492
6964
|
return 0;
|
|
6493
6965
|
}
|
|
6494
|
-
var seatSync = async (
|
|
6495
|
-
const log =
|
|
6496
|
-
const now = (
|
|
6497
|
-
const parsed = parseSeatSyncArgs(
|
|
6966
|
+
var seatSync = async (o) => {
|
|
6967
|
+
const log = o.log ?? console.log;
|
|
6968
|
+
const now = (o.now ?? Date.now)();
|
|
6969
|
+
const parsed = parseSeatSyncArgs(o.argv ?? process.argv.slice(3));
|
|
6498
6970
|
if ("error" in parsed) {
|
|
6499
6971
|
log(`seat-sync: ${parsed.error}`);
|
|
6500
6972
|
return 1;
|
|
6501
6973
|
}
|
|
6502
6974
|
const { source } = parsed;
|
|
6503
6975
|
if (source.uninstall)
|
|
6504
|
-
return uninstall(
|
|
6976
|
+
return uninstall(o.home, source, log);
|
|
6505
6977
|
if (source.install)
|
|
6506
|
-
return install(
|
|
6507
|
-
if (
|
|
6978
|
+
return install(o.home, [process.execPath, process.argv[1] ?? ""], source, log);
|
|
6979
|
+
if (platform2() !== "darwin" && !o.readCookieRows) {
|
|
6508
6980
|
log("seat-sync is macOS-only for now.");
|
|
6509
6981
|
return 2;
|
|
6510
6982
|
}
|
|
6511
|
-
const saved = meterSource(
|
|
6983
|
+
const saved = meterSource(o.home);
|
|
6512
6984
|
const proxyUrl = saved?.proxyUrl;
|
|
6513
6985
|
const token = source.token ?? saved?.token;
|
|
6514
6986
|
if (!proxyUrl || !token) {
|
|
@@ -6517,13 +6989,13 @@ var seatSync = async (o2) => {
|
|
|
6517
6989
|
}
|
|
6518
6990
|
let rows;
|
|
6519
6991
|
try {
|
|
6520
|
-
rows =
|
|
6992
|
+
rows = o.readCookieRows ? o.readCookieRows(o.home, source) : await readCookieRowsBun(o.home, source);
|
|
6521
6993
|
} catch (e) {
|
|
6522
6994
|
log(`seat-sync: ${e instanceof Error ? e.message : String(e)}`);
|
|
6523
6995
|
return 1;
|
|
6524
6996
|
}
|
|
6525
6997
|
const service = BROWSERS[source.browser].keychainService;
|
|
6526
|
-
const pw = (
|
|
6998
|
+
const pw = (o.safeStorageKey ?? keychainKey)(service);
|
|
6527
6999
|
if (!pw) {
|
|
6528
7000
|
log(`seat-sync: could not read the "${service}" key from your Keychain.`);
|
|
6529
7001
|
return 1;
|
|
@@ -6543,16 +7015,16 @@ var seatSync = async (o2) => {
|
|
|
6543
7015
|
log("seat-sync: no active org in the session cookie (pass --org <uuid>).");
|
|
6544
7016
|
return 1;
|
|
6545
7017
|
}
|
|
6546
|
-
const { usage, status } = fetchSeatUsage(jar, org, now,
|
|
7018
|
+
const { usage, status } = fetchSeatUsage(jar, org, now, o.runUsage ?? curlUsage);
|
|
6547
7019
|
if (!usage) {
|
|
6548
7020
|
const why = status === 403 ? "claude.ai refused the read (403) — session expired, or this org is not readable by this session." : status === 0 ? "could not reach claude.ai." : `unexpected HTTP ${status}.`;
|
|
6549
7021
|
log(`seat-sync: usage read failed — ${why}`);
|
|
6550
7022
|
return 2;
|
|
6551
7023
|
}
|
|
6552
7024
|
try {
|
|
6553
|
-
writeSeatState(
|
|
7025
|
+
writeSeatState(o.home, { ...readSeatState(o.home), window: usageAsWindow(usage) });
|
|
6554
7026
|
} catch {}
|
|
6555
|
-
const posted = await postSeat(proxyUrl, token, seatSyncBody(usage),
|
|
7027
|
+
const posted = await postSeat(proxyUrl, token, seatSyncBody(usage), o.fetchFn ?? fetch);
|
|
6556
7028
|
const wk = usage.sevenDay ? `wk ${Math.round(usage.sevenDay.usedPct)}%` : "wk —";
|
|
6557
7029
|
const models = Object.keys(usage.models).length;
|
|
6558
7030
|
const credits = usage.usedCreditsUsd !== undefined ? ` · $${usage.usedCreditsUsd.toFixed(2)}` : "";
|
|
@@ -6604,14 +7076,14 @@ function sessionContext(a) {
|
|
|
6604
7076
|
return lines.join(`
|
|
6605
7077
|
`);
|
|
6606
7078
|
}
|
|
6607
|
-
var sessionEvent = async (
|
|
6608
|
-
const log =
|
|
6609
|
-
const now = (
|
|
6610
|
-
const raw =
|
|
7079
|
+
var sessionEvent = async (o) => {
|
|
7080
|
+
const log = o.log ?? console.log;
|
|
7081
|
+
const now = (o.now ?? Date.now)();
|
|
7082
|
+
const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
|
|
6611
7083
|
const payload = parseSessionPayload(raw);
|
|
6612
7084
|
if (payload.session_id) {
|
|
6613
7085
|
try {
|
|
6614
|
-
writeSession(
|
|
7086
|
+
writeSession(o.home, {
|
|
6615
7087
|
sessionId: payload.session_id,
|
|
6616
7088
|
startedAt: now,
|
|
6617
7089
|
...payload.source ? { source: payload.source } : {}
|
|
@@ -6621,13 +7093,13 @@ var sessionEvent = async (o2) => {
|
|
|
6621
7093
|
let email;
|
|
6622
7094
|
let org;
|
|
6623
7095
|
try {
|
|
6624
|
-
const cfg = readConfig(
|
|
7096
|
+
const cfg = readConfig(o.home);
|
|
6625
7097
|
email = cfg.email;
|
|
6626
7098
|
org = cfg.org?.slug;
|
|
6627
7099
|
} catch {}
|
|
6628
7100
|
let owed = [];
|
|
6629
7101
|
try {
|
|
6630
|
-
owed = readOwed(
|
|
7102
|
+
owed = readOwed(o.home, now);
|
|
6631
7103
|
} catch {
|
|
6632
7104
|
owed = [];
|
|
6633
7105
|
}
|
|
@@ -6672,13 +7144,13 @@ function parseHookInput(raw) {
|
|
|
6672
7144
|
}
|
|
6673
7145
|
if (typeof parsed !== "object" || parsed === null)
|
|
6674
7146
|
return null;
|
|
6675
|
-
const
|
|
6676
|
-
const toolName = typeof
|
|
6677
|
-
const input = typeof
|
|
7147
|
+
const p = parsed;
|
|
7148
|
+
const toolName = typeof p.tool_name === "string" ? p.tool_name : "";
|
|
7149
|
+
const input = typeof p.tool_input === "object" && p.tool_input !== null ? p.tool_input : {};
|
|
6678
7150
|
const filePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
6679
7151
|
if (!filePath)
|
|
6680
7152
|
return null;
|
|
6681
|
-
const cwd = typeof
|
|
7153
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : "";
|
|
6682
7154
|
return { toolName, filePath, cwd };
|
|
6683
7155
|
}
|
|
6684
7156
|
function projectKey(filePath, cwd) {
|
|
@@ -6698,16 +7170,16 @@ function readFileOrNull(path) {
|
|
|
6698
7170
|
return null;
|
|
6699
7171
|
}
|
|
6700
7172
|
}
|
|
6701
|
-
var specEvent = async (
|
|
6702
|
-
const log =
|
|
6703
|
-
const raw =
|
|
7173
|
+
var specEvent = async (o) => {
|
|
7174
|
+
const log = o.log ?? console.log;
|
|
7175
|
+
const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS3);
|
|
6704
7176
|
const input = raw ? parseHookInput(raw) : null;
|
|
6705
7177
|
if (!input || !isSpecPath(input.filePath))
|
|
6706
7178
|
return 0;
|
|
6707
|
-
const src = meterSource(
|
|
7179
|
+
const src = meterSource(o.home);
|
|
6708
7180
|
if (!src)
|
|
6709
7181
|
return 0;
|
|
6710
|
-
const body = (
|
|
7182
|
+
const body = (o.readFile ?? readFileOrNull)(input.filePath);
|
|
6711
7183
|
if (!body || !looksLikeSpec(body))
|
|
6712
7184
|
return 0;
|
|
6713
7185
|
const project = projectKey(input.filePath, input.cwd);
|
|
@@ -6717,7 +7189,7 @@ var specEvent = async (o2) => {
|
|
|
6717
7189
|
repoHint: input.cwd ? basename2(input.cwd) : undefined
|
|
6718
7190
|
};
|
|
6719
7191
|
try {
|
|
6720
|
-
const res = await (
|
|
7192
|
+
const res = await (o.fetchFn ?? fetch)(`${src.proxyUrl}/proxy/estimate`, {
|
|
6721
7193
|
method: "POST",
|
|
6722
7194
|
headers: { authorization: `Bearer ${src.token}`, "content-type": "application/json" },
|
|
6723
7195
|
body: JSON.stringify({ spec, project }),
|
|
@@ -6732,15 +7204,15 @@ var specEvent = async (o2) => {
|
|
|
6732
7204
|
log(`sleds: ${project} sized at $${b.lowUsd.toFixed(2)}–${b.highUsd.toFixed(2)} · ${min(b.lowMinutes)}–${min(b.highMinutes)} min`);
|
|
6733
7205
|
if (typeof b.lowMinutes === "number" && typeof b.highMinutes === "number") {
|
|
6734
7206
|
try {
|
|
6735
|
-
writeSeatState(
|
|
6736
|
-
...readSeatState(
|
|
7207
|
+
writeSeatState(o.home, {
|
|
7208
|
+
...readSeatState(o.home),
|
|
6737
7209
|
estimate: {
|
|
6738
7210
|
project,
|
|
6739
7211
|
lowUsd: b.lowUsd,
|
|
6740
7212
|
highUsd: b.highUsd,
|
|
6741
7213
|
lowMin: b.lowMinutes,
|
|
6742
7214
|
highMin: b.highMinutes,
|
|
6743
|
-
at: (
|
|
7215
|
+
at: (o.now ?? Date.now)()
|
|
6744
7216
|
}
|
|
6745
7217
|
});
|
|
6746
7218
|
} catch {}
|
|
@@ -6823,16 +7295,16 @@ var formatStatus = (s) => {
|
|
|
6823
7295
|
}
|
|
6824
7296
|
return lines;
|
|
6825
7297
|
};
|
|
6826
|
-
var status = async (
|
|
6827
|
-
const log =
|
|
6828
|
-
const nowMs = (
|
|
6829
|
-
const cfg = readConfig(
|
|
6830
|
-
const settings = existsSync13(settingsPath(
|
|
7298
|
+
var status = async (o) => {
|
|
7299
|
+
const log = o.log ?? console.log;
|
|
7300
|
+
const nowMs = (o.now ?? Date.now)();
|
|
7301
|
+
const cfg = readConfig(o.home);
|
|
7302
|
+
const settings = existsSync13(settingsPath(o.home)) ? readSettings(o.home) : {};
|
|
6831
7303
|
const inSettings = readProxy(settings);
|
|
6832
7304
|
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
6833
7305
|
const token = inSettings?.token ?? cfg.token;
|
|
6834
7306
|
if (!proxyUrl && !token && cfg.mode === undefined) {
|
|
6835
|
-
log(`SLEDS is not set up (${configPath(
|
|
7307
|
+
log(`SLEDS is not set up (${configPath(o.home)} and ${settingsPath(o.home)}).`);
|
|
6836
7308
|
log("Run `sleds init` first.");
|
|
6837
7309
|
return 1;
|
|
6838
7310
|
}
|
|
@@ -6840,8 +7312,8 @@ var status = async (o2) => {
|
|
|
6840
7312
|
mode: cfg.mode ?? "off",
|
|
6841
7313
|
pointed: inSettings !== null,
|
|
6842
7314
|
nowMs,
|
|
6843
|
-
settingsFile: settingsPath(
|
|
6844
|
-
configFile: configPath(
|
|
7315
|
+
settingsFile: settingsPath(o.home),
|
|
7316
|
+
configFile: configPath(o.home)
|
|
6845
7317
|
};
|
|
6846
7318
|
if (proxyUrl)
|
|
6847
7319
|
info.proxyUrl = proxyUrl;
|
|
@@ -6861,8 +7333,8 @@ var status = async (o2) => {
|
|
|
6861
7333
|
};
|
|
6862
7334
|
if (cfg.reason)
|
|
6863
7335
|
info.reason = cfg.reason;
|
|
6864
|
-
if (
|
|
6865
|
-
info.tz =
|
|
7336
|
+
if (o.tz)
|
|
7337
|
+
info.tz = o.tz;
|
|
6866
7338
|
const since = cfg.since ? Date.parse(cfg.since) : Number.NaN;
|
|
6867
7339
|
if (!Number.isNaN(since))
|
|
6868
7340
|
info.sinceMs = since;
|
|
@@ -6871,8 +7343,8 @@ var status = async (o2) => {
|
|
|
6871
7343
|
info.untilResetMs = until;
|
|
6872
7344
|
if (proxyUrl) {
|
|
6873
7345
|
const [health, proxy] = await Promise.all([
|
|
6874
|
-
checkHealth2(proxyUrl,
|
|
6875
|
-
fetchProxyStatus(proxyUrl,
|
|
7346
|
+
checkHealth2(proxyUrl, o.fetchFn, o.timeoutMs),
|
|
7347
|
+
fetchProxyStatus(proxyUrl, o.fetchFn, o.timeoutMs)
|
|
6876
7348
|
]);
|
|
6877
7349
|
info.health = health;
|
|
6878
7350
|
info.proxy = proxy;
|
|
@@ -6885,62 +7357,62 @@ var status = async (o2) => {
|
|
|
6885
7357
|
// src/commands/statusline.ts
|
|
6886
7358
|
init_config();
|
|
6887
7359
|
init_meter();
|
|
6888
|
-
var statusline = async (
|
|
6889
|
-
const log =
|
|
6890
|
-
const now = (
|
|
6891
|
-
const input =
|
|
7360
|
+
var statusline = async (o) => {
|
|
7361
|
+
const log = o.log ?? console.log;
|
|
7362
|
+
const now = (o.now ?? Date.now)();
|
|
7363
|
+
const input = o.stdin ?? await readStdin(STDIN_TIMEOUT_MS);
|
|
6892
7364
|
const window = seatWindowFromStdin(input, now);
|
|
6893
7365
|
const session = seatSessionFromStdin(input, now);
|
|
6894
7366
|
let posts = [];
|
|
6895
7367
|
let events = [];
|
|
6896
7368
|
try {
|
|
6897
7369
|
if (window)
|
|
6898
|
-
({ posts, events } = rememberSeat(
|
|
7370
|
+
({ posts, events } = rememberSeat(o, window, session, now, input));
|
|
6899
7371
|
else {
|
|
6900
|
-
const st = readSeatState(
|
|
6901
|
-
const credits = creditsEvent(
|
|
7372
|
+
const st = readSeatState(o.home);
|
|
7373
|
+
const credits = creditsEvent(o, st, input, now);
|
|
6902
7374
|
if (session || credits.scan) {
|
|
6903
7375
|
const next = {
|
|
6904
7376
|
...st,
|
|
6905
7377
|
...session ? { session } : {},
|
|
6906
7378
|
...credits.scan ? { transcriptScan: credits.scan } : {}
|
|
6907
7379
|
};
|
|
6908
|
-
writeSeatState(
|
|
7380
|
+
writeSeatState(o.home, credits.event ? markWarned(next, [credits.event]) : next);
|
|
6909
7381
|
}
|
|
6910
7382
|
if (credits.event) {
|
|
6911
7383
|
events = [credits.event];
|
|
6912
|
-
posts = announce(
|
|
7384
|
+
posts = announce(o, events);
|
|
6913
7385
|
}
|
|
6914
7386
|
}
|
|
6915
7387
|
} catch {}
|
|
6916
7388
|
const seatFragment = formatSeatFragment(window, now);
|
|
6917
7389
|
let line;
|
|
6918
7390
|
try {
|
|
6919
|
-
const
|
|
6920
|
-
home:
|
|
7391
|
+
const meter = await loadMeter({
|
|
7392
|
+
home: o.home,
|
|
6921
7393
|
now,
|
|
6922
|
-
fetchFn:
|
|
6923
|
-
timeoutMs:
|
|
7394
|
+
fetchFn: o.fetchFn,
|
|
7395
|
+
timeoutMs: o.timeoutMs
|
|
6924
7396
|
});
|
|
6925
|
-
line = formatMeterLine(
|
|
7397
|
+
line = formatMeterLine(meter, now, seatFragment);
|
|
6926
7398
|
} catch {
|
|
6927
7399
|
line = formatMeterLine(null, now, seatFragment);
|
|
6928
7400
|
}
|
|
6929
7401
|
log(line);
|
|
6930
|
-
posts.push(speakToCockpit(
|
|
7402
|
+
posts.push(speakToCockpit(o, window, events, now));
|
|
6931
7403
|
await Promise.allSettled(posts);
|
|
6932
7404
|
return 0;
|
|
6933
7405
|
};
|
|
6934
|
-
var speakToCockpit = async (
|
|
6935
|
-
const cockpit = detectCockpit(
|
|
7406
|
+
var speakToCockpit = async (o, window, events, now) => {
|
|
7407
|
+
const cockpit = detectCockpit(o.env ?? process.env);
|
|
6936
7408
|
if (!cockpit)
|
|
6937
7409
|
return;
|
|
6938
7410
|
try {
|
|
6939
|
-
const st = readSeatState(
|
|
7411
|
+
const st = readSeatState(o.home);
|
|
6940
7412
|
let feed;
|
|
6941
|
-
const src = meterSource(
|
|
7413
|
+
const src = meterSource(o.home);
|
|
6942
7414
|
if (src && now - (st.cockpit?.feed?.at ?? 0) >= FEED_MIN_INTERVAL_MS) {
|
|
6943
|
-
const got = await fetchFeed({ ...src, email: readConfig(
|
|
7415
|
+
const got = await fetchFeed({ ...src, email: readConfig(o.home).email, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs }, now);
|
|
6944
7416
|
if (got)
|
|
6945
7417
|
feed = got;
|
|
6946
7418
|
}
|
|
@@ -6951,32 +7423,32 @@ var speakToCockpit = async (o2, window, events, now) => {
|
|
|
6951
7423
|
const changed = r.argv.length > 0 || feed !== undefined;
|
|
6952
7424
|
if (!changed)
|
|
6953
7425
|
return;
|
|
6954
|
-
writeSeatState(
|
|
7426
|
+
writeSeatState(o.home, { ...readSeatState(o.home), cockpit: r.state });
|
|
6955
7427
|
if (r.argv.length)
|
|
6956
|
-
await runCockpit(cockpit, r.argv, { spawn:
|
|
7428
|
+
await runCockpit(cockpit, r.argv, { spawn: o.spawn });
|
|
6957
7429
|
} catch {}
|
|
6958
7430
|
};
|
|
6959
|
-
var rememberSeat = (
|
|
6960
|
-
const prior = readSeatState(
|
|
7431
|
+
var rememberSeat = (o, window, session, now, input) => {
|
|
7432
|
+
const prior = readSeatState(o.home);
|
|
6961
7433
|
let state = { ...prior, window, ...session ? { session } : {} };
|
|
6962
|
-
const src = meterSource(
|
|
7434
|
+
const src = meterSource(o.home);
|
|
6963
7435
|
const posts = [];
|
|
6964
7436
|
if (src && shouldPostSeat(prior, window, now)) {
|
|
6965
7437
|
state = markPosted(state, window, now);
|
|
6966
|
-
posts.push(postSeatWindow({ ...src, fetchFn:
|
|
7438
|
+
posts.push(postSeatWindow({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, window));
|
|
6967
7439
|
}
|
|
6968
7440
|
const events = seatWallEvents(prior, window);
|
|
6969
|
-
const credits = creditsEvent(
|
|
7441
|
+
const credits = creditsEvent(o, prior, input, now);
|
|
6970
7442
|
if (credits.scan)
|
|
6971
7443
|
state = { ...state, transcriptScan: credits.scan };
|
|
6972
7444
|
if (credits.event)
|
|
6973
7445
|
events.push(credits.event);
|
|
6974
7446
|
state = markWarned(state, events);
|
|
6975
|
-
posts.push(...announce(
|
|
6976
|
-
writeSeatState(
|
|
7447
|
+
posts.push(...announce(o, events));
|
|
7448
|
+
writeSeatState(o.home, state);
|
|
6977
7449
|
return { posts, events };
|
|
6978
7450
|
};
|
|
6979
|
-
var creditsEvent = (
|
|
7451
|
+
var creditsEvent = (o, prior, input, now) => {
|
|
6980
7452
|
const path = transcriptPathFromStdin(input);
|
|
6981
7453
|
if (!path)
|
|
6982
7454
|
return { event: null, scan: undefined };
|
|
@@ -6986,13 +7458,13 @@ var creditsEvent = (o2, prior, input, now) => {
|
|
|
6986
7458
|
return { event: null, scan: next };
|
|
6987
7459
|
return { event: { bar: "credits", stage: "hit", usedPct: 100, resetsAt: now }, scan: next };
|
|
6988
7460
|
};
|
|
6989
|
-
var announce = (
|
|
7461
|
+
var announce = (o, events) => {
|
|
6990
7462
|
if (events.length === 0)
|
|
6991
7463
|
return [];
|
|
6992
|
-
const src = meterSource(
|
|
7464
|
+
const src = meterSource(o.home);
|
|
6993
7465
|
if (!src)
|
|
6994
7466
|
return [];
|
|
6995
|
-
return events.map((e) => postSeatWall({ ...src, fetchFn:
|
|
7467
|
+
return events.map((e) => postSeatWall({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, {
|
|
6996
7468
|
bar: e.bar,
|
|
6997
7469
|
stage: e.stage,
|
|
6998
7470
|
usedPct: e.usedPct,
|
|
@@ -7026,14 +7498,14 @@ function decideStop(a) {
|
|
|
7026
7498
|
rows: markAsked(a.rows, due.map((r) => r.key), a.now)
|
|
7027
7499
|
};
|
|
7028
7500
|
}
|
|
7029
|
-
var stopEvent = async (
|
|
7030
|
-
const log =
|
|
7031
|
-
const now = (
|
|
7032
|
-
const raw =
|
|
7501
|
+
var stopEvent = async (o) => {
|
|
7502
|
+
const log = o.log ?? console.log;
|
|
7503
|
+
const now = (o.now ?? Date.now)();
|
|
7504
|
+
const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
|
|
7033
7505
|
const payload = parseStopPayload(raw);
|
|
7034
7506
|
let rows;
|
|
7035
7507
|
try {
|
|
7036
|
-
rows = readOwed(
|
|
7508
|
+
rows = readOwed(o.home, now);
|
|
7037
7509
|
} catch {
|
|
7038
7510
|
return 0;
|
|
7039
7511
|
}
|
|
@@ -7046,14 +7518,14 @@ var stopEvent = async (o2) => {
|
|
|
7046
7518
|
if (!d.block)
|
|
7047
7519
|
return 0;
|
|
7048
7520
|
try {
|
|
7049
|
-
writeOwed(
|
|
7521
|
+
writeOwed(o.home, d.rows);
|
|
7050
7522
|
} catch {}
|
|
7051
7523
|
log(JSON.stringify({ decision: "block", reason: d.reason }));
|
|
7052
7524
|
return 0;
|
|
7053
7525
|
};
|
|
7054
7526
|
|
|
7055
7527
|
// src/commands/uninstall.ts
|
|
7056
|
-
import { spawnSync as
|
|
7528
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
7057
7529
|
import { existsSync as existsSync15, rmSync as rmSync5 } from "node:fs";
|
|
7058
7530
|
|
|
7059
7531
|
// src/codex.ts
|
|
@@ -7063,7 +7535,7 @@ import { dirname as dirname6, join as join12 } from "node:path";
|
|
|
7063
7535
|
var PROVIDER_ID2 = "sleds";
|
|
7064
7536
|
var codexDir2 = (home) => join12(home, ".codex");
|
|
7065
7537
|
var codexConfigPath2 = (home) => join12(codexDir2(home), "config.toml");
|
|
7066
|
-
function removeCodexProvider2(toml,
|
|
7538
|
+
function removeCodexProvider2(toml, o = {}) {
|
|
7067
7539
|
const lines = toml.split(`
|
|
7068
7540
|
`);
|
|
7069
7541
|
const out = [];
|
|
@@ -7076,7 +7548,7 @@ function removeCodexProvider2(toml, o2 = {}) {
|
|
|
7076
7548
|
}
|
|
7077
7549
|
let text = out.join(`
|
|
7078
7550
|
`);
|
|
7079
|
-
if (!
|
|
7551
|
+
if (!o.keepPrior) {
|
|
7080
7552
|
const prior = text.match(/^# sleds-prior-model-provider = "([^"]*)"\s*\n/m);
|
|
7081
7553
|
if (prior) {
|
|
7082
7554
|
text = text.replace(prior[0], "");
|
|
@@ -7106,74 +7578,74 @@ init_config();
|
|
|
7106
7578
|
init_meter();
|
|
7107
7579
|
init_settings();
|
|
7108
7580
|
var defaultUnload = (plist) => {
|
|
7109
|
-
|
|
7581
|
+
spawnSync5("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
7110
7582
|
};
|
|
7111
|
-
var uninstall2 = (
|
|
7112
|
-
const codexLine = uninstallCodex(
|
|
7583
|
+
var uninstall2 = (o) => {
|
|
7584
|
+
const codexLine = uninstallCodex(o.home);
|
|
7113
7585
|
if (codexLine)
|
|
7114
|
-
(
|
|
7115
|
-
const log =
|
|
7586
|
+
(o.log ?? console.log)(` ${codexLine}`);
|
|
7587
|
+
const log = o.log ?? console.log;
|
|
7116
7588
|
const removed = [];
|
|
7117
|
-
if (existsSync15(settingsPath(
|
|
7118
|
-
let settings = readSettings(
|
|
7589
|
+
if (existsSync15(settingsPath(o.home))) {
|
|
7590
|
+
let settings = readSettings(o.home);
|
|
7119
7591
|
let changed = false;
|
|
7120
7592
|
if (hasEitherKey(settings)) {
|
|
7121
7593
|
settings = removeProxy(settings);
|
|
7122
7594
|
changed = true;
|
|
7123
|
-
removed.push(`env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN from ${settingsPath(
|
|
7595
|
+
removed.push(`env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN from ${settingsPath(o.home)}`);
|
|
7124
7596
|
}
|
|
7125
7597
|
if (isSledsStatusLine(settings[STATUS_LINE_KEY])) {
|
|
7126
|
-
const cfg = readConfig(
|
|
7598
|
+
const cfg = readConfig(o.home);
|
|
7127
7599
|
settings = removeStatusLine(settings, cfg.priorStatusLine);
|
|
7128
7600
|
changed = true;
|
|
7129
|
-
removed.push(cfg.priorStatusLine === undefined ? `statusLine from ${settingsPath(
|
|
7601
|
+
removed.push(cfg.priorStatusLine === undefined ? `statusLine from ${settingsPath(o.home)}` : `statusLine from ${settingsPath(o.home)} (previous one restored)`);
|
|
7130
7602
|
if (cfg.priorStatusLine !== undefined) {
|
|
7131
7603
|
const { priorStatusLine: _restored, ...rest } = cfg;
|
|
7132
|
-
writeConfig(
|
|
7604
|
+
writeConfig(o.home, rest);
|
|
7133
7605
|
}
|
|
7134
7606
|
}
|
|
7135
7607
|
if (hasSeatHook(settings)) {
|
|
7136
7608
|
settings = removeSeatHook(settings);
|
|
7137
7609
|
changed = true;
|
|
7138
|
-
removed.push(`hooks.Notification seat-event entry from ${settingsPath(
|
|
7610
|
+
removed.push(`hooks.Notification seat-event entry from ${settingsPath(o.home)}`);
|
|
7139
7611
|
}
|
|
7140
7612
|
if (hasPromptHook(settings)) {
|
|
7141
7613
|
settings = removePromptHook(settings);
|
|
7142
7614
|
changed = true;
|
|
7143
|
-
removed.push(`hooks.UserPromptSubmit prompt-event entry from ${settingsPath(
|
|
7615
|
+
removed.push(`hooks.UserPromptSubmit prompt-event entry from ${settingsPath(o.home)}`);
|
|
7144
7616
|
}
|
|
7145
7617
|
if (hasSpecHook(settings)) {
|
|
7146
7618
|
settings = removeSpecHook(settings);
|
|
7147
7619
|
changed = true;
|
|
7148
|
-
removed.push(`hooks.PostToolUse spec-event entry from ${settingsPath(
|
|
7620
|
+
removed.push(`hooks.PostToolUse spec-event entry from ${settingsPath(o.home)}`);
|
|
7149
7621
|
}
|
|
7150
7622
|
if (hasSessionHook(settings)) {
|
|
7151
7623
|
settings = removeSessionHook(settings);
|
|
7152
7624
|
changed = true;
|
|
7153
|
-
removed.push(`hooks.SessionStart session-event entry from ${settingsPath(
|
|
7625
|
+
removed.push(`hooks.SessionStart session-event entry from ${settingsPath(o.home)}`);
|
|
7154
7626
|
}
|
|
7155
7627
|
if (hasStopHook(settings)) {
|
|
7156
7628
|
settings = removeStopHook(settings);
|
|
7157
7629
|
changed = true;
|
|
7158
|
-
removed.push(`hooks.Stop stop-event entry from ${settingsPath(
|
|
7630
|
+
removed.push(`hooks.Stop stop-event entry from ${settingsPath(o.home)}`);
|
|
7159
7631
|
}
|
|
7160
7632
|
if (changed)
|
|
7161
|
-
writeSettings(
|
|
7633
|
+
writeSettings(o.home, settings);
|
|
7162
7634
|
}
|
|
7163
|
-
for (const file of [meterCachePath(
|
|
7635
|
+
for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
|
|
7164
7636
|
if (existsSync15(file)) {
|
|
7165
7637
|
rmSync5(file, { force: true });
|
|
7166
7638
|
removed.push(file);
|
|
7167
7639
|
}
|
|
7168
7640
|
}
|
|
7169
|
-
const skill = skillDir(
|
|
7641
|
+
const skill = skillDir(o.home);
|
|
7170
7642
|
if (existsSync15(skill)) {
|
|
7171
7643
|
rmSync5(skill, { recursive: true, force: true });
|
|
7172
7644
|
removed.push(skill);
|
|
7173
7645
|
}
|
|
7174
|
-
const plist = musherPlistPath(
|
|
7646
|
+
const plist = musherPlistPath(o.home);
|
|
7175
7647
|
if (existsSync15(plist)) {
|
|
7176
|
-
(
|
|
7648
|
+
(o.unloadAgent ?? defaultUnload)(plist);
|
|
7177
7649
|
rmSync5(plist, { force: true });
|
|
7178
7650
|
removed.push(`launchd agent ${musherLabel()}`);
|
|
7179
7651
|
}
|
|
@@ -7184,7 +7656,7 @@ var uninstall2 = (o2) => {
|
|
|
7184
7656
|
for (const r of removed)
|
|
7185
7657
|
log(` ${r}`);
|
|
7186
7658
|
}
|
|
7187
|
-
log(`Kept ${tokenPath(
|
|
7659
|
+
log(`Kept ${tokenPath(o.home)} (delete it yourself if you want the token gone).`);
|
|
7188
7660
|
return 0;
|
|
7189
7661
|
};
|
|
7190
7662
|
var hasEitherKey = (settings) => {
|
|
@@ -7203,17 +7675,17 @@ var jobIdFromTarget = (target) => {
|
|
|
7203
7675
|
return m[1];
|
|
7204
7676
|
return /^[A-Za-z0-9_-]{3,64}$/.test(t) ? t : null;
|
|
7205
7677
|
};
|
|
7206
|
-
var verify = async (
|
|
7207
|
-
const log =
|
|
7208
|
-
const error =
|
|
7209
|
-
const jobId =
|
|
7678
|
+
var verify = async (o) => {
|
|
7679
|
+
const log = o.log ?? console.log;
|
|
7680
|
+
const error = o.error ?? console.error;
|
|
7681
|
+
const jobId = o.target ? jobIdFromTarget(o.target) : null;
|
|
7210
7682
|
if (!jobId) {
|
|
7211
7683
|
error("usage: sleds verify <jobId> (or paste the SLEDS-Attest line)");
|
|
7212
7684
|
return 2;
|
|
7213
7685
|
}
|
|
7214
|
-
const client =
|
|
7215
|
-
const src = meterSource(
|
|
7216
|
-
return src ? musherClient({ ...src, fetchFn:
|
|
7686
|
+
const client = o.client ?? (() => {
|
|
7687
|
+
const src = meterSource(o.home);
|
|
7688
|
+
return src ? musherClient({ ...src, fetchFn: o.fetchFn }) : null;
|
|
7217
7689
|
})();
|
|
7218
7690
|
if (!client) {
|
|
7219
7691
|
error("No seat on this machine. Run `sleds init` first.");
|
|
@@ -7247,8 +7719,8 @@ init_settings();
|
|
|
7247
7719
|
import { chmodSync as chmodSync2, existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync13 } from "node:fs";
|
|
7248
7720
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
7249
7721
|
var configDir2 = (home, env = process.env) => {
|
|
7250
|
-
const
|
|
7251
|
-
return
|
|
7722
|
+
const o = env.SLEDS_HOME?.trim();
|
|
7723
|
+
return o ? resolve4(home, o) : join13(home, ".config", "sleds");
|
|
7252
7724
|
};
|
|
7253
7725
|
var configPath2 = (home) => join13(configDir2(home), "config.json");
|
|
7254
7726
|
var isObject12 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -7307,9 +7779,9 @@ var parseConfig2 = (raw) => {
|
|
|
7307
7779
|
const org = parseOrgInfo2(raw.org);
|
|
7308
7780
|
if (org)
|
|
7309
7781
|
cfg.org = org;
|
|
7310
|
-
const
|
|
7311
|
-
if (
|
|
7312
|
-
cfg.musher =
|
|
7782
|
+
const musher = parseMusher2(raw.musher);
|
|
7783
|
+
if (musher)
|
|
7784
|
+
cfg.musher = musher;
|
|
7313
7785
|
return cfg;
|
|
7314
7786
|
};
|
|
7315
7787
|
var parseMusher2 = (raw) => {
|
|
@@ -7332,16 +7804,16 @@ var parseOrgInfo2 = (raw) => {
|
|
|
7332
7804
|
if (!isObject12(raw) || !isObject12(raw.capabilities))
|
|
7333
7805
|
return;
|
|
7334
7806
|
const name = optString2(raw.name);
|
|
7335
|
-
const
|
|
7336
|
-
const
|
|
7807
|
+
const slug = optString2(raw.slug);
|
|
7808
|
+
const plan = raw.plan;
|
|
7337
7809
|
const c = raw.capabilities;
|
|
7338
7810
|
const src = c.upstreamSource;
|
|
7339
|
-
if (name === undefined ||
|
|
7811
|
+
if (name === undefined || slug === undefined || plan !== "team" && plan !== "enterprise" && plan !== "api" && plan !== "individual" || src !== "own" && src !== "shared" && src !== "none")
|
|
7340
7812
|
return;
|
|
7341
7813
|
return {
|
|
7342
7814
|
name,
|
|
7343
|
-
slug
|
|
7344
|
-
plan
|
|
7815
|
+
slug,
|
|
7816
|
+
plan,
|
|
7345
7817
|
capabilities: {
|
|
7346
7818
|
upstreamKey: c.upstreamKey === true,
|
|
7347
7819
|
adminKey: c.adminKey === true,
|
|
@@ -7352,16 +7824,16 @@ var parseOrgInfo2 = (raw) => {
|
|
|
7352
7824
|
};
|
|
7353
7825
|
};
|
|
7354
7826
|
var readConfig2 = (home) => {
|
|
7355
|
-
const
|
|
7356
|
-
if (!existsSync16(
|
|
7827
|
+
const p = configPath2(home);
|
|
7828
|
+
if (!existsSync16(p))
|
|
7357
7829
|
return {};
|
|
7358
|
-
return parseConfig2(JSON.parse(readFileSync13(
|
|
7830
|
+
return parseConfig2(JSON.parse(readFileSync13(p, "utf8")));
|
|
7359
7831
|
};
|
|
7360
7832
|
|
|
7361
7833
|
// src/schedule.ts
|
|
7362
|
-
import { spawnSync as
|
|
7834
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
7363
7835
|
var defaultExec = (cmd, args) => {
|
|
7364
|
-
const r =
|
|
7836
|
+
const r = spawnSync6(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
7365
7837
|
if (r.error)
|
|
7366
7838
|
return { ok: false, stderr: r.error.message };
|
|
7367
7839
|
return { ok: r.status === 0, stderr: r.stderr ?? "" };
|
|
@@ -7395,13 +7867,15 @@ var isSledsStopHook2 = isSledsHookFor2("stop-event");
|
|
|
7395
7867
|
var USAGE = `usage: sleds <command> [options]
|
|
7396
7868
|
|
|
7397
7869
|
commands:
|
|
7398
|
-
init
|
|
7399
|
-
|
|
7870
|
+
init Sign in through the browser, make this machine a seat, install the /sled skill
|
|
7871
|
+
and the status line. No flags needed: it opens app.sleds.ai/connect and waits.
|
|
7872
|
+
--token <sled_\u2026> skip the browser: a seat token (else SLEDS_TOKEN, ~/.sleds-token)
|
|
7400
7873
|
--url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
|
|
7874
|
+
--no-browser print the sign-in URL and code instead of opening a browser
|
|
7401
7875
|
--no-probe skip the /healthz check of the proxy URL
|
|
7402
7876
|
--seat-first keep Claude Code on your seat; the proxy only at a wall (default for seat plans)
|
|
7403
7877
|
--proxy-always every request through the proxy on the platform key (default for API-plan orgs)
|
|
7404
|
-
--non-interactive never prompt; exit 2 if no token is available
|
|
7878
|
+
--non-interactive never open a browser or prompt; exit 2 if no token is available
|
|
7405
7879
|
--agent codex Codex CLI instead of Claude Code: writes ~/.codex/config.toml
|
|
7406
7880
|
(provider "sleds" \u2192 the proxy, key from $SLEDS_TOKEN); no hooks/status line
|
|
7407
7881
|
--signup no org? create one: (1) just you \u2014 your email + your own Anthropic
|
|
@@ -7490,8 +7964,8 @@ var stringFlag = (flags, name) => {
|
|
|
7490
7964
|
var hookStdin = async () => {
|
|
7491
7965
|
if (process.stdin.isTTY)
|
|
7492
7966
|
return;
|
|
7493
|
-
|
|
7494
|
-
const text = await
|
|
7967
|
+
await Promise.resolve().then(() => init_seat());
|
|
7968
|
+
const text = await readStdin2(300);
|
|
7495
7969
|
return text.trim() ? text : undefined;
|
|
7496
7970
|
};
|
|
7497
7971
|
var sledsArgv = () => {
|
|
@@ -7509,7 +7983,7 @@ var main = async (argv) => {
|
|
|
7509
7983
|
const { cmd, flags } = parseArgs(argv);
|
|
7510
7984
|
const home = homedir();
|
|
7511
7985
|
const scheduler = {
|
|
7512
|
-
platform:
|
|
7986
|
+
platform: platform3(),
|
|
7513
7987
|
home,
|
|
7514
7988
|
sledsArgv: sledsArgv(),
|
|
7515
7989
|
exec: defaultExec
|
|
@@ -7557,10 +8031,11 @@ var main = async (argv) => {
|
|
|
7557
8031
|
team: typeof flags.team === "string" ? flags.team : undefined,
|
|
7558
8032
|
email: typeof flags.email === "string" ? flags.email : undefined,
|
|
7559
8033
|
noProbe: flags["no-probe"] === true,
|
|
8034
|
+
noBrowser: flags["no-browser"] === true,
|
|
7560
8035
|
proxyAlways: flags["proxy-always"] === true,
|
|
7561
8036
|
seatFirst: flags["seat-first"] === true,
|
|
7562
8037
|
installMusher: () => {
|
|
7563
|
-
if (
|
|
8038
|
+
if (platform3() !== "darwin")
|
|
7564
8039
|
return null;
|
|
7565
8040
|
const lines = [];
|
|
7566
8041
|
const code = musherInstall({
|
|
@@ -7643,7 +8118,7 @@ ${renderHelpText(argv[1])}`);
|
|
|
7643
8118
|
case "version":
|
|
7644
8119
|
case "--version":
|
|
7645
8120
|
case "-v":
|
|
7646
|
-
console.log("sleds 0.1.
|
|
8121
|
+
console.log("sleds 0.1.24");
|
|
7647
8122
|
return 0;
|
|
7648
8123
|
default:
|
|
7649
8124
|
console.error(`unknown command: ${cmd}
|