pullfrog 0.1.59 → 0.1.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.61",
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
@@ -109476,6 +109481,19 @@ var claude = agent({
109476
109481
  delete env2.CLAUDE_CODE_OAUTH_TOKEN;
109477
109482
  }
109478
109483
  }
109484
+ if (env2.CLAUDE_CODE_USE_BEDROCK) {
109485
+ ctx.toolState.credential = "bedrock";
109486
+ } else if (env2.CLAUDE_CODE_USE_VERTEX) {
109487
+ ctx.toolState.credential = "vertex";
109488
+ } else if (env2.CLAUDE_CODE_USE_FOUNDRY) {
109489
+ ctx.toolState.credential = "foundry";
109490
+ } else if (env2.ANTHROPIC_AUTH_TOKEN) {
109491
+ ctx.toolState.credential = "gateway";
109492
+ } else if (env2.ANTHROPIC_API_KEY) {
109493
+ ctx.toolState.credential = "api_key";
109494
+ } else if (env2.CLAUDE_CODE_OAUTH_TOKEN) {
109495
+ ctx.toolState.credential = "subscription";
109496
+ }
109479
109497
  const effortEnvOverride = env2[CLAUDE_EFFORT_ENV]?.trim();
109480
109498
  if (effortEnvOverride) {
109481
109499
  log2.warning(
@@ -109938,6 +109956,18 @@ function toTodoWriteInput(item) {
109938
109956
  }))
109939
109957
  };
109940
109958
  }
109959
+ var CODEX_MODEL_PRICING = {
109960
+ "gpt-5.6-sol": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, output: 30 },
109961
+ "gpt-5.6-luna": { input: 0.2, cacheRead: 0.02, cacheWrite: 0.25, output: 1.2 },
109962
+ "gpt-5.6-terra": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 12 }
109963
+ };
109964
+ function codexCostUsd(params) {
109965
+ const price = params.model ? CODEX_MODEL_PRICING[params.model] : void 0;
109966
+ if (!price) return void 0;
109967
+ const fresh = Math.max(0, params.input - params.cacheRead - params.cacheWrite);
109968
+ const usd = (fresh * price.input + params.cacheRead * price.cacheRead + params.cacheWrite * price.cacheWrite + params.output * price.output) / 1e6;
109969
+ return usd > 0 ? usd : void 0;
109970
+ }
109941
109971
  async function runCodex2(params) {
109942
109972
  const startTime = performance6.now();
109943
109973
  const thinkingTimer = new ThinkingTimer();
@@ -109949,14 +109979,15 @@ async function runCodex2(params) {
109949
109979
  let turnError = null;
109950
109980
  let lastProviderError = null;
109951
109981
  let stdoutBuffer = "";
109952
- const tokens = { input: 0, cacheRead: 0, output: 0 };
109982
+ const tokens = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
109953
109983
  function buildUsage2() {
109954
109984
  if (tokens.input === 0 && tokens.output === 0) return void 0;
109955
109985
  return {
109956
109986
  agent: "codex",
109957
109987
  inputTokens: tokens.input,
109958
109988
  outputTokens: tokens.output,
109959
- cacheReadTokens: tokens.cacheRead || void 0
109989
+ cacheReadTokens: tokens.cacheRead || void 0,
109990
+ costUsd: codexCostUsd({ model: params.model, ...tokens })
109960
109991
  };
109961
109992
  }
109962
109993
  function onItem(item, phase) {
@@ -110021,6 +110052,7 @@ async function runCodex2(params) {
110021
110052
  case "turn.completed":
110022
110053
  tokens.input += event.usage.input_tokens ?? 0;
110023
110054
  tokens.cacheRead += event.usage.cached_input_tokens ?? 0;
110055
+ tokens.cacheWrite += event.usage.cache_write_input_tokens ?? 0;
110024
110056
  tokens.output += event.usage.output_tokens ?? 0;
110025
110057
  return;
110026
110058
  case "turn.failed":
@@ -110181,6 +110213,11 @@ var codex = agent({
110181
110213
  } else if (process.env.OPENAI_API_KEY) {
110182
110214
  env2.CODEX_API_KEY = process.env.OPENAI_API_KEY;
110183
110215
  }
110216
+ if (codexHomeAuth) {
110217
+ ctx.toolState.credential = "subscription";
110218
+ } else if (env2.CODEX_API_KEY) {
110219
+ ctx.toolState.credential = "api_key";
110220
+ }
110184
110221
  const securityFlags = securityOverrideFlags({
110185
110222
  ctx,
110186
110223
  sandboxMode: ctx.payload.push === "disabled" ? "read-only" : "workspace-write",
@@ -110189,7 +110226,13 @@ var codex = agent({
110189
110226
  model: resolveCodexModel(ctx),
110190
110227
  effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0
110191
110228
  });
110192
- const runnerArgs = { cliPath, cwd: process.cwd(), env: env2, todoTracker: ctx.todoTracker };
110229
+ const runnerArgs = {
110230
+ cliPath,
110231
+ cwd: process.cwd(),
110232
+ env: env2,
110233
+ todoTracker: ctx.todoTracker,
110234
+ model: resolveCodexModel(ctx)
110235
+ };
110193
110236
  const initial = await runCodex2({
110194
110237
  ...runnerArgs,
110195
110238
  args: [...securityFlags, "exec", "--json", ctx.instructions.full],
@@ -116277,6 +116320,7 @@ function processTerminalToolPart(ctx, part, label, isOrchestrator) {
116277
116320
  const callLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
116278
116321
  log2.info(withLabel(label, callLine));
116279
116322
  if (isOrchestrator) ctx.loggedToolCallIDs.add(toolId);
116323
+ if (isOrchestrator && toolName.startsWith("pullfrog_")) ctx.mcpToolCalls++;
116280
116324
  if (state.status === "completed") {
116281
116325
  log2.debug(withLabel(label, ` output: ${state.output}`));
116282
116326
  } else {
@@ -116515,21 +116559,28 @@ function formatConfigError(name, data) {
116515
116559
  ).join("");
116516
116560
  return `${name}${detail}${bullets}`;
116517
116561
  }
116562
+ 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
116563
  function startInnerActivityWatchdog(params) {
116519
116564
  let fired = false;
116565
+ let everFired = false;
116520
116566
  const id = setInterval(() => {
116521
116567
  if (fired) return;
116522
116568
  const idleMs = performance7.now() - params.ctx.lastEventAt;
116523
- const budgetMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116569
+ const compiledMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116570
+ const budgetMs = everFired ? compiledMs : watchdogBudgetMs(
116571
+ compiledMs,
116572
+ params.ctx.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
116573
+ );
116524
116574
  if (idleMs <= budgetMs) return;
116525
116575
  fired = true;
116576
+ everFired = true;
116526
116577
  const idleSec = Math.round(idleMs / 1e3);
116527
116578
  params.ctx.diagnostic.idleSec = idleSec;
116528
116579
  params.ctx.diagnostic.sawModelOutput = params.ctx.sawModelOutput;
116529
116580
  log2.info(
116530
116581
  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
116582
  );
116532
- params.abortController.abort();
116583
+ params.abortTurn();
116533
116584
  try {
116534
116585
  params.ctx.onActivityTimeout?.();
116535
116586
  } catch (err) {
@@ -116539,7 +116590,22 @@ function startInnerActivityWatchdog(params) {
116539
116590
  }
116540
116591
  }, 5e3);
116541
116592
  id.unref?.();
116542
- return { stop: () => clearInterval(id) };
116593
+ return {
116594
+ stop: () => clearInterval(id),
116595
+ /**
116596
+ * Start a turn's idle budget from now. The clock must be reset with the
116597
+ * latch: `lastEventAt` only advances on model output, so a turn following a
116598
+ * stall would inherit the full stalled interval and be aborted on the very
116599
+ * next 5s tick — killing the salvage before the provider could answer. It
116600
+ * is the right semantics for an ordinary resume too, whose budget should
116601
+ * not be pre-spent by the post-run gate checks that ran between turns.
116602
+ */
116603
+ armForTurn: () => {
116604
+ fired = false;
116605
+ params.ctx.lastEventAt = performance7.now();
116606
+ },
116607
+ firedThisTurn: () => fired
116608
+ };
116543
116609
  }
116544
116610
  var opencode = agent({
116545
116611
  name: "opencode",
@@ -116651,6 +116717,7 @@ var opencode = agent({
116651
116717
  variant: effort.rung,
116652
116718
  todoTracker: ctx.todoTracker,
116653
116719
  onActivityTimeout: ctx.onActivityTimeout,
116720
+ onTurnRecovered: ctx.onTurnRecovered,
116654
116721
  onToolUse: ctx.onToolUse,
116655
116722
  currentTurn: null,
116656
116723
  eventCount: 0,
@@ -116659,6 +116726,7 @@ var opencode = agent({
116659
116726
  promptMessageID: void 0,
116660
116727
  taskDispatchByCallID: /* @__PURE__ */ new Map(),
116661
116728
  loggedToolCallIDs: /* @__PURE__ */ new Set(),
116729
+ mcpToolCalls: 0,
116662
116730
  recentStderr: server.recentStderr,
116663
116731
  diagnostic: {
116664
116732
  label: "Pullfrog",
@@ -116682,9 +116750,14 @@ var opencode = agent({
116682
116750
  }
116683
116751
  }
116684
116752
  });
116685
- const abortController = new AbortController();
116686
- const eventLoopPromise = consumeEvents(runnerCtx, abortController.signal).catch((err) => {
116687
- if (!abortController.signal.aborted) {
116753
+ const runController = new AbortController();
116754
+ let turnController = new AbortController();
116755
+ const nextTurnSignal = () => {
116756
+ turnController = new AbortController();
116757
+ return AbortSignal.any([runController.signal, turnController.signal]);
116758
+ };
116759
+ const eventLoopPromise = consumeEvents(runnerCtx, runController.signal).catch((err) => {
116760
+ if (!runController.signal.aborted) {
116688
116761
  log2.warning(
116689
116762
  `\xBB opencode event subscription ended: ${err instanceof Error ? err.message : String(err)}`
116690
116763
  );
@@ -116700,31 +116773,48 @@ var opencode = agent({
116700
116773
  // a long synchronous tool call (no part.updated while it runs) can't
116701
116774
  // false-positive it.
116702
116775
  timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
116703
- abortController
116776
+ abortTurn: () => turnController.abort()
116704
116777
  });
116705
116778
  const sdkModel = parseModel2(model);
116779
+ let salvagesLeft = 1;
116780
+ const runTurn = async (text) => {
116781
+ const attempt = (prompt) => {
116782
+ watchdog.armForTurn();
116783
+ return runTurnGuarded(
116784
+ runnerCtx,
116785
+ () => runPromptTurn(runnerCtx, { text: prompt, model: sdkModel, signal: nextTurnSignal() })
116786
+ );
116787
+ };
116788
+ const standDownIfRecovered = (turn) => {
116789
+ if (watchdog.firedThisTurn() && turn.success) ctx.onTurnRecovered?.();
116790
+ return turn;
116791
+ };
116792
+ const result = await attempt(text);
116793
+ if (!watchdog.firedThisTurn()) return result;
116794
+ if (result.success) return standDownIfRecovered(result);
116795
+ if (salvagesLeft <= 0) return result;
116796
+ salvagesLeft--;
116797
+ ctx.onTurnRecovered?.();
116798
+ log2.info("\xBB activity watchdog cut the turn off \u2014 re-prompting once on the same session");
116799
+ const mcpCallsBefore = runnerCtx.mcpToolCalls;
116800
+ const salvaged = await attempt(WATCHDOG_SALVAGE_PROMPT);
116801
+ const usage = mergeAgentUsage(result.usage, salvaged.usage);
116802
+ if (runnerCtx.mcpToolCalls === mcpCallsBefore) {
116803
+ log2.info(
116804
+ "\xBB salvage turn reached no Pullfrog tool \u2014 keeping the activity-timeout failure"
116805
+ );
116806
+ return { ...result, usage };
116807
+ }
116808
+ return standDownIfRecovered({ ...salvaged, usage });
116809
+ };
116706
116810
  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
- );
116811
+ const initial = await runTurn(ctx.instructions.full);
116715
116812
  const result = await runPostRunRetryLoop({
116716
116813
  ctx,
116717
116814
  initialResult: initial,
116718
116815
  initialUsage: initial.usage,
116719
116816
  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
- )
116817
+ resume: async (c2) => runTurn(c2.prompt)
116728
116818
  });
116729
116819
  if (result.success) {
116730
116820
  await ctx.todoTracker?.flush();
@@ -116734,7 +116824,7 @@ var opencode = agent({
116734
116824
  return result;
116735
116825
  } finally {
116736
116826
  watchdog.stop();
116737
- abortController.abort();
116827
+ runController.abort();
116738
116828
  await eventLoopPromise.catch(() => {
116739
116829
  });
116740
116830
  }
@@ -189364,7 +189454,9 @@ var STRING_KEYS = [
189364
189454
  "reviewNodeId",
189365
189455
  "planCommentNodeId",
189366
189456
  "summarySnapshot",
189367
- "model"
189457
+ "model",
189458
+ "agent",
189459
+ "credential"
189368
189460
  ];
189369
189461
  var NUMBER_KEYS = [
189370
189462
  "inputTokens",
@@ -194525,7 +194617,7 @@ function hasEnvVar2(name) {
194525
194617
  return typeof val === "string" && val.length > 0;
194526
194618
  }
194527
194619
  function hasClaudeCodeAuth() {
194528
- return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY");
194620
+ return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY") || hasEnvVar2("ANTHROPIC_AUTH_TOKEN");
194529
194621
  }
194530
194622
  function hasCodexAuth() {
194531
194623
  return hasEnvVar2("CODEX_AUTH_JSON") || hasEnvVar2("OPENAI_API_KEY");
@@ -194618,8 +194710,11 @@ function resolveAgent(ctx) {
194618
194710
  } catch {
194619
194711
  }
194620
194712
  }
194621
- if (!ctx.model && ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) {
194622
- return agents.codex;
194713
+ if (!ctx.model) {
194714
+ if (hasEnvVar2("ANTHROPIC_AUTH_TOKEN") && !hasEnvVar2("ANTHROPIC_API_KEY") && !hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN")) {
194715
+ return agents.claude;
194716
+ }
194717
+ if (ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) return agents.codex;
194623
194718
  }
194624
194719
  return agents.opencode;
194625
194720
  }
@@ -194805,7 +194900,7 @@ function hasSingleProviderAuth(agentName) {
194805
194900
  if (agentName === "codex") {
194806
194901
  return hasEnvVar3("OPENAI_API_KEY") || hasEnvVar3("CODEX_AUTH_JSON");
194807
194902
  }
194808
- return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
194903
+ return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("ANTHROPIC_AUTH_TOKEN") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
194809
194904
  }
194810
194905
  function validateAgentApiKey(params) {
194811
194906
  if (params.model) {
@@ -195137,7 +195232,8 @@ var PROBES = {
195137
195232
  request: (value2) => ({
195138
195233
  url: "https://api.anthropic.com/v1/models",
195139
195234
  headers: { "x-api-key": value2, "anthropic-version": "2023-06-01" }
195140
- })
195235
+ }),
195236
+ hostConfigurable: true
195141
195237
  },
195142
195238
  OPENROUTER_API_KEY: {
195143
195239
  request: (value2) => ({
@@ -195186,12 +195282,18 @@ function hasEnvVar4(name) {
195186
195282
  const value2 = process.env[name];
195187
195283
  return typeof value2 === "string" && value2.length > 0;
195188
195284
  }
195285
+ var HOST_OVERRIDES = {
195286
+ ANTHROPIC_API_KEY: "ANTHROPIC_BASE_URL",
195287
+ CLAUDE_CODE_OAUTH_TOKEN: "ANTHROPIC_BASE_URL"
195288
+ };
195189
195289
  function envVarsFor(model) {
195190
195290
  return model.includes("/") ? getModelEnvVars(model) : [];
195191
195291
  }
195192
195292
  async function checkOne(params) {
195193
195293
  const value2 = process.env[params.envVar];
195194
195294
  if (!value2) return null;
195295
+ const hostOverride = HOST_OVERRIDES[params.envVar];
195296
+ if (hostOverride && hasEnvVar4(hostOverride)) return null;
195195
195297
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") {
195196
195298
  const preflight = await preflightClaudeSubscription({
195197
195299
  token: value2,
@@ -196133,7 +196235,10 @@ var JsonPayload = type({
196133
196235
  // optional so a payload from an older server build (pre-`checkRun`) still parses
196134
196236
  // against a newer action across a rolling deploy.
196135
196237
  "checkRun?": type({ id: "string" }).or("undefined"),
196136
- "generateSummary?": "boolean | undefined"
196238
+ "generateSummary?": "boolean | undefined",
196239
+ // optional so a payload from a pre-canary server build still parses against a
196240
+ // newer action across a rolling deploy.
196241
+ "codexArm?": "boolean | undefined"
196137
196242
  });
196138
196243
  var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
196139
196244
  function isCollaborator(event) {
@@ -196262,6 +196367,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
196262
196367
  progressComment: jsonPayload?.progressComment,
196263
196368
  checkRun: jsonPayload?.checkRun,
196264
196369
  generateSummary: jsonPayload?.generateSummary,
196370
+ codexArm: jsonPayload?.codexArm,
196265
196371
  // permissions: inputs > repoSettings > fallbacks
196266
196372
  push: inputs.push ?? repoSettings.push ?? "restricted",
196267
196373
  shell: resolvedShell,
@@ -198206,8 +198312,12 @@ async function main() {
198206
198312
  const agent2 = resolveAgent({
198207
198313
  model: resolvedModel,
198208
198314
  proxyModel: payload.proxyModel,
198209
- codexAgent: runContext.repoSettings.codexAgent
198315
+ // the account opt-in and the canary arm are both admissions to codex, so
198316
+ // they OR: an account that opted in explicitly always gets it, and the
198317
+ // canary widens the pool without ever demoting a run that already had it.
198318
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
198210
198319
  });
198320
+ toolState.agent = agent2.name;
198211
198321
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
198212
198322
  toolState.model = effectiveModel;
198213
198323
  if (!payload.proxyModel) {
@@ -198424,16 +198534,14 @@ ${instructions.user}` : null,
198424
198534
  const onInnerActivityTimeout = () => {
198425
198535
  if (innerTimeoutFired) return;
198426
198536
  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
- });
198537
+ log2.info("\xBB inner activity timeout fired \u2014 starting 5min safety-net timer");
198435
198538
  safetyNetTimer = setTimeout(
198436
198539
  () => {
198540
+ mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
198541
+ log2.debug(
198542
+ `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
198543
+ );
198544
+ });
198437
198545
  activityTimeout?.forceReject(
198438
198546
  "agent still pending 5min after inner activity kill \u2014 forcing exit"
198439
198547
  );
@@ -198442,6 +198550,13 @@ ${instructions.user}` : null,
198442
198550
  );
198443
198551
  safetyNetTimer.unref?.();
198444
198552
  };
198553
+ const onTurnRecovered = () => {
198554
+ if (!innerTimeoutFired) return;
198555
+ innerTimeoutFired = false;
198556
+ if (safetyNetTimer) clearTimeout(safetyNetTimer);
198557
+ safetyNetTimer = void 0;
198558
+ log2.info("\xBB inner activity safety net stood down \u2014 turn recovered");
198559
+ };
198445
198560
  const agentPromise = agent2.run({
198446
198561
  payload,
198447
198562
  resolvedModel,
@@ -198462,6 +198577,7 @@ ${instructions.user}` : null,
198462
198577
  toolState,
198463
198578
  apiToken: runContext.apiToken,
198464
198579
  onActivityTimeout: onInnerActivityTimeout,
198580
+ onTurnRecovered,
198465
198581
  onToolUse: (event) => {
198466
198582
  const wasTracked = recordDiffReadFromToolUse({
198467
198583
  state: primaryRepoState(toolState).diffCoverage,
@@ -198559,6 +198675,8 @@ ${instructions.user}` : null,
198559
198675
  if (toolContext) {
198560
198676
  const patch = aggregateUsage(toolState.usageEntries);
198561
198677
  if (toolState.model) patch.model = toolState.model;
198678
+ if (toolState.agent) patch.agent = toolState.agent;
198679
+ if (toolState.credential) patch.credential = toolState.credential;
198562
198680
  if (Object.keys(patch).length > 0) {
198563
198681
  await patchWorkflowRunFields(toolContext, patch);
198564
198682
  }
@@ -198735,34 +198853,6 @@ var PULLFROG_API_URL2 = (process.env.PULLFROG_API_URL || "https://pullfrog.com")
198735
198853
  function link(text, url4) {
198736
198854
  return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
198737
198855
  }
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
198856
  var activeSpin2 = null;
198767
198857
  function bail2(msg) {
198768
198858
  if (activeSpin2) {
@@ -198847,15 +198937,11 @@ function openBrowser(url4) {
198847
198937
  }
198848
198938
  }
198849
198939
  async function pullfrogApi2(ctx) {
198850
- const headers = { authorization: `Bearer ${ctx.token}` };
198851
- if (ctx.body) headers["content-type"] = "application/json";
198852
198940
  const controller = new AbortController();
198853
198941
  const timeout = setTimeout(() => controller.abort(), 3e4);
198854
198942
  try {
198855
198943
  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,
198944
+ headers: { authorization: `Bearer ${ctx.token}` },
198859
198945
  signal: controller.signal
198860
198946
  });
198861
198947
  const data = await response.json().catch(() => ({}));
@@ -198883,406 +198969,27 @@ async function fetchStatus2(ctx) {
198883
198969
  isOrg: result.data.isOrg === true
198884
198970
  };
198885
198971
  }
198886
- bail2(errorMsg || `secrets check failed (${result.status})`);
198972
+ bail2(errorMsg || `installation check failed (${result.status})`);
198887
198973
  }
198888
198974
  return {
198889
198975
  installed: true,
198890
198976
  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
- }
198977
+ installationId: typeof result.data.installationId === "number" ? result.data.installationId : null
198954
198978
  };
198955
198979
  }
198956
198980
  function installationConfigUrl(ctx) {
198957
198981
  return ctx.isOrg ? `https://github.com/organizations/${ctx.owner}/settings/installations/${ctx.installationId}` : `https://github.com/settings/installations/${ctx.installationId}`;
198958
198982
  }
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
- );
198983
+ function consoleUrl(ctx) {
198984
+ return `${PULLFROG_API_URL2}/console/${ctx.owner}?repo=${encodeURIComponent(ctx.repo)}`;
199067
198985
  }
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
- }
199092
- }
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
- }
198986
+ function installUrl(ctx) {
198987
+ const state = encodeURIComponent(`cli:${ctx.owner}/${ctx.repo}`);
198988
+ return `https://github.com/apps/${ctx.appSlug}/installations/select_target?state=${state}`;
199255
198989
  }
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
- }
198990
+ function printLink(url4) {
198991
+ process.stdout.write(`${import_picocolors3.default.gray(S_BAR)} ${link(import_picocolors3.default.dim(url4), url4)}
198992
+ `);
199286
198993
  }
199287
198994
  async function main2() {
199288
198995
  intro(import_picocolors3.default.bgGreen(import_picocolors3.default.black(" pullfrog ")));
@@ -199304,98 +199011,58 @@ async function main2() {
199304
199011
  spin.start("detecting repository");
199305
199012
  const remote = parseGitRemote2();
199306
199013
  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;
199014
+ spin.start("checking pullfrog app installation");
199015
+ const status = await fetchStatus2({ token, owner: remote.owner, repo: remote.repo });
199016
+ const handoff = consoleUrl({ owner: remote.owner, repo: remote.repo });
199017
+ if (status.installed) {
199018
+ spin.stop(`pullfrog app is installed on ${import_picocolors3.default.cyan(`@${remote.owner}`)}`);
199317
199019
  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
- `
199020
+ spin.stop("opening your dashboard");
199021
+ printLink(handoff);
199022
+ openBrowser(handoff);
199023
+ activeSpin2 = null;
199024
+ outro(
199025
+ `finish setup in your browser \u2014 ${import_picocolors3.default.cyan(`${remote.owner}/${remote.repo}`)} is ready.`
199367
199026
  );
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
- ]
199027
+ return;
199028
+ }
199029
+ if (status.installationId) {
199030
+ const repoRef = import_picocolors3.default.bold(`${remote.owner}/${remote.repo}`);
199031
+ const configUrl = installationConfigUrl({
199032
+ owner: remote.owner,
199033
+ installationId: status.installationId,
199034
+ isOrg: status.isOrg
199375
199035
  });
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"
199036
+ spin.stop(`pullfrog is installed on selected repos, but ${repoRef} is not included.`);
199037
+ log.info(
199038
+ `add it under "Repository access" on the installation config page.
199039
+ ${import_picocolors3.default.dim(configUrl)}`
199382
199040
  );
199041
+ const openIt = await confirm({ message: "open browser?", active: "yes", inactive: "no" });
199042
+ handleCancel2(openIt);
199043
+ if (openIt) openBrowser(configUrl);
199044
+ log.info("once the repo is added, finish setup at:");
199045
+ printLink(handoff);
199046
+ activeSpin2 = null;
199047
+ outro("done.");
199048
+ return;
199383
199049
  }
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
- `);
199050
+ spin.stop("pullfrog app not installed");
199051
+ const install = installUrl({
199052
+ appSlug: status.appSlug,
199053
+ owner: remote.owner,
199054
+ repo: remote.repo
199055
+ });
199056
+ log.info("opening browser to install...");
199057
+ printLink(install);
199058
+ openBrowser(install);
199392
199059
  activeSpin2 = null;
199393
- outro("done.");
199060
+ outro("GitHub will drop you at your dashboard to finish setup.");
199394
199061
  }
199395
199062
  function printInitUsage(params) {
199396
199063
  params.stream(`usage: ${params.prog} init
199397
199064
  `);
199398
- params.stream("set up pullfrog on the current repository.");
199065
+ params.stream("install pullfrog on the current repository and open its dashboard.");
199399
199066
  params.stream("");
199400
199067
  params.stream("options:");
199401
199068
  params.stream(" -h, --help show help");
@@ -199567,7 +199234,7 @@ async function runCli4(input) {
199567
199234
  }
199568
199235
 
199569
199236
  // cli.ts
199570
- var VERSION10 = "0.1.59";
199237
+ var VERSION10 = "0.1.61";
199571
199238
  var bin = basename2(process.argv[1] || "");
199572
199239
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
199573
199240
  var rawArgs = process.argv.slice(2);
@@ -199575,7 +199242,7 @@ function printMainUsage(stream) {
199575
199242
  stream(`usage: ${PROG} <command>
199576
199243
  `);
199577
199244
  stream("commands:");
199578
- stream(" init set up pullfrog on the current repository");
199245
+ stream(" init install pullfrog on the current repository and open its dashboard");
199579
199246
  stream(" auth manage provider credentials for the current repository");
199580
199247
  stream(" watch stream a PR's activity as one JSON line per event");
199581
199248
  stream("");