pullfrog 0.1.14 → 0.1.16

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.
@@ -3,11 +3,13 @@
3
3
  * Re-exports shared types, values, and utilities needed by the Next.js app.
4
4
  */
5
5
  export type { AuthorPermission, ModelAlias, ModelProvider, Payload, PayloadEvent, ProviderConfig, PushPermission, ShellPermission, ToolPermission, WriteablePayload, } from "../external.ts";
6
- export { DEFAULT_PROXY_MODEL, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, modelAliases, parseModel, providers, pullfrogMcpName, resolveCliModel, resolveDisplayAlias, resolveModelSlug, resolveOpenRouterModel, } from "../external.ts";
6
+ export { DEFAULT_PROXY_MODEL, getAutoSelectHintModel, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, modelAliases, parseModel, providers, pullfrogMcpName, resolveCliModel, resolveDisplayAlias, resolveModelSlug, resolveOpenRouterModel, } from "../external.ts";
7
7
  export type { Mode } from "../modes.ts";
8
8
  export { modes } from "../modes.ts";
9
9
  export type { BuildPullfrogFooterParams, WorkflowRunFooterInfo, } from "../utils/buildPullfrogFooter.ts";
10
10
  export { buildPullfrogFooter, PULLFROG_DIVIDER, stripExistingFooter, } from "../utils/buildPullfrogFooter.ts";
11
+ export type { CodexAuthBody } from "../utils/codexOAuth.ts";
12
+ export { decodeJwtExpMs, OAuthInvalidGrantError, parseCodexAuthBody, refreshCodexAuthBody, stringifyCodexAuthBody, } from "../utils/codexOAuth.ts";
11
13
  export type { ResourceUsage, UsageSummary } from "../utils/github.ts";
12
14
  export { isLeapingIntoActionCommentBody, LEAPING_INTO_ACTION_PREFIX, } from "../utils/leapingComment.ts";
13
15
  export { MAX_LEARNINGS_LENGTH, truncateAtLineBoundary } from "../utils/learningsTruncate.ts";
package/dist/internal.js CHANGED
@@ -11,8 +11,8 @@ var providers = {
11
11
  models: {
12
12
  "claude-opus": {
13
13
  displayName: "Claude Opus",
14
- resolve: "anthropic/claude-opus-4-7",
15
- openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
14
+ resolve: "anthropic/claude-opus-4-8",
15
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
16
16
  preferred: true,
17
17
  subagentModel: "claude-sonnet"
18
18
  },
@@ -101,7 +101,7 @@ var providers = {
101
101
  "gemini-flash": {
102
102
  displayName: "Gemini Flash",
103
103
  resolve: "google/gemini-3.5-flash",
104
- openRouterResolve: "openrouter/google/gemini-3-flash-preview"
104
+ openRouterResolve: "openrouter/google/gemini-3.5-flash"
105
105
  }
106
106
  }
107
107
  }),
@@ -190,8 +190,8 @@ var providers = {
190
190
  },
191
191
  "claude-opus": {
192
192
  displayName: "Claude Opus",
193
- resolve: "opencode/claude-opus-4-7",
194
- openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
193
+ resolve: "opencode/claude-opus-4-8",
194
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
195
195
  subagentModel: "claude-sonnet"
196
196
  },
197
197
  "claude-sonnet": {
@@ -249,8 +249,8 @@ var providers = {
249
249
  },
250
250
  "gemini-flash": {
251
251
  displayName: "Gemini Flash",
252
- resolve: "opencode/gemini-3-flash",
253
- openRouterResolve: "openrouter/google/gemini-3-flash-preview"
252
+ resolve: "opencode/gemini-3.5-flash",
253
+ openRouterResolve: "openrouter/google/gemini-3.5-flash"
254
254
  },
255
255
  "kimi-k2": {
256
256
  displayName: "Kimi K2",
@@ -323,8 +323,8 @@ var providers = {
323
323
  models: {
324
324
  "claude-opus": {
325
325
  displayName: "Claude Opus",
326
- resolve: "openrouter/anthropic/claude-opus-4.7",
327
- openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
326
+ resolve: "openrouter/anthropic/claude-opus-4.8",
327
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.8",
328
328
  preferred: true,
329
329
  subagentModel: "claude-sonnet"
330
330
  },
@@ -388,8 +388,8 @@ var providers = {
388
388
  },
389
389
  "gemini-flash": {
390
390
  displayName: "Gemini Flash",
391
- resolve: "openrouter/google/gemini-3-flash-preview",
392
- openRouterResolve: "openrouter/google/gemini-3-flash-preview"
391
+ resolve: "openrouter/google/gemini-3.5-flash",
392
+ openRouterResolve: "openrouter/google/gemini-3.5-flash"
393
393
  },
394
394
  grok: {
395
395
  displayName: "Grok",
@@ -481,6 +481,16 @@ if (!defaultProxyAlias?.openRouterResolve) {
481
481
  throw new Error("DEFAULT_PROXY_MODEL: moonshotai/kimi-k2 missing openRouterResolve");
482
482
  }
483
483
  var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
484
+ function getAutoSelectHintModel() {
485
+ const alias = defaultProxyAlias;
486
+ if (!alias) return "Kimi 2.6";
487
+ const modelId = alias.resolve.split("/")[1] ?? "kimi-k2.6";
488
+ const version = modelId.replace(/^kimi-k2\./, "");
489
+ if (version && version !== modelId) {
490
+ return `Kimi 2.${version}`;
491
+ }
492
+ return alias.displayName;
493
+ }
484
494
  function resolveModelSlug(slug) {
485
495
  return modelAliases.find((a) => a.slug === slug)?.resolve;
486
496
  }
@@ -704,6 +714,8 @@ function computeModes(agentId) {
704
714
 
705
715
  Otherwise delegate the \`${REVIEWER_AGENT_NAME}\` subagent to review your diff with fresh eyes against YOUR TASK. The subagent's baked-in system prompt enforces a non-mutative + non-recursive contract: read-only file/search/web tools and read-only MCP queries only; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch. Enforcement is prose-only \u2014 restate the constraint in your dispatch instructions and do not relax it.
706
716
 
717
+ Before dispatching, ensure \`origin/<base>\` is locally available \u2014 the runner is often a shallow single-branch \`actions/checkout\` (depth=1, head-only refspec), and the reviewer's \`git diff --merge-base origin/<base>\` will fail with \`ambiguous argument\` or \`no merge base\` otherwise. Run \`git fetch --no-tags --deepen=1000 origin <base>:refs/remotes/origin/<base>\` once (the explicit destination refspec is required \u2014 a shallow single-branch checkout configures a head-only refspec, so a bare \`origin <base>\` only updates \`FETCH_HEAD\` and never creates the \`origin/<base>\` tracking ref); it's a no-op if the ref already has enough history. (The reviewer is read-only by contract, so it cannot do this itself \u2014 fetching is the orchestrator's job.)
718
+
707
719
  Compose your \`${REVIEWER_AGENT_NAME}\` dispatch prompt using this template verbatim, substituting the \`<...>\` placeholders. The preamble aligns the orchestrator side of the dispatch contract with the reviewer's baked-in system prompt \u2014 both ends say the same thing about where the work lives and what to do on an empty diff.
708
720
 
709
721
  \`\`\`
@@ -711,9 +723,11 @@ function computeModes(agentId) {
711
723
  This is a PRE-COMMIT Build-mode self-review. The work to review lives in the working tree (uncommitted), NOT in committed history.
712
724
 
713
725
  Branch: <branch> (off <base>)
714
- Canonical diff command: git diff origin/<base>
726
+ Canonical diff command: git diff --merge-base origin/<base>
727
+
728
+ Use \`--merge-base\` (single MCP \`git\` call, no shell substitution required). NOT bare \`git diff origin/<base>\` or two-dot \`git diff origin/<base>..HEAD\` \u2014 the symmetric forms include the inverse of every commit landed on \`<base>\` since this branch forked, which is noise (and the git tool will reject those forms when the divergence is detected). \`origin/<base>...HEAD\` (three-dot) and \`--cached\` both miss the uncommitted edits self-review runs on, so they're also wrong here.
715
729
 
716
- If that command returns empty, treat it as "no changes \u2014 nothing to review" and stop per your system prompt. Do not search for the work elsewhere.
730
+ If the merge-base diff returns empty, treat it as "no changes \u2014 nothing to review" and stop per your system prompt. Do not search for the work elsewhere.
717
731
 
718
732
  ## Your task
719
733
  <YOUR TASK content>
@@ -722,7 +736,7 @@ function computeModes(agentId) {
722
736
  <tight summary \u2014 what broke, root cause, the fix \u2014 or "no build-phase failures">
723
737
  \`\`\`
724
738
 
725
- Follow the template with the diff content (\`git diff origin/<base-branch>\`, single-rev form \u2014 \`main...HEAD\` and \`--cached\` both miss the uncommitted edits self-review runs on) and your task brief. Instruct the subagent to flag bugs, logic errors, missing edge cases, gaps between request and diff, and unintended changes.
739
+ Follow the template with the diff content (\`git diff --merge-base origin/<base-branch>\` \u2014 single MCP \`git\` call, captures committed + staged + unstaged, excludes base-branch progress) and your task brief. Instruct the subagent to flag bugs, logic errors, missing edge cases, gaps between request and diff, and unintended changes.
726
740
 
727
741
  Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
728
742
  - Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
@@ -864,7 +878,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
864
878
  You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches \u2014 concurrent context-pulling on the orchestrator side runs in parallel with the lens fan-out and costs zero extra wall time.
865
879
 
866
880
  if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip the fan-out entirely on a single subagent failure. each subagent gets:
867
- - the diff path / target \u2014 reading the diff and the codebase is its job
881
+ - **the absolute \`diffPath\` (and \`incrementalDiffPath\` if available) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt** (e.g. \`diffPath: /tmp/pullfrog-XXXX/pr-NNN-SHA.diff\`). the reviewer's baked-in system prompt selects its FIRST action on this token \u2014 paraphrasing ("review the diff", "look at this PR") sends it down the \`git diff origin/<base>\` fallback, which fails on shallow GHA checkouts. the subagent \`read\`s those files for scope; it must NOT re-derive the diff via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of any commits that landed on \`<base>\` since the branch forked \u2014 pure noise, and the git tool rejects it). reading and codebase exploration are still its job.
868
882
  - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
869
883
  - **a Task \`description\` set to the lens name** (e.g. \`"security"\`, \`"correctness"\`, \`"billing-subsystem"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
870
884
  - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search rather than trust training data, and to quote source URLs in its reasoning. action runs are non-interactive \u2014 there's no human in the loop to catch "I'm pretty sure Stripe does X."
@@ -974,7 +988,7 @@ ${PR_SUMMARY_FORMAT}`
974
988
  You can also include your own \`read\` / \`grep\` / \`webfetch\` calls in the SAME turn as the parallel \`${REVIEWER_AGENT_NAME}\` dispatches.
975
989
 
976
990
  if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body. each subagent gets:
977
- - the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 8), not in the subagent prompt
991
+ - **the absolute diff path(s) from step 2's \`${t("checkout_pr")}\` return, named verbatim in the dispatch prompt.** when \`incrementalDiffPath\` is present, name BOTH (\`incrementalDiffPath: /tmp/.../pr-NNN-SHA-incremental.diff\` then \`diffPath: /tmp/.../pr-NNN-SHA.diff\`) \u2014 the reviewer's baked-in prompt reads incremental first and uses full for context; when only \`diffPath\` exists, name it alone. the subagent \`read\`s those files; it must NOT re-derive via \`git diff\` (bare \`git diff origin/<base>\` is symmetric and pulls in the inverse of base-branch progress \u2014 pure noise, and the git tool rejects it), and paraphrasing ("review the new commits") sends it down that fallback, which also fails on shallow GHA checkouts. do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 8), not in the subagent prompt.
978
992
  - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
979
993
  - **a Task \`description\` set to the lens name** \u2014 the harness reads this field to label log lines so parallel runs can be told apart.
980
994
  - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs.
@@ -1083,7 +1097,7 @@ ${PR_SUMMARY_FORMAT}`
1083
1097
 
1084
1098
  1. **task list**: create your task list for this run as your first action.
1085
1099
 
1086
- 2. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
1100
+ 2. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly \u2014 but your answer only reaches the user through \`${t("report_progress")}\` (step 4); raw assistant text is discarded.
1087
1101
 
1088
1102
  3. For substantial work \u2014 code changes across multiple files, multi-step investigations:
1089
1103
  - plan your approach before starting
@@ -1157,6 +1171,92 @@ function stripExistingFooter(body) {
1157
1171
  return body.substring(0, dividerIndex).trimEnd();
1158
1172
  }
1159
1173
 
1174
+ // utils/codexOAuth.ts
1175
+ var CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
1176
+ var CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
1177
+ var OAuthInvalidGrantError = class extends Error {
1178
+ status;
1179
+ constructor(status, body) {
1180
+ super(`Codex token refresh failed: ${status} ${body}`);
1181
+ this.name = "OAuthInvalidGrantError";
1182
+ this.status = status;
1183
+ }
1184
+ };
1185
+ async function refreshCodexAuthBody(body) {
1186
+ const response = await fetch(CODEX_OAUTH_TOKEN_URL, {
1187
+ method: "POST",
1188
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1189
+ body: new URLSearchParams({
1190
+ grant_type: "refresh_token",
1191
+ refresh_token: body.tokens.refresh_token,
1192
+ client_id: CODEX_OAUTH_CLIENT_ID
1193
+ }).toString(),
1194
+ signal: AbortSignal.timeout(1e4)
1195
+ });
1196
+ if (!response.ok) {
1197
+ const text = await response.text().catch(() => "");
1198
+ if (response.status >= 400 && response.status < 500) {
1199
+ throw new OAuthInvalidGrantError(response.status, text);
1200
+ }
1201
+ throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
1202
+ }
1203
+ const tokens = await response.json();
1204
+ const idToken = tokens.id_token ?? body.tokens.id_token;
1205
+ const accountId = body.tokens.account_id;
1206
+ return {
1207
+ auth_mode: "chatgpt",
1208
+ tokens: {
1209
+ access_token: tokens.access_token,
1210
+ refresh_token: tokens.refresh_token,
1211
+ ...idToken ? { id_token: idToken } : {},
1212
+ ...accountId ? { account_id: accountId } : {}
1213
+ },
1214
+ last_refresh: (/* @__PURE__ */ new Date()).toISOString()
1215
+ };
1216
+ }
1217
+ function decodeJwtExpMs(token) {
1218
+ const parts = token.split(".");
1219
+ if (parts.length !== 3) return null;
1220
+ let payload;
1221
+ try {
1222
+ payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1223
+ } catch {
1224
+ return null;
1225
+ }
1226
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) return null;
1227
+ return payload.exp * 1e3;
1228
+ }
1229
+ function parseCodexAuthBody(raw) {
1230
+ let parsed;
1231
+ try {
1232
+ parsed = JSON.parse(raw);
1233
+ } catch {
1234
+ return null;
1235
+ }
1236
+ if (!parsed || typeof parsed !== "object") return null;
1237
+ const v = parsed;
1238
+ if (v.auth_mode !== "chatgpt") return null;
1239
+ const tokens = v.tokens;
1240
+ if (!tokens || typeof tokens !== "object") return null;
1241
+ const t = tokens;
1242
+ if (typeof t.access_token !== "string" || t.access_token.length === 0) return null;
1243
+ if (typeof t.refresh_token !== "string" || t.refresh_token.length === 0) return null;
1244
+ return {
1245
+ auth_mode: "chatgpt",
1246
+ tokens: {
1247
+ access_token: t.access_token,
1248
+ refresh_token: t.refresh_token,
1249
+ ...typeof t.id_token === "string" ? { id_token: t.id_token } : {},
1250
+ ...typeof t.account_id === "string" ? { account_id: t.account_id } : {}
1251
+ },
1252
+ ...typeof v.last_refresh === "string" ? { last_refresh: v.last_refresh } : {}
1253
+ };
1254
+ }
1255
+ function stringifyCodexAuthBody(body) {
1256
+ return `${JSON.stringify(body, null, 2)}
1257
+ `;
1258
+ }
1259
+
1160
1260
  // utils/leapingComment.ts
1161
1261
  var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
1162
1262
  function isLeapingIntoActionCommentBody(body) {
@@ -1292,11 +1392,14 @@ export {
1292
1392
  DEFAULT_PROXY_MODEL,
1293
1393
  LEAPING_INTO_ACTION_PREFIX,
1294
1394
  MAX_LEARNINGS_LENGTH,
1395
+ OAuthInvalidGrantError,
1295
1396
  PULLFROG_DIVIDER,
1296
1397
  TIMEOUT_DISABLED,
1297
1398
  buildPullfrogFooter,
1298
1399
  createLeapingProgressComment,
1400
+ decodeJwtExpMs,
1299
1401
  deleteProgressCommentApi,
1402
+ getAutoSelectHintModel,
1300
1403
  getModelEnvVars,
1301
1404
  getModelManagedCredentials,
1302
1405
  getModelProvider,
@@ -1306,14 +1409,17 @@ export {
1306
1409
  isValidTimeString,
1307
1410
  modelAliases,
1308
1411
  modes,
1412
+ parseCodexAuthBody,
1309
1413
  parseModel,
1310
1414
  parseTimeString,
1311
1415
  providers,
1312
1416
  pullfrogMcpName,
1417
+ refreshCodexAuthBody,
1313
1418
  resolveCliModel,
1314
1419
  resolveDisplayAlias,
1315
1420
  resolveModelSlug,
1316
1421
  resolveOpenRouterModel,
1422
+ stringifyCodexAuthBody,
1317
1423
  stripExistingFooter,
1318
1424
  truncateAtLineBoundary,
1319
1425
  updateProgressComment
@@ -38,7 +38,8 @@ export declare const ReportProgress: import("arktype/internal/variants/object.ts
38
38
  * - object: active comment — will update it in place via the right REST endpoint for its type
39
39
  * - null: deliberately deleted (e.g. after submitting a PR review) — skips silently
40
40
  *
41
- * The body is always tracked in lastProgressBody for the job summary regardless of comment state.
41
+ * The body is tracked in lastProgressBody for the job summary regardless of comment state,
42
+ * EXCEPT for `liveProgress` (todo-tracker) writes — see the param note below.
42
43
  *
43
44
  * The "existing plan comment" path always targets a top-level issue comment (plan comments are
44
45
  * created by create_issue_comment with type:"Plan", never as review-thread replies).
@@ -46,6 +47,7 @@ export declare const ReportProgress: import("arktype/internal/variants/object.ts
46
47
  export declare function reportProgress(ctx: ToolContext, params: {
47
48
  body: string;
48
49
  target_plan_comment?: boolean;
50
+ liveProgress?: boolean;
49
51
  }): Promise<{
50
52
  commentId?: number;
51
53
  url?: string;
@@ -1,9 +1,10 @@
1
1
  import type { Octokit } from "@octokit/rest";
2
2
  import type { ToolContext } from "./server.ts";
3
- export declare const REVIEW_THREADS_QUERY = "\nquery ($owner: String!, $name: String!, $prNumber: Int!) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $prNumber) {\n reviewThreads(first: 100) {\n nodes {\n id\n path\n line\n startLine\n diffSide\n isResolved\n isOutdated\n comments(first: 50) {\n nodes {\n fullDatabaseId\n body\n createdAt\n diffHunk\n line\n startLine\n originalLine\n originalStartLine\n author { login }\n pullRequestReview {\n databaseId\n author { login }\n }\n reactionGroups {\n content\n reactors(first: 10) {\n nodes {\n ... on Actor { login }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n";
3
+ export declare const REVIEW_THREADS_QUERY = "\nquery ($owner: String!, $name: String!, $prNumber: Int!) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $prNumber) {\n reviewThreads(first: 100) {\n nodes {\n id\n path\n line\n startLine\n diffSide\n isResolved\n isOutdated\n comments(first: 50) {\n nodes {\n fullDatabaseId\n body\n bodyHTML\n createdAt\n diffHunk\n line\n startLine\n originalLine\n originalStartLine\n author { login }\n pullRequestReview {\n databaseId\n author { login }\n }\n reactionGroups {\n content\n reactors(first: 10) {\n nodes {\n ... on Actor { login }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n";
4
4
  export type ReviewThreadComment = {
5
5
  fullDatabaseId: string | null;
6
6
  body: string;
7
+ bodyHTML: string;
7
8
  createdAt: string;
8
9
  diffHunk: string;
9
10
  line: number | null;
@@ -96,6 +97,8 @@ interface GetReviewDataInput {
96
97
  pullNumber: number;
97
98
  reviewId: number;
98
99
  approvedBy?: string | undefined;
100
+ tmpdir: string;
101
+ githubToken: string;
99
102
  }
100
103
  export interface FormatReviewDataInput {
101
104
  review: ReviewResponse;
package/dist/models.d.ts CHANGED
@@ -104,6 +104,8 @@ export declare function getModelEnvVars(slug: string): string[];
104
104
  export declare function getModelManagedCredentials(slug: string): string[];
105
105
  export declare const modelAliases: ModelAlias[];
106
106
  export declare const DEFAULT_PROXY_MODEL: string;
107
+ /** short label for the model auto-select picks today (console hint copy). */
108
+ export declare function getAutoSelectHintModel(): string;
107
109
  /** resolve a model slug to its concrete models.dev specifier (e.g. "anthropic/claude-opus-4-6") */
108
110
  export declare function resolveModelSlug(slug: string): string | undefined;
109
111
  /**
@@ -20,6 +20,8 @@ export type PrepResult = NodePrepResult | PythonPrepResult | UnknownLanguagePrep
20
20
  export type PrepOptions = {
21
21
  /** when true, lifecycle scripts (postinstall, etc.) are suppressed */
22
22
  ignoreScripts: boolean;
23
+ /** directory the corepack shim is installed into (see `packageManagerBinDir`) */
24
+ binDir: string;
23
25
  };
24
26
  export interface PrepDefinition {
25
27
  name: string;
@@ -102,7 +102,7 @@ export interface ToolState {
102
102
  learningsFilePath?: string;
103
103
  learningsSeed?: string;
104
104
  learningsPersistAttempted?: boolean;
105
- output?: string;
105
+ output?: string | undefined;
106
106
  usageEntries: AgentUsage[];
107
107
  model?: string | undefined;
108
108
  modelFallback?: {
@@ -1,10 +1,19 @@
1
- /** check if the user has a BYOK key for the given model's provider (does not throw) */
2
- export declare function hasProviderKey(model: string): boolean;
1
+ /**
2
+ * Validate that the resolved model can actually be served by the chosen
3
+ * agent. For routing slugs (Bedrock / Vertex) the auth shape is multi-var
4
+ * (auth + region/location + model-id) and `opencode models` doesn't catch
5
+ * gaps in the latter two — keep dedicated setup validators. For the
6
+ * opencode path, the authoritative answer comes from OpenCode's own model
7
+ * introspection (`authorized` set captured in `openCodeModels.ts`). For
8
+ * the claude path, fall back to the static check (`ANTHROPIC_API_KEY` /
9
+ * `CLAUDE_CODE_OAUTH_TOKEN`).
10
+ */
3
11
  export declare function validateAgentApiKey(params: {
4
12
  agent: {
5
13
  name: string;
6
14
  };
7
15
  model: string | undefined;
16
+ authorized: Set<string>;
8
17
  owner: string;
9
18
  name: string;
10
19
  }): void;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * downloads any github-hosted image/video assets referenced in `markdown` to
3
+ * `<tmpdir>/assets` and rewrites the urls to the local file paths, so the agent can
4
+ * read screenshots directly instead of relying on remote (often short-lived, signed)
5
+ * urls. unique urls are downloaded once and every occurrence is rewritten. assets that
6
+ * fail to download are left untouched.
7
+ */
8
+ export declare function downloadAssetsInMarkdown(markdown: string, tmpdir: string, githubToken: string): Promise<string>;
@@ -5,6 +5,8 @@ interface ResolveBodyContext {
5
5
  event: PayloadEvent;
6
6
  octokit: OctokitWithPlugins;
7
7
  repo: RunContextData["repo"];
8
+ tmpdir: string;
9
+ githubToken: string;
8
10
  }
9
11
  /**
10
12
  * resolves the body of an event by fetching body_html and converting to markdown.
@@ -13,4 +15,20 @@ interface ResolveBodyContext {
13
15
  * broken user-attachments URLs.
14
16
  */
15
17
  export declare function resolveBody(ctx: ResolveBodyContext): Promise<string | null>;
18
+ interface ResolveBodyAssetsContext {
19
+ body: string | null | undefined;
20
+ bodyHtml: string | null | undefined;
21
+ tmpdir: string;
22
+ githubToken: string;
23
+ }
24
+ /**
25
+ * downloads github-hosted image assets in a body to disk and rewrites the urls to local
26
+ * paths so the agent can read them. when the body has images and a rendered `bodyHtml`
27
+ * is supplied, the html is turndowned first: github only exposes attachments as signed,
28
+ * self-authenticating `*.githubusercontent.com` urls through body_html — the raw
29
+ * `github.com/user-attachments/...` urls in unrendered markdown 404 for the installation
30
+ * token. callers that fetch a body should request it with the `application/vnd.github.full+json`
31
+ * media type and pass `body_html` here.
32
+ */
33
+ export declare function resolveBodyAssets(ctx: ResolveBodyAssetsContext): Promise<string | null>;
16
34
  export {};
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Slug we fall back to when a BYOK-required model is configured but the
3
- * runner has no provider key in env. Picked because it's free
4
- * (`isFree: true`, `envVars: []` — see `action/models.ts`), stable, and
3
+ * runner has no provider key in env. Picked because it's free, stable, and
5
4
  * currently served by OpenCode Zen without a key.
6
5
  *
7
6
  * The slug is intentionally hard-coded and not a config knob — the
@@ -18,32 +17,22 @@ export type FallbackDecision = {
18
17
  to: string;
19
18
  };
20
19
  /**
21
- * If the resolved model requires a BYOK key but no provider key is
22
- * available in env, return `fallback: true` with a free OpenCode slug
23
- * so the run can still succeed. Caller is responsible for swapping the
24
- * model state and surfacing the fallback (log line + run summary).
20
+ * If the resolved model is NOT in OpenCode's `authorized` set (the
21
+ * authoritative "what can OpenCode route right now" snapshot captured
22
+ * after dbSecrets + Codex auth.json are in place), swap to a free
23
+ * OpenCode slug so the run can still produce value. Caller is responsible
24
+ * for surfacing the swap (log line + run summary).
25
25
  *
26
- * Gates on `resolvedModel` directly (not the configured slug) so the
27
- * decision matches both code paths that reach this point: payload-based
28
- * config (`repo.model` from DB) and `PULLFROG_MODEL` env var. Both end
29
- * up in `resolvedModel` after `resolveModel()` runs upstream.
30
- *
31
- * Skip cases:
32
- * - Router / proxy runs (`proxyModel` set): Pullfrog mints the key,
33
- * no BYOK in play — never fall back.
34
- * - No resolved model: keeps the existing auto-select-with-throw
35
- * behavior in `validateAgentApiKey` for the "neither model nor
36
- * key" case (genuine misconfig the user should see).
37
- * - Resolved model is itself the free fallback: avoid suggesting we
38
- * fell back to the model we're already running.
39
- * - Resolved model is a Bedrock raw ID (no `/`): Bedrock has its own
40
- * auth shape (`AWS_BEARER_TOKEN_BEDROCK` + region + model ID), and
41
- * `validateBedrockSetup` already surfaces a tailored error. Skipping
42
- * here also avoids `parseModel`'s slash requirement crashing inside
43
- * `hasProviderKey`.
44
- * - Resolved model has its provider key present: no fallback needed.
26
+ * Skip cases (return `fallback: false` without consulting `authorized`):
27
+ * - Router / proxy runs (`proxyModel` set): Pullfrog mints the key.
28
+ * - No resolved model: auto-select handles it downstream.
29
+ * - Resolved model is the free fallback already.
30
+ * - Resolved model is a raw Bedrock / Vertex ID (no `/`): the routing
31
+ * validators (`validateBedrockSetup` / `validateVertexSetup`) cover
32
+ * auth + region/location/model-id; `opencode models` does not.
45
33
  */
46
34
  export declare function selectFallbackModelIfNeeded(input: {
47
35
  resolvedModel: string | undefined;
48
36
  proxyModel: string | undefined;
37
+ authorized: Set<string>;
49
38
  }): FallbackDecision;
@@ -1,15 +1,28 @@
1
+ /** sandbox-hidden home for pullfrog-managed on-disk secrets in CI. bash via
2
+ * MCP shell tmpfs-overlays this path; opencode's internal auth module
3
+ * bypasses external_directory and reaches the real file. mirrors the
4
+ * pattern in action/agents/claude.ts installManagedSettings.
5
+ *
6
+ * not used for codex auth in local dev — the sandbox is no-op there, so
7
+ * the path doesn't matter. local dev keeps the existing $HOME path. */
8
+ export declare const PULLFROG_DATA_DIR = "/var/lib/pullfrog";
1
9
  export interface InstalledCodexAuth {
2
10
  /** absolute path of the auth.json we wrote — caller passes this to the
3
11
  * post-hook via core.saveState for refresh-detection later. */
4
12
  authPath: string;
5
13
  /** value to set as XDG_DATA_HOME for the OpenCode subprocess. */
6
14
  xdgDataHome: string;
7
- /** refresh_token from the env at materialization time. post-hook compares
8
- * against the on-disk file after the run to detect whether OpenCode
9
- * refreshed during the session. */
15
+ /** refresh_token from the env at materialization time. post-hook
16
+ * compares against the on-disk file after the run to detect whether
17
+ * OpenCode refreshed during the session (only happens on long runs
18
+ * that span >50min — see wiki/codex-auth.md "Concurrency"). */
10
19
  originalRefresh: string;
11
20
  }
12
21
  /** materialize CODEX_AUTH_JSON from env into a disk path OpenCode reads from.
13
22
  * returns null when the env var is absent, malformed, or wrong auth mode —
14
- * caller treats null as "no codex auth, fall through to API key flow". */
23
+ * caller treats null as "no codex auth, fall through to API key flow".
24
+ *
25
+ * The env value is server-side guaranteed fresh by `maybeRotateCodexSecret`
26
+ * in the run-context endpoint. We only parse + write it here; no refresh,
27
+ * no DB interaction. */
15
28
  export declare function installCodexAuth(): InstalledCodexAuth | null;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pure-stdlib (fetch + Buffer) Codex OAuth refresh + JWT exp decoding.
3
+ *
4
+ * Lives here (not in codexAuth.ts) so the Next.js server side can import it
5
+ * via pullfrog/internal without dragging in node:child_process / spawn /
6
+ * mkdtemp from the rest of codexAuth.ts. Used by:
7
+ * - action/utils/codexAuth.ts (re-exports refreshCodexAuthBody)
8
+ * - utils/codexSecretRotation.ts (server-side maybeRotate at run-context)
9
+ *
10
+ * See wiki/codex-auth.md for the end-to-end refresh lifecycle.
11
+ */
12
+ export interface CodexAuthBody {
13
+ auth_mode: "chatgpt";
14
+ tokens: {
15
+ access_token: string;
16
+ refresh_token: string;
17
+ id_token?: string;
18
+ account_id?: string;
19
+ };
20
+ last_refresh?: string;
21
+ }
22
+ /** OAuth client id Codex CLI and OpenCode both use against `auth.openai.com`.
23
+ * Same chain — a refresh token minted via `codex login --device-auth` can be
24
+ * refreshed against this client_id. */
25
+ export declare const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
26
+ export declare const CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
27
+ /** thrown when the OAuth provider rejects the refresh token (4xx). callers
28
+ * can distinguish "race-lost / token revoked" from network errors via
29
+ * `instanceof OAuthInvalidGrantError`. */
30
+ export declare class OAuthInvalidGrantError extends Error {
31
+ readonly status: number;
32
+ constructor(status: number, body: string);
33
+ }
34
+ /** force one refresh round-trip against the OAuth provider. returns the
35
+ * rotated Codex-shaped blob (the auth.json body verbatim). does NOT persist
36
+ * — caller is responsible for writing back to wherever the token lives.
37
+ *
38
+ * server-side callers (maybeRotateCodexSecret) hold a DB row lock around
39
+ * this call so concurrent runs serialize: first one rotates, subsequent
40
+ * ones see the fresh value and skip. The 10s timeout is critical for that
41
+ * use: it caps how long a stalled auth.openai.com holds the row lock,
42
+ * keeping us well under the enclosing 30s transaction budget so the lock
43
+ * always releases and queued callers get a turn instead of timing out on
44
+ * the tx wrapper. Real OAuth latency is sub-second; 10s is generous. */
45
+ export declare function refreshCodexAuthBody(body: CodexAuthBody): Promise<CodexAuthBody>;
46
+ /** decode the access_token's JWT payload and return its `exp` claim in ms
47
+ * since epoch. returns null if the token isn't a parseable JWT or has no
48
+ * `exp` claim — caller falls back to "treat as expired".
49
+ *
50
+ * We don't verify the JWT signature (we'd need OpenAI's JWKS); we're only
51
+ * using the claim as a freshness hint. The actual auth check happens
52
+ * server-side at OpenAI when the token is used — trusting a fake JWT here
53
+ * would just delay the inevitable 401 from OpenAI. No security boundary
54
+ * at this decode step. */
55
+ export declare function decodeJwtExpMs(token: string): number | null;
56
+ /** parse + validate a Codex auth.json body from its JSON-string form.
57
+ * returns null on any shape mismatch — caller treats as "no codex auth". */
58
+ export declare function parseCodexAuthBody(raw: string): CodexAuthBody | null;
59
+ /** serialize a CodexAuthBody to its canonical on-disk form. */
60
+ export declare function stringifyCodexAuthBody(body: CodexAuthBody): string;
@@ -17,6 +17,10 @@ interface InstructionsContext {
17
17
  * inline into the LEARNINGS prompt section so the agent can `read_file`
18
18
  * targeted line ranges instead of pulling the whole file into context. */
19
19
  learningsHeadings: LearningsHeading[];
20
+ /** agent-facing description of a setup lifecycle hook failure (see
21
+ * `describeSetupFailure`), rendered as a SETUP HOOK FAILED banner. empty
22
+ * string when the hook succeeded, was skipped, or wasn't configured. */
23
+ setupHookFailure: string;
20
24
  }
21
25
  export interface ResolvedInstructions {
22
26
  full: string;
@@ -1,6 +1,18 @@
1
1
  export interface ExecuteLifecycleHookParams {
2
2
  event: string;
3
3
  script: string | null;
4
+ /**
5
+ * when true, after the hook runs (success or failure), discard tracked-file
6
+ * mods so the agent doesn't see hook-generated drift (e.g. `pnpm install`
7
+ * rewriting a lockfile). untracked files are preserved — hooks that
8
+ * intentionally materialize files (e.g. a `.env` from a template) stay
9
+ * visible to the agent. skipped (with a warning) if the tree had
10
+ * pre-existing tracked changes before the hook ran, so we never clobber
11
+ * pre-existing work; pre-existing untracked files are ignored for this
12
+ * gate because `git restore --staged --worktree .` doesn't touch them
13
+ * anyway. no-op when no script was configured.
14
+ */
15
+ normalizeWorkingTreeAfter?: boolean;
4
16
  }
5
17
  /** structured failure info — `output` on the `exit` variant is trimmed
6
18
  * stderr, falling back to stdout when stderr is empty. */
@@ -14,6 +26,10 @@ export type LifecycleHookFailure = {
14
26
  kind: "spawn";
15
27
  spawnError: string;
16
28
  };
29
+ /** one-line, agent-facing description of a hook failure. empty string when
30
+ * there was no failure, so callers can pass the result straight through to a
31
+ * prompt section that omits itself on empty. */
32
+ export declare function describeSetupFailure(failure: LifecycleHookFailure | undefined): string;
17
33
  export interface LifecycleHookResult {
18
34
  /**
19
35
  * human-readable warning when the hook failed. includes retry guidance:
@@ -34,8 +50,8 @@ export interface LifecycleHookResult {
34
50
  * execute a lifecycle hook script if one is configured.
35
51
  *
36
52
  * soft-fails: instead of throwing on hook errors, returns a warning string
37
- * (and structured failure info) so callers can choose whether to surface
38
- * it (mcp tools) or upgrade it to a fatal error (setup). timeouts are
39
- * flagged as non-retryable in the warning text.
53
+ * (and structured failure info) so callers can choose how to surface it
54
+ * (mcp tools relay it to the agent; setup logs it and adds a prompt banner).
55
+ * timeouts are flagged as non-retryable in the warning text.
40
56
  */
41
57
  export declare function executeLifecycleHook(params: ExecuteLifecycleHookParams): Promise<LifecycleHookResult>;
@@ -0,0 +1,11 @@
1
+ /** Snapshot the set of models OpenCode can serve from the current env, BEFORE
2
+ * Pullfrog-stored credentials are merged in. Call once early in `main.ts`. */
3
+ export declare function captureBaselineModels(cliPath: string): void;
4
+ /** Snapshot the set of models OpenCode can serve AFTER dbSecrets +
5
+ * Codex auth.json are in place. Logs the diff against the baseline as
6
+ * `» BYOK auth enabled N model(s): …`. */
7
+ export declare function captureAuthorizedModels(cliPath: string): void;
8
+ /** Authorized set captured after Pullfrog-stored auth is applied. Throws if
9
+ * called before `captureAuthorizedModels` — the call sites (fallback gate,
10
+ * api-key validation, auto-select) all run strictly after capture. */
11
+ export declare function getAuthorizedModels(): Set<string>;