@whittlelabs/sifter 0.1.3 → 0.3.0
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 +467 -80
- 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({
|
|
@@ -15279,6 +15320,7 @@ var require_client = __commonJS({
|
|
|
15279
15320
|
var JobsClient = class {
|
|
15280
15321
|
baseUrl;
|
|
15281
15322
|
headers;
|
|
15323
|
+
onResponse;
|
|
15282
15324
|
constructor(config) {
|
|
15283
15325
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
15284
15326
|
this.headers = { "Content-Type": "application/json" };
|
|
@@ -15287,6 +15329,7 @@ var require_client = __commonJS({
|
|
|
15287
15329
|
} else if (config.accessToken) {
|
|
15288
15330
|
this.headers["Authorization"] = `Bearer ${config.accessToken}`;
|
|
15289
15331
|
}
|
|
15332
|
+
this.onResponse = config.onResponse;
|
|
15290
15333
|
}
|
|
15291
15334
|
// ── Attention Pools ──────────────────────────────────────────────
|
|
15292
15335
|
async createAttentionPool(options) {
|
|
@@ -15490,6 +15533,7 @@ var require_client = __commonJS({
|
|
|
15490
15533
|
headers: this.headers,
|
|
15491
15534
|
body: body ? JSON.stringify(body) : void 0
|
|
15492
15535
|
});
|
|
15536
|
+
this.onResponse?.(response);
|
|
15493
15537
|
const json = await response.json();
|
|
15494
15538
|
if (!response.ok || !json.success) {
|
|
15495
15539
|
const errorMessage = json.error?.message ?? `Request failed with status ${response.status}`;
|
|
@@ -15503,6 +15547,7 @@ var require_client = __commonJS({
|
|
|
15503
15547
|
async requestPaginated(method, path) {
|
|
15504
15548
|
const url = this.buildUrl(path);
|
|
15505
15549
|
const response = await fetch(url, { method, headers: this.headers });
|
|
15550
|
+
this.onResponse?.(response);
|
|
15506
15551
|
const json = await response.json();
|
|
15507
15552
|
if (!response.ok || !json.success) {
|
|
15508
15553
|
const errorMessage = json.error?.message ?? `Request failed with status ${response.status}`;
|
|
@@ -16245,6 +16290,32 @@ var require_client2 = __commonJS({
|
|
|
16245
16290
|
ttlSeconds: request.ttlSeconds
|
|
16246
16291
|
});
|
|
16247
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
|
+
}
|
|
16248
16319
|
// ── HTTP ────────────────────────────────────────────────────────
|
|
16249
16320
|
async request(method, path, body) {
|
|
16250
16321
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -16282,11 +16353,13 @@ var require_service_token = __commonJS({
|
|
|
16282
16353
|
clientId;
|
|
16283
16354
|
clientSecret;
|
|
16284
16355
|
enabled;
|
|
16356
|
+
onResponse;
|
|
16285
16357
|
constructor(config) {
|
|
16286
16358
|
this.keepApiUrl = config.keepApiUrl.replace(/\/$/, "");
|
|
16287
16359
|
this.clientId = config.clientId;
|
|
16288
16360
|
this.clientSecret = config.clientSecret;
|
|
16289
16361
|
this.enabled = !!(config.clientId && config.clientSecret && config.keepApiUrl);
|
|
16362
|
+
this.onResponse = config.onResponse;
|
|
16290
16363
|
if (!this.enabled) {
|
|
16291
16364
|
console.warn("[ServiceTokenManager] Keep credentials not configured (KEEP_CLIENT_ID, KEEP_CLIENT_SECRET, KEEP_API_URL). Service-to-service auth is disabled.");
|
|
16292
16365
|
}
|
|
@@ -16321,6 +16394,7 @@ var require_service_token = __commonJS({
|
|
|
16321
16394
|
clientSecret: this.clientSecret
|
|
16322
16395
|
})
|
|
16323
16396
|
});
|
|
16397
|
+
this.onResponse?.(response);
|
|
16324
16398
|
const json = await response.json();
|
|
16325
16399
|
if (!response.ok || !json.success) {
|
|
16326
16400
|
const msg = json.error?.message ?? `Token exchange failed with status ${response.status}`;
|
|
@@ -16335,12 +16409,75 @@ var require_service_token = __commonJS({
|
|
|
16335
16409
|
}
|
|
16336
16410
|
});
|
|
16337
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
|
+
|
|
16338
16475
|
// ../../packages/keep/dist/index.js
|
|
16339
16476
|
var require_dist3 = __commonJS({
|
|
16340
16477
|
"../../packages/keep/dist/index.js"(exports2) {
|
|
16341
16478
|
"use strict";
|
|
16342
16479
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
16343
|
-
exports2.ServiceTokenManager = exports2.KeepClient = void 0;
|
|
16480
|
+
exports2.AgentTokenManager = exports2.ServiceTokenManager = exports2.KeepClient = void 0;
|
|
16344
16481
|
var client_1 = require_client2();
|
|
16345
16482
|
Object.defineProperty(exports2, "KeepClient", { enumerable: true, get: function() {
|
|
16346
16483
|
return client_1.KeepClient;
|
|
@@ -16349,6 +16486,10 @@ var require_dist3 = __commonJS({
|
|
|
16349
16486
|
Object.defineProperty(exports2, "ServiceTokenManager", { enumerable: true, get: function() {
|
|
16350
16487
|
return service_token_1.ServiceTokenManager;
|
|
16351
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
|
+
} });
|
|
16352
16493
|
}
|
|
16353
16494
|
});
|
|
16354
16495
|
|
|
@@ -16739,6 +16880,206 @@ var require_http_api = __commonJS({
|
|
|
16739
16880
|
}
|
|
16740
16881
|
});
|
|
16741
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
|
+
|
|
16742
17083
|
// ../../packages/shuttle/dist/executors/webhook.js
|
|
16743
17084
|
var require_webhook = __commonJS({
|
|
16744
17085
|
"../../packages/shuttle/dist/executors/webhook.js"(exports2) {
|
|
@@ -17446,10 +17787,12 @@ var require_shuttle = __commonJS({
|
|
|
17446
17787
|
var jobs_1 = require_dist2();
|
|
17447
17788
|
var keep_1 = require_dist3();
|
|
17448
17789
|
var apply_1 = require_apply();
|
|
17790
|
+
var version_check_1 = require_version_check();
|
|
17449
17791
|
var logger_1 = require_logger();
|
|
17450
17792
|
var registry_1 = require_registry();
|
|
17451
17793
|
var claude_code_1 = require_claude_code();
|
|
17452
17794
|
var http_api_1 = require_http_api();
|
|
17795
|
+
var anthropic_api_1 = require_anthropic_api();
|
|
17453
17796
|
var webhook_1 = require_webhook();
|
|
17454
17797
|
var custom_script_1 = require_custom_script();
|
|
17455
17798
|
var logger_2 = require_logger2();
|
|
@@ -17477,11 +17820,19 @@ var require_shuttle = __commonJS({
|
|
|
17477
17820
|
const logger = (0, logger_1.getLogger)();
|
|
17478
17821
|
logger.info(`${this.brand.product.title} starting...`);
|
|
17479
17822
|
const paths = (0, apply_1.resolvePaths)(this.brand);
|
|
17480
|
-
const
|
|
17823
|
+
const enforceMinVersion = (response) => {
|
|
17824
|
+
(0, version_check_1.checkResponseMinVersion)({
|
|
17825
|
+
response,
|
|
17826
|
+
currentVersion: this.brand.product.version,
|
|
17827
|
+
packageName: this.brand.product.packageName
|
|
17828
|
+
});
|
|
17829
|
+
};
|
|
17830
|
+
const auth = await resolveAuth(this.brand, this.config, paths.configFile, enforceMinVersion);
|
|
17481
17831
|
const jobsClient = new jobs_1.JobsClient({
|
|
17482
17832
|
baseUrl: this.config.auth.jobsApiUrl,
|
|
17483
17833
|
apiKey: auth.kind === "apiKey" ? auth.apiKey : void 0,
|
|
17484
|
-
accessToken: auth.kind === "serviceToken" ? auth.accessToken : void 0
|
|
17834
|
+
accessToken: auth.kind === "serviceToken" ? auth.accessToken : void 0,
|
|
17835
|
+
onResponse: enforceMinVersion
|
|
17485
17836
|
});
|
|
17486
17837
|
const registry = new registry_1.ExecutorRegistry(this.brand.executorAllowlist);
|
|
17487
17838
|
registerBuiltInExecutors(registry, this.brand.executorAllowlist);
|
|
@@ -17539,6 +17890,7 @@ var require_shuttle = __commonJS({
|
|
|
17539
17890
|
const all = [
|
|
17540
17891
|
["claude-code", claude_code_1.createClaudeCodeExecutor],
|
|
17541
17892
|
["http-api", http_api_1.createHttpApiExecutor],
|
|
17893
|
+
["anthropic-api", anthropic_api_1.createAnthropicApiExecutor],
|
|
17542
17894
|
["webhook", webhook_1.createWebhookExecutor],
|
|
17543
17895
|
["custom-script", custom_script_1.createCustomScriptExecutor]
|
|
17544
17896
|
];
|
|
@@ -17549,14 +17901,15 @@ var require_shuttle = __commonJS({
|
|
|
17549
17901
|
}
|
|
17550
17902
|
}
|
|
17551
17903
|
}
|
|
17552
|
-
async function resolveAuth(brand, config, pairingFile) {
|
|
17904
|
+
async function resolveAuth(brand, config, pairingFile, onResponse) {
|
|
17553
17905
|
const store = new store_1.PairingConfigStore(pairingFile);
|
|
17554
17906
|
if (store.exists()) {
|
|
17555
17907
|
const pairing = store.read();
|
|
17556
|
-
const tokens = new keep_1.
|
|
17908
|
+
const tokens = new keep_1.AgentTokenManager({
|
|
17557
17909
|
keepApiUrl: pairing.keepApiUrl,
|
|
17558
17910
|
clientId: pairing.credentials.clientId,
|
|
17559
|
-
clientSecret: pairing.credentials.clientSecret
|
|
17911
|
+
clientSecret: pairing.credentials.clientSecret,
|
|
17912
|
+
onResponse
|
|
17560
17913
|
});
|
|
17561
17914
|
const token = await tokens.getToken();
|
|
17562
17915
|
if (token)
|
|
@@ -18082,69 +18435,67 @@ var require_dist4 = __commonJS({
|
|
|
18082
18435
|
}
|
|
18083
18436
|
});
|
|
18084
18437
|
|
|
18085
|
-
// package.json
|
|
18086
|
-
var require_package = __commonJS({
|
|
18087
|
-
"package.json"(exports2, module2) {
|
|
18088
|
-
module2.exports = {
|
|
18089
|
-
name: "@whittlelabs/sifter",
|
|
18090
|
-
version: "0.1.3",
|
|
18091
|
-
description: "Whittle Sifter: paired AI reviewer for Whittle Sift attention pools.",
|
|
18092
|
-
bin: {
|
|
18093
|
-
"whittle-sifter": "./dist/bin.js"
|
|
18094
|
-
},
|
|
18095
|
-
main: "dist/bin.js",
|
|
18096
|
-
engines: {
|
|
18097
|
-
node: ">=20"
|
|
18098
|
-
},
|
|
18099
|
-
scripts: {
|
|
18100
|
-
dev: "tsx watch src/bin.ts",
|
|
18101
|
-
build: "node scripts/bundle.mjs",
|
|
18102
|
-
typecheck: "tsc --noEmit",
|
|
18103
|
-
start: "node dist/bin.js",
|
|
18104
|
-
lint: "eslint src/"
|
|
18105
|
-
},
|
|
18106
|
-
publishConfig: {
|
|
18107
|
-
registry: "https://registry.npmjs.org/",
|
|
18108
|
-
access: "public"
|
|
18109
|
-
},
|
|
18110
|
-
repository: {
|
|
18111
|
-
type: "git",
|
|
18112
|
-
url: "https://github.com/whittlelabs/whittlelabs.git",
|
|
18113
|
-
directory: "apps/whittle-sifter"
|
|
18114
|
-
},
|
|
18115
|
-
author: "Whittle Labs",
|
|
18116
|
-
license: "UNLICENSED",
|
|
18117
|
-
private: false,
|
|
18118
|
-
dependencies: {
|
|
18119
|
-
"@whittlelabs/shuttle": "workspace:*",
|
|
18120
|
-
yaml: "^2.7.1"
|
|
18121
|
-
},
|
|
18122
|
-
devDependencies: {
|
|
18123
|
-
"@types/node": "^20.10.5",
|
|
18124
|
-
esbuild: "^0.25.0",
|
|
18125
|
-
tsx: "^4.21.0",
|
|
18126
|
-
typescript: "^5.3.3"
|
|
18127
|
-
}
|
|
18128
|
-
};
|
|
18129
|
-
}
|
|
18130
|
-
});
|
|
18131
|
-
|
|
18132
18438
|
// src/bin.ts
|
|
18133
18439
|
var import_shuttle2 = __toESM(require_dist4());
|
|
18134
18440
|
|
|
18135
18441
|
// src/brand.ts
|
|
18136
18442
|
var import_path = require("path");
|
|
18137
18443
|
var import_promises = require("fs/promises");
|
|
18444
|
+
var import_readline = require("readline");
|
|
18138
18445
|
var import_yaml = __toESM(require_dist());
|
|
18139
18446
|
var import_shuttle = __toESM(require_dist4());
|
|
18140
|
-
|
|
18447
|
+
|
|
18448
|
+
// package.json
|
|
18449
|
+
var package_default = {
|
|
18450
|
+
name: "@whittlelabs/sifter",
|
|
18451
|
+
version: "0.3.0",
|
|
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
|
|
18141
18492
|
var sifterBrand = {
|
|
18142
18493
|
product: {
|
|
18143
18494
|
id: "sifter",
|
|
18144
18495
|
title: "Whittle Sifter",
|
|
18145
18496
|
cliBinary: "whittle-sifter",
|
|
18146
18497
|
packageName: "@whittlelabs/sifter",
|
|
18147
|
-
version:
|
|
18498
|
+
version: package_default.version,
|
|
18148
18499
|
description: "Pairs with Whittle Sift to run AI code reviews on your hardware with your credentials."
|
|
18149
18500
|
},
|
|
18150
18501
|
paths: {
|
|
@@ -18155,10 +18506,10 @@ var sifterBrand = {
|
|
|
18155
18506
|
// shared config across multiple machines.
|
|
18156
18507
|
configSearchPaths: ["~/.sifter/sifter.yaml"]
|
|
18157
18508
|
},
|
|
18158
|
-
executorAllowlist: ["claude-code", "http-api", "webhook", "custom-script"],
|
|
18509
|
+
executorAllowlist: ["claude-code", "anthropic-api", "http-api", "webhook", "custom-script"],
|
|
18159
18510
|
keepRegistration: {
|
|
18160
|
-
keepApiUrl: process.env.KEEP_API_URL ?? "https://keep.
|
|
18161
|
-
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",
|
|
18162
18513
|
oauthClientId: process.env.SIFTER_OAUTH_CLIENT_ID ?? "sifter",
|
|
18163
18514
|
orgSlug: "whittle-labs",
|
|
18164
18515
|
roleSlug: "sift-sifter",
|
|
@@ -18174,20 +18525,22 @@ var sifterBrand = {
|
|
|
18174
18525
|
id: "scaffold-run-config",
|
|
18175
18526
|
phase: "post-pair",
|
|
18176
18527
|
// The substrate's run command requires a YAML describing pools,
|
|
18177
|
-
// executors, and Jobs auth.
|
|
18178
|
-
//
|
|
18179
|
-
//
|
|
18180
|
-
//
|
|
18181
|
-
//
|
|
18182
|
-
//
|
|
18183
|
-
run: async (
|
|
18184
|
-
const yamlPath = (0, import_path.join)(configDir, "sifter.yaml");
|
|
18185
|
-
const pairing = new import_shuttle.PairingConfigStore((0, import_path.join)(configDir, "config.json")).read();
|
|
18528
|
+
// executors, and Jobs auth. The web form that minted the pair
|
|
18529
|
+
// command bakes the user's product-level choices (executor type,
|
|
18530
|
+
// poll interval, caps) into the init flags. This step reads
|
|
18531
|
+
// those choices from `ctx.setupOptions` and writes the matching
|
|
18532
|
+
// YAML. For `claude-code` we prompt interactively for `cwd` if
|
|
18533
|
+
// the user didn't pass `--cwd`; other executors don't need one.
|
|
18534
|
+
run: async (ctx) => {
|
|
18535
|
+
const yamlPath = (0, import_path.join)(ctx.configDir, "sifter.yaml");
|
|
18536
|
+
const pairing = new import_shuttle.PairingConfigStore((0, import_path.join)(ctx.configDir, "config.json")).read();
|
|
18186
18537
|
if (pairing.attentionPools.length === 0) {
|
|
18187
18538
|
throw new Error(
|
|
18188
18539
|
"Pairing returned no attention pools. The Sift backend should have provisioned one for this user during pre-pair."
|
|
18189
18540
|
);
|
|
18190
18541
|
}
|
|
18542
|
+
const executor = ctx.setupOptions.executor ?? "claude-code";
|
|
18543
|
+
const executorBlock = await buildExecutorBlock(executor, ctx);
|
|
18191
18544
|
const doc = {
|
|
18192
18545
|
auth: {
|
|
18193
18546
|
jobsApiUrl: pairing.jobsApiUrl
|
|
@@ -18199,21 +18552,55 @@ var sifterBrand = {
|
|
|
18199
18552
|
pools: pairing.attentionPools.map((p) => ({
|
|
18200
18553
|
id: p.id,
|
|
18201
18554
|
name: p.name,
|
|
18202
|
-
executor
|
|
18555
|
+
executor
|
|
18203
18556
|
})),
|
|
18204
18557
|
executors: {
|
|
18205
|
-
|
|
18558
|
+
[executor]: executorBlock
|
|
18206
18559
|
}
|
|
18207
18560
|
};
|
|
18208
|
-
|
|
18209
|
-
|
|
18210
|
-
|
|
18211
|
-
|
|
18212
|
-
})
|
|
18561
|
+
if (ctx.setupOptions.pollIntervalMs !== void 0) {
|
|
18562
|
+
doc.polling = { intervalMs: ctx.setupOptions.pollIntervalMs };
|
|
18563
|
+
}
|
|
18564
|
+
await (0, import_promises.mkdir)(ctx.configDir, { recursive: true });
|
|
18565
|
+
await (0, import_promises.writeFile)(yamlPath, (0, import_yaml.stringify)(doc), { flag: "wx" }).catch(
|
|
18566
|
+
(err) => {
|
|
18567
|
+
if (err.code === "EEXIST") return;
|
|
18568
|
+
throw err;
|
|
18569
|
+
}
|
|
18570
|
+
);
|
|
18213
18571
|
}
|
|
18214
18572
|
}
|
|
18215
18573
|
]
|
|
18216
18574
|
};
|
|
18575
|
+
async function buildExecutorBlock(executor, ctx) {
|
|
18576
|
+
switch (executor) {
|
|
18577
|
+
case "claude-code": {
|
|
18578
|
+
const cwd = ctx.setupOptions.cwd ?? await promptForCwd(`Where should Claude Code run? [${process.cwd()}]: `, process.cwd());
|
|
18579
|
+
return { type: "claude-code", cwd };
|
|
18580
|
+
}
|
|
18581
|
+
case "anthropic-api": {
|
|
18582
|
+
return {
|
|
18583
|
+
type: "anthropic-api",
|
|
18584
|
+
apiKey: "${ANTHROPIC_API_KEY}"
|
|
18585
|
+
};
|
|
18586
|
+
}
|
|
18587
|
+
default:
|
|
18588
|
+
return { type: executor };
|
|
18589
|
+
}
|
|
18590
|
+
}
|
|
18591
|
+
async function promptForCwd(prompt, fallback) {
|
|
18592
|
+
if (!process.stdin.isTTY) return fallback;
|
|
18593
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
18594
|
+
try {
|
|
18595
|
+
const answer = await new Promise((resolve) => {
|
|
18596
|
+
rl.question(prompt, (input) => resolve(input));
|
|
18597
|
+
});
|
|
18598
|
+
const trimmed = answer.trim();
|
|
18599
|
+
return trimmed.length === 0 ? fallback : trimmed;
|
|
18600
|
+
} finally {
|
|
18601
|
+
rl.close();
|
|
18602
|
+
}
|
|
18603
|
+
}
|
|
18217
18604
|
|
|
18218
18605
|
// src/bin.ts
|
|
18219
18606
|
(0, import_shuttle2.createCli)(sifterBrand).parseAsync(process.argv).catch((err) => {
|