Frank пре 7 месеци
родитељ
комит
a86d42149f

+ 8 - 0
packages/opencode/script/publish-github-action.ts

@@ -0,0 +1,8 @@
+#!/usr/bin/env bun
+
+import { $ } from "bun"
+
+await $`git tag -d v1`
+await $`git push origin :refs/tags/v1`
+await $`git tag -a v1 -m "Update v1 to latest"`
+await $`git push origin v1`

+ 244 - 0
packages/opencode/src/cli/cmd/install-github.ts

@@ -0,0 +1,244 @@
+import { $ } from "bun"
+import path from "path"
+import { exec } from "child_process"
+import * as prompts from "@clack/prompts"
+import { map, pipe, sortBy, values } from "remeda"
+import { UI } from "../ui"
+import { cmd } from "./cmd"
+import { ModelsDev } from "../../provider/models"
+import { App } from "../../app/app"
+
+const WORKFLOW_FILE = ".github/workflows/opencode.yml"
+
+export const InstallGithubCommand = cmd({
+  command: "install-github",
+  describe: "install the GitHub agent",
+  async handler() {
+    await App.provide({ cwd: process.cwd() }, async () => {
+      UI.empty()
+      prompts.intro("Install GitHub agent")
+      const app = await getAppInfo()
+      await installGitHubApp()
+
+      const providers = await ModelsDev.get()
+      const provider = await promptProvider()
+      const model = await promptModel()
+      //const key = await promptKey()
+
+      await addWorkflowFiles()
+      printNextSteps()
+
+      function printNextSteps() {
+        let step2
+        if (provider === "amazon-bedrock") {
+          step2 =
+            "Configure OIDC in AWS - https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
+        } else {
+          const url = `https://github.com/organizations/${app.owner}/settings/secrets/actions`
+          const env = providers[provider].env
+          const envStr =
+            env.length === 1
+              ? `\`${env[0]}\` secret`
+              : `\`${[env.slice(0, -1).join("\`, \`"), ...env.slice(-1)].join("\` and \`")}\` secrets`
+          step2 = `Add ${envStr} for ${providers[provider].name} - ${url}`
+        }
+
+        prompts.outro(
+          [
+            "Next steps:",
+            `    1. Commit "${WORKFLOW_FILE}" file and push`,
+            `    2. ${step2}`,
+            "    3. Learn how to use the GitHub agent - https://docs.opencode.ai/docs/github/getting-started",
+          ].join("\n"),
+        )
+      }
+
+      async function getAppInfo() {
+        const app = App.info()
+        if (!app.git) {
+          prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
+          throw new UI.CancelledError()
+        }
+
+        // Get repo info
+        const info = await $`git remote get-url origin`.quiet().nothrow().text()
+        // match https or git pattern
+        // ie. https://github.com/sst/opencode.git
+        // ie. [email protected]:sst/opencode.git
+        const parsed = info.match(/git@github\.com:(.*)\.git/) ?? info.match(/github\.com\/(.*)\.git/)
+        if (!parsed) {
+          prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
+          throw new UI.CancelledError()
+        }
+        const [owner, repo] = parsed[1].split("/")
+        return { owner, repo, root: app.path.root }
+      }
+
+      async function promptProvider() {
+        const priority: Record<string, number> = {
+          anthropic: 0,
+          "github-copilot": 1,
+          openai: 2,
+          google: 3,
+        }
+        let provider = await prompts.select({
+          message: "Select provider",
+          maxItems: 8,
+          options: [
+            ...pipe(
+              providers,
+              values(),
+              sortBy(
+                (x) => priority[x.id] ?? 99,
+                (x) => x.name ?? x.id,
+              ),
+              map((x) => ({
+                label: x.name,
+                value: x.id,
+                hint: priority[x.id] === 0 ? "recommended" : undefined,
+              })),
+            ),
+            {
+              value: "other",
+              label: "Other",
+            },
+          ],
+        })
+
+        if (prompts.isCancel(provider)) throw new UI.CancelledError()
+        if (provider === "other") {
+          provider = await prompts.text({
+            message: "Enter provider id",
+            validate: (x) => (x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only"),
+          })
+          if (prompts.isCancel(provider)) throw new UI.CancelledError()
+          provider = provider.replace(/^@ai-sdk\//, "")
+          if (prompts.isCancel(provider)) throw new UI.CancelledError()
+          prompts.log.warn(
+            `This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
+          )
+        }
+
+        return provider
+      }
+
+      async function promptModel() {
+        const providerData = providers[provider]!
+
+        const model = await prompts.select({
+          message: "Select model",
+          maxItems: 8,
+          options: pipe(
+            providerData.models,
+            values(),
+            sortBy((x) => x.name ?? x.id),
+            map((x) => ({
+              label: x.name ?? x.id,
+              value: x.id,
+            })),
+          ),
+        })
+
+        if (prompts.isCancel(model)) throw new UI.CancelledError()
+        return model
+      }
+
+      async function promptKey() {
+        const key = await prompts.password({
+          message: "Enter your API key",
+          validate: (x) => (x.length > 0 ? undefined : "Required"),
+        })
+        if (prompts.isCancel(key)) throw new UI.CancelledError()
+        return key
+      }
+
+      async function installGitHubApp() {
+        const s = prompts.spinner()
+        s.start("Installing GitHub app")
+
+        // Get installation
+        const installation = await getInstallation()
+        if (installation) return s.stop("GitHub app already installed")
+
+        // Open browser
+        const url = "https://github.com/apps/opencode-agent"
+        const command =
+          process.platform === "darwin"
+            ? `open "${url}"`
+            : process.platform === "win32"
+              ? `start "${url}"`
+              : `xdg-open "${url}"`
+
+        exec(command, (error) => {
+          if (error) {
+            prompts.log.warn(`Could not open browser. Please visit: ${url}`)
+          }
+        })
+
+        // Wait for installation
+        s.message("Waiting for GitHub app to be installed")
+        const MAX_RETRIES = 60
+        let retries = 0
+        do {
+          const installation = await getInstallation()
+          if (installation) break
+
+          if (retries > MAX_RETRIES) {
+            s.stop(
+              `Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
+            )
+            throw new UI.CancelledError()
+          }
+
+          retries++
+          await new Promise((resolve) => setTimeout(resolve, 1000))
+        } while (true)
+
+        s.stop("Installed GitHub app")
+
+        async function getInstallation() {
+          return await fetch(`https://api.opencode.ai/get_github_app_installation?owner=${app.owner}&repo=${app.repo}`)
+            .then((res) => res.json())
+            .then((data) => data.installation)
+        }
+      }
+
+      async function addWorkflowFiles() {
+        const envStr =
+          provider === "amazon-bedrock"
+            ? ""
+            : `\n        env:${providers[provider].env.map((e) => `\n          ${e}: \${{ secrets.${e} }}`).join("")}`
+
+        await Bun.write(
+          path.join(app.root, WORKFLOW_FILE),
+          `
+name: opencode
+
+on:
+  issue_comment:
+    types: [created]
+
+jobs:
+  opencode:
+    if: startsWith(github.event.comment.body, 'hey opencode')
+    runs-on: ubuntu-latest
+    permissions:
+      id-token: write
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
+
+      - name: Run opencode
+        uses: sst/opencode/sdks/github@dev${envStr}
+        with:
+          model: ${provider}/${model}
+`.trim(),
+        )
+
+        prompts.log.success(`Added workflow file: "${WORKFLOW_FILE}"`)
+      }
+    })
+  },
+})

+ 2 - 0
packages/opencode/src/index.ts

@@ -16,6 +16,7 @@ import { TuiCommand } from "./cli/cmd/tui"
 import { DebugCommand } from "./cli/cmd/debug"
 import { DebugCommand } from "./cli/cmd/debug"
 import { StatsCommand } from "./cli/cmd/stats"
 import { StatsCommand } from "./cli/cmd/stats"
 import { McpCommand } from "./cli/cmd/mcp"
 import { McpCommand } from "./cli/cmd/mcp"
+import { InstallGithubCommand } from "./cli/cmd/install-github"
 
 
 const cancel = new AbortController()
 const cancel = new AbortController()
 
 
@@ -76,6 +77,7 @@ const cli = yargs(hideBin(process.argv))
   .command(ServeCommand)
   .command(ServeCommand)
   .command(ModelsCommand)
   .command(ModelsCommand)
   .command(StatsCommand)
   .command(StatsCommand)
+  .command(InstallGithubCommand)
   .fail((msg) => {
   .fail((msg) => {
     if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
     if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
       cli.showHelp("log")
       cli.showHelp("log")

+ 189 - 237
sdks/github/src/index.ts

@@ -1,341 +1,299 @@
 #!/usr/bin/env bun
 #!/usr/bin/env bun
 
 
-import os from "os";
-import path from "path";
-import { $ } from "bun";
-import { Octokit } from "@octokit/rest";
-import { graphql } from "@octokit/graphql";
-import * as core from "@actions/core";
-import * as github from "@actions/github";
-import type { IssueCommentEvent } from "@octokit/webhooks-types";
-import type {
-  GitHubIssue,
-  GitHubPullRequest,
-  IssueQueryResponse,
-  PullRequestQueryResponse,
-} from "./types";
+import os from "os"
+import path from "path"
+import { $ } from "bun"
+import { Octokit } from "@octokit/rest"
+import { graphql } from "@octokit/graphql"
+import * as core from "@actions/core"
+import * as github from "@actions/github"
+import type { IssueCommentEvent } from "@octokit/webhooks-types"
+import type { GitHubIssue, GitHubPullRequest, IssueQueryResponse, PullRequestQueryResponse } from "./types"
 
 
 if (github.context.eventName !== "issue_comment") {
 if (github.context.eventName !== "issue_comment") {
-  core.setFailed(`Unsupported event type: ${github.context.eventName}`);
-  process.exit(1);
+  core.setFailed(`Unsupported event type: ${github.context.eventName}`)
+  process.exit(1)
 }
 }
 
 
-const { owner, repo } = github.context.repo;
-const payload = github.context.payload as IssueCommentEvent;
-const actor = github.context.actor;
-const issueId = payload.issue.number;
-const body = payload.comment.body;
-
-let appToken: string;
-let octoRest: Octokit;
-let octoGraph: typeof graphql;
-let commentId: number;
-let gitCredentials: string;
-let shareUrl: string | undefined;
+const { owner, repo } = github.context.repo
+const payload = github.context.payload as IssueCommentEvent
+const actor = github.context.actor
+const issueId = payload.issue.number
+const body = payload.comment.body
+
+let appToken: string
+let octoRest: Octokit
+let octoGraph: typeof graphql
+let commentId: number
+let gitCredentials: string
+let shareUrl: string | undefined
 let state:
 let state:
   | {
   | {
-      type: "issue";
-      issue: GitHubIssue;
+      type: "issue"
+      issue: GitHubIssue
     }
     }
   | {
   | {
-      type: "local-pr";
-      pr: GitHubPullRequest;
+      type: "local-pr"
+      pr: GitHubPullRequest
     }
     }
   | {
   | {
-      type: "fork-pr";
-      pr: GitHubPullRequest;
-    };
+      type: "fork-pr"
+      pr: GitHubPullRequest
+    }
 
 
 async function run() {
 async function run() {
   try {
   try {
-    const match = body.match(/^hey\s*opencode,?\s*(.*)$/);
-    if (!match?.[1]) throw new Error("Command must start with `hey opencode`");
-    const userPrompt = match[1];
+    const match = body.match(/^hey\s*opencode,?\s*(.*)$/)
+    if (!match?.[1]) throw new Error("Command must start with `hey opencode`")
+    const userPrompt = match[1]
 
 
-    const oidcToken = await generateGitHubToken();
-    appToken = await exchangeForAppToken(oidcToken);
-    octoRest = new Octokit({ auth: appToken });
+    const oidcToken = await generateGitHubToken()
+    appToken = await exchangeForAppToken(oidcToken)
+    octoRest = new Octokit({ auth: appToken })
     octoGraph = graphql.defaults({
     octoGraph = graphql.defaults({
       headers: { authorization: `token ${appToken}` },
       headers: { authorization: `token ${appToken}` },
-    });
+    })
 
 
-    await configureGit(appToken);
-    await assertPermissions();
+    await configureGit(appToken)
+    await assertPermissions()
 
 
-    const comment = await createComment("opencode started...");
-    commentId = comment.data.id;
+    const comment = await createComment("opencode started...")
+    commentId = comment.data.id
 
 
     // Set state
     // Set state
-    const repoData = await fetchRepo();
+    const repoData = await fetchRepo()
     if (payload.issue.pull_request) {
     if (payload.issue.pull_request) {
-      const prData = await fetchPR();
+      const prData = await fetchPR()
       state = {
       state = {
-        type:
-          prData.headRepository.nameWithOwner ===
-          prData.baseRepository.nameWithOwner
-            ? "local-pr"
-            : "fork-pr",
+        type: prData.headRepository.nameWithOwner === prData.baseRepository.nameWithOwner ? "local-pr" : "fork-pr",
         pr: prData,
         pr: prData,
-      };
+      }
     } else {
     } else {
       state = {
       state = {
         type: "issue",
         type: "issue",
         issue: await fetchIssue(),
         issue: await fetchIssue(),
-      };
+      }
     }
     }
 
 
     // Setup git branch
     // Setup git branch
-    if (state.type === "local-pr") await checkoutLocalBranch(state.pr);
-    else if (state.type === "fork-pr") await checkoutForkBranch(state.pr);
+    if (state.type === "local-pr") await checkoutLocalBranch(state.pr)
+    else if (state.type === "fork-pr") await checkoutForkBranch(state.pr)
 
 
     // Prompt
     // Prompt
-    const share = process.env.INPUT_SHARE === "true" || !repoData.data.private;
-    const promptData =
-      state.type === "issue"
-        ? buildPromptDataForIssue(state.issue)
-        : buildPromptDataForPR(state.pr);
+    const share = process.env.INPUT_SHARE === "true" || !repoData.data.private
+    const promptData = state.type === "issue" ? buildPromptDataForIssue(state.issue) : buildPromptDataForPR(state.pr)
     const responseRet = await runOpencode(`${userPrompt}\n\n${promptData}`, {
     const responseRet = await runOpencode(`${userPrompt}\n\n${promptData}`, {
       share,
       share,
-    });
+    })
 
 
-    const response = responseRet.stdout;
-    shareUrl = responseRet.stderr.match(/https:\/\/opencode\.ai\/s\/\w+/)?.[0];
+    const response = responseRet.stdout
+    shareUrl = responseRet.stderr.match(/https:\/\/opencode\.ai\/s\/\w+/)?.[0]
 
 
     // Comment and push changes
     // Comment and push changes
     if (await branchIsDirty()) {
     if (await branchIsDirty()) {
       const summary =
       const summary =
-        (
-          await runOpencode(
-            `Summarize the following in less than 40 characters:\n\n${response}`,
-            { share: false }
-          )
-        )?.stdout || `Fix issue: ${payload.issue.title}`;
+        (await runOpencode(`Summarize the following in less than 40 characters:\n\n${response}`, { share: false }))
+          ?.stdout || `Fix issue: ${payload.issue.title}`
 
 
       if (state.type === "issue") {
       if (state.type === "issue") {
-        const branch = await pushToNewBranch(summary);
-        const pr = await createPR(
-          repoData.data.default_branch,
-          branch,
-          summary,
-          `${response}\n\nCloses #${issueId}`
-        );
-        await updateComment(`opencode created pull request #${pr}`);
+        const branch = await pushToNewBranch(summary)
+        const pr = await createPR(repoData.data.default_branch, branch, summary, `${response}\n\nCloses #${issueId}`)
+        await updateComment(`opencode created pull request #${pr}`)
       } else if (state.type === "local-pr") {
       } else if (state.type === "local-pr") {
-        await pushToCurrentBranch(summary);
-        await updateComment(response);
+        await pushToCurrentBranch(summary)
+        await updateComment(response)
       } else if (state.type === "fork-pr") {
       } else if (state.type === "fork-pr") {
-        await pushToForkBranch(summary, state.pr);
-        await updateComment(response);
+        await pushToForkBranch(summary, state.pr)
+        await updateComment(response)
       }
       }
     } else {
     } else {
-      await updateComment(response);
+      await updateComment(response)
     }
     }
-    await restoreGitConfig();
-    await revokeAppToken();
+    await restoreGitConfig()
+    await revokeAppToken()
   } catch (e: any) {
   } catch (e: any) {
-    await restoreGitConfig();
-    await revokeAppToken();
-    console.error(e);
-    let msg = e;
+    await restoreGitConfig()
+    await revokeAppToken()
+    console.error(e)
+    let msg = e
     if (e instanceof $.ShellError) {
     if (e instanceof $.ShellError) {
-      msg = e.stderr.toString();
+      msg = e.stderr.toString()
     } else if (e instanceof Error) {
     } else if (e instanceof Error) {
-      msg = e.message;
+      msg = e.message
     }
     }
-    if (commentId) await updateComment(msg);
-    core.setFailed(`opencode failed with error: ${msg}`);
+    if (commentId) await updateComment(msg)
+    core.setFailed(`opencode failed with error: ${msg}`)
     // Also output the clean error message for the action to capture
     // Also output the clean error message for the action to capture
     //core.setOutput("prepare_error", e.message);
     //core.setOutput("prepare_error", e.message);
-    process.exit(1);
+    process.exit(1)
   }
   }
 }
 }
 
 
 if (import.meta.main) {
 if (import.meta.main) {
-  run();
+  run()
 }
 }
 
 
 async function generateGitHubToken() {
 async function generateGitHubToken() {
   try {
   try {
-    return await core.getIDToken("opencode-github-action");
+    return await core.getIDToken("opencode-github-action")
   } catch (error) {
   } catch (error) {
-    console.error("Failed to get OIDC token:", error);
-    throw new Error(
-      "Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions."
-    );
+    console.error("Failed to get OIDC token:", error)
+    throw new Error("Could not fetch an OIDC token. Make sure to add `id-token: write` to your workflow permissions.")
   }
   }
 }
 }
 
 
 async function exchangeForAppToken(oidcToken: string) {
 async function exchangeForAppToken(oidcToken: string) {
-  const response = await fetch(
-    "https://api.frank.dev.opencode.ai/exchange_github_app_token",
-    {
-      method: "POST",
-      headers: {
-        Authorization: `Bearer ${oidcToken}`,
-      },
-    }
-  );
+  const response = await fetch("https://api.opencode.ai/exchange_github_app_token", {
+    method: "POST",
+    headers: {
+      Authorization: `Bearer ${oidcToken}`,
+    },
+  })
 
 
   if (!response.ok) {
   if (!response.ok) {
-    const responseJson = (await response.json()) as { error?: string };
-    throw new Error(
-      `App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`
-    );
+    const responseJson = (await response.json()) as { error?: string }
+    throw new Error(`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson.error}`)
   }
   }
 
 
-  const responseJson = (await response.json()) as { token: string };
-  return responseJson.token;
+  const responseJson = (await response.json()) as { token: string }
+  return responseJson.token
 }
 }
 
 
 async function configureGit(appToken: string) {
 async function configureGit(appToken: string) {
-  console.log("Configuring git...");
-  const config = "http.https://github.com/.extraheader";
-  const ret = await $`git config --local --get ${config}`;
-  gitCredentials = ret.stdout.toString().trim();
-
-  const newCredentials = Buffer.from(
-    `x-access-token:${appToken}`,
-    "utf8"
-  ).toString("base64");
-
-  await $`git config --local --unset-all ${config}`;
-  await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`;
-  await $`git config --global user.name "opencode-agent[bot]"`;
-  await $`git config --global user.email "opencode-agent[bot]@users.noreply.github.com"`;
+  console.log("Configuring git...")
+  const config = "http.https://github.com/.extraheader"
+  const ret = await $`git config --local --get ${config}`
+  gitCredentials = ret.stdout.toString().trim()
+
+  const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
+
+  await $`git config --local --unset-all ${config}`
+  await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
+  await $`git config --global user.name "opencode-agent[bot]"`
+  await $`git config --global user.email "opencode-agent[bot]@users.noreply.github.com"`
 }
 }
 
 
 async function checkoutLocalBranch(pr: GitHubPullRequest) {
 async function checkoutLocalBranch(pr: GitHubPullRequest) {
-  console.log("Checking out local branch...");
+  console.log("Checking out local branch...")
 
 
-  const branch = pr.headRefName;
-  const depth = Math.max(pr.commits.totalCount, 20);
+  const branch = pr.headRefName
+  const depth = Math.max(pr.commits.totalCount, 20)
 
 
-  await $`git fetch origin --depth=${depth} ${branch}`;
-  await $`git checkout ${branch}`;
+  await $`git fetch origin --depth=${depth} ${branch}`
+  await $`git checkout ${branch}`
 }
 }
 
 
 async function checkoutForkBranch(pr: GitHubPullRequest) {
 async function checkoutForkBranch(pr: GitHubPullRequest) {
-  console.log("Checking out fork branch...");
+  console.log("Checking out fork branch...")
 
 
-  const remoteBranch = pr.headRefName;
-  const localBranch = generateBranchName();
-  const depth = Math.max(pr.commits.totalCount, 20);
+  const remoteBranch = pr.headRefName
+  const localBranch = generateBranchName()
+  const depth = Math.max(pr.commits.totalCount, 20)
 
 
-  await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git`;
-  await $`git fetch fork --depth=${depth} ${remoteBranch}`;
-  await $`git checkout -b ${localBranch} fork/${remoteBranch}`;
+  await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git`
+  await $`git fetch fork --depth=${depth} ${remoteBranch}`
+  await $`git checkout -b ${localBranch} fork/${remoteBranch}`
 }
 }
 
 
 async function restoreGitConfig() {
 async function restoreGitConfig() {
-  if (!gitCredentials) return;
-  const config = "http.https://github.com/.extraheader";
-  await $`git config --local ${config} "${gitCredentials}"`;
+  if (!gitCredentials) return
+  const config = "http.https://github.com/.extraheader"
+  await $`git config --local ${config} "${gitCredentials}"`
 }
 }
 
 
 async function assertPermissions() {
 async function assertPermissions() {
-  console.log(`Asserting permissions for user ${actor}...`);
+  console.log(`Asserting permissions for user ${actor}...`)
 
 
-  let permission;
+  let permission
   try {
   try {
     const response = await octoRest.repos.getCollaboratorPermissionLevel({
     const response = await octoRest.repos.getCollaboratorPermissionLevel({
       owner,
       owner,
       repo,
       repo,
       username: actor,
       username: actor,
-    });
+    })
 
 
-    permission = response.data.permission;
-    console.log(`  permission: ${permission}`);
+    permission = response.data.permission
+    console.log(`  permission: ${permission}`)
   } catch (error) {
   } catch (error) {
-    console.error(`Failed to check permissions: ${error}`);
-    throw new Error(`Failed to check permissions for user ${actor}: ${error}`);
+    console.error(`Failed to check permissions: ${error}`)
+    throw new Error(`Failed to check permissions for user ${actor}: ${error}`)
   }
   }
 
 
-  if (!["admin", "write"].includes(permission))
-    throw new Error(`User ${actor} does not have write permissions`);
+  if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
 }
 }
 
 
 function buildComment(content: string) {
 function buildComment(content: string) {
-  const runId = process.env.GITHUB_RUN_ID!;
-  const runUrl = `/${owner}/${repo}/actions/runs/${runId}`;
-  return [
-    content,
-    "\n\n",
-    shareUrl ? `[view session](${shareUrl}) | ` : "",
-    `[view log](${runUrl})`,
-  ].join("");
+  const runId = process.env.GITHUB_RUN_ID!
+  const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
+  return [content, "\n\n", shareUrl ? `[view session](${shareUrl}) | ` : "", `[view log](${runUrl})`].join("")
 }
 }
 
 
 async function createComment(body: string) {
 async function createComment(body: string) {
-  console.log("Creating comment...");
+  console.log("Creating comment...")
   return await octoRest.rest.issues.createComment({
   return await octoRest.rest.issues.createComment({
     owner,
     owner,
     repo,
     repo,
     issue_number: issueId,
     issue_number: issueId,
     body: buildComment(body),
     body: buildComment(body),
-  });
+  })
 }
 }
 
 
 async function updateComment(body: string) {
 async function updateComment(body: string) {
-  console.log("Updating comment...");
+  console.log("Updating comment...")
   return await octoRest.rest.issues.updateComment({
   return await octoRest.rest.issues.updateComment({
     owner,
     owner,
     repo,
     repo,
     comment_id: commentId,
     comment_id: commentId,
     body: buildComment(body),
     body: buildComment(body),
-  });
+  })
 }
 }
 
 
 function generateBranchName() {
 function generateBranchName() {
-  const type = state.type === "issue" ? "issue" : "pr";
+  const type = state.type === "issue" ? "issue" : "pr"
   const timestamp = new Date()
   const timestamp = new Date()
     .toISOString()
     .toISOString()
     .replace(/[:-]/g, "")
     .replace(/[:-]/g, "")
     .replace(/\.\d{3}Z/, "")
     .replace(/\.\d{3}Z/, "")
     .split("T")
     .split("T")
-    .join("_");
-  return `opencode/${type}${issueId}-${timestamp}`;
+    .join("_")
+  return `opencode/${type}${issueId}-${timestamp}`
 }
 }
 
 
 async function pushToCurrentBranch(summary: string) {
 async function pushToCurrentBranch(summary: string) {
-  console.log("Pushing to current branch...");
-  await $`git add .`;
+  console.log("Pushing to current branch...")
+  await $`git add .`
   await $`git commit -m "${summary}
   await $`git commit -m "${summary}
   
   
-Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`;
-  await $`git push`;
+Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
+  await $`git push`
 }
 }
 
 
 async function pushToForkBranch(summary: string, pr: GitHubPullRequest) {
 async function pushToForkBranch(summary: string, pr: GitHubPullRequest) {
-  console.log("Pushing to fork branch...");
+  console.log("Pushing to fork branch...")
 
 
-  const remoteBranch = pr.headRefName;
+  const remoteBranch = pr.headRefName
 
 
-  await $`git add .`;
+  await $`git add .`
   await $`git commit -m "${summary}
   await $`git commit -m "${summary}
   
   
-Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`;
-  await $`git push fork HEAD:${remoteBranch}`;
+Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
+  await $`git push fork HEAD:${remoteBranch}`
 }
 }
 
 
 async function pushToNewBranch(summary: string) {
 async function pushToNewBranch(summary: string) {
-  console.log("Pushing to new branch...");
-  const branch = generateBranchName();
-  await $`git checkout -b ${branch}`;
-  await $`git add .`;
+  console.log("Pushing to new branch...")
+  const branch = generateBranchName()
+  await $`git checkout -b ${branch}`
+  await $`git add .`
   await $`git commit -m "${summary}
   await $`git commit -m "${summary}
   
   
-Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`;
-  await $`git push -u origin ${branch}`;
-  return branch;
+Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
+  await $`git push -u origin ${branch}`
+  return branch
 }
 }
 
 
-async function createPR(
-  base: string,
-  branch: string,
-  title: string,
-  body: string
-) {
-  console.log("Creating pull request...");
+async function createPR(base: string, branch: string, title: string, body: string) {
+  console.log("Creating pull request...")
   const pr = await octoRest.rest.pulls.create({
   const pr = await octoRest.rest.pulls.create({
     owner,
     owner,
     repo,
     repo,
@@ -343,41 +301,39 @@ async function createPR(
     base,
     base,
     title,
     title,
     body: buildComment(body),
     body: buildComment(body),
-  });
-  return pr.data.number;
+  })
+  return pr.data.number
 }
 }
 
 
 async function runOpencode(
 async function runOpencode(
   prompt: string,
   prompt: string,
   opts?: {
   opts?: {
-    share?: boolean;
-  }
+    share?: boolean
+  },
 ) {
 ) {
-  console.log("Running opencode...");
+  console.log("Running opencode...")
 
 
-  const promptPath = path.join(os.tmpdir(), "PROMPT");
-  await Bun.write(promptPath, prompt);
-  const ret = await $`cat ${promptPath} | opencode run -m ${
-    process.env.INPUT_MODEL
-  } ${opts?.share ? "--share" : ""}`;
+  const promptPath = path.join(os.tmpdir(), "PROMPT")
+  await Bun.write(promptPath, prompt)
+  const ret = await $`cat ${promptPath} | opencode run -m ${process.env.INPUT_MODEL} ${opts?.share ? "--share" : ""}`
   return {
   return {
     stdout: ret.stdout.toString().trim(),
     stdout: ret.stdout.toString().trim(),
     stderr: ret.stderr.toString().trim(),
     stderr: ret.stderr.toString().trim(),
-  };
+  }
 }
 }
 
 
 async function branchIsDirty() {
 async function branchIsDirty() {
-  console.log("Checking if branch is dirty...");
-  const ret = await $`git status --porcelain`;
-  return ret.stdout.toString().trim().length > 0;
+  console.log("Checking if branch is dirty...")
+  const ret = await $`git status --porcelain`
+  return ret.stdout.toString().trim().length > 0
 }
 }
 
 
 async function fetchRepo() {
 async function fetchRepo() {
-  return await octoRest.rest.repos.get({ owner, repo });
+  return await octoRest.rest.repos.get({ owner, repo })
 }
 }
 
 
 async function fetchIssue() {
 async function fetchIssue() {
-  console.log("Fetching prompt data for issue...");
+  console.log("Fetching prompt data for issue...")
   const issueResult = await octoGraph<IssueQueryResponse>(
   const issueResult = await octoGraph<IssueQueryResponse>(
     `
     `
 query($owner: String!, $repo: String!, $number: Int!) {
 query($owner: String!, $repo: String!, $number: Int!) {
@@ -408,22 +364,22 @@ query($owner: String!, $repo: String!, $number: Int!) {
       owner,
       owner,
       repo,
       repo,
       number: issueId,
       number: issueId,
-    }
-  );
+    },
+  )
 
 
-  const issue = issueResult.repository.issue;
-  if (!issue) throw new Error(`Issue #${issueId} not found`);
+  const issue = issueResult.repository.issue
+  if (!issue) throw new Error(`Issue #${issueId} not found`)
 
 
-  return issue;
+  return issue
 }
 }
 
 
 function buildPromptDataForIssue(issue: GitHubIssue) {
 function buildPromptDataForIssue(issue: GitHubIssue) {
   const comments = (issue.comments?.nodes || [])
   const comments = (issue.comments?.nodes || [])
     .filter((c) => {
     .filter((c) => {
-      const id = parseInt(c.databaseId);
-      return id !== commentId && id !== payload.comment.id;
+      const id = parseInt(c.databaseId)
+      return id !== commentId && id !== payload.comment.id
     })
     })
-    .map((c) => `  - ${c.author.login} at ${c.createdAt}: ${c.body}`);
+    .map((c) => `  - ${c.author.login} at ${c.createdAt}: ${c.body}`)
 
 
   return [
   return [
     "Here is the context for the issue:",
     "Here is the context for the issue:",
@@ -433,11 +389,11 @@ function buildPromptDataForIssue(issue: GitHubIssue) {
     `- Created At: ${issue.createdAt}`,
     `- Created At: ${issue.createdAt}`,
     `- State: ${issue.state}`,
     `- State: ${issue.state}`,
     ...(comments.length > 0 ? ["- Comments:", ...comments] : []),
     ...(comments.length > 0 ? ["- Comments:", ...comments] : []),
-  ].join("\n");
+  ].join("\n")
 }
 }
 
 
 async function fetchPR() {
 async function fetchPR() {
-  console.log("Fetching prompt data for PR...");
+  console.log("Fetching prompt data for PR...")
   const prResult = await octoGraph<PullRequestQueryResponse>(
   const prResult = await octoGraph<PullRequestQueryResponse>(
     `
     `
 query($owner: String!, $repo: String!, $number: Int!) {
 query($owner: String!, $repo: String!, $number: Int!) {
@@ -525,36 +481,32 @@ query($owner: String!, $repo: String!, $number: Int!) {
       owner,
       owner,
       repo,
       repo,
       number: issueId,
       number: issueId,
-    }
-  );
+    },
+  )
 
 
-  const pr = prResult.repository.pullRequest;
-  if (!pr) throw new Error(`PR #${issueId} not found`);
+  const pr = prResult.repository.pullRequest
+  if (!pr) throw new Error(`PR #${issueId} not found`)
 
 
-  return pr;
+  return pr
 }
 }
 
 
 function buildPromptDataForPR(pr: GitHubPullRequest) {
 function buildPromptDataForPR(pr: GitHubPullRequest) {
   const comments = (pr.comments?.nodes || [])
   const comments = (pr.comments?.nodes || [])
     .filter((c) => {
     .filter((c) => {
-      const id = parseInt(c.databaseId);
-      return id !== commentId && id !== payload.comment.id;
+      const id = parseInt(c.databaseId)
+      return id !== commentId && id !== payload.comment.id
     })
     })
-    .map((c) => `  - ${c.author.login} at ${c.createdAt}: ${c.body}`);
+    .map((c) => `  - ${c.author.login} at ${c.createdAt}: ${c.body}`)
 
 
-  const files = (pr.files.nodes || []).map(
-    (f) => `  - ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`
-  );
+  const files = (pr.files.nodes || []).map((f) => `  - ${f.path} (${f.changeType}) +${f.additions}/-${f.deletions}`)
   const reviewData = (pr.reviews.nodes || []).map((r) => {
   const reviewData = (pr.reviews.nodes || []).map((r) => {
-    const comments = (r.comments.nodes || []).map(
-      (c) => `      - ${c.path}:${c.line ?? "?"}: ${c.body}`
-    );
+    const comments = (r.comments.nodes || []).map((c) => `      - ${c.path}:${c.line ?? "?"}: ${c.body}`)
     return [
     return [
       `  - ${r.author.login} at ${r.submittedAt}:`,
       `  - ${r.author.login} at ${r.submittedAt}:`,
       `    - Review body: ${r.body}`,
       `    - Review body: ${r.body}`,
       ...(comments.length > 0 ? ["    - Comments:", ...comments] : []),
       ...(comments.length > 0 ? ["    - Comments:", ...comments] : []),
-    ];
-  });
+    ]
+  })
 
 
   return [
   return [
     "Here is the context for the pull request:",
     "Here is the context for the pull request:",
@@ -572,11 +524,11 @@ function buildPromptDataForPR(pr: GitHubPullRequest) {
     ...(comments.length > 0 ? ["- Comments:", ...comments] : []),
     ...(comments.length > 0 ? ["- Comments:", ...comments] : []),
     ...(files.length > 0 ? ["- Changed files:", ...files] : []),
     ...(files.length > 0 ? ["- Changed files:", ...files] : []),
     ...(reviewData.length > 0 ? ["- Reviews:", ...reviewData] : []),
     ...(reviewData.length > 0 ? ["- Reviews:", ...reviewData] : []),
-  ].join("\n");
+  ].join("\n")
 }
 }
 
 
 async function revokeAppToken() {
 async function revokeAppToken() {
-  if (!appToken) return;
+  if (!appToken) return
 
 
   await fetch("https://api.github.com/installation/token", {
   await fetch("https://api.github.com/installation/token", {
     method: "DELETE",
     method: "DELETE",
@@ -585,5 +537,5 @@ async function revokeAppToken() {
       Accept: "application/vnd.github+json",
       Accept: "application/vnd.github+json",
       "X-GitHub-Api-Version": "2022-11-28",
       "X-GitHub-Api-Version": "2022-11-28",
     },
     },
-  });
+  })
 }
 }