@whittlelabs/sifter 0.2.0 → 0.3.1
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/bin.js +463 -76
- package/bin.js.map +4 -4
- package/package.json +1 -1
package/bin.js
CHANGED
|
@@ -3514,8 +3514,9 @@ var require_init = __commonJS({
|
|
|
3514
3514
|
var store_1 = require_store();
|
|
3515
3515
|
var spend_store_1 = require_spend_store();
|
|
3516
3516
|
function buildInitCommand(brand) {
|
|
3517
|
-
return new commander_1.Command("init").description(`Pair this ${brand.product.title} with Keep and seed local state`).option("--pairing-code <code>", "Pairing code from the product UI").option("--keep-url <url>", "Override the Keep API URL from BrandConfig").option("--jobs-url <url>", "Override the Jobs API URL from BrandConfig").action(async (options) => {
|
|
3517
|
+
return new commander_1.Command("init").description(`Pair this ${brand.product.title} with Keep and seed local state`).option("--pairing-code <code>", "Pairing code from the product UI").option("--keep-url <url>", "Override the Keep API URL from BrandConfig").option("--jobs-url <url>", "Override the Jobs API URL from BrandConfig").option("--executor <type>", `Executor to write into ${brand.product.id}.yaml (e.g. claude-code). Must be in the brand's executor allowlist.`).option("--poll-interval <ms>", "Run-loop poll interval in milliseconds").option("--daily-cap <tokens>", "Daily token cap; overrides the brand default").option("--monthly-cap <tokens>", "Monthly token cap; overrides the brand default").option("--cwd <path>", "Working directory for executors that run a local subprocess (e.g. claude-code)").action(async (options) => {
|
|
3518
3518
|
try {
|
|
3519
|
+
const setupOptions = parseSetupOptions(brand, options);
|
|
3519
3520
|
const effectiveBrand = options.keepUrl || options.jobsUrl ? {
|
|
3520
3521
|
...brand,
|
|
3521
3522
|
keepRegistration: {
|
|
@@ -3531,7 +3532,8 @@ var require_init = __commonJS({
|
|
|
3531
3532
|
await step.run({
|
|
3532
3533
|
brand: effectiveBrand,
|
|
3533
3534
|
configDir: paths.configDir,
|
|
3534
|
-
log: (line) => console.log(line)
|
|
3535
|
+
log: (line) => console.log(line),
|
|
3536
|
+
setupOptions
|
|
3535
3537
|
});
|
|
3536
3538
|
}
|
|
3537
3539
|
}
|
|
@@ -3543,8 +3545,8 @@ var require_init = __commonJS({
|
|
|
3543
3545
|
await store.write(pairing);
|
|
3544
3546
|
const spendStore = new spend_store_1.SpendStore(paths.spendStateFile);
|
|
3545
3547
|
const seedCaps = {
|
|
3546
|
-
dailyTokens: effectiveBrand.defaultCaps.dailyTokens,
|
|
3547
|
-
monthlyTokens: effectiveBrand.defaultCaps.monthlyTokens,
|
|
3548
|
+
dailyTokens: setupOptions.dailyCap ?? effectiveBrand.defaultCaps.dailyTokens,
|
|
3549
|
+
monthlyTokens: setupOptions.monthlyCap ?? effectiveBrand.defaultCaps.monthlyTokens,
|
|
3548
3550
|
dailyUsd: effectiveBrand.defaultCaps.dailyUsd,
|
|
3549
3551
|
monthlyUsd: effectiveBrand.defaultCaps.monthlyUsd
|
|
3550
3552
|
};
|
|
@@ -3554,7 +3556,8 @@ var require_init = __commonJS({
|
|
|
3554
3556
|
await step.run({
|
|
3555
3557
|
brand: effectiveBrand,
|
|
3556
3558
|
configDir: paths.configDir,
|
|
3557
|
-
log: (line) => console.log(line)
|
|
3559
|
+
log: (line) => console.log(line),
|
|
3560
|
+
setupOptions
|
|
3558
3561
|
});
|
|
3559
3562
|
}
|
|
3560
3563
|
}
|
|
@@ -3572,6 +3575,44 @@ var require_init = __commonJS({
|
|
|
3572
3575
|
}
|
|
3573
3576
|
});
|
|
3574
3577
|
}
|
|
3578
|
+
function parseSetupOptions(brand, raw) {
|
|
3579
|
+
const out = {};
|
|
3580
|
+
if (raw.executor !== void 0) {
|
|
3581
|
+
const allowed = brand.executorAllowlist;
|
|
3582
|
+
const wildcard = allowed.includes("*");
|
|
3583
|
+
if (!wildcard && !allowed.includes(raw.executor)) {
|
|
3584
|
+
throw new Error(`--executor=${raw.executor} is not in this brand's allowlist (${allowed.join(", ")})`);
|
|
3585
|
+
}
|
|
3586
|
+
out.executor = raw.executor;
|
|
3587
|
+
}
|
|
3588
|
+
if (raw.pollInterval !== void 0) {
|
|
3589
|
+
out.pollIntervalMs = requirePositiveInt("--poll-interval", raw.pollInterval);
|
|
3590
|
+
}
|
|
3591
|
+
if (raw.dailyCap !== void 0) {
|
|
3592
|
+
out.dailyCap = requireNonNegativeInt("--daily-cap", raw.dailyCap);
|
|
3593
|
+
}
|
|
3594
|
+
if (raw.monthlyCap !== void 0) {
|
|
3595
|
+
out.monthlyCap = requireNonNegativeInt("--monthly-cap", raw.monthlyCap);
|
|
3596
|
+
}
|
|
3597
|
+
if (raw.cwd !== void 0) {
|
|
3598
|
+
out.cwd = raw.cwd;
|
|
3599
|
+
}
|
|
3600
|
+
return out;
|
|
3601
|
+
}
|
|
3602
|
+
function requirePositiveInt(flag, value) {
|
|
3603
|
+
const n = Number(value);
|
|
3604
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
|
|
3605
|
+
throw new Error(`${flag} must be a positive integer, got "${value}"`);
|
|
3606
|
+
}
|
|
3607
|
+
return n;
|
|
3608
|
+
}
|
|
3609
|
+
function requireNonNegativeInt(flag, value) {
|
|
3610
|
+
const n = Number(value);
|
|
3611
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
|
3612
|
+
throw new Error(`${flag} must be a non-negative integer, got "${value}"`);
|
|
3613
|
+
}
|
|
3614
|
+
return n;
|
|
3615
|
+
}
|
|
3575
3616
|
}
|
|
3576
3617
|
});
|
|
3577
3618
|
|
|
@@ -15068,7 +15109,7 @@ var require_schema4 = __commonJS({
|
|
|
15068
15109
|
message: 'Each pool must specify at least one of "id" or "name"'
|
|
15069
15110
|
});
|
|
15070
15111
|
var executorSchema = zod_1.z.object({
|
|
15071
|
-
type: zod_1.z.enum(["claude-code", "http-api", "webhook", "custom-script"])
|
|
15112
|
+
type: zod_1.z.enum(["claude-code", "http-api", "anthropic-api", "webhook", "custom-script"])
|
|
15072
15113
|
}).passthrough();
|
|
15073
15114
|
var shuttleConfigSchema = zod_1.z.object({
|
|
15074
15115
|
auth: zod_1.z.object({
|
|
@@ -16249,6 +16290,32 @@ var require_client2 = __commonJS({
|
|
|
16249
16290
|
ttlSeconds: request.ttlSeconds
|
|
16250
16291
|
});
|
|
16251
16292
|
}
|
|
16293
|
+
// ── Managed Sifter ──────────────────────────────────────────────
|
|
16294
|
+
/**
|
|
16295
|
+
* Reveal a hosted-Sifter customer's plaintext Anthropic API key for
|
|
16296
|
+
* one dispatch. Service-token only; requires the
|
|
16297
|
+
* `keep:managed-sifter-keys:reveal` scope, granted only to the
|
|
16298
|
+
* `whittle-hosted-sifter` service identity.
|
|
16299
|
+
*
|
|
16300
|
+
* Callers must use the returned key for a single Anthropic request
|
|
16301
|
+
* and drop it from memory afterwards. Keep stamps `last_used_at` on
|
|
16302
|
+
* every call and is the audit-of-record for who decrypted whose key
|
|
16303
|
+
* when.
|
|
16304
|
+
*/
|
|
16305
|
+
async revealManagedSifterKey(userId) {
|
|
16306
|
+
return this.request("POST", `/api/managed-sifters/${encodeURIComponent(userId)}/reveal-key`);
|
|
16307
|
+
}
|
|
16308
|
+
/**
|
|
16309
|
+
* Read a user's hosted-Sifter opt-in status. Returns `{enabled:
|
|
16310
|
+
* false}` for users who never opted in (no row) and users who
|
|
16311
|
+
* opted in then disabled. Service-to-service only; requires the
|
|
16312
|
+
* `keep:managed-sifter-keys:read-status` scope, which is distinct
|
|
16313
|
+
* from `:reveal` so a service identity can hold one without the
|
|
16314
|
+
* other.
|
|
16315
|
+
*/
|
|
16316
|
+
async getManagedSifterStatus(userId) {
|
|
16317
|
+
return this.request("GET", `/api/users/${encodeURIComponent(userId)}/managed-sifter-status`);
|
|
16318
|
+
}
|
|
16252
16319
|
// ── HTTP ────────────────────────────────────────────────────────
|
|
16253
16320
|
async request(method, path, body) {
|
|
16254
16321
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -16342,12 +16409,75 @@ var require_service_token = __commonJS({
|
|
|
16342
16409
|
}
|
|
16343
16410
|
});
|
|
16344
16411
|
|
|
16412
|
+
// ../../packages/keep/dist/agent-token.js
|
|
16413
|
+
var require_agent_token = __commonJS({
|
|
16414
|
+
"../../packages/keep/dist/agent-token.js"(exports2) {
|
|
16415
|
+
"use strict";
|
|
16416
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
16417
|
+
exports2.AgentTokenManager = void 0;
|
|
16418
|
+
var AgentTokenManager = class {
|
|
16419
|
+
token = null;
|
|
16420
|
+
expiresAt = 0;
|
|
16421
|
+
pendingExchange = null;
|
|
16422
|
+
keepApiUrl;
|
|
16423
|
+
clientId;
|
|
16424
|
+
clientSecret;
|
|
16425
|
+
enabled;
|
|
16426
|
+
onResponse;
|
|
16427
|
+
constructor(config) {
|
|
16428
|
+
this.keepApiUrl = config.keepApiUrl.replace(/\/$/, "");
|
|
16429
|
+
this.clientId = config.clientId;
|
|
16430
|
+
this.clientSecret = config.clientSecret;
|
|
16431
|
+
this.enabled = !!(config.clientId && config.clientSecret && config.keepApiUrl);
|
|
16432
|
+
this.onResponse = config.onResponse;
|
|
16433
|
+
}
|
|
16434
|
+
async getToken() {
|
|
16435
|
+
if (!this.enabled)
|
|
16436
|
+
return null;
|
|
16437
|
+
if (this.token && Date.now() < this.expiresAt - 3e4) {
|
|
16438
|
+
return this.token;
|
|
16439
|
+
}
|
|
16440
|
+
if (this.pendingExchange) {
|
|
16441
|
+
return this.pendingExchange;
|
|
16442
|
+
}
|
|
16443
|
+
this.pendingExchange = this.exchange();
|
|
16444
|
+
try {
|
|
16445
|
+
return await this.pendingExchange;
|
|
16446
|
+
} finally {
|
|
16447
|
+
this.pendingExchange = null;
|
|
16448
|
+
}
|
|
16449
|
+
}
|
|
16450
|
+
async exchange() {
|
|
16451
|
+
const url = `${this.keepApiUrl}/api/agent-tokens`;
|
|
16452
|
+
const response = await fetch(url, {
|
|
16453
|
+
method: "POST",
|
|
16454
|
+
headers: { "Content-Type": "application/json" },
|
|
16455
|
+
body: JSON.stringify({
|
|
16456
|
+
clientId: this.clientId,
|
|
16457
|
+
clientSecret: this.clientSecret
|
|
16458
|
+
})
|
|
16459
|
+
});
|
|
16460
|
+
this.onResponse?.(response);
|
|
16461
|
+
const json = await response.json();
|
|
16462
|
+
if (!response.ok || !json.success) {
|
|
16463
|
+
const msg = json.error?.message ?? `Agent token exchange failed with status ${response.status}`;
|
|
16464
|
+
throw new Error(`[AgentTokenManager] ${msg}`);
|
|
16465
|
+
}
|
|
16466
|
+
this.token = json.data.token;
|
|
16467
|
+
this.expiresAt = Date.now() + json.data.expiresIn * 1e3;
|
|
16468
|
+
return this.token;
|
|
16469
|
+
}
|
|
16470
|
+
};
|
|
16471
|
+
exports2.AgentTokenManager = AgentTokenManager;
|
|
16472
|
+
}
|
|
16473
|
+
});
|
|
16474
|
+
|
|
16345
16475
|
// ../../packages/keep/dist/index.js
|
|
16346
16476
|
var require_dist3 = __commonJS({
|
|
16347
16477
|
"../../packages/keep/dist/index.js"(exports2) {
|
|
16348
16478
|
"use strict";
|
|
16349
16479
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
16350
|
-
exports2.ServiceTokenManager = exports2.KeepClient = void 0;
|
|
16480
|
+
exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
|
|
16351
16481
|
var client_1 = require_client2();
|
|
16352
16482
|
Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
|
|
16353
16483
|
return client_1.KeepClient;
|
|
@@ -16356,6 +16486,10 @@ var require_dist3 = __commonJS({
|
|
|
16356
16486
|
Object.defineProperty(exports2, "ServiceTokenManager", { enumerable: true, get: function() {
|
|
16357
16487
|
return service_token_1.ServiceTokenManager;
|
|
16358
16488
|
} });
|
|
16489
|
+
var agent_token_1 = require_agent_token();
|
|
16490
|
+
Object.defineProperty(exports2, "AgentTokenManager", { enumerable: true, get: function() {
|
|
16491
|
+
return agent_token_1.AgentTokenManager;
|
|
16492
|
+
} });
|
|
16359
16493
|
}
|
|
16360
16494
|
});
|
|
16361
16495
|
|
|
@@ -16746,6 +16880,206 @@ var require_http_api = __commonJS({
|
|
|
16746
16880
|
}
|
|
16747
16881
|
});
|
|
16748
16882
|
|
|
16883
|
+
// ../../packages/shuttle/dist/config/env-ref.js
|
|
16884
|
+
var require_env_ref = __commonJS({
|
|
16885
|
+
"../../packages/shuttle/dist/config/env-ref.js"(exports2) {
|
|
16886
|
+
"use strict";
|
|
16887
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
16888
|
+
exports2.resolveEnvRef = resolveEnvRef;
|
|
16889
|
+
exports2.isEnvRef = isEnvRef;
|
|
16890
|
+
var ENV_REF_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
16891
|
+
function resolveEnvRef(value, fieldName) {
|
|
16892
|
+
const match = ENV_REF_PATTERN.exec(value);
|
|
16893
|
+
if (!match)
|
|
16894
|
+
return value;
|
|
16895
|
+
const varName = match[1];
|
|
16896
|
+
const resolved = process.env[varName];
|
|
16897
|
+
if (resolved === void 0 || resolved === "") {
|
|
16898
|
+
const subject = fieldName ? `${fieldName} (${value})` : value;
|
|
16899
|
+
throw new Error(`Environment variable "${varName}" referenced by ${subject} is not set. Export it before starting the Sifter, or hard-code the value in the YAML.`);
|
|
16900
|
+
}
|
|
16901
|
+
return resolved;
|
|
16902
|
+
}
|
|
16903
|
+
function isEnvRef(value) {
|
|
16904
|
+
return ENV_REF_PATTERN.test(value);
|
|
16905
|
+
}
|
|
16906
|
+
}
|
|
16907
|
+
});
|
|
16908
|
+
|
|
16909
|
+
// ../../packages/shuttle/dist/executors/anthropic-api.js
|
|
16910
|
+
var require_anthropic_api = __commonJS({
|
|
16911
|
+
"../../packages/shuttle/dist/executors/anthropic-api.js"(exports2) {
|
|
16912
|
+
"use strict";
|
|
16913
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
16914
|
+
exports2.createAnthropicApiExecutor = createAnthropicApiExecutor;
|
|
16915
|
+
var keep_1 = require_dist3();
|
|
16916
|
+
var env_ref_1 = require_env_ref();
|
|
16917
|
+
var DEFAULT_BASE_URL = "https://api.anthropic.com/v1/messages";
|
|
16918
|
+
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
16919
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
16920
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
16921
|
+
var ANTHROPIC_API_VERSION = "2023-06-01";
|
|
16922
|
+
var AnthropicApiExecutor = class {
|
|
16923
|
+
capability;
|
|
16924
|
+
instance;
|
|
16925
|
+
constructor(instance) {
|
|
16926
|
+
this.instance = instance;
|
|
16927
|
+
this.capability = {
|
|
16928
|
+
id: instance.capabilityId,
|
|
16929
|
+
jobTypes: ["prompt-execution"]
|
|
16930
|
+
};
|
|
16931
|
+
}
|
|
16932
|
+
async dispatch(req, signal) {
|
|
16933
|
+
const apiKey = await this.instance.resolveKey(req);
|
|
16934
|
+
const body = {
|
|
16935
|
+
model: this.instance.model,
|
|
16936
|
+
max_tokens: this.instance.maxTokens,
|
|
16937
|
+
messages: [{ role: "user", content: req.prompt }]
|
|
16938
|
+
};
|
|
16939
|
+
const timeoutController = new AbortController();
|
|
16940
|
+
const timer = setTimeout(() => timeoutController.abort(), this.instance.timeoutMs);
|
|
16941
|
+
function onAbort() {
|
|
16942
|
+
timeoutController.abort();
|
|
16943
|
+
}
|
|
16944
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
16945
|
+
try {
|
|
16946
|
+
const response = await fetch(this.instance.baseUrl, {
|
|
16947
|
+
method: "POST",
|
|
16948
|
+
headers: {
|
|
16949
|
+
"content-type": "application/json",
|
|
16950
|
+
"x-api-key": apiKey,
|
|
16951
|
+
"anthropic-version": ANTHROPIC_API_VERSION
|
|
16952
|
+
},
|
|
16953
|
+
body: JSON.stringify(body),
|
|
16954
|
+
signal: timeoutController.signal
|
|
16955
|
+
});
|
|
16956
|
+
if (!response.ok) {
|
|
16957
|
+
let bodyText = "";
|
|
16958
|
+
try {
|
|
16959
|
+
bodyText = await response.text();
|
|
16960
|
+
} catch {
|
|
16961
|
+
}
|
|
16962
|
+
throw new Error(`HTTP ${response.status}${bodyText ? `: ${bodyText}` : ""}`);
|
|
16963
|
+
}
|
|
16964
|
+
const data = await response.json();
|
|
16965
|
+
const content = data.content;
|
|
16966
|
+
const rawText = content?.[0]?.text ?? "";
|
|
16967
|
+
const u = data.usage;
|
|
16968
|
+
const usage = u ? {
|
|
16969
|
+
inputTokens: u.input_tokens,
|
|
16970
|
+
outputTokens: u.output_tokens,
|
|
16971
|
+
aiProvider: "anthropic",
|
|
16972
|
+
aiModel: this.instance.model
|
|
16973
|
+
} : void 0;
|
|
16974
|
+
return {
|
|
16975
|
+
rawText,
|
|
16976
|
+
parsed: safeJsonParse(rawText),
|
|
16977
|
+
...usage ? { usage } : {}
|
|
16978
|
+
};
|
|
16979
|
+
} finally {
|
|
16980
|
+
clearTimeout(timer);
|
|
16981
|
+
signal.removeEventListener("abort", onAbort);
|
|
16982
|
+
}
|
|
16983
|
+
}
|
|
16984
|
+
};
|
|
16985
|
+
function createAnthropicApiExecutor(config, capabilityId = "anthropic-api", deps = {}) {
|
|
16986
|
+
const rawApiKeyFrom = config.apiKeyFrom;
|
|
16987
|
+
const rawApiKey = config.apiKey;
|
|
16988
|
+
if (rawApiKeyFrom !== void 0 && rawApiKey !== void 0) {
|
|
16989
|
+
throw new Error('AnthropicApiExecutor accepts either "apiKey" (static) or "apiKeyFrom" (dynamic), not both');
|
|
16990
|
+
}
|
|
16991
|
+
const resolveKey = rawApiKeyFrom !== void 0 ? buildDynamicResolver(config, deps) : buildStaticResolver(rawApiKey);
|
|
16992
|
+
let baseUrl = DEFAULT_BASE_URL;
|
|
16993
|
+
if (config.baseUrl !== void 0) {
|
|
16994
|
+
if (typeof config.baseUrl !== "string" || config.baseUrl.length === 0) {
|
|
16995
|
+
throw new Error('"baseUrl" must be a non-empty string when set');
|
|
16996
|
+
}
|
|
16997
|
+
baseUrl = config.baseUrl;
|
|
16998
|
+
}
|
|
16999
|
+
let model = DEFAULT_MODEL;
|
|
17000
|
+
if (config.model !== void 0) {
|
|
17001
|
+
if (typeof config.model !== "string" || config.model.length === 0) {
|
|
17002
|
+
throw new Error('"model" must be a non-empty string when set');
|
|
17003
|
+
}
|
|
17004
|
+
model = config.model;
|
|
17005
|
+
}
|
|
17006
|
+
let maxTokens = DEFAULT_MAX_TOKENS;
|
|
17007
|
+
if (config.maxTokens !== void 0) {
|
|
17008
|
+
if (typeof config.maxTokens !== "number" || config.maxTokens <= 0) {
|
|
17009
|
+
throw new Error('"maxTokens" must be a positive number');
|
|
17010
|
+
}
|
|
17011
|
+
maxTokens = config.maxTokens;
|
|
17012
|
+
}
|
|
17013
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
17014
|
+
if (config.timeout !== void 0) {
|
|
17015
|
+
if (typeof config.timeout !== "number" || config.timeout <= 0) {
|
|
17016
|
+
throw new Error('"timeout" must be a positive number');
|
|
17017
|
+
}
|
|
17018
|
+
timeoutMs = config.timeout;
|
|
17019
|
+
}
|
|
17020
|
+
return new AnthropicApiExecutor({
|
|
17021
|
+
resolveKey,
|
|
17022
|
+
baseUrl,
|
|
17023
|
+
model,
|
|
17024
|
+
maxTokens,
|
|
17025
|
+
timeoutMs,
|
|
17026
|
+
capabilityId
|
|
17027
|
+
});
|
|
17028
|
+
}
|
|
17029
|
+
function buildStaticResolver(rawApiKey) {
|
|
17030
|
+
if (typeof rawApiKey !== "string" || rawApiKey.length === 0) {
|
|
17031
|
+
throw new Error('AnthropicApiExecutor requires a non-empty "apiKey" string in config (or "apiKeyFrom: \\"job-metadata\\"" for dynamic key mode)');
|
|
17032
|
+
}
|
|
17033
|
+
const resolved = (0, env_ref_1.resolveEnvRef)(rawApiKey, "apiKey");
|
|
17034
|
+
return async () => resolved;
|
|
17035
|
+
}
|
|
17036
|
+
function buildDynamicResolver(config, deps) {
|
|
17037
|
+
if (config.apiKeyFrom !== "job-metadata") {
|
|
17038
|
+
throw new Error(`Unsupported "apiKeyFrom" value: ${JSON.stringify(config.apiKeyFrom)}. Only "job-metadata" is supported in v0.2.`);
|
|
17039
|
+
}
|
|
17040
|
+
const keepApiUrl = resolveStringConfig(config, "keepApiUrl", "KEEP_API_URL");
|
|
17041
|
+
const clientId = resolveStringConfig(config, "keepClientId", "KEEP_CLIENT_ID");
|
|
17042
|
+
const clientSecret = resolveStringConfig(config, "keepClientSecret", "KEEP_CLIENT_SECRET");
|
|
17043
|
+
const stmFactory = deps.buildServiceTokenManager ?? ((cfg) => new keep_1.ServiceTokenManager(cfg));
|
|
17044
|
+
const stm = stmFactory({ keepApiUrl, clientId, clientSecret });
|
|
17045
|
+
const keepClientFactory = deps.buildKeepClient ?? ((cfg) => new keep_1.KeepClient(cfg));
|
|
17046
|
+
return async (req) => {
|
|
17047
|
+
const hints = req.providerHints ?? {};
|
|
17048
|
+
const customerId = typeof hints.customerId === "string" ? hints.customerId : void 0;
|
|
17049
|
+
if (!customerId) {
|
|
17050
|
+
throw new Error("AnthropicApiExecutor dynamic-key mode requires providerHints.customerId on every job");
|
|
17051
|
+
}
|
|
17052
|
+
const serviceToken = await stm.getToken();
|
|
17053
|
+
if (!serviceToken) {
|
|
17054
|
+
throw new Error("AnthropicApiExecutor dynamic-key mode failed to exchange a Keep service token. Check KEEP_CLIENT_ID / KEEP_CLIENT_SECRET.");
|
|
17055
|
+
}
|
|
17056
|
+
const keep = keepClientFactory({ baseUrl: keepApiUrl, serviceToken });
|
|
17057
|
+
const reveal = await keep.revealManagedSifterKey(customerId);
|
|
17058
|
+
return reveal.apiKey;
|
|
17059
|
+
};
|
|
17060
|
+
}
|
|
17061
|
+
function resolveStringConfig(config, field, envFallback) {
|
|
17062
|
+
const raw = config[field];
|
|
17063
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
17064
|
+
return (0, env_ref_1.resolveEnvRef)(raw, field);
|
|
17065
|
+
}
|
|
17066
|
+
const fromEnv = process.env[envFallback];
|
|
17067
|
+
if (fromEnv && fromEnv.length > 0) {
|
|
17068
|
+
return fromEnv;
|
|
17069
|
+
}
|
|
17070
|
+
throw new Error(`AnthropicApiExecutor dynamic-key mode requires "${field}" in config or the ${envFallback} env var`);
|
|
17071
|
+
}
|
|
17072
|
+
function safeJsonParse(s) {
|
|
17073
|
+
try {
|
|
17074
|
+
const parsed = JSON.parse(s);
|
|
17075
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
17076
|
+
} catch {
|
|
17077
|
+
return void 0;
|
|
17078
|
+
}
|
|
17079
|
+
}
|
|
17080
|
+
}
|
|
17081
|
+
});
|
|
17082
|
+
|
|
16749
17083
|
// ../../packages/shuttle/dist/executors/webhook.js
|
|
16750
17084
|
var require_webhook = __commonJS({
|
|
16751
17085
|
"../../packages/shuttle/dist/executors/webhook.js"(exports2) {
|
|
@@ -17458,6 +17792,7 @@ var require_shuttle = __commonJS({
|
|
|
17458
17792
|
var registry_1 = require_registry();
|
|
17459
17793
|
var claude_code_1 = require_claude_code();
|
|
17460
17794
|
var http_api_1 = require_http_api();
|
|
17795
|
+
var anthropic_api_1 = require_anthropic_api();
|
|
17461
17796
|
var webhook_1 = require_webhook();
|
|
17462
17797
|
var custom_script_1 = require_custom_script();
|
|
17463
17798
|
var logger_2 = require_logger2();
|
|
@@ -17555,6 +17890,7 @@ var require_shuttle = __commonJS({
|
|
|
17555
17890
|
const all = [
|
|
17556
17891
|
["claude-code", claude_code_1.createClaudeCodeExecutor],
|
|
17557
17892
|
["http-api", http_api_1.createHttpApiExecutor],
|
|
17893
|
+
["anthropic-api", anthropic_api_1.createAnthropicApiExecutor],
|
|
17558
17894
|
["webhook", webhook_1.createWebhookExecutor],
|
|
17559
17895
|
["custom-script", custom_script_1.createCustomScriptExecutor]
|
|
17560
17896
|
];
|
|
@@ -17569,7 +17905,7 @@ var require_shuttle = __commonJS({
|
|
|
17569
17905
|
const store = new store_1.PairingConfigStore(pairingFile);
|
|
17570
17906
|
if (store.exists()) {
|
|
17571
17907
|
const pairing = store.read();
|
|
17572
|
-
const tokens = new keep_1.
|
|
17908
|
+
const tokens = new keep_1.AgentTokenManager({
|
|
17573
17909
|
keepApiUrl: pairing.keepApiUrl,
|
|
17574
17910
|
clientId: pairing.credentials.clientId,
|
|
17575
17911
|
clientSecret: pairing.credentials.clientSecret,
|
|
@@ -18099,69 +18435,67 @@ var require_dist4 = __commonJS({
|
|
|
18099
18435
|
}
|
|
18100
18436
|
});
|
|
18101
18437
|
|
|
18102
|
-
// package.json
|
|
18103
|
-
var require_package = __commonJS({
|
|
18104
|
-
"package.json"(exports2, module2) {
|
|
18105
|
-
module2.exports = {
|
|
18106
|
-
name: "@whittlelabs/sifter",
|
|
18107
|
-
version: "0.2.0",
|
|
18108
|
-
description: "Whittle Sifter: paired AI reviewer for Whittle Sift attention pools.",
|
|
18109
|
-
bin: {
|
|
18110
|
-
"whittle-sifter": "./dist/bin.js"
|
|
18111
|
-
},
|
|
18112
|
-
main: "dist/bin.js",
|
|
18113
|
-
engines: {
|
|
18114
|
-
node: ">=20"
|
|
18115
|
-
},
|
|
18116
|
-
scripts: {
|
|
18117
|
-
dev: "tsx watch src/bin.ts",
|
|
18118
|
-
build: "node scripts/bundle.mjs",
|
|
18119
|
-
typecheck: "tsc --noEmit",
|
|
18120
|
-
start: "node dist/bin.js",
|
|
18121
|
-
lint: "eslint src/"
|
|
18122
|
-
},
|
|
18123
|
-
publishConfig: {
|
|
18124
|
-
registry: "https://registry.npmjs.org/",
|
|
18125
|
-
access: "public"
|
|
18126
|
-
},
|
|
18127
|
-
repository: {
|
|
18128
|
-
type: "git",
|
|
18129
|
-
url: "https://github.com/whittlelabs/whittlelabs.git",
|
|
18130
|
-
directory: "apps/whittle-sifter"
|
|
18131
|
-
},
|
|
18132
|
-
author: "Whittle Labs",
|
|
18133
|
-
license: "UNLICENSED",
|
|
18134
|
-
private: false,
|
|
18135
|
-
dependencies: {
|
|
18136
|
-
"@whittlelabs/shuttle": "workspace:*",
|
|
18137
|
-
yaml: "^2.7.1"
|
|
18138
|
-
},
|
|
18139
|
-
devDependencies: {
|
|
18140
|
-
"@types/node": "^20.10.5",
|
|
18141
|
-
esbuild: "^0.25.0",
|
|
18142
|
-
tsx: "^4.21.0",
|
|
18143
|
-
typescript: "^5.3.3"
|
|
18144
|
-
}
|
|
18145
|
-
};
|
|
18146
|
-
}
|
|
18147
|
-
});
|
|
18148
|
-
|
|
18149
18438
|
// src/bin.ts
|
|
18150
18439
|
var import_shuttle2 = __toESM(require_dist4());
|
|
18151
18440
|
|
|
18152
18441
|
// src/brand.ts
|
|
18153
18442
|
var import_path = require("path");
|
|
18154
18443
|
var import_promises = require("fs/promises");
|
|
18444
|
+
var import_readline = require("readline");
|
|
18155
18445
|
var import_yaml = __toESM(require_dist());
|
|
18156
18446
|
var import_shuttle = __toESM(require_dist4());
|
|
18157
|
-
|
|
18447
|
+
|
|
18448
|
+
// package.json
|
|
18449
|
+
var package_default = {
|
|
18450
|
+
name: "@whittlelabs/sifter",
|
|
18451
|
+
version: "0.3.1",
|
|
18452
|
+
description: "Whittle Sifter: paired AI reviewer for Whittle Sift attention pools.",
|
|
18453
|
+
bin: {
|
|
18454
|
+
"whittle-sifter": "./dist/bin.js"
|
|
18455
|
+
},
|
|
18456
|
+
main: "dist/bin.js",
|
|
18457
|
+
engines: {
|
|
18458
|
+
node: ">=20"
|
|
18459
|
+
},
|
|
18460
|
+
scripts: {
|
|
18461
|
+
dev: "tsx watch src/bin.ts",
|
|
18462
|
+
build: "node scripts/bundle.mjs",
|
|
18463
|
+
typecheck: "tsc --noEmit",
|
|
18464
|
+
start: "node dist/bin.js",
|
|
18465
|
+
lint: "eslint src/"
|
|
18466
|
+
},
|
|
18467
|
+
publishConfig: {
|
|
18468
|
+
registry: "https://registry.npmjs.org/",
|
|
18469
|
+
access: "public"
|
|
18470
|
+
},
|
|
18471
|
+
repository: {
|
|
18472
|
+
type: "git",
|
|
18473
|
+
url: "https://github.com/whittlelabs/whittlelabs.git",
|
|
18474
|
+
directory: "apps/whittle-sifter"
|
|
18475
|
+
},
|
|
18476
|
+
author: "Whittle Labs",
|
|
18477
|
+
license: "UNLICENSED",
|
|
18478
|
+
private: false,
|
|
18479
|
+
dependencies: {
|
|
18480
|
+
"@whittlelabs/shuttle": "workspace:*",
|
|
18481
|
+
yaml: "^2.7.1"
|
|
18482
|
+
},
|
|
18483
|
+
devDependencies: {
|
|
18484
|
+
"@types/node": "^20.10.5",
|
|
18485
|
+
esbuild: "^0.25.0",
|
|
18486
|
+
tsx: "^4.21.0",
|
|
18487
|
+
typescript: "^5.3.3"
|
|
18488
|
+
}
|
|
18489
|
+
};
|
|
18490
|
+
|
|
18491
|
+
// src/brand.ts
|
|
18158
18492
|
var sifterBrand = {
|
|
18159
18493
|
product: {
|
|
18160
18494
|
id: "sifter",
|
|
18161
18495
|
title: "Whittle Sifter",
|
|
18162
18496
|
cliBinary: "whittle-sifter",
|
|
18163
18497
|
packageName: "@whittlelabs/sifter",
|
|
18164
|
-
version:
|
|
18498
|
+
version: package_default.version,
|
|
18165
18499
|
description: "Pairs with Whittle Sift to run AI code reviews on your hardware with your credentials."
|
|
18166
18500
|
},
|
|
18167
18501
|
paths: {
|
|
@@ -18172,10 +18506,10 @@ var sifterBrand = {
|
|
|
18172
18506
|
// shared config across multiple machines.
|
|
18173
18507
|
configSearchPaths: ["~/.sifter/sifter.yaml"]
|
|
18174
18508
|
},
|
|
18175
|
-
executorAllowlist: ["claude-code", "http-api", "webhook", "custom-script"],
|
|
18509
|
+
executorAllowlist: ["claude-code", "anthropic-api", "http-api", "webhook", "custom-script"],
|
|
18176
18510
|
keepRegistration: {
|
|
18177
|
-
keepApiUrl: process.env.KEEP_API_URL ?? "https://keep.
|
|
18178
|
-
jobsApiUrl: process.env.JOBS_API_URL ?? "https://jobs.
|
|
18511
|
+
keepApiUrl: process.env.KEEP_API_URL ?? "https://keep.stage.whittlelabs.com",
|
|
18512
|
+
jobsApiUrl: process.env.JOBS_API_URL ?? "https://jobs.stage.whittlelabs.com",
|
|
18179
18513
|
oauthClientId: process.env.SIFTER_OAUTH_CLIENT_ID ?? "sifter",
|
|
18180
18514
|
orgSlug: "whittle-labs",
|
|
18181
18515
|
roleSlug: "sift-sifter",
|
|
@@ -18191,20 +18525,39 @@ var sifterBrand = {
|
|
|
18191
18525
|
id: "scaffold-run-config",
|
|
18192
18526
|
phase: "post-pair",
|
|
18193
18527
|
// The substrate's run command requires a YAML describing pools,
|
|
18194
|
-
// executors, and Jobs auth.
|
|
18195
|
-
//
|
|
18196
|
-
//
|
|
18197
|
-
//
|
|
18198
|
-
//
|
|
18199
|
-
//
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18528
|
+
// executors, and Jobs auth. The web form that minted the pair command
|
|
18529
|
+
// bakes the user's product-level choices (executor type, poll interval)
|
|
18530
|
+
// into the init flags; this step reads them from `ctx.setupOptions`.
|
|
18531
|
+
//
|
|
18532
|
+
// A re-pair must refresh the pairing-derived fields (Jobs URL, service
|
|
18533
|
+
// identity, pools) even when sifter.yaml already exists — otherwise a
|
|
18534
|
+
// stale file silently shadows the new pairing (exactly how a leftover
|
|
18535
|
+
// `localhost:3005` and an old `cwd` once survived a fresh pair). So we
|
|
18536
|
+
// merge rather than skip: rebuild the pairing fields from `pairing`, carry
|
|
18537
|
+
// over the user's executor block (e.g. claude-code `cwd`) and polling
|
|
18538
|
+
// unless a fresh flag overrides them, and log what happened. We only
|
|
18539
|
+
// (re)scaffold an executor block — which may prompt for `cwd` — when there
|
|
18540
|
+
// isn't one to preserve.
|
|
18541
|
+
run: async (ctx) => {
|
|
18542
|
+
const yamlPath = (0, import_path.join)(ctx.configDir, "sifter.yaml");
|
|
18543
|
+
const pairing = new import_shuttle.PairingConfigStore((0, import_path.join)(ctx.configDir, "config.json")).read();
|
|
18203
18544
|
if (pairing.attentionPools.length === 0) {
|
|
18204
18545
|
throw new Error(
|
|
18205
18546
|
"Pairing returned no attention pools. The Sift backend should have provisioned one for this user during pre-pair."
|
|
18206
18547
|
);
|
|
18207
18548
|
}
|
|
18549
|
+
let existing = {};
|
|
18550
|
+
try {
|
|
18551
|
+
existing = (0, import_yaml.parse)(await (0, import_promises.readFile)(yamlPath, "utf8")) ?? {};
|
|
18552
|
+
} catch (err) {
|
|
18553
|
+
if (err.code !== "ENOENT") throw err;
|
|
18554
|
+
}
|
|
18555
|
+
const isUpdate = Object.keys(existing).length > 0;
|
|
18556
|
+
const existingExecutors = existing.executors ?? {};
|
|
18557
|
+
const existingPools = Array.isArray(existing.pools) ? existing.pools : [];
|
|
18558
|
+
const priorExecutor = typeof existingPools[0]?.executor === "string" ? existingPools[0].executor : void 0;
|
|
18559
|
+
const executor = ctx.setupOptions.executor ?? priorExecutor ?? "claude-code";
|
|
18560
|
+
const executorBlock = !ctx.setupOptions.executor && existingExecutors[executor] !== void 0 ? existingExecutors[executor] : await buildExecutorBlock(executor, ctx);
|
|
18208
18561
|
const doc = {
|
|
18209
18562
|
auth: {
|
|
18210
18563
|
jobsApiUrl: pairing.jobsApiUrl
|
|
@@ -18216,21 +18569,55 @@ var sifterBrand = {
|
|
|
18216
18569
|
pools: pairing.attentionPools.map((p) => ({
|
|
18217
18570
|
id: p.id,
|
|
18218
18571
|
name: p.name,
|
|
18219
|
-
executor
|
|
18572
|
+
executor
|
|
18220
18573
|
})),
|
|
18221
18574
|
executors: {
|
|
18222
|
-
|
|
18575
|
+
...existingExecutors,
|
|
18576
|
+
[executor]: executorBlock
|
|
18223
18577
|
}
|
|
18224
18578
|
};
|
|
18225
|
-
|
|
18226
|
-
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
});
|
|
18579
|
+
const pollIntervalMs = ctx.setupOptions.pollIntervalMs ?? existing.polling?.intervalMs;
|
|
18580
|
+
if (pollIntervalMs !== void 0) {
|
|
18581
|
+
doc.polling = { intervalMs: pollIntervalMs };
|
|
18582
|
+
}
|
|
18583
|
+
await (0, import_promises.mkdir)(ctx.configDir, { recursive: true });
|
|
18584
|
+
await (0, import_promises.writeFile)(yamlPath, (0, import_yaml.stringify)(doc));
|
|
18585
|
+
ctx.log(
|
|
18586
|
+
isUpdate ? `Refreshed ${yamlPath} from this pairing (Jobs URL, identity, pools); kept your executor and polling settings.` : `Wrote ${yamlPath}.`
|
|
18587
|
+
);
|
|
18230
18588
|
}
|
|
18231
18589
|
}
|
|
18232
18590
|
]
|
|
18233
18591
|
};
|
|
18592
|
+
async function buildExecutorBlock(executor, ctx) {
|
|
18593
|
+
switch (executor) {
|
|
18594
|
+
case "claude-code": {
|
|
18595
|
+
const cwd = ctx.setupOptions.cwd ?? await promptForCwd(`Where should Claude Code run? [${process.cwd()}]: `, process.cwd());
|
|
18596
|
+
return { type: "claude-code", cwd };
|
|
18597
|
+
}
|
|
18598
|
+
case "anthropic-api": {
|
|
18599
|
+
return {
|
|
18600
|
+
type: "anthropic-api",
|
|
18601
|
+
apiKey: "${ANTHROPIC_API_KEY}"
|
|
18602
|
+
};
|
|
18603
|
+
}
|
|
18604
|
+
default:
|
|
18605
|
+
return { type: executor };
|
|
18606
|
+
}
|
|
18607
|
+
}
|
|
18608
|
+
async function promptForCwd(prompt, fallback) {
|
|
18609
|
+
if (!process.stdin.isTTY) return fallback;
|
|
18610
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
18611
|
+
try {
|
|
18612
|
+
const answer = await new Promise((resolve) => {
|
|
18613
|
+
rl.question(prompt, (input) => resolve(input));
|
|
18614
|
+
});
|
|
18615
|
+
const trimmed = answer.trim();
|
|
18616
|
+
return trimmed.length === 0 ? fallback : trimmed;
|
|
18617
|
+
} finally {
|
|
18618
|
+
rl.close();
|
|
18619
|
+
}
|
|
18620
|
+
}
|
|
18234
18621
|
|
|
18235
18622
|
// src/bin.ts
|
|
18236
18623
|
(0, import_shuttle2.createCli)(sifterBrand).parseAsync(process.argv).catch((err) => {
|