@sleds/cli 0.1.23 → 0.1.25
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 +1761 -1158
- 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",
|
|
@@ -1236,16 +833,21 @@ var GUIDES = [
|
|
|
1236
833
|
{
|
|
1237
834
|
do: "Add the custom connector",
|
|
1238
835
|
where: "claude.ai › Settings › Connectors › `Add custom connector`",
|
|
1239
|
-
note: "URL `https://
|
|
836
|
+
note: "URL `https://mcp.sleds.ai/mcp`. It signs you in with OAuth — the same email your workspace invited."
|
|
1240
837
|
},
|
|
1241
838
|
{
|
|
1242
839
|
do: "Say `/sled usage` in any chat",
|
|
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
|
{
|
|
@@ -1603,7 +1221,7 @@ var VERBS = [
|
|
|
1603
1221
|
id: "connector",
|
|
1604
1222
|
group: "trust",
|
|
1605
1223
|
want: "Use SLEDS from chat, Cowork or Claude Design",
|
|
1606
|
-
mcp: "add https://
|
|
1224
|
+
mcp: "add https://mcp.sleds.ai/mcp as a connector (OAuth) — then every verb in this column",
|
|
1607
1225
|
how: "The conversation is your cockpit: the same tools, no terminal. Your seat is the account you signed in with."
|
|
1608
1226
|
},
|
|
1609
1227
|
{
|
|
@@ -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,8 +1524,139 @@ function parseWorthBand(raw) {
|
|
|
1868
1524
|
...typeof w.note === "string" && w.note.trim() ? { note: clip(w.note, 140) } : {}
|
|
1869
1525
|
};
|
|
1870
1526
|
}
|
|
1871
|
-
// ../../packages/shared/src/
|
|
1872
|
-
|
|
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
|
+
]);
|
|
1564
|
+
// ../../packages/shared/src/dispatch/plan.ts
|
|
1565
|
+
init_format();
|
|
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
|
+
}
|
|
1873
1660
|
|
|
1874
1661
|
// ../../packages/shared/src/mcp/server.ts
|
|
1875
1662
|
init_format();
|
|
@@ -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 {
|
|
@@ -2106,15 +2076,15 @@ var SERVER_INFO = {
|
|
|
2106
2076
|
description: "The cost and limits of AI agent work — estimate, hand off, and track what a task takes.",
|
|
2107
2077
|
websiteUrl: "https://sleds.ai",
|
|
2108
2078
|
icons: [
|
|
2109
|
-
{ src: "https://
|
|
2110
|
-
{ src: "https://
|
|
2079
|
+
{ src: "https://mcp.sleds.ai/icon.png", mimeType: "image/png", sizes: ["192x192"] },
|
|
2080
|
+
{ src: "https://mcp.sleds.ai/icon.svg", mimeType: "image/svg+xml", sizes: ["any"] }
|
|
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
|
|
|
@@ -3033,7 +3318,7 @@ async function actualTool(ctx, args) {
|
|
|
3033
3318
|
...num3(args.tokensIn) !== undefined ? { tokensIn: num3(args.tokensIn) } : {},
|
|
3034
3319
|
...num3(args.tokensOut) !== undefined ? { tokensOut: num3(args.tokensOut) } : {},
|
|
3035
3320
|
...str4(args.by) ? { by: str4(args.by) } : {},
|
|
3036
|
-
...sessionIdOf(args.sessionId) ? { sessionId: sessionIdOf(args.sessionId) } : {},
|
|
3321
|
+
...sessionIdOf(args.sessionId) ?? ctx.sessionId?.() ? { sessionId: sessionIdOf(args.sessionId) ?? ctx.sessionId?.() } : {},
|
|
3037
3322
|
...str4(args.ref) ? { ref: str4(args.ref) } : {},
|
|
3038
3323
|
...phases && Object.keys(phases).length > 0 ? { phases } : {},
|
|
3039
3324
|
...num3(args.seatCreditsUsd) !== undefined ? { seatCreditsUsd: num3(args.seatCreditsUsd) } : {}
|
|
@@ -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")
|
|
@@ -4891,65 +5214,70 @@ function contextFor(deps) {
|
|
|
4891
5214
|
const src = meterSource(deps.home);
|
|
4892
5215
|
const now = deps.now ?? Date.now;
|
|
4893
5216
|
const client = src ? trackingClient(fetchClient({ ...src, fetchFn: deps.fetchFn, timeoutMs: deps.timeoutMs }), deps.home, now) : null;
|
|
4894
|
-
return {
|
|
5217
|
+
return {
|
|
5218
|
+
client,
|
|
5219
|
+
now: deps.now,
|
|
5220
|
+
usage: () => usageTool2(deps),
|
|
5221
|
+
sessionId: () => readSession(deps.home)?.sessionId
|
|
5222
|
+
};
|
|
4895
5223
|
}
|
|
4896
5224
|
var callTool2 = (name, args, deps) => callTool(name, args, contextFor(deps));
|
|
4897
5225
|
var handleLine2 = (line, deps) => handleLine(line, contextFor(deps));
|
|
4898
5226
|
|
|
4899
5227
|
// src/commands/handoffs.ts
|
|
4900
|
-
var handoffsCmd = async (
|
|
4901
|
-
const r = await callTool2("handoffs", {}, { home:
|
|
4902
|
-
(r.isError ?
|
|
5228
|
+
var handoffsCmd = async (o) => {
|
|
5229
|
+
const r = await callTool2("handoffs", {}, { home: o.home });
|
|
5230
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
4903
5231
|
return r.isError ? 1 : 0;
|
|
4904
5232
|
};
|
|
4905
|
-
var pickupCmd = async (
|
|
4906
|
-
const id =
|
|
5233
|
+
var pickupCmd = async (o) => {
|
|
5234
|
+
const id = o.argv[0];
|
|
4907
5235
|
if (!id) {
|
|
4908
|
-
(
|
|
5236
|
+
(o.error ?? console.error)("usage: sleds pickup <handoffId> (ids from `sleds handoffs`)");
|
|
4909
5237
|
return 2;
|
|
4910
5238
|
}
|
|
4911
|
-
const r = await callTool2("pickup", { handoffId: id }, { home:
|
|
4912
|
-
(r.isError ?
|
|
5239
|
+
const r = await callTool2("pickup", { handoffId: id }, { home: o.home });
|
|
5240
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
4913
5241
|
return r.isError ? 1 : 0;
|
|
4914
5242
|
};
|
|
4915
|
-
var withdrawCmd = async (
|
|
4916
|
-
const id =
|
|
5243
|
+
var withdrawCmd = async (o) => {
|
|
5244
|
+
const id = o.argv[0];
|
|
4917
5245
|
if (!id) {
|
|
4918
|
-
(
|
|
5246
|
+
(o.error ?? console.error)("usage: sleds withdraw <handoffId> (one you parked)");
|
|
4919
5247
|
return 2;
|
|
4920
5248
|
}
|
|
4921
|
-
const r = await callTool2("withdraw", { handoffId: id }, { home:
|
|
4922
|
-
(r.isError ?
|
|
5249
|
+
const r = await callTool2("withdraw", { handoffId: id }, { home: o.home });
|
|
5250
|
+
(r.isError ? o.error ?? console.error : o.log ?? console.log)(r.text);
|
|
4923
5251
|
return r.isError ? 1 : 0;
|
|
4924
5252
|
};
|
|
4925
5253
|
|
|
4926
5254
|
// src/commands/hooks.ts
|
|
4927
5255
|
init_settings();
|
|
4928
|
-
var hooks = (
|
|
4929
|
-
const log =
|
|
4930
|
-
const error =
|
|
4931
|
-
if (!hasClaudeCode(
|
|
4932
|
-
error(`Claude Code not found (${claudeDir(
|
|
5256
|
+
var hooks = (o) => {
|
|
5257
|
+
const log = o.log ?? console.log;
|
|
5258
|
+
const error = o.error ?? console.error;
|
|
5259
|
+
if (!hasClaudeCode(o.home)) {
|
|
5260
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
4933
5261
|
return 1;
|
|
4934
5262
|
}
|
|
4935
|
-
const settings = readSettings(
|
|
5263
|
+
const settings = readSettings(o.home);
|
|
4936
5264
|
const had = hasSeatHook(settings);
|
|
4937
5265
|
const hadSpec = hasSpecHook(settings);
|
|
4938
|
-
const withSeat = applySeatHook(settings,
|
|
5266
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
4939
5267
|
const hadPrompt = hasPromptHook(settings);
|
|
4940
5268
|
const hadSession = hasSessionHook(settings);
|
|
4941
5269
|
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(
|
|
5270
|
+
const withPrompt = applyPromptHook(applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env)), o.promptEventCommand ?? sledsCommand("prompt-event", o.home, o.env));
|
|
5271
|
+
const withSpec = applyStopHook(applySessionHook(withPrompt, o.sessionEventCommand ?? sledsCommand("session-event", o.home, o.env)), o.stopEventCommand ?? sledsCommand("stop-event", o.home, o.env));
|
|
5272
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
5273
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
5274
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
4947
5275
|
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(
|
|
5276
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
5277
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
5278
|
+
log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o.home)}`);
|
|
5279
|
+
log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(o.home)}`);
|
|
5280
|
+
log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(o.home)}`);
|
|
4953
5281
|
return 0;
|
|
4954
5282
|
};
|
|
4955
5283
|
|
|
@@ -5100,43 +5428,172 @@ From Cowork or any MCP client (\`sleds mcp\`): the \`handoffs\` tool lists what
|
|
|
5100
5428
|
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
5429
|
`;
|
|
5102
5430
|
|
|
5431
|
+
// src/commands/device.ts
|
|
5432
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5433
|
+
import { hostname as hostname2, platform } from "node:os";
|
|
5434
|
+
var DEVICE_REASONS = {
|
|
5435
|
+
unreachable: "could not reach the proxy.",
|
|
5436
|
+
rate_limited: "too many sign-ins are open right now. Try again in a few minutes.",
|
|
5437
|
+
denied: "that sign-in was declined on the web.",
|
|
5438
|
+
expired: "the code expired before it was approved (they last fifteen minutes). Run `sleds init` again.",
|
|
5439
|
+
bad_answer: "the proxy gave an answer this CLI does not understand — update it: curl -fsSL sleds.ai/install | sh"
|
|
5440
|
+
};
|
|
5441
|
+
function deviceFailureText(reason) {
|
|
5442
|
+
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 …\`.`;
|
|
5443
|
+
}
|
|
5444
|
+
function parseStart(body) {
|
|
5445
|
+
if (typeof body !== "object" || body === null)
|
|
5446
|
+
return null;
|
|
5447
|
+
const b = body;
|
|
5448
|
+
if (typeof b.deviceCode !== "string" || typeof b.userCode !== "string")
|
|
5449
|
+
return null;
|
|
5450
|
+
if (typeof b.verifyUrl !== "string" || !/^https?:\/\//.test(b.verifyUrl))
|
|
5451
|
+
return null;
|
|
5452
|
+
const expiresIn = typeof b.expiresIn === "number" && b.expiresIn > 0 ? Math.min(b.expiresIn, 3600) : 900;
|
|
5453
|
+
const interval = typeof b.interval === "number" && b.interval >= 1 ? Math.min(b.interval, 30) : 3;
|
|
5454
|
+
return {
|
|
5455
|
+
deviceCode: b.deviceCode,
|
|
5456
|
+
userCode: b.userCode,
|
|
5457
|
+
verifyUrl: b.verifyUrl,
|
|
5458
|
+
expiresIn,
|
|
5459
|
+
interval
|
|
5460
|
+
};
|
|
5461
|
+
}
|
|
5462
|
+
function parsePoll(body) {
|
|
5463
|
+
if (typeof body !== "object" || body === null)
|
|
5464
|
+
return null;
|
|
5465
|
+
const b = body;
|
|
5466
|
+
if (b.status === "pending" || b.status === "denied" || b.status === "expired")
|
|
5467
|
+
return { status: b.status };
|
|
5468
|
+
if (b.status !== "approved")
|
|
5469
|
+
return null;
|
|
5470
|
+
const org = b.org;
|
|
5471
|
+
if (typeof b.token !== "string" || typeof b.email !== "string" || !org)
|
|
5472
|
+
return null;
|
|
5473
|
+
if (typeof org.name !== "string" || typeof org.slug !== "string")
|
|
5474
|
+
return null;
|
|
5475
|
+
return {
|
|
5476
|
+
status: "approved",
|
|
5477
|
+
approved: {
|
|
5478
|
+
token: b.token,
|
|
5479
|
+
email: b.email,
|
|
5480
|
+
org: { name: org.name, slug: org.slug, plan: typeof org.plan === "string" ? org.plan : "" },
|
|
5481
|
+
workspaceUrl: typeof b.workspaceUrl === "string" ? b.workspaceUrl : ""
|
|
5482
|
+
}
|
|
5483
|
+
};
|
|
5484
|
+
}
|
|
5485
|
+
function openInBrowser(url) {
|
|
5486
|
+
const p = platform();
|
|
5487
|
+
const cmd = p === "darwin" ? "open" : p === "win32" ? "cmd" : "xdg-open";
|
|
5488
|
+
const args = p === "win32" ? ["/c", "start", "", url] : [url];
|
|
5489
|
+
try {
|
|
5490
|
+
const r = spawnSync3(cmd, args, { stdio: "ignore", timeout: 5000 });
|
|
5491
|
+
return r.error === undefined && r.status === 0;
|
|
5492
|
+
} catch {
|
|
5493
|
+
return false;
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5497
|
+
async function deviceFlow(o) {
|
|
5498
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
5499
|
+
const log = o.log ?? console.log;
|
|
5500
|
+
const sleep = o.sleep ?? defaultSleep;
|
|
5501
|
+
const now = o.now ?? Date.now;
|
|
5502
|
+
let started = null;
|
|
5503
|
+
try {
|
|
5504
|
+
const res = await fetchFn(`${o.proxyUrl}/device/start`, {
|
|
5505
|
+
method: "POST",
|
|
5506
|
+
headers: { "content-type": "application/json" },
|
|
5507
|
+
body: JSON.stringify({ agent: o.agent ?? "claude-code", host: o.host ?? hostname2() }),
|
|
5508
|
+
signal: AbortSignal.timeout(20000)
|
|
5509
|
+
});
|
|
5510
|
+
if (res.status === 429)
|
|
5511
|
+
return { ok: false, reason: "rate_limited" };
|
|
5512
|
+
if (!res.ok)
|
|
5513
|
+
return { ok: false, reason: "unreachable" };
|
|
5514
|
+
started = parseStart(await res.json().catch(() => null));
|
|
5515
|
+
} catch {
|
|
5516
|
+
return { ok: false, reason: "unreachable" };
|
|
5517
|
+
}
|
|
5518
|
+
if (!started)
|
|
5519
|
+
return { ok: false, reason: "bad_answer" };
|
|
5520
|
+
const opened = o.noBrowser ? false : (o.openBrowser ?? openInBrowser)(started.verifyUrl);
|
|
5521
|
+
log("");
|
|
5522
|
+
log(opened ? "Opening your browser to sign in to SLEDS." : "Open this in your browser to sign in to SLEDS:");
|
|
5523
|
+
log(` ${started.verifyUrl}`);
|
|
5524
|
+
log("");
|
|
5525
|
+
log(` Your code: ${started.userCode} — the page shows the same one.`);
|
|
5526
|
+
log("");
|
|
5527
|
+
log("Waiting for the approval…");
|
|
5528
|
+
const deadline = now() + started.expiresIn * 1000;
|
|
5529
|
+
let intervalMs = started.interval * 1000;
|
|
5530
|
+
while (now() < deadline) {
|
|
5531
|
+
await sleep(intervalMs);
|
|
5532
|
+
let body;
|
|
5533
|
+
try {
|
|
5534
|
+
const res = await fetchFn(`${o.proxyUrl}/device/poll`, {
|
|
5535
|
+
method: "POST",
|
|
5536
|
+
headers: { "content-type": "application/json" },
|
|
5537
|
+
body: JSON.stringify({ deviceCode: started.deviceCode }),
|
|
5538
|
+
signal: AbortSignal.timeout(20000)
|
|
5539
|
+
});
|
|
5540
|
+
if (res.status === 429) {
|
|
5541
|
+
intervalMs = Math.min(intervalMs * 2, 30000);
|
|
5542
|
+
continue;
|
|
5543
|
+
}
|
|
5544
|
+
body = await res.json().catch(() => null);
|
|
5545
|
+
} catch {
|
|
5546
|
+
continue;
|
|
5547
|
+
}
|
|
5548
|
+
const r = parsePoll(body);
|
|
5549
|
+
if (!r)
|
|
5550
|
+
return { ok: false, reason: "bad_answer" };
|
|
5551
|
+
if (r.status === "pending")
|
|
5552
|
+
continue;
|
|
5553
|
+
if (r.status === "approved")
|
|
5554
|
+
return { ok: true, approved: r.approved };
|
|
5555
|
+
return { ok: false, reason: r.status };
|
|
5556
|
+
}
|
|
5557
|
+
return { ok: false, reason: "expired" };
|
|
5558
|
+
}
|
|
5559
|
+
|
|
5103
5560
|
// src/commands/hooks.ts
|
|
5104
5561
|
init_settings();
|
|
5105
|
-
var hooks2 = (
|
|
5106
|
-
const log =
|
|
5107
|
-
const error =
|
|
5108
|
-
if (!hasClaudeCode(
|
|
5109
|
-
error(`Claude Code not found (${claudeDir(
|
|
5562
|
+
var hooks2 = (o) => {
|
|
5563
|
+
const log = o.log ?? console.log;
|
|
5564
|
+
const error = o.error ?? console.error;
|
|
5565
|
+
if (!hasClaudeCode(o.home)) {
|
|
5566
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
5110
5567
|
return 1;
|
|
5111
5568
|
}
|
|
5112
|
-
const settings = readSettings(
|
|
5569
|
+
const settings = readSettings(o.home);
|
|
5113
5570
|
const had = hasSeatHook(settings);
|
|
5114
5571
|
const hadSpec = hasSpecHook(settings);
|
|
5115
|
-
const withSeat = applySeatHook(settings,
|
|
5572
|
+
const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
|
|
5116
5573
|
const hadPrompt = hasPromptHook(settings);
|
|
5117
5574
|
const hadSession = hasSessionHook(settings);
|
|
5118
5575
|
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(
|
|
5576
|
+
const withPrompt = applyPromptHook(applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env)), o.promptEventCommand ?? sledsCommand("prompt-event", o.home, o.env));
|
|
5577
|
+
const withSpec = applyStopHook(applySessionHook(withPrompt, o.sessionEventCommand ?? sledsCommand("session-event", o.home, o.env)), o.stopEventCommand ?? sledsCommand("stop-event", o.home, o.env));
|
|
5578
|
+
const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
|
|
5579
|
+
writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
|
|
5580
|
+
log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
|
|
5124
5581
|
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(
|
|
5582
|
+
log(`refreshed statusLine in ${settingsPath(o.home)}`);
|
|
5583
|
+
log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
|
|
5584
|
+
log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o.home)}`);
|
|
5585
|
+
log(`${hadSession ? "refreshed" : "set"} hooks.SessionStart (the loop and the rows this machine owes, before the first turn) in ${settingsPath(o.home)}`);
|
|
5586
|
+
log(`${hadStop ? "refreshed" : "set"} hooks.Stop (a session that estimated and never posted its actual is asked once) in ${settingsPath(o.home)}`);
|
|
5130
5587
|
return 0;
|
|
5131
5588
|
};
|
|
5132
5589
|
|
|
5133
5590
|
// src/commands/init.ts
|
|
5134
5591
|
var DEFAULT_PROXY_URL = "https://proxy.sleds.ai";
|
|
5135
|
-
var promptStdin = (question) => new Promise((
|
|
5592
|
+
var promptStdin = (question) => new Promise((resolve) => {
|
|
5136
5593
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
5137
5594
|
rl.question(question, (answer) => {
|
|
5138
5595
|
rl.close();
|
|
5139
|
-
|
|
5596
|
+
resolve(answer.trim());
|
|
5140
5597
|
});
|
|
5141
5598
|
});
|
|
5142
5599
|
async function signupIndividual(proxyUrl, email, credential, fetchFn = fetch) {
|
|
@@ -5233,56 +5690,70 @@ async function probeProxy(url, fetchFn = fetch, opts = {}) {
|
|
|
5233
5690
|
}
|
|
5234
5691
|
return false;
|
|
5235
5692
|
}
|
|
5236
|
-
var resolveToken = async (
|
|
5237
|
-
const env =
|
|
5238
|
-
if (
|
|
5239
|
-
return { token:
|
|
5693
|
+
var resolveToken = async (o) => {
|
|
5694
|
+
const env = o.env ?? process.env;
|
|
5695
|
+
if (o.token)
|
|
5696
|
+
return { token: o.token.trim(), source: "flag" };
|
|
5240
5697
|
if (env.SLEDS_TOKEN)
|
|
5241
5698
|
return { token: env.SLEDS_TOKEN.trim(), source: "env" };
|
|
5242
|
-
const fromFile = readTokenFile(
|
|
5699
|
+
const fromFile = readTokenFile(o.home);
|
|
5243
5700
|
if (fromFile)
|
|
5244
5701
|
return { token: fromFile, source: "file" };
|
|
5245
|
-
if (
|
|
5702
|
+
if (o.nonInteractive)
|
|
5246
5703
|
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);
|
|
5704
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
5253
5705
|
if (!url)
|
|
5254
5706
|
return null;
|
|
5255
|
-
const log =
|
|
5256
|
-
const err =
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5707
|
+
const log = o.log ?? console.log;
|
|
5708
|
+
const err = o.error ?? console.error;
|
|
5709
|
+
if (!o.signup) {
|
|
5710
|
+
const r = await deviceFlow({
|
|
5711
|
+
proxyUrl: url,
|
|
5712
|
+
agent: "claude-code",
|
|
5713
|
+
host: o.host,
|
|
5714
|
+
fetchFn: o.fetchFn,
|
|
5715
|
+
openBrowser: o.openBrowser ?? openInBrowser,
|
|
5716
|
+
noBrowser: o.noBrowser,
|
|
5717
|
+
sleep: o.sleep,
|
|
5718
|
+
log
|
|
5719
|
+
});
|
|
5720
|
+
if (!r.ok) {
|
|
5721
|
+
err(deviceFailureText(r.reason));
|
|
5722
|
+
return null;
|
|
5723
|
+
}
|
|
5724
|
+
return { token: r.approved.token, source: "device", approved: r.approved };
|
|
5725
|
+
}
|
|
5726
|
+
if (!o.prompt)
|
|
5727
|
+
return null;
|
|
5728
|
+
let team = o.team?.trim() || null;
|
|
5729
|
+
if (team === null && !o.email) {
|
|
5730
|
+
const which = (await o.prompt("Set up SLEDS for (1) just you, or (2) your team? [1/2]: ")).trim();
|
|
5260
5731
|
if (which === "2") {
|
|
5261
|
-
team = (await
|
|
5732
|
+
team = (await o.prompt("Team name (e.g. your company): ")).trim();
|
|
5262
5733
|
if (!team)
|
|
5263
5734
|
return null;
|
|
5264
5735
|
}
|
|
5265
5736
|
}
|
|
5266
|
-
const email = (
|
|
5737
|
+
const email = (o.email ?? await o.prompt(team ? "Your work email: " : "Your email: ")).trim();
|
|
5267
5738
|
if (!email.includes("@"))
|
|
5268
5739
|
return null;
|
|
5269
5740
|
if (team) {
|
|
5270
|
-
const
|
|
5271
|
-
const
|
|
5272
|
-
if (!
|
|
5273
|
-
err(signupFailureText(
|
|
5741
|
+
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();
|
|
5742
|
+
const r = await signupTeam(url, email, team, credential || null, o.fetchFn);
|
|
5743
|
+
if (!r.ok) {
|
|
5744
|
+
err(signupFailureText(r));
|
|
5274
5745
|
return null;
|
|
5275
5746
|
}
|
|
5276
|
-
log(`Created ${team} with you as owner (${email}).${
|
|
5277
|
-
if (
|
|
5747
|
+
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."}`);
|
|
5748
|
+
if (r.verifyPending)
|
|
5278
5749
|
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
5750
|
log("Invite your team on the dashboard (Members → invite by email, Admin → Mint a seat); each person runs `sleds init --token …`.");
|
|
5280
|
-
return { token:
|
|
5751
|
+
return { token: r.token, source: "signup" };
|
|
5281
5752
|
}
|
|
5282
|
-
const credential = (await
|
|
5753
|
+
const credential = (await o.prompt("Your Anthropic API key (sk-ant-…, stored encrypted, spends only your account): ")).trim();
|
|
5283
5754
|
if (!credential)
|
|
5284
5755
|
return null;
|
|
5285
|
-
const r = await signupIndividual(url, email, credential,
|
|
5756
|
+
const r = await signupIndividual(url, email, credential, o.fetchFn);
|
|
5286
5757
|
if (!r.ok) {
|
|
5287
5758
|
err(signupFailureText(r));
|
|
5288
5759
|
return null;
|
|
@@ -5290,66 +5761,66 @@ var resolveToken = async (o2) => {
|
|
|
5290
5761
|
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
5762
|
return { token: r.token, source: "signup" };
|
|
5292
5763
|
};
|
|
5293
|
-
var init = async (
|
|
5294
|
-
const log =
|
|
5295
|
-
const error =
|
|
5296
|
-
const env =
|
|
5297
|
-
if (!hasClaudeCode(
|
|
5298
|
-
error(`Claude Code not found (${claudeDir(
|
|
5764
|
+
var init = async (o) => {
|
|
5765
|
+
const log = o.log ?? console.log;
|
|
5766
|
+
const error = o.error ?? console.error;
|
|
5767
|
+
const env = o.env ?? process.env;
|
|
5768
|
+
if (!hasClaudeCode(o.home)) {
|
|
5769
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
5299
5770
|
return 1;
|
|
5300
5771
|
}
|
|
5301
|
-
const resolved = await resolveToken(
|
|
5772
|
+
const resolved = await resolveToken(o);
|
|
5302
5773
|
if (!resolved) {
|
|
5303
|
-
error(
|
|
5774
|
+
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
5775
|
return 2;
|
|
5305
5776
|
}
|
|
5306
5777
|
if (!isSeatToken(resolved.token)) {
|
|
5307
5778
|
error("Invalid seat token: expected something like sled_xxxxxxxx.");
|
|
5308
5779
|
return 2;
|
|
5309
5780
|
}
|
|
5310
|
-
const url = normalizeProxyUrl(
|
|
5781
|
+
const url = normalizeProxyUrl(o.url ?? env.SLEDS_PROXY_URL ?? readConfig(o.home).proxyUrl ?? DEFAULT_PROXY_URL);
|
|
5311
5782
|
if (!url) {
|
|
5312
|
-
error(`Invalid proxy URL: ${JSON.stringify(
|
|
5783
|
+
error(`Invalid proxy URL: ${JSON.stringify(o.url ?? env.SLEDS_PROXY_URL ?? "")}. Expected https://proxy.sleds.ai.`);
|
|
5313
5784
|
return 2;
|
|
5314
5785
|
}
|
|
5315
|
-
if (!
|
|
5786
|
+
if (!o.noProbe && !await probeProxy(url, o.fetchFn)) {
|
|
5316
5787
|
error(`No SLEDS proxy answered at ${url}/healthz. Check the URL (or pass --no-probe to save it anyway).`);
|
|
5317
5788
|
return 2;
|
|
5318
5789
|
}
|
|
5319
5790
|
const wrote = [];
|
|
5320
|
-
const seatInfo = await fetchSeatInfo(url, resolved.token,
|
|
5791
|
+
const seatInfo = await fetchSeatInfo(url, resolved.token, o.fetchFn);
|
|
5321
5792
|
const org = seatInfo.org;
|
|
5322
|
-
const wiring =
|
|
5793
|
+
const wiring = o.proxyAlways ? "proxy-always" : o.seatFirst ? "seat-first" : org?.mode ?? "proxy-always";
|
|
5323
5794
|
if (wiring === "proxy-always" && org && !org.capabilities.upstreamKey) {
|
|
5324
5795
|
error(`${org.name} has no platform key for the proxy to forward with; --proxy-always would break every request. Using seat-first.`);
|
|
5325
5796
|
}
|
|
5326
5797
|
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(
|
|
5798
|
+
if (backupSettings(o.home))
|
|
5799
|
+
wrote.push(`backed up settings to ${backupPath(o.home)}`);
|
|
5800
|
+
const current = readSettings(o.home);
|
|
5330
5801
|
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(
|
|
5802
|
+
const statusLine = applyStatusLine(wired, o.statusLineCommand ?? sledsCommand("statusline", o.home, o.env));
|
|
5803
|
+
writeSettings(o.home, statusLine.settings);
|
|
5804
|
+
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`);
|
|
5805
|
+
wrote.push(statusLine.prior === undefined ? `set statusLine in ${settingsPath(o.home)}` : `set statusLine in ${settingsPath(o.home)} (previous one kept for uninstall)`);
|
|
5335
5806
|
hooks2({
|
|
5336
|
-
home:
|
|
5337
|
-
env:
|
|
5807
|
+
home: o.home,
|
|
5808
|
+
env: o.env,
|
|
5338
5809
|
refreshStatusLine: false,
|
|
5339
|
-
seatEventCommand:
|
|
5340
|
-
specEventCommand:
|
|
5341
|
-
promptEventCommand:
|
|
5342
|
-
sessionEventCommand:
|
|
5343
|
-
stopEventCommand:
|
|
5810
|
+
seatEventCommand: o.seatEventCommand,
|
|
5811
|
+
specEventCommand: o.specEventCommand,
|
|
5812
|
+
promptEventCommand: o.promptEventCommand,
|
|
5813
|
+
sessionEventCommand: o.sessionEventCommand,
|
|
5814
|
+
stopEventCommand: o.stopEventCommand,
|
|
5344
5815
|
log: (line) => wrote.push(line),
|
|
5345
5816
|
error
|
|
5346
5817
|
});
|
|
5347
5818
|
if (resolved.source !== "file") {
|
|
5348
|
-
writeTokenFile(
|
|
5349
|
-
wrote.push(`saved token to ${tokenPath(
|
|
5819
|
+
writeTokenFile(o.home, resolved.token);
|
|
5820
|
+
wrote.push(`saved token to ${tokenPath(o.home)} (0600)`);
|
|
5350
5821
|
}
|
|
5351
|
-
const prior = readConfig(
|
|
5352
|
-
writeConfig(
|
|
5822
|
+
const prior = readConfig(o.home);
|
|
5823
|
+
writeConfig(o.home, {
|
|
5353
5824
|
...prior,
|
|
5354
5825
|
token: resolved.token,
|
|
5355
5826
|
proxyUrl: url,
|
|
@@ -5357,18 +5828,24 @@ var init = async (o2) => {
|
|
|
5357
5828
|
agentKind: "claude-code",
|
|
5358
5829
|
...seatInfo.email ? { email: seatInfo.email } : {},
|
|
5359
5830
|
...org ? { org } : {},
|
|
5360
|
-
...
|
|
5831
|
+
...statusLine.prior !== undefined ? { priorStatusLine: statusLine.prior } : {}
|
|
5361
5832
|
});
|
|
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 =
|
|
5833
|
+
wrote.push(`saved token and proxy URL to ${configPath(o.home)} (0600)`);
|
|
5834
|
+
mkdirSync8(skillDir(o.home), { recursive: true });
|
|
5835
|
+
writeFileSync8(skillPath(o.home), SKILL_MD);
|
|
5836
|
+
wrote.push(`installed skill at ${skillPath(o.home)}`);
|
|
5837
|
+
if (o.installMusher) {
|
|
5838
|
+
const r = o.installMusher();
|
|
5368
5839
|
if (r)
|
|
5369
5840
|
wrote.push(r);
|
|
5370
5841
|
}
|
|
5371
5842
|
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)";
|
|
5843
|
+
if (resolved.approved) {
|
|
5844
|
+
log("");
|
|
5845
|
+
log(`Signed in as ${resolved.approved.email} · ${resolved.approved.org.name}. This machine is now a seat there.`);
|
|
5846
|
+
if (resolved.approved.workspaceUrl)
|
|
5847
|
+
log(`Your workspace on the web: ${resolved.approved.workspaceUrl}`);
|
|
5848
|
+
}
|
|
5372
5849
|
if (finalWiring === "proxy-always") {
|
|
5373
5850
|
log(`Claude Code now runs through SLEDS on the platform key. (${orgLine})`);
|
|
5374
5851
|
log("");
|
|
@@ -5395,9 +5872,9 @@ var init = async (o2) => {
|
|
|
5395
5872
|
};
|
|
5396
5873
|
|
|
5397
5874
|
// src/commands/mcp.ts
|
|
5398
|
-
var mcp = async (
|
|
5399
|
-
const stdin =
|
|
5400
|
-
const write =
|
|
5875
|
+
var mcp = async (o) => {
|
|
5876
|
+
const stdin = o.stdin ?? process.stdin;
|
|
5877
|
+
const write = o.write ?? ((line) => process.stdout.write(`${line}
|
|
5401
5878
|
`));
|
|
5402
5879
|
let buffer = "";
|
|
5403
5880
|
for await (const chunk of stdin) {
|
|
@@ -5407,7 +5884,7 @@ var mcp = async (o2) => {
|
|
|
5407
5884
|
while (nl !== -1) {
|
|
5408
5885
|
const line = buffer.slice(0, nl);
|
|
5409
5886
|
buffer = buffer.slice(nl + 1);
|
|
5410
|
-
const out = await handleLine2(line, { home:
|
|
5887
|
+
const out = await handleLine2(line, { home: o.home });
|
|
5411
5888
|
if (out !== null)
|
|
5412
5889
|
write(out);
|
|
5413
5890
|
nl = buffer.indexOf(`
|
|
@@ -5415,7 +5892,7 @@ var mcp = async (o2) => {
|
|
|
5415
5892
|
}
|
|
5416
5893
|
}
|
|
5417
5894
|
if (buffer.trim().length > 0) {
|
|
5418
|
-
const out = await handleLine2(buffer, { home:
|
|
5895
|
+
const out = await handleLine2(buffer, { home: o.home });
|
|
5419
5896
|
if (out !== null)
|
|
5420
5897
|
write(out);
|
|
5421
5898
|
}
|
|
@@ -5425,15 +5902,15 @@ var mcp = async (o2) => {
|
|
|
5425
5902
|
// src/commands/meter.ts
|
|
5426
5903
|
init_config();
|
|
5427
5904
|
init_meter();
|
|
5428
|
-
var meter = async (
|
|
5429
|
-
const log =
|
|
5430
|
-
const now = (
|
|
5431
|
-
const src = meterSource(
|
|
5905
|
+
var meter = async (o) => {
|
|
5906
|
+
const log = o.log ?? console.log;
|
|
5907
|
+
const now = (o.now ?? Date.now)();
|
|
5908
|
+
const src = meterSource(o.home);
|
|
5432
5909
|
if (!src) {
|
|
5433
|
-
log(`SLEDS is not set up (${configPath(
|
|
5910
|
+
log(`SLEDS is not set up (${configPath(o.home)}). Run \`sleds init\` first.`);
|
|
5434
5911
|
return 1;
|
|
5435
5912
|
}
|
|
5436
|
-
const m = await loadMeter({ home:
|
|
5913
|
+
const m = await loadMeter({ home: o.home, now, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs });
|
|
5437
5914
|
if (!m) {
|
|
5438
5915
|
log(`sleds offline. ${src.proxyUrl} did not answer → \`sleds status\` for the proxy health.`);
|
|
5439
5916
|
return 2;
|
|
@@ -5446,16 +5923,16 @@ var meter = async (o2) => {
|
|
|
5446
5923
|
// src/commands/musher.ts
|
|
5447
5924
|
init_config();
|
|
5448
5925
|
init_meter();
|
|
5449
|
-
import { spawnSync as
|
|
5926
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
5450
5927
|
import { existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync9 } from "node:fs";
|
|
5451
|
-
import { hostname as
|
|
5928
|
+
import { hostname as hostname3 } from "node:os";
|
|
5452
5929
|
import { dirname as dirname5 } from "node:path";
|
|
5453
5930
|
var defaultWhich3 = (cmd) => {
|
|
5454
5931
|
const path = process.env.PATH ?? "";
|
|
5455
5932
|
for (const dir of path.split(":").filter(Boolean)) {
|
|
5456
|
-
const
|
|
5457
|
-
if (existsSync10(
|
|
5458
|
-
return
|
|
5933
|
+
const p = `${dir}/${cmd}`;
|
|
5934
|
+
if (existsSync10(p))
|
|
5935
|
+
return p;
|
|
5459
5936
|
}
|
|
5460
5937
|
return null;
|
|
5461
5938
|
};
|
|
@@ -5465,68 +5942,68 @@ var findClaude2 = (home, configured, which) => {
|
|
|
5465
5942
|
const found = which("claude");
|
|
5466
5943
|
if (found)
|
|
5467
5944
|
return found;
|
|
5468
|
-
for (const
|
|
5945
|
+
for (const p of [
|
|
5469
5946
|
`${home}/.local/bin/claude`,
|
|
5470
5947
|
"/opt/homebrew/bin/claude",
|
|
5471
5948
|
"/usr/local/bin/claude",
|
|
5472
5949
|
`${home}/.npm-global/bin/claude`
|
|
5473
5950
|
])
|
|
5474
|
-
if (existsSync10(
|
|
5475
|
-
return
|
|
5951
|
+
if (existsSync10(p))
|
|
5952
|
+
return p;
|
|
5476
5953
|
return null;
|
|
5477
5954
|
};
|
|
5478
|
-
var musherInstall = (
|
|
5479
|
-
const log =
|
|
5480
|
-
const error =
|
|
5481
|
-
const platform =
|
|
5955
|
+
var musherInstall = (o) => {
|
|
5956
|
+
const log = o.log ?? console.log;
|
|
5957
|
+
const error = o.error ?? console.error;
|
|
5958
|
+
const platform = o.platform ?? process.platform;
|
|
5482
5959
|
if (platform !== "darwin") {
|
|
5483
5960
|
error(`sleds musher --install needs launchd (macOS). On ${platform}, run \`sleds musher tick\` from cron every 5 minutes.`);
|
|
5484
5961
|
return 1;
|
|
5485
5962
|
}
|
|
5486
|
-
const env =
|
|
5487
|
-
const plist = musherPlistPath(
|
|
5963
|
+
const env = o.env ?? process.env;
|
|
5964
|
+
const plist = musherPlistPath(o.home, env);
|
|
5488
5965
|
const label = musherLabel(env);
|
|
5489
|
-
const dir = configDir(
|
|
5966
|
+
const dir = configDir(o.home, env);
|
|
5490
5967
|
mkdirSync9(dirname5(plist), { recursive: true });
|
|
5491
5968
|
mkdirSync9(dir, { recursive: true, mode: 448 });
|
|
5492
|
-
const execPath =
|
|
5969
|
+
const execPath = o.sledsArgv[0] ?? process.execPath;
|
|
5493
5970
|
const agentEnv = {
|
|
5494
|
-
PATH: musherPath(
|
|
5495
|
-
HOME:
|
|
5971
|
+
PATH: musherPath(o.home, execPath, env.PATH ?? ""),
|
|
5972
|
+
HOME: o.home,
|
|
5496
5973
|
...env.SLEDS_HOME ? { SLEDS_HOME: env.SLEDS_HOME } : {},
|
|
5497
5974
|
...env.CLAUDE_CONFIG_DIR ? { CLAUDE_CONFIG_DIR: env.CLAUDE_CONFIG_DIR } : {}
|
|
5498
5975
|
};
|
|
5499
|
-
const exec =
|
|
5976
|
+
const exec = o.exec ?? defaultExecSync;
|
|
5500
5977
|
if (existsSync10(plist))
|
|
5501
5978
|
exec("launchctl", ["unload", plist]);
|
|
5502
|
-
writeFileSync9(plist, buildMusherPlist({ sledsArgv:
|
|
5979
|
+
writeFileSync9(plist, buildMusherPlist({ sledsArgv: o.sledsArgv, env: agentEnv, logPath: musherLogPath(dir), label }));
|
|
5503
5980
|
const r = exec("launchctl", ["load", plist]);
|
|
5504
5981
|
if (!r.ok) {
|
|
5505
5982
|
error(`launchctl load failed: ${r.stderr.trim() || "unknown"}`);
|
|
5506
5983
|
return 1;
|
|
5507
5984
|
}
|
|
5508
|
-
const cfg = readConfig(
|
|
5509
|
-
writeConfig(
|
|
5510
|
-
const claude = findClaude2(
|
|
5985
|
+
const cfg = readConfig(o.home);
|
|
5986
|
+
writeConfig(o.home, { ...cfg, musher: { ...cfg.musher ?? {}, agent: "launchd" } });
|
|
5987
|
+
const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3);
|
|
5511
5988
|
log(`musher installed: launchd agent ${label}, every 5 min · log ${musherLogPath(dir)}`);
|
|
5512
5989
|
if (!claude)
|
|
5513
5990
|
log("warn: `claude` not found on PATH — set it with `sleds musher --claude /path/to/claude`");
|
|
5514
5991
|
return 0;
|
|
5515
5992
|
};
|
|
5516
|
-
var musherUninstall = (
|
|
5517
|
-
const log =
|
|
5518
|
-
const env =
|
|
5519
|
-
const plist = musherPlistPath(
|
|
5520
|
-
const exec =
|
|
5993
|
+
var musherUninstall = (o) => {
|
|
5994
|
+
const log = o.log ?? console.log;
|
|
5995
|
+
const env = o.env ?? process.env;
|
|
5996
|
+
const plist = musherPlistPath(o.home, env);
|
|
5997
|
+
const exec = o.exec ?? defaultExecSync;
|
|
5521
5998
|
if (existsSync10(plist)) {
|
|
5522
5999
|
exec("launchctl", ["unload", plist]);
|
|
5523
6000
|
rmSync2(plist, { force: true });
|
|
5524
6001
|
log(`musher removed: ${musherLabel(env)}`);
|
|
5525
6002
|
} else
|
|
5526
6003
|
log("musher was not installed.");
|
|
5527
|
-
const cfg = readConfig(
|
|
6004
|
+
const cfg = readConfig(o.home);
|
|
5528
6005
|
if (cfg.musher)
|
|
5529
|
-
writeConfig(
|
|
6006
|
+
writeConfig(o.home, { ...cfg, musher: { ...cfg.musher, agent: "none" } });
|
|
5530
6007
|
return 0;
|
|
5531
6008
|
};
|
|
5532
6009
|
var formatMusherStatus = (list, now) => {
|
|
@@ -5541,23 +6018,23 @@ var formatMusherStatus = (list, now) => {
|
|
|
5541
6018
|
lines.push("nothing deferred on this seat.");
|
|
5542
6019
|
return lines;
|
|
5543
6020
|
};
|
|
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(
|
|
6021
|
+
var clientFor = (o) => {
|
|
6022
|
+
if (o.client)
|
|
6023
|
+
return o.client;
|
|
6024
|
+
const src = meterSource(o.home);
|
|
6025
|
+
return src ? musherClient({ ...src, fetchFn: o.fetchFn }) : null;
|
|
6026
|
+
};
|
|
6027
|
+
var musherStatus = async (o) => {
|
|
6028
|
+
const log = o.log ?? console.log;
|
|
6029
|
+
const now = (o.now ?? Date.now)();
|
|
6030
|
+
const env = o.env ?? process.env;
|
|
6031
|
+
const plist = musherPlistPath(o.home, env);
|
|
6032
|
+
const cfg = readConfig(o.home);
|
|
5556
6033
|
log(existsSync10(plist) ? `agent ${musherLabel(env)} installed (${plist})` : "agent not installed — sleds musher --install");
|
|
5557
|
-
const claude = findClaude2(
|
|
6034
|
+
const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3);
|
|
5558
6035
|
log(`claude ${claude ?? "not found — sleds musher --claude /path/to/claude"}`);
|
|
5559
6036
|
log(`mode ${cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE} · timeout ${cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES} min`);
|
|
5560
|
-
const client = clientFor(
|
|
6037
|
+
const client = clientFor(o);
|
|
5561
6038
|
if (!client) {
|
|
5562
6039
|
log("seat not initialised (sleds init)");
|
|
5563
6040
|
return 1;
|
|
@@ -5571,9 +6048,9 @@ var musherStatus = async (o2) => {
|
|
|
5571
6048
|
log(line);
|
|
5572
6049
|
return 0;
|
|
5573
6050
|
};
|
|
5574
|
-
var musherTick = async (
|
|
5575
|
-
const log =
|
|
5576
|
-
const client = clientFor(
|
|
6051
|
+
var musherTick = async (o) => {
|
|
6052
|
+
const log = o.log ?? console.log;
|
|
6053
|
+
const client = clientFor(o);
|
|
5577
6054
|
if (!client) {
|
|
5578
6055
|
log("musher: seat not initialised; nothing to do");
|
|
5579
6056
|
return 1;
|
|
@@ -5585,19 +6062,19 @@ var musherTick = async (o2) => {
|
|
|
5585
6062
|
}
|
|
5586
6063
|
if (list.due.length === 0)
|
|
5587
6064
|
return 0;
|
|
5588
|
-
const cfg = readConfig(
|
|
5589
|
-
const claude = findClaude2(
|
|
5590
|
-
const seat = readSeatState(
|
|
6065
|
+
const cfg = readConfig(o.home);
|
|
6066
|
+
const claude = findClaude2(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich3) ?? "claude";
|
|
6067
|
+
const seat = readSeatState(o.home);
|
|
5591
6068
|
for (const job of list.due) {
|
|
5592
6069
|
const out = await runJob({
|
|
5593
6070
|
job,
|
|
5594
6071
|
client,
|
|
5595
6072
|
fallbackSession: seat.session,
|
|
5596
|
-
host:
|
|
6073
|
+
host: o.host ?? hostname3(),
|
|
5597
6074
|
claudePath: claude,
|
|
5598
6075
|
permissionMode: cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE,
|
|
5599
6076
|
timeoutMs: (cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES) * 60000,
|
|
5600
|
-
exec:
|
|
6077
|
+
exec: o.execCapture,
|
|
5601
6078
|
log
|
|
5602
6079
|
});
|
|
5603
6080
|
if (out.kind === "skipped")
|
|
@@ -5605,9 +6082,9 @@ var musherTick = async (o2) => {
|
|
|
5605
6082
|
}
|
|
5606
6083
|
return 0;
|
|
5607
6084
|
};
|
|
5608
|
-
var musherConfigure = (
|
|
5609
|
-
const log =
|
|
5610
|
-
const cfg = readConfig(
|
|
6085
|
+
var musherConfigure = (o, flags) => {
|
|
6086
|
+
const log = o.log ?? console.log;
|
|
6087
|
+
const cfg = readConfig(o.home);
|
|
5611
6088
|
const musher = { ...cfg.musher ?? {} };
|
|
5612
6089
|
if (flags.claude)
|
|
5613
6090
|
musher.claudePath = flags.claude;
|
|
@@ -5616,23 +6093,23 @@ var musherConfigure = (o2, flags) => {
|
|
|
5616
6093
|
if (flags.timeout) {
|
|
5617
6094
|
const n = Number.parseInt(flags.timeout, 10);
|
|
5618
6095
|
if (!Number.isFinite(n) || n <= 0) {
|
|
5619
|
-
(
|
|
6096
|
+
(o.error ?? console.error)("--timeout takes minutes, a positive number");
|
|
5620
6097
|
return 1;
|
|
5621
6098
|
}
|
|
5622
6099
|
musher.timeoutMinutes = n;
|
|
5623
6100
|
}
|
|
5624
|
-
writeConfig(
|
|
6101
|
+
writeConfig(o.home, { ...cfg, musher });
|
|
5625
6102
|
log(`musher settings: claude ${musher.claudePath ?? "(PATH)"} · mode ${musher.permissionMode ?? DEFAULT_PERMISSION_MODE} · timeout ${musher.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES} min`);
|
|
5626
6103
|
return 0;
|
|
5627
6104
|
};
|
|
5628
6105
|
var defaultExecSync = (cmd, args) => {
|
|
5629
|
-
const r =
|
|
6106
|
+
const r = spawnSync4(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
5630
6107
|
if (r.error)
|
|
5631
6108
|
return { ok: false, stderr: r.error.message };
|
|
5632
6109
|
return { ok: r.status === 0, stderr: r.stderr ?? "" };
|
|
5633
6110
|
};
|
|
5634
|
-
var musher = async (
|
|
5635
|
-
const args = [...
|
|
6111
|
+
var musher = async (o) => {
|
|
6112
|
+
const args = [...o.argv];
|
|
5636
6113
|
const flags = {};
|
|
5637
6114
|
const positional = [];
|
|
5638
6115
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -5648,16 +6125,16 @@ var musher = async (o2) => {
|
|
|
5648
6125
|
positional.push(a);
|
|
5649
6126
|
}
|
|
5650
6127
|
if (flags.install)
|
|
5651
|
-
return musherInstall(
|
|
6128
|
+
return musherInstall(o);
|
|
5652
6129
|
if (flags.uninstall)
|
|
5653
|
-
return musherUninstall(
|
|
6130
|
+
return musherUninstall(o);
|
|
5654
6131
|
if (flags.claude || flags.mode || flags.timeout)
|
|
5655
|
-
return musherConfigure(
|
|
6132
|
+
return musherConfigure(o, flags);
|
|
5656
6133
|
if (positional[0] === "tick")
|
|
5657
|
-
return musherTick(
|
|
6134
|
+
return musherTick(o);
|
|
5658
6135
|
if (flags.status || positional.length === 0)
|
|
5659
|
-
return musherStatus(
|
|
5660
|
-
(
|
|
6136
|
+
return musherStatus(o);
|
|
6137
|
+
(o.error ?? console.error)(`sleds musher: unknown argument ${positional[0]}`);
|
|
5661
6138
|
return 1;
|
|
5662
6139
|
};
|
|
5663
6140
|
|
|
@@ -5705,12 +6182,12 @@ var systemdCalendar = (deadline) => `${deadline.getFullYear()}-${pad2(deadline.g
|
|
|
5705
6182
|
var scheduleRevert = (d, deadlineMs) => {
|
|
5706
6183
|
const deadline = new Date(deadlineMs);
|
|
5707
6184
|
if (d.platform === "darwin") {
|
|
5708
|
-
const
|
|
6185
|
+
const p = plistPath(d.home);
|
|
5709
6186
|
try {
|
|
5710
6187
|
mkdirSync10(join9(d.home, "Library", "LaunchAgents"), { recursive: true });
|
|
5711
|
-
d.exec("launchctl", ["unload",
|
|
5712
|
-
writeFileSync10(
|
|
5713
|
-
const r = d.exec("launchctl", ["load",
|
|
6188
|
+
d.exec("launchctl", ["unload", p]);
|
|
6189
|
+
writeFileSync10(p, buildLaunchdPlist(d.sledsArgv, deadline));
|
|
6190
|
+
const r = d.exec("launchctl", ["load", p]);
|
|
5714
6191
|
if (r.ok)
|
|
5715
6192
|
return { kind: "launchd", detail: `launchd agent ${REVERT_LABEL}` };
|
|
5716
6193
|
return { kind: "manual", detail: `launchctl load failed: ${r.stderr.trim() || "unknown"}` };
|
|
@@ -5735,10 +6212,10 @@ var scheduleRevert = (d, deadlineMs) => {
|
|
|
5735
6212
|
};
|
|
5736
6213
|
var cancelRevert = (d, kind) => {
|
|
5737
6214
|
if (kind === "launchd" || d.platform === "darwin") {
|
|
5738
|
-
const
|
|
5739
|
-
if (existsSync11(
|
|
5740
|
-
d.exec("launchctl", ["unload",
|
|
5741
|
-
rmSync3(
|
|
6215
|
+
const p = plistPath(d.home);
|
|
6216
|
+
if (existsSync11(p)) {
|
|
6217
|
+
d.exec("launchctl", ["unload", p]);
|
|
6218
|
+
rmSync3(p, { force: true });
|
|
5742
6219
|
}
|
|
5743
6220
|
}
|
|
5744
6221
|
if (kind === "systemd" || d.platform === "linux") {
|
|
@@ -5748,15 +6225,15 @@ var cancelRevert = (d, kind) => {
|
|
|
5748
6225
|
|
|
5749
6226
|
// src/commands/off.ts
|
|
5750
6227
|
init_settings();
|
|
5751
|
-
var off = (
|
|
5752
|
-
const log =
|
|
5753
|
-
const cfg = readConfig(
|
|
5754
|
-
const settings = readSettings(
|
|
6228
|
+
var off = (o) => {
|
|
6229
|
+
const log = o.log ?? console.log;
|
|
6230
|
+
const cfg = readConfig(o.home);
|
|
6231
|
+
const settings = readSettings(o.home);
|
|
5755
6232
|
const wasOn = cfg.mode === "on" || isPointedAtProxy(settings);
|
|
5756
6233
|
if (wasOn)
|
|
5757
|
-
writeSettings(
|
|
5758
|
-
if (
|
|
5759
|
-
cancelRevert(
|
|
6234
|
+
writeSettings(o.home, switchOff(settings, cfg.priorEnv ?? {}));
|
|
6235
|
+
if (o.scheduler)
|
|
6236
|
+
cancelRevert(o.scheduler, cfg.schedule);
|
|
5760
6237
|
const next = {
|
|
5761
6238
|
...cfg,
|
|
5762
6239
|
mode: "off",
|
|
@@ -5766,7 +6243,7 @@ var off = (o2) => {
|
|
|
5766
6243
|
priorEnv: undefined,
|
|
5767
6244
|
schedule: undefined
|
|
5768
6245
|
};
|
|
5769
|
-
writeConfig(
|
|
6246
|
+
writeConfig(o.home, next);
|
|
5770
6247
|
if (!wasOn) {
|
|
5771
6248
|
log("Sleds already off. Claude Code is on your seat.");
|
|
5772
6249
|
return 0;
|
|
@@ -5785,19 +6262,19 @@ init_settings();
|
|
|
5785
6262
|
var DEFAULT_PROXY_URL2 = "https://proxy.sleds.ai";
|
|
5786
6263
|
|
|
5787
6264
|
// 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(
|
|
6265
|
+
var on = (o) => {
|
|
6266
|
+
const log = o.log ?? console.log;
|
|
6267
|
+
const error = o.error ?? console.error;
|
|
6268
|
+
const env = o.env ?? process.env;
|
|
6269
|
+
const now = (o.now ?? Date.now)();
|
|
6270
|
+
if (!hasClaudeCode(o.home)) {
|
|
6271
|
+
error(`Claude Code not found (${claudeDir(o.home)} does not exist). Install it, then re-run.`);
|
|
5795
6272
|
return 1;
|
|
5796
6273
|
}
|
|
5797
|
-
const cfg = readConfig(
|
|
5798
|
-
const token = cfg.token ?? readTokenFile(
|
|
6274
|
+
const cfg = readConfig(o.home);
|
|
6275
|
+
const token = cfg.token ?? readTokenFile(o.home) ?? env.SLEDS_TOKEN?.trim();
|
|
5799
6276
|
if (!token) {
|
|
5800
|
-
error(`No seat token in ${configPath(
|
|
6277
|
+
error(`No seat token in ${configPath(o.home)}. Run \`sleds init\` first.`);
|
|
5801
6278
|
return 2;
|
|
5802
6279
|
}
|
|
5803
6280
|
if (!isSeatToken(token)) {
|
|
@@ -5808,28 +6285,28 @@ var on = (o2) => {
|
|
|
5808
6285
|
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
6286
|
return 2;
|
|
5810
6287
|
}
|
|
5811
|
-
const url = (
|
|
6288
|
+
const url = (o.url ?? cfg.proxyUrl ?? env.SLEDS_PROXY_URL ?? DEFAULT_PROXY_URL2).replace(/\/+$/, "");
|
|
5812
6289
|
let untilResetMs;
|
|
5813
|
-
if (
|
|
6290
|
+
if (o.untilReset !== undefined) {
|
|
5814
6291
|
try {
|
|
5815
|
-
untilResetMs = parseUntilReset(
|
|
6292
|
+
untilResetMs = parseUntilReset(o.untilReset, now);
|
|
5816
6293
|
} catch (err) {
|
|
5817
6294
|
error(err instanceof Error ? err.message : String(err));
|
|
5818
6295
|
return 2;
|
|
5819
6296
|
}
|
|
5820
6297
|
}
|
|
5821
|
-
const settings = readSettings(
|
|
6298
|
+
const settings = readSettings(o.home);
|
|
5822
6299
|
const priorEnv = cfg.mode === "on" && cfg.priorEnv ? cfg.priorEnv : capturePriorEnv(settings);
|
|
5823
|
-
const backedUp = backupSettings(
|
|
5824
|
-
writeSettings(
|
|
6300
|
+
const backedUp = backupSettings(o.home);
|
|
6301
|
+
writeSettings(o.home, switchOn(settings, { url, token }));
|
|
5825
6302
|
let schedule;
|
|
5826
6303
|
let scheduleNote;
|
|
5827
|
-
if (untilResetMs !== undefined && !
|
|
6304
|
+
if (untilResetMs !== undefined && !o.noSchedule && o.scheduler && hasHomeOverride(env)) {
|
|
5828
6305
|
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
6306
|
return 2;
|
|
5830
6307
|
}
|
|
5831
|
-
if (untilResetMs !== undefined && !
|
|
5832
|
-
const r = scheduleRevert(
|
|
6308
|
+
if (untilResetMs !== undefined && !o.noSchedule && o.scheduler) {
|
|
6309
|
+
const r = scheduleRevert(o.scheduler, untilResetMs);
|
|
5833
6310
|
schedule = r.kind;
|
|
5834
6311
|
scheduleNote = r.kind === "manual" ? r.detail : undefined;
|
|
5835
6312
|
}
|
|
@@ -5845,19 +6322,19 @@ var on = (o2) => {
|
|
|
5845
6322
|
next.untilReset = new Date(untilResetMs).toISOString();
|
|
5846
6323
|
else
|
|
5847
6324
|
next.untilReset = undefined;
|
|
5848
|
-
next.reason =
|
|
6325
|
+
next.reason = o.reason?.trim() || undefined;
|
|
5849
6326
|
next.schedule = schedule;
|
|
5850
|
-
writeConfig(
|
|
6327
|
+
writeConfig(o.home, next);
|
|
5851
6328
|
log(`Sleds on. Claude Code now runs on the platform key through ${url}. Restart claude to apply.`);
|
|
5852
6329
|
if (untilResetMs !== undefined) {
|
|
5853
|
-
log(`Reverts at ${formatLocal(untilResetMs,
|
|
6330
|
+
log(`Reverts at ${formatLocal(untilResetMs, o.tz)} — run \`sleds off\` earlier if you want.`);
|
|
5854
6331
|
if (schedule === undefined || schedule === "manual") {
|
|
5855
6332
|
const why = scheduleNote ? ` (${scheduleNote})` : "";
|
|
5856
6333
|
log(`Auto-revert not scheduled${why}. Run \`sleds off\` after the reset.`);
|
|
5857
6334
|
}
|
|
5858
6335
|
}
|
|
5859
6336
|
if (backedUp)
|
|
5860
|
-
log(`Backed up ${settingsPath(
|
|
6337
|
+
log(`Backed up ${settingsPath(o.home)} to ${backupPath(o.home)}.`);
|
|
5861
6338
|
return 0;
|
|
5862
6339
|
};
|
|
5863
6340
|
|
|
@@ -5905,22 +6382,22 @@ var exitsLine = (hasUpstreamKey) => hasUpstreamKey ? "`/sled handoff <what you w
|
|
|
5905
6382
|
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
6383
|
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
6384
|
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 (
|
|
6385
|
+
var promptEvent = async (o) => {
|
|
6386
|
+
const log = o.log ?? console.log;
|
|
6387
|
+
const error = o.error ?? console.error;
|
|
6388
|
+
const now = (o.now ?? Date.now)();
|
|
6389
|
+
if (o.stdin === undefined)
|
|
5913
6390
|
await readStdin(HOOK_STDIN_TIMEOUT_MS);
|
|
5914
6391
|
let state;
|
|
5915
6392
|
try {
|
|
5916
|
-
state = readSeatState(
|
|
6393
|
+
state = readSeatState(o.home);
|
|
5917
6394
|
} catch {
|
|
5918
6395
|
return 0;
|
|
5919
6396
|
}
|
|
5920
6397
|
const pending = untold(state);
|
|
5921
6398
|
if (pending.length === 0)
|
|
5922
6399
|
return 0;
|
|
5923
|
-
const hasKey = readConfig(
|
|
6400
|
+
const hasKey = readConfig(o.home).org?.capabilities.upstreamKey !== false;
|
|
5924
6401
|
const told = { ...state.told ?? {} };
|
|
5925
6402
|
const mark = (bar, stage, at) => {
|
|
5926
6403
|
told[bar] = { ...told[bar] ?? {}, [stage]: at };
|
|
@@ -5940,7 +6417,7 @@ var promptEvent = async (o2) => {
|
|
|
5940
6417
|
lines.push(e.stage === "approaching" ? approachingContext(e.bar, pct, e.resetsAt, hasKey, now) : hitContext(e.bar, e.resetsAt, hasKey, now));
|
|
5941
6418
|
}
|
|
5942
6419
|
try {
|
|
5943
|
-
writeSeatState(
|
|
6420
|
+
writeSeatState(o.home, { ...state, told });
|
|
5944
6421
|
} catch {}
|
|
5945
6422
|
if (code === 0 && lines.length > 0)
|
|
5946
6423
|
log(lines.join(`
|
|
@@ -5951,17 +6428,17 @@ var promptEvent = async (o2) => {
|
|
|
5951
6428
|
// src/commands/run.ts
|
|
5952
6429
|
init_config();
|
|
5953
6430
|
init_meter();
|
|
5954
|
-
import { hostname as
|
|
5955
|
-
var run = async (
|
|
5956
|
-
const log =
|
|
5957
|
-
const error =
|
|
5958
|
-
if (!
|
|
6431
|
+
import { hostname as hostname4 } from "node:os";
|
|
6432
|
+
var run = async (o) => {
|
|
6433
|
+
const log = o.log ?? console.log;
|
|
6434
|
+
const error = o.error ?? console.error;
|
|
6435
|
+
if (!o.jobId) {
|
|
5959
6436
|
error("usage: sleds run <jobId>");
|
|
5960
6437
|
return 2;
|
|
5961
6438
|
}
|
|
5962
|
-
const client =
|
|
5963
|
-
const src = meterSource(
|
|
5964
|
-
return src ? musherClient({ ...src, fetchFn:
|
|
6439
|
+
const client = o.client ?? (() => {
|
|
6440
|
+
const src = meterSource(o.home);
|
|
6441
|
+
return src ? musherClient({ ...src, fetchFn: o.fetchFn }) : null;
|
|
5965
6442
|
})();
|
|
5966
6443
|
if (!client) {
|
|
5967
6444
|
error("No seat on this machine. Run `sleds init` first.");
|
|
@@ -5972,13 +6449,13 @@ var run = async (o2) => {
|
|
|
5972
6449
|
error("Could not reach the SLEDS proxy.");
|
|
5973
6450
|
return 1;
|
|
5974
6451
|
}
|
|
5975
|
-
const job = [...list.due, ...list.upcoming].find((j) => j.jobId ===
|
|
6452
|
+
const job = [...list.due, ...list.upcoming].find((j) => j.jobId === o.jobId);
|
|
5976
6453
|
if (!job) {
|
|
5977
|
-
error(`Job ${
|
|
6454
|
+
error(`Job ${o.jobId} is not a scheduled local job on this seat (sleds musher --status lists them).`);
|
|
5978
6455
|
return 1;
|
|
5979
6456
|
}
|
|
5980
|
-
const cfg = readConfig(
|
|
5981
|
-
const claude = findClaude(
|
|
6457
|
+
const cfg = readConfig(o.home);
|
|
6458
|
+
const claude = findClaude(o.home, cfg.musher?.claudePath, o.which ?? defaultWhich);
|
|
5982
6459
|
if (!claude) {
|
|
5983
6460
|
error("`claude` not found. Install Claude Code, or `sleds musher --claude /path/to/claude`.");
|
|
5984
6461
|
return 1;
|
|
@@ -5986,12 +6463,12 @@ var run = async (o2) => {
|
|
|
5986
6463
|
const out = await runJob({
|
|
5987
6464
|
job,
|
|
5988
6465
|
client,
|
|
5989
|
-
fallbackSession: readSeatState(
|
|
5990
|
-
host:
|
|
6466
|
+
fallbackSession: readSeatState(o.home).session,
|
|
6467
|
+
host: o.host ?? hostname4(),
|
|
5991
6468
|
claudePath: claude,
|
|
5992
6469
|
permissionMode: cfg.musher?.permissionMode ?? DEFAULT_PERMISSION_MODE,
|
|
5993
6470
|
timeoutMs: (cfg.musher?.timeoutMinutes ?? DEFAULT_TIMEOUT_MINUTES) * 60000,
|
|
5994
|
-
exec:
|
|
6471
|
+
exec: o.execCapture,
|
|
5995
6472
|
log
|
|
5996
6473
|
});
|
|
5997
6474
|
if (out.kind === "skipped") {
|
|
@@ -6024,25 +6501,25 @@ var notificationType = (text) => {
|
|
|
6024
6501
|
return;
|
|
6025
6502
|
}
|
|
6026
6503
|
};
|
|
6027
|
-
var seatEvent = async (
|
|
6028
|
-
const log =
|
|
6029
|
-
const now = (
|
|
6030
|
-
const input =
|
|
6504
|
+
var seatEvent = async (o) => {
|
|
6505
|
+
const log = o.log ?? console.log;
|
|
6506
|
+
const now = (o.now ?? Date.now)();
|
|
6507
|
+
const input = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS2);
|
|
6031
6508
|
const kind = notificationType(input);
|
|
6032
6509
|
try {
|
|
6033
|
-
const state = readSeatState(
|
|
6510
|
+
const state = readSeatState(o.home);
|
|
6034
6511
|
const week = state.window?.sevenDay;
|
|
6035
|
-
const src = meterSource(
|
|
6512
|
+
const src = meterSource(o.home);
|
|
6036
6513
|
if (src) {
|
|
6037
|
-
await postSeatWall({ ...src, fetchFn:
|
|
6514
|
+
await postSeatWall({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, {
|
|
6038
6515
|
...week ? { resetsAt: week.resetsAt, usedPct: week.usedPct } : {},
|
|
6039
6516
|
...kind ? { notification: kind } : {}
|
|
6040
6517
|
});
|
|
6041
6518
|
}
|
|
6042
6519
|
if (week && week.resetsAt > now)
|
|
6043
|
-
writeSeatState(
|
|
6520
|
+
writeSeatState(o.home, { ...state, lastWallResetsAt: week.resetsAt });
|
|
6044
6521
|
} catch {}
|
|
6045
|
-
const line = seatWallLine(readConfig(
|
|
6522
|
+
const line = seatWallLine(readConfig(o.home).org?.capabilities.upstreamKey !== false);
|
|
6046
6523
|
log(line);
|
|
6047
6524
|
return 0;
|
|
6048
6525
|
};
|
|
@@ -6050,7 +6527,7 @@ var seatEvent = async (o2) => {
|
|
|
6050
6527
|
// src/commands/seatSync.ts
|
|
6051
6528
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
6052
6529
|
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";
|
|
6530
|
+
import { platform as platform2 } from "node:os";
|
|
6054
6531
|
import { join as join11 } from "node:path";
|
|
6055
6532
|
|
|
6056
6533
|
// src/cookieDb.ts
|
|
@@ -6062,11 +6539,11 @@ var isBun = () => typeof globalThis.Bun !== "undefined";
|
|
|
6062
6539
|
async function queryRows(dbPath) {
|
|
6063
6540
|
if (isBun()) {
|
|
6064
6541
|
const { Database } = await import("bun:sqlite");
|
|
6065
|
-
const
|
|
6542
|
+
const conn = new Database(dbPath, { readonly: true });
|
|
6066
6543
|
try {
|
|
6067
|
-
return
|
|
6544
|
+
return conn.query(QUERY).all();
|
|
6068
6545
|
} finally {
|
|
6069
|
-
|
|
6546
|
+
conn.close();
|
|
6070
6547
|
}
|
|
6071
6548
|
}
|
|
6072
6549
|
const mod = nodeSqlite();
|
|
@@ -6177,9 +6654,9 @@ function resolveProfileDir(localStateJson, profile) {
|
|
|
6177
6654
|
if (localStateJson) {
|
|
6178
6655
|
try {
|
|
6179
6656
|
const raw = JSON.parse(localStateJson);
|
|
6180
|
-
const
|
|
6181
|
-
if (
|
|
6182
|
-
cache =
|
|
6657
|
+
const p = isObject11(raw) && isObject11(raw.profile) ? raw.profile : null;
|
|
6658
|
+
if (p && isObject11(p.info_cache))
|
|
6659
|
+
cache = p.info_cache;
|
|
6183
6660
|
} catch {}
|
|
6184
6661
|
}
|
|
6185
6662
|
if (Object.hasOwn(cache, profile))
|
|
@@ -6370,11 +6847,11 @@ function findFile(root, name) {
|
|
|
6370
6847
|
continue;
|
|
6371
6848
|
}
|
|
6372
6849
|
for (const e of entries) {
|
|
6373
|
-
const
|
|
6850
|
+
const p = join11(dir, e.name);
|
|
6374
6851
|
if (e.isDirectory())
|
|
6375
|
-
stack.push(
|
|
6852
|
+
stack.push(p);
|
|
6376
6853
|
else if (e.name === name)
|
|
6377
|
-
return
|
|
6854
|
+
return p;
|
|
6378
6855
|
}
|
|
6379
6856
|
}
|
|
6380
6857
|
return null;
|
|
@@ -6385,10 +6862,10 @@ function findCookieDb(home, source) {
|
|
|
6385
6862
|
if (!existsSync12(root))
|
|
6386
6863
|
throw new Error(`no ${source.browser} data at ${root} (is it installed?)`);
|
|
6387
6864
|
if (!b.profiled) {
|
|
6388
|
-
const
|
|
6389
|
-
if (!
|
|
6865
|
+
const db = findFile(root, "Cookies");
|
|
6866
|
+
if (!db)
|
|
6390
6867
|
throw new Error(`no cookie store under ${root}`);
|
|
6391
|
-
return
|
|
6868
|
+
return db;
|
|
6392
6869
|
}
|
|
6393
6870
|
const localStatePath = join11(root, "Local State");
|
|
6394
6871
|
const localState = existsSync12(localStatePath) ? readFileSync10(localStatePath, "utf8") : null;
|
|
@@ -6398,7 +6875,7 @@ function findCookieDb(home, source) {
|
|
|
6398
6875
|
throw new Error(`"${wanted}" names ${ambiguous.length} ${source.browser} profiles (${ambiguous.join(", ")}) — pass the directory or the account email instead`);
|
|
6399
6876
|
if (!dir)
|
|
6400
6877
|
throw new Error(`no ${source.browser} profile named "${wanted}" (check chrome://version)`);
|
|
6401
|
-
const db = cookieDbCandidates(join11(root, dir)).find((
|
|
6878
|
+
const db = cookieDbCandidates(join11(root, dir)).find((p) => existsSync12(p));
|
|
6402
6879
|
if (!db)
|
|
6403
6880
|
throw new Error(`profile "${dir}" has no cookie store yet (open claude.ai in it once)`);
|
|
6404
6881
|
return db;
|
|
@@ -6429,8 +6906,8 @@ var curlUsage = (org, cookie) => {
|
|
|
6429
6906
|
return { status: 0, body: "" };
|
|
6430
6907
|
}
|
|
6431
6908
|
};
|
|
6432
|
-
function fetchSeatUsage(jar, org, now,
|
|
6433
|
-
const { status, body } =
|
|
6909
|
+
function fetchSeatUsage(jar, org, now, run) {
|
|
6910
|
+
const { status, body } = run(org, cookieHeader(jar, org));
|
|
6434
6911
|
if (status !== 200)
|
|
6435
6912
|
return { usage: null, status };
|
|
6436
6913
|
try {
|
|
@@ -6491,24 +6968,24 @@ function uninstall(home, source, log) {
|
|
|
6491
6968
|
log(existsSync12(path) ? `seat-sync timer removed (${path} can be deleted)` : "no seat-sync timer");
|
|
6492
6969
|
return 0;
|
|
6493
6970
|
}
|
|
6494
|
-
var seatSync = async (
|
|
6495
|
-
const log =
|
|
6496
|
-
const now = (
|
|
6497
|
-
const parsed = parseSeatSyncArgs(
|
|
6971
|
+
var seatSync = async (o) => {
|
|
6972
|
+
const log = o.log ?? console.log;
|
|
6973
|
+
const now = (o.now ?? Date.now)();
|
|
6974
|
+
const parsed = parseSeatSyncArgs(o.argv ?? process.argv.slice(3));
|
|
6498
6975
|
if ("error" in parsed) {
|
|
6499
6976
|
log(`seat-sync: ${parsed.error}`);
|
|
6500
6977
|
return 1;
|
|
6501
6978
|
}
|
|
6502
6979
|
const { source } = parsed;
|
|
6503
6980
|
if (source.uninstall)
|
|
6504
|
-
return uninstall(
|
|
6981
|
+
return uninstall(o.home, source, log);
|
|
6505
6982
|
if (source.install)
|
|
6506
|
-
return install(
|
|
6507
|
-
if (
|
|
6983
|
+
return install(o.home, [process.execPath, process.argv[1] ?? ""], source, log);
|
|
6984
|
+
if (platform2() !== "darwin" && !o.readCookieRows) {
|
|
6508
6985
|
log("seat-sync is macOS-only for now.");
|
|
6509
6986
|
return 2;
|
|
6510
6987
|
}
|
|
6511
|
-
const saved = meterSource(
|
|
6988
|
+
const saved = meterSource(o.home);
|
|
6512
6989
|
const proxyUrl = saved?.proxyUrl;
|
|
6513
6990
|
const token = source.token ?? saved?.token;
|
|
6514
6991
|
if (!proxyUrl || !token) {
|
|
@@ -6517,13 +6994,13 @@ var seatSync = async (o2) => {
|
|
|
6517
6994
|
}
|
|
6518
6995
|
let rows;
|
|
6519
6996
|
try {
|
|
6520
|
-
rows =
|
|
6997
|
+
rows = o.readCookieRows ? o.readCookieRows(o.home, source) : await readCookieRowsBun(o.home, source);
|
|
6521
6998
|
} catch (e) {
|
|
6522
6999
|
log(`seat-sync: ${e instanceof Error ? e.message : String(e)}`);
|
|
6523
7000
|
return 1;
|
|
6524
7001
|
}
|
|
6525
7002
|
const service = BROWSERS[source.browser].keychainService;
|
|
6526
|
-
const pw = (
|
|
7003
|
+
const pw = (o.safeStorageKey ?? keychainKey)(service);
|
|
6527
7004
|
if (!pw) {
|
|
6528
7005
|
log(`seat-sync: could not read the "${service}" key from your Keychain.`);
|
|
6529
7006
|
return 1;
|
|
@@ -6543,16 +7020,16 @@ var seatSync = async (o2) => {
|
|
|
6543
7020
|
log("seat-sync: no active org in the session cookie (pass --org <uuid>).");
|
|
6544
7021
|
return 1;
|
|
6545
7022
|
}
|
|
6546
|
-
const { usage, status } = fetchSeatUsage(jar, org, now,
|
|
7023
|
+
const { usage, status } = fetchSeatUsage(jar, org, now, o.runUsage ?? curlUsage);
|
|
6547
7024
|
if (!usage) {
|
|
6548
7025
|
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
7026
|
log(`seat-sync: usage read failed — ${why}`);
|
|
6550
7027
|
return 2;
|
|
6551
7028
|
}
|
|
6552
7029
|
try {
|
|
6553
|
-
writeSeatState(
|
|
7030
|
+
writeSeatState(o.home, { ...readSeatState(o.home), window: usageAsWindow(usage) });
|
|
6554
7031
|
} catch {}
|
|
6555
|
-
const posted = await postSeat(proxyUrl, token, seatSyncBody(usage),
|
|
7032
|
+
const posted = await postSeat(proxyUrl, token, seatSyncBody(usage), o.fetchFn ?? fetch);
|
|
6556
7033
|
const wk = usage.sevenDay ? `wk ${Math.round(usage.sevenDay.usedPct)}%` : "wk —";
|
|
6557
7034
|
const models = Object.keys(usage.models).length;
|
|
6558
7035
|
const credits = usage.usedCreditsUsd !== undefined ? ` · $${usage.usedCreditsUsd.toFixed(2)}` : "";
|
|
@@ -6604,14 +7081,14 @@ function sessionContext(a) {
|
|
|
6604
7081
|
return lines.join(`
|
|
6605
7082
|
`);
|
|
6606
7083
|
}
|
|
6607
|
-
var sessionEvent = async (
|
|
6608
|
-
const log =
|
|
6609
|
-
const now = (
|
|
6610
|
-
const raw =
|
|
7084
|
+
var sessionEvent = async (o) => {
|
|
7085
|
+
const log = o.log ?? console.log;
|
|
7086
|
+
const now = (o.now ?? Date.now)();
|
|
7087
|
+
const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
|
|
6611
7088
|
const payload = parseSessionPayload(raw);
|
|
6612
7089
|
if (payload.session_id) {
|
|
6613
7090
|
try {
|
|
6614
|
-
writeSession(
|
|
7091
|
+
writeSession(o.home, {
|
|
6615
7092
|
sessionId: payload.session_id,
|
|
6616
7093
|
startedAt: now,
|
|
6617
7094
|
...payload.source ? { source: payload.source } : {}
|
|
@@ -6621,13 +7098,13 @@ var sessionEvent = async (o2) => {
|
|
|
6621
7098
|
let email;
|
|
6622
7099
|
let org;
|
|
6623
7100
|
try {
|
|
6624
|
-
const cfg = readConfig(
|
|
7101
|
+
const cfg = readConfig(o.home);
|
|
6625
7102
|
email = cfg.email;
|
|
6626
7103
|
org = cfg.org?.slug;
|
|
6627
7104
|
} catch {}
|
|
6628
7105
|
let owed = [];
|
|
6629
7106
|
try {
|
|
6630
|
-
owed = readOwed(
|
|
7107
|
+
owed = readOwed(o.home, now);
|
|
6631
7108
|
} catch {
|
|
6632
7109
|
owed = [];
|
|
6633
7110
|
}
|
|
@@ -6672,13 +7149,13 @@ function parseHookInput(raw) {
|
|
|
6672
7149
|
}
|
|
6673
7150
|
if (typeof parsed !== "object" || parsed === null)
|
|
6674
7151
|
return null;
|
|
6675
|
-
const
|
|
6676
|
-
const toolName = typeof
|
|
6677
|
-
const input = typeof
|
|
7152
|
+
const p = parsed;
|
|
7153
|
+
const toolName = typeof p.tool_name === "string" ? p.tool_name : "";
|
|
7154
|
+
const input = typeof p.tool_input === "object" && p.tool_input !== null ? p.tool_input : {};
|
|
6678
7155
|
const filePath = typeof input.file_path === "string" ? input.file_path : "";
|
|
6679
7156
|
if (!filePath)
|
|
6680
7157
|
return null;
|
|
6681
|
-
const cwd = typeof
|
|
7158
|
+
const cwd = typeof p.cwd === "string" ? p.cwd : "";
|
|
6682
7159
|
return { toolName, filePath, cwd };
|
|
6683
7160
|
}
|
|
6684
7161
|
function projectKey(filePath, cwd) {
|
|
@@ -6698,16 +7175,16 @@ function readFileOrNull(path) {
|
|
|
6698
7175
|
return null;
|
|
6699
7176
|
}
|
|
6700
7177
|
}
|
|
6701
|
-
var specEvent = async (
|
|
6702
|
-
const log =
|
|
6703
|
-
const raw =
|
|
7178
|
+
var specEvent = async (o) => {
|
|
7179
|
+
const log = o.log ?? console.log;
|
|
7180
|
+
const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS3);
|
|
6704
7181
|
const input = raw ? parseHookInput(raw) : null;
|
|
6705
7182
|
if (!input || !isSpecPath(input.filePath))
|
|
6706
7183
|
return 0;
|
|
6707
|
-
const src = meterSource(
|
|
7184
|
+
const src = meterSource(o.home);
|
|
6708
7185
|
if (!src)
|
|
6709
7186
|
return 0;
|
|
6710
|
-
const body = (
|
|
7187
|
+
const body = (o.readFile ?? readFileOrNull)(input.filePath);
|
|
6711
7188
|
if (!body || !looksLikeSpec(body))
|
|
6712
7189
|
return 0;
|
|
6713
7190
|
const project = projectKey(input.filePath, input.cwd);
|
|
@@ -6717,7 +7194,7 @@ var specEvent = async (o2) => {
|
|
|
6717
7194
|
repoHint: input.cwd ? basename2(input.cwd) : undefined
|
|
6718
7195
|
};
|
|
6719
7196
|
try {
|
|
6720
|
-
const res = await (
|
|
7197
|
+
const res = await (o.fetchFn ?? fetch)(`${src.proxyUrl}/proxy/estimate`, {
|
|
6721
7198
|
method: "POST",
|
|
6722
7199
|
headers: { authorization: `Bearer ${src.token}`, "content-type": "application/json" },
|
|
6723
7200
|
body: JSON.stringify({ spec, project }),
|
|
@@ -6732,15 +7209,15 @@ var specEvent = async (o2) => {
|
|
|
6732
7209
|
log(`sleds: ${project} sized at $${b.lowUsd.toFixed(2)}–${b.highUsd.toFixed(2)} · ${min(b.lowMinutes)}–${min(b.highMinutes)} min`);
|
|
6733
7210
|
if (typeof b.lowMinutes === "number" && typeof b.highMinutes === "number") {
|
|
6734
7211
|
try {
|
|
6735
|
-
writeSeatState(
|
|
6736
|
-
...readSeatState(
|
|
7212
|
+
writeSeatState(o.home, {
|
|
7213
|
+
...readSeatState(o.home),
|
|
6737
7214
|
estimate: {
|
|
6738
7215
|
project,
|
|
6739
7216
|
lowUsd: b.lowUsd,
|
|
6740
7217
|
highUsd: b.highUsd,
|
|
6741
7218
|
lowMin: b.lowMinutes,
|
|
6742
7219
|
highMin: b.highMinutes,
|
|
6743
|
-
at: (
|
|
7220
|
+
at: (o.now ?? Date.now)()
|
|
6744
7221
|
}
|
|
6745
7222
|
});
|
|
6746
7223
|
} catch {}
|
|
@@ -6823,16 +7300,16 @@ var formatStatus = (s) => {
|
|
|
6823
7300
|
}
|
|
6824
7301
|
return lines;
|
|
6825
7302
|
};
|
|
6826
|
-
var status = async (
|
|
6827
|
-
const log =
|
|
6828
|
-
const nowMs = (
|
|
6829
|
-
const cfg = readConfig(
|
|
6830
|
-
const settings = existsSync13(settingsPath(
|
|
7303
|
+
var status = async (o) => {
|
|
7304
|
+
const log = o.log ?? console.log;
|
|
7305
|
+
const nowMs = (o.now ?? Date.now)();
|
|
7306
|
+
const cfg = readConfig(o.home);
|
|
7307
|
+
const settings = existsSync13(settingsPath(o.home)) ? readSettings(o.home) : {};
|
|
6831
7308
|
const inSettings = readProxy(settings);
|
|
6832
7309
|
const proxyUrl = inSettings?.url ?? cfg.proxyUrl;
|
|
6833
7310
|
const token = inSettings?.token ?? cfg.token;
|
|
6834
7311
|
if (!proxyUrl && !token && cfg.mode === undefined) {
|
|
6835
|
-
log(`SLEDS is not set up (${configPath(
|
|
7312
|
+
log(`SLEDS is not set up (${configPath(o.home)} and ${settingsPath(o.home)}).`);
|
|
6836
7313
|
log("Run `sleds init` first.");
|
|
6837
7314
|
return 1;
|
|
6838
7315
|
}
|
|
@@ -6840,8 +7317,8 @@ var status = async (o2) => {
|
|
|
6840
7317
|
mode: cfg.mode ?? "off",
|
|
6841
7318
|
pointed: inSettings !== null,
|
|
6842
7319
|
nowMs,
|
|
6843
|
-
settingsFile: settingsPath(
|
|
6844
|
-
configFile: configPath(
|
|
7320
|
+
settingsFile: settingsPath(o.home),
|
|
7321
|
+
configFile: configPath(o.home)
|
|
6845
7322
|
};
|
|
6846
7323
|
if (proxyUrl)
|
|
6847
7324
|
info.proxyUrl = proxyUrl;
|
|
@@ -6861,8 +7338,8 @@ var status = async (o2) => {
|
|
|
6861
7338
|
};
|
|
6862
7339
|
if (cfg.reason)
|
|
6863
7340
|
info.reason = cfg.reason;
|
|
6864
|
-
if (
|
|
6865
|
-
info.tz =
|
|
7341
|
+
if (o.tz)
|
|
7342
|
+
info.tz = o.tz;
|
|
6866
7343
|
const since = cfg.since ? Date.parse(cfg.since) : Number.NaN;
|
|
6867
7344
|
if (!Number.isNaN(since))
|
|
6868
7345
|
info.sinceMs = since;
|
|
@@ -6871,8 +7348,8 @@ var status = async (o2) => {
|
|
|
6871
7348
|
info.untilResetMs = until;
|
|
6872
7349
|
if (proxyUrl) {
|
|
6873
7350
|
const [health, proxy] = await Promise.all([
|
|
6874
|
-
checkHealth2(proxyUrl,
|
|
6875
|
-
fetchProxyStatus(proxyUrl,
|
|
7351
|
+
checkHealth2(proxyUrl, o.fetchFn, o.timeoutMs),
|
|
7352
|
+
fetchProxyStatus(proxyUrl, o.fetchFn, o.timeoutMs)
|
|
6876
7353
|
]);
|
|
6877
7354
|
info.health = health;
|
|
6878
7355
|
info.proxy = proxy;
|
|
@@ -6885,62 +7362,62 @@ var status = async (o2) => {
|
|
|
6885
7362
|
// src/commands/statusline.ts
|
|
6886
7363
|
init_config();
|
|
6887
7364
|
init_meter();
|
|
6888
|
-
var statusline = async (
|
|
6889
|
-
const log =
|
|
6890
|
-
const now = (
|
|
6891
|
-
const input =
|
|
7365
|
+
var statusline = async (o) => {
|
|
7366
|
+
const log = o.log ?? console.log;
|
|
7367
|
+
const now = (o.now ?? Date.now)();
|
|
7368
|
+
const input = o.stdin ?? await readStdin(STDIN_TIMEOUT_MS);
|
|
6892
7369
|
const window = seatWindowFromStdin(input, now);
|
|
6893
7370
|
const session = seatSessionFromStdin(input, now);
|
|
6894
7371
|
let posts = [];
|
|
6895
7372
|
let events = [];
|
|
6896
7373
|
try {
|
|
6897
7374
|
if (window)
|
|
6898
|
-
({ posts, events } = rememberSeat(
|
|
7375
|
+
({ posts, events } = rememberSeat(o, window, session, now, input));
|
|
6899
7376
|
else {
|
|
6900
|
-
const st = readSeatState(
|
|
6901
|
-
const credits = creditsEvent(
|
|
7377
|
+
const st = readSeatState(o.home);
|
|
7378
|
+
const credits = creditsEvent(o, st, input, now);
|
|
6902
7379
|
if (session || credits.scan) {
|
|
6903
7380
|
const next = {
|
|
6904
7381
|
...st,
|
|
6905
7382
|
...session ? { session } : {},
|
|
6906
7383
|
...credits.scan ? { transcriptScan: credits.scan } : {}
|
|
6907
7384
|
};
|
|
6908
|
-
writeSeatState(
|
|
7385
|
+
writeSeatState(o.home, credits.event ? markWarned(next, [credits.event]) : next);
|
|
6909
7386
|
}
|
|
6910
7387
|
if (credits.event) {
|
|
6911
7388
|
events = [credits.event];
|
|
6912
|
-
posts = announce(
|
|
7389
|
+
posts = announce(o, events);
|
|
6913
7390
|
}
|
|
6914
7391
|
}
|
|
6915
7392
|
} catch {}
|
|
6916
7393
|
const seatFragment = formatSeatFragment(window, now);
|
|
6917
7394
|
let line;
|
|
6918
7395
|
try {
|
|
6919
|
-
const
|
|
6920
|
-
home:
|
|
7396
|
+
const meter = await loadMeter({
|
|
7397
|
+
home: o.home,
|
|
6921
7398
|
now,
|
|
6922
|
-
fetchFn:
|
|
6923
|
-
timeoutMs:
|
|
7399
|
+
fetchFn: o.fetchFn,
|
|
7400
|
+
timeoutMs: o.timeoutMs
|
|
6924
7401
|
});
|
|
6925
|
-
line = formatMeterLine(
|
|
7402
|
+
line = formatMeterLine(meter, now, seatFragment);
|
|
6926
7403
|
} catch {
|
|
6927
7404
|
line = formatMeterLine(null, now, seatFragment);
|
|
6928
7405
|
}
|
|
6929
7406
|
log(line);
|
|
6930
|
-
posts.push(speakToCockpit(
|
|
7407
|
+
posts.push(speakToCockpit(o, window, events, now));
|
|
6931
7408
|
await Promise.allSettled(posts);
|
|
6932
7409
|
return 0;
|
|
6933
7410
|
};
|
|
6934
|
-
var speakToCockpit = async (
|
|
6935
|
-
const cockpit = detectCockpit(
|
|
7411
|
+
var speakToCockpit = async (o, window, events, now) => {
|
|
7412
|
+
const cockpit = detectCockpit(o.env ?? process.env);
|
|
6936
7413
|
if (!cockpit)
|
|
6937
7414
|
return;
|
|
6938
7415
|
try {
|
|
6939
|
-
const st = readSeatState(
|
|
7416
|
+
const st = readSeatState(o.home);
|
|
6940
7417
|
let feed;
|
|
6941
|
-
const src = meterSource(
|
|
7418
|
+
const src = meterSource(o.home);
|
|
6942
7419
|
if (src && now - (st.cockpit?.feed?.at ?? 0) >= FEED_MIN_INTERVAL_MS) {
|
|
6943
|
-
const got = await fetchFeed({ ...src, email: readConfig(
|
|
7420
|
+
const got = await fetchFeed({ ...src, email: readConfig(o.home).email, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs }, now);
|
|
6944
7421
|
if (got)
|
|
6945
7422
|
feed = got;
|
|
6946
7423
|
}
|
|
@@ -6951,32 +7428,32 @@ var speakToCockpit = async (o2, window, events, now) => {
|
|
|
6951
7428
|
const changed = r.argv.length > 0 || feed !== undefined;
|
|
6952
7429
|
if (!changed)
|
|
6953
7430
|
return;
|
|
6954
|
-
writeSeatState(
|
|
7431
|
+
writeSeatState(o.home, { ...readSeatState(o.home), cockpit: r.state });
|
|
6955
7432
|
if (r.argv.length)
|
|
6956
|
-
await runCockpit(cockpit, r.argv, { spawn:
|
|
7433
|
+
await runCockpit(cockpit, r.argv, { spawn: o.spawn });
|
|
6957
7434
|
} catch {}
|
|
6958
7435
|
};
|
|
6959
|
-
var rememberSeat = (
|
|
6960
|
-
const prior = readSeatState(
|
|
7436
|
+
var rememberSeat = (o, window, session, now, input) => {
|
|
7437
|
+
const prior = readSeatState(o.home);
|
|
6961
7438
|
let state = { ...prior, window, ...session ? { session } : {} };
|
|
6962
|
-
const src = meterSource(
|
|
7439
|
+
const src = meterSource(o.home);
|
|
6963
7440
|
const posts = [];
|
|
6964
7441
|
if (src && shouldPostSeat(prior, window, now)) {
|
|
6965
7442
|
state = markPosted(state, window, now);
|
|
6966
|
-
posts.push(postSeatWindow({ ...src, fetchFn:
|
|
7443
|
+
posts.push(postSeatWindow({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, window));
|
|
6967
7444
|
}
|
|
6968
7445
|
const events = seatWallEvents(prior, window);
|
|
6969
|
-
const credits = creditsEvent(
|
|
7446
|
+
const credits = creditsEvent(o, prior, input, now);
|
|
6970
7447
|
if (credits.scan)
|
|
6971
7448
|
state = { ...state, transcriptScan: credits.scan };
|
|
6972
7449
|
if (credits.event)
|
|
6973
7450
|
events.push(credits.event);
|
|
6974
7451
|
state = markWarned(state, events);
|
|
6975
|
-
posts.push(...announce(
|
|
6976
|
-
writeSeatState(
|
|
7452
|
+
posts.push(...announce(o, events));
|
|
7453
|
+
writeSeatState(o.home, state);
|
|
6977
7454
|
return { posts, events };
|
|
6978
7455
|
};
|
|
6979
|
-
var creditsEvent = (
|
|
7456
|
+
var creditsEvent = (o, prior, input, now) => {
|
|
6980
7457
|
const path = transcriptPathFromStdin(input);
|
|
6981
7458
|
if (!path)
|
|
6982
7459
|
return { event: null, scan: undefined };
|
|
@@ -6986,13 +7463,13 @@ var creditsEvent = (o2, prior, input, now) => {
|
|
|
6986
7463
|
return { event: null, scan: next };
|
|
6987
7464
|
return { event: { bar: "credits", stage: "hit", usedPct: 100, resetsAt: now }, scan: next };
|
|
6988
7465
|
};
|
|
6989
|
-
var announce = (
|
|
7466
|
+
var announce = (o, events) => {
|
|
6990
7467
|
if (events.length === 0)
|
|
6991
7468
|
return [];
|
|
6992
|
-
const src = meterSource(
|
|
7469
|
+
const src = meterSource(o.home);
|
|
6993
7470
|
if (!src)
|
|
6994
7471
|
return [];
|
|
6995
|
-
return events.map((e) => postSeatWall({ ...src, fetchFn:
|
|
7472
|
+
return events.map((e) => postSeatWall({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, {
|
|
6996
7473
|
bar: e.bar,
|
|
6997
7474
|
stage: e.stage,
|
|
6998
7475
|
usedPct: e.usedPct,
|
|
@@ -7002,6 +7479,128 @@ var announce = (o2, events) => {
|
|
|
7002
7479
|
};
|
|
7003
7480
|
|
|
7004
7481
|
// src/commands/stopEvent.ts
|
|
7482
|
+
init_meter();
|
|
7483
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12 } from "node:fs";
|
|
7484
|
+
|
|
7485
|
+
// src/transcript.ts
|
|
7486
|
+
var isObject12 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7487
|
+
var n = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
7488
|
+
function sumTranscript(text) {
|
|
7489
|
+
const seen = new Set;
|
|
7490
|
+
const turn = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
7491
|
+
let sub = 0;
|
|
7492
|
+
let messages = 0;
|
|
7493
|
+
const byModel = new Map;
|
|
7494
|
+
const agents = new Set;
|
|
7495
|
+
let anySidechain = false;
|
|
7496
|
+
for (const line of text.split(`
|
|
7497
|
+
`)) {
|
|
7498
|
+
const t = line.trim();
|
|
7499
|
+
if (!t)
|
|
7500
|
+
continue;
|
|
7501
|
+
let j;
|
|
7502
|
+
try {
|
|
7503
|
+
j = JSON.parse(t);
|
|
7504
|
+
} catch {
|
|
7505
|
+
continue;
|
|
7506
|
+
}
|
|
7507
|
+
if (!isObject12(j) || j.type !== "assistant" || !isObject12(j.message))
|
|
7508
|
+
continue;
|
|
7509
|
+
const m = j.message;
|
|
7510
|
+
const id = typeof m.id === "string" ? m.id : typeof j.uuid === "string" ? j.uuid : undefined;
|
|
7511
|
+
if (id) {
|
|
7512
|
+
if (seen.has(id))
|
|
7513
|
+
continue;
|
|
7514
|
+
seen.add(id);
|
|
7515
|
+
}
|
|
7516
|
+
const u = isObject12(m.usage) ? m.usage : undefined;
|
|
7517
|
+
if (!u)
|
|
7518
|
+
continue;
|
|
7519
|
+
const input = n(u.input_tokens);
|
|
7520
|
+
const output = n(u.output_tokens);
|
|
7521
|
+
const cacheRead = n(u.cache_read_input_tokens);
|
|
7522
|
+
const cacheWrite = n(u.cache_creation_input_tokens);
|
|
7523
|
+
messages++;
|
|
7524
|
+
const model = typeof m.model === "string" ? m.model : undefined;
|
|
7525
|
+
if (model)
|
|
7526
|
+
byModel.set(model, (byModel.get(model) ?? 0) + output);
|
|
7527
|
+
if (j.isSidechain === true) {
|
|
7528
|
+
anySidechain = true;
|
|
7529
|
+
sub += input + output + cacheRead + cacheWrite;
|
|
7530
|
+
if (typeof j.agentId === "string")
|
|
7531
|
+
agents.add(j.agentId);
|
|
7532
|
+
} else {
|
|
7533
|
+
turn.input += input;
|
|
7534
|
+
turn.output += output;
|
|
7535
|
+
turn.cacheRead += cacheRead;
|
|
7536
|
+
turn.cacheWrite += cacheWrite;
|
|
7537
|
+
}
|
|
7538
|
+
}
|
|
7539
|
+
let model;
|
|
7540
|
+
let best = -1;
|
|
7541
|
+
for (const [k, v] of byModel)
|
|
7542
|
+
if (v > best)
|
|
7543
|
+
[model, best] = [k, v];
|
|
7544
|
+
return {
|
|
7545
|
+
turnTokens: turn,
|
|
7546
|
+
subagentTokens: sub,
|
|
7547
|
+
messages,
|
|
7548
|
+
...model ? { model } : {},
|
|
7549
|
+
subagentRuns: agents.size > 0 ? agents.size : anySidechain ? 1 : 0
|
|
7550
|
+
};
|
|
7551
|
+
}
|
|
7552
|
+
function sessionBody(sessionId, sum, a) {
|
|
7553
|
+
return {
|
|
7554
|
+
capturedAt: a.capturedAt,
|
|
7555
|
+
sessions: [
|
|
7556
|
+
{
|
|
7557
|
+
sessionId,
|
|
7558
|
+
...a.title ? { title: a.title.slice(0, 200) } : {},
|
|
7559
|
+
updatedAt: a.capturedAt,
|
|
7560
|
+
subagentTokens: sum.subagentTokens,
|
|
7561
|
+
taskCount: sum.subagentRuns,
|
|
7562
|
+
turnTokens: sum.turnTokens,
|
|
7563
|
+
tasks: []
|
|
7564
|
+
}
|
|
7565
|
+
]
|
|
7566
|
+
};
|
|
7567
|
+
}
|
|
7568
|
+
|
|
7569
|
+
// src/commands/stopEvent.ts
|
|
7570
|
+
var SESSION_POST_TIMEOUT_MS = 2500;
|
|
7571
|
+
async function postTranscript(o, payload, now) {
|
|
7572
|
+
const sessionId = payload.session_id?.trim();
|
|
7573
|
+
const path = payload.transcript_path?.trim();
|
|
7574
|
+
if (!sessionId || !path)
|
|
7575
|
+
return false;
|
|
7576
|
+
const src = meterSource(o.home);
|
|
7577
|
+
if (!src)
|
|
7578
|
+
return false;
|
|
7579
|
+
const read = o.readFile ?? ((p) => existsSync14(p) ? readFileSync12(p, "utf8") : null);
|
|
7580
|
+
let text;
|
|
7581
|
+
try {
|
|
7582
|
+
text = read(path);
|
|
7583
|
+
} catch {
|
|
7584
|
+
return false;
|
|
7585
|
+
}
|
|
7586
|
+
if (!text)
|
|
7587
|
+
return false;
|
|
7588
|
+
const sum = sumTranscript(text);
|
|
7589
|
+
if (sum.messages === 0)
|
|
7590
|
+
return false;
|
|
7591
|
+
const fetchFn = o.fetchFn ?? fetch;
|
|
7592
|
+
try {
|
|
7593
|
+
const res = await fetchFn(`${src.proxyUrl.replace(/\/+$/, "")}/proxy/sessions`, {
|
|
7594
|
+
method: "POST",
|
|
7595
|
+
headers: { authorization: `Bearer ${src.token}`, "content-type": "application/json" },
|
|
7596
|
+
body: JSON.stringify(sessionBody(sessionId, sum, { capturedAt: now })),
|
|
7597
|
+
signal: AbortSignal.timeout(SESSION_POST_TIMEOUT_MS)
|
|
7598
|
+
});
|
|
7599
|
+
return res.ok;
|
|
7600
|
+
} catch {
|
|
7601
|
+
return false;
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7005
7604
|
var parseStopPayload = (raw) => {
|
|
7006
7605
|
try {
|
|
7007
7606
|
const j = JSON.parse(raw);
|
|
@@ -7026,14 +7625,15 @@ function decideStop(a) {
|
|
|
7026
7625
|
rows: markAsked(a.rows, due.map((r) => r.key), a.now)
|
|
7027
7626
|
};
|
|
7028
7627
|
}
|
|
7029
|
-
var stopEvent = async (
|
|
7030
|
-
const log =
|
|
7031
|
-
const now = (
|
|
7032
|
-
const raw =
|
|
7628
|
+
var stopEvent = async (o) => {
|
|
7629
|
+
const log = o.log ?? console.log;
|
|
7630
|
+
const now = (o.now ?? Date.now)();
|
|
7631
|
+
const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
|
|
7033
7632
|
const payload = parseStopPayload(raw);
|
|
7633
|
+
await postTranscript(o, payload, now);
|
|
7034
7634
|
let rows;
|
|
7035
7635
|
try {
|
|
7036
|
-
rows = readOwed(
|
|
7636
|
+
rows = readOwed(o.home, now);
|
|
7037
7637
|
} catch {
|
|
7038
7638
|
return 0;
|
|
7039
7639
|
}
|
|
@@ -7046,24 +7646,24 @@ var stopEvent = async (o2) => {
|
|
|
7046
7646
|
if (!d.block)
|
|
7047
7647
|
return 0;
|
|
7048
7648
|
try {
|
|
7049
|
-
writeOwed(
|
|
7649
|
+
writeOwed(o.home, d.rows);
|
|
7050
7650
|
} catch {}
|
|
7051
7651
|
log(JSON.stringify({ decision: "block", reason: d.reason }));
|
|
7052
7652
|
return 0;
|
|
7053
7653
|
};
|
|
7054
7654
|
|
|
7055
7655
|
// src/commands/uninstall.ts
|
|
7056
|
-
import { spawnSync as
|
|
7057
|
-
import { existsSync as
|
|
7656
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
7657
|
+
import { existsSync as existsSync16, rmSync as rmSync5 } from "node:fs";
|
|
7058
7658
|
|
|
7059
7659
|
// src/codex.ts
|
|
7060
7660
|
init_config();
|
|
7061
|
-
import { existsSync as
|
|
7661
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync12 } from "node:fs";
|
|
7062
7662
|
import { dirname as dirname6, join as join12 } from "node:path";
|
|
7063
7663
|
var PROVIDER_ID2 = "sleds";
|
|
7064
7664
|
var codexDir2 = (home) => join12(home, ".codex");
|
|
7065
7665
|
var codexConfigPath2 = (home) => join12(codexDir2(home), "config.toml");
|
|
7066
|
-
function removeCodexProvider2(toml,
|
|
7666
|
+
function removeCodexProvider2(toml, o = {}) {
|
|
7067
7667
|
const lines = toml.split(`
|
|
7068
7668
|
`);
|
|
7069
7669
|
const out = [];
|
|
@@ -7076,7 +7676,7 @@ function removeCodexProvider2(toml, o2 = {}) {
|
|
|
7076
7676
|
}
|
|
7077
7677
|
let text = out.join(`
|
|
7078
7678
|
`);
|
|
7079
|
-
if (!
|
|
7679
|
+
if (!o.keepPrior) {
|
|
7080
7680
|
const prior = text.match(/^# sleds-prior-model-provider = "([^"]*)"\s*\n/m);
|
|
7081
7681
|
if (prior) {
|
|
7082
7682
|
text = text.replace(prior[0], "");
|
|
@@ -7091,9 +7691,9 @@ function removeCodexProvider2(toml, o2 = {}) {
|
|
|
7091
7691
|
}
|
|
7092
7692
|
function uninstallCodex(home) {
|
|
7093
7693
|
const path = codexConfigPath2(home);
|
|
7094
|
-
if (!
|
|
7694
|
+
if (!existsSync15(path))
|
|
7095
7695
|
return null;
|
|
7096
|
-
const prior =
|
|
7696
|
+
const prior = readFileSync13(path, "utf8");
|
|
7097
7697
|
const next = removeCodexProvider2(prior);
|
|
7098
7698
|
if (next === prior)
|
|
7099
7699
|
return null;
|
|
@@ -7106,74 +7706,74 @@ init_config();
|
|
|
7106
7706
|
init_meter();
|
|
7107
7707
|
init_settings();
|
|
7108
7708
|
var defaultUnload = (plist) => {
|
|
7109
|
-
|
|
7709
|
+
spawnSync5("launchctl", ["unload", plist], { stdio: "ignore" });
|
|
7110
7710
|
};
|
|
7111
|
-
var uninstall2 = (
|
|
7112
|
-
const codexLine = uninstallCodex(
|
|
7711
|
+
var uninstall2 = (o) => {
|
|
7712
|
+
const codexLine = uninstallCodex(o.home);
|
|
7113
7713
|
if (codexLine)
|
|
7114
|
-
(
|
|
7115
|
-
const log =
|
|
7714
|
+
(o.log ?? console.log)(` ${codexLine}`);
|
|
7715
|
+
const log = o.log ?? console.log;
|
|
7116
7716
|
const removed = [];
|
|
7117
|
-
if (
|
|
7118
|
-
let settings = readSettings(
|
|
7717
|
+
if (existsSync16(settingsPath(o.home))) {
|
|
7718
|
+
let settings = readSettings(o.home);
|
|
7119
7719
|
let changed = false;
|
|
7120
7720
|
if (hasEitherKey(settings)) {
|
|
7121
7721
|
settings = removeProxy(settings);
|
|
7122
7722
|
changed = true;
|
|
7123
|
-
removed.push(`env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN from ${settingsPath(
|
|
7723
|
+
removed.push(`env.ANTHROPIC_BASE_URL and env.ANTHROPIC_AUTH_TOKEN from ${settingsPath(o.home)}`);
|
|
7124
7724
|
}
|
|
7125
7725
|
if (isSledsStatusLine(settings[STATUS_LINE_KEY])) {
|
|
7126
|
-
const cfg = readConfig(
|
|
7726
|
+
const cfg = readConfig(o.home);
|
|
7127
7727
|
settings = removeStatusLine(settings, cfg.priorStatusLine);
|
|
7128
7728
|
changed = true;
|
|
7129
|
-
removed.push(cfg.priorStatusLine === undefined ? `statusLine from ${settingsPath(
|
|
7729
|
+
removed.push(cfg.priorStatusLine === undefined ? `statusLine from ${settingsPath(o.home)}` : `statusLine from ${settingsPath(o.home)} (previous one restored)`);
|
|
7130
7730
|
if (cfg.priorStatusLine !== undefined) {
|
|
7131
7731
|
const { priorStatusLine: _restored, ...rest } = cfg;
|
|
7132
|
-
writeConfig(
|
|
7732
|
+
writeConfig(o.home, rest);
|
|
7133
7733
|
}
|
|
7134
7734
|
}
|
|
7135
7735
|
if (hasSeatHook(settings)) {
|
|
7136
7736
|
settings = removeSeatHook(settings);
|
|
7137
7737
|
changed = true;
|
|
7138
|
-
removed.push(`hooks.Notification seat-event entry from ${settingsPath(
|
|
7738
|
+
removed.push(`hooks.Notification seat-event entry from ${settingsPath(o.home)}`);
|
|
7139
7739
|
}
|
|
7140
7740
|
if (hasPromptHook(settings)) {
|
|
7141
7741
|
settings = removePromptHook(settings);
|
|
7142
7742
|
changed = true;
|
|
7143
|
-
removed.push(`hooks.UserPromptSubmit prompt-event entry from ${settingsPath(
|
|
7743
|
+
removed.push(`hooks.UserPromptSubmit prompt-event entry from ${settingsPath(o.home)}`);
|
|
7144
7744
|
}
|
|
7145
7745
|
if (hasSpecHook(settings)) {
|
|
7146
7746
|
settings = removeSpecHook(settings);
|
|
7147
7747
|
changed = true;
|
|
7148
|
-
removed.push(`hooks.PostToolUse spec-event entry from ${settingsPath(
|
|
7748
|
+
removed.push(`hooks.PostToolUse spec-event entry from ${settingsPath(o.home)}`);
|
|
7149
7749
|
}
|
|
7150
7750
|
if (hasSessionHook(settings)) {
|
|
7151
7751
|
settings = removeSessionHook(settings);
|
|
7152
7752
|
changed = true;
|
|
7153
|
-
removed.push(`hooks.SessionStart session-event entry from ${settingsPath(
|
|
7753
|
+
removed.push(`hooks.SessionStart session-event entry from ${settingsPath(o.home)}`);
|
|
7154
7754
|
}
|
|
7155
7755
|
if (hasStopHook(settings)) {
|
|
7156
7756
|
settings = removeStopHook(settings);
|
|
7157
7757
|
changed = true;
|
|
7158
|
-
removed.push(`hooks.Stop stop-event entry from ${settingsPath(
|
|
7758
|
+
removed.push(`hooks.Stop stop-event entry from ${settingsPath(o.home)}`);
|
|
7159
7759
|
}
|
|
7160
7760
|
if (changed)
|
|
7161
|
-
writeSettings(
|
|
7761
|
+
writeSettings(o.home, settings);
|
|
7162
7762
|
}
|
|
7163
|
-
for (const file of [meterCachePath(
|
|
7164
|
-
if (
|
|
7763
|
+
for (const file of [meterCachePath(o.home), seatStatePath(o.home)]) {
|
|
7764
|
+
if (existsSync16(file)) {
|
|
7165
7765
|
rmSync5(file, { force: true });
|
|
7166
7766
|
removed.push(file);
|
|
7167
7767
|
}
|
|
7168
7768
|
}
|
|
7169
|
-
const skill = skillDir(
|
|
7170
|
-
if (
|
|
7769
|
+
const skill = skillDir(o.home);
|
|
7770
|
+
if (existsSync16(skill)) {
|
|
7171
7771
|
rmSync5(skill, { recursive: true, force: true });
|
|
7172
7772
|
removed.push(skill);
|
|
7173
7773
|
}
|
|
7174
|
-
const plist = musherPlistPath(
|
|
7175
|
-
if (
|
|
7176
|
-
(
|
|
7774
|
+
const plist = musherPlistPath(o.home);
|
|
7775
|
+
if (existsSync16(plist)) {
|
|
7776
|
+
(o.unloadAgent ?? defaultUnload)(plist);
|
|
7177
7777
|
rmSync5(plist, { force: true });
|
|
7178
7778
|
removed.push(`launchd agent ${musherLabel()}`);
|
|
7179
7779
|
}
|
|
@@ -7184,7 +7784,7 @@ var uninstall2 = (o2) => {
|
|
|
7184
7784
|
for (const r of removed)
|
|
7185
7785
|
log(` ${r}`);
|
|
7186
7786
|
}
|
|
7187
|
-
log(`Kept ${tokenPath(
|
|
7787
|
+
log(`Kept ${tokenPath(o.home)} (delete it yourself if you want the token gone).`);
|
|
7188
7788
|
return 0;
|
|
7189
7789
|
};
|
|
7190
7790
|
var hasEitherKey = (settings) => {
|
|
@@ -7203,17 +7803,17 @@ var jobIdFromTarget = (target) => {
|
|
|
7203
7803
|
return m[1];
|
|
7204
7804
|
return /^[A-Za-z0-9_-]{3,64}$/.test(t) ? t : null;
|
|
7205
7805
|
};
|
|
7206
|
-
var verify = async (
|
|
7207
|
-
const log =
|
|
7208
|
-
const error =
|
|
7209
|
-
const jobId =
|
|
7806
|
+
var verify = async (o) => {
|
|
7807
|
+
const log = o.log ?? console.log;
|
|
7808
|
+
const error = o.error ?? console.error;
|
|
7809
|
+
const jobId = o.target ? jobIdFromTarget(o.target) : null;
|
|
7210
7810
|
if (!jobId) {
|
|
7211
7811
|
error("usage: sleds verify <jobId> (or paste the SLEDS-Attest line)");
|
|
7212
7812
|
return 2;
|
|
7213
7813
|
}
|
|
7214
|
-
const client =
|
|
7215
|
-
const src = meterSource(
|
|
7216
|
-
return src ? musherClient({ ...src, fetchFn:
|
|
7814
|
+
const client = o.client ?? (() => {
|
|
7815
|
+
const src = meterSource(o.home);
|
|
7816
|
+
return src ? musherClient({ ...src, fetchFn: o.fetchFn }) : null;
|
|
7217
7817
|
})();
|
|
7218
7818
|
if (!client) {
|
|
7219
7819
|
error("No seat on this machine. Run `sleds init` first.");
|
|
@@ -7244,17 +7844,17 @@ var verify = async (o2) => {
|
|
|
7244
7844
|
|
|
7245
7845
|
// src/config.ts
|
|
7246
7846
|
init_settings();
|
|
7247
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
7847
|
+
import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync6, writeFileSync as writeFileSync13 } from "node:fs";
|
|
7248
7848
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
7249
7849
|
var configDir2 = (home, env = process.env) => {
|
|
7250
|
-
const
|
|
7251
|
-
return
|
|
7850
|
+
const o = env.SLEDS_HOME?.trim();
|
|
7851
|
+
return o ? resolve4(home, o) : join13(home, ".config", "sleds");
|
|
7252
7852
|
};
|
|
7253
7853
|
var configPath2 = (home) => join13(configDir2(home), "config.json");
|
|
7254
|
-
var
|
|
7854
|
+
var isObject13 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7255
7855
|
var optString2 = (v) => typeof v === "string" ? v : undefined;
|
|
7256
7856
|
var parsePriorEnv2 = (v) => {
|
|
7257
|
-
if (!
|
|
7857
|
+
if (!isObject13(v))
|
|
7258
7858
|
return;
|
|
7259
7859
|
const out = {};
|
|
7260
7860
|
const url = optString2(v[BASE_URL_KEY]);
|
|
@@ -7269,7 +7869,7 @@ var parsePriorEnv2 = (v) => {
|
|
|
7269
7869
|
};
|
|
7270
7870
|
var parseSchedule2 = (v) => v === "launchd" || v === "systemd" || v === "manual" ? v : undefined;
|
|
7271
7871
|
var parseConfig2 = (raw) => {
|
|
7272
|
-
if (!
|
|
7872
|
+
if (!isObject13(raw))
|
|
7273
7873
|
return {};
|
|
7274
7874
|
const cfg = {};
|
|
7275
7875
|
const token = optString2(raw.token);
|
|
@@ -7307,13 +7907,13 @@ var parseConfig2 = (raw) => {
|
|
|
7307
7907
|
const org = parseOrgInfo2(raw.org);
|
|
7308
7908
|
if (org)
|
|
7309
7909
|
cfg.org = org;
|
|
7310
|
-
const
|
|
7311
|
-
if (
|
|
7312
|
-
cfg.musher =
|
|
7910
|
+
const musher = parseMusher2(raw.musher);
|
|
7911
|
+
if (musher)
|
|
7912
|
+
cfg.musher = musher;
|
|
7313
7913
|
return cfg;
|
|
7314
7914
|
};
|
|
7315
7915
|
var parseMusher2 = (raw) => {
|
|
7316
|
-
if (!
|
|
7916
|
+
if (!isObject13(raw))
|
|
7317
7917
|
return;
|
|
7318
7918
|
const out = {};
|
|
7319
7919
|
const pm = optString2(raw.permissionMode);
|
|
@@ -7329,19 +7929,19 @@ var parseMusher2 = (raw) => {
|
|
|
7329
7929
|
return out;
|
|
7330
7930
|
};
|
|
7331
7931
|
var parseOrgInfo2 = (raw) => {
|
|
7332
|
-
if (!
|
|
7932
|
+
if (!isObject13(raw) || !isObject13(raw.capabilities))
|
|
7333
7933
|
return;
|
|
7334
7934
|
const name = optString2(raw.name);
|
|
7335
|
-
const
|
|
7336
|
-
const
|
|
7935
|
+
const slug = optString2(raw.slug);
|
|
7936
|
+
const plan = raw.plan;
|
|
7337
7937
|
const c = raw.capabilities;
|
|
7338
7938
|
const src = c.upstreamSource;
|
|
7339
|
-
if (name === undefined ||
|
|
7939
|
+
if (name === undefined || slug === undefined || plan !== "team" && plan !== "enterprise" && plan !== "api" && plan !== "individual" || src !== "own" && src !== "shared" && src !== "none")
|
|
7340
7940
|
return;
|
|
7341
7941
|
return {
|
|
7342
7942
|
name,
|
|
7343
|
-
slug
|
|
7344
|
-
plan
|
|
7943
|
+
slug,
|
|
7944
|
+
plan,
|
|
7345
7945
|
capabilities: {
|
|
7346
7946
|
upstreamKey: c.upstreamKey === true,
|
|
7347
7947
|
adminKey: c.adminKey === true,
|
|
@@ -7352,16 +7952,16 @@ var parseOrgInfo2 = (raw) => {
|
|
|
7352
7952
|
};
|
|
7353
7953
|
};
|
|
7354
7954
|
var readConfig2 = (home) => {
|
|
7355
|
-
const
|
|
7356
|
-
if (!
|
|
7955
|
+
const p = configPath2(home);
|
|
7956
|
+
if (!existsSync17(p))
|
|
7357
7957
|
return {};
|
|
7358
|
-
return parseConfig2(JSON.parse(
|
|
7958
|
+
return parseConfig2(JSON.parse(readFileSync14(p, "utf8")));
|
|
7359
7959
|
};
|
|
7360
7960
|
|
|
7361
7961
|
// src/schedule.ts
|
|
7362
|
-
import { spawnSync as
|
|
7962
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
7363
7963
|
var defaultExec = (cmd, args) => {
|
|
7364
|
-
const r =
|
|
7964
|
+
const r = spawnSync6(cmd, [...args], { stdio: ["ignore", "ignore", "pipe"], encoding: "utf8" });
|
|
7365
7965
|
if (r.error)
|
|
7366
7966
|
return { ok: false, stderr: r.error.message };
|
|
7367
7967
|
return { ok: r.status === 0, stderr: r.stderr ?? "" };
|
|
@@ -7381,12 +7981,12 @@ var withSeatEnv2 = (command, home, env = process.env) => {
|
|
|
7381
7981
|
return parts.join(" ");
|
|
7382
7982
|
};
|
|
7383
7983
|
var shellWord2 = (v) => /^[\w./~-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
|
|
7384
|
-
var
|
|
7984
|
+
var isObject14 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7385
7985
|
var isSledsHookFor2 = (sub) => (entry) => {
|
|
7386
|
-
if (!
|
|
7986
|
+
if (!isObject14(entry) || !Array.isArray(entry.hooks))
|
|
7387
7987
|
return false;
|
|
7388
7988
|
const re = new RegExp(`\\b${sub}\\b`);
|
|
7389
|
-
return entry.hooks.some((h) =>
|
|
7989
|
+
return entry.hooks.some((h) => isObject14(h) && h.type === "command" && typeof h.command === "string" && re.test(h.command));
|
|
7390
7990
|
};
|
|
7391
7991
|
var isSledsSessionHook2 = isSledsHookFor2("session-event");
|
|
7392
7992
|
var isSledsStopHook2 = isSledsHookFor2("stop-event");
|
|
@@ -7395,13 +7995,15 @@ var isSledsStopHook2 = isSledsHookFor2("stop-event");
|
|
|
7395
7995
|
var USAGE = `usage: sleds <command> [options]
|
|
7396
7996
|
|
|
7397
7997
|
commands:
|
|
7398
|
-
init
|
|
7399
|
-
|
|
7998
|
+
init Sign in through the browser, make this machine a seat, install the /sled skill
|
|
7999
|
+
and the status line. No flags needed: it opens app.sleds.ai/connect and waits.
|
|
8000
|
+
--token <sled_\u2026> skip the browser: a seat token (else SLEDS_TOKEN, ~/.sleds-token)
|
|
7400
8001
|
--url <proxy-url> proxy base URL (else SLEDS_PROXY_URL, default ${DEFAULT_PROXY_URL})
|
|
8002
|
+
--no-browser print the sign-in URL and code instead of opening a browser
|
|
7401
8003
|
--no-probe skip the /healthz check of the proxy URL
|
|
7402
8004
|
--seat-first keep Claude Code on your seat; the proxy only at a wall (default for seat plans)
|
|
7403
8005
|
--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
|
|
8006
|
+
--non-interactive never open a browser or prompt; exit 2 if no token is available
|
|
7405
8007
|
--agent codex Codex CLI instead of Claude Code: writes ~/.codex/config.toml
|
|
7406
8008
|
(provider "sleds" \u2192 the proxy, key from $SLEDS_TOKEN); no hooks/status line
|
|
7407
8009
|
--signup no org? create one: (1) just you \u2014 your email + your own Anthropic
|
|
@@ -7490,8 +8092,8 @@ var stringFlag = (flags, name) => {
|
|
|
7490
8092
|
var hookStdin = async () => {
|
|
7491
8093
|
if (process.stdin.isTTY)
|
|
7492
8094
|
return;
|
|
7493
|
-
|
|
7494
|
-
const text = await
|
|
8095
|
+
await Promise.resolve().then(() => init_seat());
|
|
8096
|
+
const text = await readStdin2(300);
|
|
7495
8097
|
return text.trim() ? text : undefined;
|
|
7496
8098
|
};
|
|
7497
8099
|
var sledsArgv = () => {
|
|
@@ -7509,7 +8111,7 @@ var main = async (argv) => {
|
|
|
7509
8111
|
const { cmd, flags } = parseArgs(argv);
|
|
7510
8112
|
const home = homedir();
|
|
7511
8113
|
const scheduler = {
|
|
7512
|
-
platform:
|
|
8114
|
+
platform: platform3(),
|
|
7513
8115
|
home,
|
|
7514
8116
|
sledsArgv: sledsArgv(),
|
|
7515
8117
|
exec: defaultExec
|
|
@@ -7557,10 +8159,11 @@ var main = async (argv) => {
|
|
|
7557
8159
|
team: typeof flags.team === "string" ? flags.team : undefined,
|
|
7558
8160
|
email: typeof flags.email === "string" ? flags.email : undefined,
|
|
7559
8161
|
noProbe: flags["no-probe"] === true,
|
|
8162
|
+
noBrowser: flags["no-browser"] === true,
|
|
7560
8163
|
proxyAlways: flags["proxy-always"] === true,
|
|
7561
8164
|
seatFirst: flags["seat-first"] === true,
|
|
7562
8165
|
installMusher: () => {
|
|
7563
|
-
if (
|
|
8166
|
+
if (platform3() !== "darwin")
|
|
7564
8167
|
return null;
|
|
7565
8168
|
const lines = [];
|
|
7566
8169
|
const code = musherInstall({
|
|
@@ -7643,7 +8246,7 @@ ${renderHelpText(argv[1])}`);
|
|
|
7643
8246
|
case "version":
|
|
7644
8247
|
case "--version":
|
|
7645
8248
|
case "-v":
|
|
7646
|
-
console.log("sleds 0.1.
|
|
8249
|
+
console.log("sleds 0.1.25");
|
|
7647
8250
|
return 0;
|
|
7648
8251
|
default:
|
|
7649
8252
|
console.error(`unknown command: ${cmd}
|