mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-09 18:34:21 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44bf46209e | ||
|
|
b04faf0a3a | ||
|
|
90084ce43d | ||
|
|
9345830c8a | ||
|
|
5307100f89 | ||
|
|
a58e607c5f | ||
|
|
cd04c44517 | ||
|
|
88711db796 | ||
|
|
9fec8df7d0 | ||
|
|
603a3e3c71 | ||
|
|
e14de7a211 | ||
|
|
004cfe7e8e | ||
|
|
58705a1352 | ||
|
|
82de14371d |
@@ -1,9 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
||||
59
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
59
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Bug report
|
||||
description: Report an issue that should be fixed
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Describe the bug you encountered
|
||||
placeholder: What happened?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: opencode-version
|
||||
attributes:
|
||||
label: OpenCode version
|
||||
description: What version of OpenCode are you using?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: How can we reproduce this issue?
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: screenshot-or-link
|
||||
attributes:
|
||||
label: Screenshot and/or share link
|
||||
description: Run `/share` to get a share link, or attach a screenshot
|
||||
placeholder: Paste link or drag and drop screenshot here
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: what OS are you using?
|
||||
placeholder: e.g., macOS 26.0.1, Ubuntu 22.04, Windows 11
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: terminal
|
||||
attributes:
|
||||
label: Terminal
|
||||
description: what terminal are you using?
|
||||
placeholder: e.g., iTerm2, Ghostty, Alacritty, Windows Terminal
|
||||
validations:
|
||||
required: false
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💬 Discord Community
|
||||
url: https://discord.gg/opencode
|
||||
about: For quick questions or real-time discussion. Note that issues are searchable and help others with the same question.
|
||||
20
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
20
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: 🚀 Feature Request
|
||||
description: Suggest an idea, feature, or enhancement
|
||||
labels: [discussion]
|
||||
title: "[FEATURE]:"
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: verified
|
||||
attributes:
|
||||
label: Feature hasn't been suggested before.
|
||||
options:
|
||||
- label: I have verified this feature I'm about to request hasn't been suggested before.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the enhancement you want to request
|
||||
description: What do you want to change or add? What are the benefits of implementing this? Try to be detailed so we can understand your request better :)
|
||||
validations:
|
||||
required: true
|
||||
11
.github/ISSUE_TEMPLATE/question.yml
vendored
11
.github/ISSUE_TEMPLATE/question.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Question
|
||||
description: Ask a question
|
||||
labels: ["question"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: What's your question?
|
||||
validations:
|
||||
required: true
|
||||
22
.github/actions/setup-bun/action.yml
vendored
22
.github/actions/setup-bun/action.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: "Setup Bun"
|
||||
description: "Setup Bun with caching and install dependencies"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: package.json
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('package.json') }}-${{ hashFiles('bun.lockb', 'bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-${{ hashFiles('package.json') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
shell: bash
|
||||
71
.github/publish-python-sdk.yml
vendored
71
.github/publish-python-sdk.yml
vendored
@@ -1,71 +0,0 @@
|
||||
#
|
||||
# This file is intentionally in the wrong dir, will move and add later....
|
||||
#
|
||||
|
||||
# name: publish-python-sdk
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types: [published]
|
||||
# workflow_dispatch:
|
||||
|
||||
# jobs:
|
||||
# publish:
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Setup Bun
|
||||
# uses: oven-sh/setup-bun@v1
|
||||
# with:
|
||||
# bun-version: 1.2.21
|
||||
|
||||
# - name: Install dependencies (JS/Bun)
|
||||
# run: bun install
|
||||
|
||||
# - name: Install uv
|
||||
# shell: bash
|
||||
# run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# - name: Generate Python SDK from OpenAPI (CLI)
|
||||
# shell: bash
|
||||
# run: |
|
||||
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/generate.py --source cli
|
||||
|
||||
# - name: Sync Python dependencies
|
||||
# shell: bash
|
||||
# run: |
|
||||
# ~/.local/bin/uv sync --dev --project packages/sdk/python
|
||||
|
||||
# - name: Set version from release tag
|
||||
# shell: bash
|
||||
# run: |
|
||||
# TAG="${GITHUB_REF_NAME:-}"
|
||||
# if [ -z "$TAG" ]; then
|
||||
# TAG="$(git describe --tags --abbrev=0 || echo 0.0.0)"
|
||||
# fi
|
||||
# echo "Using version: $TAG"
|
||||
# VERSION="$TAG" ~/.local/bin/uv run --project packages/sdk/python python - <<'PY'
|
||||
# import os, re, pathlib
|
||||
# root = pathlib.Path('packages/sdk/python')
|
||||
# pt = (root / 'pyproject.toml').read_text()
|
||||
# version = os.environ.get('VERSION','0.0.0').lstrip('v')
|
||||
# pt = re.sub(r'(?m)^(version\s*=\s*")[^"]+("\s*)$', f"\\1{version}\\2", pt)
|
||||
# (root / 'pyproject.toml').write_text(pt)
|
||||
# # Also update generator config override for consistency
|
||||
# cfgp = root / 'openapi-python-client.yaml'
|
||||
# if cfgp.exists():
|
||||
# cfg = cfgp.read_text()
|
||||
# cfg = re.sub(r'(?m)^(package_version_override:\s*)\S+$', f"\\1{version}", cfg)
|
||||
# cfgp.write_text(cfg)
|
||||
# PY
|
||||
|
||||
# - name: Build and publish to PyPI
|
||||
# env:
|
||||
# PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
# shell: bash
|
||||
# run: |
|
||||
# ~/.local/bin/uv run --project packages/sdk/python python packages/sdk/python/scripts/publish.py
|
||||
63
.github/workflows/auto-label-tui.yml
vendored
63
.github/workflows/auto-label-tui.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Auto-label TUI Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Auto-label and assign issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title;
|
||||
const description = issue.body || '';
|
||||
|
||||
// Check for "opencode web" keyword
|
||||
const webPattern = /(opencode web)/i;
|
||||
const isWebRelated = webPattern.test(title) || webPattern.test(description);
|
||||
|
||||
// Check for version patterns like v1.0.x or 1.0.x
|
||||
const versionPattern = /[v]?1\.0\./i;
|
||||
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
||||
|
||||
// Check for "nix" keyword
|
||||
const nixPattern = /\bnix\b/i;
|
||||
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
|
||||
|
||||
const labels = [];
|
||||
|
||||
if (isWebRelated) {
|
||||
labels.push('web');
|
||||
|
||||
// Assign to adamdotdevin
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: ['adamdotdevin']
|
||||
});
|
||||
} else if (isVersionRelated) {
|
||||
// Only add opentui if NOT web-related
|
||||
labels.push('opentui');
|
||||
}
|
||||
|
||||
if (isNixRelated) {
|
||||
labels.push('nix');
|
||||
}
|
||||
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
37
.github/workflows/build.yml
vendored
Normal file
37
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.23.2"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- run: go mod download
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: build --snapshot --clean
|
||||
29
.github/workflows/deploy.yml
vendored
29
.github/workflows/deploy.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- production
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
|
||||
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
|
||||
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||
61
.github/workflows/duplicate-issues.yml
vendored
61
.github/workflows/duplicate-issues.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Duplicate Issue Detection
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-duplicates:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Check for duplicate issues
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
"bash": {
|
||||
"gh issue*": "allow",
|
||||
"*": "deny"
|
||||
},
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
|
||||
|
||||
Issue number:
|
||||
${{ github.event.issue.number }}
|
||||
|
||||
Lookup this issue and search through existing issues (excluding #${{ github.event.issue.number }}) in this repository to find any potential duplicates of this new issue.
|
||||
Consider:
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
3. Related functionality or components
|
||||
4. Similar feature requests
|
||||
|
||||
If you find any potential duplicates, please comment on the new issue with:
|
||||
- A brief explanation of why it might be a duplicate
|
||||
- Links to the potentially duplicate issues
|
||||
- A suggestion to check those issues first
|
||||
|
||||
Use this format for the comment:
|
||||
'This issue might be a duplicate of existing issues. Please check:
|
||||
- #[issue_number]: [brief description of similarity]
|
||||
|
||||
Feel free to ignore if none of these address your specific case.'
|
||||
|
||||
Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, please add a comment mentioning the pinned keybinds issue #4997:
|
||||
'For keybind-related issues, please also check our pinned keybinds documentation: #4997'
|
||||
|
||||
If no clear duplicates are found, do not comment."
|
||||
32
.github/workflows/format.yml
vendored
32
.github/workflows/format.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
format:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
./script/format.ts
|
||||
env:
|
||||
CI: true
|
||||
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
14
.github/workflows/notify-discord.yml
vendored
14
.github/workflows/notify-discord.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: discord
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published] # fires only when a release is published
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Send nicely-formatted embed to Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
33
.github/workflows/opencode.yml
vendored
33
.github/workflows/opencode.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, ' /oc') ||
|
||||
startsWith(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, ' /opencode') ||
|
||||
startsWith(github.event.comment.body, '/opencode')
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
with:
|
||||
model: opencode/claude-haiku-4-5
|
||||
30
.github/workflows/publish-github-action.yml
vendored
30
.github/workflows/publish-github-action.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: publish-github-action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "github-v*.*.*"
|
||||
- "!github-v1"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./script/publish
|
||||
working-directory: ./github
|
||||
37
.github/workflows/publish-vscode.yml
vendored
37
.github/workflows/publish-vscode.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: publish-vscode
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "vscode-v*.*.*"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- run: git fetch --force --tags
|
||||
- run: bun install -g @vscode/vsce
|
||||
|
||||
- name: Install extension dependencies
|
||||
run: bun install
|
||||
working-directory: ./sdks/vscode
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./script/publish
|
||||
working-directory: ./sdks/vscode
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }}
|
||||
182
.github/workflows/publish.yml
vendored
182
.github/workflows/publish.yml
vendored
@@ -1,182 +0,0 @@
|
||||
name: publish
|
||||
run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: "Bump major, minor, or patch"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
version:
|
||||
description: "Override version (optional)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.24.0"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y pacman-package-manager
|
||||
- name: Setup SSH for AUR
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||
|
||||
- name: Install OpenCode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Setup npm auth
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./script/publish.ts
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_VERSION: ${{ inputs.version }}
|
||||
OPENCODE_CHANNEL: latest
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
|
||||
publish-tauri:
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- host: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- host: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
- host: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
- host: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.settings.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
with:
|
||||
keychain: build
|
||||
p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: Verify Certificate
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: |
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported."
|
||||
|
||||
- name: Setup Apple API Key
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
run: |
|
||||
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: startsWith(matrix.settings.host, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.settings.target }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: packages/tauri/src-tauri
|
||||
shared-key: ${{ matrix.settings.target }}
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
cd packages/tauri
|
||||
bun ./scripts/prepare.ts
|
||||
env:
|
||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||
OPENCODE_VERSION: ${{ inputs.version }}
|
||||
OPENCODE_CHANNEL: latest
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
|
||||
if: startsWith(matrix.settings.host, 'ubuntu')
|
||||
|
||||
- name: Build and upload artifacts
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
|
||||
with:
|
||||
projectPath: packages/tauri
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }}
|
||||
updaterJsonPreferNsis: true
|
||||
# releaseId: TODO
|
||||
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.23.2"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- run: go mod download
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
79
.github/workflows/review.yml
vendored
79
.github/workflows/review.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Guidelines Check
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-guidelines:
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/review') &&
|
||||
contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Get PR details
|
||||
id: pr-details
|
||||
run: |
|
||||
gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json
|
||||
echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT
|
||||
echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check PR guidelines compliance
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
|
||||
PR_TITLE: ${{ steps.pr-details.outputs.title }}
|
||||
run: |
|
||||
PR_BODY=$(jq -r .body pr_data.json)
|
||||
opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}'
|
||||
|
||||
<pr-number>
|
||||
${{ steps.pr-number.outputs.number }}
|
||||
</pr-number>
|
||||
|
||||
<pr-description>
|
||||
$PR_BODY
|
||||
</pr-description>
|
||||
|
||||
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||
|
||||
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
|
||||
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
|
||||
|
||||
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||
|
||||
Command MUST be like this.
|
||||
\`\`\`
|
||||
gh api \
|
||||
--method POST \
|
||||
-H \"Accept: application/vnd.github+json\" \
|
||||
-H \"X-GitHub-Api-Version: 2022-11-28\" \
|
||||
/repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \
|
||||
-f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT'
|
||||
\`\`\`
|
||||
|
||||
Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!."
|
||||
39
.github/workflows/sdk.yml
vendored
39
.github/workflows/sdk.yml
vendored
@@ -1,39 +0,0 @@
|
||||
name: sdk
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
format:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
bun ./packages/sdk/js/script/build.ts
|
||||
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add -A
|
||||
git commit -m "chore: regen sdk"
|
||||
git push --no-verify
|
||||
env:
|
||||
CI: true
|
||||
38
.github/workflows/snapshot.yml
vendored
38
.github/workflows/snapshot.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: snapshot
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- test-bedrock
|
||||
- v0
|
||||
- otui-diffs
|
||||
- snapshot-*
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.24.0"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
./script/publish.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
32
.github/workflows/stats.yml
vendored
32
.github/workflows/stats.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run stats script
|
||||
run: bun script/stats.ts
|
||||
|
||||
- name: Commit stats
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add STATS.md
|
||||
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
|
||||
git push
|
||||
env:
|
||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||
34
.github/workflows/sync-zed-extension.yml
vendored
34
.github/workflows/sync-zed-extension.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: "sync-zed-extension"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
zed:
|
||||
name: Release Zed Extension
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Get version tag
|
||||
id: get_tag
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "release" ]; then
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
else
|
||||
TAG=$(git tag --list 'v[0-9]*.*' --sort=-version:refname | head -n 1)
|
||||
fi
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "Using tag: ${TAG}"
|
||||
|
||||
- name: Sync Zed extension
|
||||
run: |
|
||||
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@@ -1,30 +0,0 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- production
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- production
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: run
|
||||
run: |
|
||||
git config --global user.email "bot@opencode.ai"
|
||||
git config --global user.name "opencode"
|
||||
bun turbo typecheck
|
||||
bun turbo test
|
||||
env:
|
||||
CI: true
|
||||
19
.github/workflows/typecheck.yml
vendored
19
.github/workflows/typecheck.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: typecheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
||||
102
.github/workflows/update-nix-hashes.yml
vendored
102
.github/workflows/update-nix-hashes.yml
vendored
@@ -1,102 +0,0 @@
|
||||
name: Update Nix Hashes
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
pull_request:
|
||||
paths:
|
||||
- "bun.lock"
|
||||
- "package.json"
|
||||
- "packages/*/package.json"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
SYSTEM: x86_64-linux
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
|
||||
- name: Setup Nix
|
||||
uses: DeterminateSystems/nix-installer-action@v20
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Github Action"
|
||||
|
||||
- name: Update flake.lock
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "📦 Updating flake.lock..."
|
||||
nix flake update
|
||||
echo "✅ flake.lock updated successfully"
|
||||
|
||||
- name: Update node_modules hash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "🔄 Updating node_modules hash..."
|
||||
nix/scripts/update-hashes.sh
|
||||
echo "✅ node_modules hash updated successfully"
|
||||
|
||||
- name: Commit hash changes
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "🔍 Checking for changes in tracked Nix files..."
|
||||
|
||||
summarize() {
|
||||
local status="$1"
|
||||
{
|
||||
echo "### Nix Hash Update"
|
||||
echo ""
|
||||
echo "- ref: ${GITHUB_REF_NAME}"
|
||||
echo "- status: ${status}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
|
||||
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
|
||||
STATUS="$(git status --short -- "${FILES[@]}" || true)"
|
||||
if [ -z "$STATUS" ]; then
|
||||
echo "✅ No changes detected. Hashes are already up to date."
|
||||
summarize "no changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📝 Changes detected:"
|
||||
echo "$STATUS"
|
||||
echo "🔗 Staging files..."
|
||||
git add "${FILES[@]}"
|
||||
echo "💾 Committing changes..."
|
||||
git commit -m "Update Nix flake.lock and hashes"
|
||||
echo "✅ Changes committed"
|
||||
|
||||
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
|
||||
echo "🌳 Pulling latest from branch: $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "🚀 Pushing changes to branch: $BRANCH"
|
||||
git push origin HEAD:"$BRANCH"
|
||||
echo "✅ Changes pushed successfully"
|
||||
|
||||
summarize "committed $(git rev-parse --short HEAD)"
|
||||
61
.gitignore
vendored
61
.gitignore
vendored
@@ -1,20 +1,45 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# IDE specific files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
node_modules
|
||||
.worktrees
|
||||
.sst
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
*.log
|
||||
|
||||
# Binary output directory
|
||||
/bin/
|
||||
/dist/
|
||||
|
||||
# Local environment variables
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
*~
|
||||
playground
|
||||
tmp
|
||||
dist
|
||||
.turbo
|
||||
**/.serena
|
||||
.serena/
|
||||
/result
|
||||
refs
|
||||
Session.vim
|
||||
opencode.json
|
||||
a.out
|
||||
target
|
||||
.env.local
|
||||
|
||||
.opencode/
|
||||
|
||||
|
||||
76
.goreleaser.yml
Normal file
76
.goreleaser.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
version: 2
|
||||
project_name: opencode
|
||||
before:
|
||||
hooks:
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X github.com/opencode-ai/opencode/internal/version.Version={{.Version}}
|
||||
main: ./main.go
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
name_template: >-
|
||||
opencode-
|
||||
{{- if eq .Os "darwin" }}mac-
|
||||
{{- else if eq .Os "windows" }}windows-
|
||||
{{- else if eq .Os "linux" }}linux-{{end}}
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "#86" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
name_template: "0.0.0-{{ .Timestamp }}"
|
||||
aurs:
|
||||
- name: opencode
|
||||
homepage: "https://github.com/opencode-ai/opencode"
|
||||
description: "terminal based agent that can build anything"
|
||||
maintainers:
|
||||
- "kujtimiihoxha <kujtimii.h@gmail.com>"
|
||||
license: "MIT"
|
||||
private_key: "{{ .Env.AUR_KEY }}"
|
||||
git_url: "ssh://aur@aur.archlinux.org/opencode-ai.git"
|
||||
provides:
|
||||
- opencode
|
||||
conflicts:
|
||||
- opencode
|
||||
package: |-
|
||||
install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"
|
||||
brews:
|
||||
- repository:
|
||||
owner: opencode-ai
|
||||
name: homebrew-tap
|
||||
nfpms:
|
||||
- maintainer: kujtimiihoxha
|
||||
description: terminal based agent that can build anything
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
file_name_template: >-
|
||||
{{ .ProjectName }}-
|
||||
{{- if eq .Os "darwin" }}mac
|
||||
{{- else }}{{ .Os }}{{ end }}-{{ .Arch }}
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^doc:"
|
||||
- "^test:"
|
||||
- "^ci:"
|
||||
- "^ignore:"
|
||||
- "^example:"
|
||||
- "^wip:"
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Check if bun version matches package.json
|
||||
EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
|
||||
CURRENT_VERSION=$(bun --version)
|
||||
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
|
||||
exit 1
|
||||
fi
|
||||
bun typecheck
|
||||
8
.opencode.json
Normal file
8
.opencode.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "./opencode-schema.json",
|
||||
"lsp": {
|
||||
"gopls": {
|
||||
"command": "gopls"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
description: ALWAYS use this when writing docs
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer
|
||||
|
||||
You are not verbose
|
||||
|
||||
The title of the page should be a word or a 2-3 word phrase
|
||||
|
||||
The description should be one short line, should not start with "The", should
|
||||
avoid repeating the title of the page, should be 5-10 words long
|
||||
|
||||
Chunks of text should not be more than 2 sentences long
|
||||
|
||||
Each section is separated by a divider of 3 dashes
|
||||
|
||||
The section titles are short with only the first letter of the word capitalized
|
||||
|
||||
The section titles are in the imperative mood
|
||||
|
||||
The section titles should not repeat the term used in the page title, for
|
||||
example, if the page title is "Models", avoid using a section title like "Add
|
||||
new models". This might be unavoidable in some cases, but try to avoid it.
|
||||
|
||||
Check out the /packages/web/src/content/docs/docs/index.mdx as an example.
|
||||
|
||||
For JS or TS code snippets remove trailing semicolons and any trailing commas
|
||||
that might not be needed.
|
||||
|
||||
If you are making a commit prefix the commit message with `docs:`
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: Use this agent when you are asked to commit and push code changes to a git repository.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You commit and push to git
|
||||
|
||||
Commit messages should be brief since they are used to generate release notes.
|
||||
|
||||
Messages should say WHY the change was made and not WHAT was changed.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
description: git commit and push
|
||||
---
|
||||
|
||||
commit and push
|
||||
|
||||
make sure it includes a prefix like
|
||||
docs:
|
||||
tui:
|
||||
core:
|
||||
ci:
|
||||
ignore:
|
||||
wip:
|
||||
|
||||
For anything in the packages/web use the docs: prefix.
|
||||
|
||||
For anything in the packages/app use the ignore: prefix.
|
||||
|
||||
prefer to explain WHY something was done from an end user perspective instead of
|
||||
WHAT was done.
|
||||
|
||||
do not do generic messages like "improved agent experience" be very specific
|
||||
about what user facing changes were made
|
||||
|
||||
if there are changes do a git pull --rebase
|
||||
if there are conflicts DO NOT FIX THEM. notify me and I will fix them
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
description: "find issue(s) on github"
|
||||
model: opencode/claude-haiku-4-5
|
||||
---
|
||||
|
||||
Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
Consider:
|
||||
|
||||
1. Similar titles or descriptions
|
||||
2. Same error messages or symptoms
|
||||
3. Related functionality or components
|
||||
4. Similar feature requests
|
||||
|
||||
Please list any matching issues with:
|
||||
|
||||
- Issue number and title
|
||||
- Brief explanation of why it matches the query
|
||||
- Link to the issue
|
||||
|
||||
If no clear matches are found, say so.
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
description: Remove AI code slop
|
||||
---
|
||||
|
||||
Check the diff against dev, and remove all AI generated slop introduced in this branch.
|
||||
|
||||
This includes:
|
||||
|
||||
- Extra comments that a human wouldn't add or is inconsistent with the rest of the file
|
||||
- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted / validated codepaths)
|
||||
- Casts to any to get around type issues
|
||||
- Any other style that is inconsistent with the file
|
||||
- Unnecessary emoji usage
|
||||
|
||||
Report at the end with only a 1-3 sentence summary of what you changed
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
description: spellcheck all markdown file changes
|
||||
---
|
||||
|
||||
Look at all the unstaged changes to markdown (.md, .mdx) files, pull out the lines that have changed, and check for spelling and grammar errors.
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["opencode-openai-codex-auth"],
|
||||
// "enterprise": {
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"instructions": ["STYLE_GUIDE.md"],
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {
|
||||
// "baseURL": "http://localhost:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
"mcp": {
|
||||
"exa": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.exa.ai/mcp",
|
||||
},
|
||||
"morph": {
|
||||
"type": "local",
|
||||
"command": ["bunx", "@morphllm/morphmcp"],
|
||||
"environment": {
|
||||
"ENABLED_TOOLS": "warp_grep",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"nord0": "#2E3440",
|
||||
"nord1": "#3B4252",
|
||||
"nord2": "#434C5E",
|
||||
"nord3": "#4C566A",
|
||||
"nord4": "#D8DEE9",
|
||||
"nord5": "#E5E9F0",
|
||||
"nord6": "#ECEFF4",
|
||||
"nord7": "#8FBCBB",
|
||||
"nord8": "#88C0D0",
|
||||
"nord9": "#81A1C1",
|
||||
"nord10": "#5E81AC",
|
||||
"nord11": "#BF616A",
|
||||
"nord12": "#D08770",
|
||||
"nord13": "#EBCB8B",
|
||||
"nord14": "#A3BE8C",
|
||||
"nord15": "#B48EAD"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"error": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"success": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"info": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"text": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "nord3",
|
||||
"light": "nord1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "nord0",
|
||||
"light": "nord6"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "nord1",
|
||||
"light": "nord4"
|
||||
},
|
||||
"border": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "nord3",
|
||||
"light": "nord2"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "nord13",
|
||||
"light": "nord13"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "nord8",
|
||||
"light": "nord8"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "nord15",
|
||||
"light": "nord15"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
sst-env.d.ts
|
||||
11
.vscode/launch.example.json
vendored
11
.vscode/launch.example.json
vendored
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "opencode (attach)",
|
||||
"url": "ws://localhost:6499/"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
.vscode/settings.example.json
vendored
5
.vscode/settings.example.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"oven.bun-vscode"
|
||||
]
|
||||
}
|
||||
34
AGENTS.md
34
AGENTS.md
@@ -1,34 +0,0 @@
|
||||
## Debugging
|
||||
|
||||
- To test opencode in the `packages/opencode` directory you can run `bun dev`
|
||||
|
||||
## Tool Calling
|
||||
|
||||
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE. Here is an example illustrating how to execute 3 parallel file reads in this chat environment:
|
||||
|
||||
json
|
||||
{
|
||||
"recipient_name": "multi_tool_use.parallel",
|
||||
"parameters": {
|
||||
"tool_uses": [
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.tsx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"recipient_name": "functions.read",
|
||||
"parameters": {
|
||||
"filePath": "path/to/file.md"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
102
CONTRIBUTING.md
102
CONTRIBUTING.md
@@ -1,102 +0,0 @@
|
||||
# Contributing to OpenCode
|
||||
|
||||
We want to make it easy for you to contribute to OpenCode. Here are the most common type of changes that get merged:
|
||||
|
||||
- Bug fixes
|
||||
- Additional LSPs / Formatters
|
||||
- Improvements to LLM performance
|
||||
- Support for new providers
|
||||
- Fixes for environment-specific quirks
|
||||
- Missing standard behavior
|
||||
- Documentation improvements
|
||||
|
||||
However, any UI or core product feature must go through a design review with the core team before implementation.
|
||||
|
||||
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
|
||||
|
||||
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
|
||||
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
|
||||
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
|
||||
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
|
||||
|
||||
> [!NOTE]
|
||||
> PRs that ignore these guardrails will likely be closed.
|
||||
|
||||
Want to take on an issue? Leave a comment and a maintainer may assign it to you unless it is something we are already working on.
|
||||
|
||||
## Developing OpenCode
|
||||
|
||||
- Requirements: Bun 1.3+
|
||||
- Install dependencies and start the dev server from the repo root:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun dev
|
||||
```
|
||||
|
||||
- Core pieces:
|
||||
- `packages/opencode`: OpenCode core business logic & server.
|
||||
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
|
||||
- `packages/plugin`: Source for `@opencode-ai/plugin`
|
||||
|
||||
> [!NOTE]
|
||||
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
|
||||
|
||||
Please try to follow the [style guide](./STYLE_GUIDE.md)
|
||||
|
||||
### Setting up a Debugger
|
||||
|
||||
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
|
||||
|
||||
The most reliable way to debug OpenCode is to run it manually in a terminal via `bun run --inspect=<url> dev ...` and attach
|
||||
your debugger via that URL. Other methods can result in breakpoints being mapped incorrectly, at least in VSCode (YMMV).
|
||||
|
||||
Caveats:
|
||||
|
||||
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
|
||||
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
|
||||
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
|
||||
is triggered.
|
||||
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
|
||||
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
|
||||
|
||||
Other tips and tricks:
|
||||
|
||||
- You might want to use `--inspect-wait` or `--inspect-brk` instead of `--inspect`, depending on your workflow
|
||||
- Specifying `--inspect=ws://localhost:6499/` on every invocation can be tiresome, you may want to `export BUN_OPTIONS=--inspect=ws://localhost:6499/` instead
|
||||
|
||||
#### VSCode Setup
|
||||
|
||||
If you use VSCode, you can use our example configurations [.vscode/settings.example.json](.vscode/settings.example.json) and [.vscode/launch.example.json](.vscode/launch.example.json).
|
||||
|
||||
Some debug methods that can be problematic:
|
||||
|
||||
- Debug configurations with `"request": "launch"` can have breakpoints incorrectly mapped and thus unusable
|
||||
- The same problem arises when running OpenCode in the VSCode `JavaScript Debug Terminal`
|
||||
|
||||
With that said, you may want to try these methods, as they might work for you.
|
||||
|
||||
## Pull Request Expectations
|
||||
|
||||
- Try to keep pull requests small and focused.
|
||||
- Link relevant issue(s) in the description
|
||||
- Explain the issue and why your change fixes it
|
||||
- Avoid having verbose LLM generated PR descriptions
|
||||
- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
|
||||
|
||||
### Style Preferences
|
||||
|
||||
These are not strictly enforced, they are just general guidelines:
|
||||
|
||||
- **Functions:** Keep logic within a single function unless breaking it out adds clear reuse or composition benefits.
|
||||
- **Destructuring:** Do not do unnecessary destructuring of variables.
|
||||
- **Control flow:** Avoid `else` statements.
|
||||
- **Error handling:** Prefer `.catch(...)` instead of `try`/`catch` when possible.
|
||||
- **Types:** Reach for precise types and avoid `any`.
|
||||
- **Variables:** Stick to immutable patterns and avoid `let`.
|
||||
- **Naming:** Choose concise single-word identifiers when they remain descriptive.
|
||||
- **Runtime APIs:** Use Bun helpers such as `Bun.file()` when they fit the use case.
|
||||
|
||||
## Feature Requests
|
||||
|
||||
For net-new functionality, start with a design conversation. Open an issue describing the problem, your proposed approach (optional), and why it belongs in OpenCode. The core team will help decide whether it should move forward; please wait for that approval instead of opening a feature PR directly.
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 opencode
|
||||
Copyright (c) 2025 Kujtim Hoxha
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
567
README.md
567
README.md
@@ -1,101 +1,526 @@
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai">
|
||||
<picture>
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">The AI coding agent built for the terminal.</p>
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
# ⌬ OpenCode
|
||||
|
||||
[](https://opencode.ai)
|
||||
> **⚠️ Early Development Notice:** This project is in early development and is not yet ready for production use. Features may change, break, or be incomplete. Use at your own risk.
|
||||
|
||||
---
|
||||
A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal.
|
||||
|
||||
### Installation
|
||||
## Overview
|
||||
|
||||
OpenCode is a Go-based CLI application that brings AI assistance to your terminal. It provides a TUI (Terminal User Interface) for interacting with various AI models to help with coding tasks, debugging, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive TUI**: Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) for a smooth terminal experience
|
||||
- **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, Azure OpenAI, and OpenRouter
|
||||
- **Session Management**: Save and manage multiple conversation sessions
|
||||
- **Tool Integration**: AI can execute commands, search files, and modify code
|
||||
- **Vim-like Editor**: Integrated editor with text input capabilities
|
||||
- **Persistent Storage**: SQLite database for storing conversations and sessions
|
||||
- **LSP Integration**: Language Server Protocol support for code intelligence
|
||||
- **File Change Tracking**: Track and visualize file changes during sessions
|
||||
- **External Editor Support**: Open your preferred editor for composing messages
|
||||
|
||||
## Installation
|
||||
|
||||
### Using the Install Script
|
||||
|
||||
```bash
|
||||
# YOLO
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
# Install the latest version
|
||||
curl -fsSL https://raw.githubusercontent.com/opencode-ai/opencode/refs/heads/main/install | bash
|
||||
|
||||
# Package managers
|
||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||
scoop bucket add extras; scoop install extras/opencode # Windows
|
||||
choco install opencode # Windows
|
||||
brew install opencode # macOS and Linux
|
||||
paru -S opencode-bin # Arch Linux
|
||||
mise use --pin -g ubi:sst/opencode # Any OS
|
||||
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
||||
# Install a specific version
|
||||
curl -fsSL https://raw.githubusercontent.com/opencode-ai/opencode/refs/heads/main/install | VERSION=0.1.0 bash
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Remove versions older than 0.1.x before installing.
|
||||
|
||||
#### Installation Directory
|
||||
|
||||
The install script respects the following priority order for the installation path:
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
||||
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
||||
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
|
||||
4. `$HOME/.opencode/bin` - Default fallback
|
||||
### Using Homebrew (macOS and Linux)
|
||||
|
||||
```bash
|
||||
# Examples
|
||||
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
brew install opencode-ai/tap/opencode
|
||||
```
|
||||
|
||||
### Agents
|
||||
### Using Go
|
||||
|
||||
OpenCode includes two built-in agents you can switch between,
|
||||
you can switch between these using the `Tab` key.
|
||||
```bash
|
||||
go install github.com/opencode-ai/opencode@latest
|
||||
```
|
||||
|
||||
- **build** - Default, full access agent for development work
|
||||
- **plan** - Read-only agent for analysis and code exploration
|
||||
- Denies file edits by default
|
||||
- Asks permission before running bash commands
|
||||
- Ideal for exploring unfamiliar codebases or planning changes
|
||||
## Configuration
|
||||
|
||||
Also, included is a **general** subagent for complex searches and multi-step tasks.
|
||||
This is used internally and can be invoked using `@general` in messages.
|
||||
OpenCode looks for configuration in the following locations:
|
||||
|
||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||
- `$HOME/.opencode.json`
|
||||
- `$XDG_CONFIG_HOME/opencode/.opencode.json`
|
||||
- `./.opencode.json` (local directory)
|
||||
|
||||
### Documentation
|
||||
### Auto Compact Feature
|
||||
|
||||
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
|
||||
OpenCode includes an auto compact feature that automatically summarizes your conversation when it approaches the model's context window limit. When enabled (default setting), this feature:
|
||||
|
||||
### Contributing
|
||||
- Monitors token usage during your conversation
|
||||
- Automatically triggers summarization when usage reaches 95% of the model's context window
|
||||
- Creates a new session with the summary, allowing you to continue your work without losing context
|
||||
- Helps prevent "out of context" errors that can occur with long conversations
|
||||
|
||||
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
|
||||
You can enable or disable this feature in your configuration file:
|
||||
|
||||
### Building on OpenCode
|
||||
```json
|
||||
{
|
||||
"autoCompact": true // default is true
|
||||
}
|
||||
```
|
||||
|
||||
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway.
|
||||
### Environment Variables
|
||||
|
||||
### FAQ
|
||||
You can configure OpenCode using environment variables:
|
||||
|
||||
#### How is this different than Claude Code?
|
||||
| Environment Variable | Purpose |
|
||||
| -------------------------- | ------------------------------------------------------ |
|
||||
| `ANTHROPIC_API_KEY` | For Claude models |
|
||||
| `OPENAI_API_KEY` | For OpenAI models |
|
||||
| `GEMINI_API_KEY` | For Google Gemini models |
|
||||
| `GROQ_API_KEY` | For Groq models |
|
||||
| `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) |
|
||||
| `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) |
|
||||
| `AWS_REGION` | For AWS Bedrock (Claude) |
|
||||
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
|
||||
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
|
||||
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
|
||||
|
||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
### Configuration File Structure
|
||||
|
||||
- 100% open source
|
||||
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||
- Out of the box LSP support
|
||||
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"directory": ".opencode"
|
||||
},
|
||||
"providers": {
|
||||
"openai": {
|
||||
"apiKey": "your-api-key",
|
||||
"disabled": false
|
||||
},
|
||||
"anthropic": {
|
||||
"apiKey": "your-api-key",
|
||||
"disabled": false
|
||||
},
|
||||
"groq": {
|
||||
"apiKey": "your-api-key",
|
||||
"disabled": false
|
||||
},
|
||||
"openrouter": {
|
||||
"apiKey": "your-api-key",
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"coder": {
|
||||
"model": "claude-3.7-sonnet",
|
||||
"maxTokens": 5000
|
||||
},
|
||||
"task": {
|
||||
"model": "claude-3.7-sonnet",
|
||||
"maxTokens": 5000
|
||||
},
|
||||
"title": {
|
||||
"model": "claude-3.7-sonnet",
|
||||
"maxTokens": 80
|
||||
}
|
||||
},
|
||||
"mcpServers": {
|
||||
"example": {
|
||||
"type": "stdio",
|
||||
"command": "path/to/mcp-server",
|
||||
"env": [],
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"lsp": {
|
||||
"go": {
|
||||
"disabled": false,
|
||||
"command": "gopls"
|
||||
}
|
||||
},
|
||||
"debug": false,
|
||||
"debugLSP": false,
|
||||
"autoCompact": true
|
||||
}
|
||||
```
|
||||
|
||||
#### What's the other repo?
|
||||
## Supported AI Models
|
||||
|
||||
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
|
||||
OpenCode supports a variety of AI models from different providers:
|
||||
|
||||
---
|
||||
### OpenAI
|
||||
|
||||
**Join our community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
|
||||
- GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano)
|
||||
- GPT-4.5 Preview
|
||||
- GPT-4o family (gpt-4o, gpt-4o-mini)
|
||||
- O1 family (o1, o1-pro, o1-mini)
|
||||
- O3 family (o3, o3-mini)
|
||||
- O4 Mini
|
||||
|
||||
### Anthropic
|
||||
|
||||
- Claude 3.5 Sonnet
|
||||
- Claude 3.5 Haiku
|
||||
- Claude 3.7 Sonnet
|
||||
- Claude 3 Haiku
|
||||
- Claude 3 Opus
|
||||
|
||||
### Google
|
||||
|
||||
- Gemini 2.5
|
||||
- Gemini 2.5 Flash
|
||||
- Gemini 2.0 Flash
|
||||
- Gemini 2.0 Flash Lite
|
||||
|
||||
### AWS Bedrock
|
||||
|
||||
- Claude 3.7 Sonnet
|
||||
|
||||
### Groq
|
||||
|
||||
- Llama 4 Maverick (17b-128e-instruct)
|
||||
- Llama 4 Scout (17b-16e-instruct)
|
||||
- QWEN QWQ-32b
|
||||
- Deepseek R1 distill Llama 70b
|
||||
- Llama 3.3 70b Versatile
|
||||
|
||||
### Azure OpenAI
|
||||
|
||||
- GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano)
|
||||
- GPT-4.5 Preview
|
||||
- GPT-4o family (gpt-4o, gpt-4o-mini)
|
||||
- O1 family (o1, o1-mini)
|
||||
- O3 family (o3, o3-mini)
|
||||
- O4 Mini
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Start OpenCode
|
||||
opencode
|
||||
|
||||
# Start with debug logging
|
||||
opencode -d
|
||||
|
||||
# Start with a specific working directory
|
||||
opencode -c /path/to/project
|
||||
```
|
||||
|
||||
## Command-line Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| --------- | ----- | ----------------------------- |
|
||||
| `--help` | `-h` | Display help information |
|
||||
| `--debug` | `-d` | Enable debug mode |
|
||||
| `--cwd` | `-c` | Set current working directory |
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
### Global Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| -------- | ------------------------------------------------------- |
|
||||
| `Ctrl+C` | Quit application |
|
||||
| `Ctrl+?` | Toggle help dialog |
|
||||
| `?` | Toggle help dialog (when not in editing mode) |
|
||||
| `Ctrl+L` | View logs |
|
||||
| `Ctrl+A` | Switch session |
|
||||
| `Ctrl+K` | Command dialog |
|
||||
| `Ctrl+O` | Toggle model selection dialog |
|
||||
| `Esc` | Close current overlay/dialog or return to previous mode |
|
||||
|
||||
### Chat Page Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| -------- | --------------------------------------- |
|
||||
| `Ctrl+N` | Create new session |
|
||||
| `Ctrl+X` | Cancel current operation/generation |
|
||||
| `i` | Focus editor (when not in writing mode) |
|
||||
| `Esc` | Exit writing mode and focus messages |
|
||||
|
||||
### Editor Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| ------------------- | ----------------------------------------- |
|
||||
| `Ctrl+S` | Send message (when editor is focused) |
|
||||
| `Enter` or `Ctrl+S` | Send message (when editor is not focused) |
|
||||
| `Ctrl+E` | Open external editor |
|
||||
| `Esc` | Blur editor and focus messages |
|
||||
|
||||
### Session Dialog Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| ---------- | ---------------- |
|
||||
| `↑` or `k` | Previous session |
|
||||
| `↓` or `j` | Next session |
|
||||
| `Enter` | Select session |
|
||||
| `Esc` | Close dialog |
|
||||
|
||||
### Model Dialog Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| ---------- | ----------------- |
|
||||
| `↑` or `k` | Move up |
|
||||
| `↓` or `j` | Move down |
|
||||
| `←` or `h` | Previous provider |
|
||||
| `→` or `l` | Next provider |
|
||||
| `Esc` | Close dialog |
|
||||
|
||||
### Permission Dialog Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| ----------------------- | ---------------------------- |
|
||||
| `←` or `left` | Switch options left |
|
||||
| `→` or `right` or `tab` | Switch options right |
|
||||
| `Enter` or `space` | Confirm selection |
|
||||
| `a` | Allow permission |
|
||||
| `A` | Allow permission for session |
|
||||
| `d` | Deny permission |
|
||||
|
||||
### Logs Page Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| ------------------ | ------------------- |
|
||||
| `Backspace` or `q` | Return to chat page |
|
||||
|
||||
## AI Assistant Tools
|
||||
|
||||
OpenCode's AI assistant has access to various tools to help with coding tasks:
|
||||
|
||||
### File and Code Tools
|
||||
|
||||
| Tool | Description | Parameters |
|
||||
| ------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `glob` | Find files by pattern | `pattern` (required), `path` (optional) |
|
||||
| `grep` | Search file contents | `pattern` (required), `path` (optional), `include` (optional), `literal_text` (optional) |
|
||||
| `ls` | List directory contents | `path` (optional), `ignore` (optional array of patterns) |
|
||||
| `view` | View file contents | `file_path` (required), `offset` (optional), `limit` (optional) |
|
||||
| `write` | Write to files | `file_path` (required), `content` (required) |
|
||||
| `edit` | Edit files | Various parameters for file editing |
|
||||
| `patch` | Apply patches to files | `file_path` (required), `diff` (required) |
|
||||
| `diagnostics` | Get diagnostics information | `file_path` (optional) |
|
||||
|
||||
### Other Tools
|
||||
|
||||
| Tool | Description | Parameters |
|
||||
| ------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `bash` | Execute shell commands | `command` (required), `timeout` (optional) |
|
||||
| `fetch` | Fetch data from URLs | `url` (required), `format` (required), `timeout` (optional) |
|
||||
| `sourcegraph` | Search code across public repositories | `query` (required), `count` (optional), `context_window` (optional), `timeout` (optional) |
|
||||
| `agent` | Run sub-tasks with the AI agent | `prompt` (required) |
|
||||
|
||||
## Architecture
|
||||
|
||||
OpenCode is built with a modular architecture:
|
||||
|
||||
- **cmd**: Command-line interface using Cobra
|
||||
- **internal/app**: Core application services
|
||||
- **internal/config**: Configuration management
|
||||
- **internal/db**: Database operations and migrations
|
||||
- **internal/llm**: LLM providers and tools integration
|
||||
- **internal/tui**: Terminal UI components and layouts
|
||||
- **internal/logging**: Logging infrastructure
|
||||
- **internal/message**: Message handling
|
||||
- **internal/session**: Session management
|
||||
- **internal/lsp**: Language Server Protocol integration
|
||||
|
||||
## Custom Commands
|
||||
|
||||
OpenCode supports custom commands that can be created by users to quickly send predefined prompts to the AI assistant.
|
||||
|
||||
### Creating Custom Commands
|
||||
|
||||
Custom commands are predefined prompts stored as Markdown files in one of three locations:
|
||||
|
||||
1. **User Commands** (prefixed with `user:`):
|
||||
|
||||
```
|
||||
$XDG_CONFIG_HOME/opencode/commands/
|
||||
```
|
||||
|
||||
(typically `~/.config/opencode/commands/` on Linux/macOS)
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
$HOME/.opencode/commands/
|
||||
```
|
||||
|
||||
2. **Project Commands** (prefixed with `project:`):
|
||||
```
|
||||
<PROJECT DIR>/.opencode/commands/
|
||||
```
|
||||
|
||||
Each `.md` file in these directories becomes a custom command. The file name (without extension) becomes the command ID.
|
||||
|
||||
For example, creating a file at `~/.config/opencode/commands/prime-context.md` with content:
|
||||
|
||||
```markdown
|
||||
RUN git ls-files
|
||||
READ README.md
|
||||
```
|
||||
|
||||
This creates a command called `user:prime-context`.
|
||||
|
||||
### Command Arguments
|
||||
|
||||
You can create commands that accept arguments by including the `$ARGUMENTS` placeholder in your command file:
|
||||
|
||||
```markdown
|
||||
RUN git show $ARGUMENTS
|
||||
```
|
||||
|
||||
When you run this command, OpenCode will prompt you to enter the text that should replace `$ARGUMENTS`.
|
||||
|
||||
### Organizing Commands
|
||||
|
||||
You can organize commands in subdirectories:
|
||||
|
||||
```
|
||||
~/.config/opencode/commands/git/commit.md
|
||||
```
|
||||
|
||||
This creates a command with ID `user:git:commit`.
|
||||
|
||||
### Using Custom Commands
|
||||
|
||||
1. Press `Ctrl+K` to open the command dialog
|
||||
2. Select your custom command (prefixed with either `user:` or `project:`)
|
||||
3. Press Enter to execute the command
|
||||
|
||||
The content of the command file will be sent as a message to the AI assistant.
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
OpenCode includes several built-in commands:
|
||||
|
||||
| Command | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| Initialize Project | Creates or updates the OpenCode.md memory file with project-specific information |
|
||||
| Compact Session | Manually triggers the summarization of the current session, creating a new session with the summary |
|
||||
|
||||
## MCP (Model Context Protocol)
|
||||
|
||||
OpenCode implements the Model Context Protocol (MCP) to extend its capabilities through external tools. MCP provides a standardized way for the AI assistant to interact with external services and tools.
|
||||
|
||||
### MCP Features
|
||||
|
||||
- **External Tool Integration**: Connect to external tools and services via a standardized protocol
|
||||
- **Tool Discovery**: Automatically discover available tools from MCP servers
|
||||
- **Multiple Connection Types**:
|
||||
- **Stdio**: Communicate with tools via standard input/output
|
||||
- **SSE**: Communicate with tools via Server-Sent Events
|
||||
- **Security**: Permission system for controlling access to MCP tools
|
||||
|
||||
### Configuring MCP Servers
|
||||
|
||||
MCP servers are defined in the configuration file under the `mcpServers` section:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"example": {
|
||||
"type": "stdio",
|
||||
"command": "path/to/mcp-server",
|
||||
"env": [],
|
||||
"args": []
|
||||
},
|
||||
"web-example": {
|
||||
"type": "sse",
|
||||
"url": "https://example.com/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Tool Usage
|
||||
|
||||
Once configured, MCP tools are automatically available to the AI assistant alongside built-in tools. They follow the same permission model as other tools, requiring user approval before execution.
|
||||
|
||||
## LSP (Language Server Protocol)
|
||||
|
||||
OpenCode integrates with Language Server Protocol to provide code intelligence features across multiple programming languages.
|
||||
|
||||
### LSP Features
|
||||
|
||||
- **Multi-language Support**: Connect to language servers for different programming languages
|
||||
- **Diagnostics**: Receive error checking and linting information
|
||||
- **File Watching**: Automatically notify language servers of file changes
|
||||
|
||||
### Configuring LSP
|
||||
|
||||
Language servers are configured in the configuration file under the `lsp` section:
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp": {
|
||||
"go": {
|
||||
"disabled": false,
|
||||
"command": "gopls"
|
||||
},
|
||||
"typescript": {
|
||||
"disabled": false,
|
||||
"command": "typescript-language-server",
|
||||
"args": ["--stdio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### LSP Integration with AI
|
||||
|
||||
The AI assistant can access LSP features through the `diagnostics` tool, allowing it to:
|
||||
|
||||
- Check for errors in your code
|
||||
- Suggest fixes based on diagnostics
|
||||
|
||||
While the LSP client implementation supports the full LSP protocol (including completions, hover, definition, etc.), currently only diagnostics are exposed to the AI assistant.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.24.0 or higher
|
||||
|
||||
### Building from Source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/opencode-ai/opencode.git
|
||||
cd opencode
|
||||
|
||||
# Build
|
||||
go build -o opencode
|
||||
|
||||
# Run
|
||||
./opencode
|
||||
```
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
OpenCode gratefully acknowledges the contributions and support from these key individuals:
|
||||
|
||||
- [@isaacphi](https://github.com/isaacphi) - For the [mcp-language-server](https://github.com/isaacphi/mcp-language-server) project which provided the foundation for our LSP client implementation
|
||||
- [@adamdottv](https://github.com/adamdottv) - For the design direction and UI/UX architecture
|
||||
|
||||
Special thanks to the broader open source community whose tools and libraries have made this project possible.
|
||||
|
||||
## License
|
||||
|
||||
OpenCode is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Here's how you can contribute:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
Please make sure to update tests as appropriate and follow the existing code style.
|
||||
|
||||
166
STATS.md
166
STATS.md
@@ -1,166 +0,0 @@
|
||||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ------------------ | ----------------- | ------------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
|
||||
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
|
||||
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
|
||||
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
|
||||
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
|
||||
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
|
||||
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
|
||||
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
|
||||
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
|
||||
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
|
||||
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
|
||||
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
|
||||
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
|
||||
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
|
||||
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
|
||||
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
|
||||
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
|
||||
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
|
||||
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
|
||||
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
|
||||
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
|
||||
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
|
||||
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
|
||||
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
|
||||
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
|
||||
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
|
||||
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
|
||||
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
|
||||
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
|
||||
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
|
||||
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
|
||||
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
|
||||
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
|
||||
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
|
||||
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
|
||||
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
|
||||
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
|
||||
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
|
||||
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
|
||||
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
|
||||
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
|
||||
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
|
||||
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
|
||||
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
|
||||
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
|
||||
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
|
||||
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
|
||||
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
|
||||
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
|
||||
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
|
||||
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
|
||||
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
|
||||
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
|
||||
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
|
||||
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
|
||||
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
|
||||
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
|
||||
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
|
||||
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
|
||||
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
|
||||
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
|
||||
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
|
||||
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
|
||||
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
|
||||
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
|
||||
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
|
||||
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
|
||||
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
|
||||
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
|
||||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
|
||||
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
|
||||
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
|
||||
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
|
||||
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
|
||||
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
|
||||
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
|
||||
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
|
||||
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
|
||||
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
|
||||
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
|
||||
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
|
||||
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
|
||||
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
|
||||
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
|
||||
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
|
||||
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
|
||||
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
|
||||
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
|
||||
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
|
||||
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
|
||||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
|
||||
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
|
||||
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
|
||||
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
|
||||
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
|
||||
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
|
||||
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
|
||||
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
|
||||
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
|
||||
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
|
||||
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
|
||||
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
|
||||
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
|
||||
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
|
||||
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
|
||||
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
|
||||
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
|
||||
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
|
||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||
@@ -1,12 +0,0 @@
|
||||
## Style Guide
|
||||
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
@@ -1,2 +0,0 @@
|
||||
[install]
|
||||
exact = true
|
||||
258
cmd/root.go
Normal file
258
cmd/root.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
zone "github.com/lrstanley/bubblezone"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
"github.com/opencode-ai/opencode/internal/llm/agent"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/tui"
|
||||
"github.com/opencode-ai/opencode/internal/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "OpenCode",
|
||||
Short: "A terminal AI assistant for software development",
|
||||
Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks.
|
||||
It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
|
||||
to assist developers in writing, debugging, and understanding code directly from the terminal.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// If the help flag is set, show the help message
|
||||
if cmd.Flag("help").Changed {
|
||||
cmd.Help()
|
||||
return nil
|
||||
}
|
||||
if cmd.Flag("version").Changed {
|
||||
fmt.Println(version.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load the config
|
||||
debug, _ := cmd.Flags().GetBool("debug")
|
||||
cwd, _ := cmd.Flags().GetString("cwd")
|
||||
if cwd != "" {
|
||||
err := os.Chdir(cwd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to change directory: %v", err)
|
||||
}
|
||||
}
|
||||
if cwd == "" {
|
||||
c, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current working directory: %v", err)
|
||||
}
|
||||
cwd = c
|
||||
}
|
||||
_, err := config.Load(cwd, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Connect DB, this will also run migrations
|
||||
conn, err := db.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create main context for the application
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
app, err := app.New(ctx, conn)
|
||||
if err != nil {
|
||||
logging.Error("Failed to create app: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set up the TUI
|
||||
zone.NewGlobal()
|
||||
program := tea.NewProgram(
|
||||
tui.New(app),
|
||||
tea.WithAltScreen(),
|
||||
)
|
||||
|
||||
// Initialize MCP tools in the background
|
||||
initMCPTools(ctx, app)
|
||||
|
||||
// Setup the subscriptions, this will send services events to the TUI
|
||||
ch, cancelSubs := setupSubscriptions(app, ctx)
|
||||
|
||||
// Create a context for the TUI message handler
|
||||
tuiCtx, tuiCancel := context.WithCancel(ctx)
|
||||
var tuiWg sync.WaitGroup
|
||||
tuiWg.Add(1)
|
||||
|
||||
// Set up message handling for the TUI
|
||||
go func() {
|
||||
defer tuiWg.Done()
|
||||
defer logging.RecoverPanic("TUI-message-handler", func() {
|
||||
attemptTUIRecovery(program)
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-tuiCtx.Done():
|
||||
logging.Info("TUI message handler shutting down")
|
||||
return
|
||||
case msg, ok := <-ch:
|
||||
if !ok {
|
||||
logging.Info("TUI message channel closed")
|
||||
return
|
||||
}
|
||||
program.Send(msg)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Cleanup function for when the program exits
|
||||
cleanup := func() {
|
||||
// Shutdown the app
|
||||
app.Shutdown()
|
||||
|
||||
// Cancel subscriptions first
|
||||
cancelSubs()
|
||||
|
||||
// Then cancel TUI message handler
|
||||
tuiCancel()
|
||||
|
||||
// Wait for TUI message handler to finish
|
||||
tuiWg.Wait()
|
||||
|
||||
logging.Info("All goroutines cleaned up")
|
||||
}
|
||||
|
||||
// Run the TUI
|
||||
result, err := program.Run()
|
||||
cleanup()
|
||||
|
||||
if err != nil {
|
||||
logging.Error("TUI error: %v", err)
|
||||
return fmt.Errorf("TUI error: %v", err)
|
||||
}
|
||||
|
||||
logging.Info("TUI exited with result: %v", result)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// attemptTUIRecovery tries to recover the TUI after a panic
|
||||
func attemptTUIRecovery(program *tea.Program) {
|
||||
logging.Info("Attempting to recover TUI after panic")
|
||||
|
||||
// We could try to restart the TUI or gracefully exit
|
||||
// For now, we'll just quit the program to avoid further issues
|
||||
program.Quit()
|
||||
}
|
||||
|
||||
func initMCPTools(ctx context.Context, app *app.App) {
|
||||
go func() {
|
||||
defer logging.RecoverPanic("MCP-goroutine", nil)
|
||||
|
||||
// Create a context with timeout for the initial MCP tools fetch
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Set this up once with proper error handling
|
||||
agent.GetMcpTools(ctxWithTimeout, app.Permissions)
|
||||
logging.Info("MCP message handling goroutine exiting")
|
||||
}()
|
||||
}
|
||||
|
||||
func setupSubscriber[T any](
|
||||
ctx context.Context,
|
||||
wg *sync.WaitGroup,
|
||||
name string,
|
||||
subscriber func(context.Context) <-chan pubsub.Event[T],
|
||||
outputCh chan<- tea.Msg,
|
||||
) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
|
||||
|
||||
subCh := subscriber(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-subCh:
|
||||
if !ok {
|
||||
logging.Info("subscription channel closed", "name", name)
|
||||
return
|
||||
}
|
||||
|
||||
var msg tea.Msg = event
|
||||
|
||||
select {
|
||||
case outputCh <- msg:
|
||||
case <-time.After(2 * time.Second):
|
||||
logging.Warn("message dropped due to slow consumer", "name", name)
|
||||
case <-ctx.Done():
|
||||
logging.Info("subscription cancelled", "name", name)
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
logging.Info("subscription cancelled", "name", name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
|
||||
ch := make(chan tea.Msg, 100)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
|
||||
|
||||
setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
|
||||
setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
|
||||
setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
|
||||
setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
|
||||
setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
|
||||
|
||||
cleanupFunc := func() {
|
||||
logging.Info("Cancelling all subscriptions")
|
||||
cancel() // Signal all goroutines to stop
|
||||
|
||||
waitCh := make(chan struct{})
|
||||
go func() {
|
||||
defer logging.RecoverPanic("subscription-cleanup", nil)
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
logging.Info("All subscription goroutines completed successfully")
|
||||
close(ch) // Only close after all writers are confirmed done
|
||||
case <-time.After(5 * time.Second):
|
||||
logging.Warn("Timed out waiting for some subscription goroutines to complete")
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
return ch, cleanupFunc
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.Flags().BoolP("help", "h", false, "Help")
|
||||
rootCmd.Flags().BoolP("version", "v", false, "Version")
|
||||
rootCmd.Flags().BoolP("debug", "d", false, "Debug")
|
||||
rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
|
||||
}
|
||||
64
cmd/schema/README.md
Normal file
64
cmd/schema/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# OpenCode Configuration Schema Generator
|
||||
|
||||
This tool generates a JSON Schema for the OpenCode configuration file. The schema can be used to validate configuration files and provide autocompletion in editors that support JSON Schema.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
go run cmd/schema/main.go > opencode-schema.json
|
||||
```
|
||||
|
||||
This will generate a JSON Schema file that can be used to validate configuration files.
|
||||
|
||||
## Schema Features
|
||||
|
||||
The generated schema includes:
|
||||
|
||||
- All configuration options with descriptions
|
||||
- Default values where applicable
|
||||
- Validation for enum values (e.g., model IDs, provider types)
|
||||
- Required fields
|
||||
- Type checking
|
||||
|
||||
## Using the Schema
|
||||
|
||||
You can use the generated schema in several ways:
|
||||
|
||||
1. **Editor Integration**: Many editors (VS Code, JetBrains IDEs, etc.) support JSON Schema for validation and autocompletion. You can configure your editor to use the generated schema for `.opencode.json` files.
|
||||
|
||||
2. **Validation Tools**: You can use tools like [jsonschema](https://github.com/Julian/jsonschema) to validate your configuration files against the schema.
|
||||
|
||||
3. **Documentation**: The schema serves as documentation for the configuration options.
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's an example configuration that conforms to the schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"directory": ".opencode"
|
||||
},
|
||||
"debug": false,
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"apiKey": "your-api-key"
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"coder": {
|
||||
"model": "claude-3.7-sonnet",
|
||||
"maxTokens": 5000,
|
||||
"reasoningEffort": "medium"
|
||||
},
|
||||
"task": {
|
||||
"model": "claude-3.7-sonnet",
|
||||
"maxTokens": 5000
|
||||
},
|
||||
"title": {
|
||||
"model": "claude-3.7-sonnet",
|
||||
"maxTokens": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
307
cmd/schema/main.go
Normal file
307
cmd/schema/main.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
)
|
||||
|
||||
// JSONSchemaType represents a JSON Schema type
|
||||
type JSONSchemaType struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Properties map[string]any `json:"properties,omitempty"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
AdditionalProperties any `json:"additionalProperties,omitempty"`
|
||||
Enum []any `json:"enum,omitempty"`
|
||||
Items map[string]any `json:"items,omitempty"`
|
||||
OneOf []map[string]any `json:"oneOf,omitempty"`
|
||||
AnyOf []map[string]any `json:"anyOf,omitempty"`
|
||||
Default any `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
schema := generateSchema()
|
||||
|
||||
// Pretty print the schema
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(schema); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error encoding schema: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func generateSchema() map[string]any {
|
||||
schema := map[string]any{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "OpenCode Configuration",
|
||||
"description": "Configuration schema for the OpenCode application",
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
}
|
||||
|
||||
// Add Data configuration
|
||||
schema["properties"].(map[string]any)["data"] = map[string]any{
|
||||
"type": "object",
|
||||
"description": "Storage configuration",
|
||||
"properties": map[string]any{
|
||||
"directory": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Directory where application data is stored",
|
||||
"default": ".opencode",
|
||||
},
|
||||
},
|
||||
"required": []string{"directory"},
|
||||
}
|
||||
|
||||
// Add working directory
|
||||
schema["properties"].(map[string]any)["wd"] = map[string]any{
|
||||
"type": "string",
|
||||
"description": "Working directory for the application",
|
||||
}
|
||||
|
||||
// Add debug flags
|
||||
schema["properties"].(map[string]any)["debug"] = map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Enable debug mode",
|
||||
"default": false,
|
||||
}
|
||||
|
||||
schema["properties"].(map[string]any)["debugLSP"] = map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Enable LSP debug mode",
|
||||
"default": false,
|
||||
}
|
||||
|
||||
schema["properties"].(map[string]any)["contextPaths"] = map[string]any{
|
||||
"type": "array",
|
||||
"description": "Context paths for the application",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
"default": []string{
|
||||
".github/copilot-instructions.md",
|
||||
".cursorrules",
|
||||
".cursor/rules/",
|
||||
"CLAUDE.md",
|
||||
"CLAUDE.local.md",
|
||||
"opencode.md",
|
||||
"opencode.local.md",
|
||||
"OpenCode.md",
|
||||
"OpenCode.local.md",
|
||||
"OPENCODE.md",
|
||||
"OPENCODE.local.md",
|
||||
},
|
||||
}
|
||||
|
||||
schema["properties"].(map[string]any)["tui"] = map[string]any{
|
||||
"type": "object",
|
||||
"description": "Terminal User Interface configuration",
|
||||
"properties": map[string]any{
|
||||
"theme": map[string]any{
|
||||
"type": "string",
|
||||
"description": "TUI theme name",
|
||||
"default": "opencode",
|
||||
"enum": []string{
|
||||
"opencode",
|
||||
"catppuccin",
|
||||
"dracula",
|
||||
"flexoki",
|
||||
"gruvbox",
|
||||
"monokai",
|
||||
"onedark",
|
||||
"tokyonight",
|
||||
"tron",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add MCP servers
|
||||
schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
|
||||
"type": "object",
|
||||
"description": "Model Control Protocol server configurations",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "object",
|
||||
"description": "MCP server configuration",
|
||||
"properties": map[string]any{
|
||||
"command": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Command to execute for the MCP server",
|
||||
},
|
||||
"env": map[string]any{
|
||||
"type": "array",
|
||||
"description": "Environment variables for the MCP server",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"args": map[string]any{
|
||||
"type": "array",
|
||||
"description": "Command arguments for the MCP server",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Type of MCP server",
|
||||
"enum": []string{"stdio", "sse"},
|
||||
"default": "stdio",
|
||||
},
|
||||
"url": map[string]any{
|
||||
"type": "string",
|
||||
"description": "URL for SSE type MCP servers",
|
||||
},
|
||||
"headers": map[string]any{
|
||||
"type": "object",
|
||||
"description": "HTTP headers for SSE type MCP servers",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
},
|
||||
}
|
||||
|
||||
// Add providers
|
||||
providerSchema := map[string]any{
|
||||
"type": "object",
|
||||
"description": "LLM provider configurations",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Provider configuration",
|
||||
"properties": map[string]any{
|
||||
"apiKey": map[string]any{
|
||||
"type": "string",
|
||||
"description": "API key for the provider",
|
||||
},
|
||||
"disabled": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Whether the provider is disabled",
|
||||
"default": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add known providers
|
||||
knownProviders := []string{
|
||||
string(models.ProviderAnthropic),
|
||||
string(models.ProviderOpenAI),
|
||||
string(models.ProviderGemini),
|
||||
string(models.ProviderGROQ),
|
||||
string(models.ProviderOpenRouter),
|
||||
string(models.ProviderBedrock),
|
||||
string(models.ProviderAzure),
|
||||
}
|
||||
|
||||
providerSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["provider"] = map[string]any{
|
||||
"type": "string",
|
||||
"description": "Provider type",
|
||||
"enum": knownProviders,
|
||||
}
|
||||
|
||||
schema["properties"].(map[string]any)["providers"] = providerSchema
|
||||
|
||||
// Add agents
|
||||
agentSchema := map[string]any{
|
||||
"type": "object",
|
||||
"description": "Agent configurations",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Agent configuration",
|
||||
"properties": map[string]any{
|
||||
"model": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Model ID for the agent",
|
||||
},
|
||||
"maxTokens": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Maximum tokens for the agent",
|
||||
"minimum": 1,
|
||||
},
|
||||
"reasoningEffort": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
|
||||
"enum": []string{"low", "medium", "high"},
|
||||
},
|
||||
},
|
||||
"required": []string{"model"},
|
||||
},
|
||||
}
|
||||
|
||||
// Add model enum
|
||||
modelEnum := []string{}
|
||||
for modelID := range models.SupportedModels {
|
||||
modelEnum = append(modelEnum, string(modelID))
|
||||
}
|
||||
agentSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["model"].(map[string]any)["enum"] = modelEnum
|
||||
|
||||
// Add specific agent properties
|
||||
agentProperties := map[string]any{}
|
||||
knownAgents := []string{
|
||||
string(config.AgentCoder),
|
||||
string(config.AgentTask),
|
||||
string(config.AgentTitle),
|
||||
}
|
||||
|
||||
for _, agentName := range knownAgents {
|
||||
agentProperties[agentName] = map[string]any{
|
||||
"$ref": "#/definitions/agent",
|
||||
}
|
||||
}
|
||||
|
||||
// Create a combined schema that allows both specific agents and additional ones
|
||||
combinedAgentSchema := map[string]any{
|
||||
"type": "object",
|
||||
"description": "Agent configurations",
|
||||
"properties": agentProperties,
|
||||
"additionalProperties": agentSchema["additionalProperties"],
|
||||
}
|
||||
|
||||
schema["properties"].(map[string]any)["agents"] = combinedAgentSchema
|
||||
schema["definitions"] = map[string]any{
|
||||
"agent": agentSchema["additionalProperties"],
|
||||
}
|
||||
|
||||
// Add LSP configuration
|
||||
schema["properties"].(map[string]any)["lsp"] = map[string]any{
|
||||
"type": "object",
|
||||
"description": "Language Server Protocol configurations",
|
||||
"additionalProperties": map[string]any{
|
||||
"type": "object",
|
||||
"description": "LSP configuration for a language",
|
||||
"properties": map[string]any{
|
||||
"disabled": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Whether the LSP is disabled",
|
||||
"default": false,
|
||||
},
|
||||
"command": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Command to execute for the LSP server",
|
||||
},
|
||||
"args": map[string]any{
|
||||
"type": "array",
|
||||
"description": "Command arguments for the LSP server",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"options": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Additional options for the LSP server",
|
||||
},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
},
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
27
flake.lock
generated
27
flake.lock
generated
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1764947035,
|
||||
"narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a672be65651c80d3f592a89b3945466584a22069",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
107
flake.nix
107
flake.nix
@@ -1,107 +0,0 @@
|
||||
{
|
||||
description = "OpenCode development flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
systems = [
|
||||
"aarch64-linux"
|
||||
"x86_64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-darwin"
|
||||
];
|
||||
lib = nixpkgs.lib;
|
||||
forEachSystem = lib.genAttrs systems;
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
|
||||
bunTarget = {
|
||||
"aarch64-linux" = "bun-linux-arm64";
|
||||
"x86_64-linux" = "bun-linux-x64";
|
||||
"aarch64-darwin" = "bun-darwin-arm64";
|
||||
"x86_64-darwin" = "bun-darwin-x64";
|
||||
};
|
||||
defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
hashesFile = "${./nix}/hashes.json";
|
||||
hashesData =
|
||||
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
|
||||
nodeModulesHash = hashesData.nodeModules or defaultNodeModules;
|
||||
modelsDev = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
pkgs."models-dev"
|
||||
);
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
bun
|
||||
nodejs_20
|
||||
pkg-config
|
||||
openssl
|
||||
git
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
packages = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
|
||||
hash = nodeModulesHash;
|
||||
};
|
||||
mkPackage = pkgs.callPackage ./nix/opencode.nix { };
|
||||
in
|
||||
{
|
||||
default = mkPackage {
|
||||
version = packageJson.version;
|
||||
src = ./.;
|
||||
scripts = ./nix/scripts;
|
||||
target = bunTarget.${system};
|
||||
modelsDev = "${modelsDev.${system}}/dist/_api.json";
|
||||
mkNodeModules = mkNodeModules;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
apps = forEachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in
|
||||
{
|
||||
opencode-dev = {
|
||||
type = "app";
|
||||
meta = {
|
||||
description = "Nix devshell shell for OpenCode";
|
||||
runtimeInputs = [ pkgs.bun ];
|
||||
};
|
||||
program = "${
|
||||
pkgs.writeShellApplication {
|
||||
name = "opencode-dev";
|
||||
text = ''
|
||||
exec bun run dev "$@"
|
||||
'';
|
||||
}
|
||||
}/bin/opencode-dev";
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
34
github/.gitignore
vendored
34
github/.gitignore
vendored
@@ -1,34 +0,0 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
163
github/README.md
163
github/README.md
@@ -1,163 +0,0 @@
|
||||
# opencode GitHub Action
|
||||
|
||||
A GitHub Action that integrates [opencode](https://opencode.ai) directly into your GitHub workflow.
|
||||
|
||||
Mention `/opencode` in your comment, and opencode will execute tasks within your GitHub Actions runner.
|
||||
|
||||
## Features
|
||||
|
||||
#### Explain an issues
|
||||
|
||||
Leave the following comment on a GitHub issue. `opencode` will read the entire thread, including all comments, and reply with a clear explanation.
|
||||
|
||||
```
|
||||
/opencode explain this issue
|
||||
```
|
||||
|
||||
#### Fix an issues
|
||||
|
||||
Leave the following comment on a GitHub issue. opencode will create a new branch, implement the changes, and open a PR with the changes.
|
||||
|
||||
```
|
||||
/opencode fix this
|
||||
```
|
||||
|
||||
#### Review PRs and make changes
|
||||
|
||||
Leave the following comment on a GitHub PR. opencode will implement the requested change and commit it to the same PR.
|
||||
|
||||
```
|
||||
Delete the attachment from S3 when the note is removed /oc
|
||||
```
|
||||
|
||||
#### Review specific code lines
|
||||
|
||||
Leave a comment directly on code lines in the PR's "Files" tab. opencode will automatically detect the file, line numbers, and diff context to provide precise responses.
|
||||
|
||||
```
|
||||
[Comment on specific lines in Files tab]
|
||||
/oc add error handling here
|
||||
```
|
||||
|
||||
When commenting on specific lines, opencode receives:
|
||||
|
||||
- The exact file being reviewed
|
||||
- The specific lines of code
|
||||
- The surrounding diff context
|
||||
- Line number information
|
||||
|
||||
This allows for more targeted requests without needing to specify file paths or line numbers manually.
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command in the terminal from your GitHub repo:
|
||||
|
||||
```bash
|
||||
opencode github install
|
||||
```
|
||||
|
||||
This will walk you through installing the GitHub app, creating the workflow, and setting up secrets.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Install the GitHub app https://github.com/apps/opencode-agent. Make sure it is installed on the target repository.
|
||||
2. Add the following workflow file to `.github/workflows/opencode.yml` in your repo. Set the appropriate `model` and required API keys in `env`.
|
||||
|
||||
```yml
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, '/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/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
|
||||
|
||||
## Support
|
||||
|
||||
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
|
||||
|
||||
## Development
|
||||
|
||||
To test locally:
|
||||
|
||||
1. Navigate to a test repo (e.g. `hello-world`):
|
||||
|
||||
```bash
|
||||
cd hello-world
|
||||
```
|
||||
|
||||
2. Run:
|
||||
|
||||
```bash
|
||||
MODEL=anthropic/claude-sonnet-4-20250514 \
|
||||
ANTHROPIC_API_KEY=sk-ant-api03-1234567890 \
|
||||
GITHUB_RUN_ID=dummy \
|
||||
MOCK_TOKEN=github_pat_1234567890 \
|
||||
MOCK_EVENT='{"eventName":"issue_comment",...}' \
|
||||
bun /path/to/opencode/github/index.ts
|
||||
```
|
||||
|
||||
- `MODEL`: The model used by opencode. Same as the `MODEL` defined in the GitHub workflow.
|
||||
- `ANTHROPIC_API_KEY`: Your model provider API key. Same as the keys defined in the GitHub workflow.
|
||||
- `GITHUB_RUN_ID`: Dummy value to emulate GitHub action environment.
|
||||
- `MOCK_TOKEN`: A GitHub personal access token. This token is used to verify you have `admin` or `write` access to the test repo. Generate a token [here](https://github.com/settings/personal-access-tokens).
|
||||
- `MOCK_EVENT`: Mock GitHub event payload (see templates below).
|
||||
- `/path/to/opencode`: Path to your cloned opencode repo. `bun /path/to/opencode/github/index.ts` runs your local version of `opencode`.
|
||||
|
||||
### Issue comment event
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
Replace:
|
||||
|
||||
- `"owner":"sst"` with repo owner
|
||||
- `"repo":"hello-world"` with repo name
|
||||
- `"actor":"fwang"` with the GitHub username of commenter
|
||||
- `"number":4` with the GitHub issue id
|
||||
- `"body":"hey opencode, summarize thread"` with comment body
|
||||
|
||||
### Issue comment with image attachment.
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4},"comment":{"id":1,"body":"hey opencode, what is in my image "}}}'
|
||||
```
|
||||
|
||||
Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with a valid GitHub attachment (you can generate one by commenting with an image in any issue).
|
||||
|
||||
### PR comment event
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
|
||||
```
|
||||
|
||||
### PR review comment event
|
||||
|
||||
```
|
||||
MOCK_EVENT='{"eventName":"pull_request_review_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"pull_request":{"number":7},"comment":{"id":1,"body":"hey opencode, add error handling","path":"src/components/Button.tsx","diff_hunk":"@@ -45,8 +45,11 @@\n- const handleClick = () => {\n- console.log('clicked')\n+ const handleClick = useCallback(() => {\n+ console.log('clicked')\n+ doSomething()\n+ }, [doSomething])","line":47,"original_line":45,"position":10,"commit_id":"abc123","original_commit_id":"def456"}}}'
|
||||
```
|
||||
@@ -1,34 +0,0 @@
|
||||
name: "opencode GitHub Action"
|
||||
description: "Run opencode in GitHub Actions workflows"
|
||||
branding:
|
||||
icon: "code"
|
||||
color: "orange"
|
||||
|
||||
inputs:
|
||||
model:
|
||||
description: "Model to use"
|
||||
required: true
|
||||
|
||||
share:
|
||||
description: "Share the opencode session (defaults to true for public repos)"
|
||||
required: false
|
||||
|
||||
prompt:
|
||||
description: "Custom prompt to override the default prompt"
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install opencode
|
||||
shell: bash
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Run opencode
|
||||
shell: bash
|
||||
id: run_opencode
|
||||
run: opencode github run
|
||||
env:
|
||||
MODEL: ${{ inputs.model }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
156
github/bun.lock
156
github/bun.lock
@@ -1,156 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "github",
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@opencode-ai/sdk": "0.5.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@actions/core": ["@actions/core@1.11.1", "", { "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" } }, "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A=="],
|
||||
|
||||
"@actions/exec": ["@actions/exec@1.1.1", "", { "dependencies": { "@actions/io": "^1.0.1" } }, "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w=="],
|
||||
|
||||
"@actions/github": ["@actions/github@6.0.1", "", { "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", "@octokit/plugin-paginate-rest": "^9.2.2", "@octokit/plugin-rest-endpoint-methods": "^10.4.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "undici": "^5.28.5" } }, "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw=="],
|
||||
|
||||
"@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="],
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
|
||||
|
||||
"@octokit/core": ["@octokit/core@5.2.2", "", { "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" } }, "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg=="],
|
||||
|
||||
"@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="],
|
||||
|
||||
"@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
|
||||
|
||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@9.2.2", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ=="],
|
||||
|
||||
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@10.4.1", "", { "dependencies": { "@octokit/types": "^12.6.0" }, "peerDependencies": { "@octokit/core": "5" } }, "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg=="],
|
||||
|
||||
"@octokit/request": ["@octokit/request@8.4.1", "", { "dependencies": { "@octokit/endpoint": "^9.0.6", "@octokit/request-error": "^5.1.1", "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw=="],
|
||||
|
||||
"@octokit/request-error": ["@octokit/request-error@5.1.1", "", { "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", "once": "^1.4.0" } }, "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g=="],
|
||||
|
||||
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@0.5.4", "", {}, "sha512-bNT9hJgTvmnWGZU4LM90PMy60xOxxCOI5IaGB5voP2EVj+8RdLxmkwuAB4FUHwLo7fNlmxkZp89NVsMYw2Y3Aw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
|
||||
|
||||
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"deprecation": ["deprecation@2.3.1", "", {}, "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="],
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
|
||||
|
||||
"@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/core/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@octokit/graphql/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
|
||||
|
||||
"@octokit/request/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/request/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="],
|
||||
|
||||
"@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="],
|
||||
|
||||
"@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.1.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw=="],
|
||||
|
||||
"@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
|
||||
|
||||
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
|
||||
|
||||
"@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="],
|
||||
|
||||
"@octokit/request-error/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
|
||||
}
|
||||
}
|
||||
1023
github/index.ts
1023
github/index.ts
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "github",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Get the latest Git tag
|
||||
latest_tag=$(git tag --sort=committerdate | grep -E '^github-v[0-9]+\.[0-9]+\.[0-9]+$' | tail -1)
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "No tags found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Latest tag: $latest_tag"
|
||||
|
||||
# Update latest tag
|
||||
git tag -d latest
|
||||
git push origin :refs/tags/latest
|
||||
git tag -a latest $latest_tag -m "Update latest to $latest_tag"
|
||||
git push origin latest
|
||||
9
github/sst-env.d.ts
vendored
9
github/sst-env.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
128
go.mod
Normal file
128
go.mod
Normal file
@@ -0,0 +1,128 @@
|
||||
module github.com/opencode-ai/opencode
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0
|
||||
github.com/PuerkitoBio/goquery v1.9.2
|
||||
github.com/alecthomas/chroma/v2 v2.15.0
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2
|
||||
github.com/aymanbagabas/go-udiff v0.2.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||
github.com/catppuccin/go v0.3.0
|
||||
github.com/charmbracelet/bubbles v0.20.0
|
||||
github.com/charmbracelet/bubbletea v1.3.4
|
||||
github.com/charmbracelet/glamour v0.9.1
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/x/ansi v0.8.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/go-logfmt/logfmt v0.6.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231
|
||||
github.com/mark3labs/mcp-go v0.17.0
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/ncruces/go-sqlite3 v0.25.0
|
||||
github.com/openai/openai-go v0.1.0-beta.2
|
||||
github.com/pressly/goose/v3 v3.24.2
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/viper v1.20.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.9.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/image v0.26.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/genai v1.3.0
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
359
go.sum
Normal file
359
go.sum
Normal file
@@ -0,0 +1,359 @@
|
||||
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
|
||||
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
|
||||
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
|
||||
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
|
||||
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
|
||||
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2 h1:h7qxtumNjKPWFv1QM/HJy60MteeW23iKeEtBoY7bYZk=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
|
||||
github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 h1:9rjt7AfnrXKNSZhp36A3/4QAZAwGGCGD/p8Bse26zms=
|
||||
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231/go.mod h1:S5etECMx+sZnW0Gm100Ma9J1PgVCTgNyFaqGu2b08b4=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930=
|
||||
github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I=
|
||||
github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/openai/openai-go v0.1.0-beta.2 h1:Ra5nCFkbEl9w+UJwAciC4kqnIBUCcJazhmMA0/YN894=
|
||||
github.com/openai/openai-go v0.1.0-beta.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
|
||||
github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
|
||||
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
|
||||
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genai v1.3.0 h1:tXhPJF30skOjnnDY7ZnjK3q7IKy4PuAlEA0fk7uEaEI=
|
||||
google.golang.org/genai v1.3.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.36.2 h1:vjcSazuoFve9Wm0IVNHgmJECoOXLZM1KfMXbcX2axHA=
|
||||
modernc.org/sqlite v1.36.2/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
|
||||
46
infra/app.ts
46
infra/app.ts
@@ -1,46 +0,0 @@
|
||||
import { domain } from "./stage"
|
||||
|
||||
const GITHUB_APP_ID = new sst.Secret("GITHUB_APP_ID")
|
||||
const GITHUB_APP_PRIVATE_KEY = new sst.Secret("GITHUB_APP_PRIVATE_KEY")
|
||||
export const EMAILOCTOPUS_API_KEY = new sst.Secret("EMAILOCTOPUS_API_KEY")
|
||||
const ADMIN_SECRET = new sst.Secret("ADMIN_SECRET")
|
||||
const bucket = new sst.cloudflare.Bucket("Bucket")
|
||||
|
||||
export const api = new sst.cloudflare.Worker("Api", {
|
||||
domain: `api.${domain}`,
|
||||
handler: "packages/function/src/api.ts",
|
||||
environment: {
|
||||
WEB_DOMAIN: domain,
|
||||
},
|
||||
url: true,
|
||||
link: [bucket, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, ADMIN_SECRET],
|
||||
transform: {
|
||||
worker: (args) => {
|
||||
args.logpush = true
|
||||
args.bindings = $resolve(args.bindings).apply((bindings) => [
|
||||
...bindings,
|
||||
{
|
||||
name: "SYNC_SERVER",
|
||||
type: "durable_object_namespace",
|
||||
className: "SyncServer",
|
||||
},
|
||||
])
|
||||
args.migrations = {
|
||||
// Note: when releasing the next tag, make sure all stages use tag v2
|
||||
oldTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
|
||||
newTag: $app.stage === "production" || $app.stage === "thdxr" ? "" : "v1",
|
||||
//newSqliteClasses: ["SyncServer"],
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
new sst.cloudflare.x.Astro("Web", {
|
||||
domain: "docs." + domain,
|
||||
path: "packages/web",
|
||||
environment: {
|
||||
// For astro config
|
||||
SST_STAGE: $app.stage,
|
||||
VITE_API_URL: api.url.apply((url) => url!),
|
||||
},
|
||||
})
|
||||
169
infra/console.ts
169
infra/console.ts
@@ -1,169 +0,0 @@
|
||||
import { domain } from "./stage"
|
||||
import { EMAILOCTOPUS_API_KEY } from "./app"
|
||||
|
||||
////////////////
|
||||
// DATABASE
|
||||
////////////////
|
||||
|
||||
const cluster = planetscale.getDatabaseOutput({
|
||||
name: "opencode",
|
||||
organization: "anomalyco",
|
||||
})
|
||||
|
||||
const branch =
|
||||
$app.stage === "production"
|
||||
? planetscale.getBranchOutput({
|
||||
name: "production",
|
||||
organization: cluster.organization,
|
||||
database: cluster.name,
|
||||
})
|
||||
: new planetscale.Branch("DatabaseBranch", {
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
name: $app.stage,
|
||||
parentBranch: "production",
|
||||
})
|
||||
const password = new planetscale.Password("DatabasePassword", {
|
||||
name: $app.stage,
|
||||
database: cluster.name,
|
||||
organization: cluster.organization,
|
||||
branch: branch.name,
|
||||
})
|
||||
|
||||
export const database = new sst.Linkable("Database", {
|
||||
properties: {
|
||||
host: password.accessHostUrl,
|
||||
database: cluster.name,
|
||||
username: password.username,
|
||||
password: password.plaintext,
|
||||
port: 3306,
|
||||
},
|
||||
})
|
||||
|
||||
new sst.x.DevCommand("Studio", {
|
||||
link: [database],
|
||||
dev: {
|
||||
command: "bun db studio",
|
||||
directory: "packages/console/core",
|
||||
autostart: true,
|
||||
},
|
||||
})
|
||||
|
||||
////////////////
|
||||
// AUTH
|
||||
////////////////
|
||||
|
||||
const GITHUB_CLIENT_ID_CONSOLE = new sst.Secret("GITHUB_CLIENT_ID_CONSOLE")
|
||||
const GITHUB_CLIENT_SECRET_CONSOLE = new sst.Secret("GITHUB_CLIENT_SECRET_CONSOLE")
|
||||
const GOOGLE_CLIENT_ID = new sst.Secret("GOOGLE_CLIENT_ID")
|
||||
const authStorage = new sst.cloudflare.Kv("AuthStorage")
|
||||
export const auth = new sst.cloudflare.Worker("AuthApi", {
|
||||
domain: `auth.${domain}`,
|
||||
handler: "packages/console/function/src/auth.ts",
|
||||
url: true,
|
||||
link: [database, authStorage, GITHUB_CLIENT_ID_CONSOLE, GITHUB_CLIENT_SECRET_CONSOLE, GOOGLE_CLIENT_ID],
|
||||
})
|
||||
|
||||
////////////////
|
||||
// GATEWAY
|
||||
////////////////
|
||||
|
||||
export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint", {
|
||||
url: $interpolate`https://${domain}/stripe/webhook`,
|
||||
enabledEvents: [
|
||||
"checkout.session.async_payment_failed",
|
||||
"checkout.session.async_payment_succeeded",
|
||||
"checkout.session.completed",
|
||||
"checkout.session.expired",
|
||||
"charge.refunded",
|
||||
"customer.created",
|
||||
"customer.deleted",
|
||||
"customer.updated",
|
||||
"customer.discount.created",
|
||||
"customer.discount.deleted",
|
||||
"customer.discount.updated",
|
||||
"customer.source.created",
|
||||
"customer.source.deleted",
|
||||
"customer.source.expiring",
|
||||
"customer.source.updated",
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.deleted",
|
||||
"customer.subscription.paused",
|
||||
"customer.subscription.pending_update_applied",
|
||||
"customer.subscription.pending_update_expired",
|
||||
"customer.subscription.resumed",
|
||||
"customer.subscription.trial_will_end",
|
||||
"customer.subscription.updated",
|
||||
],
|
||||
})
|
||||
|
||||
const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS1"),
|
||||
new sst.Secret("ZEN_MODELS2"),
|
||||
new sst.Secret("ZEN_MODELS3"),
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
properties: { value: auth.url.apply((url) => url!) },
|
||||
})
|
||||
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
||||
properties: { value: stripeWebhook.secret },
|
||||
})
|
||||
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
|
||||
|
||||
////////////////
|
||||
// CONSOLE
|
||||
////////////////
|
||||
|
||||
const bucket = new sst.cloudflare.Bucket("ZenData")
|
||||
|
||||
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||
|
||||
let logProcessor
|
||||
if ($app.stage === "production" || $app.stage === "frank") {
|
||||
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
|
||||
logProcessor = new sst.cloudflare.Worker("LogProcessor", {
|
||||
handler: "packages/console/function/src/log-processor.ts",
|
||||
link: [HONEYCOMB_API_KEY],
|
||||
})
|
||||
}
|
||||
|
||||
new sst.cloudflare.x.SolidStart("Console", {
|
||||
domain,
|
||||
path: "packages/console/app",
|
||||
link: [
|
||||
bucket,
|
||||
database,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
STRIPE_SECRET_KEY,
|
||||
EMAILOCTOPUS_API_KEY,
|
||||
AWS_SES_ACCESS_KEY_ID,
|
||||
AWS_SES_SECRET_ACCESS_KEY,
|
||||
...ZEN_MODELS,
|
||||
...($dev
|
||||
? [
|
||||
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
|
||||
new sst.Secret("CLOUDFLARE_API_TOKEN", process.env.CLOUDFLARE_API_TOKEN!),
|
||||
]
|
||||
: []),
|
||||
gatewayKv,
|
||||
],
|
||||
environment: {
|
||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||
VITE_AUTH_URL: auth.url.apply((url) => url!),
|
||||
},
|
||||
transform: {
|
||||
server: {
|
||||
transform: {
|
||||
worker: {
|
||||
placement: { mode: "smart" },
|
||||
tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import { domain } from "./stage"
|
||||
|
||||
new sst.cloudflare.StaticSite("Desktop", {
|
||||
domain: "desktop." + domain,
|
||||
path: "packages/desktop",
|
||||
build: {
|
||||
command: "bun turbo build",
|
||||
output: "./dist",
|
||||
},
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { SECRET } from "./secret"
|
||||
import { domain } from "./stage"
|
||||
|
||||
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
|
||||
|
||||
const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
|
||||
domain: "enterprise." + domain,
|
||||
path: "packages/enterprise",
|
||||
buildCommand: "bun run build:cloudflare",
|
||||
environment: {
|
||||
OPENCODE_STORAGE_ADAPTER: "r2",
|
||||
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
|
||||
OPENCODE_STORAGE_ACCESS_KEY_ID: SECRET.R2AccessKey.value,
|
||||
OPENCODE_STORAGE_SECRET_ACCESS_KEY: SECRET.R2SecretKey.value,
|
||||
OPENCODE_STORAGE_BUCKET: storage.name,
|
||||
},
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
export const SECRET = {
|
||||
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
|
||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export const domain = (() => {
|
||||
if ($app.stage === "production") return "opencode.ai"
|
||||
if ($app.stage === "dev") return "dev.opencode.ai"
|
||||
return `${$app.stage}.dev.opencode.ai`
|
||||
})()
|
||||
|
||||
export const zoneID = "430ba34c138cfb5360826c4909f99be8"
|
||||
|
||||
new cloudflare.RegionalHostname("RegionalHostname", {
|
||||
hostname: domain,
|
||||
regionKey: "us",
|
||||
zoneId: zoneID,
|
||||
})
|
||||
252
install
252
install
@@ -2,116 +2,53 @@
|
||||
set -euo pipefail
|
||||
APP=opencode
|
||||
|
||||
MUTED='\033[0;2m'
|
||||
RED='\033[0;31m'
|
||||
ORANGE='\033[38;5;214m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
ORANGE='\033[38;2;255;140;0m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
requested_version=${VERSION:-}
|
||||
|
||||
raw_os=$(uname -s)
|
||||
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
|
||||
case "$raw_os" in
|
||||
Darwin*) os="darwin" ;;
|
||||
Linux*) os="linux" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
|
||||
esac
|
||||
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$os" == "darwin" ]]; then
|
||||
os="mac"
|
||||
fi
|
||||
arch=$(uname -m)
|
||||
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
if [[ "$arch" == "x86_64" ]]; then
|
||||
arch="x64"
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
|
||||
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
|
||||
if [ "$rosetta_flag" = "1" ]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
fi
|
||||
filename="$APP-$os-$arch.tar.gz"
|
||||
|
||||
combo="$os-$arch"
|
||||
case "$combo" in
|
||||
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
|
||||
|
||||
case "$filename" in
|
||||
*"-linux-"*)
|
||||
[[ "$arch" == "x86_64" || "$arch" == "arm64" || "$arch" == "i386" ]] || exit 1
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
*"-mac-"*)
|
||||
[[ "$arch" == "x86_64" || "$arch" == "arm64" ]] || exit 1
|
||||
;;
|
||||
*)
|
||||
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive_ext=".zip"
|
||||
if [ "$os" = "linux" ]; then
|
||||
archive_ext=".tar.gz"
|
||||
fi
|
||||
|
||||
is_musl=false
|
||||
if [ "$os" = "linux" ]; then
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
is_musl=true
|
||||
fi
|
||||
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
is_musl=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
needs_baseline=false
|
||||
if [ "$arch" = "x64" ]; then
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$os" = "darwin" ]; then
|
||||
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
|
||||
if [ "$avx2" != "1" ]; then
|
||||
needs_baseline=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
target="$os-$arch"
|
||||
if [ "$needs_baseline" = "true" ]; then
|
||||
target="$target-baseline"
|
||||
fi
|
||||
if [ "$is_musl" = "true" ]; then
|
||||
target="$target-musl"
|
||||
fi
|
||||
|
||||
filename="$APP-$target$archive_ext"
|
||||
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
INSTALL_DIR=$HOME/.opencode/bin
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
if [ -z "$requested_version" ]; then
|
||||
url="https://github.com/sst/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
url="https://github.com/opencode-ai/opencode/releases/latest/download/$filename"
|
||||
specific_version=$(curl -s https://api.github.com/repos/opencode-ai/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}')
|
||||
|
||||
if [[ $? -ne 0 || -z "$specific_version" ]]; then
|
||||
echo -e "${RED}Failed to fetch version information${NC}"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "${RED}Failed to fetch version information${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
|
||||
url="https://github.com/opencode-ai/opencode/releases/download/v${requested_version}/$filename"
|
||||
specific_version=$requested_version
|
||||
fi
|
||||
|
||||
@@ -121,8 +58,8 @@ print_message() {
|
||||
local color=""
|
||||
|
||||
case $level in
|
||||
info) color="${NC}" ;;
|
||||
warning) color="${NC}" ;;
|
||||
info) color="${GREEN}" ;;
|
||||
warning) color="${YELLOW}" ;;
|
||||
error) color="${RED}" ;;
|
||||
esac
|
||||
|
||||
@@ -140,122 +77,20 @@ check_version() {
|
||||
installed_version=$(echo $installed_version | awk '{print $2}')
|
||||
|
||||
if [[ "$installed_version" != "$specific_version" ]]; then
|
||||
print_message info "${MUTED}Installed version: ${NC}$installed_version."
|
||||
print_message info "Installed version: ${YELLOW}$installed_version."
|
||||
else
|
||||
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
|
||||
print_message info "Version ${YELLOW}$specific_version${GREEN} already installed"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
unbuffered_sed() {
|
||||
if echo | sed -u -e "" >/dev/null 2>&1; then
|
||||
sed -nu "$@"
|
||||
elif echo | sed -l -e "" >/dev/null 2>&1; then
|
||||
sed -nl "$@"
|
||||
else
|
||||
local pad="$(printf "\n%512s" "")"
|
||||
sed -ne "s/$/\\${pad}/" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
print_progress() {
|
||||
local bytes="$1"
|
||||
local length="$2"
|
||||
[ "$length" -gt 0 ] || return 0
|
||||
|
||||
local width=50
|
||||
local percent=$(( bytes * 100 / length ))
|
||||
[ "$percent" -gt 100 ] && percent=100
|
||||
local on=$(( percent * width / 100 ))
|
||||
local off=$(( width - on ))
|
||||
|
||||
local filled=$(printf "%*s" "$on" "")
|
||||
filled=${filled// /■}
|
||||
local empty=$(printf "%*s" "$off" "")
|
||||
empty=${empty// /・}
|
||||
|
||||
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
|
||||
}
|
||||
|
||||
download_with_progress() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
|
||||
if [ -t 2 ]; then
|
||||
exec 4>&2
|
||||
else
|
||||
exec 4>/dev/null
|
||||
fi
|
||||
|
||||
local tmp_dir=${TMPDIR:-/tmp}
|
||||
local basename="${tmp_dir}/opencode_install_$$"
|
||||
local tracefile="${basename}.trace"
|
||||
|
||||
rm -f "$tracefile"
|
||||
mkfifo "$tracefile"
|
||||
|
||||
# Hide cursor
|
||||
printf "\033[?25l" >&4
|
||||
|
||||
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
|
||||
|
||||
(
|
||||
curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
|
||||
) &
|
||||
local curl_pid=$!
|
||||
|
||||
unbuffered_sed \
|
||||
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
|
||||
-e '/^0000: content-length:/p' \
|
||||
-e '/^<= recv data/p' \
|
||||
"$tracefile" | \
|
||||
{
|
||||
local length=0
|
||||
local bytes=0
|
||||
|
||||
while IFS=" " read -r -a line; do
|
||||
[ "${#line[@]}" -lt 2 ] && continue
|
||||
local tag="${line[0]} ${line[1]}"
|
||||
|
||||
if [ "$tag" = "0000: content-length:" ]; then
|
||||
length="${line[2]}"
|
||||
length=$(echo "$length" | tr -d '\r')
|
||||
bytes=0
|
||||
elif [ "$tag" = "<= recv" ]; then
|
||||
local size="${line[3]}"
|
||||
bytes=$(( bytes + size ))
|
||||
if [ "$length" -gt 0 ]; then
|
||||
print_progress "$bytes" "$length"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
wait $curl_pid
|
||||
local ret=$?
|
||||
echo "" >&4
|
||||
return $ret
|
||||
}
|
||||
|
||||
download_and_install() {
|
||||
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
||||
mkdir -p opencodetmp && cd opencodetmp
|
||||
|
||||
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
|
||||
# Fallback to standard curl on Windows or if custom progress fails
|
||||
curl -# -L -o "$filename" "$url"
|
||||
fi
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
tar -xzf "$filename"
|
||||
else
|
||||
unzip -q "$filename"
|
||||
fi
|
||||
|
||||
mv opencode "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/opencode"
|
||||
cd .. && rm -rf opencodetmp
|
||||
curl -# -L $url | tar xz
|
||||
mv opencode $INSTALL_DIR
|
||||
cd .. && rm -rf opencodetmp
|
||||
}
|
||||
|
||||
check_version
|
||||
@@ -266,12 +101,10 @@ add_to_path() {
|
||||
local config_file=$1
|
||||
local command=$2
|
||||
|
||||
if grep -Fxq "$command" "$config_file"; then
|
||||
print_message info "Command already exists in $config_file, skipping write."
|
||||
elif [[ -w $config_file ]]; then
|
||||
if [[ -w $config_file ]]; then
|
||||
echo -e "\n# opencode" >> "$config_file"
|
||||
echo "$command" >> "$config_file"
|
||||
print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file"
|
||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
||||
else
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " $command"
|
||||
@@ -334,7 +167,6 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
*)
|
||||
export PATH=$INSTALL_DIR:$PATH
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
@@ -346,19 +178,3 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
|
||||
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
echo -e ""
|
||||
echo -e "${MUTED} ${NC} ▄ "
|
||||
echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█"
|
||||
echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀"
|
||||
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
|
||||
echo -e ""
|
||||
echo -e "cd <project> ${MUTED}# Open directory${NC}"
|
||||
echo -e "opencode ${MUTED}# Run command${NC}"
|
||||
echo -e ""
|
||||
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
|
||||
echo -e ""
|
||||
echo -e ""
|
||||
|
||||
|
||||
119
internal/app/app.go
Normal file
119
internal/app/app.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"maps"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
"github.com/opencode-ai/opencode/internal/history"
|
||||
"github.com/opencode-ai/opencode/internal/llm/agent"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Sessions session.Service
|
||||
Messages message.Service
|
||||
History history.Service
|
||||
Permissions permission.Service
|
||||
|
||||
CoderAgent agent.Service
|
||||
|
||||
LSPClients map[string]*lsp.Client
|
||||
|
||||
clientsMutex sync.RWMutex
|
||||
|
||||
watcherCancelFuncs []context.CancelFunc
|
||||
cancelFuncsMutex sync.Mutex
|
||||
watcherWG sync.WaitGroup
|
||||
}
|
||||
|
||||
func New(ctx context.Context, conn *sql.DB) (*App, error) {
|
||||
q := db.New(conn)
|
||||
sessions := session.NewService(q)
|
||||
messages := message.NewService(q)
|
||||
files := history.NewService(q, conn)
|
||||
|
||||
app := &App{
|
||||
Sessions: sessions,
|
||||
Messages: messages,
|
||||
History: files,
|
||||
Permissions: permission.NewPermissionService(),
|
||||
LSPClients: make(map[string]*lsp.Client),
|
||||
}
|
||||
|
||||
// Initialize theme based on configuration
|
||||
app.initTheme()
|
||||
|
||||
// Initialize LSP clients in the background
|
||||
go app.initLSPClients(ctx)
|
||||
|
||||
var err error
|
||||
app.CoderAgent, err = agent.NewAgent(
|
||||
config.AgentCoder,
|
||||
app.Sessions,
|
||||
app.Messages,
|
||||
agent.CoderAgentTools(
|
||||
app.Permissions,
|
||||
app.Sessions,
|
||||
app.Messages,
|
||||
app.History,
|
||||
app.LSPClients,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
logging.Error("Failed to create coder agent", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// initTheme sets the application theme based on the configuration
|
||||
func (app *App) initTheme() {
|
||||
cfg := config.Get()
|
||||
if cfg == nil || cfg.TUI.Theme == "" {
|
||||
return // Use default theme
|
||||
}
|
||||
|
||||
// Try to set the theme from config
|
||||
err := theme.SetTheme(cfg.TUI.Theme)
|
||||
if err != nil {
|
||||
logging.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
|
||||
} else {
|
||||
logging.Debug("Set theme from config", "theme", cfg.TUI.Theme)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown performs a clean shutdown of the application
|
||||
func (app *App) Shutdown() {
|
||||
// Cancel all watcher goroutines
|
||||
app.cancelFuncsMutex.Lock()
|
||||
for _, cancel := range app.watcherCancelFuncs {
|
||||
cancel()
|
||||
}
|
||||
app.cancelFuncsMutex.Unlock()
|
||||
app.watcherWG.Wait()
|
||||
|
||||
// Perform additional cleanup for LSP clients
|
||||
app.clientsMutex.RLock()
|
||||
clients := make(map[string]*lsp.Client, len(app.LSPClients))
|
||||
maps.Copy(clients, app.LSPClients)
|
||||
app.clientsMutex.RUnlock()
|
||||
|
||||
for name, client := range clients {
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
if err := client.Shutdown(shutdownCtx); err != nil {
|
||||
logging.Error("Failed to shutdown LSP client", "name", name, "error", err)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
126
internal/app/lsp.go
Normal file
126
internal/app/lsp.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/watcher"
|
||||
)
|
||||
|
||||
func (app *App) initLSPClients(ctx context.Context) {
|
||||
cfg := config.Get()
|
||||
|
||||
// Initialize LSP clients
|
||||
for name, clientConfig := range cfg.LSP {
|
||||
// Start each client initialization in its own goroutine
|
||||
go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
|
||||
}
|
||||
logging.Info("LSP clients initialization started in background")
|
||||
}
|
||||
|
||||
// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
|
||||
func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
|
||||
// Create a specific context for initialization with a timeout
|
||||
logging.Info("Creating LSP client", "name", name, "command", command, "args", args)
|
||||
|
||||
// Create the LSP client
|
||||
lspClient, err := lsp.NewClient(ctx, command, args...)
|
||||
if err != nil {
|
||||
logging.Error("Failed to create LSP client for", name, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a longer timeout for initialization (some servers take time to start)
|
||||
initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Initialize with the initialization context
|
||||
_, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
|
||||
if err != nil {
|
||||
logging.Error("Initialize failed", "name", name, "error", err)
|
||||
// Clean up the client to prevent resource leaks
|
||||
lspClient.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for the server to be ready
|
||||
if err := lspClient.WaitForServerReady(initCtx); err != nil {
|
||||
logging.Error("Server failed to become ready", "name", name, "error", err)
|
||||
// We'll continue anyway, as some functionality might still work
|
||||
lspClient.SetServerState(lsp.StateError)
|
||||
} else {
|
||||
logging.Info("LSP server is ready", "name", name)
|
||||
lspClient.SetServerState(lsp.StateReady)
|
||||
}
|
||||
|
||||
logging.Info("LSP client initialized", "name", name)
|
||||
|
||||
// Create a child context that can be canceled when the app is shutting down
|
||||
watchCtx, cancelFunc := context.WithCancel(ctx)
|
||||
|
||||
// Create a context with the server name for better identification
|
||||
watchCtx = context.WithValue(watchCtx, "serverName", name)
|
||||
|
||||
// Create the workspace watcher
|
||||
workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
|
||||
|
||||
// Store the cancel function to be called during cleanup
|
||||
app.cancelFuncsMutex.Lock()
|
||||
app.watcherCancelFuncs = append(app.watcherCancelFuncs, cancelFunc)
|
||||
app.cancelFuncsMutex.Unlock()
|
||||
|
||||
// Add the watcher to a WaitGroup to track active goroutines
|
||||
app.watcherWG.Add(1)
|
||||
|
||||
// Add to map with mutex protection before starting goroutine
|
||||
app.clientsMutex.Lock()
|
||||
app.LSPClients[name] = lspClient
|
||||
app.clientsMutex.Unlock()
|
||||
|
||||
go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
|
||||
}
|
||||
|
||||
// runWorkspaceWatcher executes the workspace watcher for an LSP client
|
||||
func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
|
||||
defer app.watcherWG.Done()
|
||||
defer logging.RecoverPanic("LSP-"+name, func() {
|
||||
// Try to restart the client
|
||||
app.restartLSPClient(ctx, name)
|
||||
})
|
||||
|
||||
workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
|
||||
logging.Info("Workspace watcher stopped", "client", name)
|
||||
}
|
||||
|
||||
// restartLSPClient attempts to restart a crashed or failed LSP client
|
||||
func (app *App) restartLSPClient(ctx context.Context, name string) {
|
||||
// Get the original configuration
|
||||
cfg := config.Get()
|
||||
clientConfig, exists := cfg.LSP[name]
|
||||
if !exists {
|
||||
logging.Error("Cannot restart client, configuration not found", "client", name)
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up the old client if it exists
|
||||
app.clientsMutex.Lock()
|
||||
oldClient, exists := app.LSPClients[name]
|
||||
if exists {
|
||||
delete(app.LSPClients, name) // Remove from map before potentially slow shutdown
|
||||
}
|
||||
app.clientsMutex.Unlock()
|
||||
|
||||
if exists && oldClient != nil {
|
||||
// Try to shut it down gracefully, but don't block on errors
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_ = oldClient.Shutdown(shutdownCtx)
|
||||
cancel()
|
||||
}
|
||||
|
||||
// Create a new client using the shared function
|
||||
app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
|
||||
logging.Info("Successfully restarted LSP client", "client", name)
|
||||
}
|
||||
813
internal/config/config.go
Normal file
813
internal/config/config.go
Normal file
@@ -0,0 +1,813 @@
|
||||
// Package config manages application configuration from various sources.
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// MCPType defines the type of MCP (Model Control Protocol) server.
|
||||
type MCPType string
|
||||
|
||||
// Supported MCP types
|
||||
const (
|
||||
MCPStdio MCPType = "stdio"
|
||||
MCPSse MCPType = "sse"
|
||||
)
|
||||
|
||||
// MCPServer defines the configuration for a Model Control Protocol server.
|
||||
type MCPServer struct {
|
||||
Command string `json:"command"`
|
||||
Env []string `json:"env"`
|
||||
Args []string `json:"args"`
|
||||
Type MCPType `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
type AgentName string
|
||||
|
||||
const (
|
||||
AgentCoder AgentName = "coder"
|
||||
AgentSummarizer AgentName = "summarizer"
|
||||
AgentTask AgentName = "task"
|
||||
AgentTitle AgentName = "title"
|
||||
)
|
||||
|
||||
// Agent defines configuration for different LLM models and their token limits.
|
||||
type Agent struct {
|
||||
Model models.ModelID `json:"model"`
|
||||
MaxTokens int64 `json:"maxTokens"`
|
||||
ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh
|
||||
}
|
||||
|
||||
// Provider defines configuration for an LLM provider.
|
||||
type Provider struct {
|
||||
APIKey string `json:"apiKey"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
// Data defines storage configuration.
|
||||
type Data struct {
|
||||
Directory string `json:"directory"`
|
||||
}
|
||||
|
||||
// LSPConfig defines configuration for Language Server Protocol integration.
|
||||
type LSPConfig struct {
|
||||
Disabled bool `json:"enabled"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Options any `json:"options"`
|
||||
}
|
||||
|
||||
// TUIConfig defines the configuration for the Terminal User Interface.
|
||||
type TUIConfig struct {
|
||||
Theme string `json:"theme,omitempty"`
|
||||
}
|
||||
|
||||
// Config is the main configuration structure for the application.
|
||||
type Config struct {
|
||||
Data Data `json:"data"`
|
||||
WorkingDir string `json:"wd,omitempty"`
|
||||
MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
|
||||
Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
|
||||
LSP map[string]LSPConfig `json:"lsp,omitempty"`
|
||||
Agents map[AgentName]Agent `json:"agents"`
|
||||
Debug bool `json:"debug,omitempty"`
|
||||
DebugLSP bool `json:"debugLSP,omitempty"`
|
||||
ContextPaths []string `json:"contextPaths,omitempty"`
|
||||
TUI TUIConfig `json:"tui"`
|
||||
AutoCompact bool `json:"autoCompact,omitempty"`
|
||||
}
|
||||
|
||||
// Application constants
|
||||
const (
|
||||
defaultDataDirectory = ".opencode"
|
||||
defaultLogLevel = "info"
|
||||
appName = "opencode"
|
||||
|
||||
MaxTokensFallbackDefault = 4096
|
||||
)
|
||||
|
||||
var defaultContextPaths = []string{
|
||||
".github/copilot-instructions.md",
|
||||
".cursorrules",
|
||||
".cursor/rules/",
|
||||
"CLAUDE.md",
|
||||
"CLAUDE.local.md",
|
||||
"opencode.md",
|
||||
"opencode.local.md",
|
||||
"OpenCode.md",
|
||||
"OpenCode.local.md",
|
||||
"OPENCODE.md",
|
||||
"OPENCODE.local.md",
|
||||
}
|
||||
|
||||
// Global configuration instance
|
||||
var cfg *Config
|
||||
|
||||
// Load initializes the configuration from environment variables and config files.
|
||||
// If debug is true, debug mode is enabled and log level is set to debug.
|
||||
// It returns an error if configuration loading fails.
|
||||
func Load(workingDir string, debug bool) (*Config, error) {
|
||||
if cfg != nil {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
cfg = &Config{
|
||||
WorkingDir: workingDir,
|
||||
MCPServers: make(map[string]MCPServer),
|
||||
Providers: make(map[models.ModelProvider]Provider),
|
||||
LSP: make(map[string]LSPConfig),
|
||||
}
|
||||
|
||||
configureViper()
|
||||
setDefaults(debug)
|
||||
|
||||
// Read global config
|
||||
if err := readConfig(viper.ReadInConfig()); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
// Load and merge local config
|
||||
mergeLocalConfig(workingDir)
|
||||
|
||||
setProviderDefaults()
|
||||
|
||||
// Apply configuration to the struct
|
||||
if err := viper.Unmarshal(cfg); err != nil {
|
||||
return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
applyDefaultValues()
|
||||
defaultLevel := slog.LevelInfo
|
||||
if cfg.Debug {
|
||||
defaultLevel = slog.LevelDebug
|
||||
}
|
||||
if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
|
||||
loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
|
||||
|
||||
// if file does not exist create it
|
||||
if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
|
||||
return cfg, fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
if _, err := os.Create(loggingFile); err != nil {
|
||||
return cfg, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
// Configure logger
|
||||
logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
|
||||
Level: defaultLevel,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
} else {
|
||||
// Configure logger
|
||||
logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
|
||||
Level: defaultLevel,
|
||||
}))
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if err := Validate(); err != nil {
|
||||
return cfg, fmt.Errorf("config validation failed: %w", err)
|
||||
}
|
||||
|
||||
if cfg.Agents == nil {
|
||||
cfg.Agents = make(map[AgentName]Agent)
|
||||
}
|
||||
|
||||
// Override the max tokens for title agent
|
||||
cfg.Agents[AgentTitle] = Agent{
|
||||
Model: cfg.Agents[AgentTitle].Model,
|
||||
MaxTokens: 80,
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// configureViper sets up viper's configuration paths and environment variables.
|
||||
func configureViper() {
|
||||
viper.SetConfigName(fmt.Sprintf(".%s", appName))
|
||||
viper.SetConfigType("json")
|
||||
viper.AddConfigPath("$HOME")
|
||||
viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
|
||||
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
|
||||
viper.SetEnvPrefix(strings.ToUpper(appName))
|
||||
viper.AutomaticEnv()
|
||||
}
|
||||
|
||||
// setDefaults configures default values for configuration options.
|
||||
func setDefaults(debug bool) {
|
||||
viper.SetDefault("data.directory", defaultDataDirectory)
|
||||
viper.SetDefault("contextPaths", defaultContextPaths)
|
||||
viper.SetDefault("tui.theme", "opencode")
|
||||
viper.SetDefault("autoCompact", true)
|
||||
|
||||
if debug {
|
||||
viper.SetDefault("debug", true)
|
||||
viper.Set("log.level", "debug")
|
||||
} else {
|
||||
viper.SetDefault("debug", false)
|
||||
viper.SetDefault("log.level", defaultLogLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// setProviderDefaults configures LLM provider defaults based on provider provided by
|
||||
// environment variables and configuration file.
|
||||
func setProviderDefaults() {
|
||||
// Set all API keys we can find in the environment
|
||||
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.anthropic.apiKey", apiKey)
|
||||
}
|
||||
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.openai.apiKey", apiKey)
|
||||
}
|
||||
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.gemini.apiKey", apiKey)
|
||||
}
|
||||
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.groq.apiKey", apiKey)
|
||||
}
|
||||
if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.openrouter.apiKey", apiKey)
|
||||
}
|
||||
if apiKey := os.Getenv("XAI_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.xai.apiKey", apiKey)
|
||||
}
|
||||
if apiKey := os.Getenv("AZURE_OPENAI_ENDPOINT"); apiKey != "" {
|
||||
// api-key may be empty when using Entra ID credentials – that's okay
|
||||
viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
|
||||
}
|
||||
|
||||
// Use this order to set the default models
|
||||
// 1. Anthropic
|
||||
// 2. OpenAI
|
||||
// 3. Google Gemini
|
||||
// 4. Groq
|
||||
// 5. OpenRouter
|
||||
// 6. AWS Bedrock
|
||||
// 7. Azure
|
||||
|
||||
// Anthropic configuration
|
||||
if key := viper.GetString("providers.anthropic.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
|
||||
viper.SetDefault("agents.summarizer.model", models.Claude37Sonnet)
|
||||
viper.SetDefault("agents.task.model", models.Claude37Sonnet)
|
||||
viper.SetDefault("agents.title.model", models.Claude37Sonnet)
|
||||
return
|
||||
}
|
||||
|
||||
// OpenAI configuration
|
||||
if key := viper.GetString("providers.openai.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.GPT41)
|
||||
viper.SetDefault("agents.summarizer.model", models.GPT41)
|
||||
viper.SetDefault("agents.task.model", models.GPT41Mini)
|
||||
viper.SetDefault("agents.title.model", models.GPT41Mini)
|
||||
return
|
||||
}
|
||||
|
||||
// Google Gemini configuration
|
||||
if key := viper.GetString("providers.gemini.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.Gemini25)
|
||||
viper.SetDefault("agents.summarizer.model", models.Gemini25)
|
||||
viper.SetDefault("agents.task.model", models.Gemini25Flash)
|
||||
viper.SetDefault("agents.title.model", models.Gemini25Flash)
|
||||
return
|
||||
}
|
||||
|
||||
// Groq configuration
|
||||
if key := viper.GetString("providers.groq.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.QWENQwq)
|
||||
viper.SetDefault("agents.summarizer.model", models.QWENQwq)
|
||||
viper.SetDefault("agents.task.model", models.QWENQwq)
|
||||
viper.SetDefault("agents.title.model", models.QWENQwq)
|
||||
return
|
||||
}
|
||||
|
||||
// OpenRouter configuration
|
||||
if key := viper.GetString("providers.openrouter.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
|
||||
viper.SetDefault("agents.summarizer.model", models.OpenRouterClaude37Sonnet)
|
||||
viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
|
||||
viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
|
||||
return
|
||||
}
|
||||
|
||||
// XAI configuration
|
||||
if key := viper.GetString("providers.xai.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.XAIGrok3Beta)
|
||||
viper.SetDefault("agents.summarizer.model", models.XAIGrok3Beta)
|
||||
viper.SetDefault("agents.task.model", models.XAIGrok3Beta)
|
||||
viper.SetDefault("agents.title.model", models.XAiGrok3MiniFastBeta)
|
||||
return
|
||||
}
|
||||
|
||||
// AWS Bedrock configuration
|
||||
if hasAWSCredentials() {
|
||||
viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
|
||||
viper.SetDefault("agents.summarizer.model", models.BedrockClaude37Sonnet)
|
||||
viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet)
|
||||
viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
|
||||
return
|
||||
}
|
||||
|
||||
// Azure OpenAI configuration
|
||||
if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
|
||||
viper.SetDefault("agents.coder.model", models.AzureGPT41)
|
||||
viper.SetDefault("agents.summarizer.model", models.AzureGPT41)
|
||||
viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
|
||||
viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// hasAWSCredentials checks if AWS credentials are available in the environment.
|
||||
func hasAWSCredentials() bool {
|
||||
// Check for explicit AWS credentials
|
||||
if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for AWS profile
|
||||
if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for AWS region
|
||||
if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if running on EC2 with instance profile
|
||||
if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
|
||||
os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// readConfig handles the result of reading a configuration file.
|
||||
func readConfig(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// It's okay if the config file doesn't exist
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
// mergeLocalConfig loads and merges configuration from the local directory.
|
||||
func mergeLocalConfig(workingDir string) {
|
||||
local := viper.New()
|
||||
local.SetConfigName(fmt.Sprintf(".%s", appName))
|
||||
local.SetConfigType("json")
|
||||
local.AddConfigPath(workingDir)
|
||||
|
||||
// Merge local config if it exists
|
||||
if err := local.ReadInConfig(); err == nil {
|
||||
viper.MergeConfigMap(local.AllSettings())
|
||||
}
|
||||
}
|
||||
|
||||
// applyDefaultValues sets default values for configuration fields that need processing.
|
||||
func applyDefaultValues() {
|
||||
// Set default MCP type if not specified
|
||||
for k, v := range cfg.MCPServers {
|
||||
if v.Type == "" {
|
||||
v.Type = MCPStdio
|
||||
cfg.MCPServers[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It validates model IDs and providers, ensuring they are supported.
|
||||
func validateAgent(cfg *Config, name AgentName, agent Agent) error {
|
||||
// Check if model exists
|
||||
model, modelExists := models.SupportedModels[agent.Model]
|
||||
if !modelExists {
|
||||
logging.Warn("unsupported model configured, reverting to default",
|
||||
"agent", name,
|
||||
"configured_model", agent.Model)
|
||||
|
||||
// Set default model based on available providers
|
||||
if setDefaultModelForAgent(name) {
|
||||
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
|
||||
} else {
|
||||
return fmt.Errorf("no valid provider available for agent %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if provider for the model is configured
|
||||
provider := model.Provider
|
||||
providerCfg, providerExists := cfg.Providers[provider]
|
||||
|
||||
if !providerExists {
|
||||
// Provider not configured, check if we have environment variables
|
||||
apiKey := getProviderAPIKey(provider)
|
||||
if apiKey == "" {
|
||||
logging.Warn("provider not configured for model, reverting to default",
|
||||
"agent", name,
|
||||
"model", agent.Model,
|
||||
"provider", provider)
|
||||
|
||||
// Set default model based on available providers
|
||||
if setDefaultModelForAgent(name) {
|
||||
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
|
||||
} else {
|
||||
return fmt.Errorf("no valid provider available for agent %s", name)
|
||||
}
|
||||
} else {
|
||||
// Add provider with API key from environment
|
||||
cfg.Providers[provider] = Provider{
|
||||
APIKey: apiKey,
|
||||
}
|
||||
logging.Info("added provider from environment", "provider", provider)
|
||||
}
|
||||
} else if providerCfg.Disabled || providerCfg.APIKey == "" {
|
||||
// Provider is disabled or has no API key
|
||||
logging.Warn("provider is disabled or has no API key, reverting to default",
|
||||
"agent", name,
|
||||
"model", agent.Model,
|
||||
"provider", provider)
|
||||
|
||||
// Set default model based on available providers
|
||||
if setDefaultModelForAgent(name) {
|
||||
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
|
||||
} else {
|
||||
return fmt.Errorf("no valid provider available for agent %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate max tokens
|
||||
if agent.MaxTokens <= 0 {
|
||||
logging.Warn("invalid max tokens, setting to default",
|
||||
"agent", name,
|
||||
"model", agent.Model,
|
||||
"max_tokens", agent.MaxTokens)
|
||||
|
||||
// Update the agent with default max tokens
|
||||
updatedAgent := cfg.Agents[name]
|
||||
if model.DefaultMaxTokens > 0 {
|
||||
updatedAgent.MaxTokens = model.DefaultMaxTokens
|
||||
} else {
|
||||
updatedAgent.MaxTokens = MaxTokensFallbackDefault
|
||||
}
|
||||
cfg.Agents[name] = updatedAgent
|
||||
} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
|
||||
// Ensure max tokens doesn't exceed half the context window (reasonable limit)
|
||||
logging.Warn("max tokens exceeds half the context window, adjusting",
|
||||
"agent", name,
|
||||
"model", agent.Model,
|
||||
"max_tokens", agent.MaxTokens,
|
||||
"context_window", model.ContextWindow)
|
||||
|
||||
// Update the agent with adjusted max tokens
|
||||
updatedAgent := cfg.Agents[name]
|
||||
updatedAgent.MaxTokens = model.ContextWindow / 2
|
||||
cfg.Agents[name] = updatedAgent
|
||||
}
|
||||
|
||||
// Validate reasoning effort for models that support reasoning
|
||||
if model.CanReason && provider == models.ProviderOpenAI {
|
||||
if agent.ReasoningEffort == "" {
|
||||
// Set default reasoning effort for models that support it
|
||||
logging.Info("setting default reasoning effort for model that supports reasoning",
|
||||
"agent", name,
|
||||
"model", agent.Model)
|
||||
|
||||
// Update the agent with default reasoning effort
|
||||
updatedAgent := cfg.Agents[name]
|
||||
updatedAgent.ReasoningEffort = "medium"
|
||||
cfg.Agents[name] = updatedAgent
|
||||
} else {
|
||||
// Check if reasoning effort is valid (low, medium, high)
|
||||
effort := strings.ToLower(agent.ReasoningEffort)
|
||||
if effort != "low" && effort != "medium" && effort != "high" {
|
||||
logging.Warn("invalid reasoning effort, setting to medium",
|
||||
"agent", name,
|
||||
"model", agent.Model,
|
||||
"reasoning_effort", agent.ReasoningEffort)
|
||||
|
||||
// Update the agent with valid reasoning effort
|
||||
updatedAgent := cfg.Agents[name]
|
||||
updatedAgent.ReasoningEffort = "medium"
|
||||
cfg.Agents[name] = updatedAgent
|
||||
}
|
||||
}
|
||||
} else if !model.CanReason && agent.ReasoningEffort != "" {
|
||||
// Model doesn't support reasoning but reasoning effort is set
|
||||
logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
|
||||
"agent", name,
|
||||
"model", agent.Model,
|
||||
"reasoning_effort", agent.ReasoningEffort)
|
||||
|
||||
// Update the agent to remove reasoning effort
|
||||
updatedAgent := cfg.Agents[name]
|
||||
updatedAgent.ReasoningEffort = ""
|
||||
cfg.Agents[name] = updatedAgent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid and applies defaults where needed.
|
||||
func Validate() error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config not loaded")
|
||||
}
|
||||
|
||||
// Validate agent models
|
||||
for name, agent := range cfg.Agents {
|
||||
if err := validateAgent(cfg, name, agent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate providers
|
||||
for provider, providerCfg := range cfg.Providers {
|
||||
if providerCfg.APIKey == "" && !providerCfg.Disabled {
|
||||
logging.Warn("provider has no API key, marking as disabled", "provider", provider)
|
||||
providerCfg.Disabled = true
|
||||
cfg.Providers[provider] = providerCfg
|
||||
}
|
||||
}
|
||||
|
||||
// Validate LSP configurations
|
||||
for language, lspConfig := range cfg.LSP {
|
||||
if lspConfig.Command == "" && !lspConfig.Disabled {
|
||||
logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
|
||||
lspConfig.Disabled = true
|
||||
cfg.LSP[language] = lspConfig
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getProviderAPIKey gets the API key for a provider from environment variables
|
||||
func getProviderAPIKey(provider models.ModelProvider) string {
|
||||
switch provider {
|
||||
case models.ProviderAnthropic:
|
||||
return os.Getenv("ANTHROPIC_API_KEY")
|
||||
case models.ProviderOpenAI:
|
||||
return os.Getenv("OPENAI_API_KEY")
|
||||
case models.ProviderGemini:
|
||||
return os.Getenv("GEMINI_API_KEY")
|
||||
case models.ProviderGROQ:
|
||||
return os.Getenv("GROQ_API_KEY")
|
||||
case models.ProviderAzure:
|
||||
return os.Getenv("AZURE_OPENAI_API_KEY")
|
||||
case models.ProviderOpenRouter:
|
||||
return os.Getenv("OPENROUTER_API_KEY")
|
||||
case models.ProviderBedrock:
|
||||
if hasAWSCredentials() {
|
||||
return "aws-credentials-available"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setDefaultModelForAgent sets a default model for an agent based on available providers
|
||||
func setDefaultModelForAgent(agent AgentName) bool {
|
||||
// Check providers in order of preference
|
||||
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
|
||||
maxTokens := int64(5000)
|
||||
if agent == AgentTitle {
|
||||
maxTokens = 80
|
||||
}
|
||||
cfg.Agents[agent] = Agent{
|
||||
Model: models.Claude37Sonnet,
|
||||
MaxTokens: maxTokens,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
||||
var model models.ModelID
|
||||
maxTokens := int64(5000)
|
||||
reasoningEffort := ""
|
||||
|
||||
switch agent {
|
||||
case AgentTitle:
|
||||
model = models.GPT41Mini
|
||||
maxTokens = 80
|
||||
case AgentTask:
|
||||
model = models.GPT41Mini
|
||||
default:
|
||||
model = models.GPT41
|
||||
}
|
||||
|
||||
// Check if model supports reasoning
|
||||
if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
|
||||
reasoningEffort = "medium"
|
||||
}
|
||||
|
||||
cfg.Agents[agent] = Agent{
|
||||
Model: model,
|
||||
MaxTokens: maxTokens,
|
||||
ReasoningEffort: reasoningEffort,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
|
||||
var model models.ModelID
|
||||
maxTokens := int64(5000)
|
||||
reasoningEffort := ""
|
||||
|
||||
switch agent {
|
||||
case AgentTitle:
|
||||
model = models.OpenRouterClaude35Haiku
|
||||
maxTokens = 80
|
||||
case AgentTask:
|
||||
model = models.OpenRouterClaude37Sonnet
|
||||
default:
|
||||
model = models.OpenRouterClaude37Sonnet
|
||||
}
|
||||
|
||||
// Check if model supports reasoning
|
||||
if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
|
||||
reasoningEffort = "medium"
|
||||
}
|
||||
|
||||
cfg.Agents[agent] = Agent{
|
||||
Model: model,
|
||||
MaxTokens: maxTokens,
|
||||
ReasoningEffort: reasoningEffort,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
|
||||
var model models.ModelID
|
||||
maxTokens := int64(5000)
|
||||
|
||||
if agent == AgentTitle {
|
||||
model = models.Gemini25Flash
|
||||
maxTokens = 80
|
||||
} else {
|
||||
model = models.Gemini25
|
||||
}
|
||||
|
||||
cfg.Agents[agent] = Agent{
|
||||
Model: model,
|
||||
MaxTokens: maxTokens,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
|
||||
maxTokens := int64(5000)
|
||||
if agent == AgentTitle {
|
||||
maxTokens = 80
|
||||
}
|
||||
|
||||
cfg.Agents[agent] = Agent{
|
||||
Model: models.QWENQwq,
|
||||
MaxTokens: maxTokens,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if hasAWSCredentials() {
|
||||
maxTokens := int64(5000)
|
||||
if agent == AgentTitle {
|
||||
maxTokens = 80
|
||||
}
|
||||
|
||||
cfg.Agents[agent] = Agent{
|
||||
Model: models.BedrockClaude37Sonnet,
|
||||
MaxTokens: maxTokens,
|
||||
ReasoningEffort: "medium", // Claude models support reasoning
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Get returns the current configuration.
|
||||
// It's safe to call this function multiple times.
|
||||
func Get() *Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// WorkingDirectory returns the current working directory from the configuration.
|
||||
func WorkingDirectory() string {
|
||||
if cfg == nil {
|
||||
panic("config not loaded")
|
||||
}
|
||||
return cfg.WorkingDir
|
||||
}
|
||||
|
||||
func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
|
||||
if cfg == nil {
|
||||
panic("config not loaded")
|
||||
}
|
||||
|
||||
existingAgentCfg := cfg.Agents[agentName]
|
||||
|
||||
model, ok := models.SupportedModels[modelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("model %s not supported", modelID)
|
||||
}
|
||||
|
||||
maxTokens := existingAgentCfg.MaxTokens
|
||||
if model.DefaultMaxTokens > 0 {
|
||||
maxTokens = model.DefaultMaxTokens
|
||||
}
|
||||
|
||||
newAgentCfg := Agent{
|
||||
Model: modelID,
|
||||
MaxTokens: maxTokens,
|
||||
ReasoningEffort: existingAgentCfg.ReasoningEffort,
|
||||
}
|
||||
cfg.Agents[agentName] = newAgentCfg
|
||||
|
||||
if err := validateAgent(cfg, agentName, newAgentCfg); err != nil {
|
||||
// revert config update on failure
|
||||
cfg.Agents[agentName] = existingAgentCfg
|
||||
return fmt.Errorf("failed to update agent model: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTheme updates the theme in the configuration and writes it to the config file.
|
||||
func UpdateTheme(themeName string) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config not loaded")
|
||||
}
|
||||
|
||||
// Update the in-memory config
|
||||
cfg.TUI.Theme = themeName
|
||||
|
||||
// Get the config file path
|
||||
configFile := viper.ConfigFileUsed()
|
||||
var configData []byte
|
||||
if configFile == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
|
||||
logging.Info("config file not found, creating new one", "path", configFile)
|
||||
configData = []byte(`{}`)
|
||||
} else {
|
||||
// Read the existing config file
|
||||
data, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
configData = data
|
||||
}
|
||||
|
||||
// Parse the JSON
|
||||
var configMap map[string]interface{}
|
||||
if err := json.Unmarshal(configData, &configMap); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Update just the theme value
|
||||
tuiConfig, ok := configMap["tui"].(map[string]interface{})
|
||||
if !ok {
|
||||
// TUI config doesn't exist yet, create it
|
||||
configMap["tui"] = map[string]interface{}{"theme": themeName}
|
||||
} else {
|
||||
// Update existing TUI config
|
||||
tuiConfig["theme"] = themeName
|
||||
configMap["tui"] = tuiConfig
|
||||
}
|
||||
|
||||
// Write the updated config back to file
|
||||
updatedData, err := json.MarshalIndent(configMap, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
internal/config/init.go
Normal file
61
internal/config/init.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
// InitFlagFilename is the name of the file that indicates whether the project has been initialized
|
||||
InitFlagFilename = "init"
|
||||
)
|
||||
|
||||
// ProjectInitFlag represents the initialization status for a project directory
|
||||
type ProjectInitFlag struct {
|
||||
Initialized bool `json:"initialized"`
|
||||
}
|
||||
|
||||
// ShouldShowInitDialog checks if the initialization dialog should be shown for the current directory
|
||||
func ShouldShowInitDialog() (bool, error) {
|
||||
if cfg == nil {
|
||||
return false, fmt.Errorf("config not loaded")
|
||||
}
|
||||
|
||||
// Create the flag file path
|
||||
flagFilePath := filepath.Join(cfg.Data.Directory, InitFlagFilename)
|
||||
|
||||
// Check if the flag file exists
|
||||
_, err := os.Stat(flagFilePath)
|
||||
if err == nil {
|
||||
// File exists, don't show the dialog
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If the error is not "file not found", return the error
|
||||
if !os.IsNotExist(err) {
|
||||
return false, fmt.Errorf("failed to check init flag file: %w", err)
|
||||
}
|
||||
|
||||
// File doesn't exist, show the dialog
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// MarkProjectInitialized marks the current project as initialized
|
||||
func MarkProjectInitialized() error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config not loaded")
|
||||
}
|
||||
// Create the flag file path
|
||||
flagFilePath := filepath.Join(cfg.Data.Directory, InitFlagFilename)
|
||||
|
||||
// Create an empty file to mark the project as initialized
|
||||
file, err := os.Create(flagFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create init flag file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
68
internal/db/connect.go
Normal file
68
internal/db/connect.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func Connect() (*sql.DB, error) {
|
||||
dataDir := config.Get().Data.Directory
|
||||
if dataDir == "" {
|
||||
return nil, fmt.Errorf("data.dir is not set")
|
||||
}
|
||||
if err := os.MkdirAll(dataDir, 0o700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
dbPath := filepath.Join(dataDir, "opencode.db")
|
||||
// Open the SQLite database
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// Verify connection
|
||||
if err = db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
// Set pragmas for better performance
|
||||
pragmas := []string{
|
||||
"PRAGMA foreign_keys = ON;",
|
||||
"PRAGMA journal_mode = WAL;",
|
||||
"PRAGMA page_size = 4096;",
|
||||
"PRAGMA cache_size = -8000;",
|
||||
"PRAGMA synchronous = NORMAL;",
|
||||
}
|
||||
|
||||
for _, pragma := range pragmas {
|
||||
if _, err = db.Exec(pragma); err != nil {
|
||||
logging.Error("Failed to set pragma", pragma, err)
|
||||
} else {
|
||||
logging.Debug("Set pragma", "pragma", pragma)
|
||||
}
|
||||
}
|
||||
|
||||
goose.SetBaseFS(FS)
|
||||
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
logging.Error("Failed to set dialect", "error", err)
|
||||
return nil, fmt.Errorf("failed to set dialect: %w", err)
|
||||
}
|
||||
|
||||
if err := goose.Up(db, "migrations"); err != nil {
|
||||
logging.Error("Failed to apply migrations", "error", err)
|
||||
return nil, fmt.Errorf("failed to apply migrations: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
288
internal/db/db.go
Normal file
288
internal/db/db.go
Normal file
@@ -0,0 +1,288 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
q := Queries{db: db}
|
||||
var err error
|
||||
if q.createFileStmt, err = db.PrepareContext(ctx, createFile); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query CreateFile: %w", err)
|
||||
}
|
||||
if q.createMessageStmt, err = db.PrepareContext(ctx, createMessage); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query CreateMessage: %w", err)
|
||||
}
|
||||
if q.createSessionStmt, err = db.PrepareContext(ctx, createSession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query CreateSession: %w", err)
|
||||
}
|
||||
if q.deleteFileStmt, err = db.PrepareContext(ctx, deleteFile); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteFile: %w", err)
|
||||
}
|
||||
if q.deleteMessageStmt, err = db.PrepareContext(ctx, deleteMessage); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteMessage: %w", err)
|
||||
}
|
||||
if q.deleteSessionStmt, err = db.PrepareContext(ctx, deleteSession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteSession: %w", err)
|
||||
}
|
||||
if q.deleteSessionFilesStmt, err = db.PrepareContext(ctx, deleteSessionFiles); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteSessionFiles: %w", err)
|
||||
}
|
||||
if q.deleteSessionMessagesStmt, err = db.PrepareContext(ctx, deleteSessionMessages); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteSessionMessages: %w", err)
|
||||
}
|
||||
if q.getFileStmt, err = db.PrepareContext(ctx, getFile); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetFile: %w", err)
|
||||
}
|
||||
if q.getFileByPathAndSessionStmt, err = db.PrepareContext(ctx, getFileByPathAndSession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetFileByPathAndSession: %w", err)
|
||||
}
|
||||
if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
|
||||
}
|
||||
if q.getSessionByIDStmt, err = db.PrepareContext(ctx, getSessionByID); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetSessionByID: %w", err)
|
||||
}
|
||||
if q.listFilesByPathStmt, err = db.PrepareContext(ctx, listFilesByPath); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListFilesByPath: %w", err)
|
||||
}
|
||||
if q.listFilesBySessionStmt, err = db.PrepareContext(ctx, listFilesBySession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListFilesBySession: %w", err)
|
||||
}
|
||||
if q.listLatestSessionFilesStmt, err = db.PrepareContext(ctx, listLatestSessionFiles); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListLatestSessionFiles: %w", err)
|
||||
}
|
||||
if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
|
||||
}
|
||||
if q.listNewFilesStmt, err = db.PrepareContext(ctx, listNewFiles); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListNewFiles: %w", err)
|
||||
}
|
||||
if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListSessions: %w", err)
|
||||
}
|
||||
if q.updateFileStmt, err = db.PrepareContext(ctx, updateFile); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query UpdateFile: %w", err)
|
||||
}
|
||||
if q.updateMessageStmt, err = db.PrepareContext(ctx, updateMessage); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query UpdateMessage: %w", err)
|
||||
}
|
||||
if q.updateSessionStmt, err = db.PrepareContext(ctx, updateSession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query UpdateSession: %w", err)
|
||||
}
|
||||
return &q, nil
|
||||
}
|
||||
|
||||
func (q *Queries) Close() error {
|
||||
var err error
|
||||
if q.createFileStmt != nil {
|
||||
if cerr := q.createFileStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing createFileStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.createMessageStmt != nil {
|
||||
if cerr := q.createMessageStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing createMessageStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.createSessionStmt != nil {
|
||||
if cerr := q.createSessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing createSessionStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.deleteFileStmt != nil {
|
||||
if cerr := q.deleteFileStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteFileStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.deleteMessageStmt != nil {
|
||||
if cerr := q.deleteMessageStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteMessageStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.deleteSessionStmt != nil {
|
||||
if cerr := q.deleteSessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteSessionStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.deleteSessionFilesStmt != nil {
|
||||
if cerr := q.deleteSessionFilesStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteSessionFilesStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.deleteSessionMessagesStmt != nil {
|
||||
if cerr := q.deleteSessionMessagesStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteSessionMessagesStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.getFileStmt != nil {
|
||||
if cerr := q.getFileStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing getFileStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.getFileByPathAndSessionStmt != nil {
|
||||
if cerr := q.getFileByPathAndSessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing getFileByPathAndSessionStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.getMessageStmt != nil {
|
||||
if cerr := q.getMessageStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.getSessionByIDStmt != nil {
|
||||
if cerr := q.getSessionByIDStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing getSessionByIDStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listFilesByPathStmt != nil {
|
||||
if cerr := q.listFilesByPathStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listFilesByPathStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listFilesBySessionStmt != nil {
|
||||
if cerr := q.listFilesBySessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listFilesBySessionStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listLatestSessionFilesStmt != nil {
|
||||
if cerr := q.listLatestSessionFilesStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listLatestSessionFilesStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listMessagesBySessionStmt != nil {
|
||||
if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listNewFilesStmt != nil {
|
||||
if cerr := q.listNewFilesStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listNewFilesStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listSessionsStmt != nil {
|
||||
if cerr := q.listSessionsStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listSessionsStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.updateFileStmt != nil {
|
||||
if cerr := q.updateFileStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing updateFileStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.updateMessageStmt != nil {
|
||||
if cerr := q.updateMessageStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing updateMessageStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.updateSessionStmt != nil {
|
||||
if cerr := q.updateSessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing updateSessionStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.ExecContext(ctx, args...)
|
||||
default:
|
||||
return q.db.ExecContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.QueryContext(ctx, args...)
|
||||
default:
|
||||
return q.db.QueryContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row {
|
||||
switch {
|
||||
case stmt != nil && q.tx != nil:
|
||||
return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...)
|
||||
case stmt != nil:
|
||||
return stmt.QueryRowContext(ctx, args...)
|
||||
default:
|
||||
return q.db.QueryRowContext(ctx, query, args...)
|
||||
}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
tx *sql.Tx
|
||||
createFileStmt *sql.Stmt
|
||||
createMessageStmt *sql.Stmt
|
||||
createSessionStmt *sql.Stmt
|
||||
deleteFileStmt *sql.Stmt
|
||||
deleteMessageStmt *sql.Stmt
|
||||
deleteSessionStmt *sql.Stmt
|
||||
deleteSessionFilesStmt *sql.Stmt
|
||||
deleteSessionMessagesStmt *sql.Stmt
|
||||
getFileStmt *sql.Stmt
|
||||
getFileByPathAndSessionStmt *sql.Stmt
|
||||
getMessageStmt *sql.Stmt
|
||||
getSessionByIDStmt *sql.Stmt
|
||||
listFilesByPathStmt *sql.Stmt
|
||||
listFilesBySessionStmt *sql.Stmt
|
||||
listLatestSessionFilesStmt *sql.Stmt
|
||||
listMessagesBySessionStmt *sql.Stmt
|
||||
listNewFilesStmt *sql.Stmt
|
||||
listSessionsStmt *sql.Stmt
|
||||
updateFileStmt *sql.Stmt
|
||||
updateMessageStmt *sql.Stmt
|
||||
updateSessionStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
tx: tx,
|
||||
createFileStmt: q.createFileStmt,
|
||||
createMessageStmt: q.createMessageStmt,
|
||||
createSessionStmt: q.createSessionStmt,
|
||||
deleteFileStmt: q.deleteFileStmt,
|
||||
deleteMessageStmt: q.deleteMessageStmt,
|
||||
deleteSessionStmt: q.deleteSessionStmt,
|
||||
deleteSessionFilesStmt: q.deleteSessionFilesStmt,
|
||||
deleteSessionMessagesStmt: q.deleteSessionMessagesStmt,
|
||||
getFileStmt: q.getFileStmt,
|
||||
getFileByPathAndSessionStmt: q.getFileByPathAndSessionStmt,
|
||||
getMessageStmt: q.getMessageStmt,
|
||||
getSessionByIDStmt: q.getSessionByIDStmt,
|
||||
listFilesByPathStmt: q.listFilesByPathStmt,
|
||||
listFilesBySessionStmt: q.listFilesBySessionStmt,
|
||||
listLatestSessionFilesStmt: q.listLatestSessionFilesStmt,
|
||||
listMessagesBySessionStmt: q.listMessagesBySessionStmt,
|
||||
listNewFilesStmt: q.listNewFilesStmt,
|
||||
listSessionsStmt: q.listSessionsStmt,
|
||||
updateFileStmt: q.updateFileStmt,
|
||||
updateMessageStmt: q.updateMessageStmt,
|
||||
updateSessionStmt: q.updateSessionStmt,
|
||||
}
|
||||
}
|
||||
6
internal/db/embed.go
Normal file
6
internal/db/embed.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package db
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var FS embed.FS
|
||||
311
internal/db/files.sql.go
Normal file
311
internal/db/files.sql.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: files.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const createFile = `-- name: CreateFile :one
|
||||
INSERT INTO files (
|
||||
id,
|
||||
session_id,
|
||||
path,
|
||||
content,
|
||||
version,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
)
|
||||
RETURNING id, session_id, path, content, version, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateFileParams struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
|
||||
row := q.queryRow(ctx, q.createFileStmt, createFile,
|
||||
arg.ID,
|
||||
arg.SessionID,
|
||||
arg.Path,
|
||||
arg.Content,
|
||||
arg.Version,
|
||||
)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteFile = `-- name: DeleteFile :exec
|
||||
DELETE FROM files
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteFile(ctx context.Context, id string) error {
|
||||
_, err := q.exec(ctx, q.deleteFileStmt, deleteFile, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteSessionFiles = `-- name: DeleteSessionFiles :exec
|
||||
DELETE FROM files
|
||||
WHERE session_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSessionFiles(ctx context.Context, sessionID string) error {
|
||||
_, err := q.exec(ctx, q.deleteSessionFilesStmt, deleteSessionFiles, sessionID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getFile = `-- name: GetFile :one
|
||||
SELECT id, session_id, path, content, version, created_at, updated_at
|
||||
FROM files
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetFile(ctx context.Context, id string) (File, error) {
|
||||
row := q.queryRow(ctx, q.getFileStmt, getFile, id)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getFileByPathAndSession = `-- name: GetFileByPathAndSession :one
|
||||
SELECT id, session_id, path, content, version, created_at, updated_at
|
||||
FROM files
|
||||
WHERE path = ? AND session_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
type GetFileByPathAndSessionParams struct {
|
||||
Path string `json:"path"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error) {
|
||||
row := q.queryRow(ctx, q.getFileByPathAndSessionStmt, getFileByPathAndSession, arg.Path, arg.SessionID)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listFilesByPath = `-- name: ListFilesByPath :many
|
||||
SELECT id, session_id, path, content, version, created_at, updated_at
|
||||
FROM files
|
||||
WHERE path = ?
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) {
|
||||
rows, err := q.query(ctx, q.listFilesByPathStmt, listFilesByPath, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []File{}
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listFilesBySession = `-- name: ListFilesBySession :many
|
||||
SELECT id, session_id, path, content, version, created_at, updated_at
|
||||
FROM files
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) {
|
||||
rows, err := q.query(ctx, q.listFilesBySessionStmt, listFilesBySession, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []File{}
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLatestSessionFiles = `-- name: ListLatestSessionFiles :many
|
||||
SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
|
||||
FROM files f
|
||||
INNER JOIN (
|
||||
SELECT path, MAX(created_at) as max_created_at
|
||||
FROM files
|
||||
GROUP BY path
|
||||
) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
|
||||
WHERE f.session_id = ?
|
||||
ORDER BY f.path
|
||||
`
|
||||
|
||||
func (q *Queries) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) {
|
||||
rows, err := q.query(ctx, q.listLatestSessionFilesStmt, listLatestSessionFiles, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []File{}
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listNewFiles = `-- name: ListNewFiles :many
|
||||
SELECT id, session_id, path, content, version, created_at, updated_at
|
||||
FROM files
|
||||
WHERE is_new = 1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
|
||||
rows, err := q.query(ctx, q.listNewFilesStmt, listNewFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []File{}
|
||||
for rows.Next() {
|
||||
var i File
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateFile = `-- name: UpdateFile :one
|
||||
UPDATE files
|
||||
SET
|
||||
content = ?,
|
||||
version = ?,
|
||||
updated_at = strftime('%s', 'now')
|
||||
WHERE id = ?
|
||||
RETURNING id, session_id, path, content, version, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateFileParams struct {
|
||||
Content string `json:"content"`
|
||||
Version string `json:"version"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error) {
|
||||
row := q.queryRow(ctx, q.updateFileStmt, updateFile, arg.Content, arg.Version, arg.ID)
|
||||
var i File
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Path,
|
||||
&i.Content,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
157
internal/db/messages.sql.go
Normal file
157
internal/db/messages.sql.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: messages.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createMessage = `-- name: CreateMessage :one
|
||||
INSERT INTO messages (
|
||||
id,
|
||||
session_id,
|
||||
role,
|
||||
parts,
|
||||
model,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
)
|
||||
RETURNING id, session_id, role, parts, model, created_at, updated_at, finished_at
|
||||
`
|
||||
|
||||
type CreateMessageParams struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Role string `json:"role"`
|
||||
Parts string `json:"parts"`
|
||||
Model sql.NullString `json:"model"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
|
||||
row := q.queryRow(ctx, q.createMessageStmt, createMessage,
|
||||
arg.ID,
|
||||
arg.SessionID,
|
||||
arg.Role,
|
||||
arg.Parts,
|
||||
arg.Model,
|
||||
)
|
||||
var i Message
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Role,
|
||||
&i.Parts,
|
||||
&i.Model,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FinishedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteMessage = `-- name: DeleteMessage :exec
|
||||
DELETE FROM messages
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteMessage(ctx context.Context, id string) error {
|
||||
_, err := q.exec(ctx, q.deleteMessageStmt, deleteMessage, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteSessionMessages = `-- name: DeleteSessionMessages :exec
|
||||
DELETE FROM messages
|
||||
WHERE session_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) error {
|
||||
_, err := q.exec(ctx, q.deleteSessionMessagesStmt, deleteSessionMessages, sessionID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getMessage = `-- name: GetMessage :one
|
||||
SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at
|
||||
FROM messages
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
|
||||
row := q.queryRow(ctx, q.getMessageStmt, getMessage, id)
|
||||
var i Message
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Role,
|
||||
&i.Parts,
|
||||
&i.Model,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FinishedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listMessagesBySession = `-- name: ListMessagesBySession :many
|
||||
SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at
|
||||
FROM messages
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error) {
|
||||
rows, err := q.query(ctx, q.listMessagesBySessionStmt, listMessagesBySession, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Message{}
|
||||
for rows.Next() {
|
||||
var i Message
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Role,
|
||||
&i.Parts,
|
||||
&i.Model,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FinishedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateMessage = `-- name: UpdateMessage :exec
|
||||
UPDATE messages
|
||||
SET
|
||||
parts = ?,
|
||||
finished_at = ?,
|
||||
updated_at = strftime('%s', 'now')
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateMessageParams struct {
|
||||
Parts string `json:"parts"`
|
||||
FinishedAt sql.NullInt64 `json:"finished_at"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error {
|
||||
_, err := q.exec(ctx, q.updateMessageStmt, updateMessage, arg.Parts, arg.FinishedAt, arg.ID)
|
||||
return err
|
||||
}
|
||||
98
internal/db/migrations/20250424200609_initial.sql
Normal file
98
internal/db/migrations/20250424200609_initial.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Sessions
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
parent_session_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
message_count INTEGER NOT NULL DEFAULT 0 CHECK (message_count >= 0),
|
||||
prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0),
|
||||
completion_tokens INTEGER NOT NULL DEFAULT 0 CHECK (completion_tokens>= 0),
|
||||
cost REAL NOT NULL DEFAULT 0.0 CHECK (cost >= 0.0),
|
||||
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
created_at INTEGER NOT NULL -- Unix timestamp in milliseconds
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS update_sessions_updated_at
|
||||
AFTER UPDATE ON sessions
|
||||
BEGIN
|
||||
UPDATE sessions SET updated_at = strftime('%s', 'now')
|
||||
WHERE id = new.id;
|
||||
END;
|
||||
|
||||
-- Files
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
|
||||
UNIQUE(path, session_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_files_session_id ON files (session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_path ON files (path);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS update_files_updated_at
|
||||
AFTER UPDATE ON files
|
||||
BEGIN
|
||||
UPDATE files SET updated_at = strftime('%s', 'now')
|
||||
WHERE id = new.id;
|
||||
END;
|
||||
|
||||
-- Messages
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
parts TEXT NOT NULL default '[]',
|
||||
model TEXT,
|
||||
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
finished_at INTEGER, -- Unix timestamp in milliseconds
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages (session_id);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS update_messages_updated_at
|
||||
AFTER UPDATE ON messages
|
||||
BEGIN
|
||||
UPDATE messages SET updated_at = strftime('%s', 'now')
|
||||
WHERE id = new.id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS update_session_message_count_on_insert
|
||||
AFTER INSERT ON messages
|
||||
BEGIN
|
||||
UPDATE sessions SET
|
||||
message_count = message_count + 1
|
||||
WHERE id = new.session_id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS update_session_message_count_on_delete
|
||||
AFTER DELETE ON messages
|
||||
BEGIN
|
||||
UPDATE sessions SET
|
||||
message_count = message_count - 1
|
||||
WHERE id = old.session_id;
|
||||
END;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TRIGGER IF EXISTS update_sessions_updated_at;
|
||||
DROP TRIGGER IF EXISTS update_messages_updated_at;
|
||||
DROP TRIGGER IF EXISTS update_files_updated_at;
|
||||
|
||||
DROP TRIGGER IF EXISTS update_session_message_count_on_delete;
|
||||
DROP TRIGGER IF EXISTS update_session_message_count_on_insert;
|
||||
|
||||
DROP TABLE IF EXISTS sessions;
|
||||
DROP TABLE IF EXISTS messages;
|
||||
DROP TABLE IF EXISTS files;
|
||||
-- +goose StatementEnd
|
||||
42
internal/db/models.go
Normal file
42
internal/db/models.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Version string `json:"version"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Role string `json:"role"`
|
||||
Parts string `json:"parts"`
|
||||
Model sql.NullString `json:"model"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
FinishedAt sql.NullInt64 `json:"finished_at"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID string `json:"id"`
|
||||
ParentSessionID sql.NullString `json:"parent_session_id"`
|
||||
Title string `json:"title"`
|
||||
MessageCount int64 `json:"message_count"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
Cost float64 `json:"cost"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
35
internal/db/querier.go
Normal file
35
internal/db/querier.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
CreateFile(ctx context.Context, arg CreateFileParams) (File, error)
|
||||
CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error)
|
||||
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
|
||||
DeleteFile(ctx context.Context, id string) error
|
||||
DeleteMessage(ctx context.Context, id string) error
|
||||
DeleteSession(ctx context.Context, id string) error
|
||||
DeleteSessionFiles(ctx context.Context, sessionID string) error
|
||||
DeleteSessionMessages(ctx context.Context, sessionID string) error
|
||||
GetFile(ctx context.Context, id string) (File, error)
|
||||
GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
|
||||
GetMessage(ctx context.Context, id string) (Message, error)
|
||||
GetSessionByID(ctx context.Context, id string) (Session, error)
|
||||
ListFilesByPath(ctx context.Context, path string) ([]File, error)
|
||||
ListFilesBySession(ctx context.Context, sessionID string) ([]File, error)
|
||||
ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
|
||||
ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
|
||||
ListNewFiles(ctx context.Context) ([]File, error)
|
||||
ListSessions(ctx context.Context) ([]Session, error)
|
||||
UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error)
|
||||
UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
|
||||
UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
185
internal/db/sessions.sql.go
Normal file
185
internal/db/sessions.sql.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: sessions.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createSession = `-- name: CreateSession :one
|
||||
INSERT INTO sessions (
|
||||
id,
|
||||
parent_session_id,
|
||||
title,
|
||||
message_count,
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
cost,
|
||||
updated_at,
|
||||
created_at
|
||||
) VALUES (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now')
|
||||
) RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
|
||||
`
|
||||
|
||||
type CreateSessionParams struct {
|
||||
ID string `json:"id"`
|
||||
ParentSessionID sql.NullString `json:"parent_session_id"`
|
||||
Title string `json:"title"`
|
||||
MessageCount int64 `json:"message_count"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
Cost float64 `json:"cost"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
|
||||
row := q.queryRow(ctx, q.createSessionStmt, createSession,
|
||||
arg.ID,
|
||||
arg.ParentSessionID,
|
||||
arg.Title,
|
||||
arg.MessageCount,
|
||||
arg.PromptTokens,
|
||||
arg.CompletionTokens,
|
||||
arg.Cost,
|
||||
)
|
||||
var i Session
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ParentSessionID,
|
||||
&i.Title,
|
||||
&i.MessageCount,
|
||||
&i.PromptTokens,
|
||||
&i.CompletionTokens,
|
||||
&i.Cost,
|
||||
&i.UpdatedAt,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteSession = `-- name: DeleteSession :exec
|
||||
DELETE FROM sessions
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSession(ctx context.Context, id string) error {
|
||||
_, err := q.exec(ctx, q.deleteSessionStmt, deleteSession, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getSessionByID = `-- name: GetSessionByID :one
|
||||
SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
|
||||
FROM sessions
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSessionByID(ctx context.Context, id string) (Session, error) {
|
||||
row := q.queryRow(ctx, q.getSessionByIDStmt, getSessionByID, id)
|
||||
var i Session
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ParentSessionID,
|
||||
&i.Title,
|
||||
&i.MessageCount,
|
||||
&i.PromptTokens,
|
||||
&i.CompletionTokens,
|
||||
&i.Cost,
|
||||
&i.UpdatedAt,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listSessions = `-- name: ListSessions :many
|
||||
SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
|
||||
FROM sessions
|
||||
WHERE parent_session_id is NULL
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) {
|
||||
rows, err := q.query(ctx, q.listSessionsStmt, listSessions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Session{}
|
||||
for rows.Next() {
|
||||
var i Session
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ParentSessionID,
|
||||
&i.Title,
|
||||
&i.MessageCount,
|
||||
&i.PromptTokens,
|
||||
&i.CompletionTokens,
|
||||
&i.Cost,
|
||||
&i.UpdatedAt,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateSession = `-- name: UpdateSession :one
|
||||
UPDATE sessions
|
||||
SET
|
||||
title = ?,
|
||||
prompt_tokens = ?,
|
||||
completion_tokens = ?,
|
||||
cost = ?
|
||||
WHERE id = ?
|
||||
RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
|
||||
`
|
||||
|
||||
type UpdateSessionParams struct {
|
||||
Title string `json:"title"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
Cost float64 `json:"cost"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) {
|
||||
row := q.queryRow(ctx, q.updateSessionStmt, updateSession,
|
||||
arg.Title,
|
||||
arg.PromptTokens,
|
||||
arg.CompletionTokens,
|
||||
arg.Cost,
|
||||
arg.ID,
|
||||
)
|
||||
var i Session
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ParentSessionID,
|
||||
&i.Title,
|
||||
&i.MessageCount,
|
||||
&i.PromptTokens,
|
||||
&i.CompletionTokens,
|
||||
&i.Cost,
|
||||
&i.UpdatedAt,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
71
internal/db/sql/files.sql
Normal file
71
internal/db/sql/files.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- name: GetFile :one
|
||||
SELECT *
|
||||
FROM files
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: GetFileByPathAndSession :one
|
||||
SELECT *
|
||||
FROM files
|
||||
WHERE path = ? AND session_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- name: ListFilesBySession :many
|
||||
SELECT *
|
||||
FROM files
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: ListFilesByPath :many
|
||||
SELECT *
|
||||
FROM files
|
||||
WHERE path = ?
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: CreateFile :one
|
||||
INSERT INTO files (
|
||||
id,
|
||||
session_id,
|
||||
path,
|
||||
content,
|
||||
version,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateFile :one
|
||||
UPDATE files
|
||||
SET
|
||||
content = ?,
|
||||
version = ?,
|
||||
updated_at = strftime('%s', 'now')
|
||||
WHERE id = ?
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteFile :exec
|
||||
DELETE FROM files
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteSessionFiles :exec
|
||||
DELETE FROM files
|
||||
WHERE session_id = ?;
|
||||
|
||||
-- name: ListLatestSessionFiles :many
|
||||
SELECT f.*
|
||||
FROM files f
|
||||
INNER JOIN (
|
||||
SELECT path, MAX(created_at) as max_created_at
|
||||
FROM files
|
||||
GROUP BY path
|
||||
) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
|
||||
WHERE f.session_id = ?
|
||||
ORDER BY f.path;
|
||||
|
||||
-- name: ListNewFiles :many
|
||||
SELECT *
|
||||
FROM files
|
||||
WHERE is_new = 1
|
||||
ORDER BY created_at DESC;
|
||||
41
internal/db/sql/messages.sql
Normal file
41
internal/db/sql/messages.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- name: GetMessage :one
|
||||
SELECT *
|
||||
FROM messages
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListMessagesBySession :many
|
||||
SELECT *
|
||||
FROM messages
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: CreateMessage :one
|
||||
INSERT INTO messages (
|
||||
id,
|
||||
session_id,
|
||||
role,
|
||||
parts,
|
||||
model,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateMessage :exec
|
||||
UPDATE messages
|
||||
SET
|
||||
parts = ?,
|
||||
finished_at = ?,
|
||||
updated_at = strftime('%s', 'now')
|
||||
WHERE id = ?;
|
||||
|
||||
|
||||
-- name: DeleteMessage :exec
|
||||
DELETE FROM messages
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteSessionMessages :exec
|
||||
DELETE FROM messages
|
||||
WHERE session_id = ?;
|
||||
48
internal/db/sql/sessions.sql
Normal file
48
internal/db/sql/sessions.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- name: CreateSession :one
|
||||
INSERT INTO sessions (
|
||||
id,
|
||||
parent_session_id,
|
||||
title,
|
||||
message_count,
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
cost,
|
||||
updated_at,
|
||||
created_at
|
||||
) VALUES (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now')
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetSessionByID :one
|
||||
SELECT *
|
||||
FROM sessions
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListSessions :many
|
||||
SELECT *
|
||||
FROM sessions
|
||||
WHERE parent_session_id is NULL
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: UpdateSession :one
|
||||
UPDATE sessions
|
||||
SET
|
||||
title = ?,
|
||||
prompt_tokens = ?,
|
||||
completion_tokens = ?,
|
||||
cost = ?
|
||||
WHERE id = ?
|
||||
RETURNING *;
|
||||
|
||||
|
||||
-- name: DeleteSession :exec
|
||||
DELETE FROM sessions
|
||||
WHERE id = ?;
|
||||
873
internal/diff/diff.go
Normal file
873
internal/diff/diff.go
Normal file
@@ -0,0 +1,873 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/aymanbagabas/go-udiff"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Core Types
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// LineType represents the kind of line in a diff.
|
||||
type LineType int
|
||||
|
||||
const (
|
||||
LineContext LineType = iota // Line exists in both files
|
||||
LineAdded // Line added in the new file
|
||||
LineRemoved // Line removed from the old file
|
||||
)
|
||||
|
||||
// Segment represents a portion of a line for intra-line highlighting
|
||||
type Segment struct {
|
||||
Start int
|
||||
End int
|
||||
Type LineType
|
||||
Text string
|
||||
}
|
||||
|
||||
// DiffLine represents a single line in a diff
|
||||
type DiffLine struct {
|
||||
OldLineNo int // Line number in old file (0 for added lines)
|
||||
NewLineNo int // Line number in new file (0 for removed lines)
|
||||
Kind LineType // Type of line (added, removed, context)
|
||||
Content string // Content of the line
|
||||
Segments []Segment // Segments for intraline highlighting
|
||||
}
|
||||
|
||||
// Hunk represents a section of changes in a diff
|
||||
type Hunk struct {
|
||||
Header string
|
||||
Lines []DiffLine
|
||||
}
|
||||
|
||||
// DiffResult contains the parsed result of a diff
|
||||
type DiffResult struct {
|
||||
OldFile string
|
||||
NewFile string
|
||||
Hunks []Hunk
|
||||
}
|
||||
|
||||
// linePair represents a pair of lines for side-by-side display
|
||||
type linePair struct {
|
||||
left *DiffLine
|
||||
right *DiffLine
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Parse Configuration
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// ParseConfig configures the behavior of diff parsing
|
||||
type ParseConfig struct {
|
||||
ContextSize int // Number of context lines to include
|
||||
}
|
||||
|
||||
// ParseOption modifies a ParseConfig
|
||||
type ParseOption func(*ParseConfig)
|
||||
|
||||
// WithContextSize sets the number of context lines to include
|
||||
func WithContextSize(size int) ParseOption {
|
||||
return func(p *ParseConfig) {
|
||||
if size >= 0 {
|
||||
p.ContextSize = size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Side-by-Side Configuration
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// SideBySideConfig configures the rendering of side-by-side diffs
|
||||
type SideBySideConfig struct {
|
||||
TotalWidth int
|
||||
}
|
||||
|
||||
// SideBySideOption modifies a SideBySideConfig
|
||||
type SideBySideOption func(*SideBySideConfig)
|
||||
|
||||
// NewSideBySideConfig creates a SideBySideConfig with default values
|
||||
func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
|
||||
config := SideBySideConfig{
|
||||
TotalWidth: 160, // Default width for side-by-side view
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&config)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// WithTotalWidth sets the total width for side-by-side view
|
||||
func WithTotalWidth(width int) SideBySideOption {
|
||||
return func(s *SideBySideConfig) {
|
||||
if width > 0 {
|
||||
s.TotalWidth = width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Diff Parsing
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// ParseUnifiedDiff parses a unified diff format string into structured data
|
||||
func ParseUnifiedDiff(diff string) (DiffResult, error) {
|
||||
var result DiffResult
|
||||
var currentHunk *Hunk
|
||||
|
||||
hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`)
|
||||
lines := strings.Split(diff, "\n")
|
||||
|
||||
var oldLine, newLine int
|
||||
inFileHeader := true
|
||||
|
||||
for _, line := range lines {
|
||||
// Parse file headers
|
||||
if inFileHeader {
|
||||
if strings.HasPrefix(line, "--- a/") {
|
||||
result.OldFile = strings.TrimPrefix(line, "--- a/")
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "+++ b/") {
|
||||
result.NewFile = strings.TrimPrefix(line, "+++ b/")
|
||||
inFileHeader = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Parse hunk headers
|
||||
if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil {
|
||||
if currentHunk != nil {
|
||||
result.Hunks = append(result.Hunks, *currentHunk)
|
||||
}
|
||||
currentHunk = &Hunk{
|
||||
Header: line,
|
||||
Lines: []DiffLine{},
|
||||
}
|
||||
|
||||
oldStart, _ := strconv.Atoi(matches[1])
|
||||
newStart, _ := strconv.Atoi(matches[3])
|
||||
oldLine = oldStart
|
||||
newLine = newStart
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore "No newline at end of file" markers
|
||||
if strings.HasPrefix(line, "\\ No newline at end of file") {
|
||||
continue
|
||||
}
|
||||
|
||||
if currentHunk == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the line based on its prefix
|
||||
if len(line) > 0 {
|
||||
switch line[0] {
|
||||
case '+':
|
||||
currentHunk.Lines = append(currentHunk.Lines, DiffLine{
|
||||
OldLineNo: 0,
|
||||
NewLineNo: newLine,
|
||||
Kind: LineAdded,
|
||||
Content: line[1:],
|
||||
})
|
||||
newLine++
|
||||
case '-':
|
||||
currentHunk.Lines = append(currentHunk.Lines, DiffLine{
|
||||
OldLineNo: oldLine,
|
||||
NewLineNo: 0,
|
||||
Kind: LineRemoved,
|
||||
Content: line[1:],
|
||||
})
|
||||
oldLine++
|
||||
default:
|
||||
currentHunk.Lines = append(currentHunk.Lines, DiffLine{
|
||||
OldLineNo: oldLine,
|
||||
NewLineNo: newLine,
|
||||
Kind: LineContext,
|
||||
Content: line,
|
||||
})
|
||||
oldLine++
|
||||
newLine++
|
||||
}
|
||||
} else {
|
||||
// Handle empty lines
|
||||
currentHunk.Lines = append(currentHunk.Lines, DiffLine{
|
||||
OldLineNo: oldLine,
|
||||
NewLineNo: newLine,
|
||||
Kind: LineContext,
|
||||
Content: "",
|
||||
})
|
||||
oldLine++
|
||||
newLine++
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last hunk if there is one
|
||||
if currentHunk != nil {
|
||||
result.Hunks = append(result.Hunks, *currentHunk)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// HighlightIntralineChanges updates lines in a hunk to show character-level differences
|
||||
func HighlightIntralineChanges(h *Hunk) {
|
||||
var updated []DiffLine
|
||||
dmp := diffmatchpatch.New()
|
||||
|
||||
for i := 0; i < len(h.Lines); i++ {
|
||||
// Look for removed line followed by added line
|
||||
if i+1 < len(h.Lines) &&
|
||||
h.Lines[i].Kind == LineRemoved &&
|
||||
h.Lines[i+1].Kind == LineAdded {
|
||||
|
||||
oldLine := h.Lines[i]
|
||||
newLine := h.Lines[i+1]
|
||||
|
||||
// Find character-level differences
|
||||
patches := dmp.DiffMain(oldLine.Content, newLine.Content, false)
|
||||
patches = dmp.DiffCleanupSemantic(patches)
|
||||
patches = dmp.DiffCleanupMerge(patches)
|
||||
patches = dmp.DiffCleanupEfficiency(patches)
|
||||
|
||||
segments := make([]Segment, 0)
|
||||
|
||||
removeStart := 0
|
||||
addStart := 0
|
||||
for _, patch := range patches {
|
||||
switch patch.Type {
|
||||
case diffmatchpatch.DiffDelete:
|
||||
segments = append(segments, Segment{
|
||||
Start: removeStart,
|
||||
End: removeStart + len(patch.Text),
|
||||
Type: LineRemoved,
|
||||
Text: patch.Text,
|
||||
})
|
||||
removeStart += len(patch.Text)
|
||||
case diffmatchpatch.DiffInsert:
|
||||
segments = append(segments, Segment{
|
||||
Start: addStart,
|
||||
End: addStart + len(patch.Text),
|
||||
Type: LineAdded,
|
||||
Text: patch.Text,
|
||||
})
|
||||
addStart += len(patch.Text)
|
||||
default:
|
||||
// Context text, no highlighting needed
|
||||
removeStart += len(patch.Text)
|
||||
addStart += len(patch.Text)
|
||||
}
|
||||
}
|
||||
oldLine.Segments = segments
|
||||
newLine.Segments = segments
|
||||
|
||||
updated = append(updated, oldLine, newLine)
|
||||
i++ // Skip the next line as we've already processed it
|
||||
} else {
|
||||
updated = append(updated, h.Lines[i])
|
||||
}
|
||||
}
|
||||
|
||||
h.Lines = updated
|
||||
}
|
||||
|
||||
// pairLines converts a flat list of diff lines to pairs for side-by-side display
|
||||
func pairLines(lines []DiffLine) []linePair {
|
||||
var pairs []linePair
|
||||
i := 0
|
||||
|
||||
for i < len(lines) {
|
||||
switch lines[i].Kind {
|
||||
case LineRemoved:
|
||||
// Check if the next line is an addition, if so pair them
|
||||
if i+1 < len(lines) && lines[i+1].Kind == LineAdded {
|
||||
pairs = append(pairs, linePair{left: &lines[i], right: &lines[i+1]})
|
||||
i += 2
|
||||
} else {
|
||||
pairs = append(pairs, linePair{left: &lines[i], right: nil})
|
||||
i++
|
||||
}
|
||||
case LineAdded:
|
||||
pairs = append(pairs, linePair{left: nil, right: &lines[i]})
|
||||
i++
|
||||
case LineContext:
|
||||
pairs = append(pairs, linePair{left: &lines[i], right: &lines[i]})
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Syntax Highlighting
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// SyntaxHighlight applies syntax highlighting to text based on file extension
|
||||
func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
// Determine the language lexer to use
|
||||
l := lexers.Match(fileName)
|
||||
if l == nil {
|
||||
l = lexers.Analyse(source)
|
||||
}
|
||||
if l == nil {
|
||||
l = lexers.Fallback
|
||||
}
|
||||
l = chroma.Coalesce(l)
|
||||
|
||||
// Get the formatter
|
||||
f := formatters.Get(formatter)
|
||||
if f == nil {
|
||||
f = formatters.Fallback
|
||||
}
|
||||
|
||||
// Dynamic theme based on current theme values
|
||||
syntaxThemeXml := fmt.Sprintf(`
|
||||
<style name="opencode-theme">
|
||||
<!-- Base colors -->
|
||||
<entry type="Background" style="bg:%s"/>
|
||||
<entry type="Text" style="%s"/>
|
||||
<entry type="Other" style="%s"/>
|
||||
<entry type="Error" style="%s"/>
|
||||
<!-- Keywords -->
|
||||
<entry type="Keyword" style="%s"/>
|
||||
<entry type="KeywordConstant" style="%s"/>
|
||||
<entry type="KeywordDeclaration" style="%s"/>
|
||||
<entry type="KeywordNamespace" style="%s"/>
|
||||
<entry type="KeywordPseudo" style="%s"/>
|
||||
<entry type="KeywordReserved" style="%s"/>
|
||||
<entry type="KeywordType" style="%s"/>
|
||||
<!-- Names -->
|
||||
<entry type="Name" style="%s"/>
|
||||
<entry type="NameAttribute" style="%s"/>
|
||||
<entry type="NameBuiltin" style="%s"/>
|
||||
<entry type="NameBuiltinPseudo" style="%s"/>
|
||||
<entry type="NameClass" style="%s"/>
|
||||
<entry type="NameConstant" style="%s"/>
|
||||
<entry type="NameDecorator" style="%s"/>
|
||||
<entry type="NameEntity" style="%s"/>
|
||||
<entry type="NameException" style="%s"/>
|
||||
<entry type="NameFunction" style="%s"/>
|
||||
<entry type="NameLabel" style="%s"/>
|
||||
<entry type="NameNamespace" style="%s"/>
|
||||
<entry type="NameOther" style="%s"/>
|
||||
<entry type="NameTag" style="%s"/>
|
||||
<entry type="NameVariable" style="%s"/>
|
||||
<entry type="NameVariableClass" style="%s"/>
|
||||
<entry type="NameVariableGlobal" style="%s"/>
|
||||
<entry type="NameVariableInstance" style="%s"/>
|
||||
<!-- Literals -->
|
||||
<entry type="Literal" style="%s"/>
|
||||
<entry type="LiteralDate" style="%s"/>
|
||||
<entry type="LiteralString" style="%s"/>
|
||||
<entry type="LiteralStringBacktick" style="%s"/>
|
||||
<entry type="LiteralStringChar" style="%s"/>
|
||||
<entry type="LiteralStringDoc" style="%s"/>
|
||||
<entry type="LiteralStringDouble" style="%s"/>
|
||||
<entry type="LiteralStringEscape" style="%s"/>
|
||||
<entry type="LiteralStringHeredoc" style="%s"/>
|
||||
<entry type="LiteralStringInterpol" style="%s"/>
|
||||
<entry type="LiteralStringOther" style="%s"/>
|
||||
<entry type="LiteralStringRegex" style="%s"/>
|
||||
<entry type="LiteralStringSingle" style="%s"/>
|
||||
<entry type="LiteralStringSymbol" style="%s"/>
|
||||
<!-- Numbers -->
|
||||
<entry type="LiteralNumber" style="%s"/>
|
||||
<entry type="LiteralNumberBin" style="%s"/>
|
||||
<entry type="LiteralNumberFloat" style="%s"/>
|
||||
<entry type="LiteralNumberHex" style="%s"/>
|
||||
<entry type="LiteralNumberInteger" style="%s"/>
|
||||
<entry type="LiteralNumberIntegerLong" style="%s"/>
|
||||
<entry type="LiteralNumberOct" style="%s"/>
|
||||
<!-- Operators -->
|
||||
<entry type="Operator" style="%s"/>
|
||||
<entry type="OperatorWord" style="%s"/>
|
||||
<entry type="Punctuation" style="%s"/>
|
||||
<!-- Comments -->
|
||||
<entry type="Comment" style="%s"/>
|
||||
<entry type="CommentHashbang" style="%s"/>
|
||||
<entry type="CommentMultiline" style="%s"/>
|
||||
<entry type="CommentSingle" style="%s"/>
|
||||
<entry type="CommentSpecial" style="%s"/>
|
||||
<entry type="CommentPreproc" style="%s"/>
|
||||
<!-- Generic styles -->
|
||||
<entry type="Generic" style="%s"/>
|
||||
<entry type="GenericDeleted" style="%s"/>
|
||||
<entry type="GenericEmph" style="italic %s"/>
|
||||
<entry type="GenericError" style="%s"/>
|
||||
<entry type="GenericHeading" style="bold %s"/>
|
||||
<entry type="GenericInserted" style="%s"/>
|
||||
<entry type="GenericOutput" style="%s"/>
|
||||
<entry type="GenericPrompt" style="%s"/>
|
||||
<entry type="GenericStrong" style="bold %s"/>
|
||||
<entry type="GenericSubheading" style="bold %s"/>
|
||||
<entry type="GenericTraceback" style="%s"/>
|
||||
<entry type="GenericUnderline" style="underline"/>
|
||||
<entry type="TextWhitespace" style="%s"/>
|
||||
</style>
|
||||
`,
|
||||
getColor(t.Background()), // Background
|
||||
getColor(t.Text()), // Text
|
||||
getColor(t.Text()), // Other
|
||||
getColor(t.Error()), // Error
|
||||
|
||||
getColor(t.SyntaxKeyword()), // Keyword
|
||||
getColor(t.SyntaxKeyword()), // KeywordConstant
|
||||
getColor(t.SyntaxKeyword()), // KeywordDeclaration
|
||||
getColor(t.SyntaxKeyword()), // KeywordNamespace
|
||||
getColor(t.SyntaxKeyword()), // KeywordPseudo
|
||||
getColor(t.SyntaxKeyword()), // KeywordReserved
|
||||
getColor(t.SyntaxType()), // KeywordType
|
||||
|
||||
getColor(t.Text()), // Name
|
||||
getColor(t.SyntaxVariable()), // NameAttribute
|
||||
getColor(t.SyntaxType()), // NameBuiltin
|
||||
getColor(t.SyntaxVariable()), // NameBuiltinPseudo
|
||||
getColor(t.SyntaxType()), // NameClass
|
||||
getColor(t.SyntaxVariable()), // NameConstant
|
||||
getColor(t.SyntaxFunction()), // NameDecorator
|
||||
getColor(t.SyntaxVariable()), // NameEntity
|
||||
getColor(t.SyntaxType()), // NameException
|
||||
getColor(t.SyntaxFunction()), // NameFunction
|
||||
getColor(t.Text()), // NameLabel
|
||||
getColor(t.SyntaxType()), // NameNamespace
|
||||
getColor(t.SyntaxVariable()), // NameOther
|
||||
getColor(t.SyntaxKeyword()), // NameTag
|
||||
getColor(t.SyntaxVariable()), // NameVariable
|
||||
getColor(t.SyntaxVariable()), // NameVariableClass
|
||||
getColor(t.SyntaxVariable()), // NameVariableGlobal
|
||||
getColor(t.SyntaxVariable()), // NameVariableInstance
|
||||
|
||||
getColor(t.SyntaxString()), // Literal
|
||||
getColor(t.SyntaxString()), // LiteralDate
|
||||
getColor(t.SyntaxString()), // LiteralString
|
||||
getColor(t.SyntaxString()), // LiteralStringBacktick
|
||||
getColor(t.SyntaxString()), // LiteralStringChar
|
||||
getColor(t.SyntaxString()), // LiteralStringDoc
|
||||
getColor(t.SyntaxString()), // LiteralStringDouble
|
||||
getColor(t.SyntaxString()), // LiteralStringEscape
|
||||
getColor(t.SyntaxString()), // LiteralStringHeredoc
|
||||
getColor(t.SyntaxString()), // LiteralStringInterpol
|
||||
getColor(t.SyntaxString()), // LiteralStringOther
|
||||
getColor(t.SyntaxString()), // LiteralStringRegex
|
||||
getColor(t.SyntaxString()), // LiteralStringSingle
|
||||
getColor(t.SyntaxString()), // LiteralStringSymbol
|
||||
|
||||
getColor(t.SyntaxNumber()), // LiteralNumber
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberBin
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberFloat
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberHex
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberInteger
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberOct
|
||||
|
||||
getColor(t.SyntaxOperator()), // Operator
|
||||
getColor(t.SyntaxKeyword()), // OperatorWord
|
||||
getColor(t.SyntaxPunctuation()), // Punctuation
|
||||
|
||||
getColor(t.SyntaxComment()), // Comment
|
||||
getColor(t.SyntaxComment()), // CommentHashbang
|
||||
getColor(t.SyntaxComment()), // CommentMultiline
|
||||
getColor(t.SyntaxComment()), // CommentSingle
|
||||
getColor(t.SyntaxComment()), // CommentSpecial
|
||||
getColor(t.SyntaxKeyword()), // CommentPreproc
|
||||
|
||||
getColor(t.Text()), // Generic
|
||||
getColor(t.Error()), // GenericDeleted
|
||||
getColor(t.Text()), // GenericEmph
|
||||
getColor(t.Error()), // GenericError
|
||||
getColor(t.Text()), // GenericHeading
|
||||
getColor(t.Success()), // GenericInserted
|
||||
getColor(t.TextMuted()), // GenericOutput
|
||||
getColor(t.Text()), // GenericPrompt
|
||||
getColor(t.Text()), // GenericStrong
|
||||
getColor(t.Text()), // GenericSubheading
|
||||
getColor(t.Error()), // GenericTraceback
|
||||
getColor(t.Text()), // TextWhitespace
|
||||
)
|
||||
|
||||
r := strings.NewReader(syntaxThemeXml)
|
||||
style := chroma.MustNewXMLStyle(r)
|
||||
|
||||
// Modify the style to use the provided background
|
||||
s, err := style.Builder().Transform(
|
||||
func(t chroma.StyleEntry) chroma.StyleEntry {
|
||||
r, g, b, _ := bg.RGBA()
|
||||
t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||
return t
|
||||
},
|
||||
).Build()
|
||||
if err != nil {
|
||||
s = styles.Fallback
|
||||
}
|
||||
|
||||
// Tokenize and format
|
||||
it, err := l.Tokenise(nil, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.Format(w, s, it)
|
||||
}
|
||||
|
||||
// getColor returns the appropriate hex color string based on terminal background
|
||||
func getColor(adaptiveColor lipgloss.AdaptiveColor) string {
|
||||
if lipgloss.HasDarkBackground() {
|
||||
return adaptiveColor.Dark
|
||||
}
|
||||
return adaptiveColor.Light
|
||||
}
|
||||
|
||||
// highlightLine applies syntax highlighting to a single line
|
||||
func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
|
||||
var buf bytes.Buffer
|
||||
err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
|
||||
if err != nil {
|
||||
return line
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// createStyles generates the lipgloss styles needed for rendering diffs
|
||||
func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
|
||||
removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
|
||||
addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
|
||||
contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
lineNumberStyle = lipgloss.NewStyle().Foreground(t.DiffLineNumber())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Functions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
func lipglossToHex(color lipgloss.Color) string {
|
||||
r, g, b, a := color.RGBA()
|
||||
|
||||
// Scale uint32 values (0-65535) to uint8 (0-255).
|
||||
r8 := uint8(r >> 8)
|
||||
g8 := uint8(g >> 8)
|
||||
b8 := uint8(b >> 8)
|
||||
a8 := uint8(a >> 8)
|
||||
|
||||
return fmt.Sprintf("#%02x%02x%02x%02x", r8, g8, b8, a8)
|
||||
}
|
||||
|
||||
// applyHighlighting applies intra-line highlighting to a piece of text
|
||||
func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string {
|
||||
// Find all ANSI sequences in the content
|
||||
ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
|
||||
ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
|
||||
|
||||
// Build a mapping of visible character positions to their actual indices
|
||||
visibleIdx := 0
|
||||
ansiSequences := make(map[int]string)
|
||||
lastAnsiSeq := "\x1b[0m" // Default reset sequence
|
||||
|
||||
for i := 0; i < len(content); {
|
||||
isAnsi := false
|
||||
for _, match := range ansiMatches {
|
||||
if match[0] == i {
|
||||
ansiSequences[visibleIdx] = content[match[0]:match[1]]
|
||||
lastAnsiSeq = content[match[0]:match[1]]
|
||||
i = match[1]
|
||||
isAnsi = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isAnsi {
|
||||
continue
|
||||
}
|
||||
|
||||
// For non-ANSI positions, store the last ANSI sequence
|
||||
if _, exists := ansiSequences[visibleIdx]; !exists {
|
||||
ansiSequences[visibleIdx] = lastAnsiSeq
|
||||
}
|
||||
visibleIdx++
|
||||
i++
|
||||
}
|
||||
|
||||
// Apply highlighting
|
||||
var sb strings.Builder
|
||||
inSelection := false
|
||||
currentPos := 0
|
||||
|
||||
// Get the appropriate color based on terminal background
|
||||
bgColor := lipgloss.Color(getColor(highlightBg))
|
||||
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background()))
|
||||
|
||||
for i := 0; i < len(content); {
|
||||
// Check if we're at an ANSI sequence
|
||||
isAnsi := false
|
||||
for _, match := range ansiMatches {
|
||||
if match[0] == i {
|
||||
sb.WriteString(content[match[0]:match[1]]) // Preserve ANSI sequence
|
||||
i = match[1]
|
||||
isAnsi = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isAnsi {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for segment boundaries
|
||||
for _, seg := range segments {
|
||||
if seg.Type == segmentType {
|
||||
if currentPos == seg.Start {
|
||||
inSelection = true
|
||||
}
|
||||
if currentPos == seg.End {
|
||||
inSelection = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current character
|
||||
char := string(content[i])
|
||||
|
||||
if inSelection {
|
||||
// Get the current styling
|
||||
currentStyle := ansiSequences[currentPos]
|
||||
|
||||
// Apply foreground and background highlight
|
||||
sb.WriteString("\x1b[38;2;")
|
||||
r, g, b, _ := fgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
sb.WriteString("\x1b[48;2;")
|
||||
r, g, b, _ = bgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
sb.WriteString(char)
|
||||
// Reset foreground and background
|
||||
sb.WriteString("\x1b[39m")
|
||||
|
||||
// Reapply the original ANSI sequence
|
||||
sb.WriteString(currentStyle)
|
||||
} else {
|
||||
// Not in selection, just copy the character
|
||||
sb.WriteString(char)
|
||||
}
|
||||
|
||||
currentPos++
|
||||
i++
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// renderLeftColumn formats the left side of a side-by-side diff
|
||||
func renderLeftColumn(fileName string, dl *DiffLine, colWidth int) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
if dl == nil {
|
||||
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
return contextLineStyle.Width(colWidth).Render("")
|
||||
}
|
||||
|
||||
removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(t)
|
||||
|
||||
// Determine line style based on line type
|
||||
var marker string
|
||||
var bgStyle lipgloss.Style
|
||||
switch dl.Kind {
|
||||
case LineRemoved:
|
||||
marker = removedLineStyle.Foreground(t.DiffRemoved()).Render("-")
|
||||
bgStyle = removedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
|
||||
case LineAdded:
|
||||
marker = "?"
|
||||
bgStyle = contextLineStyle
|
||||
case LineContext:
|
||||
marker = contextLineStyle.Render(" ")
|
||||
bgStyle = contextLineStyle
|
||||
}
|
||||
|
||||
// Format line number
|
||||
lineNum := ""
|
||||
if dl.OldLineNo > 0 {
|
||||
lineNum = fmt.Sprintf("%6d", dl.OldLineNo)
|
||||
}
|
||||
|
||||
// Create the line prefix
|
||||
prefix := lineNumberStyle.Render(lineNum + " " + marker)
|
||||
|
||||
// Apply syntax highlighting
|
||||
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
|
||||
|
||||
// Apply intra-line highlighting for removed lines
|
||||
if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
|
||||
content = applyHighlighting(content, dl.Segments, LineRemoved, t.DiffHighlightRemoved())
|
||||
}
|
||||
|
||||
// Add a padding space for removed lines
|
||||
if dl.Kind == LineRemoved {
|
||||
content = bgStyle.Render(" ") + content
|
||||
}
|
||||
|
||||
// Create the final line and truncate if needed
|
||||
lineText := prefix + content
|
||||
return bgStyle.MaxHeight(1).Width(colWidth).Render(
|
||||
ansi.Truncate(
|
||||
lineText,
|
||||
colWidth,
|
||||
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// renderRightColumn formats the right side of a side-by-side diff
|
||||
func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
if dl == nil {
|
||||
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
return contextLineStyle.Width(colWidth).Render("")
|
||||
}
|
||||
|
||||
_, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
|
||||
|
||||
// Determine line style based on line type
|
||||
var marker string
|
||||
var bgStyle lipgloss.Style
|
||||
switch dl.Kind {
|
||||
case LineAdded:
|
||||
marker = addedLineStyle.Foreground(t.DiffAdded()).Render("+")
|
||||
bgStyle = addedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
|
||||
case LineRemoved:
|
||||
marker = "?"
|
||||
bgStyle = contextLineStyle
|
||||
case LineContext:
|
||||
marker = contextLineStyle.Render(" ")
|
||||
bgStyle = contextLineStyle
|
||||
}
|
||||
|
||||
// Format line number
|
||||
lineNum := ""
|
||||
if dl.NewLineNo > 0 {
|
||||
lineNum = fmt.Sprintf("%6d", dl.NewLineNo)
|
||||
}
|
||||
|
||||
// Create the line prefix
|
||||
prefix := lineNumberStyle.Render(lineNum + " " + marker)
|
||||
|
||||
// Apply syntax highlighting
|
||||
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
|
||||
|
||||
// Apply intra-line highlighting for added lines
|
||||
if dl.Kind == LineAdded && len(dl.Segments) > 0 {
|
||||
content = applyHighlighting(content, dl.Segments, LineAdded, t.DiffHighlightAdded())
|
||||
}
|
||||
|
||||
// Add a padding space for added lines
|
||||
if dl.Kind == LineAdded {
|
||||
content = bgStyle.Render(" ") + content
|
||||
}
|
||||
|
||||
// Create the final line and truncate if needed
|
||||
lineText := prefix + content
|
||||
return bgStyle.MaxHeight(1).Width(colWidth).Render(
|
||||
ansi.Truncate(
|
||||
lineText,
|
||||
colWidth,
|
||||
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// RenderSideBySideHunk formats a hunk for side-by-side display
|
||||
func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
|
||||
// Apply options to create the configuration
|
||||
config := NewSideBySideConfig(opts...)
|
||||
|
||||
// Make a copy of the hunk so we don't modify the original
|
||||
hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))}
|
||||
copy(hunkCopy.Lines, h.Lines)
|
||||
|
||||
// Highlight changes within lines
|
||||
HighlightIntralineChanges(&hunkCopy)
|
||||
|
||||
// Pair lines for side-by-side display
|
||||
pairs := pairLines(hunkCopy.Lines)
|
||||
|
||||
// Calculate column width
|
||||
colWidth := config.TotalWidth / 2
|
||||
|
||||
leftWidth := colWidth
|
||||
rightWidth := config.TotalWidth - colWidth
|
||||
var sb strings.Builder
|
||||
for _, p := range pairs {
|
||||
leftStr := renderLeftColumn(fileName, p.left, leftWidth)
|
||||
rightStr := renderRightColumn(fileName, p.right, rightWidth)
|
||||
sb.WriteString(leftStr + rightStr + "\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// FormatDiff creates a side-by-side formatted view of a diff
|
||||
func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
|
||||
diffResult, err := ParseUnifiedDiff(diffText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, h := range diffResult.Hunks {
|
||||
sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// GenerateDiff creates a unified diff from two file contents
|
||||
func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) {
|
||||
// remove the cwd prefix and ensure consistent path format
|
||||
// this prevents issues with absolute paths in different environments
|
||||
cwd := config.WorkingDirectory()
|
||||
fileName = strings.TrimPrefix(fileName, cwd)
|
||||
fileName = strings.TrimPrefix(fileName, "/")
|
||||
|
||||
var (
|
||||
unified = udiff.Unified("a/"+fileName, "b/"+fileName, beforeContent, afterContent)
|
||||
additions = 0
|
||||
removals = 0
|
||||
)
|
||||
|
||||
lines := strings.SplitSeq(unified, "\n")
|
||||
for line := range lines {
|
||||
if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
|
||||
additions++
|
||||
} else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
|
||||
removals++
|
||||
}
|
||||
}
|
||||
|
||||
return unified, additions, removals
|
||||
}
|
||||
740
internal/diff/patch.go
Normal file
740
internal/diff/patch.go
Normal file
@@ -0,0 +1,740 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
ActionAdd ActionType = "add"
|
||||
ActionDelete ActionType = "delete"
|
||||
ActionUpdate ActionType = "update"
|
||||
)
|
||||
|
||||
type FileChange struct {
|
||||
Type ActionType
|
||||
OldContent *string
|
||||
NewContent *string
|
||||
MovePath *string
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
Changes map[string]FileChange
|
||||
}
|
||||
|
||||
type Chunk struct {
|
||||
OrigIndex int // line index of the first line in the original file
|
||||
DelLines []string // lines to delete
|
||||
InsLines []string // lines to insert
|
||||
}
|
||||
|
||||
type PatchAction struct {
|
||||
Type ActionType
|
||||
NewFile *string
|
||||
Chunks []Chunk
|
||||
MovePath *string
|
||||
}
|
||||
|
||||
type Patch struct {
|
||||
Actions map[string]PatchAction
|
||||
}
|
||||
|
||||
type DiffError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e DiffError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// Helper functions for error handling
|
||||
func NewDiffError(message string) DiffError {
|
||||
return DiffError{message: message}
|
||||
}
|
||||
|
||||
func fileError(action, reason, path string) DiffError {
|
||||
return NewDiffError(fmt.Sprintf("%s File Error: %s: %s", action, reason, path))
|
||||
}
|
||||
|
||||
func contextError(index int, context string, isEOF bool) DiffError {
|
||||
prefix := "Invalid Context"
|
||||
if isEOF {
|
||||
prefix = "Invalid EOF Context"
|
||||
}
|
||||
return NewDiffError(fmt.Sprintf("%s %d:\n%s", prefix, index, context))
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
currentFiles map[string]string
|
||||
lines []string
|
||||
index int
|
||||
patch Patch
|
||||
fuzz int
|
||||
}
|
||||
|
||||
func NewParser(currentFiles map[string]string, lines []string) *Parser {
|
||||
return &Parser{
|
||||
currentFiles: currentFiles,
|
||||
lines: lines,
|
||||
index: 0,
|
||||
patch: Patch{Actions: make(map[string]PatchAction, len(currentFiles))},
|
||||
fuzz: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) isDone(prefixes []string) bool {
|
||||
if p.index >= len(p.lines) {
|
||||
return true
|
||||
}
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(p.lines[p.index], prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) startsWith(prefix any) bool {
|
||||
var prefixes []string
|
||||
switch v := prefix.(type) {
|
||||
case string:
|
||||
prefixes = []string{v}
|
||||
case []string:
|
||||
prefixes = v
|
||||
}
|
||||
|
||||
for _, pfx := range prefixes {
|
||||
if strings.HasPrefix(p.lines[p.index], pfx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Parser) readStr(prefix string, returnEverything bool) string {
|
||||
if p.index >= len(p.lines) {
|
||||
return "" // Changed from panic to return empty string for safer operation
|
||||
}
|
||||
if strings.HasPrefix(p.lines[p.index], prefix) {
|
||||
var text string
|
||||
if returnEverything {
|
||||
text = p.lines[p.index]
|
||||
} else {
|
||||
text = p.lines[p.index][len(prefix):]
|
||||
}
|
||||
p.index++
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Parser) Parse() error {
|
||||
endPatchPrefixes := []string{"*** End Patch"}
|
||||
|
||||
for !p.isDone(endPatchPrefixes) {
|
||||
path := p.readStr("*** Update File: ", false)
|
||||
if path != "" {
|
||||
if _, exists := p.patch.Actions[path]; exists {
|
||||
return fileError("Update", "Duplicate Path", path)
|
||||
}
|
||||
moveTo := p.readStr("*** Move to: ", false)
|
||||
if _, exists := p.currentFiles[path]; !exists {
|
||||
return fileError("Update", "Missing File", path)
|
||||
}
|
||||
text := p.currentFiles[path]
|
||||
action, err := p.parseUpdateFile(text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if moveTo != "" {
|
||||
action.MovePath = &moveTo
|
||||
}
|
||||
p.patch.Actions[path] = action
|
||||
continue
|
||||
}
|
||||
|
||||
path = p.readStr("*** Delete File: ", false)
|
||||
if path != "" {
|
||||
if _, exists := p.patch.Actions[path]; exists {
|
||||
return fileError("Delete", "Duplicate Path", path)
|
||||
}
|
||||
if _, exists := p.currentFiles[path]; !exists {
|
||||
return fileError("Delete", "Missing File", path)
|
||||
}
|
||||
p.patch.Actions[path] = PatchAction{Type: ActionDelete, Chunks: []Chunk{}}
|
||||
continue
|
||||
}
|
||||
|
||||
path = p.readStr("*** Add File: ", false)
|
||||
if path != "" {
|
||||
if _, exists := p.patch.Actions[path]; exists {
|
||||
return fileError("Add", "Duplicate Path", path)
|
||||
}
|
||||
if _, exists := p.currentFiles[path]; exists {
|
||||
return fileError("Add", "File already exists", path)
|
||||
}
|
||||
action, err := p.parseAddFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.patch.Actions[path] = action
|
||||
continue
|
||||
}
|
||||
|
||||
return NewDiffError(fmt.Sprintf("Unknown Line: %s", p.lines[p.index]))
|
||||
}
|
||||
|
||||
if !p.startsWith("*** End Patch") {
|
||||
return NewDiffError("Missing End Patch")
|
||||
}
|
||||
p.index++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseUpdateFile(text string) (PatchAction, error) {
|
||||
action := PatchAction{Type: ActionUpdate, Chunks: []Chunk{}}
|
||||
fileLines := strings.Split(text, "\n")
|
||||
index := 0
|
||||
|
||||
endPrefixes := []string{
|
||||
"*** End Patch",
|
||||
"*** Update File:",
|
||||
"*** Delete File:",
|
||||
"*** Add File:",
|
||||
"*** End of File",
|
||||
}
|
||||
|
||||
for !p.isDone(endPrefixes) {
|
||||
defStr := p.readStr("@@ ", false)
|
||||
sectionStr := ""
|
||||
if defStr == "" && p.index < len(p.lines) && p.lines[p.index] == "@@" {
|
||||
sectionStr = p.lines[p.index]
|
||||
p.index++
|
||||
}
|
||||
if defStr == "" && sectionStr == "" && index != 0 {
|
||||
return action, NewDiffError(fmt.Sprintf("Invalid Line:\n%s", p.lines[p.index]))
|
||||
}
|
||||
if strings.TrimSpace(defStr) != "" {
|
||||
found := false
|
||||
for i := range fileLines[:index] {
|
||||
if fileLines[i] == defStr {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
for i := index; i < len(fileLines); i++ {
|
||||
if fileLines[i] == defStr {
|
||||
index = i + 1
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
for i := range fileLines[:index] {
|
||||
if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
for i := index; i < len(fileLines); i++ {
|
||||
if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
|
||||
index = i + 1
|
||||
p.fuzz++
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextChunkContext, chunks, endPatchIndex, eof := peekNextSection(p.lines, p.index)
|
||||
newIndex, fuzz := findContext(fileLines, nextChunkContext, index, eof)
|
||||
if newIndex == -1 {
|
||||
ctxText := strings.Join(nextChunkContext, "\n")
|
||||
return action, contextError(index, ctxText, eof)
|
||||
}
|
||||
p.fuzz += fuzz
|
||||
|
||||
for _, ch := range chunks {
|
||||
ch.OrigIndex += newIndex
|
||||
action.Chunks = append(action.Chunks, ch)
|
||||
}
|
||||
index = newIndex + len(nextChunkContext)
|
||||
p.index = endPatchIndex
|
||||
}
|
||||
return action, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseAddFile() (PatchAction, error) {
|
||||
lines := make([]string, 0, 16) // Preallocate space for better performance
|
||||
endPrefixes := []string{
|
||||
"*** End Patch",
|
||||
"*** Update File:",
|
||||
"*** Delete File:",
|
||||
"*** Add File:",
|
||||
}
|
||||
|
||||
for !p.isDone(endPrefixes) {
|
||||
s := p.readStr("", true)
|
||||
if !strings.HasPrefix(s, "+") {
|
||||
return PatchAction{}, NewDiffError(fmt.Sprintf("Invalid Add File Line: %s", s))
|
||||
}
|
||||
lines = append(lines, s[1:])
|
||||
}
|
||||
|
||||
newFile := strings.Join(lines, "\n")
|
||||
return PatchAction{
|
||||
Type: ActionAdd,
|
||||
NewFile: &newFile,
|
||||
Chunks: []Chunk{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refactored to use a matcher function for each comparison type
|
||||
func findContextCore(lines []string, context []string, start int) (int, int) {
|
||||
if len(context) == 0 {
|
||||
return start, 0
|
||||
}
|
||||
|
||||
// Try exact match
|
||||
if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
|
||||
return a == b
|
||||
}); idx >= 0 {
|
||||
return idx, fuzz
|
||||
}
|
||||
|
||||
// Try trimming right whitespace
|
||||
if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
|
||||
return strings.TrimRight(a, " \t") == strings.TrimRight(b, " \t")
|
||||
}); idx >= 0 {
|
||||
return idx, fuzz
|
||||
}
|
||||
|
||||
// Try trimming all whitespace
|
||||
if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
|
||||
return strings.TrimSpace(a) == strings.TrimSpace(b)
|
||||
}); idx >= 0 {
|
||||
return idx, fuzz
|
||||
}
|
||||
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
// Helper function to DRY up the match logic
|
||||
func tryFindMatch(lines []string, context []string, start int,
|
||||
compareFunc func(string, string) bool,
|
||||
) (int, int) {
|
||||
for i := start; i < len(lines); i++ {
|
||||
if i+len(context) <= len(lines) {
|
||||
match := true
|
||||
for j := range context {
|
||||
if !compareFunc(lines[i+j], context[j]) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
// Return fuzz level: 0 for exact, 1 for trimRight, 100 for trimSpace
|
||||
var fuzz int
|
||||
if compareFunc("a ", "a") && !compareFunc("a", "b") {
|
||||
fuzz = 1
|
||||
} else if compareFunc("a ", "a") {
|
||||
fuzz = 100
|
||||
}
|
||||
return i, fuzz
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
func findContext(lines []string, context []string, start int, eof bool) (int, int) {
|
||||
if eof {
|
||||
newIndex, fuzz := findContextCore(lines, context, len(lines)-len(context))
|
||||
if newIndex != -1 {
|
||||
return newIndex, fuzz
|
||||
}
|
||||
newIndex, fuzz = findContextCore(lines, context, start)
|
||||
return newIndex, fuzz + 10000
|
||||
}
|
||||
return findContextCore(lines, context, start)
|
||||
}
|
||||
|
||||
func peekNextSection(lines []string, initialIndex int) ([]string, []Chunk, int, bool) {
|
||||
index := initialIndex
|
||||
old := make([]string, 0, 32) // Preallocate for better performance
|
||||
delLines := make([]string, 0, 8)
|
||||
insLines := make([]string, 0, 8)
|
||||
chunks := make([]Chunk, 0, 4)
|
||||
mode := "keep"
|
||||
|
||||
// End conditions for the section
|
||||
endSectionConditions := func(s string) bool {
|
||||
return strings.HasPrefix(s, "@@") ||
|
||||
strings.HasPrefix(s, "*** End Patch") ||
|
||||
strings.HasPrefix(s, "*** Update File:") ||
|
||||
strings.HasPrefix(s, "*** Delete File:") ||
|
||||
strings.HasPrefix(s, "*** Add File:") ||
|
||||
strings.HasPrefix(s, "*** End of File") ||
|
||||
s == "***" ||
|
||||
strings.HasPrefix(s, "***")
|
||||
}
|
||||
|
||||
for index < len(lines) {
|
||||
s := lines[index]
|
||||
if endSectionConditions(s) {
|
||||
break
|
||||
}
|
||||
index++
|
||||
lastMode := mode
|
||||
line := s
|
||||
|
||||
if len(line) > 0 {
|
||||
switch line[0] {
|
||||
case '+':
|
||||
mode = "add"
|
||||
case '-':
|
||||
mode = "delete"
|
||||
case ' ':
|
||||
mode = "keep"
|
||||
default:
|
||||
mode = "keep"
|
||||
line = " " + line
|
||||
}
|
||||
} else {
|
||||
mode = "keep"
|
||||
line = " "
|
||||
}
|
||||
|
||||
line = line[1:]
|
||||
if mode == "keep" && lastMode != mode {
|
||||
if len(insLines) > 0 || len(delLines) > 0 {
|
||||
chunks = append(chunks, Chunk{
|
||||
OrigIndex: len(old) - len(delLines),
|
||||
DelLines: delLines,
|
||||
InsLines: insLines,
|
||||
})
|
||||
}
|
||||
delLines = make([]string, 0, 8)
|
||||
insLines = make([]string, 0, 8)
|
||||
}
|
||||
switch mode {
|
||||
case "delete":
|
||||
delLines = append(delLines, line)
|
||||
old = append(old, line)
|
||||
case "add":
|
||||
insLines = append(insLines, line)
|
||||
default:
|
||||
old = append(old, line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(insLines) > 0 || len(delLines) > 0 {
|
||||
chunks = append(chunks, Chunk{
|
||||
OrigIndex: len(old) - len(delLines),
|
||||
DelLines: delLines,
|
||||
InsLines: insLines,
|
||||
})
|
||||
}
|
||||
|
||||
if index < len(lines) && lines[index] == "*** End of File" {
|
||||
index++
|
||||
return old, chunks, index, true
|
||||
}
|
||||
return old, chunks, index, false
|
||||
}
|
||||
|
||||
func TextToPatch(text string, orig map[string]string) (Patch, int, error) {
|
||||
text = strings.TrimSpace(text)
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) < 2 || !strings.HasPrefix(lines[0], "*** Begin Patch") || lines[len(lines)-1] != "*** End Patch" {
|
||||
return Patch{}, 0, NewDiffError("Invalid patch text")
|
||||
}
|
||||
parser := NewParser(orig, lines)
|
||||
parser.index = 1
|
||||
if err := parser.Parse(); err != nil {
|
||||
return Patch{}, 0, err
|
||||
}
|
||||
return parser.patch, parser.fuzz, nil
|
||||
}
|
||||
|
||||
func IdentifyFilesNeeded(text string) []string {
|
||||
text = strings.TrimSpace(text)
|
||||
lines := strings.Split(text, "\n")
|
||||
result := make(map[string]bool)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "*** Update File: ") {
|
||||
result[line[len("*** Update File: "):]] = true
|
||||
}
|
||||
if strings.HasPrefix(line, "*** Delete File: ") {
|
||||
result[line[len("*** Delete File: "):]] = true
|
||||
}
|
||||
}
|
||||
|
||||
files := make([]string, 0, len(result))
|
||||
for file := range result {
|
||||
files = append(files, file)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func IdentifyFilesAdded(text string) []string {
|
||||
text = strings.TrimSpace(text)
|
||||
lines := strings.Split(text, "\n")
|
||||
result := make(map[string]bool)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "*** Add File: ") {
|
||||
result[line[len("*** Add File: "):]] = true
|
||||
}
|
||||
}
|
||||
|
||||
files := make([]string, 0, len(result))
|
||||
for file := range result {
|
||||
files = append(files, file)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func getUpdatedFile(text string, action PatchAction, path string) (string, error) {
|
||||
if action.Type != ActionUpdate {
|
||||
return "", errors.New("expected UPDATE action")
|
||||
}
|
||||
origLines := strings.Split(text, "\n")
|
||||
destLines := make([]string, 0, len(origLines)) // Preallocate with capacity
|
||||
origIndex := 0
|
||||
|
||||
for _, chunk := range action.Chunks {
|
||||
if chunk.OrigIndex > len(origLines) {
|
||||
return "", NewDiffError(fmt.Sprintf("%s: chunk.orig_index %d > len(lines) %d", path, chunk.OrigIndex, len(origLines)))
|
||||
}
|
||||
if origIndex > chunk.OrigIndex {
|
||||
return "", NewDiffError(fmt.Sprintf("%s: orig_index %d > chunk.orig_index %d", path, origIndex, chunk.OrigIndex))
|
||||
}
|
||||
destLines = append(destLines, origLines[origIndex:chunk.OrigIndex]...)
|
||||
delta := chunk.OrigIndex - origIndex
|
||||
origIndex += delta
|
||||
|
||||
if len(chunk.InsLines) > 0 {
|
||||
destLines = append(destLines, chunk.InsLines...)
|
||||
}
|
||||
origIndex += len(chunk.DelLines)
|
||||
}
|
||||
|
||||
destLines = append(destLines, origLines[origIndex:]...)
|
||||
return strings.Join(destLines, "\n"), nil
|
||||
}
|
||||
|
||||
func PatchToCommit(patch Patch, orig map[string]string) (Commit, error) {
|
||||
commit := Commit{Changes: make(map[string]FileChange, len(patch.Actions))}
|
||||
for pathKey, action := range patch.Actions {
|
||||
switch action.Type {
|
||||
case ActionDelete:
|
||||
oldContent := orig[pathKey]
|
||||
commit.Changes[pathKey] = FileChange{
|
||||
Type: ActionDelete,
|
||||
OldContent: &oldContent,
|
||||
}
|
||||
case ActionAdd:
|
||||
commit.Changes[pathKey] = FileChange{
|
||||
Type: ActionAdd,
|
||||
NewContent: action.NewFile,
|
||||
}
|
||||
case ActionUpdate:
|
||||
newContent, err := getUpdatedFile(orig[pathKey], action, pathKey)
|
||||
if err != nil {
|
||||
return Commit{}, err
|
||||
}
|
||||
oldContent := orig[pathKey]
|
||||
fileChange := FileChange{
|
||||
Type: ActionUpdate,
|
||||
OldContent: &oldContent,
|
||||
NewContent: &newContent,
|
||||
}
|
||||
if action.MovePath != nil {
|
||||
fileChange.MovePath = action.MovePath
|
||||
}
|
||||
commit.Changes[pathKey] = fileChange
|
||||
}
|
||||
}
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
func AssembleChanges(orig map[string]string, updatedFiles map[string]string) Commit {
|
||||
commit := Commit{Changes: make(map[string]FileChange, len(updatedFiles))}
|
||||
for p, newContent := range updatedFiles {
|
||||
oldContent, exists := orig[p]
|
||||
if exists && oldContent == newContent {
|
||||
continue
|
||||
}
|
||||
|
||||
if exists && newContent != "" {
|
||||
commit.Changes[p] = FileChange{
|
||||
Type: ActionUpdate,
|
||||
OldContent: &oldContent,
|
||||
NewContent: &newContent,
|
||||
}
|
||||
} else if newContent != "" {
|
||||
commit.Changes[p] = FileChange{
|
||||
Type: ActionAdd,
|
||||
NewContent: &newContent,
|
||||
}
|
||||
} else if exists {
|
||||
commit.Changes[p] = FileChange{
|
||||
Type: ActionDelete,
|
||||
OldContent: &oldContent,
|
||||
}
|
||||
} else {
|
||||
return commit // Changed from panic to simply return current commit
|
||||
}
|
||||
}
|
||||
return commit
|
||||
}
|
||||
|
||||
func LoadFiles(paths []string, openFn func(string) (string, error)) (map[string]string, error) {
|
||||
orig := make(map[string]string, len(paths))
|
||||
for _, p := range paths {
|
||||
content, err := openFn(p)
|
||||
if err != nil {
|
||||
return nil, fileError("Open", "File not found", p)
|
||||
}
|
||||
orig[p] = content
|
||||
}
|
||||
return orig, nil
|
||||
}
|
||||
|
||||
func ApplyCommit(commit Commit, writeFn func(string, string) error, removeFn func(string) error) error {
|
||||
for p, change := range commit.Changes {
|
||||
switch change.Type {
|
||||
case ActionDelete:
|
||||
if err := removeFn(p); err != nil {
|
||||
return err
|
||||
}
|
||||
case ActionAdd:
|
||||
if change.NewContent == nil {
|
||||
return NewDiffError(fmt.Sprintf("Add action for %s has nil new_content", p))
|
||||
}
|
||||
if err := writeFn(p, *change.NewContent); err != nil {
|
||||
return err
|
||||
}
|
||||
case ActionUpdate:
|
||||
if change.NewContent == nil {
|
||||
return NewDiffError(fmt.Sprintf("Update action for %s has nil new_content", p))
|
||||
}
|
||||
if change.MovePath != nil {
|
||||
if err := writeFn(*change.MovePath, *change.NewContent); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := removeFn(p); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := writeFn(p, *change.NewContent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProcessPatch(text string, openFn func(string) (string, error), writeFn func(string, string) error, removeFn func(string) error) (string, error) {
|
||||
if !strings.HasPrefix(text, "*** Begin Patch") {
|
||||
return "", NewDiffError("Patch must start with *** Begin Patch")
|
||||
}
|
||||
paths := IdentifyFilesNeeded(text)
|
||||
orig, err := LoadFiles(paths, openFn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
patch, fuzz, err := TextToPatch(text, orig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fuzz > 0 {
|
||||
return "", NewDiffError(fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz))
|
||||
}
|
||||
|
||||
commit, err := PatchToCommit(patch, orig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := ApplyCommit(commit, writeFn, removeFn); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "Patch applied successfully", nil
|
||||
}
|
||||
|
||||
func OpenFile(p string) (string, error) {
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func WriteFile(p string, content string) error {
|
||||
if filepath.IsAbs(p) {
|
||||
return NewDiffError("We do not support absolute paths.")
|
||||
}
|
||||
|
||||
dir := filepath.Dir(p)
|
||||
if dir != "." {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(p, []byte(content), 0o644)
|
||||
}
|
||||
|
||||
func RemoveFile(p string) error {
|
||||
return os.Remove(p)
|
||||
}
|
||||
|
||||
func ValidatePatch(patchText string, files map[string]string) (bool, string, error) {
|
||||
if !strings.HasPrefix(patchText, "*** Begin Patch") {
|
||||
return false, "Patch must start with *** Begin Patch", nil
|
||||
}
|
||||
|
||||
neededFiles := IdentifyFilesNeeded(patchText)
|
||||
for _, filePath := range neededFiles {
|
||||
if _, exists := files[filePath]; !exists {
|
||||
return false, fmt.Sprintf("File not found: %s", filePath), nil
|
||||
}
|
||||
}
|
||||
|
||||
patch, fuzz, err := TextToPatch(patchText, files)
|
||||
if err != nil {
|
||||
return false, err.Error(), nil
|
||||
}
|
||||
|
||||
if fuzz > 0 {
|
||||
return false, fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz), nil
|
||||
}
|
||||
|
||||
_, err = PatchToCommit(patch, files)
|
||||
if err != nil {
|
||||
return false, err.Error(), nil
|
||||
}
|
||||
|
||||
return true, "Patch is valid", nil
|
||||
}
|
||||
252
internal/history/file.go
Normal file
252
internal/history/file.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
)
|
||||
|
||||
const (
|
||||
InitialVersion = "initial"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
ID string
|
||||
SessionID string
|
||||
Path string
|
||||
Content string
|
||||
Version string
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
pubsub.Suscriber[File]
|
||||
Create(ctx context.Context, sessionID, path, content string) (File, error)
|
||||
CreateVersion(ctx context.Context, sessionID, path, content string) (File, error)
|
||||
Get(ctx context.Context, id string) (File, error)
|
||||
GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
|
||||
ListBySession(ctx context.Context, sessionID string) ([]File, error)
|
||||
ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
|
||||
Update(ctx context.Context, file File) (File, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
DeleteSessionFiles(ctx context.Context, sessionID string) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
*pubsub.Broker[File]
|
||||
db *sql.DB
|
||||
q *db.Queries
|
||||
}
|
||||
|
||||
func NewService(q *db.Queries, db *sql.DB) Service {
|
||||
return &service{
|
||||
Broker: pubsub.NewBroker[File](),
|
||||
q: q,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Create(ctx context.Context, sessionID, path, content string) (File, error) {
|
||||
return s.createWithVersion(ctx, sessionID, path, content, InitialVersion)
|
||||
}
|
||||
|
||||
func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) {
|
||||
// Get the latest version for this path
|
||||
files, err := s.q.ListFilesByPath(ctx, path)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
// No previous versions, create initial
|
||||
return s.Create(ctx, sessionID, path, content)
|
||||
}
|
||||
|
||||
// Get the latest version
|
||||
latestFile := files[0] // Files are ordered by created_at DESC
|
||||
latestVersion := latestFile.Version
|
||||
|
||||
// Generate the next version
|
||||
var nextVersion string
|
||||
if latestVersion == InitialVersion {
|
||||
nextVersion = "v1"
|
||||
} else if strings.HasPrefix(latestVersion, "v") {
|
||||
versionNum, err := strconv.Atoi(latestVersion[1:])
|
||||
if err != nil {
|
||||
// If we can't parse the version, just use a timestamp-based version
|
||||
nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
|
||||
} else {
|
||||
nextVersion = fmt.Sprintf("v%d", versionNum+1)
|
||||
}
|
||||
} else {
|
||||
// If the version format is unexpected, use a timestamp-based version
|
||||
nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
|
||||
}
|
||||
|
||||
return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
|
||||
}
|
||||
|
||||
func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
|
||||
// Maximum number of retries for transaction conflicts
|
||||
const maxRetries = 3
|
||||
var file File
|
||||
var err error
|
||||
|
||||
// Retry loop for transaction conflicts
|
||||
for attempt := range maxRetries {
|
||||
// Start a transaction
|
||||
tx, txErr := s.db.Begin()
|
||||
if txErr != nil {
|
||||
return File{}, fmt.Errorf("failed to begin transaction: %w", txErr)
|
||||
}
|
||||
|
||||
// Create a new queries instance with the transaction
|
||||
qtx := s.q.WithTx(tx)
|
||||
|
||||
// Try to create the file within the transaction
|
||||
dbFile, txErr := qtx.CreateFile(ctx, db.CreateFileParams{
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
Path: path,
|
||||
Content: content,
|
||||
Version: version,
|
||||
})
|
||||
if txErr != nil {
|
||||
// Rollback the transaction
|
||||
tx.Rollback()
|
||||
|
||||
// Check if this is a uniqueness constraint violation
|
||||
if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
|
||||
if attempt < maxRetries-1 {
|
||||
// If we have retries left, generate a new version and try again
|
||||
if strings.HasPrefix(version, "v") {
|
||||
versionNum, parseErr := strconv.Atoi(version[1:])
|
||||
if parseErr == nil {
|
||||
version = fmt.Sprintf("v%d", versionNum+1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// If we can't parse the version, use a timestamp-based version
|
||||
version = fmt.Sprintf("v%d", time.Now().Unix())
|
||||
continue
|
||||
}
|
||||
}
|
||||
return File{}, txErr
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
if txErr = tx.Commit(); txErr != nil {
|
||||
return File{}, fmt.Errorf("failed to commit transaction: %w", txErr)
|
||||
}
|
||||
|
||||
file = s.fromDBItem(dbFile)
|
||||
s.Publish(pubsub.CreatedEvent, file)
|
||||
return file, nil
|
||||
}
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (s *service) Get(ctx context.Context, id string) (File, error) {
|
||||
dbFile, err := s.q.GetFile(ctx, id)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
return s.fromDBItem(dbFile), nil
|
||||
}
|
||||
|
||||
func (s *service) GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error) {
|
||||
dbFile, err := s.q.GetFileByPathAndSession(ctx, db.GetFileByPathAndSessionParams{
|
||||
Path: path,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
return s.fromDBItem(dbFile), nil
|
||||
}
|
||||
|
||||
func (s *service) ListBySession(ctx context.Context, sessionID string) ([]File, error) {
|
||||
dbFiles, err := s.q.ListFilesBySession(ctx, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]File, len(dbFiles))
|
||||
for i, dbFile := range dbFiles {
|
||||
files[i] = s.fromDBItem(dbFile)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) {
|
||||
dbFiles, err := s.q.ListLatestSessionFiles(ctx, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := make([]File, len(dbFiles))
|
||||
for i, dbFile := range dbFiles {
|
||||
files[i] = s.fromDBItem(dbFile)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *service) Update(ctx context.Context, file File) (File, error) {
|
||||
dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{
|
||||
ID: file.ID,
|
||||
Content: file.Content,
|
||||
Version: file.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
updatedFile := s.fromDBItem(dbFile)
|
||||
s.Publish(pubsub.UpdatedEvent, updatedFile)
|
||||
return updatedFile, nil
|
||||
}
|
||||
|
||||
func (s *service) Delete(ctx context.Context, id string) error {
|
||||
file, err := s.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.q.DeleteFile(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Publish(pubsub.DeletedEvent, file)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error {
|
||||
files, err := s.ListBySession(ctx, sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
err = s.Delete(ctx, file.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) fromDBItem(item db.File) File {
|
||||
return File{
|
||||
ID: item.ID,
|
||||
SessionID: item.SessionID,
|
||||
Path: item.Path,
|
||||
Content: item.Content,
|
||||
Version: item.Version,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}
|
||||
}
|
||||
109
internal/llm/agent/agent-tool.go
Normal file
109
internal/llm/agent/agent-tool.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
)
|
||||
|
||||
type agentTool struct {
|
||||
sessions session.Service
|
||||
messages message.Service
|
||||
lspClients map[string]*lsp.Client
|
||||
}
|
||||
|
||||
const (
|
||||
AgentToolName = "agent"
|
||||
)
|
||||
|
||||
type AgentParams struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
|
||||
func (b *agentTool) Info() tools.ToolInfo {
|
||||
return tools.ToolInfo{
|
||||
Name: AgentToolName,
|
||||
Description: "Launch a new agent that has access to the following tools: GlobTool, GrepTool, LS, View. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you. For example:\n\n- If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended\n- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the GlobTool tool instead, to find the match more quickly\n\nUsage notes:\n1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n4. The agent's outputs should generally be trusted\n5. IMPORTANT: The agent can not use Bash, Replace, Edit, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.",
|
||||
Parameters: map[string]any{
|
||||
"prompt": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The task for the agent to perform",
|
||||
},
|
||||
},
|
||||
Required: []string{"prompt"},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolResponse, error) {
|
||||
var params AgentParams
|
||||
if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil {
|
||||
return tools.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
|
||||
}
|
||||
if params.Prompt == "" {
|
||||
return tools.NewTextErrorResponse("prompt is required"), nil
|
||||
}
|
||||
|
||||
sessionID, messageID := tools.GetContextValues(ctx)
|
||||
if sessionID == "" || messageID == "" {
|
||||
return tools.ToolResponse{}, fmt.Errorf("session_id and message_id are required")
|
||||
}
|
||||
|
||||
agent, err := NewAgent(config.AgentTask, b.sessions, b.messages, TaskAgentTools(b.lspClients))
|
||||
if err != nil {
|
||||
return tools.ToolResponse{}, fmt.Errorf("error creating agent: %s", err)
|
||||
}
|
||||
|
||||
session, err := b.sessions.CreateTaskSession(ctx, call.ID, sessionID, "New Agent Session")
|
||||
if err != nil {
|
||||
return tools.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
|
||||
}
|
||||
|
||||
done, err := agent.Run(ctx, session.ID, params.Prompt)
|
||||
if err != nil {
|
||||
return tools.ToolResponse{}, fmt.Errorf("error generating agent: %s", err)
|
||||
}
|
||||
result := <-done
|
||||
if result.Error != nil {
|
||||
return tools.ToolResponse{}, fmt.Errorf("error generating agent: %s", result.Error)
|
||||
}
|
||||
|
||||
response := result.Message
|
||||
if response.Role != message.Assistant {
|
||||
return tools.NewTextErrorResponse("no response"), nil
|
||||
}
|
||||
|
||||
updatedSession, err := b.sessions.Get(ctx, session.ID)
|
||||
if err != nil {
|
||||
return tools.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
|
||||
}
|
||||
parentSession, err := b.sessions.Get(ctx, sessionID)
|
||||
if err != nil {
|
||||
return tools.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
|
||||
}
|
||||
|
||||
parentSession.Cost += updatedSession.Cost
|
||||
|
||||
_, err = b.sessions.Save(ctx, parentSession)
|
||||
if err != nil {
|
||||
return tools.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
|
||||
}
|
||||
return tools.NewTextResponse(response.Content().String()), nil
|
||||
}
|
||||
|
||||
func NewAgentTool(
|
||||
Sessions session.Service,
|
||||
Messages message.Service,
|
||||
LspClients map[string]*lsp.Client,
|
||||
) tools.BaseTool {
|
||||
return &agentTool{
|
||||
sessions: Sessions,
|
||||
messages: Messages,
|
||||
lspClients: LspClients,
|
||||
}
|
||||
}
|
||||
710
internal/llm/agent/agent.go
Normal file
710
internal/llm/agent/agent.go
Normal file
@@ -0,0 +1,710 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/prompt"
|
||||
"github.com/opencode-ai/opencode/internal/llm/provider"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrRequestCancelled = errors.New("request cancelled by user")
|
||||
ErrSessionBusy = errors.New("session is currently processing another request")
|
||||
)
|
||||
|
||||
type AgentEventType string
|
||||
|
||||
const (
|
||||
AgentEventTypeError AgentEventType = "error"
|
||||
AgentEventTypeResponse AgentEventType = "response"
|
||||
AgentEventTypeSummarize AgentEventType = "summarize"
|
||||
)
|
||||
|
||||
type AgentEvent struct {
|
||||
Type AgentEventType
|
||||
Message message.Message
|
||||
Error error
|
||||
|
||||
// When summarizing
|
||||
SessionID string
|
||||
Progress string
|
||||
Done bool
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
pubsub.Suscriber[AgentEvent]
|
||||
Model() models.Model
|
||||
Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
|
||||
Cancel(sessionID string)
|
||||
IsSessionBusy(sessionID string) bool
|
||||
IsBusy() bool
|
||||
Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
|
||||
Summarize(ctx context.Context, sessionID string) error
|
||||
}
|
||||
|
||||
type agent struct {
|
||||
*pubsub.Broker[AgentEvent]
|
||||
sessions session.Service
|
||||
messages message.Service
|
||||
|
||||
tools []tools.BaseTool
|
||||
provider provider.Provider
|
||||
|
||||
titleProvider provider.Provider
|
||||
summarizeProvider provider.Provider
|
||||
|
||||
activeRequests sync.Map
|
||||
}
|
||||
|
||||
func NewAgent(
|
||||
agentName config.AgentName,
|
||||
sessions session.Service,
|
||||
messages message.Service,
|
||||
agentTools []tools.BaseTool,
|
||||
) (Service, error) {
|
||||
agentProvider, err := createAgentProvider(agentName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var titleProvider provider.Provider
|
||||
// Only generate titles for the coder agent
|
||||
if agentName == config.AgentCoder {
|
||||
titleProvider, err = createAgentProvider(config.AgentTitle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var summarizeProvider provider.Provider
|
||||
if agentName == config.AgentCoder {
|
||||
summarizeProvider, err = createAgentProvider(config.AgentSummarizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
agent := &agent{
|
||||
Broker: pubsub.NewBroker[AgentEvent](),
|
||||
provider: agentProvider,
|
||||
messages: messages,
|
||||
sessions: sessions,
|
||||
tools: agentTools,
|
||||
titleProvider: titleProvider,
|
||||
summarizeProvider: summarizeProvider,
|
||||
activeRequests: sync.Map{},
|
||||
}
|
||||
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (a *agent) Model() models.Model {
|
||||
return a.provider.Model()
|
||||
}
|
||||
|
||||
func (a *agent) Cancel(sessionID string) {
|
||||
// Cancel regular requests
|
||||
if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
|
||||
if cancel, ok := cancelFunc.(context.CancelFunc); ok {
|
||||
logging.InfoPersist(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for summarize requests
|
||||
if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
|
||||
if cancel, ok := cancelFunc.(context.CancelFunc); ok {
|
||||
logging.InfoPersist(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) IsBusy() bool {
|
||||
busy := false
|
||||
a.activeRequests.Range(func(key, value interface{}) bool {
|
||||
if cancelFunc, ok := value.(context.CancelFunc); ok {
|
||||
if cancelFunc != nil {
|
||||
busy = true
|
||||
return false // Stop iterating
|
||||
}
|
||||
}
|
||||
return true // Continue iterating
|
||||
})
|
||||
return busy
|
||||
}
|
||||
|
||||
func (a *agent) IsSessionBusy(sessionID string) bool {
|
||||
_, busy := a.activeRequests.Load(sessionID)
|
||||
return busy
|
||||
}
|
||||
|
||||
func (a *agent) generateTitle(ctx context.Context, sessionID string, content string) error {
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
if a.titleProvider == nil {
|
||||
return nil
|
||||
}
|
||||
session, err := a.sessions.Get(ctx, sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts := []message.ContentPart{message.TextContent{Text: content}}
|
||||
response, err := a.titleProvider.SendMessages(
|
||||
ctx,
|
||||
[]message.Message{
|
||||
{
|
||||
Role: message.User,
|
||||
Parts: parts,
|
||||
},
|
||||
},
|
||||
make([]tools.BaseTool, 0),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(strings.ReplaceAll(response.Content, "\n", " "))
|
||||
if title == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
session.Title = title
|
||||
_, err = a.sessions.Save(ctx, session)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *agent) err(err error) AgentEvent {
|
||||
return AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) {
|
||||
if !a.provider.Model().SupportsAttachments && attachments != nil {
|
||||
attachments = nil
|
||||
}
|
||||
events := make(chan AgentEvent)
|
||||
if a.IsSessionBusy(sessionID) {
|
||||
return nil, ErrSessionBusy
|
||||
}
|
||||
|
||||
genCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
a.activeRequests.Store(sessionID, cancel)
|
||||
go func() {
|
||||
logging.Debug("Request started", "sessionID", sessionID)
|
||||
defer logging.RecoverPanic("agent.Run", func() {
|
||||
events <- a.err(fmt.Errorf("panic while running the agent"))
|
||||
})
|
||||
var attachmentParts []message.ContentPart
|
||||
for _, attachment := range attachments {
|
||||
attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
|
||||
}
|
||||
result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
|
||||
if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
|
||||
logging.ErrorPersist(result.Error.Error())
|
||||
}
|
||||
logging.Debug("Request completed", "sessionID", sessionID)
|
||||
a.activeRequests.Delete(sessionID)
|
||||
cancel()
|
||||
a.Publish(pubsub.CreatedEvent, result)
|
||||
events <- result
|
||||
close(events)
|
||||
}()
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (a *agent) processGeneration(ctx context.Context, sessionID, content string, attachmentParts []message.ContentPart) AgentEvent {
|
||||
// List existing messages; if none, start title generation asynchronously.
|
||||
msgs, err := a.messages.List(ctx, sessionID)
|
||||
if err != nil {
|
||||
return a.err(fmt.Errorf("failed to list messages: %w", err))
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
go func() {
|
||||
defer logging.RecoverPanic("agent.Run", func() {
|
||||
logging.ErrorPersist("panic while generating title")
|
||||
})
|
||||
titleErr := a.generateTitle(context.Background(), sessionID, content)
|
||||
if titleErr != nil {
|
||||
logging.ErrorPersist(fmt.Sprintf("failed to generate title: %v", titleErr))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
userMsg, err := a.createUserMessage(ctx, sessionID, content, attachmentParts)
|
||||
if err != nil {
|
||||
return a.err(fmt.Errorf("failed to create user message: %w", err))
|
||||
}
|
||||
// Append the new user message to the conversation history.
|
||||
msgHistory := append(msgs, userMsg)
|
||||
|
||||
for {
|
||||
// Check for cancellation before each iteration
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return a.err(ctx.Err())
|
||||
default:
|
||||
// Continue processing
|
||||
}
|
||||
agentMessage, toolResults, err := a.streamAndHandleEvents(ctx, sessionID, msgHistory)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
agentMessage.AddFinish(message.FinishReasonCanceled)
|
||||
a.messages.Update(context.Background(), agentMessage)
|
||||
return a.err(ErrRequestCancelled)
|
||||
}
|
||||
return a.err(fmt.Errorf("failed to process events: %w", err))
|
||||
}
|
||||
logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
|
||||
if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
|
||||
// We are not done, we need to respond with the tool response
|
||||
msgHistory = append(msgHistory, agentMessage, *toolResults)
|
||||
continue
|
||||
}
|
||||
return AgentEvent{
|
||||
Type: AgentEventTypeResponse,
|
||||
Message: agentMessage,
|
||||
Done: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) createUserMessage(ctx context.Context, sessionID, content string, attachmentParts []message.ContentPart) (message.Message, error) {
|
||||
parts := []message.ContentPart{message.TextContent{Text: content}}
|
||||
parts = append(parts, attachmentParts...)
|
||||
return a.messages.Create(ctx, sessionID, message.CreateMessageParams{
|
||||
Role: message.User,
|
||||
Parts: parts,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msgHistory []message.Message) (message.Message, *message.Message, error) {
|
||||
eventChan := a.provider.StreamResponse(ctx, msgHistory, a.tools)
|
||||
|
||||
assistantMsg, err := a.messages.Create(ctx, sessionID, message.CreateMessageParams{
|
||||
Role: message.Assistant,
|
||||
Parts: []message.ContentPart{},
|
||||
Model: a.provider.Model().ID,
|
||||
})
|
||||
if err != nil {
|
||||
return assistantMsg, nil, fmt.Errorf("failed to create assistant message: %w", err)
|
||||
}
|
||||
|
||||
// Add the session and message ID into the context if needed by tools.
|
||||
ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
|
||||
ctx = context.WithValue(ctx, tools.SessionIDContextKey, sessionID)
|
||||
|
||||
// Process each event in the stream.
|
||||
for event := range eventChan {
|
||||
if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil {
|
||||
a.finishMessage(ctx, &assistantMsg, message.FinishReasonCanceled)
|
||||
return assistantMsg, nil, processErr
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
|
||||
return assistantMsg, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
toolResults := make([]message.ToolResult, len(assistantMsg.ToolCalls()))
|
||||
toolCalls := assistantMsg.ToolCalls()
|
||||
for i, toolCall := range toolCalls {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled)
|
||||
// Make all future tool calls cancelled
|
||||
for j := i; j < len(toolCalls); j++ {
|
||||
toolResults[j] = message.ToolResult{
|
||||
ToolCallID: toolCalls[j].ID,
|
||||
Content: "Tool execution canceled by user",
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
goto out
|
||||
default:
|
||||
// Continue processing
|
||||
var tool tools.BaseTool
|
||||
for _, availableTools := range a.tools {
|
||||
if availableTools.Info().Name == toolCall.Name {
|
||||
tool = availableTools
|
||||
}
|
||||
}
|
||||
|
||||
// Tool not found
|
||||
if tool == nil {
|
||||
toolResults[i] = message.ToolResult{
|
||||
ToolCallID: toolCall.ID,
|
||||
Content: fmt.Sprintf("Tool not found: %s", toolCall.Name),
|
||||
IsError: true,
|
||||
}
|
||||
continue
|
||||
}
|
||||
toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
|
||||
ID: toolCall.ID,
|
||||
Name: toolCall.Name,
|
||||
Input: toolCall.Input,
|
||||
})
|
||||
if toolErr != nil {
|
||||
if errors.Is(toolErr, permission.ErrorPermissionDenied) {
|
||||
toolResults[i] = message.ToolResult{
|
||||
ToolCallID: toolCall.ID,
|
||||
Content: "Permission denied",
|
||||
IsError: true,
|
||||
}
|
||||
for j := i + 1; j < len(toolCalls); j++ {
|
||||
toolResults[j] = message.ToolResult{
|
||||
ToolCallID: toolCalls[j].ID,
|
||||
Content: "Tool execution canceled by user",
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
a.finishMessage(ctx, &assistantMsg, message.FinishReasonPermissionDenied)
|
||||
break
|
||||
}
|
||||
}
|
||||
toolResults[i] = message.ToolResult{
|
||||
ToolCallID: toolCall.ID,
|
||||
Content: toolResult.Content,
|
||||
Metadata: toolResult.Metadata,
|
||||
IsError: toolResult.IsError,
|
||||
}
|
||||
}
|
||||
}
|
||||
out:
|
||||
if len(toolResults) == 0 {
|
||||
return assistantMsg, nil, nil
|
||||
}
|
||||
parts := make([]message.ContentPart, 0)
|
||||
for _, tr := range toolResults {
|
||||
parts = append(parts, tr)
|
||||
}
|
||||
msg, err := a.messages.Create(context.Background(), assistantMsg.SessionID, message.CreateMessageParams{
|
||||
Role: message.Tool,
|
||||
Parts: parts,
|
||||
})
|
||||
if err != nil {
|
||||
return assistantMsg, nil, fmt.Errorf("failed to create cancelled tool message: %w", err)
|
||||
}
|
||||
|
||||
return assistantMsg, &msg, err
|
||||
}
|
||||
|
||||
func (a *agent) finishMessage(ctx context.Context, msg *message.Message, finishReson message.FinishReason) {
|
||||
msg.AddFinish(finishReson)
|
||||
_ = a.messages.Update(ctx, *msg)
|
||||
}
|
||||
|
||||
func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg *message.Message, event provider.ProviderEvent) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
// Continue processing.
|
||||
}
|
||||
|
||||
switch event.Type {
|
||||
case provider.EventThinkingDelta:
|
||||
assistantMsg.AppendReasoningContent(event.Content)
|
||||
return a.messages.Update(ctx, *assistantMsg)
|
||||
case provider.EventContentDelta:
|
||||
assistantMsg.AppendContent(event.Content)
|
||||
return a.messages.Update(ctx, *assistantMsg)
|
||||
case provider.EventToolUseStart:
|
||||
assistantMsg.AddToolCall(*event.ToolCall)
|
||||
return a.messages.Update(ctx, *assistantMsg)
|
||||
// TODO: see how to handle this
|
||||
// case provider.EventToolUseDelta:
|
||||
// tm := time.Unix(assistantMsg.UpdatedAt, 0)
|
||||
// assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
|
||||
// if time.Since(tm) > 1000*time.Millisecond {
|
||||
// err := a.messages.Update(ctx, *assistantMsg)
|
||||
// assistantMsg.UpdatedAt = time.Now().Unix()
|
||||
// return err
|
||||
// }
|
||||
case provider.EventToolUseStop:
|
||||
assistantMsg.FinishToolCall(event.ToolCall.ID)
|
||||
return a.messages.Update(ctx, *assistantMsg)
|
||||
case provider.EventError:
|
||||
if errors.Is(event.Error, context.Canceled) {
|
||||
logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
|
||||
return context.Canceled
|
||||
}
|
||||
logging.ErrorPersist(event.Error.Error())
|
||||
return event.Error
|
||||
case provider.EventComplete:
|
||||
assistantMsg.SetToolCalls(event.Response.ToolCalls)
|
||||
assistantMsg.AddFinish(event.Response.FinishReason)
|
||||
if err := a.messages.Update(ctx, *assistantMsg); err != nil {
|
||||
return fmt.Errorf("failed to update message: %w", err)
|
||||
}
|
||||
return a.TrackUsage(ctx, sessionID, a.provider.Model(), event.Response.Usage)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.Model, usage provider.TokenUsage) error {
|
||||
sess, err := a.sessions.Get(ctx, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get session: %w", err)
|
||||
}
|
||||
|
||||
cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
|
||||
model.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
|
||||
model.CostPer1MIn/1e6*float64(usage.InputTokens) +
|
||||
model.CostPer1MOut/1e6*float64(usage.OutputTokens)
|
||||
|
||||
sess.Cost += cost
|
||||
sess.CompletionTokens = usage.OutputTokens + usage.CacheReadTokens
|
||||
sess.PromptTokens = usage.InputTokens + usage.CacheCreationTokens
|
||||
|
||||
_, err = a.sessions.Save(ctx, sess)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *agent) Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error) {
|
||||
if a.IsBusy() {
|
||||
return models.Model{}, fmt.Errorf("cannot change model while processing requests")
|
||||
}
|
||||
|
||||
if err := config.UpdateAgentModel(agentName, modelID); err != nil {
|
||||
return models.Model{}, fmt.Errorf("failed to update config: %w", err)
|
||||
}
|
||||
|
||||
provider, err := createAgentProvider(agentName)
|
||||
if err != nil {
|
||||
return models.Model{}, fmt.Errorf("failed to create provider for model %s: %w", modelID, err)
|
||||
}
|
||||
|
||||
a.provider = provider
|
||||
|
||||
return a.provider.Model(), nil
|
||||
}
|
||||
|
||||
func (a *agent) Summarize(ctx context.Context, sessionID string) error {
|
||||
if a.summarizeProvider == nil {
|
||||
return fmt.Errorf("summarize provider not available")
|
||||
}
|
||||
|
||||
// Check if session is busy
|
||||
if a.IsSessionBusy(sessionID) {
|
||||
return ErrSessionBusy
|
||||
}
|
||||
|
||||
// Create a new context with cancellation
|
||||
summarizeCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// Store the cancel function in activeRequests to allow cancellation
|
||||
a.activeRequests.Store(sessionID+"-summarize", cancel)
|
||||
|
||||
go func() {
|
||||
defer a.activeRequests.Delete(sessionID + "-summarize")
|
||||
defer cancel()
|
||||
event := AgentEvent{
|
||||
Type: AgentEventTypeSummarize,
|
||||
Progress: "Starting summarization...",
|
||||
}
|
||||
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
// Get all messages from the session
|
||||
msgs, err := a.messages.List(summarizeCtx, sessionID)
|
||||
if err != nil {
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: fmt.Errorf("failed to list messages: %w", err),
|
||||
Done: true,
|
||||
}
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
return
|
||||
}
|
||||
|
||||
if len(msgs) == 0 {
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: fmt.Errorf("no messages to summarize"),
|
||||
Done: true,
|
||||
}
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
return
|
||||
}
|
||||
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeSummarize,
|
||||
Progress: "Analyzing conversation...",
|
||||
}
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
|
||||
// Add a system message to guide the summarization
|
||||
summarizePrompt := "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next."
|
||||
|
||||
// Create a new message with the summarize prompt
|
||||
promptMsg := message.Message{
|
||||
Role: message.User,
|
||||
Parts: []message.ContentPart{message.TextContent{Text: summarizePrompt}},
|
||||
}
|
||||
|
||||
// Append the prompt to the messages
|
||||
msgsWithPrompt := append(msgs, promptMsg)
|
||||
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeSummarize,
|
||||
Progress: "Generating summary...",
|
||||
}
|
||||
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
|
||||
// Send the messages to the summarize provider
|
||||
response, err := a.summarizeProvider.SendMessages(
|
||||
summarizeCtx,
|
||||
msgsWithPrompt,
|
||||
make([]tools.BaseTool, 0),
|
||||
)
|
||||
if err != nil {
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: fmt.Errorf("failed to summarize: %w", err),
|
||||
Done: true,
|
||||
}
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
return
|
||||
}
|
||||
|
||||
summary := strings.TrimSpace(response.Content)
|
||||
if summary == "" {
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: fmt.Errorf("empty summary returned"),
|
||||
Done: true,
|
||||
}
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
return
|
||||
}
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeSummarize,
|
||||
Progress: "Creating new session...",
|
||||
}
|
||||
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
oldSession, err := a.sessions.Get(summarizeCtx, sessionID)
|
||||
if err != nil {
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: fmt.Errorf("failed to get session: %w", err),
|
||||
Done: true,
|
||||
}
|
||||
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
return
|
||||
}
|
||||
// Create a new session with the summary
|
||||
newSession, err := a.sessions.Create(summarizeCtx, oldSession.Title+" - Continuation")
|
||||
if err != nil {
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: fmt.Errorf("failed to create new session: %w", err),
|
||||
Done: true,
|
||||
}
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a message in the new session with the summary
|
||||
_, err = a.messages.Create(summarizeCtx, newSession.ID, message.CreateMessageParams{
|
||||
Role: message.Assistant,
|
||||
Parts: []message.ContentPart{message.TextContent{Text: summary}},
|
||||
Model: a.summarizeProvider.Model().ID,
|
||||
})
|
||||
if err != nil {
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeError,
|
||||
Error: fmt.Errorf("failed to create summary message: %w", err),
|
||||
Done: true,
|
||||
}
|
||||
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
return
|
||||
}
|
||||
event = AgentEvent{
|
||||
Type: AgentEventTypeSummarize,
|
||||
SessionID: newSession.ID,
|
||||
Progress: "Summary complete",
|
||||
Done: true,
|
||||
}
|
||||
a.Publish(pubsub.CreatedEvent, event)
|
||||
// Send final success event with the new session ID
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
|
||||
cfg := config.Get()
|
||||
agentConfig, ok := cfg.Agents[agentName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("agent %s not found", agentName)
|
||||
}
|
||||
model, ok := models.SupportedModels[agentConfig.Model]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("model %s not supported", agentConfig.Model)
|
||||
}
|
||||
|
||||
providerCfg, ok := cfg.Providers[model.Provider]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("provider %s not supported", model.Provider)
|
||||
}
|
||||
if providerCfg.Disabled {
|
||||
return nil, fmt.Errorf("provider %s is not enabled", model.Provider)
|
||||
}
|
||||
maxTokens := model.DefaultMaxTokens
|
||||
if agentConfig.MaxTokens > 0 {
|
||||
maxTokens = agentConfig.MaxTokens
|
||||
}
|
||||
opts := []provider.ProviderClientOption{
|
||||
provider.WithAPIKey(providerCfg.APIKey),
|
||||
provider.WithModel(model),
|
||||
provider.WithSystemMessage(prompt.GetAgentPrompt(agentName, model.Provider)),
|
||||
provider.WithMaxTokens(maxTokens),
|
||||
}
|
||||
if model.Provider == models.ProviderOpenAI && model.CanReason {
|
||||
opts = append(
|
||||
opts,
|
||||
provider.WithOpenAIOptions(
|
||||
provider.WithReasoningEffort(agentConfig.ReasoningEffort),
|
||||
),
|
||||
)
|
||||
} else if model.Provider == models.ProviderAnthropic && model.CanReason && agentName == config.AgentCoder {
|
||||
opts = append(
|
||||
opts,
|
||||
provider.WithAnthropicOptions(
|
||||
provider.WithAnthropicShouldThinkFn(provider.DefaultShouldThinkFn),
|
||||
),
|
||||
)
|
||||
}
|
||||
agentProvider, err := provider.NewProvider(
|
||||
model.Provider,
|
||||
opts...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create provider: %v", err)
|
||||
}
|
||||
|
||||
return agentProvider, nil
|
||||
}
|
||||
197
internal/llm/agent/mcp-tools.go
Normal file
197
internal/llm/agent/mcp-tools.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/version"
|
||||
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type mcpTool struct {
|
||||
mcpName string
|
||||
tool mcp.Tool
|
||||
mcpConfig config.MCPServer
|
||||
permissions permission.Service
|
||||
}
|
||||
|
||||
type MCPClient interface {
|
||||
Initialize(
|
||||
ctx context.Context,
|
||||
request mcp.InitializeRequest,
|
||||
) (*mcp.InitializeResult, error)
|
||||
ListTools(ctx context.Context, request mcp.ListToolsRequest) (*mcp.ListToolsResult, error)
|
||||
CallTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
func (b *mcpTool) Info() tools.ToolInfo {
|
||||
return tools.ToolInfo{
|
||||
Name: fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name),
|
||||
Description: b.tool.Description,
|
||||
Parameters: b.tool.InputSchema.Properties,
|
||||
Required: b.tool.InputSchema.Required,
|
||||
}
|
||||
}
|
||||
|
||||
func runTool(ctx context.Context, c MCPClient, toolName string, input string) (tools.ToolResponse, error) {
|
||||
defer c.Close()
|
||||
initRequest := mcp.InitializeRequest{}
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
||||
Name: "OpenCode",
|
||||
Version: version.Version,
|
||||
}
|
||||
|
||||
_, err := c.Initialize(ctx, initRequest)
|
||||
if err != nil {
|
||||
return tools.NewTextErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
toolRequest := mcp.CallToolRequest{}
|
||||
toolRequest.Params.Name = toolName
|
||||
var args map[string]any
|
||||
if err = json.Unmarshal([]byte(input), &args); err != nil {
|
||||
return tools.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
|
||||
}
|
||||
toolRequest.Params.Arguments = args
|
||||
result, err := c.CallTool(ctx, toolRequest)
|
||||
if err != nil {
|
||||
return tools.NewTextErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
output := ""
|
||||
for _, v := range result.Content {
|
||||
if v, ok := v.(mcp.TextContent); ok {
|
||||
output = v.Text
|
||||
} else {
|
||||
output = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
return tools.NewTextResponse(output), nil
|
||||
}
|
||||
|
||||
func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolResponse, error) {
|
||||
sessionID, messageID := tools.GetContextValues(ctx)
|
||||
if sessionID == "" || messageID == "" {
|
||||
return tools.ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file")
|
||||
}
|
||||
permissionDescription := fmt.Sprintf("execute %s with the following parameters: %s", b.Info().Name, params.Input)
|
||||
p := b.permissions.Request(
|
||||
permission.CreatePermissionRequest{
|
||||
SessionID: sessionID,
|
||||
Path: config.WorkingDirectory(),
|
||||
ToolName: b.Info().Name,
|
||||
Action: "execute",
|
||||
Description: permissionDescription,
|
||||
Params: params.Input,
|
||||
},
|
||||
)
|
||||
if !p {
|
||||
return tools.NewTextErrorResponse("permission denied"), nil
|
||||
}
|
||||
|
||||
switch b.mcpConfig.Type {
|
||||
case config.MCPStdio:
|
||||
c, err := client.NewStdioMCPClient(
|
||||
b.mcpConfig.Command,
|
||||
b.mcpConfig.Env,
|
||||
b.mcpConfig.Args...,
|
||||
)
|
||||
if err != nil {
|
||||
return tools.NewTextErrorResponse(err.Error()), nil
|
||||
}
|
||||
return runTool(ctx, c, b.tool.Name, params.Input)
|
||||
case config.MCPSse:
|
||||
c, err := client.NewSSEMCPClient(
|
||||
b.mcpConfig.URL,
|
||||
client.WithHeaders(b.mcpConfig.Headers),
|
||||
)
|
||||
if err != nil {
|
||||
return tools.NewTextErrorResponse(err.Error()), nil
|
||||
}
|
||||
return runTool(ctx, c, b.tool.Name, params.Input)
|
||||
}
|
||||
|
||||
return tools.NewTextErrorResponse("invalid mcp type"), nil
|
||||
}
|
||||
|
||||
func NewMcpTool(name string, tool mcp.Tool, permissions permission.Service, mcpConfig config.MCPServer) tools.BaseTool {
|
||||
return &mcpTool{
|
||||
mcpName: name,
|
||||
tool: tool,
|
||||
mcpConfig: mcpConfig,
|
||||
permissions: permissions,
|
||||
}
|
||||
}
|
||||
|
||||
var mcpTools []tools.BaseTool
|
||||
|
||||
func getTools(ctx context.Context, name string, m config.MCPServer, permissions permission.Service, c MCPClient) []tools.BaseTool {
|
||||
var stdioTools []tools.BaseTool
|
||||
initRequest := mcp.InitializeRequest{}
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
||||
Name: "OpenCode",
|
||||
Version: version.Version,
|
||||
}
|
||||
|
||||
_, err := c.Initialize(ctx, initRequest)
|
||||
if err != nil {
|
||||
logging.Error("error initializing mcp client", "error", err)
|
||||
return stdioTools
|
||||
}
|
||||
toolsRequest := mcp.ListToolsRequest{}
|
||||
tools, err := c.ListTools(ctx, toolsRequest)
|
||||
if err != nil {
|
||||
logging.Error("error listing tools", "error", err)
|
||||
return stdioTools
|
||||
}
|
||||
for _, t := range tools.Tools {
|
||||
stdioTools = append(stdioTools, NewMcpTool(name, t, permissions, m))
|
||||
}
|
||||
defer c.Close()
|
||||
return stdioTools
|
||||
}
|
||||
|
||||
func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.BaseTool {
|
||||
if len(mcpTools) > 0 {
|
||||
return mcpTools
|
||||
}
|
||||
for name, m := range config.Get().MCPServers {
|
||||
switch m.Type {
|
||||
case config.MCPStdio:
|
||||
c, err := client.NewStdioMCPClient(
|
||||
m.Command,
|
||||
m.Env,
|
||||
m.Args...,
|
||||
)
|
||||
if err != nil {
|
||||
logging.Error("error creating mcp client", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
|
||||
case config.MCPSse:
|
||||
c, err := client.NewSSEMCPClient(
|
||||
m.URL,
|
||||
client.WithHeaders(m.Headers),
|
||||
)
|
||||
if err != nil {
|
||||
logging.Error("error creating mcp client", "error", err)
|
||||
continue
|
||||
}
|
||||
mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
|
||||
}
|
||||
}
|
||||
|
||||
return mcpTools
|
||||
}
|
||||
51
internal/llm/agent/tools.go
Normal file
51
internal/llm/agent/tools.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/history"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
)
|
||||
|
||||
func CoderAgentTools(
|
||||
permissions permission.Service,
|
||||
sessions session.Service,
|
||||
messages message.Service,
|
||||
history history.Service,
|
||||
lspClients map[string]*lsp.Client,
|
||||
) []tools.BaseTool {
|
||||
ctx := context.Background()
|
||||
otherTools := GetMcpTools(ctx, permissions)
|
||||
if len(lspClients) > 0 {
|
||||
otherTools = append(otherTools, tools.NewDiagnosticsTool(lspClients))
|
||||
}
|
||||
return append(
|
||||
[]tools.BaseTool{
|
||||
tools.NewBashTool(permissions),
|
||||
tools.NewEditTool(lspClients, permissions, history),
|
||||
tools.NewFetchTool(permissions),
|
||||
tools.NewGlobTool(),
|
||||
tools.NewGrepTool(),
|
||||
tools.NewLsTool(),
|
||||
tools.NewSourcegraphTool(),
|
||||
tools.NewViewTool(lspClients),
|
||||
tools.NewPatchTool(lspClients, permissions, history),
|
||||
tools.NewWriteTool(lspClients, permissions, history),
|
||||
NewAgentTool(sessions, messages, lspClients),
|
||||
}, otherTools...,
|
||||
)
|
||||
}
|
||||
|
||||
func TaskAgentTools(lspClients map[string]*lsp.Client) []tools.BaseTool {
|
||||
return []tools.BaseTool{
|
||||
tools.NewGlobTool(),
|
||||
tools.NewGrepTool(),
|
||||
tools.NewLsTool(),
|
||||
tools.NewSourcegraphTool(),
|
||||
tools.NewViewTool(lspClients),
|
||||
}
|
||||
}
|
||||
82
internal/llm/models/anthropic.go
Normal file
82
internal/llm/models/anthropic.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
ProviderAnthropic ModelProvider = "anthropic"
|
||||
|
||||
// Models
|
||||
Claude35Sonnet ModelID = "claude-3.5-sonnet"
|
||||
Claude3Haiku ModelID = "claude-3-haiku"
|
||||
Claude37Sonnet ModelID = "claude-3.7-sonnet"
|
||||
Claude35Haiku ModelID = "claude-3.5-haiku"
|
||||
Claude3Opus ModelID = "claude-3-opus"
|
||||
)
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models/all-models
|
||||
var AnthropicModels = map[ModelID]Model{
|
||||
Claude35Sonnet: {
|
||||
ID: Claude35Sonnet,
|
||||
Name: "Claude 3.5 Sonnet",
|
||||
Provider: ProviderAnthropic,
|
||||
APIModel: "claude-3-5-sonnet-latest",
|
||||
CostPer1MIn: 3.0,
|
||||
CostPer1MInCached: 3.75,
|
||||
CostPer1MOutCached: 0.30,
|
||||
CostPer1MOut: 15.0,
|
||||
ContextWindow: 200000,
|
||||
DefaultMaxTokens: 5000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
Claude3Haiku: {
|
||||
ID: Claude3Haiku,
|
||||
Name: "Claude 3 Haiku",
|
||||
Provider: ProviderAnthropic,
|
||||
APIModel: "claude-3-haiku-20240307", // doesn't support "-latest"
|
||||
CostPer1MIn: 0.25,
|
||||
CostPer1MInCached: 0.30,
|
||||
CostPer1MOutCached: 0.03,
|
||||
CostPer1MOut: 1.25,
|
||||
ContextWindow: 200000,
|
||||
DefaultMaxTokens: 4096,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
Claude37Sonnet: {
|
||||
ID: Claude37Sonnet,
|
||||
Name: "Claude 3.7 Sonnet",
|
||||
Provider: ProviderAnthropic,
|
||||
APIModel: "claude-3-7-sonnet-latest",
|
||||
CostPer1MIn: 3.0,
|
||||
CostPer1MInCached: 3.75,
|
||||
CostPer1MOutCached: 0.30,
|
||||
CostPer1MOut: 15.0,
|
||||
ContextWindow: 200000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
Claude35Haiku: {
|
||||
ID: Claude35Haiku,
|
||||
Name: "Claude 3.5 Haiku",
|
||||
Provider: ProviderAnthropic,
|
||||
APIModel: "claude-3-5-haiku-latest",
|
||||
CostPer1MIn: 0.80,
|
||||
CostPer1MInCached: 1.0,
|
||||
CostPer1MOutCached: 0.08,
|
||||
CostPer1MOut: 4.0,
|
||||
ContextWindow: 200000,
|
||||
DefaultMaxTokens: 4096,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
Claude3Opus: {
|
||||
ID: Claude3Opus,
|
||||
Name: "Claude 3 Opus",
|
||||
Provider: ProviderAnthropic,
|
||||
APIModel: "claude-3-opus-latest",
|
||||
CostPer1MIn: 15.0,
|
||||
CostPer1MInCached: 18.75,
|
||||
CostPer1MOutCached: 1.50,
|
||||
CostPer1MOut: 75.0,
|
||||
ContextWindow: 200000,
|
||||
DefaultMaxTokens: 4096,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
}
|
||||
168
internal/llm/models/azure.go
Normal file
168
internal/llm/models/azure.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package models
|
||||
|
||||
const ProviderAzure ModelProvider = "azure"
|
||||
|
||||
const (
|
||||
AzureGPT41 ModelID = "azure.gpt-4.1"
|
||||
AzureGPT41Mini ModelID = "azure.gpt-4.1-mini"
|
||||
AzureGPT41Nano ModelID = "azure.gpt-4.1-nano"
|
||||
AzureGPT45Preview ModelID = "azure.gpt-4.5-preview"
|
||||
AzureGPT4o ModelID = "azure.gpt-4o"
|
||||
AzureGPT4oMini ModelID = "azure.gpt-4o-mini"
|
||||
AzureO1 ModelID = "azure.o1"
|
||||
AzureO1Mini ModelID = "azure.o1-mini"
|
||||
AzureO3 ModelID = "azure.o3"
|
||||
AzureO3Mini ModelID = "azure.o3-mini"
|
||||
AzureO4Mini ModelID = "azure.o4-mini"
|
||||
)
|
||||
|
||||
var AzureModels = map[ModelID]Model{
|
||||
AzureGPT41: {
|
||||
ID: AzureGPT41,
|
||||
Name: "Azure OpenAI – GPT 4.1",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.1",
|
||||
CostPer1MIn: OpenAIModels[GPT41].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT41Mini: {
|
||||
ID: AzureGPT41Mini,
|
||||
Name: "Azure OpenAI – GPT 4.1 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.1-mini",
|
||||
CostPer1MIn: OpenAIModels[GPT41Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41Mini].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT41Nano: {
|
||||
ID: AzureGPT41Nano,
|
||||
Name: "Azure OpenAI – GPT 4.1 nano",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.1-nano",
|
||||
CostPer1MIn: OpenAIModels[GPT41Nano].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41Nano].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41Nano].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41Nano].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41Nano].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41Nano].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT45Preview: {
|
||||
ID: AzureGPT45Preview,
|
||||
Name: "Azure OpenAI – GPT 4.5 preview",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.5-preview",
|
||||
CostPer1MIn: OpenAIModels[GPT45Preview].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT45Preview].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT45Preview].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT45Preview].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT45Preview].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT45Preview].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT4o: {
|
||||
ID: AzureGPT4o,
|
||||
Name: "Azure OpenAI – GPT-4o",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4o",
|
||||
CostPer1MIn: OpenAIModels[GPT4o].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT4o].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT4o].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT4o].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT4o].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT4o].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT4oMini: {
|
||||
ID: AzureGPT4oMini,
|
||||
Name: "Azure OpenAI – GPT-4o mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4o-mini",
|
||||
CostPer1MIn: OpenAIModels[GPT4oMini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT4oMini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT4oMini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT4oMini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT4oMini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT4oMini].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO1: {
|
||||
ID: AzureO1,
|
||||
Name: "Azure OpenAI – O1",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o1",
|
||||
CostPer1MIn: OpenAIModels[O1].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O1].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O1].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O1].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O1].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O1].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O1].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO1Mini: {
|
||||
ID: AzureO1Mini,
|
||||
Name: "Azure OpenAI – O1 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o1-mini",
|
||||
CostPer1MIn: OpenAIModels[O1Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O1Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O1Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O1Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O1Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O1Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O1Mini].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO3: {
|
||||
ID: AzureO3,
|
||||
Name: "Azure OpenAI – O3",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o3",
|
||||
CostPer1MIn: OpenAIModels[O3].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O3].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O3].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O3].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O3].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O3].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O3].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO3Mini: {
|
||||
ID: AzureO3Mini,
|
||||
Name: "Azure OpenAI – O3 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o3-mini",
|
||||
CostPer1MIn: OpenAIModels[O3Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O3Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O3Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O3Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O3Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O3Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O3Mini].CanReason,
|
||||
SupportsAttachments: false,
|
||||
},
|
||||
AzureO4Mini: {
|
||||
ID: AzureO4Mini,
|
||||
Name: "Azure OpenAI – O4 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o4-mini",
|
||||
CostPer1MIn: OpenAIModels[O4Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O4Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O4Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O4Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O4Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O4Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O4Mini].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
}
|
||||
67
internal/llm/models/gemini.go
Normal file
67
internal/llm/models/gemini.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
ProviderGemini ModelProvider = "gemini"
|
||||
|
||||
// Models
|
||||
Gemini25Flash ModelID = "gemini-2.5-flash"
|
||||
Gemini25 ModelID = "gemini-2.5"
|
||||
Gemini20Flash ModelID = "gemini-2.0-flash"
|
||||
Gemini20FlashLite ModelID = "gemini-2.0-flash-lite"
|
||||
)
|
||||
|
||||
var GeminiModels = map[ModelID]Model{
|
||||
Gemini25Flash: {
|
||||
ID: Gemini25Flash,
|
||||
Name: "Gemini 2.5 Flash",
|
||||
Provider: ProviderGemini,
|
||||
APIModel: "gemini-2.5-flash-preview-04-17",
|
||||
CostPer1MIn: 0.15,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 0.60,
|
||||
ContextWindow: 1000000,
|
||||
DefaultMaxTokens: 50000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
Gemini25: {
|
||||
ID: Gemini25,
|
||||
Name: "Gemini 2.5 Pro",
|
||||
Provider: ProviderGemini,
|
||||
APIModel: "gemini-2.5-pro-preview-03-25",
|
||||
CostPer1MIn: 1.25,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 10,
|
||||
ContextWindow: 1000000,
|
||||
DefaultMaxTokens: 50000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
|
||||
Gemini20Flash: {
|
||||
ID: Gemini20Flash,
|
||||
Name: "Gemini 2.0 Flash",
|
||||
Provider: ProviderGemini,
|
||||
APIModel: "gemini-2.0-flash",
|
||||
CostPer1MIn: 0.10,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 0.40,
|
||||
ContextWindow: 1000000,
|
||||
DefaultMaxTokens: 6000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
Gemini20FlashLite: {
|
||||
ID: Gemini20FlashLite,
|
||||
Name: "Gemini 2.0 Flash Lite",
|
||||
Provider: ProviderGemini,
|
||||
APIModel: "gemini-2.0-flash-lite",
|
||||
CostPer1MIn: 0.05,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 0.30,
|
||||
ContextWindow: 1000000,
|
||||
DefaultMaxTokens: 6000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
}
|
||||
87
internal/llm/models/groq.go
Normal file
87
internal/llm/models/groq.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
ProviderGROQ ModelProvider = "groq"
|
||||
|
||||
// GROQ
|
||||
QWENQwq ModelID = "qwen-qwq"
|
||||
|
||||
// GROQ preview models
|
||||
Llama4Scout ModelID = "meta-llama/llama-4-scout-17b-16e-instruct"
|
||||
Llama4Maverick ModelID = "meta-llama/llama-4-maverick-17b-128e-instruct"
|
||||
Llama3_3_70BVersatile ModelID = "llama-3.3-70b-versatile"
|
||||
DeepseekR1DistillLlama70b ModelID = "deepseek-r1-distill-llama-70b"
|
||||
)
|
||||
|
||||
var GroqModels = map[ModelID]Model{
|
||||
//
|
||||
// GROQ
|
||||
QWENQwq: {
|
||||
ID: QWENQwq,
|
||||
Name: "Qwen Qwq",
|
||||
Provider: ProviderGROQ,
|
||||
APIModel: "qwen-qwq-32b",
|
||||
CostPer1MIn: 0.29,
|
||||
CostPer1MInCached: 0.275,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 0.39,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
// for some reason, the groq api doesn't like the reasoningEffort parameter
|
||||
CanReason: false,
|
||||
SupportsAttachments: false,
|
||||
},
|
||||
|
||||
Llama4Scout: {
|
||||
ID: Llama4Scout,
|
||||
Name: "Llama4Scout",
|
||||
Provider: ProviderGROQ,
|
||||
APIModel: "meta-llama/llama-4-scout-17b-16e-instruct",
|
||||
CostPer1MIn: 0.11,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 0.34,
|
||||
ContextWindow: 128_000, // 10M when?
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
|
||||
Llama4Maverick: {
|
||||
ID: Llama4Maverick,
|
||||
Name: "Llama4Maverick",
|
||||
Provider: ProviderGROQ,
|
||||
APIModel: "meta-llama/llama-4-maverick-17b-128e-instruct",
|
||||
CostPer1MIn: 0.20,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 0.20,
|
||||
ContextWindow: 128_000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
|
||||
Llama3_3_70BVersatile: {
|
||||
ID: Llama3_3_70BVersatile,
|
||||
Name: "Llama3_3_70BVersatile",
|
||||
Provider: ProviderGROQ,
|
||||
APIModel: "llama-3.3-70b-versatile",
|
||||
CostPer1MIn: 0.59,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 0.79,
|
||||
ContextWindow: 128_000,
|
||||
SupportsAttachments: false,
|
||||
},
|
||||
|
||||
DeepseekR1DistillLlama70b: {
|
||||
ID: DeepseekR1DistillLlama70b,
|
||||
Name: "DeepseekR1DistillLlama70b",
|
||||
Provider: ProviderGROQ,
|
||||
APIModel: "deepseek-r1-distill-llama-70b",
|
||||
CostPer1MIn: 0.75,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 0.99,
|
||||
ContextWindow: 128_000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: false,
|
||||
},
|
||||
}
|
||||
94
internal/llm/models/models.go
Normal file
94
internal/llm/models/models.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package models
|
||||
|
||||
import "maps"
|
||||
|
||||
type (
|
||||
ModelID string
|
||||
ModelProvider string
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
ID ModelID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider ModelProvider `json:"provider"`
|
||||
APIModel string `json:"api_model"`
|
||||
CostPer1MIn float64 `json:"cost_per_1m_in"`
|
||||
CostPer1MOut float64 `json:"cost_per_1m_out"`
|
||||
CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
|
||||
CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
|
||||
ContextWindow int64 `json:"context_window"`
|
||||
DefaultMaxTokens int64 `json:"default_max_tokens"`
|
||||
CanReason bool `json:"can_reason"`
|
||||
SupportsAttachments bool `json:"supports_attachments"`
|
||||
}
|
||||
|
||||
// Model IDs
|
||||
const ( // GEMINI
|
||||
// Bedrock
|
||||
BedrockClaude37Sonnet ModelID = "bedrock.claude-3.7-sonnet"
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderBedrock ModelProvider = "bedrock"
|
||||
// ForTests
|
||||
ProviderMock ModelProvider = "__mock"
|
||||
)
|
||||
|
||||
// Providers in order of popularity
|
||||
var ProviderPopularity = map[ModelProvider]int{
|
||||
ProviderAnthropic: 1,
|
||||
ProviderOpenAI: 2,
|
||||
ProviderGemini: 3,
|
||||
ProviderGROQ: 4,
|
||||
ProviderOpenRouter: 5,
|
||||
ProviderBedrock: 6,
|
||||
ProviderAzure: 7,
|
||||
}
|
||||
|
||||
var SupportedModels = map[ModelID]Model{
|
||||
//
|
||||
// // GEMINI
|
||||
// GEMINI25: {
|
||||
// ID: GEMINI25,
|
||||
// Name: "Gemini 2.5 Pro",
|
||||
// Provider: ProviderGemini,
|
||||
// APIModel: "gemini-2.5-pro-exp-03-25",
|
||||
// CostPer1MIn: 0,
|
||||
// CostPer1MInCached: 0,
|
||||
// CostPer1MOutCached: 0,
|
||||
// CostPer1MOut: 0,
|
||||
// },
|
||||
//
|
||||
// GRMINI20Flash: {
|
||||
// ID: GRMINI20Flash,
|
||||
// Name: "Gemini 2.0 Flash",
|
||||
// Provider: ProviderGemini,
|
||||
// APIModel: "gemini-2.0-flash",
|
||||
// CostPer1MIn: 0.1,
|
||||
// CostPer1MInCached: 0,
|
||||
// CostPer1MOutCached: 0.025,
|
||||
// CostPer1MOut: 0.4,
|
||||
// },
|
||||
//
|
||||
// // Bedrock
|
||||
BedrockClaude37Sonnet: {
|
||||
ID: BedrockClaude37Sonnet,
|
||||
Name: "Bedrock: Claude 3.7 Sonnet",
|
||||
Provider: ProviderBedrock,
|
||||
APIModel: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
CostPer1MIn: 3.0,
|
||||
CostPer1MInCached: 3.75,
|
||||
CostPer1MOutCached: 0.30,
|
||||
CostPer1MOut: 15.0,
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
maps.Copy(SupportedModels, AnthropicModels)
|
||||
maps.Copy(SupportedModels, OpenAIModels)
|
||||
maps.Copy(SupportedModels, GeminiModels)
|
||||
maps.Copy(SupportedModels, GroqModels)
|
||||
maps.Copy(SupportedModels, AzureModels)
|
||||
maps.Copy(SupportedModels, OpenRouterModels)
|
||||
maps.Copy(SupportedModels, XAIModels)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user