@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.
Files changed (3) hide show
  1. package/bin.js +467 -80
  2. package/bin.js.map +4 -4
  3. 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 auth = await resolveAuth(this.brand, this.config, paths.configFile);
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.ServiceTokenManager({
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
- var pkg = require_package();
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: pkg.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.staging.whittlelabs.com",
18161
- jobsApiUrl: process.env.JOBS_API_URL ?? "https://jobs.staging.whittlelabs.com",
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. At v0.1 the Sifter has no per-user
18178
- // executor variation worth surfacing, so we synthesize the YAML
18179
- // here from the pairing config that init just wrote. The user
18180
- // never has to author the file by hand. If the file already
18181
- // exists from a prior pair, leave it alone so any local tweaks
18182
- // survive re-pairing.
18183
- run: async ({ configDir }) => {
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: "claude-code"
18555
+ executor
18203
18556
  })),
18204
18557
  executors: {
18205
- "claude-code": { type: "claude-code" }
18558
+ [executor]: executorBlock
18206
18559
  }
18207
18560
  };
18208
- await (0, import_promises.mkdir)(configDir, { recursive: true });
18209
- await (0, import_promises.writeFile)(yamlPath, (0, import_yaml.stringify)(doc), { flag: "wx" }).catch((err) => {
18210
- if (err.code === "EEXIST") return;
18211
- throw err;
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) => {