pullfrog 0.1.59 → 0.1.60

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.
@@ -120,6 +120,13 @@ export interface AgentRunContext {
120
120
  * server) so lingering SSE reconnects don't keep the outer timer alive.
121
121
  */
122
122
  onActivityTimeout?: (() => void) | undefined;
123
+ /**
124
+ * called when an aborted turn actually comes back and the harness intends to
125
+ * keep going. stands down the safety-net timer `onActivityTimeout` armed, so
126
+ * it only ever guards the window where an abort might have been ignored
127
+ * rather than force-exiting a run mid-salvage. see #1085.
128
+ */
129
+ onTurnRecovered?: (() => void) | undefined;
123
130
  onToolUse?: ((event: AgentToolUseEvent) => void) | undefined;
124
131
  /**
125
132
  * Pullfrog API JWT scoped to this run. agents only need this when they
package/dist/cli.mjs CHANGED
@@ -106587,6 +106587,11 @@ var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
106587
106587
  var AGENT_ACTIVITY_TIMEOUT_MS = 9e5;
106588
106588
  var AGENT_FIRST_EVENT_TIMEOUT_MS = 12e4;
106589
106589
  var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
106590
+ function watchdogBudgetMs(compiled, envVar) {
106591
+ const raw2 = Number(process.env[envVar]);
106592
+ if (!Number.isFinite(raw2) || raw2 <= 0) return compiled;
106593
+ return Math.min(compiled, raw2);
106594
+ }
106590
106595
  var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
106591
106596
  var ACTIVITY_NOISE_PATTERNS = [
106592
106597
  new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
@@ -107350,7 +107355,7 @@ var import_semver = __toESM(require_semver2(), 1);
107350
107355
  // package.json
107351
107356
  var package_default = {
107352
107357
  name: "pullfrog",
107353
- version: "0.1.59",
107358
+ version: "0.1.60",
107354
107359
  type: "module",
107355
107360
  bin: {
107356
107361
  pullfrog: "dist/cli.mjs",
@@ -109459,7 +109464,7 @@ var claude = agent({
109459
109464
  if ((isBedrockRoute || isVertexRoute2) && specifier && effort.alias?.effort) {
109460
109465
  applyHostedEffortCapabilities({ env: env2, modelId: specifier, levels: effort.alias.effort });
109461
109466
  }
109462
- if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && env2.ANTHROPIC_API_KEY) {
109467
+ if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && !isVertexRoute2 && !env2.ANTHROPIC_BASE_URL && !env2.ANTHROPIC_AUTH_TOKEN && env2.ANTHROPIC_API_KEY) {
109463
109468
  const preflight = await preflightClaudeSubscription({
109464
109469
  token: env2.CLAUDE_CODE_OAUTH_TOKEN,
109465
109470
  model
@@ -116277,6 +116282,7 @@ function processTerminalToolPart(ctx, part, label, isOrchestrator) {
116277
116282
  const callLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
116278
116283
  log2.info(withLabel(label, callLine));
116279
116284
  if (isOrchestrator) ctx.loggedToolCallIDs.add(toolId);
116285
+ if (isOrchestrator && toolName.startsWith("pullfrog_")) ctx.mcpToolCalls++;
116280
116286
  if (state.status === "completed") {
116281
116287
  log2.debug(withLabel(label, ` output: ${state.output}`));
116282
116288
  } else {
@@ -116515,21 +116521,28 @@ function formatConfigError(name, data) {
116515
116521
  ).join("");
116516
116522
  return `${name}${detail}${bullets}`;
116517
116523
  }
116524
+ var WATCHDOG_SALVAGE_PROMPT = "Your previous turn was cut off mid-response by a provider stall. Nothing you had not already submitted through a tool was saved. Continue from where you stopped and submit your work now \u2014 do not restart your analysis.";
116518
116525
  function startInnerActivityWatchdog(params) {
116519
116526
  let fired = false;
116527
+ let everFired = false;
116520
116528
  const id = setInterval(() => {
116521
116529
  if (fired) return;
116522
116530
  const idleMs = performance7.now() - params.ctx.lastEventAt;
116523
- const budgetMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116531
+ const compiledMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116532
+ const budgetMs = everFired ? compiledMs : watchdogBudgetMs(
116533
+ compiledMs,
116534
+ params.ctx.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
116535
+ );
116524
116536
  if (idleMs <= budgetMs) return;
116525
116537
  fired = true;
116538
+ everFired = true;
116526
116539
  const idleSec = Math.round(idleMs / 1e3);
116527
116540
  params.ctx.diagnostic.idleSec = idleSec;
116528
116541
  params.ctx.diagnostic.sawModelOutput = params.ctx.sawModelOutput;
116529
116542
  log2.info(
116530
116543
  params.ctx.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
116531
116544
  );
116532
- params.abortController.abort();
116545
+ params.abortTurn();
116533
116546
  try {
116534
116547
  params.ctx.onActivityTimeout?.();
116535
116548
  } catch (err) {
@@ -116539,7 +116552,22 @@ function startInnerActivityWatchdog(params) {
116539
116552
  }
116540
116553
  }, 5e3);
116541
116554
  id.unref?.();
116542
- return { stop: () => clearInterval(id) };
116555
+ return {
116556
+ stop: () => clearInterval(id),
116557
+ /**
116558
+ * Start a turn's idle budget from now. The clock must be reset with the
116559
+ * latch: `lastEventAt` only advances on model output, so a turn following a
116560
+ * stall would inherit the full stalled interval and be aborted on the very
116561
+ * next 5s tick — killing the salvage before the provider could answer. It
116562
+ * is the right semantics for an ordinary resume too, whose budget should
116563
+ * not be pre-spent by the post-run gate checks that ran between turns.
116564
+ */
116565
+ armForTurn: () => {
116566
+ fired = false;
116567
+ params.ctx.lastEventAt = performance7.now();
116568
+ },
116569
+ firedThisTurn: () => fired
116570
+ };
116543
116571
  }
116544
116572
  var opencode = agent({
116545
116573
  name: "opencode",
@@ -116651,6 +116679,7 @@ var opencode = agent({
116651
116679
  variant: effort.rung,
116652
116680
  todoTracker: ctx.todoTracker,
116653
116681
  onActivityTimeout: ctx.onActivityTimeout,
116682
+ onTurnRecovered: ctx.onTurnRecovered,
116654
116683
  onToolUse: ctx.onToolUse,
116655
116684
  currentTurn: null,
116656
116685
  eventCount: 0,
@@ -116659,6 +116688,7 @@ var opencode = agent({
116659
116688
  promptMessageID: void 0,
116660
116689
  taskDispatchByCallID: /* @__PURE__ */ new Map(),
116661
116690
  loggedToolCallIDs: /* @__PURE__ */ new Set(),
116691
+ mcpToolCalls: 0,
116662
116692
  recentStderr: server.recentStderr,
116663
116693
  diagnostic: {
116664
116694
  label: "Pullfrog",
@@ -116682,9 +116712,14 @@ var opencode = agent({
116682
116712
  }
116683
116713
  }
116684
116714
  });
116685
- const abortController = new AbortController();
116686
- const eventLoopPromise = consumeEvents(runnerCtx, abortController.signal).catch((err) => {
116687
- if (!abortController.signal.aborted) {
116715
+ const runController = new AbortController();
116716
+ let turnController = new AbortController();
116717
+ const nextTurnSignal = () => {
116718
+ turnController = new AbortController();
116719
+ return AbortSignal.any([runController.signal, turnController.signal]);
116720
+ };
116721
+ const eventLoopPromise = consumeEvents(runnerCtx, runController.signal).catch((err) => {
116722
+ if (!runController.signal.aborted) {
116688
116723
  log2.warning(
116689
116724
  `\xBB opencode event subscription ended: ${err instanceof Error ? err.message : String(err)}`
116690
116725
  );
@@ -116700,31 +116735,48 @@ var opencode = agent({
116700
116735
  // a long synchronous tool call (no part.updated while it runs) can't
116701
116736
  // false-positive it.
116702
116737
  timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
116703
- abortController
116738
+ abortTurn: () => turnController.abort()
116704
116739
  });
116705
116740
  const sdkModel = parseModel2(model);
116741
+ let salvagesLeft = 1;
116742
+ const runTurn = async (text) => {
116743
+ const attempt = (prompt) => {
116744
+ watchdog.armForTurn();
116745
+ return runTurnGuarded(
116746
+ runnerCtx,
116747
+ () => runPromptTurn(runnerCtx, { text: prompt, model: sdkModel, signal: nextTurnSignal() })
116748
+ );
116749
+ };
116750
+ const standDownIfRecovered = (turn) => {
116751
+ if (watchdog.firedThisTurn() && turn.success) ctx.onTurnRecovered?.();
116752
+ return turn;
116753
+ };
116754
+ const result = await attempt(text);
116755
+ if (!watchdog.firedThisTurn()) return result;
116756
+ if (result.success) return standDownIfRecovered(result);
116757
+ if (salvagesLeft <= 0) return result;
116758
+ salvagesLeft--;
116759
+ ctx.onTurnRecovered?.();
116760
+ log2.info("\xBB activity watchdog cut the turn off \u2014 re-prompting once on the same session");
116761
+ const mcpCallsBefore = runnerCtx.mcpToolCalls;
116762
+ const salvaged = await attempt(WATCHDOG_SALVAGE_PROMPT);
116763
+ const usage = mergeAgentUsage(result.usage, salvaged.usage);
116764
+ if (runnerCtx.mcpToolCalls === mcpCallsBefore) {
116765
+ log2.info(
116766
+ "\xBB salvage turn reached no Pullfrog tool \u2014 keeping the activity-timeout failure"
116767
+ );
116768
+ return { ...result, usage };
116769
+ }
116770
+ return standDownIfRecovered({ ...salvaged, usage });
116771
+ };
116706
116772
  try {
116707
- const initial = await runTurnGuarded(
116708
- runnerCtx,
116709
- () => runPromptTurn(runnerCtx, {
116710
- text: ctx.instructions.full,
116711
- model: sdkModel,
116712
- signal: abortController.signal
116713
- })
116714
- );
116773
+ const initial = await runTurn(ctx.instructions.full);
116715
116774
  const result = await runPostRunRetryLoop({
116716
116775
  ctx,
116717
116776
  initialResult: initial,
116718
116777
  initialUsage: initial.usage,
116719
116778
  reflectionPrompt: buildReflectionPrompt(ctx.toolState),
116720
- resume: async (c2) => runTurnGuarded(
116721
- runnerCtx,
116722
- () => runPromptTurn(runnerCtx, {
116723
- text: c2.prompt,
116724
- model: sdkModel,
116725
- signal: abortController.signal
116726
- })
116727
- )
116779
+ resume: async (c2) => runTurn(c2.prompt)
116728
116780
  });
116729
116781
  if (result.success) {
116730
116782
  await ctx.todoTracker?.flush();
@@ -116734,7 +116786,7 @@ var opencode = agent({
116734
116786
  return result;
116735
116787
  } finally {
116736
116788
  watchdog.stop();
116737
- abortController.abort();
116789
+ runController.abort();
116738
116790
  await eventLoopPromise.catch(() => {
116739
116791
  });
116740
116792
  }
@@ -194525,7 +194577,7 @@ function hasEnvVar2(name) {
194525
194577
  return typeof val === "string" && val.length > 0;
194526
194578
  }
194527
194579
  function hasClaudeCodeAuth() {
194528
- return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY");
194580
+ return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY") || hasEnvVar2("ANTHROPIC_AUTH_TOKEN");
194529
194581
  }
194530
194582
  function hasCodexAuth() {
194531
194583
  return hasEnvVar2("CODEX_AUTH_JSON") || hasEnvVar2("OPENAI_API_KEY");
@@ -194618,8 +194670,11 @@ function resolveAgent(ctx) {
194618
194670
  } catch {
194619
194671
  }
194620
194672
  }
194621
- if (!ctx.model && ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) {
194622
- return agents.codex;
194673
+ if (!ctx.model) {
194674
+ if (hasEnvVar2("ANTHROPIC_AUTH_TOKEN") && !hasEnvVar2("ANTHROPIC_API_KEY") && !hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN")) {
194675
+ return agents.claude;
194676
+ }
194677
+ if (ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) return agents.codex;
194623
194678
  }
194624
194679
  return agents.opencode;
194625
194680
  }
@@ -194805,7 +194860,7 @@ function hasSingleProviderAuth(agentName) {
194805
194860
  if (agentName === "codex") {
194806
194861
  return hasEnvVar3("OPENAI_API_KEY") || hasEnvVar3("CODEX_AUTH_JSON");
194807
194862
  }
194808
- return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
194863
+ return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("ANTHROPIC_AUTH_TOKEN") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
194809
194864
  }
194810
194865
  function validateAgentApiKey(params) {
194811
194866
  if (params.model) {
@@ -195137,7 +195192,8 @@ var PROBES = {
195137
195192
  request: (value2) => ({
195138
195193
  url: "https://api.anthropic.com/v1/models",
195139
195194
  headers: { "x-api-key": value2, "anthropic-version": "2023-06-01" }
195140
- })
195195
+ }),
195196
+ hostConfigurable: true
195141
195197
  },
195142
195198
  OPENROUTER_API_KEY: {
195143
195199
  request: (value2) => ({
@@ -195186,12 +195242,18 @@ function hasEnvVar4(name) {
195186
195242
  const value2 = process.env[name];
195187
195243
  return typeof value2 === "string" && value2.length > 0;
195188
195244
  }
195245
+ var HOST_OVERRIDES = {
195246
+ ANTHROPIC_API_KEY: "ANTHROPIC_BASE_URL",
195247
+ CLAUDE_CODE_OAUTH_TOKEN: "ANTHROPIC_BASE_URL"
195248
+ };
195189
195249
  function envVarsFor(model) {
195190
195250
  return model.includes("/") ? getModelEnvVars(model) : [];
195191
195251
  }
195192
195252
  async function checkOne(params) {
195193
195253
  const value2 = process.env[params.envVar];
195194
195254
  if (!value2) return null;
195255
+ const hostOverride = HOST_OVERRIDES[params.envVar];
195256
+ if (hostOverride && hasEnvVar4(hostOverride)) return null;
195195
195257
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") {
195196
195258
  const preflight = await preflightClaudeSubscription({
195197
195259
  token: value2,
@@ -198424,16 +198486,14 @@ ${instructions.user}` : null,
198424
198486
  const onInnerActivityTimeout = () => {
198425
198487
  if (innerTimeoutFired) return;
198426
198488
  innerTimeoutFired = true;
198427
- log2.info(
198428
- "\xBB inner activity timeout fired \u2014 stopping MCP server and starting 5min safety-net timer"
198429
- );
198430
- mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
198431
- log2.debug(
198432
- `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
198433
- );
198434
- });
198489
+ log2.info("\xBB inner activity timeout fired \u2014 starting 5min safety-net timer");
198435
198490
  safetyNetTimer = setTimeout(
198436
198491
  () => {
198492
+ mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
198493
+ log2.debug(
198494
+ `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
198495
+ );
198496
+ });
198437
198497
  activityTimeout?.forceReject(
198438
198498
  "agent still pending 5min after inner activity kill \u2014 forcing exit"
198439
198499
  );
@@ -198442,6 +198502,13 @@ ${instructions.user}` : null,
198442
198502
  );
198443
198503
  safetyNetTimer.unref?.();
198444
198504
  };
198505
+ const onTurnRecovered = () => {
198506
+ if (!innerTimeoutFired) return;
198507
+ innerTimeoutFired = false;
198508
+ if (safetyNetTimer) clearTimeout(safetyNetTimer);
198509
+ safetyNetTimer = void 0;
198510
+ log2.info("\xBB inner activity safety net stood down \u2014 turn recovered");
198511
+ };
198445
198512
  const agentPromise = agent2.run({
198446
198513
  payload,
198447
198514
  resolvedModel,
@@ -198462,6 +198529,7 @@ ${instructions.user}` : null,
198462
198529
  toolState,
198463
198530
  apiToken: runContext.apiToken,
198464
198531
  onActivityTimeout: onInnerActivityTimeout,
198532
+ onTurnRecovered,
198465
198533
  onToolUse: (event) => {
198466
198534
  const wasTracked = recordDiffReadFromToolUse({
198467
198535
  state: primaryRepoState(toolState).diffCoverage,
@@ -198735,34 +198803,6 @@ var PULLFROG_API_URL2 = (process.env.PULLFROG_API_URL || "https://pullfrog.com")
198735
198803
  function link(text, url4) {
198736
198804
  return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
198737
198805
  }
198738
- function buildProviders() {
198739
- return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
198740
- const aliases = modelAliases.filter(
198741
- (a2) => a2.provider === key && !a2.fallback && !a2.routing && !a2.hidden
198742
- );
198743
- const recommended = aliases.find((a2) => a2.preferred);
198744
- const sorted = [...aliases].sort((a2, b) => {
198745
- if (a2.preferred && !b.preferred) return -1;
198746
- if (!a2.preferred && b.preferred) return 1;
198747
- return 0;
198748
- });
198749
- return {
198750
- id: key,
198751
- name: config3.displayName,
198752
- envVars: config3.envVars,
198753
- models: sorted.map((a2) => ({
198754
- value: a2.slug,
198755
- label: a2.displayName,
198756
- hint: a2 === recommended ? "recommended" : void 0
198757
- }))
198758
- };
198759
- }).filter((p) => p.models.length > 0);
198760
- }
198761
- var CLI_PROVIDERS = buildProviders();
198762
- function resolveModelProvider(slug2) {
198763
- const providerId = slug2.split("/")[0];
198764
- return CLI_PROVIDERS.find((p) => p.id === providerId) ?? null;
198765
- }
198766
198806
  var activeSpin2 = null;
198767
198807
  function bail2(msg) {
198768
198808
  if (activeSpin2) {
@@ -198847,15 +198887,11 @@ function openBrowser(url4) {
198847
198887
  }
198848
198888
  }
198849
198889
  async function pullfrogApi2(ctx) {
198850
- const headers = { authorization: `Bearer ${ctx.token}` };
198851
- if (ctx.body) headers["content-type"] = "application/json";
198852
198890
  const controller = new AbortController();
198853
198891
  const timeout = setTimeout(() => controller.abort(), 3e4);
198854
198892
  try {
198855
198893
  const response = await fetch(`${PULLFROG_API_URL2}${ctx.path}`, {
198856
- method: ctx.method || "GET",
198857
- headers,
198858
- body: ctx.body ? JSON.stringify(ctx.body) : null,
198894
+ headers: { authorization: `Bearer ${ctx.token}` },
198859
198895
  signal: controller.signal
198860
198896
  });
198861
198897
  const data = await response.json().catch(() => ({}));
@@ -198883,406 +198919,27 @@ async function fetchStatus2(ctx) {
198883
198919
  isOrg: result.data.isOrg === true
198884
198920
  };
198885
198921
  }
198886
- bail2(errorMsg || `secrets check failed (${result.status})`);
198922
+ bail2(errorMsg || `installation check failed (${result.status})`);
198887
198923
  }
198888
198924
  return {
198889
198925
  installed: true,
198890
198926
  isOrg: result.data.isOrg === true,
198891
- installationId: typeof result.data.installationId === "number" ? result.data.installationId : null,
198892
- secretsAccessible: result.data.accessible !== false,
198893
- repoSecrets: result.data.repoSecrets || [],
198894
- orgSecrets: result.data.orgSecrets || [],
198895
- pullfrogSecrets: result.data.pullfrogSecrets || [],
198896
- model: result.data.repoModel ?? null,
198897
- hasRuns: result.data.hasRuns === true
198898
- };
198899
- }
198900
- async function createSession(ctx) {
198901
- try {
198902
- const result = await pullfrogApi2({
198903
- path: "/api/cli/session",
198904
- token: ctx.token,
198905
- method: "POST",
198906
- body: { owner: ctx.owner.toLowerCase(), repo: ctx.repo.toLowerCase() }
198907
- });
198908
- if (!result.ok || !result.data.id) return null;
198909
- return result.data.id;
198910
- } catch {
198911
- return null;
198912
- }
198913
- }
198914
- async function pollSession(ctx) {
198915
- const result = await pullfrogApi2({
198916
- path: `/api/cli/session/${ctx.sessionId}`,
198917
- token: ctx.token
198918
- });
198919
- if (result.status === 410) return "expired";
198920
- if (!result.ok) return "pending";
198921
- return result.data.installed === true ? "installed" : "pending";
198922
- }
198923
- function cleanupSession(ctx) {
198924
- void pullfrogApi2({
198925
- path: `/api/cli/session/${ctx.sessionId}`,
198926
- token: ctx.token,
198927
- method: "DELETE"
198928
- }).catch(() => {
198929
- });
198930
- }
198931
- var SESSION_POLL_MS = 750;
198932
- var FALLBACK_POLL_MS = 5e3;
198933
- var HINT_AFTER_MS = 1e4;
198934
- var TIMEOUT_MS = 3 * 60 * 1e3;
198935
- function listenForKey(key) {
198936
- let triggered = false;
198937
- const onData = (data) => {
198938
- if (data.toString().toLowerCase() === key) triggered = true;
198939
- };
198940
- process.stdin.setRawMode?.(true);
198941
- process.stdin.resume();
198942
- process.stdin.on("data", onData);
198943
- return {
198944
- consume() {
198945
- if (!triggered) return false;
198946
- triggered = false;
198947
- return true;
198948
- },
198949
- stop() {
198950
- process.stdin.removeListener("data", onData);
198951
- process.stdin.setRawMode?.(false);
198952
- process.stdin.pause();
198953
- }
198927
+ installationId: typeof result.data.installationId === "number" ? result.data.installationId : null
198954
198928
  };
198955
198929
  }
198956
198930
  function installationConfigUrl(ctx) {
198957
198931
  return ctx.isOrg ? `https://github.com/organizations/${ctx.owner}/settings/installations/${ctx.installationId}` : `https://github.com/settings/installations/${ctx.installationId}`;
198958
198932
  }
198959
- async function ensureInstallation(ctx) {
198960
- activeSpin2.start("checking pullfrog app installation");
198961
- const initial = await fetchStatus2(ctx);
198962
- if (initial.installed) {
198963
- activeSpin2.stop(`pullfrog app is installed on ${import_picocolors3.default.cyan(`@${ctx.owner}`)}`);
198964
- if (initial.installationId) {
198965
- const configUrl = installationConfigUrl({
198966
- owner: ctx.owner,
198967
- installationId: initial.installationId,
198968
- isOrg: initial.isOrg
198969
- });
198970
- process.stdout.write(`${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(configUrl), configUrl)}
198971
- `);
198972
- }
198973
- return initial;
198974
- }
198975
- const sessionId = await createSession(ctx);
198976
- if (initial.installationId) {
198977
- const repoRef = import_picocolors3.default.bold(`${ctx.owner}/${ctx.repo}`);
198978
- const configUrl = installationConfigUrl({
198979
- owner: ctx.owner,
198980
- installationId: initial.installationId,
198981
- isOrg: initial.isOrg
198982
- });
198983
- activeSpin2.stop(`pullfrog is installed on selected repos, but ${repoRef} is not included.`);
198984
- log.info(
198985
- `add it under "Repository access" on the installation config page.
198986
- ${import_picocolors3.default.dim(configUrl)}`
198987
- );
198988
- const openIt = await confirm({ message: "open browser?", active: "yes", inactive: "no" });
198989
- handleCancel2(openIt);
198990
- if (openIt) openBrowser(configUrl);
198991
- } else {
198992
- activeSpin2.stop("pullfrog app not installed");
198993
- const installUrl = `https://github.com/apps/${initial.appSlug}/installations/select_target?state=cli`;
198994
- log.info(`opening browser to install...
198995
- ${import_picocolors3.default.dim(installUrl)}`);
198996
- openBrowser(installUrl);
198997
- }
198998
- const isRepoAccessUpdate = !!initial.installationId;
198999
- const baseMsg = isRepoAccessUpdate ? "once you've added the repo, onboarding will proceed automatically" : "once you've installed the app, onboarding will proceed automatically";
199000
- activeSpin2.start(baseMsg);
199001
- let activeSessionId = sessionId;
199002
- let pollMs = activeSessionId ? SESSION_POLL_MS : FALLBACK_POLL_MS;
199003
- const listener = listenForKey("r");
199004
- const startedAt = Date.now();
199005
- let hintShown = false;
199006
- try {
199007
- while (Date.now() - startedAt < TIMEOUT_MS) {
199008
- await new Promise((r2) => setTimeout(r2, pollMs));
199009
- if (!hintShown && Date.now() - startedAt > HINT_AFTER_MS) {
199010
- activeSpin2.message(`${baseMsg} ${import_picocolors3.default.dim("(press r to recheck manually)")}`);
199011
- hintShown = true;
199012
- }
199013
- const doneMsg = isRepoAccessUpdate ? "repo access confirmed" : "pullfrog app installed";
199014
- if (listener.consume()) {
199015
- activeSpin2.message("rechecking via GitHub API");
199016
- try {
199017
- const status = await fetchStatus2(ctx);
199018
- if (status.installed) {
199019
- if (activeSessionId) cleanupSession({ token: ctx.token, sessionId: activeSessionId });
199020
- activeSpin2.stop(doneMsg);
199021
- return status;
199022
- }
199023
- } catch {
199024
- }
199025
- activeSpin2.message(`${baseMsg} ${import_picocolors3.default.dim("(press r to recheck manually)")}`);
199026
- continue;
199027
- }
199028
- if (activeSessionId) {
199029
- try {
199030
- const result = await pollSession({ token: ctx.token, sessionId: activeSessionId });
199031
- if (result === "expired") {
199032
- activeSessionId = null;
199033
- pollMs = FALLBACK_POLL_MS;
199034
- continue;
199035
- }
199036
- if (result === "installed") {
199037
- const status = await fetchStatus2(ctx);
199038
- if (status.installed) {
199039
- cleanupSession({ token: ctx.token, sessionId: activeSessionId });
199040
- activeSpin2.stop(doneMsg);
199041
- return status;
199042
- }
199043
- }
199044
- } catch {
199045
- }
199046
- } else {
199047
- try {
199048
- const status = await fetchStatus2(ctx);
199049
- if (status.installed) {
199050
- activeSpin2.stop(doneMsg);
199051
- return status;
199052
- }
199053
- } catch {
199054
- }
199055
- }
199056
- }
199057
- } finally {
199058
- listener.stop();
199059
- }
199060
- if (activeSessionId) cleanupSession({ token: ctx.token, sessionId: activeSessionId });
199061
- bail2(
199062
- isRepoAccessUpdate ? `timed out waiting for repo access.
199063
- ${import_picocolors3.default.dim("add the repo, then re-run:")} npx pullfrog init` : `timed out waiting for app installation.
199064
- ${import_picocolors3.default.dim("if your org requires admin approval, ask an admin to approve,")}
199065
- ${import_picocolors3.default.dim("then re-run:")} npx pullfrog init`
199066
- );
198933
+ function consoleUrl(ctx) {
198934
+ return `${PULLFROG_API_URL2}/console/${ctx.owner}?repo=${encodeURIComponent(ctx.repo)}`;
199067
198935
  }
199068
- function setGhSecret(ctx) {
199069
- let orgFailed = false;
199070
- if (ctx.org) {
199071
- try {
199072
- execFileSync8("gh", ["secret", "set", ctx.name, "--org", ctx.org, "--visibility", "all"], {
199073
- input: ctx.value,
199074
- stdio: ["pipe", "ignore", "pipe"],
199075
- encoding: "utf-8"
199076
- });
199077
- return { saved: true, orgFailed: false };
199078
- } catch {
199079
- orgFailed = true;
199080
- }
199081
- }
199082
- try {
199083
- execFileSync8("gh", ["secret", "set", ctx.name, "--repo", ctx.repoSlug], {
199084
- input: ctx.value,
199085
- stdio: ["pipe", "ignore", "pipe"],
199086
- encoding: "utf-8"
199087
- });
199088
- return { saved: true, orgFailed };
199089
- } catch {
199090
- return { saved: false, orgFailed };
199091
- }
198936
+ function installUrl(ctx) {
198937
+ const state = encodeURIComponent(`cli:${ctx.owner}/${ctx.repo}`);
198938
+ return `https://github.com/apps/${ctx.appSlug}/installations/select_target?state=${state}`;
199092
198939
  }
199093
- async function setPullfrogSecret2(ctx) {
199094
- const result = await pullfrogApi2({
199095
- path: "/api/cli/secrets",
199096
- token: ctx.token,
199097
- method: "POST",
199098
- body: {
199099
- owner: ctx.owner,
199100
- repo: ctx.repo,
199101
- name: ctx.name,
199102
- value: ctx.value,
199103
- scope: ctx.scope
199104
- }
199105
- });
199106
- if (result.ok && result.data.success === true) {
199107
- return { saved: true, error: "" };
199108
- }
199109
- return { saved: false, error: result.data.error || `api returned ${result.status}` };
199110
- }
199111
- async function promptScope2(ctx) {
199112
- const scope2 = await select({
199113
- message: "secret scope",
199114
- options: [
199115
- { value: "account", label: `${ctx.owner} organization`, hint: "shared across repos" },
199116
- { value: "repo", label: `${ctx.owner}/${ctx.repo} only` }
199117
- ]
199118
- });
199119
- handleCancel2(scope2);
199120
- return scope2;
199121
- }
199122
- async function handleSecret(ctx) {
199123
- const repoSecretsUrl = `https://github.com/${ctx.owner}/${ctx.repo}/settings/secrets/actions`;
199124
- const matches = [];
199125
- for (const v of ctx.provider.envVars) {
199126
- if (ctx.secrets.pullfrogSecrets.includes(v)) matches.push({ name: v, source: "pullfrog" });
199127
- else if (ctx.secrets.secretsAccessible && ctx.secrets.orgSecrets.includes(v))
199128
- matches.push({ name: v, source: "org secret" });
199129
- else if (ctx.secrets.secretsAccessible && ctx.secrets.repoSecrets.includes(v))
199130
- matches.push({ name: v, source: "repo secret" });
199131
- }
199132
- if (matches.length > 0) {
199133
- activeSpin2.start("");
199134
- activeSpin2.stop("secrets already configured");
199135
- for (const m of matches) {
199136
- process.stdout.write(
199137
- `${import_picocolors3.default.gray(S_BAR)} ${import_picocolors3.default.cyan(m.name)} ${import_picocolors3.default.dim(`(${m.source})`)}
199138
- `
199139
- );
199140
- }
199141
- return;
199142
- }
199143
- if (!ctx.secrets.secretsAccessible) {
199144
- log.info(`could not verify GitHub secrets (app lacks permission)`);
199145
- }
199146
- const hasOAuthOption = ctx.provider.envVars.includes("CLAUDE_CODE_OAUTH_TOKEN");
199147
- let envVar = ctx.provider.envVars[0];
199148
- if (hasOAuthOption) {
199149
- const authMethod = await select({
199150
- message: "which credential do you want to use?",
199151
- options: [
199152
- {
199153
- value: "oauth",
199154
- label: "Claude Code OAuth token",
199155
- hint: `run ${import_picocolors3.default.cyan("claude setup-token")} \u2014 works with Pro/Max subscriptions`
199156
- },
199157
- {
199158
- value: "api",
199159
- label: "Anthropic API key",
199160
- hint: "from console.anthropic.com"
199161
- }
199162
- ]
199163
- });
199164
- handleCancel2(authMethod);
199165
- if (authMethod === "oauth") envVar = "CLAUDE_CODE_OAUTH_TOKEN";
199166
- }
199167
- const method = await select({
199168
- message: `where should ${import_picocolors3.default.cyan(envVar)} be stored?`,
199169
- options: [
199170
- {
199171
- value: "pullfrog",
199172
- label: "Pullfrog",
199173
- hint: "recommended \u2014 auto-injected, no workflow changes"
199174
- },
199175
- {
199176
- value: "github",
199177
- label: "GitHub Actions secret",
199178
- hint: "requires env block in pullfrog.yml"
199179
- }
199180
- ]
199181
- });
199182
- handleCancel2(method);
199183
- const pasteLabel = envVar === "CLAUDE_CODE_OAUTH_TOKEN" ? "OAuth token" : `${ctx.provider.name} API key`;
199184
- const apiKey = await password({
199185
- message: `paste your ${pasteLabel} ${import_picocolors3.default.dim("(Enter to skip)")}`,
199186
- mask: "*",
199187
- validate: () => void 0
199188
- });
199189
- handleCancel2(apiKey);
199190
- if (!apiKey) {
199191
- log.info(
199192
- `skipped \u2014 set it manually at:
199193
- ${import_picocolors3.default.dim(method === "pullfrog" ? `${PULLFROG_API_URL2}/console/${ctx.owner}` : repoSecretsUrl)}`
199194
- );
199195
- return;
199196
- }
199197
- if (method === "pullfrog") {
199198
- const scope2 = ctx.secrets.isOrg ? await promptScope2(ctx) : "account";
199199
- const target = describeSecretTarget({ owner: ctx.owner, repo: ctx.repo, scope: scope2 });
199200
- activeSpin2.start(`saving ${import_picocolors3.default.cyan(envVar)} to ${target}`);
199201
- let saveResult;
199202
- try {
199203
- saveResult = await setPullfrogSecret2({
199204
- token: ctx.token,
199205
- owner: ctx.owner,
199206
- repo: ctx.repo,
199207
- name: envVar,
199208
- value: apiKey,
199209
- scope: scope2
199210
- });
199211
- } catch (error52) {
199212
- activeSpin2.stop(import_picocolors3.default.red("could not save secret"));
199213
- log.warn(
199214
- `${error52 instanceof Error ? error52.message : "network error"}
199215
- set it manually at: ${import_picocolors3.default.dim(`${PULLFROG_API_URL2}/console/${ctx.owner}`)}`
199216
- );
199217
- return;
199218
- }
199219
- if (saveResult.saved) {
199220
- activeSpin2.stop(`saved ${import_picocolors3.default.cyan(envVar)} to ${target}`);
199221
- } else {
199222
- activeSpin2.stop(import_picocolors3.default.red("could not save secret"));
199223
- log.warn(
199224
- `${saveResult.error}
199225
- set it manually at: ${import_picocolors3.default.dim(`${PULLFROG_API_URL2}/console/${ctx.owner}`)}`
199226
- );
199227
- }
199228
- return;
199229
- }
199230
- let org = null;
199231
- if (ctx.secrets.isOrg) {
199232
- const scope2 = await promptScope2(ctx);
199233
- org = scope2 === "account" ? ctx.owner : null;
199234
- }
199235
- const secretsUrl = org ? `https://github.com/organizations/${org}/settings/secrets/actions` : repoSecretsUrl;
199236
- activeSpin2.start(`saving ${envVar}`);
199237
- const secretResult = setGhSecret({
199238
- name: envVar,
199239
- value: apiKey,
199240
- org,
199241
- repoSlug: `${ctx.owner}/${ctx.repo}`
199242
- });
199243
- if (secretResult.saved) {
199244
- activeSpin2.stop(
199245
- `saved ${import_picocolors3.default.cyan(envVar)} to ${org && !secretResult.orgFailed ? `${import_picocolors3.default.dim(ctx.owner)} org secret` : "GitHub Actions secret"}`
199246
- );
199247
- if (secretResult.orgFailed) {
199248
- log.warn("org secret failed (admin access required) \u2014 saved as repo secret instead");
199249
- }
199250
- } else {
199251
- activeSpin2.stop(import_picocolors3.default.red("could not set secret"));
199252
- log.warn(`set it manually at:
199253
- ${import_picocolors3.default.dim(secretsUrl)}`);
199254
- }
199255
- }
199256
- async function promptTestRun(ctx) {
199257
- const proceed = await select({
199258
- message: "test your installation?",
199259
- options: [
199260
- { value: true, label: "yes", hint: "dispatches a test run in your GitHub Actions" },
199261
- { value: false, label: "skip" }
199262
- ]
199263
- });
199264
- handleCancel2(proceed);
199265
- if (!proceed) return;
199266
- activeSpin2.start("dispatching test run");
199267
- const result = await pullfrogApi2({
199268
- path: "/api/cli/dispatch",
199269
- token: ctx.token,
199270
- method: "POST",
199271
- body: { owner: ctx.owner, repo: ctx.repo, prompt: "Tell me a joke" }
199272
- });
199273
- if (!result.ok) {
199274
- activeSpin2.stop(import_picocolors3.default.red("could not dispatch"));
199275
- log.warn(result.data.error || `dispatch failed (${result.status})`);
199276
- return;
199277
- }
199278
- activeSpin2.stop("dispatched test run");
199279
- if (result.data.url) {
199280
- process.stdout.write(
199281
- `${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(result.data.url), result.data.url)}
199282
- `
199283
- );
199284
- openBrowser(result.data.url);
199285
- }
198940
+ function printLink(url4) {
198941
+ process.stdout.write(`${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(url4), url4)}
198942
+ `);
199286
198943
  }
199287
198944
  async function main2() {
199288
198945
  intro(import_picocolors3.default.bgGreen(import_picocolors3.default.black(" pullfrog ")));
@@ -199304,98 +198961,58 @@ async function main2() {
199304
198961
  spin.start("detecting repository");
199305
198962
  const remote = parseGitRemote2();
199306
198963
  spin.stop(`detected repo ${import_picocolors3.default.cyan(`${remote.owner}/${remote.repo}`)}`);
199307
- const secrets = await ensureInstallation({ token, owner: remote.owner, repo: remote.repo });
199308
- let model;
199309
- let provider2;
199310
- if (secrets.model) {
199311
- model = secrets.model;
199312
- const resolved = resolveModelProvider(secrets.model);
199313
- if (!resolved) bail2(`unknown model provider: ${secrets.model}`);
199314
- provider2 = resolved;
199315
- const displayAlias = resolveDisplayAlias(secrets.model);
199316
- const label = displayAlias ? displayAlias.displayName : secrets.model;
198964
+ spin.start("checking pullfrog app installation");
198965
+ const status = await fetchStatus2({ token, owner: remote.owner, repo: remote.repo });
198966
+ const handoff = consoleUrl({ owner: remote.owner, repo: remote.repo });
198967
+ if (status.installed) {
198968
+ spin.stop(`pullfrog app is installed on ${import_picocolors3.default.cyan(`@${remote.owner}`)}`);
199317
198969
  spin.start("");
199318
- spin.stop(`using model ${import_picocolors3.default.cyan(label)}`);
199319
- } else {
199320
- const providerId = await select({
199321
- message: "select your preferred model provider",
199322
- options: CLI_PROVIDERS.map((cp) => ({
199323
- value: cp.id,
199324
- label: cp.name
199325
- }))
199326
- });
199327
- handleCancel2(providerId);
199328
- const found = CLI_PROVIDERS.find((cp) => cp.id === providerId);
199329
- if (!found) bail2(`unknown provider: ${providerId}`);
199330
- provider2 = found;
199331
- if (provider2.models.length === 1) {
199332
- model = provider2.models[0].value;
199333
- spin.start("");
199334
- spin.stop(`using ${import_picocolors3.default.bold(provider2.models[0].label)}`);
199335
- } else {
199336
- const recommendedModel = provider2.models.find((m) => m.hint === "recommended");
199337
- const options = provider2.models.map((m) => {
199338
- if (m.hint) return { value: m.value, label: m.label, hint: m.hint };
199339
- return { value: m.value, label: m.label };
199340
- });
199341
- const selected = await select(
199342
- recommendedModel ? { message: "select model", initialValue: recommendedModel.value, options } : { message: "select model", options }
199343
- );
199344
- handleCancel2(selected);
199345
- model = selected;
199346
- }
199347
- }
199348
- await handleSecret({ token, owner: remote.owner, repo: remote.repo, provider: provider2, secrets });
199349
- spin.start("creating pullfrog.yml workflow");
199350
- const result = await pullfrogApi2({
199351
- path: "/api/cli/setup",
199352
- token,
199353
- method: "POST",
199354
- body: { owner: remote.owner, repo: remote.repo, model }
199355
- });
199356
- if (!result.ok) {
199357
- bail2(result.data.error || `api returned ${result.status}`);
199358
- }
199359
- let skipTestRun = false;
199360
- if (result.data.already_existed) {
199361
- spin.stop("pullfrog.yml already exists");
199362
- } else if (result.data.pull_request_url) {
199363
- spin.stop("opened pull request with pullfrog.yml");
199364
- process.stdout.write(
199365
- `${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(result.data.pull_request_url), result.data.pull_request_url)}
199366
- `
198970
+ spin.stop("opening your dashboard");
198971
+ printLink(handoff);
198972
+ openBrowser(handoff);
198973
+ activeSpin2 = null;
198974
+ outro(
198975
+ `finish setup in your browser \u2014 ${import_picocolors3.default.cyan(`${remote.owner}/${remote.repo}`)} is ready.`
199367
198976
  );
199368
- openBrowser(result.data.pull_request_url);
199369
- const merged = await select({
199370
- message: "merge the PR to activate pullfrog, then continue",
199371
- options: [
199372
- { value: true, label: "continue", hint: "PR has been merged" },
199373
- { value: false, label: "skip" }
199374
- ]
198977
+ return;
198978
+ }
198979
+ if (status.installationId) {
198980
+ const repoRef = import_picocolors3.default.bold(`${remote.owner}/${remote.repo}`);
198981
+ const configUrl = installationConfigUrl({
198982
+ owner: remote.owner,
198983
+ installationId: status.installationId,
198984
+ isOrg: status.isOrg
199375
198985
  });
199376
- handleCancel2(merged);
199377
- if (!merged) skipTestRun = true;
199378
- } else {
199379
- const short = result.data.hash?.slice(0, 7);
199380
- spin.stop(
199381
- short ? `committed pullfrog.yml to repo ${import_picocolors3.default.dim(short)}` : "committed pullfrog.yml to repo"
198986
+ spin.stop(`pullfrog is installed on selected repos, but ${repoRef} is not included.`);
198987
+ log.info(
198988
+ `add it under "Repository access" on the installation config page.
198989
+ ${import_picocolors3.default.dim(configUrl)}`
199382
198990
  );
198991
+ const openIt = await confirm({ message: "open browser?", active: "yes", inactive: "no" });
198992
+ handleCancel2(openIt);
198993
+ if (openIt) openBrowser(configUrl);
198994
+ log.info("once the repo is added, finish setup at:");
198995
+ printLink(handoff);
198996
+ activeSpin2 = null;
198997
+ outro("done.");
198998
+ return;
199383
198999
  }
199384
- if (!skipTestRun && !secrets.hasRuns) {
199385
- await promptTestRun({ token, owner: remote.owner, repo: remote.repo });
199386
- }
199387
- const consoleUrl = `${PULLFROG_API_URL2}/console/${remote.owner}/${remote.repo}`;
199388
- spin.start("");
199389
- spin.stop("repo is configurable via the Pullfrog dashboard");
199390
- process.stdout.write(`${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(consoleUrl), consoleUrl)}
199391
- `);
199000
+ spin.stop("pullfrog app not installed");
199001
+ const install = installUrl({
199002
+ appSlug: status.appSlug,
199003
+ owner: remote.owner,
199004
+ repo: remote.repo
199005
+ });
199006
+ log.info("opening browser to install...");
199007
+ printLink(install);
199008
+ openBrowser(install);
199392
199009
  activeSpin2 = null;
199393
- outro("done.");
199010
+ outro("GitHub will drop you at your dashboard to finish setup.");
199394
199011
  }
199395
199012
  function printInitUsage(params) {
199396
199013
  params.stream(`usage: ${params.prog} init
199397
199014
  `);
199398
- params.stream("set up pullfrog on the current repository.");
199015
+ params.stream("install pullfrog on the current repository and open its dashboard.");
199399
199016
  params.stream("");
199400
199017
  params.stream("options:");
199401
199018
  params.stream(" -h, --help show help");
@@ -199567,7 +199184,7 @@ async function runCli4(input) {
199567
199184
  }
199568
199185
 
199569
199186
  // cli.ts
199570
- var VERSION10 = "0.1.59";
199187
+ var VERSION10 = "0.1.60";
199571
199188
  var bin = basename2(process.argv[1] || "");
199572
199189
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
199573
199190
  var rawArgs = process.argv.slice(2);
@@ -199575,7 +199192,7 @@ function printMainUsage(stream) {
199575
199192
  stream(`usage: ${PROG} <command>
199576
199193
  `);
199577
199194
  stream("commands:");
199578
- stream(" init set up pullfrog on the current repository");
199195
+ stream(" init install pullfrog on the current repository and open its dashboard");
199579
199196
  stream(" auth manage provider credentials for the current repository");
199580
199197
  stream(" watch stream a PR's activity as one JSON line per event");
199581
199198
  stream("");
package/dist/index.js CHANGED
@@ -104420,6 +104420,11 @@ var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
104420
104420
  var AGENT_ACTIVITY_TIMEOUT_MS = 9e5;
104421
104421
  var AGENT_FIRST_EVENT_TIMEOUT_MS = 12e4;
104422
104422
  var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
104423
+ function watchdogBudgetMs(compiled, envVar) {
104424
+ const raw2 = Number(process.env[envVar]);
104425
+ if (!Number.isFinite(raw2) || raw2 <= 0) return compiled;
104426
+ return Math.min(compiled, raw2);
104427
+ }
104423
104428
  var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
104424
104429
  var ACTIVITY_NOISE_PATTERNS = [
104425
104430
  new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
@@ -105183,7 +105188,7 @@ var import_semver = __toESM(require_semver2(), 1);
105183
105188
  // package.json
105184
105189
  var package_default = {
105185
105190
  name: "pullfrog",
105186
- version: "0.1.59",
105191
+ version: "0.1.60",
105187
105192
  type: "module",
105188
105193
  bin: {
105189
105194
  pullfrog: "dist/cli.mjs",
@@ -107292,7 +107297,7 @@ var claude = agent({
107292
107297
  if ((isBedrockRoute || isVertexRoute2) && specifier && effort.alias?.effort) {
107293
107298
  applyHostedEffortCapabilities({ env: env2, modelId: specifier, levels: effort.alias.effort });
107294
107299
  }
107295
- if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && env2.ANTHROPIC_API_KEY) {
107300
+ if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && !isVertexRoute2 && !env2.ANTHROPIC_BASE_URL && !env2.ANTHROPIC_AUTH_TOKEN && env2.ANTHROPIC_API_KEY) {
107296
107301
  const preflight = await preflightClaudeSubscription({
107297
107302
  token: env2.CLAUDE_CODE_OAUTH_TOKEN,
107298
107303
  model
@@ -114157,6 +114162,7 @@ function processTerminalToolPart(ctx, part, label, isOrchestrator) {
114157
114162
  const callLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
114158
114163
  log.info(withLabel(label, callLine));
114159
114164
  if (isOrchestrator) ctx.loggedToolCallIDs.add(toolId);
114165
+ if (isOrchestrator && toolName.startsWith("pullfrog_")) ctx.mcpToolCalls++;
114160
114166
  if (state.status === "completed") {
114161
114167
  log.debug(withLabel(label, ` output: ${state.output}`));
114162
114168
  } else {
@@ -114395,21 +114401,28 @@ function formatConfigError(name, data) {
114395
114401
  ).join("");
114396
114402
  return `${name}${detail}${bullets}`;
114397
114403
  }
114404
+ var WATCHDOG_SALVAGE_PROMPT = "Your previous turn was cut off mid-response by a provider stall. Nothing you had not already submitted through a tool was saved. Continue from where you stopped and submit your work now \u2014 do not restart your analysis.";
114398
114405
  function startInnerActivityWatchdog(params) {
114399
114406
  let fired = false;
114407
+ let everFired = false;
114400
114408
  const id = setInterval(() => {
114401
114409
  if (fired) return;
114402
114410
  const idleMs = performance7.now() - params.ctx.lastEventAt;
114403
- const budgetMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
114411
+ const compiledMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
114412
+ const budgetMs = everFired ? compiledMs : watchdogBudgetMs(
114413
+ compiledMs,
114414
+ params.ctx.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
114415
+ );
114404
114416
  if (idleMs <= budgetMs) return;
114405
114417
  fired = true;
114418
+ everFired = true;
114406
114419
  const idleSec = Math.round(idleMs / 1e3);
114407
114420
  params.ctx.diagnostic.idleSec = idleSec;
114408
114421
  params.ctx.diagnostic.sawModelOutput = params.ctx.sawModelOutput;
114409
114422
  log.info(
114410
114423
  params.ctx.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
114411
114424
  );
114412
- params.abortController.abort();
114425
+ params.abortTurn();
114413
114426
  try {
114414
114427
  params.ctx.onActivityTimeout?.();
114415
114428
  } catch (err) {
@@ -114419,7 +114432,22 @@ function startInnerActivityWatchdog(params) {
114419
114432
  }
114420
114433
  }, 5e3);
114421
114434
  id.unref?.();
114422
- return { stop: () => clearInterval(id) };
114435
+ return {
114436
+ stop: () => clearInterval(id),
114437
+ /**
114438
+ * Start a turn's idle budget from now. The clock must be reset with the
114439
+ * latch: `lastEventAt` only advances on model output, so a turn following a
114440
+ * stall would inherit the full stalled interval and be aborted on the very
114441
+ * next 5s tick — killing the salvage before the provider could answer. It
114442
+ * is the right semantics for an ordinary resume too, whose budget should
114443
+ * not be pre-spent by the post-run gate checks that ran between turns.
114444
+ */
114445
+ armForTurn: () => {
114446
+ fired = false;
114447
+ params.ctx.lastEventAt = performance7.now();
114448
+ },
114449
+ firedThisTurn: () => fired
114450
+ };
114423
114451
  }
114424
114452
  var opencode = agent({
114425
114453
  name: "opencode",
@@ -114531,6 +114559,7 @@ var opencode = agent({
114531
114559
  variant: effort.rung,
114532
114560
  todoTracker: ctx.todoTracker,
114533
114561
  onActivityTimeout: ctx.onActivityTimeout,
114562
+ onTurnRecovered: ctx.onTurnRecovered,
114534
114563
  onToolUse: ctx.onToolUse,
114535
114564
  currentTurn: null,
114536
114565
  eventCount: 0,
@@ -114539,6 +114568,7 @@ var opencode = agent({
114539
114568
  promptMessageID: void 0,
114540
114569
  taskDispatchByCallID: /* @__PURE__ */ new Map(),
114541
114570
  loggedToolCallIDs: /* @__PURE__ */ new Set(),
114571
+ mcpToolCalls: 0,
114542
114572
  recentStderr: server.recentStderr,
114543
114573
  diagnostic: {
114544
114574
  label: "Pullfrog",
@@ -114562,9 +114592,14 @@ var opencode = agent({
114562
114592
  }
114563
114593
  }
114564
114594
  });
114565
- const abortController = new AbortController();
114566
- const eventLoopPromise = consumeEvents(runnerCtx, abortController.signal).catch((err) => {
114567
- if (!abortController.signal.aborted) {
114595
+ const runController = new AbortController();
114596
+ let turnController = new AbortController();
114597
+ const nextTurnSignal = () => {
114598
+ turnController = new AbortController();
114599
+ return AbortSignal.any([runController.signal, turnController.signal]);
114600
+ };
114601
+ const eventLoopPromise = consumeEvents(runnerCtx, runController.signal).catch((err) => {
114602
+ if (!runController.signal.aborted) {
114568
114603
  log.warning(
114569
114604
  `\xBB opencode event subscription ended: ${err instanceof Error ? err.message : String(err)}`
114570
114605
  );
@@ -114580,31 +114615,48 @@ var opencode = agent({
114580
114615
  // a long synchronous tool call (no part.updated while it runs) can't
114581
114616
  // false-positive it.
114582
114617
  timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
114583
- abortController
114618
+ abortTurn: () => turnController.abort()
114584
114619
  });
114585
114620
  const sdkModel = parseModel2(model);
114621
+ let salvagesLeft = 1;
114622
+ const runTurn = async (text) => {
114623
+ const attempt = (prompt) => {
114624
+ watchdog.armForTurn();
114625
+ return runTurnGuarded(
114626
+ runnerCtx,
114627
+ () => runPromptTurn(runnerCtx, { text: prompt, model: sdkModel, signal: nextTurnSignal() })
114628
+ );
114629
+ };
114630
+ const standDownIfRecovered = (turn) => {
114631
+ if (watchdog.firedThisTurn() && turn.success) ctx.onTurnRecovered?.();
114632
+ return turn;
114633
+ };
114634
+ const result = await attempt(text);
114635
+ if (!watchdog.firedThisTurn()) return result;
114636
+ if (result.success) return standDownIfRecovered(result);
114637
+ if (salvagesLeft <= 0) return result;
114638
+ salvagesLeft--;
114639
+ ctx.onTurnRecovered?.();
114640
+ log.info("\xBB activity watchdog cut the turn off \u2014 re-prompting once on the same session");
114641
+ const mcpCallsBefore = runnerCtx.mcpToolCalls;
114642
+ const salvaged = await attempt(WATCHDOG_SALVAGE_PROMPT);
114643
+ const usage = mergeAgentUsage(result.usage, salvaged.usage);
114644
+ if (runnerCtx.mcpToolCalls === mcpCallsBefore) {
114645
+ log.info(
114646
+ "\xBB salvage turn reached no Pullfrog tool \u2014 keeping the activity-timeout failure"
114647
+ );
114648
+ return { ...result, usage };
114649
+ }
114650
+ return standDownIfRecovered({ ...salvaged, usage });
114651
+ };
114586
114652
  try {
114587
- const initial = await runTurnGuarded(
114588
- runnerCtx,
114589
- () => runPromptTurn(runnerCtx, {
114590
- text: ctx.instructions.full,
114591
- model: sdkModel,
114592
- signal: abortController.signal
114593
- })
114594
- );
114653
+ const initial = await runTurn(ctx.instructions.full);
114595
114654
  const result = await runPostRunRetryLoop({
114596
114655
  ctx,
114597
114656
  initialResult: initial,
114598
114657
  initialUsage: initial.usage,
114599
114658
  reflectionPrompt: buildReflectionPrompt(ctx.toolState),
114600
- resume: async (c) => runTurnGuarded(
114601
- runnerCtx,
114602
- () => runPromptTurn(runnerCtx, {
114603
- text: c.prompt,
114604
- model: sdkModel,
114605
- signal: abortController.signal
114606
- })
114607
- )
114659
+ resume: async (c) => runTurn(c.prompt)
114608
114660
  });
114609
114661
  if (result.success) {
114610
114662
  await ctx.todoTracker?.flush();
@@ -114614,7 +114666,7 @@ var opencode = agent({
114614
114666
  return result;
114615
114667
  } finally {
114616
114668
  watchdog.stop();
114617
- abortController.abort();
114669
+ runController.abort();
114618
114670
  await eventLoopPromise.catch(() => {
114619
114671
  });
114620
114672
  }
@@ -192405,7 +192457,7 @@ function hasEnvVar2(name) {
192405
192457
  return typeof val === "string" && val.length > 0;
192406
192458
  }
192407
192459
  function hasClaudeCodeAuth() {
192408
- return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY");
192460
+ return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY") || hasEnvVar2("ANTHROPIC_AUTH_TOKEN");
192409
192461
  }
192410
192462
  function hasCodexAuth() {
192411
192463
  return hasEnvVar2("CODEX_AUTH_JSON") || hasEnvVar2("OPENAI_API_KEY");
@@ -192498,8 +192550,11 @@ function resolveAgent(ctx) {
192498
192550
  } catch {
192499
192551
  }
192500
192552
  }
192501
- if (!ctx.model && ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) {
192502
- return agents.codex;
192553
+ if (!ctx.model) {
192554
+ if (hasEnvVar2("ANTHROPIC_AUTH_TOKEN") && !hasEnvVar2("ANTHROPIC_API_KEY") && !hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN")) {
192555
+ return agents.claude;
192556
+ }
192557
+ if (ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) return agents.codex;
192503
192558
  }
192504
192559
  return agents.opencode;
192505
192560
  }
@@ -192685,7 +192740,7 @@ function hasSingleProviderAuth(agentName) {
192685
192740
  if (agentName === "codex") {
192686
192741
  return hasEnvVar3("OPENAI_API_KEY") || hasEnvVar3("CODEX_AUTH_JSON");
192687
192742
  }
192688
- return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
192743
+ return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("ANTHROPIC_AUTH_TOKEN") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
192689
192744
  }
192690
192745
  function validateAgentApiKey(params) {
192691
192746
  if (params.model) {
@@ -193017,7 +193072,8 @@ var PROBES = {
193017
193072
  request: (value2) => ({
193018
193073
  url: "https://api.anthropic.com/v1/models",
193019
193074
  headers: { "x-api-key": value2, "anthropic-version": "2023-06-01" }
193020
- })
193075
+ }),
193076
+ hostConfigurable: true
193021
193077
  },
193022
193078
  OPENROUTER_API_KEY: {
193023
193079
  request: (value2) => ({
@@ -193066,12 +193122,18 @@ function hasEnvVar4(name) {
193066
193122
  const value2 = process.env[name];
193067
193123
  return typeof value2 === "string" && value2.length > 0;
193068
193124
  }
193125
+ var HOST_OVERRIDES = {
193126
+ ANTHROPIC_API_KEY: "ANTHROPIC_BASE_URL",
193127
+ CLAUDE_CODE_OAUTH_TOKEN: "ANTHROPIC_BASE_URL"
193128
+ };
193069
193129
  function envVarsFor(model) {
193070
193130
  return model.includes("/") ? getModelEnvVars(model) : [];
193071
193131
  }
193072
193132
  async function checkOne(params) {
193073
193133
  const value2 = process.env[params.envVar];
193074
193134
  if (!value2) return null;
193135
+ const hostOverride = HOST_OVERRIDES[params.envVar];
193136
+ if (hostOverride && hasEnvVar4(hostOverride)) return null;
193075
193137
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") {
193076
193138
  const preflight = await preflightClaudeSubscription({
193077
193139
  token: value2,
@@ -196304,16 +196366,14 @@ ${instructions.user}` : null,
196304
196366
  const onInnerActivityTimeout = () => {
196305
196367
  if (innerTimeoutFired) return;
196306
196368
  innerTimeoutFired = true;
196307
- log.info(
196308
- "\xBB inner activity timeout fired \u2014 stopping MCP server and starting 5min safety-net timer"
196309
- );
196310
- mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
196311
- log.debug(
196312
- `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
196313
- );
196314
- });
196369
+ log.info("\xBB inner activity timeout fired \u2014 starting 5min safety-net timer");
196315
196370
  safetyNetTimer = setTimeout(
196316
196371
  () => {
196372
+ mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
196373
+ log.debug(
196374
+ `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
196375
+ );
196376
+ });
196317
196377
  activityTimeout?.forceReject(
196318
196378
  "agent still pending 5min after inner activity kill \u2014 forcing exit"
196319
196379
  );
@@ -196322,6 +196382,13 @@ ${instructions.user}` : null,
196322
196382
  );
196323
196383
  safetyNetTimer.unref?.();
196324
196384
  };
196385
+ const onTurnRecovered = () => {
196386
+ if (!innerTimeoutFired) return;
196387
+ innerTimeoutFired = false;
196388
+ if (safetyNetTimer) clearTimeout(safetyNetTimer);
196389
+ safetyNetTimer = void 0;
196390
+ log.info("\xBB inner activity safety net stood down \u2014 turn recovered");
196391
+ };
196325
196392
  const agentPromise = agent2.run({
196326
196393
  payload,
196327
196394
  resolvedModel,
@@ -196342,6 +196409,7 @@ ${instructions.user}` : null,
196342
196409
  toolState,
196343
196410
  apiToken: runContext.apiToken,
196344
196411
  onActivityTimeout: onInnerActivityTimeout,
196412
+ onTurnRecovered,
196345
196413
  onToolUse: (event) => {
196346
196414
  const wasTracked = recordDiffReadFromToolUse({
196347
196415
  state: primaryRepoState(toolState).diffCoverage,
package/dist/internal.js CHANGED
@@ -777,9 +777,16 @@ function getModelManagedCredentials(slug) {
777
777
  const providerConfig = providers[parsed.provider];
778
778
  return providerConfig?.managedCredentials?.slice() ?? [];
779
779
  }
780
+ var HARNESS_ONLY_CREDENTIALS = {
781
+ anthropic: ["ANTHROPIC_AUTH_TOKEN"]
782
+ };
780
783
  function modelHasStoredAuth(params) {
781
784
  const slug = resolveDisplayAlias(params.model)?.slug ?? params.model;
782
- const authVars = [...getModelEnvVars(slug), ...getModelManagedCredentials(slug)];
785
+ const authVars = [
786
+ ...getModelEnvVars(slug),
787
+ ...getModelManagedCredentials(slug),
788
+ ...HARNESS_ONLY_CREDENTIALS[getModelProvider(slug)] ?? []
789
+ ];
783
790
  return authVars.some((v) => params.secretNames.includes(v));
784
791
  }
785
792
  var modelAliases = Object.entries(providers).flatMap(
@@ -1542,7 +1549,8 @@ var PROBES = {
1542
1549
  request: (value) => ({
1543
1550
  url: "https://api.anthropic.com/v1/models",
1544
1551
  headers: { "x-api-key": value, "anthropic-version": "2023-06-01" }
1545
- })
1552
+ }),
1553
+ hostConfigurable: true
1546
1554
  },
1547
1555
  OPENROUTER_API_KEY: {
1548
1556
  request: (value) => ({
@@ -1565,7 +1573,9 @@ async function verifyClaudeSubscription(value) {
1565
1573
  return preflight.status === 401 ? "dead" : "alive";
1566
1574
  }
1567
1575
  function isCredentialProbeable(envVar) {
1568
- return envVar === "CLAUDE_CODE_OAUTH_TOKEN" || envVar in PROBES;
1576
+ if (envVar === "CLAUDE_CODE_OAUTH_TOKEN") return true;
1577
+ const probe = PROBES[envVar];
1578
+ return probe !== void 0 && !probe.hostConfigurable;
1569
1579
  }
1570
1580
  async function verifyCredential(params) {
1571
1581
  const value = params.value.trim();
@@ -50,6 +50,18 @@ export declare const AGENT_ACTIVITY_TIMEOUT_MS = 900000;
50
50
  */
51
51
  export declare const AGENT_FIRST_EVENT_TIMEOUT_MS = 120000;
52
52
  export declare const DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5000;
53
+ /**
54
+ * E2E affordance: shorten a watchdog budget for one dispatch, so the stall path
55
+ * can be exercised on demand. A real provider stall is not provocable, which is
56
+ * how #1085 shipped three escalations deep with its salvage branch never once
57
+ * run end to end.
58
+ *
59
+ * **Shorten-only, deliberately.** The value is clamped to the compiled budget,
60
+ * so an override can make the watchdog stricter but can never relax or disable
61
+ * it. That keeps this unable to manufacture the zombie run the watchdog exists
62
+ * to prevent, even if a customer sets it in their own workflow env.
63
+ */
64
+ export declare function watchdogBudgetMs(compiled: number, envVar: string): number;
53
65
  export declare const ACTIVITY_NOISE_PATTERNS: readonly RegExp[];
54
66
  export declare function isActivityNoise(chunk: string | Uint8Array): boolean;
55
67
  type ActivityTimeoutContext = {
@@ -7,7 +7,13 @@
7
7
  * provider outage rewrite a working account's configuration.
8
8
  */
9
9
  export type CredentialVerdict = "alive" | "dead" | "unknown";
10
- /** whether asking the provider about this credential would tell us anything. */
10
+ /**
11
+ * Whether asking the provider about this credential would tell us anything —
12
+ * the paste-time gate, where the caller stores a secret and cannot see the run
13
+ * env. A `hostConfigurable` credential is excluded here and stays probeable at
14
+ * run time, because only the runner knows whether a gateway is in play: the
15
+ * base URL usually lives in the workflow file, which the console never reads.
16
+ */
11
17
  export declare function isCredentialProbeable(envVar: string): boolean;
12
18
  /**
13
19
  * Ask the provider whether it still accepts this credential. Used at the two
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",