@sleds/cli 0.1.66 → 0.1.67

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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/sleds.mjs +225 -64
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -115,6 +115,8 @@ sleuth-export/
115
115
 
116
116
  Nested items carry `_parents` (the project and organization they came from). The exporter reads Sleuth's schema and builds every query from it, so a field Sleuth renames or drops is followed, not a crash. Re-run the same command to resume after a stop: finished past months are skipped (`--fresh` fetches them again). `--since`/`--until YYYY-MM`, `--only deploys,projects`, `--page-size`, `--depth`; `sleds sleuth` lists them all. The command exits 1 when a section is incomplete, and the manifest says which.
117
117
 
118
+ The key goes as `authorization: apikey <key>`; when Sleuth refuses it or answers PERMISSION_DENIED, the export tries `Bearer <key>` once and keeps whichever answers. `SLEUTH_AUTH=bearer` (or `apikey`) sends that scheme alone. A key Sleuth denies under both stops the export at once (exit 2) rather than splitting the month in search of a smaller query.
119
+
118
120
  ## Docs
119
121
 
120
122
  Repo: github.com/spdyadam/sleds · Site: sleds.ai
package/dist/sleds.mjs CHANGED
@@ -1640,10 +1640,26 @@ var init_outcome = __esm(() => {
1640
1640
  OUTCOME_WINDOW_MS = OUTCOME_WINDOW_DAYS * 86400000;
1641
1641
  });
1642
1642
 
1643
+ // ../../packages/shared/src/deploys/workingHours.ts
1644
+ var MIN_MS, DAY_MS2, WEEK_MS, DAY_MIN, WORKING_HOURS_DEFAULT;
1645
+ var init_workingHours = __esm(() => {
1646
+ MIN_MS = 60 * 1000;
1647
+ DAY_MS2 = 24 * 60 * MIN_MS;
1648
+ WEEK_MS = 7 * DAY_MS2;
1649
+ DAY_MIN = 24 * 60;
1650
+ WORKING_HOURS_DEFAULT = Object.freeze({
1651
+ on: false,
1652
+ days: Object.freeze([1, 2, 3, 4, 5]),
1653
+ start: 9 * 60,
1654
+ end: 17 * 60
1655
+ });
1656
+ });
1657
+
1643
1658
  // ../../packages/shared/src/deploys/metrics.ts
1644
- var WEEK_MS;
1659
+ var WEEK_MS2;
1645
1660
  var init_metrics = __esm(() => {
1646
- WEEK_MS = 7 * 24 * 60 * 60 * 1000;
1661
+ init_workingHours();
1662
+ WEEK_MS2 = 7 * 24 * 60 * 60 * 1000;
1647
1663
  });
1648
1664
 
1649
1665
  // ../../packages/shared/src/deploys/batchSize.ts
@@ -1666,9 +1682,9 @@ function periodChange(cur, prev, better) {
1666
1682
  const dir = pct > 0 ? "up" : "down";
1667
1683
  return { pct, dir, tone: dir === better ? "success" : "error" };
1668
1684
  }
1669
- var DAY_MS2, FLAT_PCT = 0.5;
1685
+ var DAY_MS3, FLAT_PCT = 0.5;
1670
1686
  var init_period = __esm(() => {
1671
- DAY_MS2 = 24 * 60 * 60 * 1000;
1687
+ DAY_MS3 = 24 * 60 * 60 * 1000;
1672
1688
  });
1673
1689
 
1674
1690
  // ../../packages/shared/src/deploys/report.ts
@@ -1681,23 +1697,28 @@ function duration(ms) {
1681
1697
  return `${hours.toFixed(1)}h`;
1682
1698
  return `${(hours / 24).toFixed(1)}d`;
1683
1699
  }
1684
- function change(pair, better) {
1700
+ function changeWord(pair, better) {
1685
1701
  const { current, previous } = pair;
1686
1702
  if (current == null)
1687
- return "";
1703
+ return null;
1688
1704
  if (previous == null)
1689
- return " (none before)";
1705
+ return "none before";
1690
1706
  const c = periodChange(current, previous, better);
1691
1707
  if (c.dir === "flat")
1692
- return " (flat)";
1708
+ return "flat";
1693
1709
  if (c.pct == null)
1694
- return " (none before)";
1695
- return ` (${c.pct > 0 ? "+" : "−"}${Math.round(Math.abs(c.pct))}%)`;
1710
+ return "none before";
1711
+ return `${c.pct > 0 ? "+" : "−"}${Math.round(Math.abs(c.pct))}%`;
1712
+ }
1713
+ function change(pair, better) {
1714
+ const word = changeWord(pair, better);
1715
+ return word == null ? "" : ` (${word})`;
1696
1716
  }
1697
- function timeFigure(label, pair) {
1717
+ function timeFigure(label, pair, note) {
1698
1718
  if (pair.current == null)
1699
1719
  return `${label} none`;
1700
- return `${label} ${duration(pair.current)}${change(pair, "down")}`;
1720
+ const notes = [note, changeWord(pair, "down")].filter(Boolean).join(", ");
1721
+ return `${label} ${duration(pair.current)}${notes ? ` (${notes})` : ""}`;
1701
1722
  }
1702
1723
  function rateFigure(pair) {
1703
1724
  const { current, previous } = pair;
@@ -1715,29 +1736,31 @@ function batchFigure(pair) {
1715
1736
  const before = total(pair.previous) === 0 ? "none before" : `was ${tiers(pair.previous)}`;
1716
1737
  return `batch ${tiers(pair.current)} (${before})`;
1717
1738
  }
1718
- function lineText(l) {
1739
+ function lineText(l, clock) {
1719
1740
  const head = `${l.name} (${l.slug})`;
1720
1741
  if (l.production == null)
1721
1742
  return `${head} · no production environment set, so no deploy metrics`;
1722
1743
  const deploys = `${l.deploys.current} deploy${l.deploys.current === 1 ? "" : "s"}${change(l.deploys, "up")}`;
1723
1744
  return [
1724
1745
  `${head} · ${l.production}: ${deploys}`,
1725
- timeFigure("lead time", l.leadTime),
1746
+ timeFigure("lead time", l.leadTime, clock === "working" ? "working hours" : undefined),
1726
1747
  rateFigure(l.failRate),
1727
1748
  timeFigure("time to recover", l.recover),
1728
1749
  batchFigure(l.batch)
1729
1750
  ].join(" · ");
1730
1751
  }
1731
1752
  function metricsText(r) {
1753
+ const clock = r.clock ?? "wall";
1732
1754
  const sameYear = r.from.slice(0, 4) === r.to.slice(0, 4);
1733
1755
  const span = `${dayLabel(r.from, !sameYear)} – ${dayLabel(r.to, true)}`;
1734
1756
  const days = `${r.days} day${r.days === 1 ? "" : "s"}`;
1735
- const header = `${r.scope === "org" ? "Org" : "Workspace"} · ${span} (${days}), against the ${days} before`;
1736
- const lines = r.lines.length ? r.lines.map(lineText) : [r.scope === "org" ? "No workspaces in this org." : "No workspace."];
1757
+ const window = clock === "working" ? ` · lead time in working hours${r.hours ? `, ${r.hours}` : ""}` : "";
1758
+ const header = `${r.scope === "org" ? "Org" : "Workspace"} · ${span} (${days}), against the ${days} before${window}`;
1759
+ const lines = r.lines.length ? r.lines.map((l) => lineText(l, clock)) : [r.scope === "org" ? "No workspaces in this org." : "No workspace."];
1737
1760
  return [header, ...lines].join(`
1738
1761
  `);
1739
1762
  }
1740
- var DAY_MS3, METRICS_RANGES, tiers = (c) => TIERS.map((t) => `${t}${c[t]}`).join(" "), total = (c) => TIERS.reduce((n, t) => n + c[t], 0), dayLabel = (day, year) => new Date(`${day}T00:00:00Z`).toLocaleDateString("en-US", {
1763
+ var DAY_MS4, METRICS_RANGES, tiers = (c) => TIERS.map((t) => `${t}${c[t]}`).join(" "), total = (c) => TIERS.reduce((n, t) => n + c[t], 0), dayLabel = (day, year) => new Date(`${day}T00:00:00Z`).toLocaleDateString("en-US", {
1741
1764
  month: "short",
1742
1765
  day: "numeric",
1743
1766
  ...year ? { year: "numeric" } : {},
@@ -1746,7 +1769,7 @@ var DAY_MS3, METRICS_RANGES, tiers = (c) => TIERS.map((t) => `${t}${c[t]}`).join
1746
1769
  var init_report = __esm(() => {
1747
1770
  init_batchSize();
1748
1771
  init_period();
1749
- DAY_MS3 = 24 * 60 * 60 * 1000;
1772
+ DAY_MS4 = 24 * 60 * 60 * 1000;
1750
1773
  METRICS_RANGES = [
1751
1774
  { key: "2w", days: 14 },
1752
1775
  { key: "1m", days: 30 },
@@ -2241,6 +2264,15 @@ var init_route = __esm(() => {
2241
2264
  };
2242
2265
  });
2243
2266
 
2267
+ // ../../packages/shared/src/route/verdict.ts
2268
+ function toneOf(x) {
2269
+ return typeof x === "string" && VERDICT_TONES.includes(x) ? x : undefined;
2270
+ }
2271
+ var VERDICT_TONES;
2272
+ var init_verdict = __esm(() => {
2273
+ VERDICT_TONES = ["go", "change", "stop"];
2274
+ });
2275
+
2244
2276
  // ../../packages/shared/src/mcp/annotations.ts
2245
2277
  function withAnnotations(tools) {
2246
2278
  return tools.map((t) => {
@@ -3698,6 +3730,8 @@ async function runTool(ctx, args) {
3698
3730
  const answer = a ? {
3699
3731
  md: str5(a.md),
3700
3732
  cites: strList(a.cites) ?? [],
3733
+ ...str5(a.verdict)?.trim() ? { verdict: str5(a.verdict)?.trim() } : {},
3734
+ ...toneOf(a.tone) ? { tone: toneOf(a.tone) } : {},
3701
3735
  ...typeof a.irreversible === "boolean" ? { irreversible: a.irreversible } : {}
3702
3736
  } : null;
3703
3737
  const r = isObject4(args.release) ? args.release : null;
@@ -4664,6 +4698,7 @@ var init_server = __esm(() => {
4664
4698
  init_brief();
4665
4699
  init_due();
4666
4700
  init_route();
4701
+ init_verdict();
4667
4702
  init_annotations();
4668
4703
  init_format();
4669
4704
  init_plan2();
@@ -5052,7 +5087,7 @@ var init_server = __esm(() => {
5052
5087
  },
5053
5088
  {
5054
5089
  name: "run",
5055
- description: "An engine's word on a run it holds (spec 26): with nothing but `runId`, a heartbeat that renews the lease (every few minutes while the session runs; a lapsed lease returns the node to ready). `park: {question}` when the session needs a person — the node parks with the question, the seat is free, the engine takes the next node. `fail: {log}` when the session exited without a PR — back to ready once, parked the second time. `watch` updates where to watch it. On a decider's (answerer's) run: `answer: {md, cites}` when the record settles the question, `release: {why, waitsOn, cites}` when the record says the node must wait on other items (its session stops, the node goes back to ready without a seat, and it is not taken again until those land), `escalate: {why, suggested?, forPerson?}` when it does not — `forPerson: {relation: {key, how}}` when the fix is a relation only a person can set (`blockedBy SL-1474`), which the person's card then sets in one press — SLEDS may refuse an answer and escalate it, and says why. When the PR opens: `actual` (or `land`) with `node` and state `pr`; the merge lands the node from the PR body's `SLEDS-Node:` line.",
5090
+ description: "An engine's word on a run it holds (spec 26): with nothing but `runId`, a heartbeat that renews the lease (every few minutes while the session runs; a lapsed lease returns the node to ready). `park: {question}` when the session needs a person — the node parks with the question, the seat is free, the engine takes the next node. `fail: {log}` when the session exited without a PR — back to ready once, parked the second time. `watch` updates where to watch it. On a decider's (answerer's) run: `answer: {verdict, tone, md, cites}` when the record settles the question, `release: {why, waitsOn, cites}` when the record says the node must wait on other items (its session stops, the node goes back to ready without a seat, and it is not taken again until those land), `escalate: {why, suggested?, forPerson?}` when it does not — `forPerson: {relation: {key, how}}` when the fix is a relation only a person can set (`blockedBy SL-1474`), which the person's card then sets in one press — SLEDS may refuse an answer and escalate it, and says why. When the PR opens: `actual` (or `land`) with `node` and state `pr`; the merge lands the node from the PR body's `SLEDS-Node:` line.",
5056
5091
  inputSchema: {
5057
5092
  type: "object",
5058
5093
  properties: {
@@ -5078,6 +5113,15 @@ var init_server = __esm(() => {
5078
5113
  description: "A decider's answer, on its own run: the record settles the question.",
5079
5114
  properties: {
5080
5115
  md: { type: "string", description: "The answer as you would tell the session." },
5116
+ verdict: {
5117
+ type: "string",
5118
+ description: "SL-2172: the verdict on one line, in plain words — md's first line. The card leads with it."
5119
+ },
5120
+ tone: {
5121
+ type: "string",
5122
+ enum: ["go", "change", "stop"],
5123
+ description: "go: go ahead as filed · change: change the plan · stop: stop or hold."
5124
+ },
5081
5125
  cites: {
5082
5126
  type: "array",
5083
5127
  items: { type: "string" },
@@ -6821,11 +6865,11 @@ var init_deploys = __esm(() => {
6821
6865
  });
6822
6866
 
6823
6867
  // ../../packages/shared/src/deploys/series.ts
6824
- var DAY_MS4, WEEK_MS2;
6868
+ var DAY_MS5, WEEK_MS3;
6825
6869
  var init_series = __esm(() => {
6826
6870
  init_metrics();
6827
- DAY_MS4 = 24 * 60 * 60 * 1000;
6828
- WEEK_MS2 = 7 * DAY_MS4;
6871
+ DAY_MS5 = 24 * 60 * 60 * 1000;
6872
+ WEEK_MS3 = 7 * DAY_MS5;
6829
6873
  });
6830
6874
 
6831
6875
  // ../../packages/shared/src/deploys/stability.ts
@@ -6836,27 +6880,40 @@ var init_stability = __esm(() => {
6836
6880
  });
6837
6881
 
6838
6882
  // ../../packages/shared/src/deploys/shipping.ts
6839
- var DAY_MS5;
6883
+ var DAY_MS6;
6840
6884
  var init_shipping = __esm(() => {
6841
- DAY_MS5 = 24 * 60 * 60 * 1000;
6885
+ DAY_MS6 = 24 * 60 * 60 * 1000;
6842
6886
  });
6843
6887
 
6844
6888
  // ../../packages/shared/src/deploys/doraBands.ts
6845
- var HOUR_MS2, DAY_MS6, WEEK_MS3, MONTH_DAYS = 30, MONTH_MS;
6889
+ var HOUR_MS2, DAY_MS7, WEEK_MS4, MONTH_DAYS = 30, MONTH_MS;
6846
6890
  var init_doraBands = __esm(() => {
6847
6891
  HOUR_MS2 = 60 * 60 * 1000;
6848
- DAY_MS6 = 24 * HOUR_MS2;
6849
- WEEK_MS3 = 7 * DAY_MS6;
6850
- MONTH_MS = MONTH_DAYS * DAY_MS6;
6892
+ DAY_MS7 = 24 * HOUR_MS2;
6893
+ WEEK_MS4 = 7 * DAY_MS7;
6894
+ MONTH_MS = MONTH_DAYS * DAY_MS7;
6851
6895
  });
6852
6896
 
6853
6897
  // ../../packages/shared/src/deploys/trends.ts
6854
- var DAY_MS7;
6898
+ var DAY_MS8;
6855
6899
  var init_trends = __esm(() => {
6856
6900
  init_batchSize();
6857
6901
  init_metrics();
6858
6902
  init_series();
6859
- DAY_MS7 = 24 * 60 * 60 * 1000;
6903
+ DAY_MS8 = 24 * 60 * 60 * 1000;
6904
+ });
6905
+
6906
+ // ../../packages/shared/src/deploys/sleuth.ts
6907
+ var SLEUTH_WORKING_HOURS;
6908
+ var init_sleuth = __esm(() => {
6909
+ init_deploys();
6910
+ init_workingHours();
6911
+ SLEUTH_WORKING_HOURS = Object.freeze({
6912
+ on: true,
6913
+ days: Object.freeze([1, 2, 3, 4, 5]),
6914
+ start: 14 * 60,
6915
+ end: 23 * 60
6916
+ });
6860
6917
  });
6861
6918
 
6862
6919
  // ../../packages/shared/src/newrelic/newrelic.ts
@@ -7489,7 +7546,7 @@ function eventFrame(o) {
7489
7546
  "",
7490
7547
  "## Post one verdict, then stop",
7491
7548
  "",
7492
- `- ${o.answer} Post it with the SLEDS \`${run}\` tool, \`{runId: "${a.runId}", answer: {md, cites}}\` — \`md\` as the session it goes to would read it, \`cites\` the list of pointers.`,
7549
+ `- ${o.answer} Post it with the SLEDS \`${run}\` tool, \`{runId: "${a.runId}", answer: {verdict, tone, md, cites}}\` — \`md\` as the session it goes to would read it, \`cites\` the list of pointers.`,
7493
7550
  `- ${o.escalate} Post it with \`${run}\`, \`{runId: "${a.runId}", escalate: {why, suggested?, cites?, irreversible?}}\` — \`why\` in one line for the person (with any action only a person can take), \`suggested\` your best answer as the session would be told it, when you have one. Mark \`irreversible: true\` when it is.`,
7494
7551
  ...VERDICT_SHAPE,
7495
7552
  "- Read what the tool answers: an answer can come back `escalated` with `refused` (irreversible, no cites, or the autonomy is ask). That is SLEDS's call, not a failure; do not post again.",
@@ -7701,6 +7758,7 @@ var init_engine = __esm(() => {
7701
7758
  };
7702
7759
  VERDICT_SHAPE = [
7703
7760
  "- **Verdict first.** The first line of `md` (and of `why`) is the verdict on its own, in plain words: what to do, or what is so. No file paths, line numbers or links in it.",
7761
+ "- On an answer, state that line as `verdict` too, and its `tone`: `go` (go ahead as filed), `change` (change the plan) or `stop` (stop or hold).",
7704
7762
  "- Then at most 3 short points, one line each, as a `-` list. Cites go in `cites`, not in the prose.",
7705
7763
  "- **One cite per span, no prose in it.** Code is `code:<path>:<line>`, the path from the repo root (`code:apps/web/src/App.tsx:42`); cite each line on its own, the cards merge them by file."
7706
7764
  ];
@@ -8379,6 +8437,8 @@ var init_src = __esm(() => {
8379
8437
  init_shipping();
8380
8438
  init_doraBands();
8381
8439
  init_trends();
8440
+ init_workingHours();
8441
+ init_sleuth();
8382
8442
  init_sentry();
8383
8443
  init_newrelic();
8384
8444
  init_musher();
@@ -8390,6 +8450,7 @@ var init_src = __esm(() => {
8390
8450
  init_say();
8391
8451
  init_engine();
8392
8452
  init_sources();
8453
+ init_verdict();
8393
8454
  init_otlp();
8394
8455
  init_session();
8395
8456
  init_effort();
@@ -9153,6 +9214,8 @@ init_series();
9153
9214
  init_shipping();
9154
9215
  init_doraBands();
9155
9216
  init_trends();
9217
+ init_workingHours();
9218
+ init_sleuth();
9156
9219
  init_sentry();
9157
9220
  init_newrelic();
9158
9221
  init_musher();
@@ -9164,6 +9227,7 @@ init_reshape();
9164
9227
  init_say();
9165
9228
  init_engine();
9166
9229
  init_sources();
9230
+ init_verdict();
9167
9231
  init_otlp();
9168
9232
  init_session();
9169
9233
  init_effort();
@@ -11822,7 +11886,7 @@ function beatBody(state, flags, host = {}, cap = {}) {
11822
11886
  name: flags.name,
11823
11887
  os: host.os ?? process.platform,
11824
11888
  runner: flags.runner,
11825
- version: host.version ?? "0.1.66" ?? "dev",
11889
+ version: host.version ?? "0.1.67" ?? "dev",
11826
11890
  running: live.length,
11827
11891
  items: live.slice(0, BEAT_ITEMS).map((r) => `${r.item}/${r.node}`),
11828
11892
  ...cap.atOnce !== undefined ? { atOnce: cap.atOnce } : {},
@@ -12938,7 +13002,7 @@ function answerPrompt(o) {
12938
13002
  "",
12939
13003
  "## Post one verdict, then stop",
12940
13004
  "",
12941
- `- Settled by the record: the SLEDS \`${run}\` tool with \`{runId: "${c.runId}", answer: {md, cites}}\` — \`md\` is the answer as you would tell the session, \`cites\` the list of pointers.`,
13005
+ `- Settled by the record: the SLEDS \`${run}\` tool with \`{runId: "${c.runId}", answer: {verdict, tone, md, cites}}\` — \`md\` is the answer as you would tell the session, \`cites\` the list of pointers.`,
12942
13006
  `- The record says node ${c.node} must wait for another item to land ("build nothing until #123 merges") and that item is on the board: \`${run}\` with \`{runId: "${c.runId}", release: {why, waitsOn, cites}}\` — \`waitsOn\` the keys of the items it waits on (\`SL-123\`). It frees the seat: node ${c.node}'s session is closed, and the node is retaken fresh once they land. Release rather than answer "stay parked", which only parks it again.`,
12943
13007
  `- Not settled, irreversible, or unsure: \`${run}\` with \`{runId: "${c.runId}", escalate: {why, suggested?, cites?, irreversible?}}\` — \`why\` in one line for the person (with any action only a person can take), \`suggested\` your best answer as the session would be told it, when you have one. Mark \`irreversible: true\` when it is, even if the session did not.`,
12944
13008
  `- When the fix is a relation on the item only a person can set (node ${c.node} looks like it waits on SL-1474, but the record does not settle it, so it is not a release), escalate with \`forPerson: {relation: {key, how}}\` — \`{key: "SL-1474", how: "blockedBy"}\` — so the person's card sets it in one press. Never tell the session to set it: a session cannot set relations.`,
@@ -18640,7 +18704,7 @@ async function fetchVersion(o) {
18640
18704
  const platform = o.platform ?? process.platform;
18641
18705
  const arch = o.arch ?? process.arch;
18642
18706
  const publicKey = o.publicKey ?? RELEASE_PUBLIC_KEY;
18643
- const running = o.running ?? "0.1.66";
18707
+ const running = o.running ?? "0.1.67";
18644
18708
  if (!publicKey)
18645
18709
  return { ok: false, why: "no release key yet" };
18646
18710
  if (!VERSION_RE2.test(o.version))
@@ -19866,7 +19930,7 @@ function engineUpdater(o) {
19866
19930
 
19867
19931
  // src/version.ts
19868
19932
  function cliVersion(env) {
19869
- return "0.1.66";
19933
+ return "0.1.67";
19870
19934
  }
19871
19935
 
19872
19936
  // src/commands/update.ts
@@ -20252,7 +20316,7 @@ function engineClient(o, org, breaker) {
20252
20316
  ...src,
20253
20317
  ...breaker ? { fetchFn: breaker.fetch } : {},
20254
20318
  timeoutMs: ENGINE_TIMEOUT_MS,
20255
- version: "0.1.66"
20319
+ version: "0.1.67"
20256
20320
  });
20257
20321
  const asked = org?.trim() || env.SLEDS_ORG?.trim();
20258
20322
  if (asked && !selectOrg(readConfig(o.home), org, env).seat)
@@ -20558,7 +20622,7 @@ async function driveEngine(o, flags, deps, until = {}) {
20558
20622
  runner: deps.runner.name,
20559
20623
  atOnce: flags.atOnce,
20560
20624
  interval: flags.interval,
20561
- cliVersion: "0.1.66"
20625
+ cliVersion: "0.1.67"
20562
20626
  };
20563
20627
  const beat = (checkedAt) => {
20564
20628
  const m = deps.machine;
@@ -22427,7 +22491,7 @@ function engineClient2(o, org, breaker) {
22427
22491
  ...src,
22428
22492
  ...breaker ? { fetchFn: breaker.fetch } : {},
22429
22493
  timeoutMs: ENGINE_TIMEOUT_MS2,
22430
- version: "0.1.66"
22494
+ version: "0.1.67"
22431
22495
  });
22432
22496
  const asked = org?.trim() || env.SLEDS_ORG?.trim();
22433
22497
  if (asked && !selectOrg(readConfig(o.home), org, env).seat)
@@ -22768,7 +22832,7 @@ async function driveEngine2(o, flags, deps, until = {}) {
22768
22832
  runner: deps.runner.name,
22769
22833
  atOnce: flags.atOnce,
22770
22834
  interval: flags.interval,
22771
- cliVersion: "0.1.66"
22835
+ cliVersion: "0.1.67"
22772
22836
  };
22773
22837
  const beat = (checkedAt) => {
22774
22838
  const m = deps.machine;
@@ -24879,7 +24943,7 @@ function contextFor(deps) {
24879
24943
  ...src,
24880
24944
  fetchFn: deps.fetchFn,
24881
24945
  timeoutMs: deps.timeoutMs,
24882
- version: "0.1.66"
24946
+ version: "0.1.67"
24883
24947
  }), deps.home, now) : null;
24884
24948
  return {
24885
24949
  channel: "code",
@@ -28414,6 +28478,15 @@ class SleuthError extends Error {
28414
28478
  this.adaptable = adaptable;
28415
28479
  }
28416
28480
  }
28481
+
28482
+ class SleuthDenied extends SleuthError {
28483
+ constructor(message) {
28484
+ super(message, false);
28485
+ }
28486
+ }
28487
+ var DENIED = /\bPERMISSION_DENIED\b/;
28488
+ var isDenial = (e) => DENIED.test(e.message) || typeof e.extensions?.code === "string" && DENIED.test(e.extensions.code);
28489
+ var deniedOutright = (data, errors) => errors.some(isDenial) && (!data || Object.values(data).every((v) => v === null));
28417
28490
  var RATE = /rate.?limit|throttl|too many requests/i;
28418
28491
  var backoffMs = (attempt, retryAfter) => {
28419
28492
  const s = Number(retryAfter);
@@ -28427,11 +28500,17 @@ var sleuthClient = (o) => {
28427
28500
  const sleep = o.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
28428
28501
  const retries = o.retries ?? 6;
28429
28502
  const timeoutMs = o.timeoutMs ?? 120000;
28430
- let scheme = "apikey";
28431
- let settled = false;
28503
+ const forced = o.scheme !== undefined;
28504
+ let scheme = o.scheme ?? "apikey";
28505
+ let settled = forced;
28506
+ let deniedOnce = false;
28507
+ let kept = forced;
28432
28508
  let count = 0;
28509
+ const other = (s) => s === "apikey" ? "Bearer" : "apikey";
28433
28510
  const request = async (query) => {
28434
28511
  let lastWhy = "";
28512
+ const startedAs = scheme;
28513
+ let switched = false;
28435
28514
  for (let attempt = 0;attempt <= retries; attempt++) {
28436
28515
  let res;
28437
28516
  count++;
@@ -28455,7 +28534,7 @@ var sleuthClient = (o) => {
28455
28534
  continue;
28456
28535
  }
28457
28536
  if (res.status === 401 || res.status === 403)
28458
- throw new SleuthError(`Sleuth refused the key (HTTP ${res.status}) — check SLEUTH_API_KEY`, false);
28537
+ throw new SleuthError(`Sleuth refused the key (HTTP ${res.status})${forced ? ` as ${scheme} (SLEUTH_AUTH)` : ""} — check SLEUTH_API_KEY`, false);
28459
28538
  if (res.status === 429 || res.status >= 500) {
28460
28539
  lastWhy = `HTTP ${res.status}`;
28461
28540
  const wait = backoffMs(attempt, res.headers.get("retry-after"));
@@ -28474,16 +28553,36 @@ var sleuthClient = (o) => {
28474
28553
  if (res.status >= 400 && !errors.length)
28475
28554
  throw new SleuthError(`Sleuth answered HTTP ${res.status}`, false);
28476
28555
  settled = true;
28477
- if (!body.data && errors.some((e) => RATE.test(e.message))) {
28556
+ const data = body.data ?? null;
28557
+ if (!data && errors.some((e) => RATE.test(e.message))) {
28478
28558
  lastWhy = errors[0]?.message ?? "rate limited";
28479
28559
  await sleep(backoffMs(attempt, null));
28480
28560
  continue;
28481
28561
  }
28482
- return { data: body.data ?? null, errors };
28562
+ if (deniedOutright(data, errors)) {
28563
+ deniedOnce = true;
28564
+ const root = errors.find(isDenial)?.path?.[0];
28565
+ const what = typeof root === "string" ? ` to ${root}` : "";
28566
+ if (!kept && !switched) {
28567
+ switched = true;
28568
+ scheme = other(scheme);
28569
+ o.log?.(`sleuth: PERMISSION_DENIED${what} as ${startedAs} — trying ${scheme}`);
28570
+ attempt--;
28571
+ continue;
28572
+ }
28573
+ if (switched) {
28574
+ scheme = startedAs;
28575
+ throw new SleuthDenied(`Sleuth answered PERMISSION_DENIED${what} as both apikey and Bearer — the key cannot read it; check its scopes in Sleuth › Settings › API`);
28576
+ }
28577
+ throw new SleuthDenied(forced ? `Sleuth answered PERMISSION_DENIED${what} as ${scheme} (SLEUTH_AUTH) — unset SLEUTH_AUTH to try both schemes, or check the key's scopes` : `Sleuth answered PERMISSION_DENIED${what} as ${scheme}, the scheme its other queries take — the key cannot read it`);
28578
+ }
28579
+ if (deniedOnce)
28580
+ kept = true;
28581
+ return { data, errors };
28483
28582
  }
28484
28583
  throw new SleuthError(`Sleuth did not answer after ${retries + 1} tries (${lastWhy})`, true);
28485
28584
  };
28486
- return { request, calls: () => count };
28585
+ return { request, calls: () => count, scheme: () => scheme };
28487
28586
  };
28488
28587
 
28489
28588
  // src/sleuth/schema.ts
@@ -28544,9 +28643,20 @@ var START = /^(start|startDate|startTime|startAt|from|fromDate|dateFrom|since|be
28544
28643
  var END = /^(end|endDate|endTime|endAt|to|toDate|dateTo|until|before)$/i;
28545
28644
  var CURSOR = /^(after|cursor|nextCursor|pageCursor|pageToken)$/i;
28546
28645
  var PAGE = /^(page|pageNumber|pageNum)$/i;
28547
- var SIZE = /^(first|pageSize|limit|perPage|per_page|size|count|pageLimit)$/i;
28646
+ var SIZE_NAMES = [
28647
+ "pagesize",
28648
+ "first",
28649
+ "limit",
28650
+ "perpage",
28651
+ "per_page",
28652
+ "pagelimit",
28653
+ "size",
28654
+ "count"
28655
+ ];
28656
+ var sizeRank = (name) => SIZE_NAMES.indexOf(name.toLowerCase());
28548
28657
  var OFFSET = /^(offset|skip)$/i;
28549
28658
  var DATEISH = /date|time/i;
28659
+ var NUMERIC = /^(Int|Float|Long|BigInt|PositiveInt|NonNegativeInt)$/i;
28550
28660
  var dateArg = (path, typeName) => ({
28551
28661
  path,
28552
28662
  dateOnly: /^date$/i.test(typeName)
@@ -28554,18 +28664,21 @@ var dateArg = (path, typeName) => ({
28554
28664
  var planArgs = (s, args) => {
28555
28665
  const plan = { org: [], missing: [] };
28556
28666
  const visit = (vals, prefix, depth) => {
28667
+ const unfilled = [];
28557
28668
  for (const a of vals) {
28558
28669
  const u = unwrap(a.type);
28559
28670
  const path = [...prefix, a.name];
28560
28671
  const leaf = isLeaf(s, u.name);
28672
+ const numeric = leaf && !u.list && NUMERIC.test(u.name);
28561
28673
  const dated = leaf && (DATEISH.test(u.name) || u.name === "String" && DATEISH.test(a.name) && (START.test(a.name) || END.test(a.name)));
28562
28674
  if (kindOf(s, u.name) === "INPUT_OBJECT") {
28563
28675
  const before = JSON.stringify(plan);
28564
28676
  const inputs = s.types.get(u.name)?.inputFields ?? [];
28565
- if (depth < 3)
28566
- visit(inputs, path, depth + 1);
28567
- if (isRequired(a) && JSON.stringify(plan) === before && inputs.some(isRequired))
28568
- plan.missing.push(path.join("."));
28677
+ const inner = depth < 3 ? visit(inputs, path, depth + 1) : [];
28678
+ if (JSON.stringify(plan) !== before)
28679
+ unfilled.push(...inner);
28680
+ else if (isRequired(a) && inputs.some(isRequired))
28681
+ unfilled.push(path.join("."));
28569
28682
  continue;
28570
28683
  }
28571
28684
  if (leaf && !u.list && ORG.test(a.name))
@@ -28576,17 +28689,20 @@ var planArgs = (s, args) => {
28576
28689
  plan.end = dateArg(path, u.name);
28577
28690
  else if (leaf && !dated && CURSOR.test(a.name) && !plan.cursor)
28578
28691
  plan.cursor = path;
28579
- else if (leaf && PAGE.test(a.name) && !plan.page)
28692
+ else if (numeric && PAGE.test(a.name) && !plan.page)
28580
28693
  plan.page = { path, first: Number(a.defaultValue ?? 1) || 0 };
28581
- else if (leaf && SIZE.test(a.name) && !plan.size)
28582
- plan.size = path;
28583
- else if (leaf && OFFSET.test(a.name) && !plan.offset)
28694
+ else if (numeric && sizeRank(a.name) >= 0) {
28695
+ const held = plan.size?.[plan.size.length - 1];
28696
+ if (!held || sizeRank(a.name) < sizeRank(held))
28697
+ plan.size = path;
28698
+ } else if (numeric && OFFSET.test(a.name) && !plan.offset)
28584
28699
  plan.offset = path;
28585
28700
  else if (isRequired(a))
28586
- plan.missing.push(path.join("."));
28701
+ unfilled.push(path.join("."));
28587
28702
  }
28703
+ return unfilled;
28588
28704
  };
28589
- visit(args, [], 0);
28705
+ plan.missing = visit(args, [], 0);
28590
28706
  return plan;
28591
28707
  };
28592
28708
  var windowed = (p) => !!(p.start && p.end);
@@ -28908,6 +29024,23 @@ var readJsonl = (path) => readFileSync32(path, "utf8").split(`
28908
29024
  `).filter((l) => l.trim()).map((l) => JSON.parse(l));
28909
29025
  var CANNOT_QUERY = /Cannot query field "(\w+)" on type "(\w+)"/;
28910
29026
  var TOO_DEEP = /\bmax(imum)?\b.{0,24}\bdepth\b|\btoo deep\b/i;
29027
+ var PERMISSION_DENIED = /\bPERMISSION_DENIED\b/;
29028
+ var ORG_DENIED_NOTE = "Sleuth refused this key the organization object. Deploys come through orgChanges and do not need it; what is read under the organization (projects, teams, labels, metricsRecap counts) is missing from this export.";
29029
+ var whyNotFound = (schema, patterns) => {
29030
+ for (const re of patterns)
29031
+ for (const [type, t] of schema.types) {
29032
+ const f = (t.fields ?? []).find((x) => re.test(x.name));
29033
+ if (!f)
29034
+ continue;
29035
+ const args = planArgs(schema, f.args);
29036
+ if (args.missing.length)
29037
+ return `${type}.${f.name} needs ${args.missing.join(", ")}, which the export cannot fill`;
29038
+ if (type === schema.query && !args.org.length)
29039
+ return `${type}.${f.name} takes no org argument`;
29040
+ return `${type}.${f.name} is not reachable from the org`;
29041
+ }
29042
+ return "Sleuth's schema has no field by that name";
29043
+ };
28911
29044
  var exportSleuth = async (o) => {
28912
29045
  const log = o.log ?? (() => {});
28913
29046
  const now = (o.now ?? (() => new Date))();
@@ -28954,6 +29087,7 @@ var exportSleuth = async (o) => {
28954
29087
  const schema = indexSchema(intro.data);
28955
29088
  manifest.schema.types = schema.types.size;
28956
29089
  const fitDepth = new Map;
29090
+ let answered = false;
28957
29091
  const ask = async (build, steps) => {
28958
29092
  const keep = structural(steps);
28959
29093
  const root = steps[0]?.field.name ?? "";
@@ -28961,8 +29095,10 @@ var exportSleuth = async (o) => {
28961
29095
  let depth = fitDepth.get(path) ?? o.depth;
28962
29096
  for (let i = 0;i < 25; i++) {
28963
29097
  const r = await o.client.request(build(depth));
28964
- if (r.data && !r.errors.length)
29098
+ if (r.data && !r.errors.length) {
29099
+ answered = true;
28965
29100
  return r.data;
29101
+ }
28966
29102
  const tooDeep = r.errors.some((e) => TOO_DEEP.test(e.message));
28967
29103
  if (!r.data && tooDeep && depth > 0) {
28968
29104
  depth--;
@@ -28986,12 +29122,16 @@ var exportSleuth = async (o) => {
28986
29122
  continue;
28987
29123
  }
28988
29124
  const why = r.errors.slice(0, 3).map((e) => e.message).join("; ");
28989
- if (empty)
29125
+ if (empty) {
29126
+ if (r.errors.some(isDenial))
29127
+ throw new SleuthDenied(why);
28990
29128
  throw new SleuthError(why || "Sleuth answered with no data", !tooDeep);
29129
+ }
28991
29130
  for (const e of r.errors) {
28992
29131
  const tf = e.path ? typeFieldAt(schema, e.path) : undefined;
28993
29132
  warn(`${tf ? `${tf}: ` : ""}${e.message}`);
28994
29133
  }
29134
+ answered = true;
28995
29135
  return r.data;
28996
29136
  }
28997
29137
  throw new SleuthError("Sleuth kept refusing fields after 25 tries", false);
@@ -29136,10 +29276,11 @@ var exportSleuth = async (o) => {
29136
29276
  }
29137
29277
  };
29138
29278
  const months = monthsBetween(o.since, o.until);
29279
+ let denial;
29139
29280
  const runSection = async (sec) => {
29140
29281
  const steps = findSection(schema, sec.patterns);
29141
29282
  if (!steps)
29142
- return { found: false, why: "Sleuth's schema has no field by that name" };
29283
+ return { found: false, why: whyNotFound(schema, sec.patterns) };
29143
29284
  const leaf = steps[steps.length - 1];
29144
29285
  const res = {
29145
29286
  found: true,
@@ -29195,6 +29336,11 @@ var exportSleuth = async (o) => {
29195
29336
  const why = e instanceof Error ? e.message : String(e);
29196
29337
  res.months.push({ month, file, count: 0, error: why });
29197
29338
  log(`${sec.name} ${month}: failed — ${why}`);
29339
+ if (e instanceof SleuthDenied) {
29340
+ res.error = `stopped at ${month} — ${why}`;
29341
+ denial = e;
29342
+ break;
29343
+ }
29198
29344
  if (++failedInARow >= MAX_FAILED_MONTHS) {
29199
29345
  res.error = `${MAX_FAILED_MONTHS} months in a row failed; stopped at ${month} — ${why}`;
29200
29346
  break;
@@ -29249,7 +29395,8 @@ var exportSleuth = async (o) => {
29249
29395
  `);
29250
29396
  manifest.organization = { file: "organization.json" };
29251
29397
  } catch (e) {
29252
- manifest.organization = { error: e instanceof Error ? e.message : String(e) };
29398
+ const error = e instanceof Error ? e.message : String(e);
29399
+ manifest.organization = e instanceof SleuthDenied || PERMISSION_DENIED.test(error) ? { error, note: ORG_DENIED_NOTE } : { error };
29253
29400
  }
29254
29401
  } else
29255
29402
  manifest.organization = { error: "Sleuth's schema has no organization(orgSlug) field" };
@@ -29261,6 +29408,8 @@ var exportSleuth = async (o) => {
29261
29408
  try {
29262
29409
  manifest.sections[sec.name] = await runSection(sec);
29263
29410
  } catch (e) {
29411
+ if (e instanceof SleuthDenied)
29412
+ denial = e;
29264
29413
  manifest.sections[sec.name] = {
29265
29414
  found: true,
29266
29415
  path: [],
@@ -29273,6 +29422,8 @@ var exportSleuth = async (o) => {
29273
29422
  log(`${sec.name}: failed — ${e instanceof Error ? e.message : String(e)}`);
29274
29423
  }
29275
29424
  save();
29425
+ if (denial && !answered)
29426
+ throw denial;
29276
29427
  }
29277
29428
  const deploys = manifest.sections.deploys;
29278
29429
  const recap = orgRoot ? fieldOf2(schema, unwrap(orgRoot.type).name, "metricsRecap") : undefined;
@@ -29332,6 +29483,8 @@ var SLEUTH_USAGE = `usage: sleds sleuth export --org <slug> --out <dir> [options
29332
29483
  Sleuth reports. Re-run to resume: finished past months on disk are not fetched again.
29333
29484
 
29334
29485
  SLEUTH_API_KEY the org's API key or access token (Sleuth › Settings › API)
29486
+ SLEUTH_AUTH apikey or bearer: send only that scheme (default: apikey, switching to
29487
+ Bearer once if Sleuth refuses or answers PERMISSION_DENIED)
29335
29488
  --org <slug> the Sleuth org slug (required)
29336
29489
  --out <dir> where to write (required; created if missing)
29337
29490
  --since <YYYY-MM> first month (default 2022-01)
@@ -29366,6 +29519,7 @@ var parse3 = (argv) => {
29366
29519
  return { words, flags };
29367
29520
  };
29368
29521
  var outDir = (raw, home) => resolve12(raw.startsWith("~/") ? join35(home, raw.slice(2)) : raw);
29522
+ var SCHEMES = { apikey: "apikey", bearer: "Bearer" };
29369
29523
  var str15 = (f, k) => typeof f[k] === "string" ? f[k] : undefined;
29370
29524
  var MONTH2 = /^\d{4}-(0[1-9]|1[0-2])$/;
29371
29525
  var KNOWN = [
@@ -29405,6 +29559,12 @@ var sleuthCmd = async (o) => {
29405
29559
  error("SLEUTH_API_KEY is not set — the org's key from Sleuth › Settings › API");
29406
29560
  return 2;
29407
29561
  }
29562
+ const authRaw = env.SLEUTH_AUTH?.trim().toLowerCase();
29563
+ const scheme = authRaw && Object.hasOwn(SCHEMES, authRaw) ? SCHEMES[authRaw] : undefined;
29564
+ if (authRaw && !scheme) {
29565
+ error(`SLEUTH_AUTH is "${env.SLEUTH_AUTH}" — apikey or bearer, or unset to try both`);
29566
+ return 2;
29567
+ }
29408
29568
  const now = (o.now ?? (() => new Date))();
29409
29569
  const since = str15(flags, "since") ?? "2022-01";
29410
29570
  const until = str15(flags, "until") ?? monthOf(now);
@@ -29427,6 +29587,7 @@ var sleuthCmd = async (o) => {
29427
29587
  const progress = error;
29428
29588
  const client = sleuthClient({
29429
29589
  key,
29590
+ ...scheme ? { scheme } : {},
29430
29591
  url: endpoint,
29431
29592
  ...o.fetch ? { fetch: o.fetch } : {},
29432
29593
  ...o.sleep ? { sleep: o.sleep } : {},
@@ -30229,7 +30390,7 @@ var realPidAlive4 = (pid) => {
30229
30390
  };
30230
30391
  function machineClient(home, env) {
30231
30392
  const src = meterSource(home, undefined, { env });
30232
- return src ? fetchClient({ ...src, version: "0.1.66" }) : null;
30393
+ return src ? fetchClient({ ...src, version: "0.1.67" }) : null;
30233
30394
  }
30234
30395
  async function noteAnswer2(home, a, now) {
30235
30396
  try {
@@ -31103,7 +31264,7 @@ var main = async (argv) => {
31103
31264
  now: Date.now(),
31104
31265
  stderr: (line) => console.error(line),
31105
31266
  isTTY: !!process.stderr.isTTY,
31106
- version: "0.1.66"
31267
+ version: "0.1.67"
31107
31268
  });
31108
31269
  switch (cmd) {
31109
31270
  case "update":
@@ -31338,7 +31499,7 @@ ${renderHelpText(argv[1])}`);
31338
31499
  case "version":
31339
31500
  case "--version":
31340
31501
  case "-v":
31341
- console.log("sleds 0.1.66");
31502
+ console.log("sleds 0.1.67");
31342
31503
  return 0;
31343
31504
  default:
31344
31505
  console.error(`unknown command: ${cmd}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleds/cli",
3
- "version": "0.1.66",
3
+ "version": "0.1.67",
4
4
  "description": "SLEDS CLI — 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",