@sleds/cli 0.1.17 → 0.1.19

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.
Files changed (2) hide show
  1. package/dist/sleds.mjs +471 -60
  2. package/package.json +1 -1
package/dist/sleds.mjs CHANGED
@@ -152,7 +152,24 @@ var BASE_URL_KEY = "ANTHROPIC_BASE_URL", AUTH_TOKEN_KEY = "ANTHROPIC_AUTH_TOKEN"
152
152
  if (!list.some(isSledsSpecHook))
153
153
  return { ...settings };
154
154
  return withEventHooks(settings, POST_TOOL_EVENT, list.filter((e) => !isSledsSpecHook(e)));
155
- }, hasSpecHook = (settings) => eventHooks(settings, POST_TOOL_EVENT).some(isSledsSpecHook), 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)}
155
+ }, hasSpecHook = (settings) => eventHooks(settings, POST_TOOL_EVENT).some(isSledsSpecHook), PROMPT_EVENT = "UserPromptSubmit", PROMPT_EVENT_TIMEOUT_S = 3, isSledsPromptHook = (entry) => {
156
+ if (!isObject(entry) || !Array.isArray(entry.hooks))
157
+ return false;
158
+ return entry.hooks.some((h) => isObject(h) && h.type === "command" && typeof h.command === "string" && /\bprompt-event\b/.test(h.command));
159
+ }, promptHookEntry = (command) => ({
160
+ hooks: [{ type: "command", command, timeout: PROMPT_EVENT_TIMEOUT_S }]
161
+ }), applyPromptHook = (settings, command) => {
162
+ const list = eventHooks(settings, PROMPT_EVENT);
163
+ const idx = list.findIndex(isSledsPromptHook);
164
+ const entry = promptHookEntry(command);
165
+ const next = idx === -1 ? [...list, entry] : list.map((e, i) => i === idx ? entry : e);
166
+ return withEventHooks(settings, PROMPT_EVENT, next);
167
+ }, removePromptHook = (settings) => {
168
+ const list = eventHooks(settings, PROMPT_EVENT);
169
+ if (!list.some(isSledsPromptHook))
170
+ return { ...settings };
171
+ return withEventHooks(settings, PROMPT_EVENT, list.filter((e) => !isSledsPromptHook(e)));
172
+ }, hasPromptHook = (settings) => eventHooks(settings, PROMPT_EVENT).some(isSledsPromptHook), 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)}
156
173
  `, hasClaudeCode = (home) => existsSync(claudeDir(home)), readSettings = (home) => {
157
174
  const p = settingsPath(home);
158
175
  if (!existsSync(p))
@@ -514,6 +531,8 @@ var init_meter = __esm(() => {
514
531
  // src/seat.ts
515
532
  var exports_seat = {};
516
533
  __export(exports_seat, {
534
+ APPROACHING_PCT: () => APPROACHING_PCT2,
535
+ CREDITS_PATTERN: () => CREDITS_PATTERN2,
517
536
  SEAT_POST_MIN_DELTA_PCT: () => SEAT_POST_MIN_DELTA_PCT2,
518
537
  SEAT_POST_MIN_INTERVAL_MS: () => SEAT_POST_MIN_INTERVAL_MS2,
519
538
  SEAT_POST_TIMEOUT_MS: () => SEAT_POST_TIMEOUT_MS2,
@@ -521,8 +540,10 @@ __export(exports_seat, {
521
540
  SEAT_WALL_LINE_NO_KEY: () => SEAT_WALL_LINE_NO_KEY2,
522
541
  STDIN_TIMEOUT_MS: () => STDIN_TIMEOUT_MS2,
523
542
  bindingResetAt: () => bindingResetAt2,
543
+ creditsHitIn: () => creditsHitIn2,
524
544
  formatSeatFragment: () => formatSeatFragment2,
525
545
  markPosted: () => markPosted2,
546
+ markWarned: () => markWarned2,
526
547
  parseResetsAt: () => parseResetsAt2,
527
548
  parseSeatState: () => parseSeatState2,
528
549
  parseSeatWindow: () => parseSeatWindow2,
@@ -530,16 +551,29 @@ __export(exports_seat, {
530
551
  postSeatWindow: () => postSeatWindow2,
531
552
  readSeatState: () => readSeatState2,
532
553
  readStdin: () => readStdin2,
554
+ scanTranscriptForCredits: () => scanTranscriptForCredits2,
533
555
  seatSessionFromStdin: () => seatSessionFromStdin2,
534
556
  seatStatePath: () => seatStatePath2,
535
- seatWallDue: () => seatWallDue2,
557
+ seatWallDue: () => seatWallDue,
558
+ seatWallEvents: () => seatWallEvents2,
536
559
  seatWallLine: () => seatWallLine2,
537
560
  seatWindowBody: () => seatWindowBody2,
538
561
  seatWindowFromStdin: () => seatWindowFromStdin2,
539
562
  shouldPostSeat: () => shouldPostSeat2,
563
+ transcriptPathFromStdin: () => transcriptPathFromStdin2,
540
564
  writeSeatState: () => writeSeatState2
541
565
  });
542
- import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync13 } from "node:fs";
566
+ import {
567
+ closeSync as closeSync2,
568
+ existsSync as existsSync16,
569
+ mkdirSync as mkdirSync13,
570
+ openSync as openSync2,
571
+ readFileSync as readFileSync13,
572
+ readSync as readSync2,
573
+ renameSync as renameSync6,
574
+ statSync as statSync3,
575
+ writeFileSync as writeFileSync13
576
+ } from "node:fs";
543
577
  import { join as join14 } from "node:path";
544
578
  var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_MS2 = 1500, STDIN_TIMEOUT_MS2 = 300, isObject11 = (v) => typeof v === "object" && v !== null && !Array.isArray(v), num6 = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined, EPOCH_MS_THRESHOLD2 = 100000000000, parseResetsAt2 = (v) => {
545
579
  const n = num6(v);
@@ -600,7 +634,7 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
600
634
  } catch {
601
635
  return null;
602
636
  }
603
- }, formatSeatFragment2 = (w) => {
637
+ }, formatSeatFragment2 = (w, now = Date.now()) => {
604
638
  if (!w)
605
639
  return null;
606
640
  const parts = [];
@@ -612,8 +646,19 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
612
646
  parts.push(`spend ${Math.round(w.spendLimit.usedPct)}%`);
613
647
  if (parts.length === 0)
614
648
  return null;
649
+ const hot = [
650
+ ["wk", w.sevenDay],
651
+ ["5h", w.fiveHour],
652
+ ["spend", w.spendLimit]
653
+ ].filter((x) => x[1] !== undefined && x[1].usedPct >= APPROACHING_PCT2).sort((a, b) => b[1].usedPct - a[1].usedPct)[0];
654
+ if (hot) {
655
+ const mins = Math.max(0, Math.round((hot[1].resetsAt - now) / 60000));
656
+ const reset = mins >= 60 ? `${Math.floor(mins / 60)}h${mins % 60 ? `${mins % 60}m` : ""}` : `${mins}m`;
657
+ const verb = hot[1].usedPct >= 100 ? "at the wall" : `${Math.round(hot[1].usedPct)}%`;
658
+ return `⚠ seat ${hot[0]} ${verb}${SEP}resets ${reset}${SEP}/sled handoff · sleds defer`;
659
+ }
615
660
  return `seat ${parts.join(SEP)}`;
616
- }, seatStatePath2 = (home) => join14(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
661
+ }, APPROACHING_PCT2 = 90, seatStatePath2 = (home) => join14(configDir(home), "seat.json"), parseWindowStored2 = (v) => {
617
662
  if (!isObject11(v))
618
663
  return;
619
664
  const capturedAt = num6(v.capturedAt);
@@ -646,6 +691,49 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
646
691
  const lastWallResetsAt = num6(raw.lastWallResetsAt);
647
692
  if (lastWallResetsAt !== undefined)
648
693
  out.lastWallResetsAt = lastWallResetsAt;
694
+ if (isObject11(raw.warned)) {
695
+ const warned = {};
696
+ for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
697
+ const w = raw.warned[bar];
698
+ if (!isObject11(w))
699
+ continue;
700
+ const entry = {};
701
+ const ap = num6(w.approaching);
702
+ const hit = num6(w.hit);
703
+ if (ap !== undefined)
704
+ entry.approaching = ap;
705
+ if (hit !== undefined)
706
+ entry.hit = hit;
707
+ warned[bar] = entry;
708
+ }
709
+ out.warned = warned;
710
+ }
711
+ if (isObject11(raw.told)) {
712
+ const told = {};
713
+ for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
714
+ const w = raw.told[bar];
715
+ if (!isObject11(w))
716
+ continue;
717
+ const entry = {};
718
+ const ap = num6(w.approaching);
719
+ const hit = num6(w.hit);
720
+ if (ap !== undefined)
721
+ entry.approaching = ap;
722
+ if (hit !== undefined)
723
+ entry.hit = hit;
724
+ told[bar] = entry;
725
+ }
726
+ out.told = told;
727
+ }
728
+ if (isObject11(raw.transcriptScan) && typeof raw.transcriptScan.path === "string") {
729
+ const offset = num6(raw.transcriptScan.offset);
730
+ const hitAt = num6(raw.transcriptScan.hitAt);
731
+ out.transcriptScan = {
732
+ path: raw.transcriptScan.path,
733
+ offset: offset ?? 0,
734
+ ...hitAt !== undefined ? { hitAt } : {}
735
+ };
736
+ }
649
737
  const session = parseSeatSession2(raw.session);
650
738
  if (session)
651
739
  out.session = session;
@@ -737,10 +825,77 @@ var SEAT_POST_MIN_DELTA_PCT2 = 1, SEAT_POST_MIN_INTERVAL_MS2, SEAT_POST_TIMEOUT_
737
825
  if (w.spendLimit)
738
826
  lastPosted.spendLimit = w.spendLimit.usedPct;
739
827
  return { ...state, window: w, lastPostedAt: now, lastPosted };
740
- }, seatWallDue2 = (state, w) => {
828
+ }, seatWallDue = (state, w) => {
741
829
  if (!w?.sevenDay || w.sevenDay.usedPct < 100)
742
830
  return false;
743
831
  return state.lastWallResetsAt === undefined || state.lastWallResetsAt < w.sevenDay.resetsAt;
832
+ }, seatWallEvents2 = (state, w) => {
833
+ if (!w)
834
+ return [];
835
+ const out = [];
836
+ const bars = [
837
+ ["fiveHour", w.fiveHour],
838
+ ["sevenDay", w.sevenDay],
839
+ ["spendLimit", w.spendLimit]
840
+ ];
841
+ for (const [bar, b] of bars) {
842
+ if (!b)
843
+ continue;
844
+ const seen = state.warned?.[bar] ?? {};
845
+ const weeklyOld = bar === "sevenDay" && state.lastWallResetsAt !== undefined && state.lastWallResetsAt >= b.resetsAt;
846
+ if (b.usedPct >= APPROACHING_PCT2 && (seen.approaching ?? -1) < b.resetsAt && !weeklyOld)
847
+ out.push({ bar, stage: "approaching", usedPct: b.usedPct, resetsAt: b.resetsAt });
848
+ if (b.usedPct >= 100 && (seen.hit ?? -1) < b.resetsAt && !weeklyOld)
849
+ out.push({ bar, stage: "hit", usedPct: b.usedPct, resetsAt: b.resetsAt });
850
+ }
851
+ return out;
852
+ }, markWarned2 = (state, events) => {
853
+ if (events.length === 0)
854
+ return state;
855
+ const warned = { ...state.warned ?? {} };
856
+ let lastWallResetsAt = state.lastWallResetsAt;
857
+ for (const e of events) {
858
+ warned[e.bar] = { ...warned[e.bar] ?? {}, [e.stage]: e.resetsAt };
859
+ if (e.bar === "sevenDay" && e.stage === "hit")
860
+ lastWallResetsAt = e.resetsAt;
861
+ }
862
+ return { ...state, warned, ...lastWallResetsAt !== undefined ? { lastWallResetsAt } : {} };
863
+ }, CREDITS_PATTERN2, creditsHitIn2 = (text) => CREDITS_PATTERN2.test(text), transcriptPathFromStdin2 = (text) => {
864
+ try {
865
+ const raw = JSON.parse(text.trim());
866
+ return isObject11(raw) && typeof raw.transcript_path === "string" && raw.transcript_path ? raw.transcript_path : null;
867
+ } catch {
868
+ return null;
869
+ }
870
+ }, scanTranscriptForCredits2 = (path, prior) => {
871
+ const from = prior && prior.path === path ? prior.offset : 0;
872
+ try {
873
+ const size = statSync3(path).size;
874
+ if (size <= from)
875
+ return {
876
+ hit: false,
877
+ next: { path, offset: size, ...prior?.hitAt ? { hitAt: prior.hitAt } : {} }
878
+ };
879
+ const start = Math.max(from, size - 256 * 1024);
880
+ const fd = openSync2(path, "r");
881
+ try {
882
+ const buf = Buffer.alloc(size - start);
883
+ readSync2(fd, buf, 0, buf.length, start);
884
+ const hit = creditsHitIn2(buf.toString("utf8"));
885
+ return {
886
+ hit,
887
+ next: {
888
+ path,
889
+ offset: size,
890
+ ...hit ? { hitAt: Date.now() } : prior?.hitAt ? { hitAt: prior.hitAt } : {}
891
+ }
892
+ };
893
+ } finally {
894
+ closeSync2(fd);
895
+ }
896
+ } catch {
897
+ return { hit: false, next: prior && prior.path === path ? prior : { path, offset: 0 } };
898
+ }
744
899
  }, seatWindowBody2 = (w) => ({
745
900
  capturedAt: w.capturedAt,
746
901
  ...w.fiveHour ? { fiveHour: w.fiveHour } : {},
@@ -786,6 +941,7 @@ var init_seat = __esm(() => {
786
941
  init_config();
787
942
  init_meter();
788
943
  SEAT_POST_MIN_INTERVAL_MS2 = 5 * 60000;
944
+ CREDITS_PATTERN2 = /out of usage credits|cc_cli_limit_message/i;
789
945
  });
790
946
 
791
947
  // src/index.ts
@@ -1155,7 +1311,17 @@ var parsePayload = (payload) => {
1155
1311
  // src/seat.ts
1156
1312
  init_config();
1157
1313
  init_meter();
1158
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
1314
+ import {
1315
+ closeSync,
1316
+ existsSync as existsSync5,
1317
+ mkdirSync as mkdirSync5,
1318
+ openSync,
1319
+ readFileSync as readFileSync5,
1320
+ readSync,
1321
+ renameSync as renameSync4,
1322
+ statSync,
1323
+ writeFileSync as writeFileSync5
1324
+ } from "node:fs";
1159
1325
  import { join as join6 } from "node:path";
1160
1326
  var SEAT_POST_MIN_DELTA_PCT = 1;
1161
1327
  var SEAT_POST_MIN_INTERVAL_MS = 5 * 60000;
@@ -1227,7 +1393,7 @@ var seatWindowFromStdin = (text, capturedAt) => {
1227
1393
  return null;
1228
1394
  }
1229
1395
  };
1230
- var formatSeatFragment = (w) => {
1396
+ var formatSeatFragment = (w, now = Date.now()) => {
1231
1397
  if (!w)
1232
1398
  return null;
1233
1399
  const parts = [];
@@ -1239,8 +1405,20 @@ var formatSeatFragment = (w) => {
1239
1405
  parts.push(`spend ${Math.round(w.spendLimit.usedPct)}%`);
1240
1406
  if (parts.length === 0)
1241
1407
  return null;
1408
+ const hot = [
1409
+ ["wk", w.sevenDay],
1410
+ ["5h", w.fiveHour],
1411
+ ["spend", w.spendLimit]
1412
+ ].filter((x) => x[1] !== undefined && x[1].usedPct >= APPROACHING_PCT).sort((a, b) => b[1].usedPct - a[1].usedPct)[0];
1413
+ if (hot) {
1414
+ const mins = Math.max(0, Math.round((hot[1].resetsAt - now) / 60000));
1415
+ const reset = mins >= 60 ? `${Math.floor(mins / 60)}h${mins % 60 ? `${mins % 60}m` : ""}` : `${mins}m`;
1416
+ const verb = hot[1].usedPct >= 100 ? "at the wall" : `${Math.round(hot[1].usedPct)}%`;
1417
+ return `⚠ seat ${hot[0]} ${verb}${SEP}resets ${reset}${SEP}/sled handoff · sleds defer`;
1418
+ }
1242
1419
  return `seat ${parts.join(SEP)}`;
1243
1420
  };
1421
+ var APPROACHING_PCT = 90;
1244
1422
  var seatStatePath = (home) => join6(configDir(home), "seat.json");
1245
1423
  var parseWindowStored = (v) => {
1246
1424
  if (!isObject5(v))
@@ -1276,6 +1454,49 @@ var parseSeatState = (raw) => {
1276
1454
  const lastWallResetsAt = num3(raw.lastWallResetsAt);
1277
1455
  if (lastWallResetsAt !== undefined)
1278
1456
  out.lastWallResetsAt = lastWallResetsAt;
1457
+ if (isObject5(raw.warned)) {
1458
+ const warned = {};
1459
+ for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
1460
+ const w = raw.warned[bar];
1461
+ if (!isObject5(w))
1462
+ continue;
1463
+ const entry = {};
1464
+ const ap = num3(w.approaching);
1465
+ const hit = num3(w.hit);
1466
+ if (ap !== undefined)
1467
+ entry.approaching = ap;
1468
+ if (hit !== undefined)
1469
+ entry.hit = hit;
1470
+ warned[bar] = entry;
1471
+ }
1472
+ out.warned = warned;
1473
+ }
1474
+ if (isObject5(raw.told)) {
1475
+ const told = {};
1476
+ for (const bar of ["fiveHour", "sevenDay", "spendLimit", "credits"]) {
1477
+ const w = raw.told[bar];
1478
+ if (!isObject5(w))
1479
+ continue;
1480
+ const entry = {};
1481
+ const ap = num3(w.approaching);
1482
+ const hit = num3(w.hit);
1483
+ if (ap !== undefined)
1484
+ entry.approaching = ap;
1485
+ if (hit !== undefined)
1486
+ entry.hit = hit;
1487
+ told[bar] = entry;
1488
+ }
1489
+ out.told = told;
1490
+ }
1491
+ if (isObject5(raw.transcriptScan) && typeof raw.transcriptScan.path === "string") {
1492
+ const offset = num3(raw.transcriptScan.offset);
1493
+ const hitAt = num3(raw.transcriptScan.hitAt);
1494
+ out.transcriptScan = {
1495
+ path: raw.transcriptScan.path,
1496
+ offset: offset ?? 0,
1497
+ ...hitAt !== undefined ? { hitAt } : {}
1498
+ };
1499
+ }
1279
1500
  const session = parseSeatSession(raw.session);
1280
1501
  if (session)
1281
1502
  out.session = session;
@@ -1376,10 +1597,78 @@ var markPosted = (state, w, now) => {
1376
1597
  lastPosted.spendLimit = w.spendLimit.usedPct;
1377
1598
  return { ...state, window: w, lastPostedAt: now, lastPosted };
1378
1599
  };
1379
- var seatWallDue = (state, w) => {
1380
- if (!w?.sevenDay || w.sevenDay.usedPct < 100)
1381
- return false;
1382
- return state.lastWallResetsAt === undefined || state.lastWallResetsAt < w.sevenDay.resetsAt;
1600
+ var seatWallEvents = (state, w) => {
1601
+ if (!w)
1602
+ return [];
1603
+ const out = [];
1604
+ const bars = [
1605
+ ["fiveHour", w.fiveHour],
1606
+ ["sevenDay", w.sevenDay],
1607
+ ["spendLimit", w.spendLimit]
1608
+ ];
1609
+ for (const [bar, b] of bars) {
1610
+ if (!b)
1611
+ continue;
1612
+ const seen = state.warned?.[bar] ?? {};
1613
+ const weeklyOld = bar === "sevenDay" && state.lastWallResetsAt !== undefined && state.lastWallResetsAt >= b.resetsAt;
1614
+ if (b.usedPct >= APPROACHING_PCT && (seen.approaching ?? -1) < b.resetsAt && !weeklyOld)
1615
+ out.push({ bar, stage: "approaching", usedPct: b.usedPct, resetsAt: b.resetsAt });
1616
+ if (b.usedPct >= 100 && (seen.hit ?? -1) < b.resetsAt && !weeklyOld)
1617
+ out.push({ bar, stage: "hit", usedPct: b.usedPct, resetsAt: b.resetsAt });
1618
+ }
1619
+ return out;
1620
+ };
1621
+ var markWarned = (state, events) => {
1622
+ if (events.length === 0)
1623
+ return state;
1624
+ const warned = { ...state.warned ?? {} };
1625
+ let lastWallResetsAt = state.lastWallResetsAt;
1626
+ for (const e of events) {
1627
+ warned[e.bar] = { ...warned[e.bar] ?? {}, [e.stage]: e.resetsAt };
1628
+ if (e.bar === "sevenDay" && e.stage === "hit")
1629
+ lastWallResetsAt = e.resetsAt;
1630
+ }
1631
+ return { ...state, warned, ...lastWallResetsAt !== undefined ? { lastWallResetsAt } : {} };
1632
+ };
1633
+ var CREDITS_PATTERN = /out of usage credits|cc_cli_limit_message/i;
1634
+ var creditsHitIn = (text) => CREDITS_PATTERN.test(text);
1635
+ var transcriptPathFromStdin = (text) => {
1636
+ try {
1637
+ const raw = JSON.parse(text.trim());
1638
+ return isObject5(raw) && typeof raw.transcript_path === "string" && raw.transcript_path ? raw.transcript_path : null;
1639
+ } catch {
1640
+ return null;
1641
+ }
1642
+ };
1643
+ var scanTranscriptForCredits = (path, prior) => {
1644
+ const from = prior && prior.path === path ? prior.offset : 0;
1645
+ try {
1646
+ const size = statSync(path).size;
1647
+ if (size <= from)
1648
+ return {
1649
+ hit: false,
1650
+ next: { path, offset: size, ...prior?.hitAt ? { hitAt: prior.hitAt } : {} }
1651
+ };
1652
+ const start = Math.max(from, size - 256 * 1024);
1653
+ const fd = openSync(path, "r");
1654
+ try {
1655
+ const buf = Buffer.alloc(size - start);
1656
+ readSync(fd, buf, 0, buf.length, start);
1657
+ const hit = creditsHitIn(buf.toString("utf8"));
1658
+ return {
1659
+ hit,
1660
+ next: {
1661
+ path,
1662
+ offset: size,
1663
+ ...hit ? { hitAt: Date.now() } : prior?.hitAt ? { hitAt: prior.hitAt } : {}
1664
+ }
1665
+ };
1666
+ } finally {
1667
+ closeSync(fd);
1668
+ }
1669
+ } catch {
1670
+ return { hit: false, next: prior && prior.path === path ? prior : { path, offset: 0 } };
1671
+ }
1383
1672
  };
1384
1673
  var seatWindowBody = (w) => ({
1385
1674
  capturedAt: w.capturedAt,
@@ -2671,13 +2960,15 @@ var hooks = (o) => {
2671
2960
  const had = hasSeatHook(settings);
2672
2961
  const hadSpec = hasSpecHook(settings);
2673
2962
  const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
2674
- const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
2963
+ const hadPrompt = hasPromptHook(settings);
2964
+ const withSpec = applyPromptHook(applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env)), o.promptEventCommand ?? sledsCommand("prompt-event", o.home, o.env));
2675
2965
  const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
2676
2966
  writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
2677
2967
  log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
2678
2968
  if (hadLine)
2679
2969
  log(`refreshed statusLine in ${settingsPath(o.home)}`);
2680
2970
  log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
2971
+ log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o.home)}`);
2681
2972
  return 0;
2682
2973
  };
2683
2974
 
@@ -2832,13 +3123,15 @@ var hooks2 = (o) => {
2832
3123
  const had = hasSeatHook(settings);
2833
3124
  const hadSpec = hasSpecHook(settings);
2834
3125
  const withSeat = applySeatHook(settings, o.seatEventCommand ?? sledsCommand("seat-event", o.home, o.env));
2835
- const withSpec = applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env));
3126
+ const hadPrompt = hasPromptHook(settings);
3127
+ const withSpec = applyPromptHook(applySpecHook(withSeat, o.specEventCommand ?? sledsCommand("spec-event", o.home, o.env)), o.promptEventCommand ?? sledsCommand("prompt-event", o.home, o.env));
2836
3128
  const hadLine = o.refreshStatusLine !== false && isSledsStatusLine(withSpec[STATUS_LINE_KEY]);
2837
3129
  writeSettings(o.home, hadLine ? applyStatusLine(withSpec, sledsCommand("statusline", o.home, o.env)).settings : withSpec);
2838
3130
  log(`${had ? "refreshed" : "set"} hooks.Notification (${SEAT_EVENT_MATCHER}) in ${settingsPath(o.home)}`);
2839
3131
  if (hadLine)
2840
3132
  log(`refreshed statusLine in ${settingsPath(o.home)}`);
2841
3133
  log(`${hadSpec ? "refreshed" : "set"} hooks.PostToolUse (${SPEC_EVENT_MATCHER} → estimate spec files) in ${settingsPath(o.home)}`);
3134
+ log(`${hadPrompt ? "refreshed" : "set"} hooks.UserPromptSubmit (the seat speaks up in the session at 90 % and at a wall) in ${settingsPath(o.home)}`);
2842
3135
  return 0;
2843
3136
  };
2844
3137
 
@@ -3050,6 +3343,7 @@ var init = async (o) => {
3050
3343
  refreshStatusLine: false,
3051
3344
  seatEventCommand: o.seatEventCommand,
3052
3345
  specEventCommand: o.specEventCommand,
3346
+ promptEventCommand: o.promptEventCommand,
3053
3347
  log: (line) => wrote.push(line),
3054
3348
  error
3055
3349
  });
@@ -3570,6 +3864,93 @@ var on = (o) => {
3570
3864
  return 0;
3571
3865
  };
3572
3866
 
3867
+ // src/commands/promptEvent.ts
3868
+ init_config();
3869
+
3870
+ // src/commands/seatEvent.ts
3871
+ init_config();
3872
+ init_meter();
3873
+ var HOOK_STDIN_TIMEOUT_MS = 1000;
3874
+
3875
+ // src/commands/promptEvent.ts
3876
+ var LABEL = {
3877
+ fiveHour: "5-hour",
3878
+ sevenDay: "weekly",
3879
+ spendLimit: "spend",
3880
+ credits: "usage credits"
3881
+ };
3882
+ var fmtIn = (ms) => {
3883
+ const m = Math.max(1, Math.round(ms / 60000));
3884
+ if (m < 60)
3885
+ return `${m} min`;
3886
+ const h = Math.floor(m / 60);
3887
+ const r = m % 60;
3888
+ return r ? `${h} h ${r} min` : `${h} h`;
3889
+ };
3890
+ var untold = (state) => {
3891
+ const out = [];
3892
+ for (const bar of ["credits", "spendLimit", "sevenDay", "fiveHour"]) {
3893
+ const w = state.warned?.[bar];
3894
+ if (!w)
3895
+ continue;
3896
+ for (const stage of ["hit", "approaching"]) {
3897
+ const at = w[stage];
3898
+ if (at === undefined)
3899
+ continue;
3900
+ if ((state.told?.[bar]?.[stage] ?? -1) >= at)
3901
+ continue;
3902
+ out.push({ bar, stage, resetsAt: at });
3903
+ }
3904
+ }
3905
+ return out;
3906
+ };
3907
+ var exitsLine = (hasUpstreamKey) => hasUpstreamKey ? "`/sled handoff <what you were doing>` hands the work to a teammate or buddy; `sleds defer` resumes this session at the reset; `sleds on` continues now on the workspace's platform key." : "`/sled handoff <what you were doing>` hands the work to a teammate or buddy; `sleds defer` resumes this session at the reset (this workspace has no platform key to switch to).";
3908
+ 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.`;
3909
+ 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.`;
3910
+ 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.`;
3911
+ var promptEvent = async (o) => {
3912
+ const log = o.log ?? console.log;
3913
+ const error = o.error ?? console.error;
3914
+ const now = (o.now ?? Date.now)();
3915
+ if (o.stdin === undefined)
3916
+ await readStdin(HOOK_STDIN_TIMEOUT_MS);
3917
+ let state;
3918
+ try {
3919
+ state = readSeatState(o.home);
3920
+ } catch {
3921
+ return 0;
3922
+ }
3923
+ const pending = untold(state);
3924
+ if (pending.length === 0)
3925
+ return 0;
3926
+ const hasKey = readConfig(o.home).org?.capabilities.upstreamKey !== false;
3927
+ const told = { ...state.told ?? {} };
3928
+ const mark = (bar, stage, at) => {
3929
+ told[bar] = { ...told[bar] ?? {}, [stage]: at };
3930
+ };
3931
+ let code = 0;
3932
+ const lines = [];
3933
+ for (const e of pending) {
3934
+ mark(e.bar, e.stage, e.resetsAt);
3935
+ if (e.bar === "credits") {
3936
+ if (code !== 2) {
3937
+ error(creditsBlockText(hasKey));
3938
+ code = 2;
3939
+ }
3940
+ continue;
3941
+ }
3942
+ const pct = state.window?.[e.bar]?.usedPct;
3943
+ lines.push(e.stage === "approaching" ? approachingContext(e.bar, pct, e.resetsAt, hasKey, now) : hitContext(e.bar, e.resetsAt, hasKey, now));
3944
+ }
3945
+ try {
3946
+ writeSeatState(o.home, { ...state, told });
3947
+ } catch {}
3948
+ if (code === 0 && lines.length > 0)
3949
+ log(lines.join(`
3950
+ `));
3951
+ return code;
3952
+ };
3953
+
3573
3954
  // src/commands/run.ts
3574
3955
  init_config();
3575
3956
  init_meter();
@@ -3634,8 +4015,7 @@ var run = async (o) => {
3634
4015
  // src/commands/seatEvent.ts
3635
4016
  init_config();
3636
4017
  init_meter();
3637
- var HOOK_STDIN_TIMEOUT_MS = 1000;
3638
- var NOTIFICATION_TITLE = "sleds";
4018
+ var HOOK_STDIN_TIMEOUT_MS2 = 1000;
3639
4019
  var isObject8 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3640
4020
  var notificationType = (text) => {
3641
4021
  try {
@@ -3647,20 +4027,10 @@ var notificationType = (text) => {
3647
4027
  return;
3648
4028
  }
3649
4029
  };
3650
- var osascriptArgs = (title, body) => [
3651
- "-e",
3652
- "on run argv",
3653
- "-e",
3654
- "display notification (item 2 of argv) with title (item 1 of argv)",
3655
- "-e",
3656
- "end run",
3657
- title,
3658
- body
3659
- ];
3660
4030
  var seatEvent = async (o) => {
3661
4031
  const log = o.log ?? console.log;
3662
4032
  const now = (o.now ?? Date.now)();
3663
- const input = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS);
4033
+ const input = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS2);
3664
4034
  const kind = notificationType(input);
3665
4035
  try {
3666
4036
  const state = readSeatState(o.home);
@@ -3677,11 +4047,6 @@ var seatEvent = async (o) => {
3677
4047
  } catch {}
3678
4048
  const line = seatWallLine(readConfig(o.home).org?.capabilities.upstreamKey !== false);
3679
4049
  log(line);
3680
- if ((o.platform ?? process.platform) === "darwin" && o.exec) {
3681
- try {
3682
- o.exec("osascript", osascriptArgs(NOTIFICATION_TITLE, line));
3683
- } catch {}
3684
- }
3685
4050
  return 0;
3686
4051
  };
3687
4052
 
@@ -4222,9 +4587,9 @@ async function postSeat(proxyUrl, token, body, fetchFn) {
4222
4587
 
4223
4588
  // src/commands/specEvent.ts
4224
4589
  init_meter();
4225
- import { readFileSync as readFileSync10, statSync } from "node:fs";
4590
+ import { readFileSync as readFileSync10, statSync as statSync2 } from "node:fs";
4226
4591
  import { basename as basename2, isAbsolute, relative } from "node:path";
4227
- var HOOK_STDIN_TIMEOUT_MS2 = 1000;
4592
+ var HOOK_STDIN_TIMEOUT_MS3 = 1000;
4228
4593
  var ESTIMATE_TIMEOUT_MS = 4000;
4229
4594
  var MIN_SPEC_CHARS = 200;
4230
4595
  var MAX_SPEC_CHARS = 12000;
@@ -4276,7 +4641,7 @@ function projectKey(filePath, cwd) {
4276
4641
  }
4277
4642
  function readFileOrNull(path) {
4278
4643
  try {
4279
- if (statSync(path).size > 2000000)
4644
+ if (statSync2(path).size > 2000000)
4280
4645
  return null;
4281
4646
  return readFileSync10(path, "utf8");
4282
4647
  } catch {
@@ -4285,7 +4650,7 @@ function readFileOrNull(path) {
4285
4650
  }
4286
4651
  var specEvent = async (o) => {
4287
4652
  const log = o.log ?? console.log;
4288
- const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS2);
4653
+ const raw = o.stdin ?? await readStdin(HOOK_STDIN_TIMEOUT_MS3);
4289
4654
  const input = raw ? parseHookInput(raw) : null;
4290
4655
  if (!input || !isSpecPath(input.filePath))
4291
4656
  return 0;
@@ -4463,11 +4828,23 @@ var statusline = async (o) => {
4463
4828
  let posts = [];
4464
4829
  try {
4465
4830
  if (window)
4466
- posts = rememberSeat(o, window, session, now);
4467
- else if (session)
4468
- writeSeatState(o.home, { ...readSeatState(o.home), session });
4831
+ posts = rememberSeat(o, window, session, now, input);
4832
+ else {
4833
+ const st = readSeatState(o.home);
4834
+ const credits = creditsEvent(o, st, input, now);
4835
+ if (session || credits.scan) {
4836
+ const next = {
4837
+ ...st,
4838
+ ...session ? { session } : {},
4839
+ ...credits.scan ? { transcriptScan: credits.scan } : {}
4840
+ };
4841
+ writeSeatState(o.home, credits.event ? markWarned(next, [credits.event]) : next);
4842
+ }
4843
+ if (credits.event)
4844
+ posts = announce(o, [credits.event]);
4845
+ }
4469
4846
  } catch {}
4470
- const seatFragment = formatSeatFragment(window);
4847
+ const seatFragment = formatSeatFragment(window, now);
4471
4848
  let line;
4472
4849
  try {
4473
4850
  const meter2 = await loadMeter({
@@ -4484,29 +4861,50 @@ var statusline = async (o) => {
4484
4861
  await Promise.allSettled(posts);
4485
4862
  return 0;
4486
4863
  };
4487
- var rememberSeat = (o, window, session, now) => {
4864
+ var rememberSeat = (o, window, session, now, input) => {
4488
4865
  const prior = readSeatState(o.home);
4489
4866
  let state = { ...prior, window, ...session ? { session } : {} };
4490
4867
  const src = meterSource(o.home);
4491
4868
  const posts = [];
4492
- if (src) {
4493
- const opts = { ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS };
4494
- if (shouldPostSeat(prior, window, now)) {
4495
- state = markPosted(state, window, now);
4496
- posts.push(postSeatWindow(opts, window));
4497
- }
4498
- if (seatWallDue(prior, window) && window.sevenDay) {
4499
- state = { ...state, lastWallResetsAt: window.sevenDay.resetsAt };
4500
- posts.push(postSeatWall(opts, {
4501
- resetsAt: window.sevenDay.resetsAt,
4502
- usedPct: window.sevenDay.usedPct,
4503
- notification: "statusline"
4504
- }));
4505
- }
4506
- }
4869
+ if (src && shouldPostSeat(prior, window, now)) {
4870
+ state = markPosted(state, window, now);
4871
+ posts.push(postSeatWindow({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, window));
4872
+ }
4873
+ const events = seatWallEvents(prior, window);
4874
+ const credits = creditsEvent(o, prior, input, now);
4875
+ if (credits.scan)
4876
+ state = { ...state, transcriptScan: credits.scan };
4877
+ if (credits.event)
4878
+ events.push(credits.event);
4879
+ state = markWarned(state, events);
4880
+ posts.push(...announce(o, events));
4507
4881
  writeSeatState(o.home, state);
4508
4882
  return posts;
4509
4883
  };
4884
+ var creditsEvent = (o, prior, input, now) => {
4885
+ const path = transcriptPathFromStdin(input);
4886
+ if (!path)
4887
+ return { event: null, scan: undefined };
4888
+ const { hit, next } = scanTranscriptForCredits(path, prior.transcriptScan);
4889
+ const recent = prior.warned?.credits?.hit !== undefined && now - prior.warned.credits.hit < 6 * 3600000;
4890
+ if (!hit || recent)
4891
+ return { event: null, scan: next };
4892
+ return { event: { bar: "credits", stage: "hit", usedPct: 100, resetsAt: now }, scan: next };
4893
+ };
4894
+ var announce = (o, events) => {
4895
+ if (events.length === 0)
4896
+ return [];
4897
+ const src = meterSource(o.home);
4898
+ if (!src)
4899
+ return [];
4900
+ return events.map((e) => postSeatWall({ ...src, fetchFn: o.fetchFn, timeoutMs: o.timeoutMs ?? SEAT_POST_TIMEOUT_MS }, {
4901
+ bar: e.bar,
4902
+ stage: e.stage,
4903
+ usedPct: e.usedPct,
4904
+ ...e.bar === "credits" ? {} : { resetsAt: e.resetsAt },
4905
+ notification: e.bar === "credits" ? "transcript" : "statusline"
4906
+ }));
4907
+ };
4510
4908
 
4511
4909
  // src/commands/uninstall.ts
4512
4910
  import { spawnSync as spawnSync4 } from "node:child_process";
@@ -4593,6 +4991,11 @@ var uninstall2 = (o) => {
4593
4991
  changed = true;
4594
4992
  removed.push(`hooks.Notification seat-event entry from ${settingsPath(o.home)}`);
4595
4993
  }
4994
+ if (hasPromptHook(settings)) {
4995
+ settings = removePromptHook(settings);
4996
+ changed = true;
4997
+ removed.push(`hooks.UserPromptSubmit prompt-event entry from ${settingsPath(o.home)}`);
4998
+ }
4596
4999
  if (hasSpecHook(settings)) {
4597
5000
  settings = removeSpecHook(settings);
4598
5001
  changed = true;
@@ -4875,6 +5278,8 @@ commands:
4875
5278
  statusline Print the one-line meter Claude Code shows (installed by init; 10s cache).
4876
5279
  hooks Register the Claude Code Notification hook for seat limits (init does this too).
4877
5280
  seat-event Claude Code hook: record a seat wall and say how to continue. Reads hook JSON on stdin.
5281
+ prompt-event Claude Code UserPromptSubmit hook: at 90 % of a seat bar the seat speaks up in the
5282
+ session (once per bar per reset); out of usage credits blocks the prompt once with the exits.
4878
5283
  spec-event Claude Code PostToolUse hook: when a spec-shaped markdown file is written, size it
4879
5284
  (POST /proxy/estimate with the file path as the project). Silent unless it estimates.
4880
5285
  --version Print the CLI version.
@@ -4927,6 +5332,7 @@ var sledsArgv = () => {
4927
5332
  var shellQuote = (s) => /^[\w./-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
4928
5333
  var statusLineCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "statusline"].map(shellQuote).join(" "), home);
4929
5334
  var seatEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "seat-event"].map(shellQuote).join(" "), home);
5335
+ var promptEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "prompt-event"].map(shellQuote).join(" "), home);
4930
5336
  var specEventCommand = (argv, home = homedir()) => withSeatEnv2([...argv, "spec-event"].map(shellQuote).join(" "), home);
4931
5337
  var main = async (argv) => {
4932
5338
  const { cmd, flags } = parseArgs(argv);
@@ -4998,19 +5404,23 @@ var main = async (argv) => {
4998
5404
  prompt: promptStdin,
4999
5405
  statusLineCommand: statusLineCommand(sledsArgv()),
5000
5406
  seatEventCommand: seatEventCommand(sledsArgv()),
5001
- specEventCommand: specEventCommand(sledsArgv())
5407
+ specEventCommand: specEventCommand(sledsArgv()),
5408
+ promptEventCommand: promptEventCommand(sledsArgv())
5002
5409
  });
5003
5410
  }
5004
5411
  case "hooks":
5005
5412
  return hooks({
5006
5413
  home,
5007
5414
  seatEventCommand: seatEventCommand(sledsArgv()),
5008
- specEventCommand: specEventCommand(sledsArgv())
5415
+ specEventCommand: specEventCommand(sledsArgv()),
5416
+ promptEventCommand: promptEventCommand(sledsArgv())
5009
5417
  });
5010
5418
  case "seat-event":
5011
- return seatEvent({ home, platform: platform2(), exec: defaultExec });
5419
+ return seatEvent({ home });
5012
5420
  case "spec-event":
5013
5421
  return specEvent({ home });
5422
+ case "prompt-event":
5423
+ return promptEvent({ home });
5014
5424
  case "on":
5015
5425
  return on({
5016
5426
  home,
@@ -5052,7 +5462,7 @@ var main = async (argv) => {
5052
5462
  case "version":
5053
5463
  case "--version":
5054
5464
  case "-v":
5055
- console.log("sleds 0.1.17");
5465
+ console.log("sleds 0.1.19");
5056
5466
  return 0;
5057
5467
  default:
5058
5468
  console.error(`unknown command: ${cmd}
@@ -5081,6 +5491,7 @@ if (isMain()) {
5081
5491
  }
5082
5492
  export {
5083
5493
  parseArgs,
5494
+ promptEventCommand,
5084
5495
  seatEventCommand,
5085
5496
  specEventCommand,
5086
5497
  statusLineCommand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "SLEDS CLI \u2014 the cost and limits of AI agent work, decided at the wall. Point Claude Code at a SLEDS proxy; estimate before you run; hold, switch model, or run overnight when you hit a limit.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",