@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 +2 -0
- package/bin.js +129 -9
- package/bin.js.map +4 -4
- package/package.json +1 -1
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),
|
|
11543
|
-
* for
|
|
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
|
-
|
|
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.
|
|
27089
|
+
var buildVersion = true ? "0.30.19" : pkg.version;
|
|
26970
27090
|
var sifterBrand = {
|
|
26971
27091
|
product: {
|
|
26972
27092
|
id: "sifter",
|