@whittlelabs/sifter 0.30.17 → 0.30.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.
package/README.md CHANGED
@@ -134,6 +134,8 @@ Sixteen checks over everything the Sifter needs before it can review anything: a
134
134
 
135
135
  `init` and `run` run the relevant subset themselves, so `doctor` is for when the Sifter looks healthy and nothing is happening.
136
136
 
137
+ The login check asks the CLI itself (`claude auth status`), so a login kept in the macOS Keychain counts, and it asks with the settings a review loads. A signed-out CLI fails it with the remedy `claude auth login`; a signed-in one passes as, for example, `signed in (claude.ai, max; not verified)`, because only `--deep` proves the login works. It never prints the email or organization the CLI reports. A `claude` too old to have `auth status` is checked the old way, by looking for its credentials file.
138
+
137
139
  ```bash
138
140
  whittle-sifter doctor --deep # also spend one real turn proving the claude login works
139
141
  whittle-sifter doctor --json # the verdicts as JSON, and nothing else
package/bin.js CHANGED
@@ -11433,6 +11433,69 @@ var require_exchange = __commonJS({
11433
11433
  }
11434
11434
  });
11435
11435
 
11436
+ // ../../packages/keep/dist/refusal-budget.js
11437
+ var require_refusal_budget = __commonJS({
11438
+ "../../packages/keep/dist/refusal-budget.js"(exports2) {
11439
+ "use strict";
11440
+ Object.defineProperty(exports2, "__esModule", { value: true });
11441
+ exports2.RefusalBudget = exports2.REFUSAL_SPAN_MS = exports2.REFUSED_EXCHANGES_PER_SPAN = exports2.KeepCredentialsRefusedError = void 0;
11442
+ var KeepCredentialsRefusedError = class extends Error {
11443
+ code = "CREDENTIALS_REFUSED";
11444
+ /** Host the exchange was sent to. */
11445
+ host;
11446
+ /** When the manager will next send an exchange for these credentials (epoch ms). */
11447
+ retryAt;
11448
+ /** How long from now that is (ms); zero while the budget has room. */
11449
+ retryAfterMs;
11450
+ constructor(message, detail) {
11451
+ super(message);
11452
+ this.name = "KeepCredentialsRefusedError";
11453
+ this.host = detail.host;
11454
+ this.retryAt = detail.retryAt;
11455
+ this.retryAfterMs = Math.max(0, detail.retryAt - Date.now());
11456
+ }
11457
+ };
11458
+ exports2.KeepCredentialsRefusedError = KeepCredentialsRefusedError;
11459
+ var KEEP_EXCHANGE_WINDOW_MS = 15 * 6e4;
11460
+ exports2.REFUSED_EXCHANGES_PER_SPAN = 3;
11461
+ exports2.REFUSAL_SPAN_MS = KEEP_EXCHANGE_WINDOW_MS + 3e4;
11462
+ var RefusalBudget = class {
11463
+ refusedAt = [];
11464
+ last = null;
11465
+ /** Throws the last refusal while the budget is spent; a no-op while it has room. */
11466
+ check() {
11467
+ const last = this.last;
11468
+ if (last === null)
11469
+ return;
11470
+ const retryAt = this.nextExchangeAt();
11471
+ if (Date.now() >= retryAt)
11472
+ return;
11473
+ const when = new Date(retryAt).toISOString().replace(/\.\d{3}Z$/, "Z");
11474
+ throw new KeepCredentialsRefusedError(`${last.message} (refused ${this.refusedAt.length} times in ${exports2.REFUSAL_SPAN_MS / 6e4} minutes; not asking Keep again before ${when})`, { host: last.host, retryAt });
11475
+ }
11476
+ /** Record a 401 and return the error to throw for it. */
11477
+ refuse(detail) {
11478
+ this.refusedAt.push(Date.now());
11479
+ this.last = new KeepCredentialsRefusedError(detail.message, { host: detail.host, retryAt: this.nextExchangeAt() });
11480
+ return this.last;
11481
+ }
11482
+ /** Keep issued a token, which clears its count; so does this. */
11483
+ clear() {
11484
+ this.refusedAt = [];
11485
+ this.last = null;
11486
+ }
11487
+ nextExchangeAt() {
11488
+ const now = Date.now();
11489
+ this.refusedAt = this.refusedAt.filter((at) => now - at < exports2.REFUSAL_SPAN_MS);
11490
+ if (this.refusedAt.length < exports2.REFUSED_EXCHANGES_PER_SPAN)
11491
+ return now;
11492
+ return this.refusedAt[this.refusedAt.length - exports2.REFUSED_EXCHANGES_PER_SPAN] + exports2.REFUSAL_SPAN_MS;
11493
+ }
11494
+ };
11495
+ exports2.RefusalBudget = RefusalBudget;
11496
+ }
11497
+ });
11498
+
11436
11499
  // ../../packages/keep/dist/wait-for-token.js
11437
11500
  var require_wait_for_token = __commonJS({
11438
11501
  "../../packages/keep/dist/wait-for-token.js"(exports2) {
@@ -11511,6 +11574,7 @@ var require_service_token = __commonJS({
11511
11574
  exports2.ServiceTokenManager = void 0;
11512
11575
  var rate_limited_1 = require_rate_limited();
11513
11576
  var exchange_1 = require_exchange();
11577
+ var refusal_budget_1 = require_refusal_budget();
11514
11578
  var wait_for_token_1 = require_wait_for_token();
11515
11579
  var ServiceTokenManager = class {
11516
11580
  token = null;
@@ -11518,6 +11582,8 @@ var require_service_token = __commonJS({
11518
11582
  pendingExchange = null;
11519
11583
  /** Closed by a 429 until Keep's retry-after: no exchange is sent before it. */
11520
11584
  rateLimit = new rate_limited_1.RateLimitGate();
11585
+ /** Spent by refused exchanges: past it, no exchange is sent until the oldest ages out. */
11586
+ refusals = new refusal_budget_1.RefusalBudget();
11521
11587
  keepApiUrl;
11522
11588
  clientId;
11523
11589
  clientSecret;
@@ -11539,8 +11605,11 @@ var require_service_token = __commonJS({
11539
11605
  *
11540
11606
  * Throws `KeepUnavailableError` when Keep did not answer or answered 5xx,
11541
11607
  * `KeepRateLimitedError` when Keep's exchange limiter locked this client out
11542
- * (and, until the time Keep named, without asking again), and a plain `Error`
11543
- * for any other refusal, a 401 above all. A refresh that fails for either
11608
+ * (and, until the time Keep named, without asking again),
11609
+ * `KeepCredentialsRefusedError` for a 401 (and, once three fall inside one of
11610
+ * Keep's lockout windows, without asking again until the oldest ages out:
11611
+ * see `RefusalBudget`), and a plain `Error` for any other refusal. A refresh
11612
+ * that fails for either
11544
11613
  * of the first two reasons while the cached token has not yet expired
11545
11614
  * answers with the cached token instead: the refresh starts 30s early
11546
11615
  * precisely so a failed one costs nothing.
@@ -11573,6 +11642,7 @@ var require_service_token = __commonJS({
11573
11642
  async refresh() {
11574
11643
  try {
11575
11644
  this.rateLimit.check();
11645
+ this.refusals.check();
11576
11646
  return await this.exchange();
11577
11647
  } catch (err) {
11578
11648
  const code = err?.code;
@@ -11590,8 +11660,10 @@ var require_service_token = __commonJS({
11590
11660
  clientSecret: this.clientSecret,
11591
11661
  rateLimit: this.rateLimit,
11592
11662
  onResponse: this.onResponse,
11593
- exchangeName: "Token exchange"
11663
+ exchangeName: "Token exchange",
11664
+ refused: (message) => this.refusals.refuse({ message: `[ServiceTokenManager] ${message}`, host: (0, exchange_1.hostOf)(this.keepApiUrl) })
11594
11665
  });
11666
+ this.refusals.clear();
11595
11667
  this.token = data.token;
11596
11668
  this.expiresAt = Date.now() + data.expiresIn * 1e3;
11597
11669
  return this.token;
@@ -11678,7 +11750,7 @@ var require_dist2 = __commonJS({
11678
11750
  "../../packages/keep/dist/index.js"(exports2) {
11679
11751
  "use strict";
11680
11752
  Object.defineProperty(exports2, "__esModule", { value: true });
11681
- exports2.waitForKeepToken = exports2.KeepRateLimitedError = exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
11753
+ exports2.waitForKeepToken = exports2.KeepCredentialsRefusedError = exports2.KeepRateLimitedError = exports2.KeepUnavailableError = exports2.AgentCredentialRevokedError = exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
11682
11754
  var client_1 = require_client();
11683
11755
  Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
11684
11756
  return client_1.KeepClient;
@@ -11702,6 +11774,10 @@ var require_dist2 = __commonJS({
11702
11774
  Object.defineProperty(exports2, "KeepRateLimitedError", { enumerable: true, get: function() {
11703
11775
  return rate_limited_1.KeepRateLimitedError;
11704
11776
  } });
11777
+ var refusal_budget_1 = require_refusal_budget();
11778
+ Object.defineProperty(exports2, "KeepCredentialsRefusedError", { enumerable: true, get: function() {
11779
+ return refusal_budget_1.KeepCredentialsRefusedError;
11780
+ } });
11705
11781
  var wait_for_token_1 = require_wait_for_token();
11706
11782
  Object.defineProperty(exports2, "waitForKeepToken", { enumerable: true, get: function() {
11707
11783
  return wait_for_token_1.waitForKeepToken;
@@ -12475,6 +12551,12 @@ var require_checks = __commonJS({
12475
12551
  const defaults = (0, environment_1.executorDefaultsFor)(subject.brand, config.auth.jobsApiUrl)?.[WORKSPACE_EXECUTOR] ?? {};
12476
12552
  return { ...defaults, ...user };
12477
12553
  }
12554
+ function claudeSettingsOf(config) {
12555
+ return config.executors[WORKSPACE_EXECUTOR]?.claudeSettings;
12556
+ }
12557
+ function describeLogin(status) {
12558
+ return [status.authMethod ?? "unknown method", status.subscriptionType].filter((part) => part !== null).join(", ");
12559
+ }
12478
12560
  function runRoot(config, subject, osTmpDir) {
12479
12561
  const settings = claudeCodeSettings(config, subject);
12480
12562
  const tmpDir = settings?.tmpDir;
@@ -12769,6 +12851,14 @@ var require_checks = __commonJS({
12769
12851
  if (probes.ambientAnthropicKey()) {
12770
12852
  return warn("claude-auth", "ANTHROPIC_API_KEY is set and will bill that account, shadowing any interactive login", "unset ANTHROPIC_API_KEY if you meant to use your Claude Code login");
12771
12853
  }
12854
+ const settingSources = claudeSettingsOf(config) === "user" ? "user" : "";
12855
+ const status = await probes.claudeAuthStatus(settingSources);
12856
+ if (status !== null) {
12857
+ if (!status.loggedIn) {
12858
+ return fail("claude-auth", "the claude CLI is not signed in", settingSources === "" ? "run `claude auth login`; if your login or provider is configured in Claude Code settings (apiKeyHelper, or an env block), set `claudeSettings: user` on the claude-code executor instead" : "run `claude auth login`");
12859
+ }
12860
+ return pass("claude-auth", `signed in (${describeLogin(status)}; not verified)`);
12861
+ }
12772
12862
  const store = await probes.claudeCredentialStore();
12773
12863
  if (store === null) {
12774
12864
  return warn("claude-auth", "no credential store found for the claude CLI (a macOS login may live in the Keychain, which this cannot read)", "run `claude` once and sign in, or run doctor --deep to spend one turn proving it");
@@ -21382,6 +21472,7 @@ var require_probes = __commonJS({
21382
21472
  };
21383
21473
  Object.defineProperty(exports2, "__esModule", { value: true });
21384
21474
  exports2.createProbes = createProbes;
21475
+ exports2.parseClaudeAuthStatus = parseClaudeAuthStatus;
21385
21476
  var child_process_1 = require("child_process");
21386
21477
  var fs_1 = require("fs");
21387
21478
  var promises_1 = require("fs/promises");
@@ -21600,6 +21691,10 @@ var require_probes = __commonJS({
21600
21691
  const candidate = path_1.default.join(configDir, ".credentials.json");
21601
21692
  return (0, fs_1.existsSync)(candidate) ? candidate : null;
21602
21693
  },
21694
+ async claudeAuthStatus(settingSources) {
21695
+ const outcome = await runForAnyExit("claude", ["--setting-sources", settingSources, "auth", "status", "--json"], VERSION_TIMEOUT_MS);
21696
+ return outcome === null ? null : parseClaudeAuthStatus(outcome);
21697
+ },
21603
21698
  ambientAnthropicKey: () => (process.env.ANTHROPIC_API_KEY ?? "").length > 0,
21604
21699
  async claudeRoundTrip() {
21605
21700
  const output = await runForOutput(
@@ -21620,13 +21715,37 @@ var require_probes = __commonJS({
21620
21715
  const outcome = await runCapturing(bin, args, timeoutMs);
21621
21716
  return outcome.ok ? outcome.stdout : null;
21622
21717
  }
21718
+ async function runForAnyExit(bin, args, timeoutMs) {
21719
+ const outcome = await runCapturing(bin, args, timeoutMs);
21720
+ if (outcome.ok)
21721
+ return outcome.stdout;
21722
+ return outcome.exited ? outcome.stdout : null;
21723
+ }
21724
+ function parseClaudeAuthStatus(stdout) {
21725
+ let parsed;
21726
+ try {
21727
+ parsed = JSON.parse(stdout);
21728
+ } catch {
21729
+ return null;
21730
+ }
21731
+ if (parsed === null || typeof parsed !== "object")
21732
+ return null;
21733
+ const { loggedIn, authMethod, subscriptionType } = parsed;
21734
+ if (typeof loggedIn !== "boolean")
21735
+ return null;
21736
+ return {
21737
+ loggedIn,
21738
+ authMethod: typeof authMethod === "string" && authMethod.length > 0 ? authMethod : null,
21739
+ subscriptionType: typeof subscriptionType === "string" && subscriptionType.length > 0 ? subscriptionType : null
21740
+ };
21741
+ }
21623
21742
  function runCapturing(bin, args, timeoutMs) {
21624
21743
  return new Promise((resolve) => {
21625
21744
  let child;
21626
21745
  try {
21627
21746
  child = (0, child_process_1.spawn)(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
21628
21747
  } catch (err) {
21629
- resolve({ ok: false, reason: describe(err) });
21748
+ resolve({ ok: false, reason: describe(err), exited: false });
21630
21749
  return;
21631
21750
  }
21632
21751
  let stdout = "";
@@ -21640,7 +21759,7 @@ var require_probes = __commonJS({
21640
21759
  };
21641
21760
  const timer = setTimeout(() => {
21642
21761
  child.kill("SIGKILL");
21643
- settle({ ok: false, reason: `did not finish within ${Math.round(timeoutMs / 1e3)}s` });
21762
+ settle({ ok: false, reason: `did not finish within ${Math.round(timeoutMs / 1e3)}s`, exited: false });
21644
21763
  }, timeoutMs);
21645
21764
  child.stdout?.on("data", (chunk) => {
21646
21765
  stdout += chunk.toString("utf8");
@@ -21650,7 +21769,7 @@ var require_probes = __commonJS({
21650
21769
  });
21651
21770
  child.on("error", (err) => {
21652
21771
  clearTimeout(timer);
21653
- settle({ ok: false, reason: describe(err) });
21772
+ settle({ ok: false, reason: describe(err), exited: false });
21654
21773
  });
21655
21774
  child.on("close", (code, signal) => {
21656
21775
  clearTimeout(timer);
@@ -21660,7 +21779,8 @@ var require_probes = __commonJS({
21660
21779
  }
21661
21780
  const status = code === null ? `was killed by ${signal ?? "a signal"}` : `exited ${code}`;
21662
21781
  const said = stderr.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
21663
- settle({ ok: false, reason: said ? `${status}: ${said}` : status });
21782
+ const reason = said ? `${status}: ${said}` : status;
21783
+ settle(code === null ? { ok: false, reason, exited: false } : { ok: false, reason, exited: true, stdout: stdout.trim() });
21664
21784
  });
21665
21785
  });
21666
21786
  }
@@ -26966,7 +27086,7 @@ var import_path = require("path");
26966
27086
  var import_promises = require("fs/promises");
26967
27087
  var import_yaml = __toESM(require_dist3());
26968
27088
  var import_shuttle = __toESM(require_dist5());
26969
- var buildVersion = true ? "0.30.17" : pkg.version;
27089
+ var buildVersion = true ? "0.30.19" : pkg.version;
26970
27090
  var sifterBrand = {
26971
27091
  product: {
26972
27092
  id: "sifter",