Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7f6c47fd | ||
|
|
50469ed750 | ||
|
|
aaab785493 | ||
|
|
9751937894 | ||
|
|
0fc8dfc77e | ||
|
|
81b7df61ec | ||
|
|
8217b96d4a | ||
|
|
7dd0918d32 | ||
|
|
4b26b43855 | ||
|
|
9d7cfda9fe | ||
|
|
a3cf18c905 | ||
|
|
0b1a8ae699 | ||
|
|
eb70b1e5c8 | ||
|
|
00a3d818b6 | ||
|
|
2384c7e734 | ||
|
|
1bad3d9894 | ||
|
|
4f715e66dc | ||
|
|
ec001ca02f | ||
|
|
a2d3b9f0c8 | ||
|
|
9cfb6ff964 | ||
|
|
6ed661c140 | ||
|
|
9dc00edfc9 | ||
|
|
e063bf888e | ||
|
|
6f18475428 | ||
|
|
3664b09812 | ||
|
|
7050cc0ac3 | ||
|
|
4d3d63294d | ||
|
|
6bc61cbc2d | ||
|
|
01d351bebe | ||
|
|
dbba4a97aa | ||
|
|
0dc586faef | ||
|
|
f19c6b05f2 | ||
|
|
bc34f08333 | ||
|
|
b7ee16aabd | ||
|
|
ed1b0d97bf | ||
|
|
8d3b2fb821 | ||
|
|
fa991920bc | ||
|
|
5e79e3d7a5 | ||
|
|
966015c9ae | ||
|
|
61f057337a | ||
|
|
0b261054a2 | ||
|
|
e2e481cbb5 | ||
|
|
5140e83012 | ||
|
|
100d6212be | ||
|
|
f0e19a6542 | ||
|
|
00c4d4f9f8 | ||
|
|
6e6fe6e013 | ||
|
|
d05b60291e | ||
|
|
5162361372 | ||
|
|
d271b9f75b | ||
|
|
333569bed3 | ||
|
|
09b89fdb23 | ||
|
|
0e8c3359d1 | ||
|
|
37e0a7050f | ||
|
|
774dcb6980 | ||
|
|
28bc49ad17 | ||
|
|
dc1947838c | ||
|
|
3ea2daaa4c | ||
|
|
137e964131 | ||
|
|
8efbe497fd | ||
|
|
119d2d966c | ||
|
|
194415e785 | ||
|
|
1684042fb6 | ||
|
|
59f0004d34 | ||
|
|
da35a64fa1 | ||
|
|
460338ca53 | ||
|
|
53c18a64b4 | ||
|
|
b8144c5654 | ||
|
|
9081e17fcc | ||
|
|
ef3fd5900f | ||
|
|
453d690c11 | ||
|
|
c45be6a645 | ||
|
|
7b9b177088 | ||
|
|
3cee5b0470 | ||
|
|
9246d1c901 | ||
|
|
cc12abc83e | ||
|
|
4f7e4a9436 | ||
|
|
eee396f903 | ||
|
|
0d2f8e175a | ||
|
|
4df40e0d9b | ||
|
|
b72e17a8b7 | ||
|
|
61160dc220 | ||
|
|
98734ff28c | ||
|
|
9991352663 | ||
|
|
91c4da5dbd | ||
|
|
2fd0e7dd6b | ||
|
|
d50b7ad481 | ||
|
|
df95c49401 | ||
|
|
8b73c52f00 | ||
|
|
5603098d17 | ||
|
|
f436a50125 | ||
|
|
e19e977591 | ||
|
|
addbe295b1 | ||
|
|
9a573dedc6 | ||
|
|
9ea0d71e8d | ||
|
|
b1a3599017 | ||
|
|
7b0329f67f | ||
|
|
311b9c74dd | ||
|
|
f7e8dd2ff8 | ||
|
|
40b1dd7ef2 | ||
|
|
261e76e0a3 | ||
|
|
a300bfaccb | ||
|
|
41dba0db08 | ||
|
|
6674c6083a | ||
|
|
f6afa2c6bb | ||
|
|
b2fb0508ea | ||
|
|
93f4252bb1 | ||
|
|
46ab9c16dd | ||
|
|
d869df4fee | ||
|
|
b99d4650ec | ||
|
|
261bb7f110 | ||
|
|
0515fbb260 | ||
|
|
88211d8c5b | ||
|
|
a812f95b9d | ||
|
|
3728a12bee | ||
|
|
af07e51213 | ||
|
|
3113788c92 | ||
|
|
efb5fe6d4e | ||
|
|
54dd6c644d | ||
|
|
39ad8f2667 | ||
|
|
c4a2c84e53 | ||
|
|
44fe012812 | ||
|
|
f5e7f079ea | ||
|
|
15a8936806 | ||
|
|
4e4cff49c0 | ||
|
|
5540503bee | ||
|
|
193718034b | ||
|
|
72108c0296 | ||
|
|
ec1c9f8cd1 | ||
|
|
a85b0a370e | ||
|
|
e7784d2864 | ||
|
|
97c4815444 | ||
|
|
7d1a1663c8 | ||
|
|
24c0ce6e53 | ||
|
|
4cdc86612c | ||
|
|
f1f3f8d12c | ||
|
|
e78d3b54bf | ||
|
|
f8a7cd372d | ||
|
|
f48eac638d | ||
|
|
e1f12f93eb | ||
|
|
7ca8334a8b | ||
|
|
f1a2b2eba4 | ||
|
|
4b132656df | ||
|
|
26bab00dab | ||
|
|
568c04753e | ||
|
|
4a06e164d2 | ||
|
|
c57b52c300 | ||
|
|
0b8f48f17f | ||
|
|
3862184ccb | ||
|
|
8619c50976 | ||
|
|
bb6b56b72a | ||
|
|
1252b65166 | ||
|
|
6840276dad | ||
|
|
bd8c3cd0f1 | ||
|
|
e5e9b3e3c0 | ||
|
|
1e8a681de9 | ||
|
|
a834bedc17 | ||
|
|
6a3392385e | ||
|
|
6a00e063c4 | ||
|
|
73a0ce2b7d | ||
|
|
4d1afd01fa | ||
|
|
801d5f47bd | ||
|
|
b6caae9708 | ||
|
|
183ca64ef9 | ||
|
|
8c32cfe829 | ||
|
|
73dcc88da1 | ||
|
|
14bded65dc | ||
|
|
87d1d3fb62 | ||
|
|
e054454109 | ||
|
|
a6142cf975 | ||
|
|
69332e5fa3 | ||
|
|
20201ba3c4 | ||
|
|
658067186a | ||
|
|
ac777b77cf | ||
|
|
5944ae2023 | ||
|
|
2f10961ba8 | ||
|
|
fae97978a3 | ||
|
|
3423415e49 | ||
|
|
1d0bfc2b2a | ||
|
|
bd46cf0f86 | ||
|
|
d4157d9a96 | ||
|
|
6e4ef585d8 | ||
|
|
e05c3b7a76 | ||
|
|
f99904bc1c | ||
|
|
b796d6763f | ||
|
|
c1250abdf8 | ||
|
|
ebe51534a1 | ||
|
|
b8bbee4718 | ||
|
|
8f852b396f | ||
|
|
ae4d089c06 | ||
|
|
5110fbdaf9 | ||
|
|
e6ddb474fc | ||
|
|
0dc71774ce | ||
|
|
b470466e30 | ||
|
|
d1f9311931 | ||
|
|
1c58023df9 | ||
|
|
4e0aa58b7e | ||
|
|
23ee34b35f | ||
|
|
674c9a5220 | ||
|
|
54c86ed43a | ||
|
|
676d75ee75 | ||
|
|
70dc0a12f2 | ||
|
|
d579c5e8aa | ||
|
|
ee91f31313 | ||
|
|
57b3051024 | ||
|
|
ae5cf3cc23 | ||
|
|
68e1b3c46c | ||
|
|
2d68814abc | ||
|
|
a5da5127fa | ||
|
|
b5a4439704 | ||
|
|
9c5616521d | ||
|
|
3fe163416d | ||
|
|
d054f88130 | ||
|
|
b929b4f4b9 | ||
|
|
4c0c83b02d | ||
|
|
d6d45bdc63 | ||
|
|
13a83721b0 | ||
|
|
f0edffbae9 | ||
|
|
8131bee49a | ||
|
|
b5f44ae13f | ||
|
|
0d23f2a7fd | ||
|
|
ac096d84ad | ||
|
|
fcaf0e6dbf | ||
|
|
19e259d90d | ||
|
|
2c9fd1e776 | ||
|
|
63996c4189 | ||
|
|
c7bb7ce4de | ||
|
|
c8eb1b24c3 | ||
|
|
b9f894f1e9 | ||
|
|
7c0d10a4ce | ||
|
|
06af406146 | ||
|
|
0e3458b112 | ||
|
|
2d15c683e0 | ||
|
|
3c94d26570 | ||
|
|
1a553e525f | ||
|
|
3c4e966216 | ||
|
|
0721620ed8 | ||
|
|
9fc6734f32 | ||
|
|
e1733a423d | ||
|
|
d42e3db7e0 | ||
|
|
cdb26f6d83 | ||
|
|
fe05edaa79 | ||
|
|
7d174767b0 | ||
|
|
c5eefd1752 | ||
|
|
77a6b3bdd6 | ||
|
|
7effff56c0 | ||
|
|
e30fba0d3c | ||
|
|
7fbb2ca9a6 | ||
|
|
230d0a1510 | ||
|
|
46ff2c0ae0 | ||
|
|
b8a89dab0f | ||
|
|
7351e12886 | ||
|
|
38879dee2d | ||
|
|
c4ff8dd205 | ||
|
|
0e035b3115 | ||
|
|
b855511d9a | ||
|
|
783faf554d | ||
|
|
bfd4269d7d | ||
|
|
25f78b053b | ||
|
|
87f260ee17 | ||
|
|
12931a869d | ||
|
|
f759e1804d | ||
|
|
c9b4564d36 | ||
|
|
d097c546db | ||
|
|
adb54521b4 | ||
|
|
2ea0399aa7 | ||
|
|
fa1266263d | ||
|
|
fe109c921e | ||
|
|
37bb8895fe | ||
|
|
89b95be4de | ||
|
|
eaf295bac7 | ||
|
|
27d3cec477 | ||
|
|
574d494c3c | ||
|
|
0239761f31 | ||
|
|
a53f9165e9 | ||
|
|
ffc231bd8b | ||
|
|
3cf4ef56fb | ||
|
|
c738e26438 | ||
|
|
9c6aa82ac1 | ||
|
|
ef74d97491 | ||
|
|
af892e5432 | ||
|
|
d7aca6230d | ||
|
|
0f9c2c5c27 | ||
|
|
6a261dedb4 | ||
|
|
ec928d88b5 | ||
|
|
59a5f120c0 | ||
|
|
ce07f80b19 | ||
|
|
168fd9b2e3 | ||
|
|
df13b155f9 | ||
|
|
eeed5b8718 | ||
|
|
148ef90210 | ||
|
|
67023bb007 | ||
|
|
a316aed4fe | ||
|
|
9f7c0bd599 | ||
|
|
c7e1068f90 | ||
|
|
e2052d790b | ||
|
|
d3b2763c14 | ||
|
|
c6492de7ac | ||
|
|
d8fa0fb50c | ||
|
|
18ab8faa1d | ||
|
|
f35ce180e2 | ||
|
|
2bee48a9bc | ||
|
|
10ddd654cf | ||
|
|
61396b93ed | ||
|
|
62b9a30a9c | ||
|
|
5706c6ad3a | ||
|
|
e8e03c895a | ||
|
|
38667682a7 | ||
|
|
d7d5fc39fb | ||
|
|
0caf25adee | ||
|
|
37febc6873 | ||
|
|
4169f0c412 | ||
|
|
b7f06bbc1f | ||
|
|
1b8cfe9e99 | ||
|
|
97837d2d23 | ||
|
|
9abc2a0cf8 | ||
|
|
9fb47bc855 | ||
|
|
73e9fb53d5 | ||
|
|
f03637b1fc | ||
|
|
2c376c5abc | ||
|
|
442e1b52ad | ||
|
|
e8c3abc369 | ||
|
|
c8648baba2 | ||
|
|
7b3a799856 | ||
|
|
9356b6c35a | ||
|
|
29a6603a89 | ||
|
|
a454ba8895 | ||
|
|
5eae7aef0e | ||
|
|
1031bceef7 | ||
|
|
653965ef59 | ||
|
|
ca0ea3f94d | ||
|
|
98bd5109c2 | ||
|
|
78f65e4789 | ||
|
|
75dd2f75aa | ||
|
|
fe86e58bbb | ||
|
|
ae339015fc | ||
|
|
cce2e4ad75 | ||
|
|
a1ce35c208 | ||
|
|
69d6709a19 | ||
|
|
52ec134b2d | ||
|
|
db88bede05 | ||
|
|
d4d218d7d6 | ||
|
|
3e086e3ab9 | ||
|
|
2f5faae34b | ||
|
|
e3ad6a0698 | ||
|
|
b536b45536 | ||
|
|
81c245035f | ||
|
|
dda7059e57 | ||
|
|
0cca75ef48 | ||
|
|
ee1f55dbe2 | ||
|
|
2fa50190e5 | ||
|
|
662b6b1258 | ||
|
|
f0dbe40522 | ||
|
|
41c54f629c | ||
|
|
4503201b15 | ||
|
|
120151ee38 | ||
|
|
4d2e556713 | ||
|
|
54a5d3a9eb |
@@ -1,37 +0,0 @@
|
|||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -3,7 +3,8 @@ name: deploy
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dontlook
|
- dev
|
||||||
|
- production
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -16,10 +17,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v1
|
- uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: 1.2.17
|
||||||
|
|
||||||
- run: bun install
|
- run: bun install
|
||||||
|
|
||||||
- run: bun sst deploy --stage=dev
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
name: publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
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.24.0"
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.2.17
|
||||||
|
|
||||||
|
- 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
|
||||||
|
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
|
||||||
|
git config --global user.email "opencode@sst.dev"
|
||||||
|
git config --global user.name "opencode"
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: |
|
||||||
|
bun install
|
||||||
|
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
|
||||||
|
./script/publish.ts
|
||||||
|
else
|
||||||
|
./script/publish.ts --snapshot
|
||||||
|
fi
|
||||||
|
working-directory: ./packages/opencode
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||||
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
|
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: 1.2.16
|
|
||||||
|
|
||||||
- run: ./script/publish.ts
|
|
||||||
dir: packages/opencode
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
|
||||||
@@ -3,3 +3,5 @@ node_modules
|
|||||||
.opencode
|
.opencode
|
||||||
.sst
|
.sst
|
||||||
.env
|
.env
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 OpenCode
|
Copyright (c) 2025 opencode
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,658 +1,87 @@
|
|||||||
◧ opencode
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai">
|
||||||
|
<picture>
|
||||||
|
<source srcset="packages/web/src/assets/logo-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="packages/web/src/assets/logo-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="packages/web/src/assets/logo-light.svg" alt="opencode logo">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">AI coding agent, built for the terminal.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></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>
|
||||||
|
|
||||||

|
[](https://opencode.ai)
|
||||||
|
|
||||||
> **⚠️ Notice:** We are in progress of a complete overhaul in the `dontlook` branch - should be released mid June. The README below is for the current version
|
---
|
||||||
|
|
||||||
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
|
|
||||||
- **Named Arguments for Custom Commands**: Create powerful custom commands with multiple named placeholders
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Using the Install Script
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install the latest version
|
# YOLO
|
||||||
curl -fsSL https://opencode.ai/install | bash
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
# Install a specific version
|
# Package managers
|
||||||
curl -fsSL https://opencode.ai/install | VERSION=0.1.0 bash
|
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||||
|
brew install sst/tap/opencode # macOS
|
||||||
|
paru -S opencode-bin # Arch Linux
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Homebrew (macOS and Linux)
|
> **Note:** Remove versions older than 0.1.x before installing
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
|
||||||
|
|
||||||
|
To run opencode locally you need.
|
||||||
|
|
||||||
|
- Bun
|
||||||
|
- Golang 1.24.x
|
||||||
|
|
||||||
|
And run.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install sst/tap/opencode
|
$ bun install
|
||||||
|
$ bun run packages/opencode/src/index.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using AUR (Arch Linux)
|
#### Development Notes
|
||||||
|
|
||||||
|
**API Client Generation**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you need to regenerate the Go client and OpenAPI specification:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using yay
|
$ cd packages/tui
|
||||||
yay -S opencode-bin
|
$ go generate ./pkg/client/
|
||||||
|
|
||||||
# Using paru
|
|
||||||
paru -S opencode-bin
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Go
|
This updates the generated Go client code that the TUI uses to communicate with the backend server.
|
||||||
|
|
||||||
```bash
|
### FAQ
|
||||||
go install github.com/sst/opencode@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
#### How is this different than Claude Code?
|
||||||
|
|
||||||
OpenCode looks for configuration in the following locations:
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
- `$HOME/.opencode.json`
|
- 100% open source
|
||||||
- `$XDG_CONFIG_HOME/opencode/.opencode.json`
|
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with 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.
|
||||||
- `./.opencode.json` (local directory)
|
- 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.
|
||||||
|
|
||||||
### Environment Variables
|
#### What about Windows support?
|
||||||
|
|
||||||
You can configure OpenCode using environment variables:
|
There are some minor problems blocking opencode from working on windows. We are working on on them now. You'll need to use WSL for now.
|
||||||
|
|
||||||
| Environment Variable | Purpose |
|
#### What's the other repo?
|
||||||
| -------------------------- | ------------------------------------------------------ |
|
|
||||||
| `ANTHROPIC_API_KEY` | For Claude models |
|
|
||||||
| `OPENAI_API_KEY` | For OpenAI models |
|
|
||||||
| `GEMINI_API_KEY` | For Google Gemini models |
|
|
||||||
| `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) |
|
|
||||||
| `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) |
|
|
||||||
| `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 |
|
|
||||||
|
|
||||||
### Configuration File Structure
|
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).
|
||||||
|
|
||||||
```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": {
|
|
||||||
"primary": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shell": {
|
|
||||||
"path": "/bin/zsh",
|
|
||||||
"args": ["-l"]
|
|
||||||
},
|
|
||||||
"debug": false,
|
|
||||||
"debugLSP": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Supported AI Models
|
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||||
|
|
||||||
OpenCode supports a variety of AI models from different providers:
|
|
||||||
|
|
||||||
### 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-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
|
|
||||||
|
|
||||||
### Google Cloud VertexAI
|
|
||||||
|
|
||||||
- Gemini 2.5
|
|
||||||
- Gemini 2.5 Flash
|
|
||||||
|
|
||||||
## Using Bedrock Models
|
|
||||||
|
|
||||||
To use bedrock models with OpenCode you need three things.
|
|
||||||
|
|
||||||
1. Valid AWS credentials (the env vars: `AWS_SECRET_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`)
|
|
||||||
2. Access to the corresponding model in AWS Bedrock in your region.
|
|
||||||
a. You can request access in the AWS console on the Bedrock -> "Model access" page.
|
|
||||||
3. A correct configuration file. You don't need the `providers` key. Instead you have to prefix your models per agent with `bedrock.` and then a valid model. For now only Claude 3.7 is supported.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"agents": {
|
|
||||||
"primary": {
|
|
||||||
"model": "bedrock.claude-3.7-sonnet",
|
|
||||||
"maxTokens": 5000,
|
|
||||||
"reasoningEffort": ""
|
|
||||||
},
|
|
||||||
"task": {
|
|
||||||
"model": "bedrock.claude-3.7-sonnet",
|
|
||||||
"maxTokens": 5000,
|
|
||||||
"reasoningEffort": ""
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"model": "bedrock.claude-3.7-sonnet",
|
|
||||||
"maxTokens": 80,
|
|
||||||
"reasoningEffort": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interactive Mode Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start OpenCode
|
|
||||||
opencode
|
|
||||||
|
|
||||||
# Start with debug logging
|
|
||||||
opencode -d
|
|
||||||
|
|
||||||
# Start with a specific working directory
|
|
||||||
opencode -c /path/to/project
|
|
||||||
```
|
|
||||||
|
|
||||||
## Non-interactive Prompt Mode
|
|
||||||
|
|
||||||
You can run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument or by piping text into the command. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run a single prompt and print the AI's response to the terminal
|
|
||||||
opencode -p "Explain the use of context in Go"
|
|
||||||
|
|
||||||
# Pipe input to OpenCode (equivalent to using -p flag)
|
|
||||||
echo "Explain the use of context in Go" | opencode
|
|
||||||
|
|
||||||
# Get response in JSON format
|
|
||||||
opencode -p "Explain the use of context in Go" -f json
|
|
||||||
# Or with piped input
|
|
||||||
echo "Explain the use of context in Go" | opencode -f json
|
|
||||||
|
|
||||||
# Run without showing the spinner
|
|
||||||
opencode -p "Explain the use of context in Go" -q
|
|
||||||
# Or with piped input
|
|
||||||
echo "Explain the use of context in Go" | opencode -q
|
|
||||||
|
|
||||||
# Enable verbose logging to stderr
|
|
||||||
opencode -p "Explain the use of context in Go" --verbose
|
|
||||||
# Or with piped input
|
|
||||||
echo "Explain the use of context in Go" | opencode --verbose
|
|
||||||
|
|
||||||
# Restrict the agent to only use specific tools
|
|
||||||
opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
|
|
||||||
# Or with piped input
|
|
||||||
echo "Explain the use of context in Go" | opencode --allowedTools=view,ls,glob
|
|
||||||
|
|
||||||
# Prevent the agent from using specific tools
|
|
||||||
opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
|
|
||||||
# Or with piped input
|
|
||||||
echo "Explain the use of context in Go" | opencode --excludedTools=bash,edit
|
|
||||||
```
|
|
||||||
|
|
||||||
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All permissions are auto-approved for the session.
|
|
||||||
|
|
||||||
### Tool Restrictions
|
|
||||||
|
|
||||||
You can control which tools the AI assistant has access to in non-interactive mode:
|
|
||||||
|
|
||||||
- `--allowedTools`: Comma-separated list of tools that the agent is allowed to use. Only these tools will be available.
|
|
||||||
- `--excludedTools`: Comma-separated list of tools that the agent is not allowed to use. All other tools will be available.
|
|
||||||
|
|
||||||
These flags are mutually exclusive - you can use either `--allowedTools` or `--excludedTools`, but not both at the same time.
|
|
||||||
|
|
||||||
### Output Formats
|
|
||||||
|
|
||||||
OpenCode supports the following output formats in non-interactive mode:
|
|
||||||
|
|
||||||
| Format | Description |
|
|
||||||
| ------ | ------------------------------- |
|
|
||||||
| `text` | Plain text output (default) |
|
|
||||||
| `json` | Output wrapped in a JSON object |
|
|
||||||
|
|
||||||
The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs.
|
|
||||||
|
|
||||||
## Command-line Flags
|
|
||||||
|
|
||||||
| Flag | Short | Description |
|
|
||||||
| ----------------- | ----- | --------------------------------------------------- |
|
|
||||||
| `--help` | `-h` | Display help information |
|
|
||||||
| `--debug` | `-d` | Enable debug mode |
|
|
||||||
| `--cwd` | `-c` | Set current working directory |
|
|
||||||
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
|
|
||||||
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
|
|
||||||
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
|
|
||||||
| `--verbose` | | Display logs to stderr in non-interactive mode |
|
|
||||||
| `--allowedTools` | | Restrict the agent to only use specified tools |
|
|
||||||
| `--excludedTools` | | Prevent the agent from using specified tools |
|
|
||||||
|
|
||||||
## 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) |
|
|
||||||
| `agent` | Run sub-tasks with the AI agent | `prompt` (required) |
|
|
||||||
|
|
||||||
### Shell Configuration
|
|
||||||
|
|
||||||
OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses:
|
|
||||||
|
|
||||||
1. The shell specified in the config file (if provided)
|
|
||||||
2. The shell from the `$SHELL` environment variable (if available)
|
|
||||||
3. Falls back to `/bin/bash` if neither of the above is available
|
|
||||||
|
|
||||||
To configure a custom shell, add a `shell` section to your `.opencode.json` configuration file:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"shell": {
|
|
||||||
"path": "/bin/zsh",
|
|
||||||
"args": ["-l"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can specify any shell executable and custom arguments:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"shell": {
|
|
||||||
"path": "/usr/bin/fish",
|
|
||||||
"args": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
OpenCode supports named arguments in custom commands using placeholders in the format `$NAME` (where NAME consists of uppercase letters, numbers, and underscores, and must start with a letter).
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Fetch Context for Issue $ISSUE_NUMBER
|
|
||||||
|
|
||||||
RUN gh issue view $ISSUE_NUMBER --json title,body,comments
|
|
||||||
RUN git grep --author="$AUTHOR_NAME" -n .
|
|
||||||
RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
|
|
||||||
```
|
|
||||||
|
|
||||||
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
|
|
||||||
|
|
||||||
- Clear identification of what each argument represents
|
|
||||||
- Ability to use the same argument multiple times
|
|
||||||
- Better organization for commands with multiple inputs
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
## 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/sst/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.
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"sst": "3.17.4",
|
"sst": "3.17.6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
"bin": {
|
||||||
|
"opencode": "./bin/opencode",
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "0.11.0",
|
"@clack/prompts": "0.11.0",
|
||||||
"@flystorage/file-storage": "1.1.0",
|
"@flystorage/file-storage": "1.1.0",
|
||||||
@@ -43,13 +46,17 @@
|
|||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
"zod-openapi": "4.2.4",
|
"zod-openapi": "4.2.4",
|
||||||
|
"zod-validation-error": "3.5.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||||
|
"@ai-sdk/anthropic": "1.2.12",
|
||||||
"@tsconfig/bun": "1.0.7",
|
"@tsconfig/bun": "1.0.7",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
"zod-to-json-schema": "3.24.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
@@ -67,16 +74,18 @@
|
|||||||
"astro": "5.7.13",
|
"astro": "5.7.13",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
"js-base64": "3.7.7",
|
"js-base64": "3.7.7",
|
||||||
|
"lang-map": "0.4.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "15.0.12",
|
"marked": "15.0.12",
|
||||||
"rehype-autolink-headings": "7.1.0",
|
"rehype-autolink-headings": "7.1.0",
|
||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"shiki": "3.4.2",
|
"shiki": "3.4.2",
|
||||||
"solid-js": "1.9.7",
|
"solid-js": "1.9.7",
|
||||||
"toolbeam-docs-theme": "0.2.4",
|
"toolbeam-docs-theme": "0.3.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "catalog:",
|
"@types/node": "catalog:",
|
||||||
|
"opencode": "workspace:*",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -85,21 +94,30 @@
|
|||||||
"sharp",
|
"sharp",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
],
|
],
|
||||||
|
"patchedDependencies": {
|
||||||
|
"ai@4.3.16": "patches/ai@4.3.16.patch",
|
||||||
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"zod": "3.24.2",
|
"zod": "3.24.2",
|
||||||
},
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"ai": "5.0.0-alpha.7",
|
"ai": "4.3.16",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"zod": "3.24.2",
|
"zod": "3.24.2",
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-gz1V165eiJnQIexfLyKm11vimrmQ3zdcJhPpjeLFmDU9wrvZwLuklfZ0WgfYSb+EjiP1cKypwt6JSGvWkfKIAQ=="],
|
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
|
||||||
|
|
||||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-alpha.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-lhdrARU3SSmt5p/GNNK7VhazvZpKSCIOjpHUfX7f5jIhVGi/vvlxP1rD6Go57nn1MtuGKNqL04AebSRFDQsQbw=="],
|
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
|
||||||
|
|
||||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-AYkT3jskmo7Lwzijo/yHKD1jC+UZizsROO8ULTg9aJZUwR4ABZzAxh4NxDIEy4TWRfBGufp+/9ICHAn6pkU71w=="],
|
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||||
|
|
||||||
|
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||||
|
|
||||||
|
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
|
||||||
|
|
||||||
|
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
||||||
|
|
||||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
@@ -127,6 +145,12 @@
|
|||||||
|
|
||||||
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@0.6.1", "", {}, "sha512-4bMLrs2KW+8/vHEE5Ffv2HbxCbbgXO+2N6MpoCsMXUlUoi7pgEEx8kbkzMXJ2dZtWF3gvwm9lvgjnFeanC2LGg=="],
|
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@0.6.1", "", {}, "sha512-4bMLrs2KW+8/vHEE5Ffv2HbxCbbgXO+2N6MpoCsMXUlUoi7pgEEx8kbkzMXJ2dZtWF3gvwm9lvgjnFeanC2LGg=="],
|
||||||
|
|
||||||
|
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||||
|
|
||||||
|
"@aws-sdk/types": ["@aws-sdk/types@3.821.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA=="],
|
||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
||||||
@@ -407,6 +431,18 @@
|
|||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.0.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.3.1", "@smithy/util-hex-encoding": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig=="],
|
||||||
|
|
||||||
|
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="],
|
||||||
|
|
||||||
|
"@smithy/types": ["@smithy/types@4.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA=="],
|
||||||
|
|
||||||
|
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="],
|
||||||
|
|
||||||
|
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw=="],
|
||||||
|
|
||||||
|
"@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="],
|
||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||||
@@ -425,10 +461,12 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
|
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||||
|
|
||||||
|
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||||
@@ -473,7 +511,7 @@
|
|||||||
|
|
||||||
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
||||||
|
|
||||||
"ai": ["ai@5.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-alpha.7", "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-ShCk3frIMdVtK9knvWKiFS7N6Vwnf8mLMv670+T//W9oqfoetSVPBhTF6Dy+oDM/bjVSsBf1BuYImLDvHICOIQ=="],
|
"ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="],
|
||||||
|
|
||||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||||
|
|
||||||
@@ -561,7 +599,7 @@
|
|||||||
|
|
||||||
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
|
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
|
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
@@ -685,6 +723,8 @@
|
|||||||
|
|
||||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||||
|
|
||||||
|
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
||||||
|
|
||||||
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
||||||
|
|
||||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||||
@@ -969,10 +1009,16 @@
|
|||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||||
|
|
||||||
|
"lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="],
|
||||||
|
|
||||||
|
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
|
||||||
|
|
||||||
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
|
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
|
||||||
|
|
||||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
@@ -1273,6 +1319,8 @@
|
|||||||
|
|
||||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||||
|
|
||||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
@@ -1353,6 +1401,8 @@
|
|||||||
|
|
||||||
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
||||||
|
|
||||||
|
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||||
|
|
||||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||||
|
|
||||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||||
@@ -1405,23 +1455,23 @@
|
|||||||
|
|
||||||
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
||||||
|
|
||||||
"sst": ["sst@3.17.4", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.4", "sst-darwin-x64": "3.17.4", "sst-linux-arm64": "3.17.4", "sst-linux-x64": "3.17.4", "sst-linux-x86": "3.17.4", "sst-win32-arm64": "3.17.4", "sst-win32-x64": "3.17.4", "sst-win32-x86": "3.17.4" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-WpAws1ASJIilKC9/DGBhZ5wk2I4gtlzHXKpuwPC25bHWjqllv1jZiehIYhhN0PpV2pV8xCvqzyN8Gdm3J4EWQg=="],
|
"sst": ["sst@3.17.6", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.6", "sst-darwin-x64": "3.17.6", "sst-linux-arm64": "3.17.6", "sst-linux-x64": "3.17.6", "sst-linux-x86": "3.17.6", "sst-win32-arm64": "3.17.6", "sst-win32-x64": "3.17.6", "sst-win32-x86": "3.17.6" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-p+AcqwfYQUdkxeRjCikQoTMviPCBiGoU7M0vcV6GDVmVis8hzhVw4EFfHTafZC+aWfy1Ke2UQi66vZlEVWuEqA=="],
|
||||||
|
|
||||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IJansQWlPdiaQNsJw3FQ+Q/ZXN1hzrq2Q31xG4l2HhA1doj1C3y+6s57vu4cTRDFo2OwBlC4+zlQBJHsOYGhrA=="],
|
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6tb7KlcPR7PTi3ofQv8dX/n6Jf7pNP9VfrnYL4HBWnWrcYaZeJ5MWobILfIJ/y2jHgoqmg9e5C3266Eds0JQyw=="],
|
||||||
|
|
||||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-mHd26/AtaQ79ajqzsutRhgEjkCxX+bXgW4KJIN0AGT3110fo2OL0x2UXmfX+sxSWOFHvJQsjFjFm4CLtQSxyBg=="],
|
"sst-darwin-x64": ["sst-darwin-x64@3.17.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-lFakq6/EgTuBSjbl8Kry4pfgAPEIyn6o7ZkyRz3hz5331wUaX88yfjs3tL9JQ8Ey6jBUYxwhP/Q1n7fzIG046g=="],
|
||||||
|
|
||||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-unaNWOY3oEI/jUUG47/2Gbreaoi/D/rLsTPeKyYEWhWEBWCojns7LfMQs1bgW0qjBGmazB2IJD4NVYhYqYQxqQ=="],
|
"sst-linux-arm64": ["sst-linux-arm64@3.17.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-SdTxXMbTEdiwOqp37w31kXv97vHqSx3oK9h/76lKg7V9k5JxPJ6JMefPLhoKWwK0Zh6AndY2zo2oRoEv4SIaDw=="],
|
||||||
|
|
||||||
"sst-linux-x64": ["sst-linux-x64@3.17.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zoErI6dVoRxWcmoVVrzNJWKEqfUF/MyQInEkGROGY2YsFFzOM5RD5Dsdm9q6oDGwx+NxFAhQWc8/8C+OmoW1nA=="],
|
"sst-linux-x64": ["sst-linux-x64@3.17.6", "", { "os": "linux", "cpu": "x64" }, "sha512-qneh7uWDiTUYx8X1Y3h2YVw3SJ0ybBBlRrVybIvCM09JqQ8+qq/XjKXGzA/3/EF0Jr7Ug8cARSn9CwxhdQGN7Q=="],
|
||||||
|
|
||||||
"sst-linux-x86": ["sst-linux-x86@3.17.4", "", { "os": "linux", "cpu": "none" }, "sha512-7ZHS2rxzxVAxMFW3u5+GMRGGACaBMuLht8JYxqruD8mFVqk9UaPQgrFKIHGKWHLBJLVnF2AdwmlHOcEKP+UJWA=="],
|
"sst-linux-x86": ["sst-linux-x86@3.17.6", "", { "os": "linux", "cpu": "none" }, "sha512-pU3D5OeqnmfxGqN31DxuwWnc1OayxhkErnITHhZ39D0MTiwbIgCapH26FuLW8B08/uxJWG8djUlOboCRhSBvWA=="],
|
||||||
|
|
||||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-q4cedr6WD3NqeQkDvmAsIgMgPIjziIWy81wA3ZmnY6UT0jFgFus23ppLIi6F4BFJfOygvAP2PeGrRR3o8giclw=="],
|
"sst-win32-arm64": ["sst-win32-arm64@3.17.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rr3RTYWAsH9sM9CbM/sAZCk7dB1OsSAljjJuuHMvdSAYW3RDpXEza0PBJGxnBID2eOrpswEchzMPL2d8LtL7oA=="],
|
||||||
|
|
||||||
"sst-win32-x64": ["sst-win32-x64@3.17.4", "", { "os": "win32", "cpu": "x64" }, "sha512-sSQL041YCusZ8/0ynYGe9DCmPYVZOFsemXKUA9tX4IGSDqXae1FN0Sj7HQ17JyY24UUirY1zR7LFk+7KrP6wiA=="],
|
"sst-win32-x64": ["sst-win32-x64@3.17.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yZ3roxwI0Wve9PFzdrrF1kfzCmIMFCCoa8qKeXY7LxCJ4QQIqHbCOccLK1Wv/MIU/mcZHWXTQVCLHw77uaa0GQ=="],
|
||||||
|
|
||||||
"sst-win32-x86": ["sst-win32-x86@3.17.4", "", { "os": "win32", "cpu": "none" }, "sha512-WhjsD2dkA2fbQ03CgwIJb+2p0osll2PTXlr7HC3L+H8wG2DgLFPjoE+6N8n6r2dVMVaDzuNwy/7J8hRB29blaw=="],
|
"sst-win32-x86": ["sst-win32-x86@3.17.6", "", { "os": "win32", "cpu": "none" }, "sha512-zV7TJWPJN9PmIXr15iXFSs0tbGsa52oBR3+xiKrUj2qj9XsZe7HBFwskRnHyiFq0durZY9kk9ZtoVlpuUuzr1g=="],
|
||||||
|
|
||||||
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
||||||
|
|
||||||
@@ -1453,6 +1503,8 @@
|
|||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||||
|
|
||||||
"tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="],
|
"tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="],
|
||||||
|
|
||||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||||
@@ -1461,6 +1513,8 @@
|
|||||||
|
|
||||||
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
|
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
|
||||||
|
|
||||||
|
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||||
|
|
||||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||||
@@ -1471,7 +1525,7 @@
|
|||||||
|
|
||||||
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
|
||||||
|
|
||||||
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.2.4", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="],
|
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.3.0", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-qlBkKRp8HVYV7p7jaG9lT2lvQY7c8b9czZ0tnsJUrN2TBTtEyFJymCdkhhpZNC9U4oGZ7lLk0glRJHrndWvVsg=="],
|
||||||
|
|
||||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
@@ -1545,6 +1599,8 @@
|
|||||||
|
|
||||||
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
|
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
|
||||||
|
|
||||||
|
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||||
|
|
||||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
@@ -1629,12 +1685,24 @@
|
|||||||
|
|
||||||
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
||||||
|
|
||||||
|
"zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||||
|
|
||||||
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
|
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
|
||||||
|
|
||||||
|
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
@@ -1655,6 +1723,18 @@
|
|||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
|
"@smithy/eventstream-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/util-hex-encoding/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
@@ -1725,6 +1805,8 @@
|
|||||||
|
|
||||||
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
@@ -1791,6 +1873,8 @@
|
|||||||
|
|
||||||
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||||
|
|
||||||
|
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
"args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||||
|
|||||||
+3
-13
@@ -23,25 +23,15 @@ export const api = new sst.cloudflare.Worker("Api", {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
args.migrations = {
|
args.migrations = {
|
||||||
oldTag: "v1",
|
// Note: when releasing the next tag, make sure all stages use tag v2
|
||||||
newTag: "v1",
|
oldTag: $app.stage === "production" ? "" : "v1",
|
||||||
|
newTag: $app.stage === "production" ? "" : "v1",
|
||||||
//newSqliteClasses: ["SyncServer"],
|
//newSqliteClasses: ["SyncServer"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// new sst.cloudflare.StaticSite("Web", {
|
|
||||||
// path: "packages/web",
|
|
||||||
// domain,
|
|
||||||
// environment: {
|
|
||||||
// VITE_API_URL: api.url,
|
|
||||||
// },
|
|
||||||
// build: {
|
|
||||||
// command: "bun run build",
|
|
||||||
// output: "dist",
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
new sst.cloudflare.x.Astro("Web", {
|
new sst.cloudflare.x.Astro("Web", {
|
||||||
domain,
|
domain,
|
||||||
path: "packages/web",
|
path: "packages/web",
|
||||||
|
|||||||
@@ -12,23 +12,28 @@ requested_version=${VERSION:-}
|
|||||||
|
|
||||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
if [[ "$os" == "darwin" ]]; then
|
if [[ "$os" == "darwin" ]]; then
|
||||||
os="mac"
|
os="darwin"
|
||||||
fi
|
fi
|
||||||
arch=$(uname -m)
|
arch=$(uname -m)
|
||||||
|
|
||||||
if [[ "$arch" == "aarch64" ]]; then
|
if [[ "$arch" == "aarch64" ]]; then
|
||||||
arch="arm64"
|
arch="arm64"
|
||||||
|
elif [[ "$arch" == "x86_64" ]]; then
|
||||||
|
arch="x64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
filename="$APP-$os-$arch.tar.gz"
|
filename="$APP-$os-$arch.zip"
|
||||||
|
|
||||||
|
|
||||||
case "$filename" in
|
case "$filename" in
|
||||||
*"-linux-"*)
|
*"-linux-"*)
|
||||||
[[ "$arch" == "x86_64" || "$arch" == "arm64" || "$arch" == "i386" ]] || exit 1
|
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||||
;;
|
;;
|
||||||
*"-mac-"*)
|
*"-darwin-"*)
|
||||||
[[ "$arch" == "x86_64" || "$arch" == "arm64" ]] || exit 1
|
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
|
||||||
|
;;
|
||||||
|
*"-windows-"*)
|
||||||
|
[[ "$arch" == "x64" ]] || exit 1
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
|
||||||
@@ -88,8 +93,9 @@ check_version() {
|
|||||||
download_and_install() {
|
download_and_install() {
|
||||||
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
|
||||||
mkdir -p opencodetmp && cd opencodetmp
|
mkdir -p opencodetmp && cd opencodetmp
|
||||||
curl -# -L $url | tar xz
|
curl -# -L -o "$filename" "$url"
|
||||||
mv opencode $INSTALL_DIR
|
unzip -q "$filename"
|
||||||
|
mv opencode "$INSTALL_DIR"
|
||||||
cd .. && rm -rf opencodetmp
|
cd .. && rm -rf opencodetmp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +107,9 @@ add_to_path() {
|
|||||||
local config_file=$1
|
local config_file=$1
|
||||||
local command=$2
|
local command=$2
|
||||||
|
|
||||||
if [[ -w $config_file ]]; then
|
if grep -Fxq "$command" "$config_file"; then
|
||||||
|
print_message info "Command already exists in $config_file, skipping write."
|
||||||
|
elif [[ -w $config_file ]]; then
|
||||||
echo -e "\n# opencode" >> "$config_file"
|
echo -e "\n# opencode" >> "$config_file"
|
||||||
echo "$command" >> "$config_file"
|
echo "$command" >> "$config_file"
|
||||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
||||||
@@ -167,6 +175,7 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
|||||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
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 warning "Manually add the directory to $config_file (or similar):"
|
||||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json"
|
||||||
|
}
|
||||||
+7
-4
@@ -6,7 +6,7 @@
|
|||||||
"packageManager": "bun@1.2.14",
|
"packageManager": "bun@1.2.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "bun run --filter='*' typecheck",
|
"typecheck": "bun run --filter='*' typecheck",
|
||||||
"dev": "sst dev"
|
"postinstall": "./scripts/hooks"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
"zod": "3.24.2",
|
"zod": "3.24.2",
|
||||||
"ai": "5.0.0-alpha.7"
|
"ai": "4.3.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"sst": "3.17.4"
|
"sst": "3.17.6"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -38,5 +38,8 @@
|
|||||||
"esbuild",
|
"esbuild",
|
||||||
"protobufjs",
|
"protobufjs",
|
||||||
"sharp"
|
"sharp"
|
||||||
]
|
],
|
||||||
|
"patchedDependencies": {
|
||||||
|
"ai@4.3.16": "patches/ai@4.3.16.patch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export class SyncServer extends DurableObject<Env> {
|
|||||||
this.ctx.acceptWebSocket(server)
|
this.ctx.acceptWebSocket(server)
|
||||||
|
|
||||||
const data = await this.ctx.storage.list()
|
const data = await this.ctx.storage.list()
|
||||||
for (const [key, content] of data.entries()) {
|
Array.from(data.entries())
|
||||||
server.send(JSON.stringify({ key, content }))
|
.filter(([key, _]) => key.startsWith("session/"))
|
||||||
}
|
.map(([key, content]) => server.send(JSON.stringify({ key, content })))
|
||||||
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 101,
|
status: 101,
|
||||||
@@ -71,11 +71,9 @@ export class SyncServer extends DurableObject<Env> {
|
|||||||
|
|
||||||
public async getData() {
|
public async getData() {
|
||||||
const data = await this.ctx.storage.list()
|
const data = await this.ctx.storage.list()
|
||||||
const messages = []
|
return Array.from(data.entries())
|
||||||
for (const [key, content] of data.entries()) {
|
.filter(([key, _]) => key.startsWith("session/"))
|
||||||
messages.push({ key, content })
|
.map(([key, content]) => ({ key, content }))
|
||||||
}
|
|
||||||
return messages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSecret() {
|
private async getSecret() {
|
||||||
@@ -122,7 +120,7 @@ export default {
|
|||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
secret,
|
secret,
|
||||||
url: "https://dev.opencode.ai/s/" + short,
|
url: "https://opencode.ai/s/" + short,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# OpenCode Agent Guidelines
|
# opencode agent guidelines
|
||||||
|
|
||||||
## Build/Test Commands
|
## Build/Test Commands
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
||||||
- **Test**: `bun test` (runs all tests)
|
- **Test**: `bun test` (runs all tests)
|
||||||
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
||||||
|
- **API Client Generation**: `cd packages/tui && go generate ./pkg/client/` (after changes to server endpoints)
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
@@ -16,9 +17,19 @@
|
|||||||
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
|
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
|
||||||
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
||||||
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
||||||
|
|
||||||
|
## IMPORTANT
|
||||||
|
|
||||||
|
- Try to keep things in one function unless composable or reusable
|
||||||
- DO NOT do unnecessary destructuring of variables
|
- DO NOT do unnecessary destructuring of variables
|
||||||
- DO NOT use else statements unless necessary
|
- DO NOT use `else` statements unless necessary
|
||||||
- DO NOT use try catch if it can be avoided
|
- 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()
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -27,4 +38,4 @@
|
|||||||
- **Validation**: All inputs validated with Zod schemas
|
- **Validation**: All inputs validated with Zod schemas
|
||||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||||
- **Storage**: Use `Storage` namespace for persistence
|
- **Storage**: Use `Storage` namespace for persistence
|
||||||
|
- **API Client**: Go TUI communicates with TypeScript server via generated client. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `cd packages/tui && go generate ./pkg/client/` to update the Go client code and OpenAPI spec.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ else
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$resolved" ]; then
|
if [ -z "$resolved" ]; then
|
||||||
printf "It seems that your package manager failed to install the right version of the SST CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
|
printf "It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
if defined OPENCODE_BIN_PATH (
|
||||||
|
set "resolved=%OPENCODE_BIN_PATH%"
|
||||||
|
goto :execute
|
||||||
|
)
|
||||||
|
|
||||||
|
rem Get the directory of this script
|
||||||
|
set "script_dir=%~dp0"
|
||||||
|
set "script_dir=%script_dir:~0,-1%"
|
||||||
|
|
||||||
|
rem Detect platform and architecture
|
||||||
|
set "platform=win32"
|
||||||
|
|
||||||
|
rem Detect architecture
|
||||||
|
if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
|
||||||
|
set "arch=x64"
|
||||||
|
) else if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
||||||
|
set "arch=arm64"
|
||||||
|
) else if "%PROCESSOR_ARCHITECTURE%"=="x86" (
|
||||||
|
set "arch=x86"
|
||||||
|
) else (
|
||||||
|
set "arch=x64"
|
||||||
|
)
|
||||||
|
|
||||||
|
set "name=opencode-!platform!-!arch!"
|
||||||
|
set "binary=opencode.exe"
|
||||||
|
|
||||||
|
rem Search for the binary starting from script location
|
||||||
|
set "resolved="
|
||||||
|
set "current_dir=%script_dir%"
|
||||||
|
|
||||||
|
:search_loop
|
||||||
|
set "candidate=%current_dir%\node_modules\%name%\bin\%binary%"
|
||||||
|
if exist "%candidate%" (
|
||||||
|
set "resolved=%candidate%"
|
||||||
|
goto :execute
|
||||||
|
)
|
||||||
|
|
||||||
|
rem Move up one directory
|
||||||
|
for %%i in ("%current_dir%") do set "parent_dir=%%~dpi"
|
||||||
|
set "parent_dir=%parent_dir:~0,-1%"
|
||||||
|
|
||||||
|
rem Check if we've reached the root
|
||||||
|
if "%current_dir%"=="%parent_dir%" goto :not_found
|
||||||
|
set "current_dir=%parent_dir%"
|
||||||
|
goto :search_loop
|
||||||
|
|
||||||
|
:not_found
|
||||||
|
echo It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the "%name%" package >&2
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:execute
|
||||||
|
rem Execute the binary with all arguments
|
||||||
|
"%resolved%" %*
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "JSON schema reference for configuration validation"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Theme name to use for the interface"
|
||||||
|
},
|
||||||
|
"keybinds": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"leader": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Leader key for keybind combinations"
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Show help dialog"
|
||||||
|
},
|
||||||
|
"editor_open": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Open external editor"
|
||||||
|
},
|
||||||
|
"session_new": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Create a new session"
|
||||||
|
},
|
||||||
|
"session_list": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "List all sessions"
|
||||||
|
},
|
||||||
|
"session_share": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Share current session"
|
||||||
|
},
|
||||||
|
"session_interrupt": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Interrupt current session"
|
||||||
|
},
|
||||||
|
"session_compact": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Toggle compact mode for session"
|
||||||
|
},
|
||||||
|
"tool_details": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Show tool details"
|
||||||
|
},
|
||||||
|
"model_list": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "List available models"
|
||||||
|
},
|
||||||
|
"theme_list": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "List available themes"
|
||||||
|
},
|
||||||
|
"project_init": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Initialize project configuration"
|
||||||
|
},
|
||||||
|
"input_clear": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Clear input field"
|
||||||
|
},
|
||||||
|
"input_paste": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Paste from clipboard"
|
||||||
|
},
|
||||||
|
"input_submit": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Submit input"
|
||||||
|
},
|
||||||
|
"input_newline": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Insert newline in input"
|
||||||
|
},
|
||||||
|
"history_previous": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to previous history item"
|
||||||
|
},
|
||||||
|
"history_next": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to next history item"
|
||||||
|
},
|
||||||
|
"messages_page_up": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages up by one page"
|
||||||
|
},
|
||||||
|
"messages_page_down": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages down by one page"
|
||||||
|
},
|
||||||
|
"messages_half_page_up": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages up by half page"
|
||||||
|
},
|
||||||
|
"messages_half_page_down": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Scroll messages down by half page"
|
||||||
|
},
|
||||||
|
"messages_previous": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to previous message"
|
||||||
|
},
|
||||||
|
"messages_next": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to next message"
|
||||||
|
},
|
||||||
|
"messages_first": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to first message"
|
||||||
|
},
|
||||||
|
"messages_last": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Navigate to last message"
|
||||||
|
},
|
||||||
|
"app_exit": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Exit the application"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Custom keybind configurations"
|
||||||
|
},
|
||||||
|
"autoshare": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Share newly created sessions automatically"
|
||||||
|
},
|
||||||
|
"autoupdate": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Automatically update to the latest version"
|
||||||
|
},
|
||||||
|
"disabled_providers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Disable providers that are loaded automatically"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Model to use in the format of provider/model, eg anthropic/claude-2"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"api": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"cache_read": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"cache_write": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"input",
|
||||||
|
"output"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"context",
|
||||||
|
"output"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"models"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"description": "Custom provider configurations and model overrides"
|
||||||
|
},
|
||||||
|
"mcp": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "local",
|
||||||
|
"description": "Type of MCP server connection"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Command and arguments to run the MCP server"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Environment variables to set when running the MCP server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"command"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "remote",
|
||||||
|
"description": "Type of MCP server connection"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "URL of the remote MCP server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "MCP (Model Context Protocol) server configurations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
@@ -5,20 +5,24 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"dev": "bun run ./src/index.ts"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"opencode": "./bin/opencode"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": [
|
"./*": "./src/*.ts"
|
||||||
"./src/*.ts",
|
|
||||||
"./src/*/index.ts"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||||
|
"@ai-sdk/anthropic": "1.2.12",
|
||||||
"@tsconfig/bun": "1.0.7",
|
"@tsconfig/bun": "1.0.7",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/turndown": "5.0.5",
|
"@types/turndown": "5.0.5",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:",
|
||||||
|
"zod-to-json-schema": "3.24.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "0.11.0",
|
"@clack/prompts": "0.11.0",
|
||||||
@@ -42,6 +46,7 @@
|
|||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
"zod-openapi": "4.2.4"
|
"zod-openapi": "4.2.4",
|
||||||
|
"zod-validation-error": "3.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Maintainer: dax
|
|
||||||
# Maintainer: adam
|
|
||||||
|
|
||||||
pkgname='opencode-bin'
|
|
||||||
pkgver={{VERSION}}
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc='The AI coding agent built for the terminal.'
|
|
||||||
url='https://github.com/sst/opencode'
|
|
||||||
arch=('aarch64' 'x86_64')
|
|
||||||
license=('MIT')
|
|
||||||
provides=('opencode')
|
|
||||||
conflicts=('opencode')
|
|
||||||
depends=('fzf' 'ripgrep')
|
|
||||||
|
|
||||||
source_aarch64=("${pkgname}_${pkgver}_aarch64.zip::{{ARM64_URL}}")
|
|
||||||
sha256sums_aarch64=('{{ARM64_SHA}}')
|
|
||||||
|
|
||||||
source_x86_64=("${pkgname}_${pkgver}_x86_64.zip::{{X64_URL}}")
|
|
||||||
sha256sums_x86_64=('{{X64_SHA}}')
|
|
||||||
|
|
||||||
package() {
|
|
||||||
install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"
|
|
||||||
}
|
|
||||||
@@ -80,9 +80,9 @@ function main() {
|
|||||||
|
|
||||||
// Create symlink to the actual binary
|
// Create symlink to the actual binary
|
||||||
fs.symlinkSync(binaryPath, binScript)
|
fs.symlinkSync(binaryPath, binScript)
|
||||||
console.log(`OpenCode binary symlinked: ${binScript} -> ${binaryPath}`)
|
console.log(`opencode binary symlinked: ${binScript} -> ${binaryPath}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create OpenCode binary symlink:", error.message)
|
console.error("Failed to create opencode binary symlink:", error.message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,13 +58,13 @@ for (const [os, arch] of targets) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!dry)
|
if (!dry)
|
||||||
await $`cd dist/${name} && npm publish --access public --tag ${npmTag}`
|
await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
|
||||||
optionalDependencies[name] = version
|
optionalDependencies[name] = version
|
||||||
}
|
}
|
||||||
|
|
||||||
await $`mkdir -p ./dist/${pkg.name}`
|
await $`mkdir -p ./dist/${pkg.name}`
|
||||||
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
||||||
await $`cp ./script/postinstall.js ./dist/${pkg.name}/postinstall.js`
|
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
|
||||||
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
@@ -73,7 +73,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|||||||
[pkg.name]: `./bin/${pkg.name}`,
|
[pkg.name]: `./bin/${pkg.name}`,
|
||||||
},
|
},
|
||||||
scripts: {
|
scripts: {
|
||||||
postinstall: "node ./postinstall.js",
|
postinstall: "node ./postinstall.mjs",
|
||||||
},
|
},
|
||||||
version,
|
version,
|
||||||
optionalDependencies,
|
optionalDependencies,
|
||||||
@@ -83,7 +83,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!dry)
|
if (!dry)
|
||||||
await $`cd ./dist/${pkg.name} && npm publish --access public --tag ${npmTag}`
|
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
|
||||||
|
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
// Github Release
|
// Github Release
|
||||||
@@ -108,9 +108,10 @@ if (!snapshot) {
|
|||||||
.filter((x: string) => {
|
.filter((x: string) => {
|
||||||
const lower = x.toLowerCase()
|
const lower = x.toLowerCase()
|
||||||
return (
|
return (
|
||||||
!lower.includes("chore:") &&
|
!lower.includes("ignore:") &&
|
||||||
!lower.includes("ci:") &&
|
!lower.includes("ci:") &&
|
||||||
!lower.includes("docs:")
|
!lower.includes("docs:") &&
|
||||||
|
!lower.includes("doc:")
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.join("\n")
|
.join("\n")
|
||||||
@@ -118,44 +119,117 @@ if (!snapshot) {
|
|||||||
if (!dry)
|
if (!dry)
|
||||||
await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
|
await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
|
||||||
|
|
||||||
// AUR package
|
// Calculate SHA values
|
||||||
const pkgbuildTemplate = await Bun.file("./script/PKGBUILD.template").text()
|
const arm64Sha =
|
||||||
const pkgbuild = pkgbuildTemplate
|
|
||||||
.replace("{{VERSION}}", version.split("-")[0])
|
|
||||||
.replace(
|
|
||||||
"{{ARM64_URL}}",
|
|
||||||
`https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip`,
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"{{ARM64_SHA}}",
|
|
||||||
await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
|
await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
|
||||||
.text()
|
.text()
|
||||||
.then((x) => x.trim()),
|
.then((x) => x.trim())
|
||||||
)
|
const x64Sha =
|
||||||
.replace(
|
|
||||||
"{{X64_URL}}",
|
|
||||||
`https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip`,
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"{{X64_SHA}}",
|
|
||||||
await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
|
await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
|
||||||
.text()
|
.text()
|
||||||
.then((x) => x.trim()),
|
.then((x) => x.trim())
|
||||||
)
|
const macX64Sha =
|
||||||
|
await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
|
||||||
|
.text()
|
||||||
|
.then((x) => x.trim())
|
||||||
|
const macArm64Sha =
|
||||||
|
await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
|
||||||
|
.text()
|
||||||
|
.then((x) => x.trim())
|
||||||
|
|
||||||
|
// AUR package
|
||||||
|
const pkgbuild = [
|
||||||
|
"# Maintainer: dax",
|
||||||
|
"# Maintainer: adam",
|
||||||
|
"",
|
||||||
|
"pkgname='opencode-bin'",
|
||||||
|
`pkgver=${version.split("-")[0]}`,
|
||||||
|
"options=('!debug' '!strip')",
|
||||||
|
"pkgrel=1",
|
||||||
|
"pkgdesc='The AI coding agent built for the terminal.'",
|
||||||
|
"url='https://github.com/sst/opencode'",
|
||||||
|
"arch=('aarch64' 'x86_64')",
|
||||||
|
"license=('MIT')",
|
||||||
|
"provides=('opencode')",
|
||||||
|
"conflicts=('opencode')",
|
||||||
|
"depends=('fzf' 'ripgrep')",
|
||||||
|
"",
|
||||||
|
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip")`,
|
||||||
|
`sha256sums_aarch64=('${arm64Sha}')`,
|
||||||
|
"",
|
||||||
|
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip")`,
|
||||||
|
`sha256sums_x86_64=('${x64Sha}')`,
|
||||||
|
"",
|
||||||
|
"package() {",
|
||||||
|
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
|
||||||
|
"}",
|
||||||
|
"",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
await $`rm -rf ./dist/aur-opencode-bin`
|
await $`rm -rf ./dist/aur-opencode-bin`
|
||||||
const gitEnv: Record<string, string> = process.env["AUR_KEY"]
|
|
||||||
? { GIT_SSH_COMMAND: `ssh -i ${process.env["AUR_KEY"]}` }
|
|
||||||
: {}
|
|
||||||
|
|
||||||
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`.env(
|
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`
|
||||||
gitEnv,
|
|
||||||
)
|
|
||||||
await Bun.file("./dist/aur-opencode-bin/PKGBUILD").write(pkgbuild)
|
await Bun.file("./dist/aur-opencode-bin/PKGBUILD").write(pkgbuild)
|
||||||
await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
|
await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
|
||||||
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`.env(gitEnv)
|
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`
|
||||||
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${version}"`.env(
|
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${version}"`
|
||||||
gitEnv,
|
if (!dry) await $`cd ./dist/aur-opencode-bin && git push`
|
||||||
)
|
|
||||||
if (!dry) await $`cd ./dist/aur-opencode-bin && git push`.env(gitEnv)
|
// Homebrew formula
|
||||||
|
const homebrewFormula = [
|
||||||
|
"# typed: false",
|
||||||
|
"# frozen_string_literal: true",
|
||||||
|
"",
|
||||||
|
"# This file was generated by GoReleaser. DO NOT EDIT.",
|
||||||
|
"class Opencode < Formula",
|
||||||
|
` desc "The AI coding agent built for the terminal."`,
|
||||||
|
` homepage "https://github.com/sst/opencode"`,
|
||||||
|
` version "${version.split("-")[0]}"`,
|
||||||
|
"",
|
||||||
|
" on_macos do",
|
||||||
|
" if Hardware::CPU.intel?",
|
||||||
|
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-x64.zip"`,
|
||||||
|
` sha256 "${macX64Sha}"`,
|
||||||
|
"",
|
||||||
|
" def install",
|
||||||
|
' bin.install "opencode"',
|
||||||
|
" end",
|
||||||
|
" end",
|
||||||
|
" if Hardware::CPU.arm?",
|
||||||
|
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-arm64.zip"`,
|
||||||
|
` sha256 "${macArm64Sha}"`,
|
||||||
|
"",
|
||||||
|
" def install",
|
||||||
|
' bin.install "opencode"',
|
||||||
|
" end",
|
||||||
|
" end",
|
||||||
|
" end",
|
||||||
|
"",
|
||||||
|
" on_linux do",
|
||||||
|
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
|
||||||
|
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip"`,
|
||||||
|
` sha256 "${x64Sha}"`,
|
||||||
|
" def install",
|
||||||
|
' bin.install "opencode"',
|
||||||
|
" end",
|
||||||
|
" end",
|
||||||
|
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
|
||||||
|
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip"`,
|
||||||
|
` sha256 "${arm64Sha}"`,
|
||||||
|
" def install",
|
||||||
|
' bin.install "opencode"',
|
||||||
|
" end",
|
||||||
|
" end",
|
||||||
|
" end",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
await $`rm -rf ./dist/homebrew-tap`
|
||||||
|
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
|
||||||
|
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
|
||||||
|
await $`cd ./dist/homebrew-tap && git add opencode.rb`
|
||||||
|
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${version}"`
|
||||||
|
if (!dry) await $`cd ./dist/homebrew-tap && git push`
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import "zod-openapi/extend"
|
||||||
|
import { Config } from "../src/config/config"
|
||||||
|
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||||
|
|
||||||
|
const result = zodToJsonSchema(Config.Info)
|
||||||
|
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "zod-openapi/extend"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Context } from "../util/context"
|
import { Context } from "../util/context"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
@@ -18,6 +19,7 @@ export namespace App {
|
|||||||
data: z.string(),
|
data: z.string(),
|
||||||
root: z.string(),
|
root: z.string(),
|
||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
|
state: z.string(),
|
||||||
}),
|
}),
|
||||||
time: z.object({
|
time: z.object({
|
||||||
initialized: z.number().optional(),
|
initialized: z.number().optional(),
|
||||||
@@ -32,7 +34,7 @@ export namespace App {
|
|||||||
|
|
||||||
const APP_JSON = "app.json"
|
const APP_JSON = "app.json"
|
||||||
|
|
||||||
async function create(input: { cwd: string; version: string }) {
|
async function create(input: { cwd: string }) {
|
||||||
log.info("creating", {
|
log.info("creating", {
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
})
|
})
|
||||||
@@ -44,14 +46,12 @@ export namespace App {
|
|||||||
const data = path.join(
|
const data = path.join(
|
||||||
Global.Path.data,
|
Global.Path.data,
|
||||||
"project",
|
"project",
|
||||||
git ? git.split(path.sep).join("-") : "global",
|
git ? directory(git) : "global",
|
||||||
)
|
)
|
||||||
const stateFile = Bun.file(path.join(data, APP_JSON))
|
const stateFile = Bun.file(path.join(data, APP_JSON))
|
||||||
const state = (await stateFile.json().catch(() => ({}))) as {
|
const state = (await stateFile.json().catch(() => ({}))) as {
|
||||||
initialized: number
|
initialized: number
|
||||||
version: string
|
|
||||||
}
|
}
|
||||||
state.version = input.version
|
|
||||||
await stateFile.write(JSON.stringify(state))
|
await stateFile.write(JSON.stringify(state))
|
||||||
|
|
||||||
const services = new Map<
|
const services = new Map<
|
||||||
@@ -70,13 +70,13 @@ export namespace App {
|
|||||||
git: git !== undefined,
|
git: git !== undefined,
|
||||||
path: {
|
path: {
|
||||||
config: Global.Path.config,
|
config: Global.Path.config,
|
||||||
|
state: Global.Path.state,
|
||||||
data,
|
data,
|
||||||
root: git ?? input.cwd,
|
root: git ?? input.cwd,
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
version: input.version,
|
|
||||||
services,
|
services,
|
||||||
info,
|
info,
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ export namespace App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function provide<T>(
|
export async function provide<T>(
|
||||||
input: { cwd: string; version: string },
|
input: { cwd: string },
|
||||||
cb: (app: Info) => Promise<T>,
|
cb: (app: Info) => Promise<T>,
|
||||||
) {
|
) {
|
||||||
const app = await create(input)
|
const app = await create(input)
|
||||||
@@ -124,14 +124,22 @@ export namespace App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initialize() {
|
export async function initialize() {
|
||||||
const { info, version } = ctx.use()
|
const { info } = ctx.use()
|
||||||
info.time.initialized = Date.now()
|
info.time.initialized = Date.now()
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
path.join(info.path.data, APP_JSON),
|
path.join(info.path.data, APP_JSON),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
version,
|
|
||||||
initialized: Date.now(),
|
initialized: Date.now(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function directory(input: string): string {
|
||||||
|
return input
|
||||||
|
.split(path.sep)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("-")
|
||||||
|
.replace(/[^A-Za-z0-9_]/g, "-")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { generatePKCE } from "@openauthjs/openauth/pkce"
|
import { generatePKCE } from "@openauthjs/openauth/pkce"
|
||||||
import fs from "fs/promises"
|
|
||||||
import { Auth } from "./index"
|
import { Auth } from "./index"
|
||||||
|
|
||||||
export namespace AuthAnthropic {
|
export namespace AuthAnthropic {
|
||||||
@@ -9,7 +8,7 @@ export namespace AuthAnthropic {
|
|||||||
const pkce = await generatePKCE()
|
const pkce = await generatePKCE()
|
||||||
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
|
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
|
||||||
url.searchParams.set("code", "true")
|
url.searchParams.set("code", "true")
|
||||||
url.searchParams.set("client_id", "9d1c250a-e61b-44d9-88ed-5944d1962f5e")
|
url.searchParams.set("client_id", CLIENT_ID)
|
||||||
url.searchParams.set("response_type", "code")
|
url.searchParams.set("response_type", "code")
|
||||||
url.searchParams.set(
|
url.searchParams.set(
|
||||||
"redirect_uri",
|
"redirect_uri",
|
||||||
@@ -39,7 +38,7 @@ export namespace AuthAnthropic {
|
|||||||
code: splits[0],
|
code: splits[0],
|
||||||
state: splits[1],
|
state: splits[1],
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
client_id: CLIENT_ID,
|
||||||
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
||||||
code_verifier: verifier,
|
code_verifier: verifier,
|
||||||
}),
|
}),
|
||||||
@@ -49,6 +48,7 @@ export namespace AuthAnthropic {
|
|||||||
await Auth.set("anthropic", {
|
await Auth.set("anthropic", {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh: json.refresh_token as string,
|
refresh: json.refresh_token as string,
|
||||||
|
access: json.access_token as string,
|
||||||
expires: Date.now() + json.expires_in * 1000,
|
expires: Date.now() + json.expires_in * 1000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,7 @@ export namespace AuthAnthropic {
|
|||||||
export async function access() {
|
export async function access() {
|
||||||
const info = await Auth.get("anthropic")
|
const info = await Auth.get("anthropic")
|
||||||
if (!info || info.type !== "oauth") return
|
if (!info || info.type !== "oauth") return
|
||||||
|
if (info.access && info.expires > Date.now()) return info.access
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://console.anthropic.com/v1/oauth/token",
|
"https://console.anthropic.com/v1/oauth/token",
|
||||||
{
|
{
|
||||||
@@ -75,6 +76,7 @@ export namespace AuthAnthropic {
|
|||||||
await Auth.set("anthropic", {
|
await Auth.set("anthropic", {
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
refresh: json.refresh_token as string,
|
refresh: json.refresh_token as string,
|
||||||
|
access: json.access_token as string,
|
||||||
expires: Date.now() + json.expires_in * 1000,
|
expires: Date.now() + json.expires_in * 1000,
|
||||||
})
|
})
|
||||||
return json.access_token as string
|
return json.access_token as string
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Global } from "../global"
|
||||||
|
import { lazy } from "../util/lazy"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export const AuthCopilot = lazy(async () => {
|
||||||
|
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
|
||||||
|
const response = fetch(
|
||||||
|
"https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts",
|
||||||
|
)
|
||||||
|
.then((x) => Bun.write(file, x))
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
const worked = await response
|
||||||
|
if (!worked) return
|
||||||
|
}
|
||||||
|
const result = await import(file.name!).catch(() => {})
|
||||||
|
if (!result) return
|
||||||
|
return result.AuthCopilot
|
||||||
|
})
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { Auth } from "./index"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
|
export namespace AuthGithubCopilot {
|
||||||
|
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
||||||
|
const DEVICE_CODE_URL = "https://github.com/login/device/code"
|
||||||
|
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
||||||
|
const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token"
|
||||||
|
|
||||||
|
interface DeviceCodeResponse {
|
||||||
|
device_code: string
|
||||||
|
user_code: string
|
||||||
|
verification_uri: string
|
||||||
|
expires_in: number
|
||||||
|
interval: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccessTokenResponse {
|
||||||
|
access_token?: string
|
||||||
|
error?: string
|
||||||
|
error_description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotTokenResponse {
|
||||||
|
token: string
|
||||||
|
expires_at: number
|
||||||
|
refresh_in: number
|
||||||
|
endpoints: {
|
||||||
|
api: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authorize() {
|
||||||
|
const deviceResponse = await fetch(DEVICE_CODE_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
scope: "read:user",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const deviceData: DeviceCodeResponse = await deviceResponse.json()
|
||||||
|
return {
|
||||||
|
device: deviceData.device_code,
|
||||||
|
user: deviceData.user_code,
|
||||||
|
verification: deviceData.verification_uri,
|
||||||
|
interval: deviceData.interval || 5,
|
||||||
|
expiry: deviceData.expires_in,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function poll(device_code: string) {
|
||||||
|
const response = await fetch(ACCESS_TOKEN_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
device_code,
|
||||||
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) return "failed"
|
||||||
|
|
||||||
|
const data: AccessTokenResponse = await response.json()
|
||||||
|
|
||||||
|
if (data.access_token) {
|
||||||
|
// Store the GitHub OAuth token
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: data.access_token,
|
||||||
|
access: "",
|
||||||
|
expires: 0,
|
||||||
|
})
|
||||||
|
return "complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error === "authorization_pending") return "pending"
|
||||||
|
|
||||||
|
if (data.error) return "failed"
|
||||||
|
|
||||||
|
return "pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function access() {
|
||||||
|
const info = await Auth.get("github-copilot")
|
||||||
|
if (!info || info.type !== "oauth") return
|
||||||
|
if (info.access && info.expires > Date.now()) return info.access
|
||||||
|
|
||||||
|
// Get new Copilot API token
|
||||||
|
const response = await fetch(COPILOT_API_KEY_URL, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${info.refresh}`,
|
||||||
|
"User-Agent": "GitHubCopilotChat/0.26.7",
|
||||||
|
"Editor-Version": "vscode/1.99.3",
|
||||||
|
"Editor-Plugin-Version": "copilot-chat/0.26.7",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) return
|
||||||
|
|
||||||
|
const tokenData: CopilotTokenResponse = await response.json()
|
||||||
|
|
||||||
|
// Store the Copilot API token
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: info.refresh,
|
||||||
|
access: tokenData.token,
|
||||||
|
expires: tokenData.expires_at * 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
return tokenData.token
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeviceCodeError = NamedError.create(
|
||||||
|
"DeviceCodeError",
|
||||||
|
z.object({}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TokenExchangeError = NamedError.create(
|
||||||
|
"TokenExchangeError",
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const AuthenticationError = NamedError.create(
|
||||||
|
"AuthenticationError",
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CopilotTokenError = NamedError.create(
|
||||||
|
"CopilotTokenError",
|
||||||
|
z.object({
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export namespace Auth {
|
|||||||
export const Oauth = z.object({
|
export const Oauth = z.object({
|
||||||
type: z.literal("oauth"),
|
type: z.literal("oauth"),
|
||||||
refresh: z.string(),
|
refresh: z.string(),
|
||||||
|
access: z.string(),
|
||||||
expires: z.number(),
|
expires: z.number(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import path from "path"
|
import { z } from "zod"
|
||||||
|
import { Global } from "../global"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import path from "path"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace BunProc {
|
export namespace BunProc {
|
||||||
const log = Log.create({ service: "bun" })
|
const log = Log.create({ service: "bun" })
|
||||||
|
|
||||||
@@ -9,10 +13,12 @@ export namespace BunProc {
|
|||||||
) {
|
) {
|
||||||
log.info("running", {
|
log.info("running", {
|
||||||
cmd: [which(), ...cmd],
|
cmd: [which(), ...cmd],
|
||||||
options,
|
...options,
|
||||||
})
|
})
|
||||||
const result = Bun.spawn([which(), ...cmd], {
|
const result = Bun.spawn([which(), ...cmd], {
|
||||||
...options,
|
...options,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
...options?.env,
|
...options?.env,
|
||||||
@@ -20,16 +26,52 @@ export namespace BunProc {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const code = await result.exited
|
const code = await result.exited
|
||||||
|
// @ts-ignore
|
||||||
|
const stdout = await result.stdout.text()
|
||||||
|
// @ts-ignore
|
||||||
|
const stderr = await result.stderr.text()
|
||||||
|
log.info("done", {
|
||||||
|
code,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
})
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
console.error(result.stderr?.toString("utf8") ?? "")
|
|
||||||
throw new Error(`Command failed with exit code ${result.exitCode}`)
|
throw new Error(`Command failed with exit code ${result.exitCode}`)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function which() {
|
export function which() {
|
||||||
return process.argv0 !== "bun"
|
return process.execPath
|
||||||
? path.resolve(process.cwd(), process.argv0)
|
}
|
||||||
: "bun"
|
|
||||||
|
export const InstallFailedError = NamedError.create(
|
||||||
|
"BunInstallFailedError",
|
||||||
|
z.object({
|
||||||
|
pkg: z.string(),
|
||||||
|
version: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function install(pkg: string, version = "latest") {
|
||||||
|
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
||||||
|
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
||||||
|
const parsed = await pkgjson.json().catch(() => ({
|
||||||
|
dependencies: {},
|
||||||
|
}))
|
||||||
|
if (parsed.dependencies[pkg] === version) return mod
|
||||||
|
parsed.dependencies[pkg] = version
|
||||||
|
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
||||||
|
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
|
||||||
|
cwd: Global.Path.cache,
|
||||||
|
}).catch((e) => {
|
||||||
|
new InstallFailedError(
|
||||||
|
{ pkg, version },
|
||||||
|
{
|
||||||
|
cause: e,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return mod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { AuthAnthropic } from "../../auth/anthropic"
|
import { AuthAnthropic } from "../../auth/anthropic"
|
||||||
|
import { AuthCopilot } from "../../auth/copilot"
|
||||||
import { Auth } from "../../auth"
|
import { Auth } from "../../auth"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { ModelsDev } from "../../provider/models"
|
import { ModelsDev } from "../../provider/models"
|
||||||
|
import { map, pipe, sortBy, values } from "remeda"
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
import { Global } from "../../global"
|
||||||
|
|
||||||
export const AuthCommand = cmd({
|
export const AuthCommand = cmd({
|
||||||
command: "auth",
|
command: "auth",
|
||||||
@@ -15,7 +20,7 @@ export const AuthCommand = cmd({
|
|||||||
.command(AuthLogoutCommand)
|
.command(AuthLogoutCommand)
|
||||||
.command(AuthListCommand)
|
.command(AuthListCommand)
|
||||||
.demandCommand(),
|
.demandCommand(),
|
||||||
async handler(args) {},
|
async handler() {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AuthListCommand = cmd({
|
export const AuthListCommand = cmd({
|
||||||
@@ -24,45 +29,110 @@ export const AuthListCommand = cmd({
|
|||||||
describe: "list providers",
|
describe: "list providers",
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Credentials")
|
const authPath = path.join(Global.Path.data, "auth.json")
|
||||||
|
const homedir = os.homedir()
|
||||||
|
const displayPath = authPath.startsWith(homedir)
|
||||||
|
? authPath.replace(homedir, "~")
|
||||||
|
: authPath
|
||||||
|
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||||
const results = await Auth.all().then((x) => Object.entries(x))
|
const results = await Auth.all().then((x) => Object.entries(x))
|
||||||
const database = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
for (const [providerID, result] of results) {
|
for (const [providerID, result] of results) {
|
||||||
const name = database[providerID]?.name || providerID
|
const name = database[providerID]?.name || providerID
|
||||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}(${result.type})`)
|
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.outro(`${results.length} credentials`)
|
prompts.outro(`${results.length} credentials`)
|
||||||
|
|
||||||
|
// Environment variables section
|
||||||
|
const activeEnvVars: Array<{ provider: string, envVar: string }> = []
|
||||||
|
|
||||||
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
|
for (const envVar of provider.env) {
|
||||||
|
if (process.env[envVar]) {
|
||||||
|
activeEnvVars.push({
|
||||||
|
provider: provider.name || providerID,
|
||||||
|
envVar
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeEnvVars.length > 0) {
|
||||||
|
UI.empty()
|
||||||
|
prompts.intro("Environment")
|
||||||
|
|
||||||
|
for (const { provider, envVar } of activeEnvVars) {
|
||||||
|
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts.outro(`${activeEnvVars.length} environment variables`)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AuthLoginCommand = cmd({
|
export const AuthLoginCommand = cmd({
|
||||||
command: "login",
|
command: "login",
|
||||||
describe: "login to a provider",
|
describe: "log in to a provider",
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Add credential")
|
prompts.intro("Add credential")
|
||||||
const provider = await prompts.select({
|
const providers = await ModelsDev.get()
|
||||||
|
const priority: Record<string, number> = {
|
||||||
|
anthropic: 0,
|
||||||
|
"github-copilot": 1,
|
||||||
|
openai: 2,
|
||||||
|
google: 3,
|
||||||
|
}
|
||||||
|
let provider = await prompts.select({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
maxItems: 2,
|
maxItems: 8,
|
||||||
options: [
|
options: [
|
||||||
|
...pipe(
|
||||||
|
providers,
|
||||||
|
values(),
|
||||||
|
sortBy(
|
||||||
|
(x) => priority[x.id] ?? 99,
|
||||||
|
(x) => x.name ?? x.id,
|
||||||
|
),
|
||||||
|
map((x) => ({
|
||||||
|
label: x.name,
|
||||||
|
value: x.id,
|
||||||
|
hint: priority[x.id] === 0 ? "recommended" : undefined,
|
||||||
|
})),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
label: "Anthropic",
|
value: "other",
|
||||||
value: "anthropic",
|
label: "Other",
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "OpenAI",
|
|
||||||
value: "openai",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Google",
|
|
||||||
value: "google",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
|
|
||||||
|
if (provider === "other") {
|
||||||
|
provider = await prompts.text({
|
||||||
|
message: "Enter provider id",
|
||||||
|
validate: (x) =>
|
||||||
|
x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
|
||||||
|
})
|
||||||
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
|
provider = provider.replace(/^@ai-sdk\//, "")
|
||||||
|
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
||||||
|
prompts.log.warn(
|
||||||
|
`This only stores a credential for ${provider} - you will need configure it in opencode.json, check the docs for examples.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider === "amazon-bedrock") {
|
||||||
|
prompts.log.info(
|
||||||
|
"Amazon bedrock can be configured with standard AWS environment variables like AWS_PROFILE or AWS_ACCESS_KEY_ID",
|
||||||
|
)
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (provider === "anthropic") {
|
if (provider === "anthropic") {
|
||||||
const method = await prompts.select({
|
const method = await prompts.select({
|
||||||
message: "Login method",
|
message: "Login method",
|
||||||
@@ -83,8 +153,14 @@ export const AuthLoginCommand = cmd({
|
|||||||
// some weird bug where program exits without this
|
// some weird bug where program exits without this
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
const { url, verifier } = await AuthAnthropic.authorize()
|
const { url, verifier } = await AuthAnthropic.authorize()
|
||||||
prompts.note("Opening browser...")
|
prompts.note("Trying to open browser...")
|
||||||
|
try {
|
||||||
await open(url)
|
await open(url)
|
||||||
|
} catch (e) {
|
||||||
|
prompts.log.error(
|
||||||
|
"Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:",
|
||||||
|
)
|
||||||
|
}
|
||||||
prompts.log.info(url)
|
prompts.log.info(url)
|
||||||
|
|
||||||
const code = await prompts.text({
|
const code = await prompts.text({
|
||||||
@@ -105,6 +181,44 @@ export const AuthLoginCommand = cmd({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copilot = await AuthCopilot()
|
||||||
|
if (provider === "github-copilot" && copilot) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||||
|
const deviceInfo = await copilot.authorize()
|
||||||
|
|
||||||
|
prompts.note(
|
||||||
|
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const spinner = prompts.spinner()
|
||||||
|
spinner.start("Waiting for authorization...")
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, deviceInfo.interval * 1000),
|
||||||
|
)
|
||||||
|
const response = await copilot.poll(deviceInfo.device)
|
||||||
|
if (response.status === "pending") continue
|
||||||
|
if (response.status === "success") {
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
refresh: response.refresh,
|
||||||
|
access: response.access,
|
||||||
|
expires: response.expires,
|
||||||
|
})
|
||||||
|
spinner.stop("Login successful")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (response.status === "failed") {
|
||||||
|
spinner.stop("Failed to authorize", 1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const key = await prompts.password({
|
const key = await prompts.password({
|
||||||
message: "Enter your API key",
|
message: "Enter your API key",
|
||||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||||
@@ -121,7 +235,7 @@ export const AuthLoginCommand = cmd({
|
|||||||
|
|
||||||
export const AuthLogoutCommand = cmd({
|
export const AuthLogoutCommand = cmd({
|
||||||
command: "logout",
|
command: "logout",
|
||||||
describe: "logout from a configured provider",
|
describe: "log out from a configured provider",
|
||||||
async handler() {
|
async handler() {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
const credentials = await Auth.all().then((x) => Object.entries(x))
|
const credentials = await Auth.all().then((x) => Object.entries(x))
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { AuthAnthropic } from "../../auth/anthropic"
|
|
||||||
import { UI } from "../ui"
|
|
||||||
|
|
||||||
// Example: https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference&code_challenge=MdFtFgFap23AWDSN0oa3-eaKjQRFE4CaEhXx8M9fHZg&code_challenge_method=S256&state=rKLtaDzm88GSwekyEqdi0wXX-YqIr13tSzYymSzpvfs
|
|
||||||
|
|
||||||
export const LoginAnthropicCommand = {
|
|
||||||
command: "anthropic",
|
|
||||||
describe: "Login to Anthropic",
|
|
||||||
handler: async () => {
|
|
||||||
const { url, verifier } = await AuthAnthropic.authorize()
|
|
||||||
|
|
||||||
UI.println("Login to Anthropic")
|
|
||||||
UI.println("Open the following URL in your browser:")
|
|
||||||
UI.println(url)
|
|
||||||
UI.println("")
|
|
||||||
|
|
||||||
const code = await UI.input("Paste the authorization code here: ")
|
|
||||||
await AuthAnthropic.exchange(code, verifier)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { App } from "../../app/app"
|
||||||
|
import { Provider } from "../../provider/provider"
|
||||||
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
|
export const ModelsCommand = cmd({
|
||||||
|
command: "models",
|
||||||
|
describe: "list all available models",
|
||||||
|
handler: async () => {
|
||||||
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
|
||||||
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
|
for (const modelID of Object.keys(provider.info.models)) {
|
||||||
|
console.log(`${providerID}/${modelID}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -6,69 +6,103 @@ import { Session } from "../../session"
|
|||||||
import { Share } from "../../share/share"
|
import { Share } from "../../share/share"
|
||||||
import { Message } from "../../session/message"
|
import { Message } from "../../session/message"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { VERSION } from "../version"
|
import { cmd } from "./cmd"
|
||||||
|
import { Flag } from "../../flag/flag"
|
||||||
const COLOR = [
|
import { Config } from "../../config/config"
|
||||||
UI.Style.TEXT_SUCCESS_BOLD,
|
|
||||||
UI.Style.TEXT_INFO_BOLD,
|
|
||||||
UI.Style.TEXT_HIGHLIGHT_BOLD,
|
|
||||||
UI.Style.TEXT_WARNING_BOLD,
|
|
||||||
]
|
|
||||||
|
|
||||||
const TOOL: Record<string, [string, string]> = {
|
const TOOL: Record<string, [string, string]> = {
|
||||||
opencode_todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||||
opencode_todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||||
opencode_bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
||||||
opencode_edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||||
opencode_glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||||
opencode_grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||||
opencode_list: ["List", UI.Style.TEXT_INFO_BOLD],
|
list: ["List", UI.Style.TEXT_INFO_BOLD],
|
||||||
opencode_read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
||||||
opencode_write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
||||||
|
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RunCommand = {
|
export const RunCommand = cmd({
|
||||||
command: "run [message..]",
|
command: "run [message..]",
|
||||||
describe: "Run OpenCode with a message",
|
describe: "run opencode with a message",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs: Argv) => {
|
||||||
return yargs
|
return yargs
|
||||||
.positional("message", {
|
.positional("message", {
|
||||||
describe: "Message to send",
|
describe: "message to send",
|
||||||
type: "string",
|
type: "string",
|
||||||
array: true,
|
array: true,
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
|
.option("continue", {
|
||||||
|
alias: ["c"],
|
||||||
|
describe: "continue the last session",
|
||||||
|
type: "boolean",
|
||||||
|
})
|
||||||
.option("session", {
|
.option("session", {
|
||||||
describe: "Session ID to continue",
|
alias: ["s"],
|
||||||
|
describe: "session id to continue",
|
||||||
type: "string",
|
type: "string",
|
||||||
})
|
})
|
||||||
|
.option("share", {
|
||||||
|
type: "boolean",
|
||||||
|
describe: "share the session",
|
||||||
|
})
|
||||||
|
.option("model", {
|
||||||
|
type: "string",
|
||||||
|
alias: ["m"],
|
||||||
|
describe: "model to use in the format of provider/model",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handler: async (args: {
|
handler: async (args) => {
|
||||||
message: string[]
|
|
||||||
session?: string
|
|
||||||
printLogs?: boolean
|
|
||||||
}) => {
|
|
||||||
const message = args.message.join(" ")
|
const message = args.message.join(" ")
|
||||||
await App.provide(
|
await App.provide(
|
||||||
{
|
{
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
version: VERSION,
|
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
await Share.init()
|
await Share.init()
|
||||||
const session = args.session
|
const session = await (async () => {
|
||||||
? await Session.get(args.session)
|
if (args.continue) {
|
||||||
: await Session.create()
|
const first = await Session.list().next()
|
||||||
|
if (first.done) return
|
||||||
|
return first.value
|
||||||
|
}
|
||||||
|
|
||||||
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
|
if (args.session) return Session.get(args.session)
|
||||||
|
|
||||||
|
return Session.create()
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
UI.error("Session not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UI.empty()
|
||||||
|
UI.println(UI.logo())
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|
||||||
|
const cfg = await Config.get()
|
||||||
|
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||||
|
await Session.share(session.id)
|
||||||
UI.println(
|
UI.println(
|
||||||
UI.Style.TEXT_INFO_BOLD +
|
UI.Style.TEXT_INFO_BOLD +
|
||||||
"~ https://dev.opencode.ai/s/" +
|
"~ https://opencode.ai/s/" +
|
||||||
session.id.slice(-8),
|
session.id.slice(-8),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
UI.empty()
|
||||||
|
|
||||||
|
const { providerID, modelID } = args.model
|
||||||
|
? Provider.parseModel(args.model)
|
||||||
|
: await Provider.defaultModel()
|
||||||
|
UI.println(
|
||||||
|
UI.Style.TEXT_NORMAL_BOLD + "@ ",
|
||||||
|
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
|
||||||
|
)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|
||||||
function printEvent(color: string, type: string, title: string) {
|
function printEvent(color: string, type: string, title: string) {
|
||||||
@@ -83,6 +117,7 @@ export const RunCommand = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
|
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
|
||||||
|
if (evt.properties.sessionID !== session.id) return
|
||||||
const part = evt.properties.part
|
const part = evt.properties.part
|
||||||
const message = await Session.getMessage(
|
const message = await Session.getMessage(
|
||||||
evt.properties.sessionID,
|
evt.properties.sessionID,
|
||||||
@@ -112,8 +147,6 @@ export const RunCommand = {
|
|||||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { providerID, modelID } = await Provider.defaultModel()
|
|
||||||
await Session.chat({
|
await Session.chat({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
providerID,
|
providerID,
|
||||||
@@ -129,4 +162,4 @@ export const RunCommand = {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { App } from "../../app/app"
|
import { App } from "../../app/app"
|
||||||
import { VERSION } from "../version"
|
import { LSP } from "../../lsp"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
export const ScrapCommand = cmd({
|
export const ScrapCommand = cmd({
|
||||||
command: "scrap <file>",
|
command: "scrap <file>",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("file", { type: "string", demandOption: true }),
|
yargs.positional("file", { type: "string", demandOption: true }),
|
||||||
async handler() {
|
async handler(args) {
|
||||||
await App.provide({ cwd: process.cwd(), version: VERSION }, async (app) => {
|
await App.provide(
|
||||||
Bun.resolveSync("typescript/lib/tsserver.js", app.path.cwd)
|
{ cwd: process.cwd() },
|
||||||
})
|
async () => {
|
||||||
|
await LSP.touchFile(args.file, true)
|
||||||
|
console.log(await LSP.diagnostics())
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { App } from "../../app/app"
|
||||||
|
import { Provider } from "../../provider/provider"
|
||||||
|
import { Server } from "../../server/server"
|
||||||
|
import { Share } from "../../share/share"
|
||||||
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
|
export const ServeCommand = cmd({
|
||||||
|
command: "serve",
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs
|
||||||
|
.option("port", {
|
||||||
|
alias: ["p"],
|
||||||
|
type: "number",
|
||||||
|
describe: "port to listen on",
|
||||||
|
default: 4096,
|
||||||
|
})
|
||||||
|
.option("hostname", {
|
||||||
|
alias: ["h"],
|
||||||
|
type: "string",
|
||||||
|
describe: "hostname to listen on",
|
||||||
|
default: "127.0.0.1",
|
||||||
|
}),
|
||||||
|
describe: "starts a headless opencode server",
|
||||||
|
handler: async (args) => {
|
||||||
|
const cwd = process.cwd()
|
||||||
|
await App.provide({ cwd }, async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
if (Object.keys(providers).length === 0) {
|
||||||
|
return "needs_provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = args.hostname
|
||||||
|
const port = args.port
|
||||||
|
|
||||||
|
await Share.init()
|
||||||
|
const server = Server.listen({
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`opencode server listening on http://${server.hostname}:${server.port}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
await new Promise(() => {})
|
||||||
|
|
||||||
|
server.stop()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Argv } from "yargs"
|
||||||
|
import { UI } from "../ui"
|
||||||
|
import * as prompts from "@clack/prompts"
|
||||||
|
import { Installation } from "../../installation"
|
||||||
|
|
||||||
|
export const UpgradeCommand = {
|
||||||
|
command: "upgrade [target]",
|
||||||
|
describe: "upgrade opencode to the latest or a specific version",
|
||||||
|
builder: (yargs: Argv) => {
|
||||||
|
return yargs
|
||||||
|
.positional("target", {
|
||||||
|
describe: "version to upgrade to, for ex '0.1.48' or 'v0.1.48'",
|
||||||
|
type: "string",
|
||||||
|
})
|
||||||
|
.option("method", {
|
||||||
|
alias: "m",
|
||||||
|
describe: "installation method to use",
|
||||||
|
type: "string",
|
||||||
|
choices: ["curl", "npm", "pnpm", "bun", "brew"],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (args: { target?: string; method?: string }) => {
|
||||||
|
UI.empty()
|
||||||
|
UI.println(UI.logo(" "))
|
||||||
|
UI.empty()
|
||||||
|
prompts.intro("Upgrade")
|
||||||
|
const detectedMethod = await Installation.method()
|
||||||
|
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||||
|
if (method === "unknown") {
|
||||||
|
prompts.log.error(
|
||||||
|
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
|
||||||
|
)
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prompts.log.info("Using method: " + method)
|
||||||
|
const target = args.target ?? (await Installation.latest())
|
||||||
|
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||||
|
const spinner = prompts.spinner()
|
||||||
|
spinner.start("Upgrading...")
|
||||||
|
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||||
|
if (err) {
|
||||||
|
spinner.stop("Upgrade failed")
|
||||||
|
if (err instanceof Installation.UpgradeFailedError)
|
||||||
|
prompts.log.error(err.data.stderr)
|
||||||
|
else if (err instanceof Error) prompts.log.error(err.message)
|
||||||
|
prompts.outro("Done")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spinner.stop("Upgrade complete")
|
||||||
|
prompts.outro("Done")
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Config } from "../config/config"
|
||||||
|
import { MCP } from "../mcp"
|
||||||
|
import { UI } from "./ui"
|
||||||
|
|
||||||
|
export function FormatError(input: unknown) {
|
||||||
|
if (MCP.Failed.isInstance(input))
|
||||||
|
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
|
||||||
|
if (Config.JsonError.isInstance(input))
|
||||||
|
return `Config file at ${input.data.path} is not valid JSON`
|
||||||
|
if (Config.InvalidError.isInstance(input))
|
||||||
|
return [
|
||||||
|
`Config file at ${input.data.path} is invalid`,
|
||||||
|
...(input.data.issues?.map(
|
||||||
|
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||||
|
) ?? []),
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
if (UI.CancelledError.isInstance(input)) return ""
|
||||||
|
}
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { createCli, type TrpcCliMeta } from "trpc-cli"
|
|
||||||
import { initTRPC } from "@trpc/server"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { Server } from "../server/server"
|
|
||||||
import { AuthAnthropic } from "../auth/anthropic"
|
|
||||||
import { UI } from "./ui"
|
|
||||||
import { App } from "../app/app"
|
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Provider } from "../provider/provider"
|
|
||||||
import { Session } from "../session"
|
|
||||||
import { Share } from "../share/share"
|
|
||||||
import { Message } from "../session/message"
|
|
||||||
import { VERSION } from "./version"
|
|
||||||
import { LSP } from "../lsp"
|
|
||||||
import fs from "fs/promises"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
const t = initTRPC.meta<TrpcCliMeta>().create()
|
|
||||||
|
|
||||||
export const router = t.router({
|
|
||||||
generate: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Generate OpenAPI and event specs",
|
|
||||||
})
|
|
||||||
.input(z.object({}))
|
|
||||||
.mutation(async () => {
|
|
||||||
const specs = await Server.openapi()
|
|
||||||
const dir = "gen"
|
|
||||||
await fs.rmdir(dir, { recursive: true }).catch(() => {})
|
|
||||||
await fs.mkdir(dir, { recursive: true })
|
|
||||||
await Bun.write(
|
|
||||||
path.join(dir, "openapi.json"),
|
|
||||||
JSON.stringify(specs, null, 2),
|
|
||||||
)
|
|
||||||
return "Generated OpenAPI specs in gen/ directory"
|
|
||||||
}),
|
|
||||||
|
|
||||||
run: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Run OpenCode with a message",
|
|
||||||
})
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
message: z.array(z.string()).default([]).describe("Message to send"),
|
|
||||||
session: z.string().optional().describe("Session ID to continue"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(
|
|
||||||
async ({ input }: { input: { message: string[]; session?: string } }) => {
|
|
||||||
const message = input.message.join(" ")
|
|
||||||
await App.provide(
|
|
||||||
{
|
|
||||||
cwd: process.cwd(),
|
|
||||||
version: "0.0.0",
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
await Share.init()
|
|
||||||
const session = input.session
|
|
||||||
? await Session.get(input.session)
|
|
||||||
: await Session.create()
|
|
||||||
|
|
||||||
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
|
|
||||||
UI.empty()
|
|
||||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
|
||||||
UI.empty()
|
|
||||||
UI.println(
|
|
||||||
UI.Style.TEXT_INFO_BOLD +
|
|
||||||
"~ https://dev.opencode.ai/s?id=" +
|
|
||||||
session.id.slice(-8),
|
|
||||||
)
|
|
||||||
UI.empty()
|
|
||||||
|
|
||||||
function printEvent(color: string, type: string, title: string) {
|
|
||||||
UI.println(
|
|
||||||
color + `|`,
|
|
||||||
UI.Style.TEXT_NORMAL +
|
|
||||||
UI.Style.TEXT_DIM +
|
|
||||||
` ${type.padEnd(7, " ")}`,
|
|
||||||
"",
|
|
||||||
UI.Style.TEXT_NORMAL + title,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Bus.subscribe(Message.Event.PartUpdated, async (message) => {
|
|
||||||
const part = message.properties.part
|
|
||||||
if (
|
|
||||||
part.type === "tool-invocation" &&
|
|
||||||
part.toolInvocation.state === "result"
|
|
||||||
) {
|
|
||||||
if (part.toolInvocation.toolName === "opencode_todowrite")
|
|
||||||
return
|
|
||||||
|
|
||||||
const args = part.toolInvocation.args as any
|
|
||||||
const tool = part.toolInvocation.toolName
|
|
||||||
|
|
||||||
if (tool === "opencode_edit")
|
|
||||||
printEvent(UI.Style.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
|
|
||||||
if (tool === "opencode_bash")
|
|
||||||
printEvent(
|
|
||||||
UI.Style.TEXT_WARNING_BOLD,
|
|
||||||
"Execute",
|
|
||||||
args.command,
|
|
||||||
)
|
|
||||||
if (tool === "opencode_read")
|
|
||||||
printEvent(UI.Style.TEXT_INFO_BOLD, "Read", args.filePath)
|
|
||||||
if (tool === "opencode_write")
|
|
||||||
printEvent(
|
|
||||||
UI.Style.TEXT_SUCCESS_BOLD,
|
|
||||||
"Create",
|
|
||||||
args.filePath,
|
|
||||||
)
|
|
||||||
if (tool === "opencode_list")
|
|
||||||
printEvent(UI.Style.TEXT_INFO_BOLD, "List", args.path)
|
|
||||||
if (tool === "opencode_glob")
|
|
||||||
printEvent(
|
|
||||||
UI.Style.TEXT_INFO_BOLD,
|
|
||||||
"Glob",
|
|
||||||
args.pattern + (args.path ? " in " + args.path : ""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.type === "text") {
|
|
||||||
if (part.text.includes("\n")) {
|
|
||||||
UI.empty()
|
|
||||||
UI.println(part.text)
|
|
||||||
UI.empty()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { providerID, modelID } = await Provider.defaultModel()
|
|
||||||
await Session.chat({
|
|
||||||
sessionID: session.id,
|
|
||||||
providerID,
|
|
||||||
modelID,
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
UI.empty()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return "Session completed"
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
scrap: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Test command for scraping files",
|
|
||||||
})
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
file: z.string().describe("File to process"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input }: { input: { file: string } }) => {
|
|
||||||
await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
|
|
||||||
await LSP.touchFile(input.file, true)
|
|
||||||
await LSP.diagnostics()
|
|
||||||
})
|
|
||||||
return `Processed file: ${input.file}`
|
|
||||||
}),
|
|
||||||
|
|
||||||
login: t.router({
|
|
||||||
anthropic: t.procedure
|
|
||||||
.meta({
|
|
||||||
description: "Login to Anthropic",
|
|
||||||
})
|
|
||||||
.input(z.object({}))
|
|
||||||
.mutation(async () => {
|
|
||||||
const { url, verifier } = await AuthAnthropic.authorize()
|
|
||||||
|
|
||||||
UI.println("Login to Anthropic")
|
|
||||||
UI.println("Open the following URL in your browser:")
|
|
||||||
UI.println(url)
|
|
||||||
UI.println("")
|
|
||||||
|
|
||||||
const code = await UI.input("Paste the authorization code here: ")
|
|
||||||
await AuthAnthropic.exchange(code, verifier)
|
|
||||||
return "Successfully logged in to Anthropic"
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export function createOpenCodeCli() {
|
|
||||||
return createCli({ router })
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { EOL } from "os"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace UI {
|
export namespace UI {
|
||||||
const LOGO = [
|
const LOGO = [
|
||||||
`█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀`,
|
[`█▀▀█ █▀▀█ █▀▀ █▀▀▄ `, `█▀▀ █▀▀█ █▀▀▄ █▀▀`],
|
||||||
`█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀`,
|
[`█░░█ █░░█ █▀▀ █░░█ `, `█░░ █░░█ █░░█ █▀▀`],
|
||||||
`▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`,
|
[`▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ `, `▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`],
|
||||||
]
|
]
|
||||||
|
|
||||||
export const CancelledError = NamedError.create("UICancelledError", z.void())
|
export const CancelledError = NamedError.create("UICancelledError", z.void())
|
||||||
@@ -29,7 +30,7 @@ export namespace UI {
|
|||||||
|
|
||||||
export function println(...message: string[]) {
|
export function println(...message: string[]) {
|
||||||
print(...message)
|
print(...message)
|
||||||
Bun.stderr.write("\n")
|
Bun.stderr.write(EOL)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function print(...message: string[]) {
|
export function print(...message: string[]) {
|
||||||
@@ -48,13 +49,11 @@ export namespace UI {
|
|||||||
const result = []
|
const result = []
|
||||||
for (const row of LOGO) {
|
for (const row of LOGO) {
|
||||||
if (pad) result.push(pad)
|
if (pad) result.push(pad)
|
||||||
for (let i = 0; i < row.length; i++) {
|
result.push(Bun.color("gray", "ansi"))
|
||||||
const color =
|
result.push(row[0])
|
||||||
i > 18 ? Bun.color("white", "ansi") : Bun.color("gray", "ansi")
|
result.push("\x1b[0m")
|
||||||
const char = row[i]
|
result.push(row[1])
|
||||||
result.push(color + char)
|
result.push(EOL)
|
||||||
}
|
|
||||||
result.push("\n")
|
|
||||||
}
|
}
|
||||||
return result.join("").trimEnd()
|
return result.join("").trimEnd()
|
||||||
}
|
}
|
||||||
@@ -73,4 +72,8 @@ export namespace UI {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function error(message: string) {
|
||||||
|
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
declare global {
|
|
||||||
const OPENCODE_VERSION: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VERSION =
|
|
||||||
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
|
||||||
@@ -1,66 +1,232 @@
|
|||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import path from "path"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import { ModelsDev } from "../provider/models"
|
||||||
|
import { mergeDeep } from "remeda"
|
||||||
|
import { Global } from "../global"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import { lazy } from "../util/lazy"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace Config {
|
export namespace Config {
|
||||||
const log = Log.create({ service: "config" })
|
const log = Log.create({ service: "config" })
|
||||||
|
|
||||||
export const state = App.state("config", async (app) => {
|
export const state = App.state("config", async (app) => {
|
||||||
let result: Info = {}
|
let result = await global()
|
||||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||||
const [resolved] = await Filesystem.findUp(
|
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
|
||||||
file,
|
for (const resolved of found.toReversed()) {
|
||||||
app.path.cwd,
|
result = mergeDeep(result, await load(resolved))
|
||||||
app.path.root,
|
|
||||||
)
|
|
||||||
if (!resolved) continue
|
|
||||||
try {
|
|
||||||
result = await import(resolved).then((mod) => Info.parse(mod.default))
|
|
||||||
log.info("found", { path: resolved })
|
|
||||||
break
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof z.ZodError) {
|
|
||||||
for (const issue of e.issues) {
|
|
||||||
log.info(issue.message)
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("loaded", result)
|
log.info("loaded", result)
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpLocal = z.object({
|
export const McpLocal = z
|
||||||
type: z.literal("local"),
|
.object({
|
||||||
command: z.string().array(),
|
type: z.literal("local").describe("Type of MCP server connection"),
|
||||||
environment: z.record(z.string(), z.string()).optional(),
|
command: z
|
||||||
|
.string()
|
||||||
|
.array()
|
||||||
|
.describe("Command and arguments to run the MCP server"),
|
||||||
|
environment: z
|
||||||
|
.record(z.string(), z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("Environment variables to set when running the MCP server"),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.McpLocal",
|
||||||
})
|
})
|
||||||
|
|
||||||
export const McpRemote = z.object({
|
export const McpRemote = z
|
||||||
type: z.literal("remote"),
|
.object({
|
||||||
url: z.string(),
|
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||||
|
url: z.string().describe("URL of the remote MCP server"),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.McpRemote",
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||||
export type Mcp = z.infer<typeof Mcp>
|
export type Mcp = z.infer<typeof Mcp>
|
||||||
|
|
||||||
export const Info = z
|
export const Keybinds = z
|
||||||
.object({
|
.object({
|
||||||
provider: z.record(z.string(), z.record(z.string(), z.any())).optional(),
|
leader: z
|
||||||
tool: z
|
.string()
|
||||||
.object({
|
.optional()
|
||||||
provider: z.record(z.string(), z.string().array()).optional(),
|
.describe("Leader key for keybind combinations"),
|
||||||
})
|
help: z.string().optional().describe("Show help dialog"),
|
||||||
.optional(),
|
editor_open: z.string().optional().describe("Open external editor"),
|
||||||
mcp: z.record(z.string(), Mcp).optional(),
|
session_new: z.string().optional().describe("Create a new session"),
|
||||||
|
session_list: z.string().optional().describe("List all sessions"),
|
||||||
|
session_share: z.string().optional().describe("Share current session"),
|
||||||
|
session_interrupt: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Interrupt current session"),
|
||||||
|
session_compact: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Toggle compact mode for session"),
|
||||||
|
tool_details: z.string().optional().describe("Show tool details"),
|
||||||
|
model_list: z.string().optional().describe("List available models"),
|
||||||
|
theme_list: z.string().optional().describe("List available themes"),
|
||||||
|
project_init: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Initialize project configuration"),
|
||||||
|
input_clear: z.string().optional().describe("Clear input field"),
|
||||||
|
input_paste: z.string().optional().describe("Paste from clipboard"),
|
||||||
|
input_submit: z.string().optional().describe("Submit input"),
|
||||||
|
input_newline: z.string().optional().describe("Insert newline in input"),
|
||||||
|
history_previous: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to previous history item"),
|
||||||
|
history_next: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to next history item"),
|
||||||
|
messages_page_up: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages up by one page"),
|
||||||
|
messages_page_down: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages down by one page"),
|
||||||
|
messages_half_page_up: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages up by half page"),
|
||||||
|
messages_half_page_down: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Scroll messages down by half page"),
|
||||||
|
messages_previous: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to previous message"),
|
||||||
|
messages_next: z.string().optional().describe("Navigate to next message"),
|
||||||
|
messages_first: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Navigate to first message"),
|
||||||
|
messages_last: z.string().optional().describe("Navigate to last message"),
|
||||||
|
app_exit: z.string().optional().describe("Exit the application"),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.Keybinds",
|
||||||
|
})
|
||||||
|
export const Info = z
|
||||||
|
.object({
|
||||||
|
$schema: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("JSON schema reference for configuration validation"),
|
||||||
|
theme: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Theme name to use for the interface"),
|
||||||
|
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||||
|
autoshare: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Share newly created sessions automatically"),
|
||||||
|
autoupdate: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Automatically update to the latest version"),
|
||||||
|
disabled_providers: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe("Disable providers that are loaded automatically"),
|
||||||
|
model: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
"Model to use in the format of provider/model, eg anthropic/claude-2",
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
provider: z
|
||||||
|
.record(
|
||||||
|
ModelsDev.Provider.partial().extend({
|
||||||
|
models: z.record(ModelsDev.Model.partial()),
|
||||||
|
options: z.record(z.any()).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.describe("Custom provider configurations and model overrides"),
|
||||||
|
mcp: z
|
||||||
|
.record(z.string(), Mcp)
|
||||||
|
.optional()
|
||||||
|
.describe("MCP (Model Context Protocol) server configurations"),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config.Info",
|
||||||
|
})
|
||||||
|
|
||||||
export type Info = z.output<typeof Info>
|
export type Info = z.output<typeof Info>
|
||||||
|
|
||||||
|
export const global = lazy(async () => {
|
||||||
|
let result = await load(path.join(Global.Path.config, "config.json"))
|
||||||
|
|
||||||
|
await import(path.join(Global.Path.config, "config"), {
|
||||||
|
with: {
|
||||||
|
type: "toml",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (mod) => {
|
||||||
|
const { provider, model, ...rest } = mod.default
|
||||||
|
if (provider && model) result.model = `${provider}/${model}`
|
||||||
|
result["$schema"] = "https://opencode.ai/config.json"
|
||||||
|
result = mergeDeep(result, rest)
|
||||||
|
await Bun.write(
|
||||||
|
path.join(Global.Path.config, "config.json"),
|
||||||
|
JSON.stringify(result, null, 2),
|
||||||
|
)
|
||||||
|
await fs.unlink(path.join(Global.Path.config, "config"))
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
async function load(path: string) {
|
||||||
|
const data = await Bun.file(path)
|
||||||
|
.json()
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.code === "ENOENT") return {}
|
||||||
|
throw new JsonError({ path }, { cause: err })
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsed = Info.safeParse(data)
|
||||||
|
if (parsed.success) return parsed.data
|
||||||
|
throw new InvalidError({ path, issues: parsed.error.issues })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const JsonError = NamedError.create(
|
||||||
|
"ConfigJsonError",
|
||||||
|
z.object({
|
||||||
|
path: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const InvalidError = NamedError.create(
|
||||||
|
"ConfigInvalidError",
|
||||||
|
z.object({
|
||||||
|
path: z.string(),
|
||||||
|
issues: z.custom<z.ZodIssue[]>().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export function get() {
|
export function get() {
|
||||||
return state()
|
return state()
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-12
@@ -1,4 +1,3 @@
|
|||||||
import { App } from "../app/app"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
@@ -6,6 +5,7 @@ import { z } from "zod"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import { $ } from "bun"
|
||||||
|
|
||||||
export namespace Fzf {
|
export namespace Fzf {
|
||||||
const log = Log.create({ service: "fzf" })
|
const log = Log.create({ service: "fzf" })
|
||||||
@@ -116,19 +116,23 @@ export namespace Fzf {
|
|||||||
return filepath
|
return filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(cwd: string, query: string) {
|
export async function search(input: {
|
||||||
const process = Bun.spawn({
|
cwd: string
|
||||||
cwd,
|
query: string
|
||||||
stdin: "inherit",
|
limit?: number
|
||||||
stdout: "pipe",
|
}) {
|
||||||
stderr: "pipe",
|
const results = await $`${await filepath()} --filter=${input.query}`
|
||||||
cmd: [await filepath(), "--filter", query],
|
.quiet()
|
||||||
})
|
.throws(false)
|
||||||
await process.exited
|
.cwd(input.cwd)
|
||||||
const stdout = await Bun.readableStreamToText(process.stdout)
|
.text()
|
||||||
return stdout
|
const split = results
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
|
log.info("results", {
|
||||||
|
count: split.length,
|
||||||
|
})
|
||||||
|
return split
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -1,10 +1,11 @@
|
|||||||
import { App } from "../app/app"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
|
import { $ } from "bun"
|
||||||
|
import { Fzf } from "./fzf"
|
||||||
|
|
||||||
export namespace Ripgrep {
|
export namespace Ripgrep {
|
||||||
const PLATFORM = {
|
const PLATFORM = {
|
||||||
@@ -111,4 +112,21 @@ export namespace Ripgrep {
|
|||||||
const { filepath } = await state()
|
const { filepath } = await state()
|
||||||
return filepath
|
return filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function files(input: {
|
||||||
|
cwd: string
|
||||||
|
query?: string
|
||||||
|
glob?: string
|
||||||
|
limit?: number
|
||||||
|
}) {
|
||||||
|
const commands = [
|
||||||
|
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
|
||||||
|
]
|
||||||
|
if (input.query)
|
||||||
|
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||||
|
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||||
|
const joined = commands.join(" | ")
|
||||||
|
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
||||||
|
return result.split("\n").filter(Boolean)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export namespace File {
|
|
||||||
const glob = new Bun.Glob("**/*")
|
|
||||||
export async function search(path: string, query: string) {
|
|
||||||
for await (const entry of glob.scan({
|
|
||||||
cwd: path,
|
|
||||||
onlyFiles: true,
|
|
||||||
})) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export namespace Flag {
|
||||||
|
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||||
|
|
||||||
|
function truthy(key: string) {
|
||||||
|
const value = process.env[key]?.toLowerCase()
|
||||||
|
return value === "true" || value === "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { xdgData, xdgCache, xdgConfig } from "xdg-basedir"
|
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const app = "opencode"
|
const app = "opencode"
|
||||||
@@ -7,18 +7,23 @@ const app = "opencode"
|
|||||||
const data = path.join(xdgData!, app)
|
const data = path.join(xdgData!, app)
|
||||||
const cache = path.join(xdgCache!, app)
|
const cache = path.join(xdgCache!, app)
|
||||||
const config = path.join(xdgConfig!, app)
|
const config = path.join(xdgConfig!, app)
|
||||||
|
const state = path.join(xdgState!, app)
|
||||||
await Promise.all([
|
|
||||||
fs.mkdir(data, { recursive: true }),
|
|
||||||
fs.mkdir(config, { recursive: true }),
|
|
||||||
fs.mkdir(cache, { recursive: true }),
|
|
||||||
])
|
|
||||||
|
|
||||||
export namespace Global {
|
export namespace Global {
|
||||||
export const Path = {
|
export const Path = {
|
||||||
data,
|
data,
|
||||||
bin: path.join(data, "bin"),
|
bin: path.join(data, "bin"),
|
||||||
|
providers: path.join(config, "providers"),
|
||||||
cache,
|
cache,
|
||||||
config,
|
config,
|
||||||
|
state,
|
||||||
} as const
|
} as const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fs.mkdir(Global.Path.data, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.config, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.cache, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.providers, { recursive: true }),
|
||||||
|
fs.mkdir(Global.Path.state, { recursive: true }),
|
||||||
|
])
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ export namespace Identifier {
|
|||||||
return given
|
return given
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function randomBase62(length: number): string {
|
||||||
|
const chars =
|
||||||
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
let result = ""
|
||||||
|
const bytes = randomBytes(length)
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars[bytes[i] % 62]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
function generateNewID(
|
function generateNewID(
|
||||||
prefix: keyof typeof prefixes,
|
prefix: keyof typeof prefixes,
|
||||||
descending: boolean,
|
descending: boolean,
|
||||||
@@ -62,14 +73,11 @@ export namespace Identifier {
|
|||||||
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
||||||
}
|
}
|
||||||
|
|
||||||
const randLength = (LENGTH - 12) / 2
|
|
||||||
const random = randomBytes(randLength)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
prefixes[prefix] +
|
prefixes[prefix] +
|
||||||
"_" +
|
"_" +
|
||||||
timeBytes.toString("hex") +
|
timeBytes.toString("hex") +
|
||||||
random.toString("hex")
|
randomBase62(LENGTH - 12)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-25
@@ -3,59 +3,98 @@ import { App } from "./app/app"
|
|||||||
import { Server } from "./server/server"
|
import { Server } from "./server/server"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
import { Share } from "./share/share"
|
import { Share } from "./share/share"
|
||||||
|
import url from "node:url"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
|
|
||||||
import yargs from "yargs"
|
import yargs from "yargs"
|
||||||
import { hideBin } from "yargs/helpers"
|
import { hideBin } from "yargs/helpers"
|
||||||
import { RunCommand } from "./cli/cmd/run"
|
import { RunCommand } from "./cli/cmd/run"
|
||||||
import { GenerateCommand } from "./cli/cmd/generate"
|
import { GenerateCommand } from "./cli/cmd/generate"
|
||||||
import { VERSION } from "./cli/version"
|
|
||||||
import { ScrapCommand } from "./cli/cmd/scrap"
|
import { ScrapCommand } from "./cli/cmd/scrap"
|
||||||
import { Log } from "./util/log"
|
import { Log } from "./util/log"
|
||||||
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
||||||
|
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||||
|
import { ModelsCommand } from "./cli/cmd/models"
|
||||||
import { Provider } from "./provider/provider"
|
import { Provider } from "./provider/provider"
|
||||||
import { UI } from "./cli/ui"
|
import { UI } from "./cli/ui"
|
||||||
|
import { Installation } from "./installation"
|
||||||
|
import { Bus } from "./bus"
|
||||||
|
import { Config } from "./config/config"
|
||||||
|
import { NamedError } from "./util/error"
|
||||||
|
import { FormatError } from "./cli/error"
|
||||||
|
import { ServeCommand } from "./cli/cmd/serve"
|
||||||
|
|
||||||
|
const cancel = new AbortController()
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (e) => {
|
||||||
|
Log.Default.error("rejection", {
|
||||||
|
e: e instanceof Error ? e.message : e,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("uncaughtException", (e) => {
|
||||||
|
Log.Default.error("exception", {
|
||||||
|
e: e instanceof Error ? e.message : e,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const cli = yargs(hideBin(process.argv))
|
const cli = yargs(hideBin(process.argv))
|
||||||
.scriptName("opencode")
|
.scriptName("opencode")
|
||||||
.version(VERSION)
|
.help("help", "show help")
|
||||||
|
.version("version", "show version number", Installation.VERSION)
|
||||||
|
.alias("version", "v")
|
||||||
.option("print-logs", {
|
.option("print-logs", {
|
||||||
describe: "Print logs to stderr",
|
describe: "print logs to stderr",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
})
|
})
|
||||||
.middleware(async () => {
|
.middleware(async () => {
|
||||||
await Log.init({ print: process.argv.includes("--print-logs") })
|
await Log.init({ print: process.argv.includes("--print-logs") })
|
||||||
Log.Default.info("opencode", {
|
Log.Default.info("opencode", {
|
||||||
version: VERSION,
|
version: Installation.VERSION,
|
||||||
args: process.argv.slice(2),
|
args: process.argv.slice(2),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.usage("\n" + UI.logo())
|
.usage("\n" + UI.logo())
|
||||||
.command({
|
.command({
|
||||||
command: "$0",
|
command: "$0 [project]",
|
||||||
describe: "Start OpenCode TUI",
|
describe: "start opencode tui",
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs.positional("project", {
|
||||||
|
type: "string",
|
||||||
|
describe: "path to start opencode in",
|
||||||
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await App.provide(
|
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||||
{ cwd: process.cwd(), version: VERSION },
|
try {
|
||||||
async () => {
|
process.chdir(cwd)
|
||||||
|
} catch (e) {
|
||||||
|
UI.error("Failed to change directory to " + cwd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await App.provide({ cwd }, async (app) => {
|
||||||
const providers = await Provider.list()
|
const providers = await Provider.list()
|
||||||
if (Object.keys(providers).length === 0) {
|
if (Object.keys(providers).length === 0) {
|
||||||
return "needs_provider"
|
return "needs_provider"
|
||||||
}
|
}
|
||||||
|
|
||||||
await Share.init()
|
await Share.init()
|
||||||
const server = Server.listen()
|
const server = Server.listen({
|
||||||
|
port: 0,
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
})
|
||||||
|
|
||||||
let cmd = ["go", "run", "./main.go"]
|
let cmd = ["go", "run", "./main.go"]
|
||||||
let cwd = new URL("../../tui/cmd/opencode", import.meta.url)
|
let cwd = url.fileURLToPath(
|
||||||
.pathname
|
new URL("../../tui/cmd/opencode", import.meta.url),
|
||||||
|
)
|
||||||
if (Bun.embeddedFiles.length > 0) {
|
if (Bun.embeddedFiles.length > 0) {
|
||||||
const blob = Bun.embeddedFiles[0] as File
|
const blob = Bun.embeddedFiles[0] as File
|
||||||
const binary = path.join(Global.Path.cache, "tui", blob.name)
|
let binaryName = blob.name
|
||||||
|
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
|
||||||
|
binaryName += ".exe"
|
||||||
|
}
|
||||||
|
const binary = path.join(Global.Path.cache, "tui", binaryName)
|
||||||
const file = Bun.file(binary)
|
const file = Bun.file(binary)
|
||||||
if (!(await file.exists())) {
|
if (!(await file.exists())) {
|
||||||
await Bun.write(file, blob, { mode: 0o755 })
|
await Bun.write(file, blob, { mode: 0o755 })
|
||||||
@@ -65,7 +104,8 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
cmd = [binary]
|
cmd = [binary]
|
||||||
}
|
}
|
||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
cmd,
|
cmd: [...cmd, ...process.argv.slice(2)],
|
||||||
|
signal: cancel.signal,
|
||||||
cwd,
|
cwd,
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit",
|
stderr: "inherit",
|
||||||
@@ -73,17 +113,35 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
OPENCODE_SERVER: server.url.toString(),
|
OPENCODE_SERVER: server.url.toString(),
|
||||||
|
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||||
},
|
},
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
server.stop()
|
server.stop()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
if (Installation.VERSION === "dev") return
|
||||||
|
if (Installation.isSnapshot()) return
|
||||||
|
const config = await Config.global()
|
||||||
|
if (config.autoupdate === false) return
|
||||||
|
const latest = await Installation.latest().catch(() => {})
|
||||||
|
if (!latest) return
|
||||||
|
if (Installation.VERSION === latest) return
|
||||||
|
const method = await Installation.method()
|
||||||
|
if (method === "unknown") return
|
||||||
|
await Installation.upgrade(method, latest)
|
||||||
|
.then(() => {
|
||||||
|
Bus.publish(Installation.Event.Updated, { version: latest })
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
})()
|
||||||
|
|
||||||
await proc.exited
|
await proc.exited
|
||||||
await server.stop()
|
server.stop()
|
||||||
|
|
||||||
return "done"
|
return "done"
|
||||||
},
|
})
|
||||||
)
|
|
||||||
if (result === "done") break
|
if (result === "done") break
|
||||||
if (result === "needs_provider") {
|
if (result === "needs_provider") {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
@@ -98,21 +156,43 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
.command(GenerateCommand)
|
.command(GenerateCommand)
|
||||||
.command(ScrapCommand)
|
.command(ScrapCommand)
|
||||||
.command(AuthCommand)
|
.command(AuthCommand)
|
||||||
.fail((msg, err) => {
|
.command(UpgradeCommand)
|
||||||
|
.command(ServeCommand)
|
||||||
|
.command(ModelsCommand)
|
||||||
|
.fail((msg) => {
|
||||||
if (
|
if (
|
||||||
msg.startsWith("Unknown argument") ||
|
msg.startsWith("Unknown argument") ||
|
||||||
msg.startsWith("Not enough non-option arguments")
|
msg.startsWith("Not enough non-option arguments")
|
||||||
) {
|
) {
|
||||||
cli.showHelp("log")
|
cli.showHelp("log")
|
||||||
}
|
}
|
||||||
Log.Default.error(msg, {
|
|
||||||
err,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cli.parse()
|
await cli.parse()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.Default.error(e)
|
const data: Record<string, any> = {}
|
||||||
|
if (e instanceof NamedError) {
|
||||||
|
const obj = e.toObject()
|
||||||
|
Object.assign(data, {
|
||||||
|
...obj.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (e instanceof Error) {
|
||||||
|
Object.assign(data, {
|
||||||
|
name: e.name,
|
||||||
|
message: e.message,
|
||||||
|
cause: e.cause?.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Log.Default.error("fatal", data)
|
||||||
|
const formatted = FormatError(e)
|
||||||
|
if (formatted) UI.error(formatted)
|
||||||
|
if (formatted === undefined)
|
||||||
|
UI.error(
|
||||||
|
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancel.abort()
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { $ } from "bun"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const OPENCODE_VERSION: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Installation {
|
||||||
|
const log = Log.create({ service: "installation" })
|
||||||
|
|
||||||
|
export type Method = Awaited<ReturnType<typeof method>>
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
Updated: Bus.event(
|
||||||
|
"installation.updated",
|
||||||
|
z.object({
|
||||||
|
version: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Info = z
|
||||||
|
.object({
|
||||||
|
version: z.string(),
|
||||||
|
latest: z.string(),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "InstallationInfo",
|
||||||
|
})
|
||||||
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
|
export async function info() {
|
||||||
|
return {
|
||||||
|
version: VERSION,
|
||||||
|
latest: await latest(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSnapshot() {
|
||||||
|
return VERSION.startsWith("0.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDev() {
|
||||||
|
return VERSION === "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function method() {
|
||||||
|
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
|
||||||
|
const exec = process.execPath.toLowerCase()
|
||||||
|
|
||||||
|
const checks = [
|
||||||
|
{
|
||||||
|
name: "npm" as const,
|
||||||
|
command: () => $`npm list -g --depth=0`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "yarn" as const,
|
||||||
|
command: () => $`yarn global list`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pnpm" as const,
|
||||||
|
command: () => $`pnpm list -g --depth=0`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bun" as const,
|
||||||
|
command: () => $`bun pm ls -g`.throws(false).text(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "brew" as const,
|
||||||
|
command: () => $`brew list --formula opencode-ai`.throws(false).text(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
checks.sort((a, b) => {
|
||||||
|
const aMatches = exec.includes(a.name)
|
||||||
|
const bMatches = exec.includes(b.name)
|
||||||
|
if (aMatches && !bMatches) return -1
|
||||||
|
if (!aMatches && bMatches) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const check of checks) {
|
||||||
|
const output = await check.command()
|
||||||
|
if (output.includes("opencode-ai")) {
|
||||||
|
return check.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpgradeFailedError = NamedError.create(
|
||||||
|
"UpgradeFailedError",
|
||||||
|
z.object({
|
||||||
|
stderr: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function upgrade(method: Method, target: string) {
|
||||||
|
const cmd = (() => {
|
||||||
|
switch (method) {
|
||||||
|
case "curl":
|
||||||
|
return $`curl -fsSL https://opencode.ai/install | bash`.env({
|
||||||
|
...process.env,
|
||||||
|
VERSION: target,
|
||||||
|
})
|
||||||
|
case "npm":
|
||||||
|
return $`npm install -g opencode-ai@${target}`
|
||||||
|
case "pnpm":
|
||||||
|
return $`pnpm install -g opencode-ai@${target}`
|
||||||
|
case "bun":
|
||||||
|
return $`bun install -g opencode-ai@${target}`
|
||||||
|
case "brew":
|
||||||
|
return $`brew install sst/tap/opencode`.env({
|
||||||
|
HOMEBREW_NO_AUTO_UPDATE: "1",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown method: ${method}`)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
const result = await cmd.quiet().throws(false)
|
||||||
|
log.info("upgraded", {
|
||||||
|
method,
|
||||||
|
target,
|
||||||
|
stdout: result.stdout.toString(),
|
||||||
|
stderr: result.stderr.toString(),
|
||||||
|
})
|
||||||
|
if (result.exitCode !== 0)
|
||||||
|
throw new UpgradeFailedError({
|
||||||
|
stderr: result.stderr.toString("utf8"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VERSION =
|
||||||
|
typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
|
||||||
|
|
||||||
|
export async function latest() {
|
||||||
|
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.tag_name.slice(1) as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { LANGUAGE_EXTENSIONS } from "./language"
|
|||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import type { LSPServer } from "./server"
|
import type { LSPServer } from "./server"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace LSPClient {
|
export namespace LSPClient {
|
||||||
const log = Log.create({ service: "lsp.client" })
|
const log = Log.create({ service: "lsp.client" })
|
||||||
@@ -19,6 +20,13 @@ export namespace LSPClient {
|
|||||||
|
|
||||||
export type Diagnostic = VSCodeDiagnostic
|
export type Diagnostic = VSCodeDiagnostic
|
||||||
|
|
||||||
|
export const InitializeError = NamedError.create(
|
||||||
|
"LSPInitializeError",
|
||||||
|
z.object({
|
||||||
|
serverID: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Diagnostics: Bus.event(
|
Diagnostics: Bus.event(
|
||||||
"lsp.client.diagnostics",
|
"lsp.client.diagnostics",
|
||||||
@@ -52,7 +60,9 @@ export namespace LSPClient {
|
|||||||
})
|
})
|
||||||
connection.listen()
|
connection.listen()
|
||||||
|
|
||||||
await connection.sendRequest("initialize", {
|
log.info("sending initialize", { id: serverID })
|
||||||
|
await Promise.race([
|
||||||
|
connection.sendRequest("initialize", {
|
||||||
processId: server.process.pid,
|
processId: server.process.pid,
|
||||||
workspaceFolders: [
|
workspaceFolders: [
|
||||||
{
|
{
|
||||||
@@ -77,7 +87,13 @@ export namespace LSPClient {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new InitializeError({ serverID }))
|
||||||
|
}, 5_000)
|
||||||
|
}),
|
||||||
|
])
|
||||||
await connection.sendNotification("initialized", {})
|
await connection.sendNotification("initialized", {})
|
||||||
log.info("initialized")
|
log.info("initialized")
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ export namespace LSP {
|
|||||||
async () => {
|
async () => {
|
||||||
log.info("initializing")
|
log.info("initializing")
|
||||||
const clients = new Map<string, LSPClient.Info>()
|
const clients = new Map<string, LSPClient.Info>()
|
||||||
|
const skip = new Set<string>()
|
||||||
return {
|
return {
|
||||||
clients,
|
clients,
|
||||||
|
skip,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async (state) => {
|
async (state) => {
|
||||||
@@ -31,11 +32,19 @@ export namespace LSP {
|
|||||||
x.extensions.includes(extension),
|
x.extensions.includes(extension),
|
||||||
)
|
)
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
|
if (s.skip.has(match.id)) continue
|
||||||
const existing = s.clients.get(match.id)
|
const existing = s.clients.get(match.id)
|
||||||
if (existing) continue
|
if (existing) continue
|
||||||
const handle = await match.spawn(App.info())
|
const handle = await match.spawn(App.info())
|
||||||
if (!handle) continue
|
if (!handle) {
|
||||||
const client = await LSPClient.create(match.id, handle)
|
s.skip.add(match.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const client = await LSPClient.create(match.id, handle).catch(() => {})
|
||||||
|
if (!client) {
|
||||||
|
s.skip.add(match.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
s.clients.set(match.id, client)
|
s.clients.set(match.id, client)
|
||||||
}
|
}
|
||||||
if (waitForDiagnostics) {
|
if (waitForDiagnostics) {
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ export namespace LSPServer {
|
|||||||
const proc = Bun.spawn({
|
const proc = Bun.spawn({
|
||||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||||
env: { ...process.env, GOBIN: Global.Path.bin },
|
env: { ...process.env, GOBIN: Global.Path.bin },
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
stdin: "pipe",
|
||||||
})
|
})
|
||||||
const exit = await proc.exited
|
const exit = await proc.exited
|
||||||
if (exit !== 0) {
|
if (exit !== 0) {
|
||||||
|
|||||||
@@ -2,8 +2,22 @@ import { experimental_createMCPClient, type Tool } from "ai"
|
|||||||
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
|
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
import { NamedError } from "../util/error"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Session } from "../session"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
|
||||||
export namespace MCP {
|
export namespace MCP {
|
||||||
|
const log = Log.create({ service: "mcp" })
|
||||||
|
|
||||||
|
export const Failed = NamedError.create(
|
||||||
|
"MCPFailed",
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const state = App.state(
|
const state = App.state(
|
||||||
"mcp",
|
"mcp",
|
||||||
async () => {
|
async () => {
|
||||||
@@ -12,27 +26,56 @@ export namespace MCP {
|
|||||||
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
|
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
|
||||||
} = {}
|
} = {}
|
||||||
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
|
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
|
||||||
|
log.info("found", { key, type: mcp.type })
|
||||||
if (mcp.type === "remote") {
|
if (mcp.type === "remote") {
|
||||||
clients[key] = await experimental_createMCPClient({
|
const client = await experimental_createMCPClient({
|
||||||
name: key,
|
name: key,
|
||||||
transport: {
|
transport: {
|
||||||
type: "sse",
|
type: "sse",
|
||||||
url: mcp.url,
|
url: mcp.url,
|
||||||
},
|
},
|
||||||
|
}).catch(() => {})
|
||||||
|
if (!client) {
|
||||||
|
Bus.publish(Session.Event.Error, {
|
||||||
|
error: {
|
||||||
|
name: "UnknownError",
|
||||||
|
data: {
|
||||||
|
message: `MCP server ${key} failed to start`,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clients[key] = client
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mcp.type === "local") {
|
if (mcp.type === "local") {
|
||||||
const [cmd, ...args] = mcp.command
|
const [cmd, ...args] = mcp.command
|
||||||
clients[key] = await experimental_createMCPClient({
|
const client = await experimental_createMCPClient({
|
||||||
name: key,
|
name: key,
|
||||||
transport: new Experimental_StdioMCPTransport({
|
transport: new Experimental_StdioMCPTransport({
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
command: cmd,
|
command: cmd,
|
||||||
args,
|
args,
|
||||||
env: mcp.environment,
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
|
||||||
|
...mcp.environment,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
}).catch(() => {})
|
||||||
|
if (!client) {
|
||||||
|
Bus.publish(Session.Event.Error, {
|
||||||
|
error: {
|
||||||
|
name: "UnknownError",
|
||||||
|
data: {
|
||||||
|
message: `MCP server ${key} failed to start`,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clients[key] = client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export namespace Permission {
|
|||||||
title: Info["title"]
|
title: Info["title"]
|
||||||
metadata: Info["metadata"]
|
metadata: Info["metadata"]
|
||||||
}) {
|
}) {
|
||||||
|
return
|
||||||
const { pending, approved } = state()
|
const { pending, approved } = state()
|
||||||
log.info("asking", {
|
log.info("asking", {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export async function data() {
|
||||||
|
const json = await fetch("https://models.dev/api.json").then((x) => x.text())
|
||||||
|
return json
|
||||||
|
}
|
||||||
@@ -1,28 +1,69 @@
|
|||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { data } from "./models-macro" with { type: "macro" }
|
||||||
|
|
||||||
export namespace ModelsDev {
|
export namespace ModelsDev {
|
||||||
const log = Log.create({ service: "models.dev" })
|
const log = Log.create({ service: "models.dev" })
|
||||||
const filepath = path.join(Global.Path.cache, "models.json")
|
const filepath = path.join(Global.Path.cache, "models.json")
|
||||||
|
|
||||||
|
export const Model = z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
attachment: z.boolean(),
|
||||||
|
reasoning: z.boolean(),
|
||||||
|
temperature: z.boolean(),
|
||||||
|
tool_call: z.boolean(),
|
||||||
|
cost: z.object({
|
||||||
|
input: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
cache_read: z.number().optional(),
|
||||||
|
cache_write: z.number().optional(),
|
||||||
|
}),
|
||||||
|
limit: z.object({
|
||||||
|
context: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
}),
|
||||||
|
id: z.string(),
|
||||||
|
options: z.record(z.any()),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "Model.Info",
|
||||||
|
})
|
||||||
|
export type Model = z.infer<typeof Model>
|
||||||
|
|
||||||
|
export const Provider = z
|
||||||
|
.object({
|
||||||
|
api: z.string().optional(),
|
||||||
|
name: z.string(),
|
||||||
|
env: z.array(z.string()),
|
||||||
|
id: z.string(),
|
||||||
|
npm: z.string().optional(),
|
||||||
|
models: z.record(Model),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "Provider.Info",
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Provider = z.infer<typeof Provider>
|
||||||
|
|
||||||
export async function get() {
|
export async function get() {
|
||||||
const file = Bun.file(filepath)
|
const file = Bun.file(filepath)
|
||||||
const result = await file.json().catch(() => {})
|
const result = await file.json().catch(() => {})
|
||||||
if (result) {
|
if (result) {
|
||||||
refresh()
|
refresh()
|
||||||
return result
|
return result as Record<string, Provider>
|
||||||
}
|
}
|
||||||
await refresh()
|
refresh()
|
||||||
return get()
|
const json = await data()
|
||||||
|
return JSON.parse(json) as Record<string, Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const file = Bun.file(filepath)
|
const file = Bun.file(filepath)
|
||||||
log.info("refreshing")
|
log.info("refreshing")
|
||||||
const result = await fetch("https://models.dev/api.json")
|
const result = await fetch("https://models.dev/api.json").catch(() => {})
|
||||||
if (!result.ok)
|
if (result && result.ok) await Bun.write(file, result)
|
||||||
throw new Error(`Failed to fetch models.dev: ${result.statusText}`)
|
|
||||||
await Bun.write(file, result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { Config } from "../config/config"
|
|||||||
import { mergeDeep, sortBy } from "remeda"
|
import { mergeDeep, sortBy } from "remeda"
|
||||||
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
|
||||||
import { Global } from "../global"
|
|
||||||
import { BunProc } from "../bun"
|
import { BunProc } from "../bun"
|
||||||
import { BashTool } from "../tool/bash"
|
import { BashTool } from "../tool/bash"
|
||||||
import { EditTool } from "../tool/edit"
|
import { EditTool } from "../tool/edit"
|
||||||
@@ -21,159 +19,269 @@ import type { Tool } from "../tool/tool"
|
|||||||
import { WriteTool } from "../tool/write"
|
import { WriteTool } from "../tool/write"
|
||||||
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
|
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
|
||||||
import { AuthAnthropic } from "../auth/anthropic"
|
import { AuthAnthropic } from "../auth/anthropic"
|
||||||
|
import { AuthCopilot } from "../auth/copilot"
|
||||||
import { ModelsDev } from "./models"
|
import { ModelsDev } from "./models"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
|
import { TaskTool } from "../tool/task"
|
||||||
|
|
||||||
export namespace Provider {
|
export namespace Provider {
|
||||||
const log = Log.create({ service: "provider" })
|
const log = Log.create({ service: "provider" })
|
||||||
|
|
||||||
export const Model = z
|
type CustomLoader = (
|
||||||
.object({
|
provider: ModelsDev.Provider,
|
||||||
id: z.string(),
|
api?: string,
|
||||||
name: z.string().optional(),
|
) => Promise<{
|
||||||
attachment: z.boolean(),
|
autoload: boolean
|
||||||
reasoning: z.boolean().optional(),
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
cost: z.object({
|
options?: Record<string, any>
|
||||||
input: z.number(),
|
}>
|
||||||
inputCached: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
outputCached: z.number(),
|
|
||||||
}),
|
|
||||||
limit: z.object({
|
|
||||||
context: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.openapi({
|
|
||||||
ref: "Provider.Model",
|
|
||||||
})
|
|
||||||
export type Model = z.output<typeof Model>
|
|
||||||
|
|
||||||
export const Info = z
|
type Source = "env" | "config" | "custom" | "api"
|
||||||
.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
models: z.record(z.string(), Model),
|
|
||||||
})
|
|
||||||
.openapi({
|
|
||||||
ref: "Provider.Info",
|
|
||||||
})
|
|
||||||
export type Info = z.output<typeof Info>
|
|
||||||
|
|
||||||
type Autodetector = (provider: Info) => Promise<
|
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||||
| {
|
|
||||||
source: Source
|
|
||||||
options: Record<string, any>
|
|
||||||
}
|
|
||||||
| false
|
|
||||||
>
|
|
||||||
|
|
||||||
function env(...keys: string[]) {
|
|
||||||
const result: Autodetector = async () => {
|
|
||||||
for (const key of keys) {
|
|
||||||
if (process.env[key])
|
|
||||||
return {
|
|
||||||
source: "env",
|
|
||||||
options: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
type Source = "oauth" | "env" | "config" | "api"
|
|
||||||
|
|
||||||
const AUTODETECT: Record<string, Autodetector> = {
|
|
||||||
async anthropic(provider) {
|
async anthropic(provider) {
|
||||||
const access = await AuthAnthropic.access()
|
const access = await AuthAnthropic.access()
|
||||||
if (access) {
|
if (!access) return { autoload: false }
|
||||||
// claude sub doesn't have usage cost
|
|
||||||
for (const model of Object.values(provider.models)) {
|
for (const model of Object.values(provider.models)) {
|
||||||
model.cost = {
|
model.cost = {
|
||||||
input: 0,
|
input: 0,
|
||||||
inputCached: 0,
|
|
||||||
output: 0,
|
output: 0,
|
||||||
outputCached: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
source: "oauth",
|
autoload: true,
|
||||||
options: {
|
options: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
headers: {
|
async fetch(input: any, init: any) {
|
||||||
|
const access = await AuthAnthropic.access()
|
||||||
|
const headers = {
|
||||||
|
...init.headers,
|
||||||
authorization: `Bearer ${access}`,
|
authorization: `Bearer ${access}`,
|
||||||
"anthropic-beta": "oauth-2025-04-20",
|
"anthropic-beta": "oauth-2025-04-20",
|
||||||
|
}
|
||||||
|
delete headers["x-api-key"]
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return env("ANTHROPIC_API_KEY")(provider)
|
|
||||||
},
|
},
|
||||||
google: env("GOOGLE_GENERATIVE_AI_API_KEY"),
|
"github-copilot": async (provider) => {
|
||||||
openai: env("OPENAI_API_KEY"),
|
const copilot = await AuthCopilot()
|
||||||
|
if (!copilot) return { autoload: false }
|
||||||
|
let info = await Auth.get("github-copilot")
|
||||||
|
if (!info || info.type !== "oauth") return { autoload: false }
|
||||||
|
|
||||||
|
if (provider && provider.models) {
|
||||||
|
for (const model of Object.values(provider.models)) {
|
||||||
|
model.cost = {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
autoload: true,
|
||||||
|
options: {
|
||||||
|
apiKey: "",
|
||||||
|
async fetch(input: any, init: any) {
|
||||||
|
const info = await Auth.get("github-copilot")
|
||||||
|
if (!info || info.type !== "oauth") return
|
||||||
|
if (!info.access || info.expires < Date.now()) {
|
||||||
|
const tokens = await copilot.access(info.refresh)
|
||||||
|
if (!tokens)
|
||||||
|
throw new Error("GitHub Copilot authentication expired")
|
||||||
|
await Auth.set("github-copilot", {
|
||||||
|
type: "oauth",
|
||||||
|
...tokens,
|
||||||
|
})
|
||||||
|
info.access = tokens.access
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
...init.headers,
|
||||||
|
...copilot.HEADERS,
|
||||||
|
Authorization: `Bearer ${info.access}`,
|
||||||
|
"Openai-Intent": "conversation-edits",
|
||||||
|
}
|
||||||
|
delete headers["x-api-key"]
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openai: async () => {
|
||||||
|
return {
|
||||||
|
autoload: false,
|
||||||
|
async getModel(sdk: any, modelID: string) {
|
||||||
|
return sdk.responses(modelID)
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"amazon-bedrock": async () => {
|
||||||
|
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
|
||||||
|
return { autoload: false }
|
||||||
|
|
||||||
|
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||||
|
|
||||||
|
const { fromNodeProviderChain } = await import(
|
||||||
|
await BunProc.install("@aws-sdk/credential-providers")
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
autoload: true,
|
||||||
|
options: {
|
||||||
|
region,
|
||||||
|
credentialProvider: fromNodeProviderChain(),
|
||||||
|
},
|
||||||
|
async getModel(sdk: any, modelID: string) {
|
||||||
|
if (modelID.includes("claude")) {
|
||||||
|
const prefix = region.split("-")[0]
|
||||||
|
modelID = `${prefix}.${modelID}`
|
||||||
|
}
|
||||||
|
return sdk.languageModel(modelID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = App.state("provider", async () => {
|
const state = App.state("provider", async () => {
|
||||||
const config = await Config.get()
|
const config = await Config.get()
|
||||||
const database: Record<string, Provider.Info> = await ModelsDev.get()
|
const database = await ModelsDev.get()
|
||||||
|
|
||||||
const providers: {
|
const providers: {
|
||||||
[providerID: string]: {
|
[providerID: string]: {
|
||||||
source: Source
|
source: Source
|
||||||
info: Provider.Info
|
info: ModelsDev.Provider
|
||||||
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
options: Record<string, any>
|
options: Record<string, any>
|
||||||
}
|
}
|
||||||
} = {}
|
} = {}
|
||||||
const models = new Map<string, { info: Model; language: LanguageModel }>()
|
const models = new Map<
|
||||||
|
string,
|
||||||
|
{ info: ModelsDev.Model; language: LanguageModel }
|
||||||
|
>()
|
||||||
const sdk = new Map<string, SDK>()
|
const sdk = new Map<string, SDK>()
|
||||||
|
|
||||||
log.info("loading")
|
log.info("init")
|
||||||
|
|
||||||
function mergeProvider(
|
function mergeProvider(
|
||||||
id: string,
|
id: string,
|
||||||
options: Record<string, any>,
|
options: Record<string, any>,
|
||||||
source: Source,
|
source: Source,
|
||||||
|
getModel?: (sdk: any, modelID: string) => Promise<any>,
|
||||||
) {
|
) {
|
||||||
const provider = providers[id]
|
const provider = providers[id]
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
|
const info = database[id]
|
||||||
|
if (!info) return
|
||||||
|
if (info.api) options["baseURL"] = info.api
|
||||||
providers[id] = {
|
providers[id] = {
|
||||||
source,
|
source,
|
||||||
info: database[id] ?? {
|
info,
|
||||||
id,
|
|
||||||
name: id,
|
|
||||||
models: [],
|
|
||||||
},
|
|
||||||
options,
|
options,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
provider.options = mergeDeep(provider.options, options)
|
provider.options = mergeDeep(provider.options, options)
|
||||||
provider.source = source
|
provider.source = source
|
||||||
|
provider.getModel = getModel ?? provider.getModel
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [providerID, fn] of Object.entries(AUTODETECT)) {
|
const configProviders = Object.entries(config.provider ?? {})
|
||||||
const provider = database[providerID]
|
|
||||||
if (!provider) continue
|
for (const [providerID, provider] of configProviders) {
|
||||||
const result = await fn(provider)
|
const existing = database[providerID]
|
||||||
if (!result) continue
|
const parsed: ModelsDev.Provider = {
|
||||||
mergeProvider(providerID, result.options, result.source)
|
id: providerID,
|
||||||
|
npm: provider.npm ?? existing?.npm,
|
||||||
|
name: provider.name ?? existing?.name ?? providerID,
|
||||||
|
env: provider.env ?? existing?.env ?? [],
|
||||||
|
models: existing?.models ?? {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [providerID, info] of Object.entries(await Auth.all())) {
|
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
|
||||||
if (info.type === "api") {
|
const existing = parsed.models[modelID]
|
||||||
mergeProvider(providerID, { apiKey: info.key }, "api")
|
const parsedModel: ModelsDev.Model = {
|
||||||
|
id: modelID,
|
||||||
|
name: model.name ?? existing?.name ?? modelID,
|
||||||
|
attachment: model.attachment ?? existing?.attachment ?? false,
|
||||||
|
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||||
|
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||||
|
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||||
|
cost: {
|
||||||
|
...existing?.cost,
|
||||||
|
...model.cost,
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cache_read: 0,
|
||||||
|
cache_write: 0,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...existing?.options,
|
||||||
|
...model.options,
|
||||||
|
},
|
||||||
|
limit: model.limit ??
|
||||||
|
existing?.limit ?? {
|
||||||
|
context: 0,
|
||||||
|
output: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
parsed.models[modelID] = parsedModel
|
||||||
|
}
|
||||||
|
database[providerID] = parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabled = await Config.get().then(
|
||||||
|
(cfg) => new Set(cfg.disabled_providers ?? []),
|
||||||
|
)
|
||||||
|
// load env
|
||||||
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
|
if (provider.env.some((item) => process.env[item])) {
|
||||||
|
mergeProvider(providerID, {}, "env")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [providerID, options] of Object.entries(config.provider ?? {})) {
|
// load apikeys
|
||||||
mergeProvider(providerID, options, "config")
|
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
|
if (provider.type === "api") {
|
||||||
|
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const providerID of Object.keys(providers)) {
|
// load custom
|
||||||
log.info("loaded", { providerID })
|
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
|
const result = await fn(database[providerID])
|
||||||
|
if (result && (result.autoload || providers[providerID])) {
|
||||||
|
mergeProvider(
|
||||||
|
providerID,
|
||||||
|
result.options ?? {},
|
||||||
|
"custom",
|
||||||
|
result.getModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load config
|
||||||
|
for (const [providerID, provider] of configProviders) {
|
||||||
|
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
|
if (Object.keys(provider.info.models).length === 0) {
|
||||||
|
delete providers[providerID]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.info("found", { providerID })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -187,32 +295,22 @@ export namespace Provider {
|
|||||||
return state().then((state) => state.providers)
|
return state().then((state) => state.providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSDK(providerID: string) {
|
async function getSDK(provider: ModelsDev.Provider) {
|
||||||
return (async () => {
|
return (async () => {
|
||||||
|
using _ = log.time("getSDK", {
|
||||||
|
providerID: provider.id,
|
||||||
|
})
|
||||||
const s = await state()
|
const s = await state()
|
||||||
const existing = s.sdk.get(providerID)
|
const existing = s.sdk.get(provider.id)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
const dir = path.join(
|
const pkg = provider.npm ?? provider.id
|
||||||
Global.Path.cache,
|
const mod = await import(await BunProc.install(pkg, "latest"))
|
||||||
`node_modules`,
|
|
||||||
`@ai-sdk`,
|
|
||||||
providerID,
|
|
||||||
)
|
|
||||||
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
|
|
||||||
log.info("installing", {
|
|
||||||
providerID,
|
|
||||||
})
|
|
||||||
await BunProc.run(["add", `@ai-sdk/${providerID}@alpha`], {
|
|
||||||
cwd: Global.Path.cache,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const mod = await import(path.join(dir))
|
|
||||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||||
const loaded = fn(s.providers[providerID]?.options)
|
const loaded = fn(s.providers[provider.id]?.options)
|
||||||
s.sdk.set(providerID, loaded)
|
s.sdk.set(provider.id, loaded)
|
||||||
return loaded as SDK
|
return loaded as SDK
|
||||||
})().catch((e) => {
|
})().catch((e) => {
|
||||||
throw new InitError({ providerID: providerID }, { cause: e })
|
throw new InitError({ providerID: provider.id }, { cause: e })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +319,7 @@ export namespace Provider {
|
|||||||
const s = await state()
|
const s = await state()
|
||||||
if (s.models.has(key)) return s.models.get(key)!
|
if (s.models.has(key)) return s.models.get(key)!
|
||||||
|
|
||||||
log.info("loading", {
|
log.info("getModel", {
|
||||||
providerID,
|
providerID,
|
||||||
modelID,
|
modelID,
|
||||||
})
|
})
|
||||||
@@ -230,13 +328,12 @@ export namespace Provider {
|
|||||||
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
|
if (!provider) throw new ModelNotFoundError({ providerID, modelID })
|
||||||
const info = provider.info.models[modelID]
|
const info = provider.info.models[modelID]
|
||||||
if (!info) throw new ModelNotFoundError({ providerID, modelID })
|
if (!info) throw new ModelNotFoundError({ providerID, modelID })
|
||||||
|
const sdk = await getSDK(provider.info)
|
||||||
const sdk = await getSDK(providerID)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const language =
|
const language = provider.getModel
|
||||||
// @ts-expect-error
|
? await provider.getModel(sdk, modelID)
|
||||||
"responses" in sdk ? sdk.responses(modelID) : sdk.languageModel(modelID)
|
: sdk.languageModel(modelID)
|
||||||
log.info("found", { providerID, modelID })
|
log.info("found", { providerID, modelID })
|
||||||
s.models.set(key, {
|
s.models.set(key, {
|
||||||
info,
|
info,
|
||||||
@@ -260,7 +357,7 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"]
|
const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"]
|
||||||
export function sort(models: Model[]) {
|
export function sort(models: ModelsDev.Model[]) {
|
||||||
return sortBy(
|
return sortBy(
|
||||||
models,
|
models,
|
||||||
[
|
[
|
||||||
@@ -273,7 +370,15 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function defaultModel() {
|
export async function defaultModel() {
|
||||||
const [provider] = await list().then((val) => Object.values(val))
|
const cfg = await Config.get()
|
||||||
|
if (cfg.model) return parseModel(cfg.model)
|
||||||
|
const provider = await list()
|
||||||
|
.then((val) => Object.values(val))
|
||||||
|
.then((x) =>
|
||||||
|
x.find(
|
||||||
|
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
if (!provider) throw new Error("no providers found")
|
if (!provider) throw new Error("no providers found")
|
||||||
const [model] = sort(Object.values(provider.info.models))
|
const [model] = sort(Object.values(provider.info.models))
|
||||||
if (!model) throw new Error("no models found")
|
if (!model) throw new Error("no models found")
|
||||||
@@ -283,6 +388,14 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseModel(model: string) {
|
||||||
|
const [providerID, ...rest] = model.split("/")
|
||||||
|
return {
|
||||||
|
providerID: providerID,
|
||||||
|
modelID: rest.join("/"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const TOOLS = [
|
const TOOLS = [
|
||||||
BashTool,
|
BashTool,
|
||||||
EditTool,
|
EditTool,
|
||||||
@@ -298,22 +411,66 @@ export namespace Provider {
|
|||||||
// MultiEditTool,
|
// MultiEditTool,
|
||||||
WriteTool,
|
WriteTool,
|
||||||
TodoWriteTool,
|
TodoWriteTool,
|
||||||
|
TaskTool,
|
||||||
TodoReadTool,
|
TodoReadTool,
|
||||||
]
|
]
|
||||||
|
|
||||||
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
|
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
|
||||||
anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"),
|
anthropic: TOOLS.filter((t) => t.id !== "patch"),
|
||||||
openai: TOOLS,
|
openai: TOOLS.map((t) => ({
|
||||||
|
...t,
|
||||||
|
parameters: optionalToNullable(t.parameters),
|
||||||
|
})),
|
||||||
|
azure: TOOLS.map((t) => ({
|
||||||
|
...t,
|
||||||
|
parameters: optionalToNullable(t.parameters),
|
||||||
|
})),
|
||||||
google: TOOLS,
|
google: TOOLS,
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tools(providerID: string) {
|
export async function tools(providerID: string) {
|
||||||
|
/*
|
||||||
const cfg = await Config.get()
|
const cfg = await Config.get()
|
||||||
if (cfg.tool?.provider?.[providerID])
|
if (cfg.tool?.provider?.[providerID])
|
||||||
return cfg.tool.provider[providerID].map(
|
return cfg.tool.provider[providerID].map(
|
||||||
(id) => TOOLS.find((t) => t.id === id)!,
|
(id) => TOOLS.find((t) => t.id === id)!,
|
||||||
)
|
)
|
||||||
|
*/
|
||||||
return TOOL_MAPPING[providerID] ?? TOOLS
|
return TOOL_MAPPING[providerID] ?? TOOLS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
|
||||||
|
if (schema instanceof z.ZodObject) {
|
||||||
|
const shape = schema.shape
|
||||||
|
const newShape: Record<string, z.ZodTypeAny> = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(shape)) {
|
||||||
|
const zodValue = value as z.ZodTypeAny
|
||||||
|
if (zodValue instanceof z.ZodOptional) {
|
||||||
|
newShape[key] = zodValue.unwrap().nullable()
|
||||||
|
} else {
|
||||||
|
newShape[key] = optionalToNullable(zodValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return z.object(newShape)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema instanceof z.ZodArray) {
|
||||||
|
return z.array(optionalToNullable(schema.element))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema instanceof z.ZodUnion) {
|
||||||
|
return z.union(
|
||||||
|
schema.options.map((option: z.ZodTypeAny) =>
|
||||||
|
optionalToNullable(option),
|
||||||
|
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
export const ModelNotFoundError = NamedError.create(
|
export const ModelNotFoundError = NamedError.create(
|
||||||
"ProviderModelNotFoundError",
|
"ProviderModelNotFoundError",
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { LanguageModelV1Prompt } from "ai"
|
||||||
|
import { unique } from "remeda"
|
||||||
|
|
||||||
|
export namespace ProviderTransform {
|
||||||
|
export function message(
|
||||||
|
msgs: LanguageModelV1Prompt,
|
||||||
|
providerID: string,
|
||||||
|
modelID: string,
|
||||||
|
) {
|
||||||
|
if (providerID === "anthropic" || modelID.includes("anthropic")) {
|
||||||
|
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||||
|
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||||
|
|
||||||
|
for (const msg of unique([...system, ...final])) {
|
||||||
|
msg.providerMetadata = {
|
||||||
|
...msg.providerMetadata,
|
||||||
|
anthropic: {
|
||||||
|
cacheControl: { type: "ephemeral" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,10 @@ import { App } from "../app/app"
|
|||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { mapValues } from "remeda"
|
import { mapValues } from "remeda"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { Fzf } from "../external/fzf"
|
import { ModelsDev } from "../provider/models"
|
||||||
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
import { Installation } from "../installation"
|
||||||
|
import { Config } from "../config/config"
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
@@ -55,12 +58,16 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.use((c, next) => {
|
.use(async (c, next) => {
|
||||||
log.info("request", {
|
log.info("request", {
|
||||||
method: c.req.method,
|
method: c.req.method,
|
||||||
path: c.req.path,
|
path: c.req.path,
|
||||||
})
|
})
|
||||||
return next()
|
const start = Date.now()
|
||||||
|
await next()
|
||||||
|
log.info("response", {
|
||||||
|
duration: Date.now() - start,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/openapi",
|
"/openapi",
|
||||||
@@ -134,6 +141,25 @@ export namespace Server {
|
|||||||
return c.json(App.info())
|
return c.json(App.info())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/config_get",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get config info",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Get config info",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Config.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.json(await Config.get())
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/app_initialize",
|
"/app_initialize",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -264,6 +290,34 @@ export namespace Server {
|
|||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/session_unshare",
|
||||||
|
describeRoute({
|
||||||
|
description: "Unshare the session",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully unshared session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Session.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
sessionID: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Session.unshare(body.sessionID)
|
||||||
|
const session = await Session.get(body.sessionID)
|
||||||
|
return c.json(session)
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session_messages",
|
"/session_messages",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -336,6 +390,33 @@ export namespace Server {
|
|||||||
return c.json(Session.abort(body.sessionID))
|
return c.json(Session.abort(body.sessionID))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"/session_delete",
|
||||||
|
describeRoute({
|
||||||
|
description: "Delete a session and all its data",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully deleted session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
sessionID: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Session.remove(body.sessionID)
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session_summarize",
|
"/session_summarize",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -406,7 +487,7 @@ export namespace Server {
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(
|
schema: resolver(
|
||||||
z.object({
|
z.object({
|
||||||
providers: Provider.Info.array(),
|
providers: ModelsDev.Provider.array(),
|
||||||
default: z.record(z.string(), z.string()),
|
default: z.record(z.string(), z.string()),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -421,7 +502,7 @@ export namespace Server {
|
|||||||
)
|
)
|
||||||
return c.json({
|
return c.json({
|
||||||
providers: Object.values(providers),
|
providers: Object.values(providers),
|
||||||
defaults: mapValues(
|
default: mapValues(
|
||||||
providers,
|
providers,
|
||||||
(item) => Provider.sort(Object.values(item.models))[0].id,
|
(item) => Provider.sort(Object.values(item.models))[0].id,
|
||||||
),
|
),
|
||||||
@@ -452,10 +533,33 @@ export namespace Server {
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
const result = await Fzf.search(app.path.cwd, body.query)
|
const result = await Ripgrep.files({
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
query: body.query,
|
||||||
|
limit: 10,
|
||||||
|
})
|
||||||
return c.json(result)
|
return c.json(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.post(
|
||||||
|
"installation_info",
|
||||||
|
describeRoute({
|
||||||
|
description: "Get installation info",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Get installation info",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Installation.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.json(Installation.info())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -475,10 +579,10 @@ export namespace Server {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listen() {
|
export function listen(opts: { port: number; hostname: string }) {
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port: 0,
|
port: opts.port,
|
||||||
hostname: "0.0.0.0",
|
hostname: opts.hostname,
|
||||||
idleTimeout: 0,
|
idleTimeout: 0,
|
||||||
fetch: app().fetch,
|
fetch: app().fetch,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { App } from "../app/app"
|
|
||||||
import { Filesystem } from "../util/filesystem"
|
|
||||||
|
|
||||||
export namespace SessionContext {
|
|
||||||
const FILES = [
|
|
||||||
"AGENTS.md",
|
|
||||||
"CLAUDE.md",
|
|
||||||
"CONTEXT.md", // deprecated
|
|
||||||
]
|
|
||||||
export async function find() {
|
|
||||||
const { cwd, root } = App.info().path
|
|
||||||
const found = []
|
|
||||||
for (const item of FILES) {
|
|
||||||
const matches = await Filesystem.findUp(item, cwd, root)
|
|
||||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
|
||||||
}
|
|
||||||
return Promise.all(found).then((parts) => parts.join("\n\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,32 +4,36 @@ import { Identifier } from "../id/id"
|
|||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import {
|
import {
|
||||||
convertToModelMessages,
|
|
||||||
generateText,
|
generateText,
|
||||||
LoadAPIKeyError,
|
LoadAPIKeyError,
|
||||||
stepCountIs,
|
convertToCoreMessages,
|
||||||
streamText,
|
streamText,
|
||||||
tool,
|
tool,
|
||||||
type Tool as AITool,
|
type Tool as AITool,
|
||||||
type LanguageModelUsage,
|
type LanguageModelUsage,
|
||||||
|
type CoreMessage,
|
||||||
|
type UIMessage,
|
||||||
|
type ProviderMetadata,
|
||||||
|
wrapLanguageModel,
|
||||||
} from "ai"
|
} from "ai"
|
||||||
import { z, ZodSchema } from "zod"
|
import { z, ZodSchema } from "zod"
|
||||||
import { Decimal } from "decimal.js"
|
import { Decimal } from "decimal.js"
|
||||||
|
|
||||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
|
||||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
|
||||||
import PROMPT_TITLE from "./prompt/title.txt"
|
|
||||||
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
|
|
||||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
||||||
|
|
||||||
import { Share } from "../share/share"
|
import { Share } from "../share/share"
|
||||||
import { Message } from "./message"
|
import { Message } from "./message"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
import { SessionContext } from "./context"
|
|
||||||
import { ListTool } from "../tool/ls"
|
|
||||||
import { MCP } from "../mcp"
|
import { MCP } from "../mcp"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
|
import type { Tool } from "../tool/tool"
|
||||||
|
import { SystemPrompt } from "./system"
|
||||||
|
import { Flag } from "../flag/flag"
|
||||||
|
import type { ModelsDev } from "../provider/models"
|
||||||
|
import { Installation } from "../installation"
|
||||||
|
import { Config } from "../config/config"
|
||||||
|
import { ProviderTransform } from "../provider/transform"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
const log = Log.create({ service: "session" })
|
const log = Log.create({ service: "session" })
|
||||||
@@ -37,13 +41,14 @@ export namespace Session {
|
|||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
id: Identifier.schema("session"),
|
id: Identifier.schema("session"),
|
||||||
|
parentID: Identifier.schema("session").optional(),
|
||||||
share: z
|
share: z
|
||||||
.object({
|
.object({
|
||||||
secret: z.string(),
|
|
||||||
url: z.string(),
|
url: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
|
version: z.string(),
|
||||||
time: z.object({
|
time: z.object({
|
||||||
created: z.number(),
|
created: z.number(),
|
||||||
updated: z.number(),
|
updated: z.number(),
|
||||||
@@ -54,6 +59,12 @@ export namespace Session {
|
|||||||
})
|
})
|
||||||
export type Info = z.output<typeof Info>
|
export type Info = z.output<typeof Info>
|
||||||
|
|
||||||
|
export const ShareInfo = z.object({
|
||||||
|
secret: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
})
|
||||||
|
export type ShareInfo = z.output<typeof ShareInfo>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event(
|
Updated: Bus.event(
|
||||||
"session.updated",
|
"session.updated",
|
||||||
@@ -61,6 +72,12 @@ export namespace Session {
|
|||||||
info: Info,
|
info: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
Deleted: Bus.event(
|
||||||
|
"session.deleted",
|
||||||
|
z.object({
|
||||||
|
info: Info,
|
||||||
|
}),
|
||||||
|
),
|
||||||
Error: Bus.event(
|
Error: Bus.event(
|
||||||
"session.error",
|
"session.error",
|
||||||
z.object({
|
z.object({
|
||||||
@@ -69,20 +86,34 @@ export namespace Session {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = App.state("session", () => {
|
const state = App.state(
|
||||||
|
"session",
|
||||||
|
() => {
|
||||||
const sessions = new Map<string, Info>()
|
const sessions = new Map<string, Info>()
|
||||||
const messages = new Map<string, Message.Info[]>()
|
const messages = new Map<string, Message.Info[]>()
|
||||||
|
const pending = new Map<string, AbortController>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessions,
|
sessions,
|
||||||
messages,
|
messages,
|
||||||
|
pending,
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
async (state) => {
|
||||||
|
for (const [_, controller] of state.pending) {
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export async function create() {
|
export async function create(parentID?: string) {
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
id: Identifier.descending("session"),
|
id: Identifier.descending("session"),
|
||||||
title: "New Session - " + new Date().toISOString(),
|
version: Installation.VERSION,
|
||||||
|
parentID,
|
||||||
|
title:
|
||||||
|
(parentID ? "Child session - " : "New Session - ") +
|
||||||
|
new Date().toISOString(),
|
||||||
time: {
|
time: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
updated: Date.now(),
|
updated: Date.now(),
|
||||||
@@ -91,6 +122,8 @@ export namespace Session {
|
|||||||
log.info("created", result)
|
log.info("created", result)
|
||||||
state().sessions.set(result.id, result)
|
state().sessions.set(result.id, result)
|
||||||
await Storage.writeJSON("session/info/" + result.id, result)
|
await Storage.writeJSON("session/info/" + result.id, result)
|
||||||
|
const cfg = await Config.get()
|
||||||
|
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.autoshare))
|
||||||
share(result.id).then((share) => {
|
share(result.id).then((share) => {
|
||||||
update(result.id, (draft) => {
|
update(result.id, (draft) => {
|
||||||
draft.share = share
|
draft.share = share
|
||||||
@@ -112,19 +145,35 @@ export namespace Session {
|
|||||||
return read as Info
|
return read as Info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getShare(id: string) {
|
||||||
|
return Storage.readJSON<ShareInfo>("session/share/" + id)
|
||||||
|
}
|
||||||
|
|
||||||
export async function share(id: string) {
|
export async function share(id: string) {
|
||||||
const session = await get(id)
|
const session = await get(id)
|
||||||
if (session.share) return session.share
|
if (session.share) return session.share
|
||||||
const share = await Share.create(id)
|
const share = await Share.create(id)
|
||||||
await update(id, (draft) => {
|
await update(id, (draft) => {
|
||||||
draft.share = share
|
draft.share = {
|
||||||
|
url: share.url,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
await Storage.writeJSON<ShareInfo>("session/share/" + id, share)
|
||||||
|
await Share.sync("session/info/" + id, session)
|
||||||
for (const msg of await messages(id)) {
|
for (const msg of await messages(id)) {
|
||||||
await Share.sync("session/message/" + id + "/" + msg.id, msg)
|
await Share.sync("session/message/" + id + "/" + msg.id, msg)
|
||||||
}
|
}
|
||||||
return share
|
return share
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function unshare(id: string) {
|
||||||
|
await Storage.remove("session/share/" + id)
|
||||||
|
await update(id, (draft) => {
|
||||||
|
draft.share = undefined
|
||||||
|
})
|
||||||
|
await Share.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
export async function update(id: string, editor: (session: Info) => void) {
|
export async function update(id: string, editor: (session: Info) => void) {
|
||||||
const { sessions } = state()
|
const { sessions } = state()
|
||||||
const session = await get(id)
|
const session = await get(id)
|
||||||
@@ -163,14 +212,47 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function children(parentID: string) {
|
||||||
|
const result = [] as Session.Info[]
|
||||||
|
for await (const item of Storage.list("session/info")) {
|
||||||
|
const sessionID = path.basename(item, ".json")
|
||||||
|
const session = await get(sessionID)
|
||||||
|
if (session.parentID !== parentID) continue
|
||||||
|
result.push(session)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function abort(sessionID: string) {
|
export function abort(sessionID: string) {
|
||||||
const controller = pending.get(sessionID)
|
const controller = state().pending.get(sessionID)
|
||||||
if (!controller) return false
|
if (!controller) return false
|
||||||
controller.abort()
|
controller.abort()
|
||||||
pending.delete(sessionID)
|
state().pending.delete(sessionID)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function remove(sessionID: string, emitEvent = true) {
|
||||||
|
try {
|
||||||
|
abort(sessionID)
|
||||||
|
const session = await get(sessionID)
|
||||||
|
for (const child of await children(sessionID)) {
|
||||||
|
await remove(child.id, false)
|
||||||
|
}
|
||||||
|
await unshare(sessionID).catch(() => {})
|
||||||
|
await Storage.remove(`session/info/${sessionID}`).catch(() => {})
|
||||||
|
await Storage.removeDir(`session/message/${sessionID}/`).catch(() => {})
|
||||||
|
state().sessions.delete(sessionID)
|
||||||
|
state().messages.delete(sessionID)
|
||||||
|
if (emitEvent) {
|
||||||
|
Bus.publish(Event.Deleted, {
|
||||||
|
info: session,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateMessage(msg: Message.Info) {
|
async function updateMessage(msg: Message.Info) {
|
||||||
await Storage.writeJSON(
|
await Storage.writeJSON(
|
||||||
"session/message/" + msg.metadata.sessionID + "/" + msg.id,
|
"session/message/" + msg.metadata.sessionID + "/" + msg.id,
|
||||||
@@ -186,17 +268,24 @@ export namespace Session {
|
|||||||
providerID: string
|
providerID: string
|
||||||
modelID: string
|
modelID: string
|
||||||
parts: Message.Part[]
|
parts: Message.Part[]
|
||||||
|
system?: string[]
|
||||||
|
tools?: Tool.Info[]
|
||||||
}) {
|
}) {
|
||||||
const l = log.clone().tag("session", input.sessionID)
|
const l = log.clone().tag("session", input.sessionID)
|
||||||
l.info("chatting")
|
l.info("chatting")
|
||||||
const model = await Provider.getModel(input.providerID, input.modelID)
|
const model = await Provider.getModel(input.providerID, input.modelID)
|
||||||
let msgs = await messages(input.sessionID)
|
let msgs = await messages(input.sessionID)
|
||||||
const previous = msgs.at(-1)
|
const previous = msgs.at(-1)
|
||||||
|
|
||||||
|
// auto summarize if too long
|
||||||
if (previous?.metadata.assistant) {
|
if (previous?.metadata.assistant) {
|
||||||
const tokens =
|
const tokens =
|
||||||
previous.metadata.assistant.tokens.input +
|
previous.metadata.assistant.tokens.input +
|
||||||
|
previous.metadata.assistant.tokens.cache.read +
|
||||||
|
previous.metadata.assistant.tokens.cache.write +
|
||||||
previous.metadata.assistant.tokens.output
|
previous.metadata.assistant.tokens.output
|
||||||
if (
|
if (
|
||||||
|
model.info.limit.context &&
|
||||||
tokens >
|
tokens >
|
||||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||||
) {
|
) {
|
||||||
@@ -214,110 +303,38 @@ export namespace Session {
|
|||||||
const lastSummary = msgs.findLast(
|
const lastSummary = msgs.findLast(
|
||||||
(msg) => msg.metadata.assistant?.summary === true,
|
(msg) => msg.metadata.assistant?.summary === true,
|
||||||
)
|
)
|
||||||
if (lastSummary)
|
if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
|
||||||
msgs = msgs.filter(
|
|
||||||
(msg) => msg.role === "system" || msg.id >= lastSummary.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (msgs.length === 0) {
|
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
if (input.providerID === "anthropic") {
|
const session = await get(input.sessionID)
|
||||||
const claude: Message.Info = {
|
if (msgs.length === 0 && !session.parentID) {
|
||||||
id: Identifier.ascending("message"),
|
|
||||||
role: "system",
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: PROMPT_ANTHROPIC_SPOOF.trim(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
metadata: {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
time: {
|
|
||||||
created: Date.now(),
|
|
||||||
},
|
|
||||||
tool: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await updateMessage(claude)
|
|
||||||
msgs.push(claude)
|
|
||||||
}
|
|
||||||
const system: Message.Info = {
|
|
||||||
id: Identifier.ascending("message"),
|
|
||||||
role: "system",
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: PROMPT_ANTHROPIC,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: [
|
|
||||||
`Here is some useful information about the environment you are running in:`,
|
|
||||||
`<env>`,
|
|
||||||
`Working directory: ${app.path.cwd}`,
|
|
||||||
`Is directory a git repo: ${app.git ? "yes" : "no"}`,
|
|
||||||
`Platform: ${process.platform}`,
|
|
||||||
`Today's date: ${new Date().toISOString()}`,
|
|
||||||
`</env>`,
|
|
||||||
`<project>`,
|
|
||||||
`${app.git ? await ListTool.execute({ path: app.path.cwd, ignore: [] }, { sessionID: input.sessionID, abort: abort.signal }).then((x) => x.output) : ""}`,
|
|
||||||
`</project>`,
|
|
||||||
].join("\n"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
metadata: {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
time: {
|
|
||||||
created: Date.now(),
|
|
||||||
},
|
|
||||||
tool: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const context = await SessionContext.find()
|
|
||||||
if (context) {
|
|
||||||
system.parts.push({
|
|
||||||
type: "text",
|
|
||||||
text: context,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
msgs.push(system)
|
|
||||||
generateText({
|
generateText({
|
||||||
maxOutputTokens: 20,
|
maxTokens: input.providerID === "google" ? 1024 : 20,
|
||||||
messages: convertToModelMessages([
|
providerOptions: model.info.options,
|
||||||
{
|
messages: [
|
||||||
|
...SystemPrompt.title(input.providerID).map(
|
||||||
|
(x): CoreMessage => ({
|
||||||
role: "system",
|
role: "system",
|
||||||
parts: [
|
content: x,
|
||||||
{
|
}),
|
||||||
type: "text",
|
),
|
||||||
text: PROMPT_ANTHROPIC_SPOOF.trim(),
|
...convertToCoreMessages([
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: PROMPT_TITLE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
parts: input.parts,
|
content: "",
|
||||||
|
parts: toParts(input.parts),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
temperature: 0,
|
],
|
||||||
model: model.language,
|
model: model.language,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
if (result.text)
|
||||||
return Session.update(input.sessionID, (draft) => {
|
return Session.update(input.sessionID, (draft) => {
|
||||||
draft.title = result.text
|
draft.title = result.text
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
await updateMessage(system)
|
|
||||||
}
|
}
|
||||||
const msg: Message.Info = {
|
const msg: Message.Info = {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -334,17 +351,27 @@ export namespace Session {
|
|||||||
await updateMessage(msg)
|
await updateMessage(msg)
|
||||||
msgs.push(msg)
|
msgs.push(msg)
|
||||||
|
|
||||||
|
const system = input.system ?? SystemPrompt.provider(input.providerID)
|
||||||
|
system.push(...(await SystemPrompt.environment()))
|
||||||
|
system.push(...(await SystemPrompt.custom()))
|
||||||
|
|
||||||
const next: Message.Info = {
|
const next: Message.Info = {
|
||||||
id: Identifier.ascending("message"),
|
id: Identifier.ascending("message"),
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
parts: [],
|
parts: [],
|
||||||
metadata: {
|
metadata: {
|
||||||
assistant: {
|
assistant: {
|
||||||
|
system,
|
||||||
|
path: {
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
root: app.path.root,
|
||||||
|
},
|
||||||
cost: 0,
|
cost: 0,
|
||||||
tokens: {
|
tokens: {
|
||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
reasoning: 0,
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
},
|
},
|
||||||
modelID: input.modelID,
|
modelID: input.modelID,
|
||||||
providerID: input.providerID,
|
providerID: input.providerID,
|
||||||
@@ -358,6 +385,7 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
const tools: Record<string, AITool> = {}
|
const tools: Record<string, AITool> = {}
|
||||||
|
|
||||||
for (const item of await Provider.tools(input.providerID)) {
|
for (const item of await Provider.tools(input.providerID)) {
|
||||||
tools[item.id.replaceAll(".", "_")] = tool({
|
tools[item.id.replaceAll(".", "_")] = tool({
|
||||||
id: item.id as any,
|
id: item.id as any,
|
||||||
@@ -369,6 +397,17 @@ export namespace Session {
|
|||||||
const result = await item.execute(args, {
|
const result = await item.execute(args, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
abort: abort.signal,
|
abort: abort.signal,
|
||||||
|
messageID: next.id,
|
||||||
|
metadata: async (val) => {
|
||||||
|
next.metadata.tool[opts.toolCallId] = {
|
||||||
|
...val,
|
||||||
|
time: {
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await updateMessage(next)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
next.metadata!.tool![opts.toolCallId] = {
|
next.metadata!.tool![opts.toolCallId] = {
|
||||||
...result.metadata,
|
...result.metadata,
|
||||||
@@ -395,6 +434,7 @@ export namespace Session {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, item] of Object.entries(await MCP.tools())) {
|
for (const [key, item] of Object.entries(await MCP.tools())) {
|
||||||
const execute = item.execute
|
const execute = item.execute
|
||||||
if (!execute) continue
|
if (!execute) continue
|
||||||
@@ -434,11 +474,9 @@ export namespace Session {
|
|||||||
let text: Message.TextPart | undefined
|
let text: Message.TextPart | undefined
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
onStepFinish: async (step) => {
|
onStepFinish: async (step) => {
|
||||||
log.info("step finish", {
|
log.info("step finish", { finishReason: step.finishReason })
|
||||||
finishReason: step.finishReason,
|
|
||||||
})
|
|
||||||
const assistant = next.metadata!.assistant!
|
const assistant = next.metadata!.assistant!
|
||||||
const usage = getUsage(step.usage, model.info)
|
const usage = getUsage(model.info, step.usage, step.providerMetadata)
|
||||||
assistant.cost += usage.cost
|
assistant.cost += usage.cost
|
||||||
assistant.tokens = usage.tokens
|
assistant.tokens = usage.tokens
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
@@ -451,18 +489,105 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
text = undefined
|
text = undefined
|
||||||
},
|
},
|
||||||
async onChunk(input) {
|
async onFinish(input) {
|
||||||
const value = input.chunk
|
log.info("message finish", {
|
||||||
|
reason: input.finishReason,
|
||||||
|
})
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(model.info, input.usage, input.providerMetadata)
|
||||||
|
assistant.cost = usage.cost
|
||||||
|
await updateMessage(next)
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
log.error("callback error", err)
|
||||||
|
switch (true) {
|
||||||
|
case LoadAPIKeyError.isInstance(err.error):
|
||||||
|
next.metadata.error = new Provider.AuthError(
|
||||||
|
{
|
||||||
|
providerID: input.providerID,
|
||||||
|
message: err.error.message,
|
||||||
|
},
|
||||||
|
{ cause: err.error },
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
case err.error instanceof Error:
|
||||||
|
next.metadata.error = new NamedError.Unknown(
|
||||||
|
{ message: err.error.toString() },
|
||||||
|
{ cause: err.error },
|
||||||
|
).toObject()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
next.metadata.error = new NamedError.Unknown(
|
||||||
|
{ message: JSON.stringify(err.error) },
|
||||||
|
{ cause: err.error },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Bus.publish(Event.Error, {
|
||||||
|
error: next.metadata.error,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// async prepareStep(step) {
|
||||||
|
// next.parts.push({
|
||||||
|
// type: "step-start",
|
||||||
|
// })
|
||||||
|
// await updateMessage(next)
|
||||||
|
// return step
|
||||||
|
// },
|
||||||
|
toolCallStreaming: true,
|
||||||
|
abortSignal: abort.signal,
|
||||||
|
maxSteps: 1000,
|
||||||
|
providerOptions: model.info.options,
|
||||||
|
messages: [
|
||||||
|
...system.map(
|
||||||
|
(x): CoreMessage => ({
|
||||||
|
role: "system",
|
||||||
|
content: x,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...convertToCoreMessages(
|
||||||
|
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
temperature: model.info.temperature ? 0 : undefined,
|
||||||
|
tools: model.info.tool_call === false ? undefined : tools,
|
||||||
|
model: wrapLanguageModel({
|
||||||
|
model: model.language,
|
||||||
|
middleware: [
|
||||||
|
{
|
||||||
|
async transformParams(args) {
|
||||||
|
if (args.type === "stream") {
|
||||||
|
args.params.prompt = ProviderTransform.message(
|
||||||
|
args.params.prompt,
|
||||||
|
input.providerID,
|
||||||
|
input.modelID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return args.params
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
for await (const value of result.fullStream) {
|
||||||
l.info("part", {
|
l.info("part", {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
})
|
})
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case "text":
|
case "step-start":
|
||||||
if (!text) {
|
next.parts.push({
|
||||||
text = value
|
type: "step-start",
|
||||||
next.parts.push(value)
|
})
|
||||||
break
|
break
|
||||||
} else text.text += value.text
|
case "text-delta":
|
||||||
|
if (!text) {
|
||||||
|
text = {
|
||||||
|
type: "text",
|
||||||
|
text: value.textDelta,
|
||||||
|
}
|
||||||
|
next.parts.push(text)
|
||||||
|
break
|
||||||
|
} else text.text += value.textDelta
|
||||||
break
|
break
|
||||||
|
|
||||||
case "tool-call": {
|
case "tool-call": {
|
||||||
@@ -501,20 +626,27 @@ export namespace Session {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case "tool-call-delta":
|
case "tool-call-delta":
|
||||||
break
|
continue
|
||||||
|
|
||||||
|
// for some reason ai sdk claims to not send this part but it does
|
||||||
|
// @ts-expect-error
|
||||||
case "tool-result":
|
case "tool-result":
|
||||||
const match = next.parts.find(
|
const match = next.parts.find(
|
||||||
(p) =>
|
(p) =>
|
||||||
p.type === "tool-invocation" &&
|
p.type === "tool-invocation" &&
|
||||||
|
// @ts-expect-error
|
||||||
p.toolInvocation.toolCallId === value.toolCallId,
|
p.toolInvocation.toolCallId === value.toolCallId,
|
||||||
)
|
)
|
||||||
if (match && match.type === "tool-invocation") {
|
if (match && match.type === "tool-invocation") {
|
||||||
match.toolInvocation = {
|
match.toolInvocation = {
|
||||||
|
// @ts-expect-error
|
||||||
args: value.args,
|
args: value.args,
|
||||||
|
// @ts-expect-error
|
||||||
toolCallId: value.toolCallId,
|
toolCallId: value.toolCallId,
|
||||||
|
// @ts-expect-error
|
||||||
toolName: value.toolName,
|
toolName: value.toolName,
|
||||||
state: "result",
|
state: "result",
|
||||||
|
// @ts-expect-error
|
||||||
result: value.result as string,
|
result: value.result as string,
|
||||||
}
|
}
|
||||||
Bus.publish(Message.Event.PartUpdated, {
|
Bus.publish(Message.Event.PartUpdated, {
|
||||||
@@ -529,68 +661,40 @@ export namespace Session {
|
|||||||
l.info("unhandled", {
|
l.info("unhandled", {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
},
|
}
|
||||||
async onFinish(input) {
|
} catch (e: any) {
|
||||||
const assistant = next.metadata!.assistant!
|
log.error("stream error", {
|
||||||
const usage = getUsage(input.totalUsage, model.info)
|
error: e,
|
||||||
assistant.cost = usage.cost
|
})
|
||||||
await updateMessage(next)
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
log.error("error", err)
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case LoadAPIKeyError.isInstance(err.error):
|
case LoadAPIKeyError.isInstance(e):
|
||||||
next.metadata.error = new Provider.AuthError(
|
next.metadata.error = new Provider.AuthError(
|
||||||
{
|
{
|
||||||
providerID: input.providerID,
|
providerID: input.providerID,
|
||||||
message: err.error.message,
|
message: e.message,
|
||||||
},
|
},
|
||||||
{ cause: err.error },
|
{ cause: e },
|
||||||
).toObject()
|
).toObject()
|
||||||
break
|
break
|
||||||
case err.error instanceof Error:
|
case e instanceof Error:
|
||||||
next.metadata.error = new NamedError.Unknown(
|
next.metadata.error = new NamedError.Unknown(
|
||||||
{ message: err.error.toString() },
|
{ message: e.toString() },
|
||||||
{ cause: err.error },
|
{ cause: e },
|
||||||
).toObject()
|
).toObject()
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
next.metadata.error = new NamedError.Unknown(
|
next.metadata.error = new NamedError.Unknown(
|
||||||
{ message: JSON.stringify(err.error) },
|
{ message: JSON.stringify(e) },
|
||||||
{ cause: err.error },
|
{ cause: e },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Bus.publish(Event.Error, {
|
Bus.publish(Event.Error, {
|
||||||
error: next.metadata.error,
|
error: next.metadata.error,
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
async prepareStep(step) {
|
|
||||||
next.parts.push({
|
|
||||||
type: "step-start",
|
|
||||||
})
|
|
||||||
await updateMessage(next)
|
|
||||||
return step
|
|
||||||
},
|
|
||||||
toolCallStreaming: true,
|
|
||||||
abortSignal: abort.signal,
|
|
||||||
stopWhen: stepCountIs(1000),
|
|
||||||
messages: convertToModelMessages(msgs),
|
|
||||||
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
|
|
||||||
tools: {
|
|
||||||
...(await MCP.tools()),
|
|
||||||
...tools,
|
|
||||||
},
|
|
||||||
model: model.language,
|
|
||||||
})
|
|
||||||
await result.consumeStream({
|
|
||||||
onError: (err) => {
|
|
||||||
log.error("stream error", {
|
|
||||||
err,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
next.metadata!.time.completed = Date.now()
|
next.metadata!.time.completed = Date.now()
|
||||||
for (const part of next.parts) {
|
for (const part of next.parts) {
|
||||||
if (
|
if (
|
||||||
@@ -618,10 +722,11 @@ export namespace Session {
|
|||||||
const lastSummary = msgs.findLast(
|
const lastSummary = msgs.findLast(
|
||||||
(msg) => msg.metadata.assistant?.summary === true,
|
(msg) => msg.metadata.assistant?.summary === true,
|
||||||
)?.id
|
)?.id
|
||||||
const filtered = msgs.filter(
|
const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary)
|
||||||
(msg) => msg.role !== "system" && (!lastSummary || msg.id >= lastSummary),
|
|
||||||
)
|
|
||||||
const model = await Provider.getModel(input.providerID, input.modelID)
|
const model = await Provider.getModel(input.providerID, input.modelID)
|
||||||
|
const app = App.info()
|
||||||
|
const system = SystemPrompt.summarize(input.providerID)
|
||||||
|
|
||||||
const next: Message.Info = {
|
const next: Message.Info = {
|
||||||
id: Identifier.ascending("message"),
|
id: Identifier.ascending("message"),
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -630,6 +735,11 @@ export namespace Session {
|
|||||||
tool: {},
|
tool: {},
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
assistant: {
|
assistant: {
|
||||||
|
system,
|
||||||
|
path: {
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
root: app.path.root,
|
||||||
|
},
|
||||||
summary: true,
|
summary: true,
|
||||||
cost: 0,
|
cost: 0,
|
||||||
modelID: input.modelID,
|
modelID: input.modelID,
|
||||||
@@ -638,6 +748,7 @@ export namespace Session {
|
|||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
reasoning: 0,
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
@@ -646,67 +757,115 @@ export namespace Session {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
const result = await generateText({
|
|
||||||
|
let text: Message.TextPart | undefined
|
||||||
|
const result = streamText({
|
||||||
abortSignal: abort.signal,
|
abortSignal: abort.signal,
|
||||||
model: model.language,
|
model: model.language,
|
||||||
messages: convertToModelMessages([
|
messages: [
|
||||||
{
|
...system.map(
|
||||||
|
(x): CoreMessage => ({
|
||||||
role: "system",
|
role: "system",
|
||||||
parts: [
|
content: x,
|
||||||
{
|
}),
|
||||||
type: "text",
|
),
|
||||||
text: PROMPT_SUMMARIZE,
|
...convertToCoreMessages(filtered.map(toUIMessage)),
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...filtered,
|
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
parts: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "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.",
|
text: "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.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]),
|
],
|
||||||
})
|
onStepFinish: async (step) => {
|
||||||
next.parts.push({
|
|
||||||
type: "text",
|
|
||||||
text: result.text,
|
|
||||||
})
|
|
||||||
const assistant = next.metadata!.assistant!
|
const assistant = next.metadata!.assistant!
|
||||||
const usage = getUsage(result.usage, model.info)
|
const usage = getUsage(model.info, step.usage, step.providerMetadata)
|
||||||
assistant.cost = usage.cost
|
assistant.cost += usage.cost
|
||||||
assistant.tokens = usage.tokens
|
assistant.tokens = usage.tokens
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
|
if (text) {
|
||||||
|
Bus.publish(Message.Event.PartUpdated, {
|
||||||
|
part: text,
|
||||||
|
messageID: next.id,
|
||||||
|
sessionID: next.metadata.sessionID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
text = undefined
|
||||||
|
},
|
||||||
|
async onFinish(input) {
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(model.info, input.usage, input.providerMetadata)
|
||||||
|
assistant.cost = usage.cost
|
||||||
|
assistant.tokens = usage.tokens
|
||||||
|
next.metadata!.time.completed = Date.now()
|
||||||
|
await updateMessage(next)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for await (const value of result.fullStream) {
|
||||||
|
switch (value.type) {
|
||||||
|
case "text-delta":
|
||||||
|
if (!text) {
|
||||||
|
text = {
|
||||||
|
type: "text",
|
||||||
|
text: value.textDelta,
|
||||||
|
}
|
||||||
|
next.parts.push(text)
|
||||||
|
} else text.text += value.textDelta
|
||||||
|
|
||||||
|
await updateMessage(next)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = new Map<string, AbortController>()
|
|
||||||
function lock(sessionID: string) {
|
function lock(sessionID: string) {
|
||||||
log.info("locking", { sessionID })
|
log.info("locking", { sessionID })
|
||||||
if (pending.has(sessionID)) throw new BusyError(sessionID)
|
if (state().pending.has(sessionID)) throw new BusyError(sessionID)
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
pending.set(sessionID, controller)
|
state().pending.set(sessionID, controller)
|
||||||
return {
|
return {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
[Symbol.dispose]() {
|
[Symbol.dispose]() {
|
||||||
log.info("unlocking", { sessionID })
|
log.info("unlocking", { sessionID })
|
||||||
pending.delete(sessionID)
|
state().pending.delete(sessionID)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsage(usage: LanguageModelUsage, model: Provider.Model) {
|
function getUsage(
|
||||||
|
model: ModelsDev.Model,
|
||||||
|
usage: LanguageModelUsage,
|
||||||
|
metadata?: ProviderMetadata,
|
||||||
|
) {
|
||||||
const tokens = {
|
const tokens = {
|
||||||
input: usage.inputTokens ?? 0,
|
input: usage.promptTokens ?? 0,
|
||||||
output: usage.outputTokens ?? 0,
|
output: usage.completionTokens ?? 0,
|
||||||
reasoning: usage.reasoningTokens ?? 0,
|
reasoning: 0,
|
||||||
|
cache: {
|
||||||
|
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||||
|
0) as number,
|
||||||
|
read: (metadata?.["anthropic"]?.["cacheReadInputTokens"] ??
|
||||||
|
0) as number,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
cost: new Decimal(0)
|
cost: new Decimal(0)
|
||||||
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
|
.add(new Decimal(tokens.input).mul(model.cost.input).div(1_000_000))
|
||||||
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
|
.add(new Decimal(tokens.output).mul(model.cost.output).div(1_000_000))
|
||||||
|
.add(
|
||||||
|
new Decimal(tokens.cache.read)
|
||||||
|
.mul(model.cost.cache_read ?? 0)
|
||||||
|
.div(1_000_000),
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
new Decimal(tokens.cache.write)
|
||||||
|
.mul(model.cost.cache_write ?? 0)
|
||||||
|
.div(1_000_000),
|
||||||
|
)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
tokens,
|
tokens,
|
||||||
}
|
}
|
||||||
@@ -738,3 +897,57 @@ export namespace Session {
|
|||||||
await App.initialize()
|
await App.initialize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toUIMessage(msg: Message.Info): UIMessage {
|
||||||
|
if (msg.role === "assistant") {
|
||||||
|
return {
|
||||||
|
id: msg.id,
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
parts: toParts(msg.parts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === "user") {
|
||||||
|
return {
|
||||||
|
id: msg.id,
|
||||||
|
role: "user",
|
||||||
|
content: "",
|
||||||
|
parts: toParts(msg.parts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
function toParts(parts: Message.Part[]): UIMessage["parts"] {
|
||||||
|
const result: UIMessage["parts"] = []
|
||||||
|
for (const part of parts) {
|
||||||
|
switch (part.type) {
|
||||||
|
case "text":
|
||||||
|
result.push({ type: "text", text: part.text })
|
||||||
|
break
|
||||||
|
case "file":
|
||||||
|
result.push({
|
||||||
|
type: "file",
|
||||||
|
data: part.url,
|
||||||
|
mimeType: part.mediaType,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "tool-invocation":
|
||||||
|
result.push({
|
||||||
|
type: "tool-invocation",
|
||||||
|
toolInvocation: part.toolInvocation,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "step-start":
|
||||||
|
result.push({
|
||||||
|
type: "step-start",
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -133,9 +133,10 @@ export namespace Message {
|
|||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
role: z.enum(["system", "user", "assistant"]),
|
role: z.enum(["user", "assistant"]),
|
||||||
parts: z.array(Part),
|
parts: z.array(Part),
|
||||||
metadata: z.object({
|
metadata: z
|
||||||
|
.object({
|
||||||
time: z.object({
|
time: z.object({
|
||||||
created: z.number(),
|
created: z.number(),
|
||||||
completed: z.number().optional(),
|
completed: z.number().optional(),
|
||||||
@@ -161,18 +162,28 @@ export namespace Message {
|
|||||||
),
|
),
|
||||||
assistant: z
|
assistant: z
|
||||||
.object({
|
.object({
|
||||||
|
system: z.string().array(),
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
providerID: z.string(),
|
providerID: z.string(),
|
||||||
|
path: z.object({
|
||||||
|
cwd: z.string(),
|
||||||
|
root: z.string(),
|
||||||
|
}),
|
||||||
cost: z.number(),
|
cost: z.number(),
|
||||||
summary: z.boolean().optional(),
|
summary: z.boolean().optional(),
|
||||||
tokens: z.object({
|
tokens: z.object({
|
||||||
input: z.number(),
|
input: z.number(),
|
||||||
output: z.number(),
|
output: z.number(),
|
||||||
reasoning: z.number(),
|
reasoning: z.number(),
|
||||||
|
cache: z.object({
|
||||||
|
read: z.number(),
|
||||||
|
write: z.number(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
}),
|
})
|
||||||
|
.openapi({ ref: "Message.Metadata" }),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Info",
|
ref: "Message.Info",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
||||||
|
|
||||||
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
||||||
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
||||||
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
||||||
|
|
||||||
If the user asks for help or wants to give feedback inform them of the following:
|
If the user asks for help or wants to give feedback inform them of the following:
|
||||||
- /help: Get help with using OpenCode
|
- /help: Get help with using opencode
|
||||||
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
||||||
|
|
||||||
When the user directly asks about OpenCode (eg 'can OpenCode do...', 'does OpenCode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from OpenCode docs at https://opencode.ai
|
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
|
||||||
|
|
||||||
# Tone and style
|
# Tone and style
|
||||||
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
you will generate a short title based on the first message a user begins a conversation with
|
Generate a short title based on the first message a user begins a conversation with. CRITICAL: Your response must be EXACTLY one line with NO line breaks, newlines, or multiple sentences.
|
||||||
- ensure it is not more than 50 characters long
|
|
||||||
- the title should be a summary of the user's message
|
Requirements:
|
||||||
- it should be one line long
|
- Maximum 50 characters
|
||||||
- do not use quotes or colons
|
- Single line only - NO newlines or line breaks
|
||||||
- the entire text you return will be used as the title
|
- Summary of the user's message
|
||||||
- never return anything that is more than one sentence (one line) long
|
- No quotes, colons, or special formatting
|
||||||
|
- Do not include explanatory text like "summary:" or similar
|
||||||
|
- Your entire response becomes the title
|
||||||
|
|
||||||
|
IMPORTANT: Return only the title text on a single line. Do not add any explanations, formatting, or additional text.
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { App } from "../app/app"
|
||||||
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
import { Global } from "../global"
|
||||||
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
|
||||||
|
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||||
|
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||||
|
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
|
||||||
|
import PROMPT_TITLE from "./prompt/title.txt"
|
||||||
|
|
||||||
|
export namespace SystemPrompt {
|
||||||
|
export function provider(providerID: string) {
|
||||||
|
const result = []
|
||||||
|
switch (providerID) {
|
||||||
|
case "anthropic":
|
||||||
|
result.push(PROMPT_ANTHROPIC_SPOOF.trim())
|
||||||
|
result.push(PROMPT_ANTHROPIC)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result.push(PROMPT_ANTHROPIC)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function environment() {
|
||||||
|
const app = App.info()
|
||||||
|
|
||||||
|
;async () => {
|
||||||
|
const files = await Ripgrep.files({
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
})
|
||||||
|
type Node = {
|
||||||
|
children: Record<string, Node>
|
||||||
|
}
|
||||||
|
const root: Node = {
|
||||||
|
children: {},
|
||||||
|
}
|
||||||
|
for (const file of files) {
|
||||||
|
const parts = file.split("/")
|
||||||
|
let node = root
|
||||||
|
for (const part of parts) {
|
||||||
|
const existing = node.children[part]
|
||||||
|
if (existing) {
|
||||||
|
node = existing
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node.children[part] = {
|
||||||
|
children: {},
|
||||||
|
}
|
||||||
|
node = node.children[part]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(path: string[], node: Node): string {
|
||||||
|
// if (path.length === 3) return "\t".repeat(path.length) + "..."
|
||||||
|
const lines: string[] = []
|
||||||
|
const entries = Object.entries(node.children).sort(([a], [b]) =>
|
||||||
|
a.localeCompare(b),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const [name, child] of entries) {
|
||||||
|
const currentPath = [...path, name]
|
||||||
|
const indent = "\t".repeat(path.length)
|
||||||
|
const hasChildren = Object.keys(child.children).length > 0
|
||||||
|
lines.push(`${indent}${name}` + (hasChildren ? "/" : ""))
|
||||||
|
|
||||||
|
if (hasChildren) lines.push(render(currentPath, child))
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n")
|
||||||
|
}
|
||||||
|
const result = render([], root)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
`Here is some useful information about the environment you are running in:`,
|
||||||
|
`<env>`,
|
||||||
|
` Working directory: ${app.path.cwd}`,
|
||||||
|
` Is directory a git repo: ${app.git ? "yes" : "no"}`,
|
||||||
|
` Platform: ${process.platform}`,
|
||||||
|
` Today's date: ${new Date().toDateString()}`,
|
||||||
|
`</env>`,
|
||||||
|
// `<project>`,
|
||||||
|
// ` ${app.git ? await tree() : ""}`,
|
||||||
|
// `</project>`,
|
||||||
|
].join("\n"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const CUSTOM_FILES = [
|
||||||
|
"AGENTS.md",
|
||||||
|
"CLAUDE.md",
|
||||||
|
"CONTEXT.md", // deprecated
|
||||||
|
]
|
||||||
|
export async function custom() {
|
||||||
|
const { cwd, root } = App.info().path
|
||||||
|
const found = []
|
||||||
|
for (const item of CUSTOM_FILES) {
|
||||||
|
const matches = await Filesystem.findUp(item, cwd, root)
|
||||||
|
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||||
|
}
|
||||||
|
found.push(
|
||||||
|
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
|
||||||
|
.text()
|
||||||
|
.catch(() => ""),
|
||||||
|
)
|
||||||
|
found.push(
|
||||||
|
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||||
|
.text()
|
||||||
|
.catch(() => ""),
|
||||||
|
)
|
||||||
|
return Promise.all(found).then((result) => result.filter(Boolean))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarize(providerID: string) {
|
||||||
|
switch (providerID) {
|
||||||
|
case "anthropic":
|
||||||
|
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE]
|
||||||
|
default:
|
||||||
|
return [PROMPT_SUMMARIZE]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function title(providerID: string) {
|
||||||
|
switch (providerID) {
|
||||||
|
case "anthropic":
|
||||||
|
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE]
|
||||||
|
default:
|
||||||
|
return [PROMPT_TITLE]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { Bus } from "../bus"
|
import { Bus } from "../bus"
|
||||||
|
import { Installation } from "../installation"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
@@ -19,10 +20,11 @@ export namespace Share {
|
|||||||
export async function sync(key: string, content: any) {
|
export async function sync(key: string, content: any) {
|
||||||
const [root, ...splits] = key.split("/")
|
const [root, ...splits] = key.split("/")
|
||||||
if (root !== "session") return
|
if (root !== "session") return
|
||||||
const [, sessionID] = splits
|
const [sub, sessionID] = splits
|
||||||
const session = await Session.get(sessionID)
|
if (sub === "share") return
|
||||||
if (!session.share) return
|
const share = await Session.getShare(sessionID).catch(() => {})
|
||||||
const { secret } = session.share
|
if (!share) return
|
||||||
|
const { secret } = share
|
||||||
pending.set(key, content)
|
pending.set(key, content)
|
||||||
queue = queue
|
queue = queue
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -55,7 +57,10 @@ export namespace Share {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const URL =
|
export const URL =
|
||||||
process.env["OPENCODE_API"] ?? "https://api.dev.opencode.ai"
|
process.env["OPENCODE_API"] ??
|
||||||
|
(Installation.isSnapshot() || Installation.isDev()
|
||||||
|
? "https://api.dev.opencode.ai"
|
||||||
|
: "https://api.opencode.ai")
|
||||||
|
|
||||||
export async function create(sessionID: string) {
|
export async function create(sessionID: string) {
|
||||||
return fetch(`${URL}/share_create`, {
|
return fetch(`${URL}/share_create`, {
|
||||||
@@ -65,4 +70,11 @@ export namespace Share {
|
|||||||
.then((x) => x.json())
|
.then((x) => x.json())
|
||||||
.then((x) => x as { url: string; secret: string })
|
.then((x) => x as { url: string; secret: string })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function remove(id: string) {
|
||||||
|
return fetch(`${URL}/share_delete`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ id }),
|
||||||
|
}).then((x) => x.json())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,15 @@ export namespace Storage {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const locks = new Map<string, Promise<void>>()
|
export async function remove(key: string) {
|
||||||
|
const target = path.join(state().dir, key + ".json")
|
||||||
|
await fs.unlink(target).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeDir(key: string) {
|
||||||
|
const target = path.join(state().dir, key)
|
||||||
|
await fs.rm(target, { recursive: true, force: true }).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
export async function readJSON<T>(key: string) {
|
export async function readJSON<T>(key: string) {
|
||||||
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
|
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
|||||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||||
|
|
||||||
export const BashTool = Tool.define({
|
export const BashTool = Tool.define({
|
||||||
id: "opencode.bash",
|
id: "bash",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
command: z.string().describe("The command to execute"),
|
command: z.string().describe("The command to execute"),
|
||||||
@@ -35,7 +35,7 @@ export const BashTool = Tool.define({
|
|||||||
.min(0)
|
.min(0)
|
||||||
.max(MAX_TIMEOUT)
|
.max(MAX_TIMEOUT)
|
||||||
.describe("Optional timeout in milliseconds")
|
.describe("Optional timeout in milliseconds")
|
||||||
.nullable(),
|
.optional(),
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@@ -63,10 +63,18 @@ export const BashTool = Tool.define({
|
|||||||
metadata: {
|
metadata: {
|
||||||
stderr,
|
stderr,
|
||||||
stdout,
|
stdout,
|
||||||
|
exit: process.exitCode,
|
||||||
description: params.description,
|
description: params.description,
|
||||||
title: params.command,
|
title: params.command,
|
||||||
},
|
},
|
||||||
output: stdout.replaceAll(/\x1b\[[0-9;]*m/g, ""),
|
output: [
|
||||||
|
`<stdout>`,
|
||||||
|
stdout ?? "",
|
||||||
|
`</stdout>`,
|
||||||
|
`<stderr>`,
|
||||||
|
stderr ?? "",
|
||||||
|
`</stderr>`,
|
||||||
|
].join("\n"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Usage notes:
|
|||||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||||
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
||||||
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
|
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
|
||||||
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all OpenCode users have pre-installed.
|
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
|
||||||
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
||||||
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
||||||
<good-example>
|
<good-example>
|
||||||
@@ -60,9 +60,9 @@ When the user asks you to create a new git commit, follow these steps carefully:
|
|||||||
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
||||||
- Add relevant untracked files to the staging area.
|
- Add relevant untracked files to the staging area.
|
||||||
- Create the commit with a message ending with:
|
- Create the commit with a message ending with:
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
|
|
||||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||||
- Run git status to make sure the commit succeeded.
|
- Run git status to make sure the commit succeeded.
|
||||||
|
|
||||||
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
||||||
@@ -81,9 +81,9 @@ Important notes:
|
|||||||
git commit -m "$(cat <<'EOF'
|
git commit -m "$(cat <<'EOF'
|
||||||
Commit message here.
|
Commit message here.
|
||||||
|
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
|
|
||||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
</example>
|
</example>
|
||||||
@@ -128,7 +128,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
|||||||
## Test plan
|
## Test plan
|
||||||
[Checklist of TODOs for testing the pull request...]
|
[Checklist of TODOs for testing the pull request...]
|
||||||
|
|
||||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
🤖 Generated with [opencode](https://opencode.ai)
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
</example>
|
</example>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import DESCRIPTION from "./edit.txt"
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
export const EditTool = Tool.define({
|
export const EditTool = Tool.define({
|
||||||
id: "opencode.edit",
|
id: "edit",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||||
@@ -21,8 +21,8 @@ export const EditTool = Tool.define({
|
|||||||
),
|
),
|
||||||
replaceAll: z
|
replaceAll: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe("Replace all occurences of old_string (default false)"),
|
.describe("Replace all occurrences of old_string (default false)"),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
if (!params.filePath) {
|
if (!params.filePath) {
|
||||||
@@ -35,7 +35,7 @@ export const EditTool = Tool.define({
|
|||||||
: path.join(app.path.cwd, params.filePath)
|
: path.join(app.path.cwd, params.filePath)
|
||||||
|
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
id: "opencode.edit",
|
id: "edit",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
title: "Edit this file: " + filepath,
|
title: "Edit this file: " + filepath,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -87,7 +87,9 @@ export const EditTool = Tool.define({
|
|||||||
await file.write(contentNew)
|
await file.write(contentNew)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const diff = createTwoFilesPatch(filepath, filepath, contentOld, contentNew)
|
const diff = trimDiff(
|
||||||
|
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
|
||||||
|
)
|
||||||
|
|
||||||
FileTimes.read(ctx.sessionID, filepath)
|
FileTimes.read(ctx.sessionID, filepath)
|
||||||
|
|
||||||
@@ -113,3 +115,39 @@ export const EditTool = Tool.define({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function trimDiff(diff: string): string {
|
||||||
|
const lines = diff.split("\n")
|
||||||
|
const contentLines = lines.filter(
|
||||||
|
(line) =>
|
||||||
|
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
|
||||||
|
!line.startsWith("---") &&
|
||||||
|
!line.startsWith("+++"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (contentLines.length === 0) return diff
|
||||||
|
|
||||||
|
let min = Infinity
|
||||||
|
for (const line of contentLines) {
|
||||||
|
const content = line.slice(1)
|
||||||
|
if (content.trim().length > 0) {
|
||||||
|
const match = content.match(/^(\s*)/)
|
||||||
|
if (match) min = Math.min(min, match[1].length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (min === Infinity || min === 0) return diff
|
||||||
|
const trimmedLines = lines.map((line) => {
|
||||||
|
if (
|
||||||
|
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
|
||||||
|
!line.startsWith("---") &&
|
||||||
|
!line.startsWith("+++")
|
||||||
|
) {
|
||||||
|
const prefix = line[0]
|
||||||
|
const content = line.slice(1)
|
||||||
|
return prefix + content.slice(min)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
})
|
||||||
|
|
||||||
|
return trimmedLines.join("\n")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import path from "path"
|
|||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import DESCRIPTION from "./glob.txt"
|
import DESCRIPTION from "./glob.txt"
|
||||||
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
|
||||||
export const GlobTool = Tool.define({
|
export const GlobTool = Tool.define({
|
||||||
id: "opencode.glob",
|
id: "glob",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
pattern: z.string().describe("The glob pattern to match files against"),
|
pattern: z.string().describe("The glob pattern to match files against"),
|
||||||
path: z
|
path: z
|
||||||
.string()
|
.string()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
|
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
|
||||||
),
|
),
|
||||||
@@ -24,10 +25,12 @@ export const GlobTool = Tool.define({
|
|||||||
: path.resolve(app.path.cwd, search)
|
: path.resolve(app.path.cwd, search)
|
||||||
|
|
||||||
const limit = 100
|
const limit = 100
|
||||||
const glob = new Bun.Glob(params.pattern)
|
|
||||||
const files = []
|
const files = []
|
||||||
let truncated = false
|
let truncated = false
|
||||||
for await (const file of glob.scan({ cwd: search })) {
|
for (const file of await Ripgrep.files({
|
||||||
|
cwd: search,
|
||||||
|
glob: params.pattern,
|
||||||
|
})) {
|
||||||
if (files.length >= limit) {
|
if (files.length >= limit) {
|
||||||
truncated = true
|
truncated = true
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Ripgrep } from "../external/ripgrep"
|
|||||||
import DESCRIPTION from "./grep.txt"
|
import DESCRIPTION from "./grep.txt"
|
||||||
|
|
||||||
export const GrepTool = Tool.define({
|
export const GrepTool = Tool.define({
|
||||||
id: "opencode.grep",
|
id: "grep",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
pattern: z
|
pattern: z
|
||||||
@@ -14,13 +14,13 @@ export const GrepTool = Tool.define({
|
|||||||
.describe("The regex pattern to search for in file contents"),
|
.describe("The regex pattern to search for in file contents"),
|
||||||
path: z
|
path: z
|
||||||
.string()
|
.string()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"The directory to search in. Defaults to the current working directory.",
|
"The directory to search in. Defaults to the current working directory.",
|
||||||
),
|
),
|
||||||
include: z
|
include: z
|
||||||
.string()
|
.string()
|
||||||
.nullable()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { App } from "../app/app"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import DESCRIPTION from "./ls.txt"
|
import DESCRIPTION from "./ls.txt"
|
||||||
|
|
||||||
const IGNORE_PATTERNS = [
|
export const IGNORE_PATTERNS = [
|
||||||
"node_modules/",
|
"node_modules/",
|
||||||
"__pycache__/",
|
"__pycache__/",
|
||||||
".git/",
|
".git/",
|
||||||
@@ -18,8 +18,10 @@ const IGNORE_PATTERNS = [
|
|||||||
".vscode/",
|
".vscode/",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const LIMIT = 100
|
||||||
|
|
||||||
export const ListTool = Tool.define({
|
export const ListTool = Tool.define({
|
||||||
id: "opencode.list",
|
id: "list",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
path: z
|
path: z
|
||||||
@@ -27,11 +29,11 @@ export const ListTool = Tool.define({
|
|||||||
.describe(
|
.describe(
|
||||||
"The absolute path to the directory to list (must be absolute, not relative)",
|
"The absolute path to the directory to list (must be absolute, not relative)",
|
||||||
)
|
)
|
||||||
.nullable(),
|
.optional(),
|
||||||
ignore: z
|
ignore: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.describe("List of glob patterns to ignore")
|
.describe("List of glob patterns to ignore")
|
||||||
.nullable(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params) {
|
async execute(params) {
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
@@ -40,13 +42,12 @@ export const ListTool = Tool.define({
|
|||||||
const glob = new Bun.Glob("**/*")
|
const glob = new Bun.Glob("**/*")
|
||||||
const files = []
|
const files = []
|
||||||
|
|
||||||
for await (const file of glob.scan({ cwd: searchPath })) {
|
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
|
||||||
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
|
if (IGNORE_PATTERNS.some((p) => file.includes(p))) continue
|
||||||
continue
|
|
||||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
||||||
continue
|
continue
|
||||||
files.push(file)
|
files.push(file)
|
||||||
if (files.length >= 1000) break
|
if (files.length >= LIMIT) break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build directory structure
|
// Build directory structure
|
||||||
@@ -100,7 +101,7 @@ export const ListTool = Tool.define({
|
|||||||
return {
|
return {
|
||||||
metadata: {
|
metadata: {
|
||||||
count: files.length,
|
count: files.length,
|
||||||
truncated: files.length >= 1000,
|
truncated: files.length >= LIMIT,
|
||||||
title: path.relative(app.path.root, searchPath),
|
title: path.relative(app.path.root, searchPath),
|
||||||
},
|
},
|
||||||
output,
|
output,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
|||||||
import DESCRIPTION from "./lsp-diagnostics.txt"
|
import DESCRIPTION from "./lsp-diagnostics.txt"
|
||||||
|
|
||||||
export const LspDiagnosticTool = Tool.define({
|
export const LspDiagnosticTool = Tool.define({
|
||||||
id: "opencode.lsp_diagnostics",
|
id: "lsp_diagnostics",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
path: z.string().describe("The path to the file to get diagnostics."),
|
path: z.string().describe("The path to the file to get diagnostics."),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
|||||||
import DESCRIPTION from "./lsp-hover.txt"
|
import DESCRIPTION from "./lsp-hover.txt"
|
||||||
|
|
||||||
export const LspHoverTool = Tool.define({
|
export const LspHoverTool = Tool.define({
|
||||||
id: "opencode.lsp_hover",
|
id: "lsp_hover",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
file: z.string().describe("The path to the file to get diagnostics."),
|
file: z.string().describe("The path to the file to get diagnostics."),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from "path"
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
export const MultiEditTool = Tool.define({
|
export const MultiEditTool = Tool.define({
|
||||||
id: "opencode.multiedit",
|
id: "multiedit",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ To make multiple file edits, provide the following:
|
|||||||
2. edits: An array of edit operations to perform, where each edit contains:
|
2. edits: An array of edit operations to perform, where each edit contains:
|
||||||
- old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)
|
- old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)
|
||||||
- new_string: The edited text to replace the old_string
|
- new_string: The edited text to replace the old_string
|
||||||
- replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.
|
- replace_all: Replace all occurrences of old_string. This parameter is optional and defaults to false.
|
||||||
|
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
- All edits are applied in sequence, in the order they are provided
|
- All edits are applied in sequence, in the order they are provided
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import * as fs from "fs/promises"
|
|||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { FileTimes } from "./util/file-times"
|
import { FileTimes } from "./util/file-times"
|
||||||
import DESCRIPTION from "./patch.txt"
|
import DESCRIPTION from "./patch.txt"
|
||||||
import { App } from "../app/app"
|
|
||||||
|
|
||||||
const PatchParams = z.object({
|
const PatchParams = z.object({
|
||||||
patchText: z
|
patchText: z
|
||||||
@@ -233,7 +232,7 @@ async function applyCommit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PatchTool = Tool.define({
|
export const PatchTool = Tool.define({
|
||||||
id: "opencode.patch",
|
id: "patch",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: PatchParams,
|
parameters: PatchParams,
|
||||||
execute: async (params, ctx) => {
|
execute: async (params, ctx) => {
|
||||||
|
|||||||
@@ -12,18 +12,18 @@ const DEFAULT_READ_LIMIT = 2000
|
|||||||
const MAX_LINE_LENGTH = 2000
|
const MAX_LINE_LENGTH = 2000
|
||||||
|
|
||||||
export const ReadTool = Tool.define({
|
export const ReadTool = Tool.define({
|
||||||
id: "opencode.read",
|
id: "read",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z.string().describe("The path to the file to read"),
|
filePath: z.string().describe("The path to the file to read"),
|
||||||
offset: z
|
offset: z
|
||||||
.number()
|
.number()
|
||||||
.describe("The line number to start reading from (0-based)")
|
.describe("The line number to start reading from (0-based)")
|
||||||
.nullable(),
|
.optional(),
|
||||||
limit: z
|
limit: z
|
||||||
.number()
|
.number()
|
||||||
.describe("The number of lines to read (defaults to 2000)")
|
.describe("The number of lines to read (defaults to 2000)")
|
||||||
.nullable(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
let filePath = params.filePath
|
let filePath = params.filePath
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Tool } from "./tool"
|
||||||
|
import DESCRIPTION from "./task.txt"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Session } from "../session"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { Message } from "../session/message"
|
||||||
|
|
||||||
|
export const TaskTool = Tool.define({
|
||||||
|
id: "task",
|
||||||
|
description: DESCRIPTION,
|
||||||
|
parameters: z.object({
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.describe("A short (3-5 words) description of the task"),
|
||||||
|
prompt: z.string().describe("The task for the agent to perform"),
|
||||||
|
}),
|
||||||
|
async execute(params, ctx) {
|
||||||
|
const session = await Session.create(ctx.sessionID)
|
||||||
|
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
||||||
|
const metadata = msg.metadata.assistant!
|
||||||
|
|
||||||
|
function summary(input: Message.Info) {
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (const part of input.parts) {
|
||||||
|
if (part.type === "tool-invocation") {
|
||||||
|
result.push({
|
||||||
|
toolInvocation: part.toolInvocation,
|
||||||
|
metadata: input.metadata.tool[part.toolInvocation.toolCallId],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
|
||||||
|
if (evt.properties.info.metadata.sessionID !== session.id) return
|
||||||
|
ctx.metadata({
|
||||||
|
title: params.description,
|
||||||
|
summary: summary(evt.properties.info),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.abort.addEventListener("abort", () => {
|
||||||
|
Session.abort(session.id)
|
||||||
|
})
|
||||||
|
const result = await Session.chat({
|
||||||
|
sessionID: session.id,
|
||||||
|
modelID: metadata.modelID,
|
||||||
|
providerID: metadata.providerID,
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: params.prompt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
unsub()
|
||||||
|
return {
|
||||||
|
metadata: {
|
||||||
|
title: params.description,
|
||||||
|
summary: summary(result),
|
||||||
|
},
|
||||||
|
output: result.parts.findLast((x) => x.type === "text")!.text,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -23,7 +23,7 @@ const state = App.state("todo-tool", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const TodoWriteTool = Tool.define({
|
export const TodoWriteTool = Tool.define({
|
||||||
id: "opencode.todowrite",
|
id: "todowrite",
|
||||||
description: DESCRIPTION_WRITE,
|
description: DESCRIPTION_WRITE,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
todos: z.array(TodoInfo).describe("The updated todo list"),
|
todos: z.array(TodoInfo).describe("The updated todo list"),
|
||||||
@@ -42,7 +42,7 @@ export const TodoWriteTool = Tool.define({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const TodoReadTool = Tool.define({
|
export const TodoReadTool = Tool.define({
|
||||||
id: "opencode.todoread",
|
id: "todoread",
|
||||||
description: "Use this tool to read your todo list",
|
description: "Use this tool to read your todo list",
|
||||||
parameters: z.object({}),
|
parameters: z.object({}),
|
||||||
async execute(_params, opts) {
|
async execute(_params, opts) {
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ export namespace Tool {
|
|||||||
title: string
|
title: string
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
export type Context = {
|
export type Context<M extends Metadata = Metadata> = {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
abort: AbortSignal
|
abort: AbortSignal
|
||||||
|
metadata(meta: M): void
|
||||||
}
|
}
|
||||||
export interface Info<
|
export interface Info<
|
||||||
Parameters extends StandardSchemaV1 = StandardSchemaV1,
|
Parameters extends StandardSchemaV1 = StandardSchemaV1,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
|||||||
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||||
|
|
||||||
export const WebFetchTool = Tool.define({
|
export const WebFetchTool = Tool.define({
|
||||||
id: "opencode.webfetch",
|
id: "webfetch",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
url: z.string().describe("The URL to fetch content from"),
|
url: z.string().describe("The URL to fetch content from"),
|
||||||
@@ -22,7 +22,7 @@ export const WebFetchTool = Tool.define({
|
|||||||
.min(0)
|
.min(0)
|
||||||
.max(MAX_TIMEOUT / 1000)
|
.max(MAX_TIMEOUT / 1000)
|
||||||
.describe("Optional timeout in seconds (max 120)")
|
.describe("Optional timeout in seconds (max 120)")
|
||||||
.nullable(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
// Validate URL
|
// Validate URL
|
||||||
@@ -76,7 +76,7 @@ export const WebFetchTool = Tool.define({
|
|||||||
switch (params.format) {
|
switch (params.format) {
|
||||||
case "text":
|
case "text":
|
||||||
if (contentType.includes("text/html")) {
|
if (contentType.includes("text/html")) {
|
||||||
const text = extractTextFromHTML(content)
|
const text = await extractTextFromHTML(content)
|
||||||
return {
|
return {
|
||||||
output: text,
|
output: text,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -127,10 +127,45 @@ export const WebFetchTool = Tool.define({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function extractTextFromHTML(html: string): string {
|
async function extractTextFromHTML(html: string) {
|
||||||
const doc = new DOMParser().parseFromString(html, "text/html")
|
let text = ""
|
||||||
const text = doc.body.textContent || doc.body.innerText || ""
|
let skipContent = false
|
||||||
return text.replace(/\s+/g, " ").trim()
|
|
||||||
|
const rewriter = new HTMLRewriter()
|
||||||
|
.on("script, style, noscript, iframe, object, embed", {
|
||||||
|
element() {
|
||||||
|
skipContent = true
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
// Skip text content inside these elements
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.on("*", {
|
||||||
|
element(element) {
|
||||||
|
// Reset skip flag when entering other elements
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
"script",
|
||||||
|
"style",
|
||||||
|
"noscript",
|
||||||
|
"iframe",
|
||||||
|
"object",
|
||||||
|
"embed",
|
||||||
|
].includes(element.tagName)
|
||||||
|
) {
|
||||||
|
skipContent = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text(input) {
|
||||||
|
if (!skipContent) {
|
||||||
|
text += input.text
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.transform(new Response(html))
|
||||||
|
|
||||||
|
await rewriter.text()
|
||||||
|
return text.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertHTMLToMarkdown(html: string): string {
|
function convertHTMLToMarkdown(html: string): string {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
- Allows OpenCode to search the web and use the results to inform responses
|
- Allows opencode to search the web and use the results to inform responses
|
||||||
- Provides up-to-date information for current events and recent data
|
- Provides up-to-date information for current events and recent data
|
||||||
- Returns search result information formatted as search result blocks
|
- Returns search result information formatted as search result blocks
|
||||||
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import DESCRIPTION from "./write.txt"
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
export const WriteTool = Tool.define({
|
export const WriteTool = Tool.define({
|
||||||
id: "opencode.write",
|
id: "write",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
filePath: z
|
filePath: z
|
||||||
@@ -29,7 +29,7 @@ export const WriteTool = Tool.define({
|
|||||||
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
||||||
|
|
||||||
await Permission.ask({
|
await Permission.ask({
|
||||||
id: "opencode.write",
|
id: "write",
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
title: exists
|
title: exists
|
||||||
? "Overwrite this file: " + filepath
|
? "Overwrite this file: " + filepath
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z, type ZodSchema } from "zod"
|
import { z, type ZodSchema } from "zod"
|
||||||
import { Log } from "./log"
|
// import { Log } from "./log"
|
||||||
|
|
||||||
const log = Log.create()
|
// const log = Log.create()
|
||||||
|
|
||||||
export abstract class NamedError extends Error {
|
export abstract class NamedError extends Error {
|
||||||
abstract schema(): ZodSchema
|
abstract schema(): ZodSchema
|
||||||
@@ -11,15 +11,16 @@ export abstract class NamedError extends Error {
|
|||||||
name: Name,
|
name: Name,
|
||||||
data: Data,
|
data: Data,
|
||||||
) {
|
) {
|
||||||
const result = class extends NamedError {
|
const schema = z
|
||||||
public static readonly Schema = z
|
|
||||||
.object({
|
.object({
|
||||||
name: z.literal(name),
|
name: z.literal(name),
|
||||||
data: data,
|
data,
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: name,
|
ref: name,
|
||||||
})
|
})
|
||||||
|
const result = class extends NamedError {
|
||||||
|
public static readonly Schema = schema
|
||||||
|
|
||||||
public readonly name = name as Name
|
public readonly name = name as Name
|
||||||
|
|
||||||
@@ -29,7 +30,6 @@ export abstract class NamedError extends Error {
|
|||||||
) {
|
) {
|
||||||
super(name, options)
|
super(name, options)
|
||||||
this.name = name
|
this.name = name
|
||||||
log.error(name, this.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static isInstance(input: any): input is InstanceType<typeof result> {
|
static isInstance(input: any): input is InstanceType<typeof result> {
|
||||||
@@ -37,7 +37,7 @@ export abstract class NamedError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schema() {
|
schema() {
|
||||||
return result.Schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
toObject() {
|
toObject() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
export namespace Log {
|
export namespace Log {
|
||||||
export const Default = create()
|
export const Default = create({ service: "default" })
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
print: boolean
|
print: boolean
|
||||||
@@ -19,7 +19,7 @@ export namespace Log {
|
|||||||
await fs.mkdir(dir, { recursive: true })
|
await fs.mkdir(dir, { recursive: true })
|
||||||
cleanup(dir)
|
cleanup(dir)
|
||||||
if (options.print) return
|
if (options.print) return
|
||||||
logpath = path.join(dir, new Date().toISOString().split(".")[0] + ".log")
|
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
||||||
const logfile = Bun.file(logpath)
|
const logfile = Bun.file(logpath)
|
||||||
await fs.truncate(logpath).catch(() => {})
|
await fs.truncate(logpath).catch(() => {})
|
||||||
const writer = logfile.writer()
|
const writer = logfile.writer()
|
||||||
@@ -45,6 +45,7 @@ export namespace Log {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let last = Date.now()
|
||||||
export function create(tags?: Record<string, any>) {
|
export function create(tags?: Record<string, any>) {
|
||||||
tags = tags || {}
|
tags = tags || {}
|
||||||
|
|
||||||
@@ -56,20 +57,24 @@ export namespace Log {
|
|||||||
.filter(([_, value]) => value !== undefined && value !== null)
|
.filter(([_, value]) => value !== undefined && value !== null)
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
.join(" ")
|
.join(" ")
|
||||||
|
const next = new Date()
|
||||||
|
const diff = next.getTime() - last
|
||||||
|
last = next.getTime()
|
||||||
return (
|
return (
|
||||||
[new Date().toISOString(), prefix, message].filter(Boolean).join(" ") +
|
[next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message]
|
||||||
"\n"
|
.filter(Boolean)
|
||||||
|
.join(" ") + "\n"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
info(message?: any, extra?: Record<string, any>) {
|
info(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write(build(message, extra))
|
process.stderr.write("INFO " + build(message, extra))
|
||||||
},
|
},
|
||||||
error(message?: any, extra?: Record<string, any>) {
|
error(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write(build(message, extra))
|
process.stderr.write("ERROR " + build(message, extra))
|
||||||
},
|
},
|
||||||
warn(message?: any, extra?: Record<string, any>) {
|
warn(message?: any, extra?: Record<string, any>) {
|
||||||
process.stderr.write(build(message, extra))
|
process.stderr.write("WARN " + build(message, extra))
|
||||||
},
|
},
|
||||||
tag(key: string, value: string) {
|
tag(key: string, value: string) {
|
||||||
if (tags) tags[key] = value
|
if (tags) tags[key] = value
|
||||||
@@ -78,6 +83,23 @@ export namespace Log {
|
|||||||
clone() {
|
clone() {
|
||||||
return Log.create({ ...tags })
|
return Log.create({ ...tags })
|
||||||
},
|
},
|
||||||
|
time(message: string, extra?: Record<string, any>) {
|
||||||
|
const now = Date.now()
|
||||||
|
result.info(message, { status: "started", ...extra })
|
||||||
|
function stop() {
|
||||||
|
result.info(message, {
|
||||||
|
status: "completed",
|
||||||
|
duration: Date.now() - now,
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stop,
|
||||||
|
[Symbol.dispose]() {
|
||||||
|
stop()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -3,25 +3,37 @@ import { App } from "../../src/app/app"
|
|||||||
import { GlobTool } from "../../src/tool/glob"
|
import { GlobTool } from "../../src/tool/glob"
|
||||||
import { ListTool } from "../../src/tool/ls"
|
import { ListTool } from "../../src/tool/ls"
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
sessionID: "test",
|
||||||
|
messageID: "",
|
||||||
|
abort: AbortSignal.any([]),
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
describe("tool.glob", () => {
|
describe("tool.glob", () => {
|
||||||
test("truncate", async () => {
|
test("truncate", async () => {
|
||||||
await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
let result = await GlobTool.execute(
|
let result = await GlobTool.execute(
|
||||||
{ pattern: "./node_modules/**/*" },
|
{
|
||||||
{ sessionID: "test" },
|
pattern: "../../node_modules/**/*",
|
||||||
|
path: undefined,
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
)
|
)
|
||||||
expect(result.metadata.truncated).toBe(true)
|
expect(result.metadata.truncated).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
await App.provide({ cwd: process.cwd(), version: "test" }, async () => {
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
let result = await GlobTool.execute(
|
let result = await GlobTool.execute(
|
||||||
{ pattern: "*.json" },
|
{
|
||||||
{ sessionID: "test" },
|
pattern: "*.json",
|
||||||
|
path: undefined,
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
)
|
)
|
||||||
expect(result.metadata).toMatchObject({
|
expect(result.metadata).toMatchObject({
|
||||||
truncated: false,
|
truncated: false,
|
||||||
count: 2,
|
count: 3,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -29,15 +41,12 @@ describe("tool.glob", () => {
|
|||||||
|
|
||||||
describe("tool.ls", () => {
|
describe("tool.ls", () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const result = await App.provide(
|
const result = await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
{ cwd: process.cwd(), version: "test" },
|
|
||||||
async () => {
|
|
||||||
return await ListTool.execute(
|
return await ListTool.execute(
|
||||||
{ path: "./example" },
|
{ path: "./example", ignore: [".git"] },
|
||||||
{ sessionID: "test" },
|
ctx,
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
})
|
||||||
expect(result.output).toMatchSnapshot()
|
expect(result.output).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
opencode-test
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
time=2025-05-30T22:01:45.386-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-30T22:01:45.391-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-30T22:01:50.683-04:00 level=INFO msg="TUI exited" result="{width:98 height:57 currentPage:chat previousPage: pages:map[chat:0xc00013b450] loadedPages:map[chat:true] status:{app:0xc0002e05b0 queue:[] width:98 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002e05b0 showPermissions:false permissions:0xc000159408 showHelp:false help:0xc0006822d0 showQuit:true quit:0xc00024b479 showSessionDialog:false sessionDialog:0xc0001f0240 showCommandDialog:false commandDialog:0xc0003cbba0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0001f45a0 showInitDialog:false initDialog:{width:98 height:57 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0001f0480 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0001f04c0}"
|
|
||||||
time=2025-05-30T22:13:24.046-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-30T22:13:24.051-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-30T22:13:25.991-04:00 level=INFO msg="TUI exited" result="{width:199 height:57 currentPage:chat previousPage: pages:map[chat:0xc00025f950] loadedPages:map[chat:true] status:{app:0xc0000ca230 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0000ca230 showPermissions:false permissions:0xc00029f908 showHelp:false help:0xc00045d9b0 showQuit:true quit:0xc0005a0be9 showSessionDialog:false sessionDialog:0xc00012e3c0 showCommandDialog:false commandDialog:0xc0004379e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0002f2e60 showInitDialog:false initDialog:{width:199 height:57 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00012e600 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00012e640}"
|
|
||||||
time=2025-05-31T16:00:29.137-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T16:00:29.141-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T16:00:36.530-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T16:00:36.531-04:00 level=INFO msg="TUI exited" result="{width:106 height:54 currentPage:chat previousPage: pages:map[chat:0xc000157450] loadedPages:map[chat:true] status:{app:0xc00020c5b0 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc00020c5b0 showPermissions:false permissions:0xc000175408 showHelp:false help:0xc00070c270 showQuit:true quit:0xc000299979 showSessionDialog:false sessionDialog:0xc0001f02c0 showCommandDialog:false commandDialog:0xc0003cbba0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc00021a5a0 showInitDialog:false initDialog:{width:106 height:54 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0001f0500 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0001f0540}"
|
|
||||||
time=2025-05-31T16:06:20.089-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T16:06:20.094-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T16:06:20.095-04:00 level=ERROR msg="Failed to subscribe to events" error="Get \"http://localhost:16713/event\": dial tcp [::1]:16713: connect: connection refused"
|
|
||||||
time=2025-05-31T17:54:04.009-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:54:04.014-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T17:54:06.337-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc0002332c0] loadedPages:map[chat:true] status:{app:0xc0002b1810 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002b1810 showPermissions:false permissions:0xc000267408 showHelp:false help:0xc00048dbc0 showQuit:true quit:0xc0004a2719 showSessionDialog:false sessionDialog:0xc000319ec0 showCommandDialog:false commandDialog:0xc000387980 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0000c6960 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc0000ac480 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac4c0}"
|
|
||||||
time=2025-05-31T17:54:17.103-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:54:17.108-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T17:54:18.391-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T17:54:18.392-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc00042a960] loadedPages:map[chat:true] status:{app:0xc000163ce0 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000163ce0 showPermissions:false permissions:0xc0001df408 showHelp:false help:0xc0005198f0 showQuit:true quit:0xc0003a5ef9 showSessionDialog:false sessionDialog:0xc000323840 showCommandDialog:false commandDialog:0xc00043b0e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0004028c0 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc000323a80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000323ac0}"
|
|
||||||
time=2025-05-31T17:59:54.360-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:59:54.364-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T17:59:55.814-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T17:59:55.815-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc0002787d0] loadedPages:map[chat:true] status:{app:0xc0003fed90 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0003fed90 showPermissions:false permissions:0xc0002b1908 showHelp:false help:0xc000126150 showQuit:true quit:0xc00011d439 showSessionDialog:false sessionDialog:0xc00025e380 showCommandDialog:false commandDialog:0xc00047fc00 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc0002f6d20 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0001b2c88 showThemeDialog:false themeDialog:0xc00025e5c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00025e600}"
|
|
||||||
time=2025-05-31T17:59:56.746-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T17:59:56.750-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T18:00:10.757-04:00 level=INFO msg="TUI exited" result="{width:211 height:54 currentPage:chat previousPage: pages:map[chat:0xc00053b090] loadedPages:map[chat:true] status:{app:0xc000300cb0 queue:[] width:211 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000300cb0 showPermissions:false permissions:0xc0002c5408 showHelp:false help:0xc000682f90 showQuit:true quit:0xc0006134d9 showSessionDialog:false sessionDialog:0xc00031f980 showCommandDialog:false commandDialog:0xc0003d9520 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc000395220 showInitDialog:false initDialog:{width:211 height:54 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00031fbc0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00031fc00}"
|
|
||||||
time=2025-05-31T18:35:42.289-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T18:35:42.294-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T18:36:54.773-04:00 level=INFO msg="TUI exited" result="{width:106 height:25 currentPage:chat previousPage: pages:map[chat:0xc00012f0e0] loadedPages:map[chat:true] status:{app:0xc0002aa070 queue:[] width:106 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002aa070 showPermissions:false permissions:0xc000267408 showHelp:false help:0xc00041b8f0 showQuit:true quit:0xc000345ee9 showSessionDialog:false sessionDialog:0xc00032ba40 showCommandDialog:false commandDialog:0xc00043b300 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc000426f00 showInitDialog:false initDialog:{width:106 height:25 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc00032bc80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00032bcc0}"
|
|
||||||
time=2025-05-31T18:36:56.011-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T18:36:56.015-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T18:37:44.063-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T18:37:44.064-04:00 level=INFO msg="TUI exited" result="{width:211 height:54 currentPage:chat previousPage: pages:map[chat:0xc000420280] loadedPages:map[chat:true] status:{app:0xc0002d8000 queue:[] width:211 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002d8000 showPermissions:false permissions:0xc000271408 showHelp:false help:0xc00048da70 showQuit:true quit:0xc000390809 showSessionDialog:false sessionDialog:0xc000323b80 showCommandDialog:false commandDialog:0xc0003e5920 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a7a0} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a620}] showModelDialog:false modelDialog:0xc00025f9a0 showInitDialog:false initDialog:{width:211 height:54 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d6c88 showThemeDialog:false themeDialog:0xc000323dc0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000323e00}"
|
|
||||||
time=2025-05-31T20:32:32.443-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-05-31T20:32:32.448-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-05-31T20:33:09.783-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc00032c960] loadedPages:map[chat:true] status:{app:0xc000279420 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000279420 showPermissions:false permissions:0xc0001fb408 showHelp:false help:0xc000154150 showQuit:true quit:0xc000528849 showSessionDialog:false sessionDialog:0xc000309e40 showCommandDialog:false commandDialog:0xc0003a3800 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc0002e7cc0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac400 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac440}"
|
|
||||||
time=2025-06-01T14:37:36.423-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:37:36.427-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:38:19.951-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc00035b9f0] loadedPages:map[chat:true] status:{app:0xc000226d90 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000226d90 showPermissions:false permissions:0xc00027f908 showHelp:false help:0xc0005139e0 showQuit:true quit:0xc000510d49 showSessionDialog:false sessionDialog:0xc0001e84c0 showCommandDialog:false commandDialog:0xc00051a160 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc0002675e0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc0001e8700 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0001e8740}"
|
|
||||||
time=2025-06-01T14:38:50.886-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:38:50.891-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:38:53.495-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0005ac8c0] loadedPages:map[chat:true] status:{app:0xc0002796c0 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002796c0 showPermissions:false permissions:0xc00028b408 showHelp:false help:0xc000490d80 showQuit:true quit:0xc000582589 showSessionDialog:false sessionDialog:0xc0003359c0 showCommandDialog:false commandDialog:0xc00042d480 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc000389360 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000335c00 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000335c80}"
|
|
||||||
time=2025-06-01T14:39:49.852-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:39:49.856-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:39:57.071-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc000616f00] loadedPages:map[chat:true] status:{app:0xc000333490 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000333490 showPermissions:false permissions:0xc0004faa08 showHelp:false help:0xc000471140 showQuit:true quit:0xc000459299 showSessionDialog:false sessionDialog:0xc000352500 showCommandDialog:false commandDialog:0xc00041ed80 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc000515a40 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc000352740 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000352780}"
|
|
||||||
time=2025-06-01T14:40:21.954-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:40:21.958-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:41:29.195-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0002cc280] loadedPages:map[chat:true] status:{app:0xc0002e64d0 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002e64d0 showPermissions:false permissions:0xc00026f408 showHelp:false help:0xc00051c1b0 showQuit:true quit:0xc00051a819 showSessionDialog:false sessionDialog:0xc00030fec0 showCommandDialog:false commandDialog:0xc00042d760 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc0002ce1e0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac480 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac4c0}"
|
|
||||||
time=2025-06-01T14:58:27.272-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T14:58:27.276-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T14:58:59.711-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc000316280] loadedPages:map[chat:true] status:{app:0xc0002b5810 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002b5810 showPermissions:false permissions:0xc000269408 showHelp:false help:0xc000490e10 showQuit:true quit:0xc00047a929 showSessionDialog:false sessionDialog:0xc0000adb40 showCommandDialog:false commandDialog:0xc0003e59c0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6a800} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6a680}] showModelDialog:false modelDialog:0xc00024fd60 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000add80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000addc0}"
|
|
||||||
time=2025-06-01T15:02:54.453-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:02:54.458-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:02:56.136-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc000392ff0] loadedPages:map[chat:true] status:{app:0xc0001ecc40 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0001ecc40 showPermissions:false permissions:0xc000205408 showHelp:false help:0xc00051c0c0 showQuit:true quit:0xc0003b3f49 showSessionDialog:false sessionDialog:0xc000319980 showCommandDialog:false commandDialog:0xc00042d220 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0005c52c0 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000319bc0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000319c00}"
|
|
||||||
time=2025-06-01T15:02:57.053-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:02:57.057-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:02:58.135-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0004411d0] loadedPages:map[chat:true] status:{app:0xc00023ee70 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc00023ee70 showPermissions:false permissions:0xc000177408 showHelp:false help:0xc000520030 showQuit:true quit:0xc000314929 showSessionDialog:false sessionDialog:0xc000319d00 showCommandDialog:false commandDialog:0xc0003e5860 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0002c9a40 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000319f40 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac000}"
|
|
||||||
time=2025-06-01T15:15:13.582-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:15:13.587-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:15:19.009-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:15:19.010-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0001490e0] loadedPages:map[chat:true] status:{app:0xc0001efb90 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0001efb90 showPermissions:false permissions:0xc000167408 showHelp:false help:0xc00052c1b0 showQuit:true quit:0xc000254629 showSessionDialog:false sessionDialog:0xc00030fe80 showCommandDialog:false commandDialog:0xc0003a3420 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0000c6640 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac440 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac480}"
|
|
||||||
time=2025-06-01T15:15:20.678-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-01T15:15:20.683-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-01T15:15:23.252-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-01T15:15:23.253-04:00 level=INFO msg="TUI exited" result="{width:199 height:56 currentPage:chat previousPage: pages:map[chat:0xc0002c47d0] loadedPages:map[chat:true] status:{app:0xc0003363f0 queue:[] width:199 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0003363f0 showPermissions:false permissions:0xc0002f3408 showHelp:false help:0xc0007055f0 showQuit:true quit:0xc00041c9b9 showSessionDialog:false sessionDialog:0xc00033bd00 showCommandDialog:false commandDialog:0xc000437700 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0003d9c20 showInitDialog:false initDialog:{width:199 height:56 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00033bf40 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00034a140}"
|
|
||||||
time=2025-06-02T11:40:21.643-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T11:40:21.648-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T11:40:24.684-04:00 level=INFO msg="TUI exited" result="{width:347 height:89 currentPage:chat previousPage: pages:map[chat:0xc00020d180] loadedPages:map[chat:true] status:{app:0xc0002a8230 queue:[] width:347 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002a8230 showPermissions:false permissions:0xc000239408 showHelp:false help:0xc00051c1e0 showQuit:true quit:0xc000598909 showSessionDialog:false sessionDialog:0xc000309f00 showCommandDialog:false commandDialog:0xc0003a3660 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0000c4a00 showInitDialog:false initDialog:{width:347 height:89 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000aa4c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000aa500}"
|
|
||||||
time=2025-06-02T11:40:55.224-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T11:40:55.228-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T11:41:03.512-04:00 level=INFO msg="TUI exited" result="{width:347 height:89 currentPage:chat previousPage: pages:map[chat:0xc0001467d0] loadedPages:map[chat:true] status:{app:0xc0004feee0 queue:[] width:347 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0004feee0 showPermissions:false permissions:0xc000167408 showHelp:false help:0xc00059cd50 showQuit:true quit:0xc00038aaa9 showSessionDialog:false sessionDialog:0xc00030ff00 showCommandDialog:false commandDialog:0xc0003e5aa0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc00029be00 showInitDialog:false initDialog:{width:347 height:89 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000aa4c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000aa500}"
|
|
||||||
time=2025-06-02T11:41:05.131-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T11:41:05.136-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T11:41:06.417-04:00 level=INFO msg="TUI exited" result="{width:347 height:89 currentPage:chat previousPage: pages:map[chat:0xc0002527d0] loadedPages:map[chat:true] status:{app:0xc0002e0d90 queue:[] width:347 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0002e0d90 showPermissions:false permissions:0xc00027b408 showHelp:false help:0xc0004900c0 showQuit:true quit:0xc00047ae69 showSessionDialog:false sessionDialog:0xc000319f40 showCommandDialog:false commandDialog:0xc00042d880 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc000357e00 showInitDialog:false initDialog:{width:347 height:89 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000aa500 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000aa540}"
|
|
||||||
time=2025-06-02T19:36:04.879-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:36:04.883-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:36:07.068-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc000544b40] loadedPages:map[chat:true] status:{app:0xc000249b90 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000249b90 showPermissions:false permissions:0xc000207408 showHelp:false help:0xc00011a1e0 showQuit:true quit:0xc0003890b9 showSessionDialog:false sessionDialog:0xc000319f40 showCommandDialog:false commandDialog:0xc0003e5520 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc000547220 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac500 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac540}"
|
|
||||||
time=2025-06-02T19:44:20.524-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:44:20.529-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:45:43.720-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc0001f87d0] loadedPages:map[chat:true] status:{app:0xc000270cb0 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000270cb0 showPermissions:false permissions:0xc00022f408 showHelp:false help:0xc000490e70 showQuit:true quit:0xc000388ab9 showSessionDialog:false sessionDialog:0xc000319f00 showCommandDialog:false commandDialog:0xc0003e55e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc00030bd60 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc0000ac4c0 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc0000ac500}"
|
|
||||||
time=2025-06-02T19:45:47.456-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:45:47.462-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:46:50.039-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc00035b9f0] loadedPages:map[chat:true] status:{app:0xc0000ec230 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc0000ec230 showPermissions:false permissions:0xc0005e5408 showHelp:false help:0xc0005ad950 showQuit:true quit:0xc0005a0c09 showSessionDialog:false sessionDialog:0xc00012e440 showCommandDialog:false commandDialog:0xc0003c2160 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc0002c74a0 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc00013ac88 showThemeDialog:false themeDialog:0xc00012e680 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00012e6c0}"
|
|
||||||
time=2025-06-02T19:47:11.433-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:47:11.438-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:48:43.841-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:48:43.841-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:48:43.842-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc0001f9040] loadedPages:map[chat:true] status:{app:0xc000270070 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000270070 showPermissions:false permissions:0xc00022f408 showHelp:false help:0xc000122090 showQuit:true quit:0xc000447c19 showSessionDialog:false sessionDialog:0xc000323b40 showCommandDialog:false commandDialog:0xc0003cb540 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc00044d5e0 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc000323d80 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc000323dc0}"
|
|
||||||
time=2025-06-02T19:48:57.679-04:00 level=DEBUG msg="Set theme from config" theme=opencode
|
|
||||||
time=2025-06-02T19:48:57.685-04:00 level=INFO msg="Reading directory: /home/thdxr"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="Cancelling all subscriptions"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="subscription cancelled" name=status
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="All subscription goroutines completed successfully"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="TUI message channel closed"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="All goroutines cleaned up"
|
|
||||||
time=2025-06-02T19:50:56.190-04:00 level=INFO msg="TUI exited" result="{width:145 height:36 currentPage:chat previousPage: pages:map[chat:0xc000564be0] loadedPages:map[chat:true] status:{app:0xc000250d20 queue:[] width:145 messageTTL:4000000000 activeUntil:{wall:0 ext:0 loc:<nil>}} app:0xc000250d20 showPermissions:false permissions:0xc0004d6a08 showHelp:false help:0xc00061d5c0 showQuit:true quit:0xc0005578a9 showSessionDialog:false sessionDialog:0xc00032a640 showCommandDialog:false commandDialog:0xc0003e51e0 commands:[{ID:init Title:Initialize Project Description:Create/Update the CONTEXT.md memory file Handler:0xb6b340} {ID:compact_conversation Title:Compact Conversation Description:Summarize the current session to save tokens Handler:0xb6b1c0}] showModelDialog:false modelDialog:0xc000620aa0 showInitDialog:false initDialog:{width:145 height:36 selected:0 keys:{Tab:{keys:[] help:{Key: Desc:} disabled:false} Left:{keys:[] help:{Key: Desc:} disabled:false} Right:{keys:[] help:{Key: Desc:} disabled:false} Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false} Y:{keys:[] help:{Key: Desc:} disabled:false} N:{keys:[] help:{Key: Desc:} disabled:false}}} showFilepicker:false filepicker:0xc0000d4c88 showThemeDialog:false themeDialog:0xc00032a880 showMultiArgumentsDialog:false multiArgumentsDialog:{width:0 height:0 inputs:[] focusIndex:0 keys:{Enter:{keys:[] help:{Key: Desc:} disabled:false} Escape:{keys:[] help:{Key: Desc:} disabled:false}} commandID: content: argNames:[]} showToolsDialog:false toolsDialog:0xc00032a8c0}"
|
|
||||||
@@ -2,17 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
zone "github.com/lrstanley/bubblezone"
|
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/pubsub"
|
|
||||||
"github.com/sst/opencode/internal/tui"
|
"github.com/sst/opencode/internal/tui"
|
||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
@@ -20,18 +17,22 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
version := Version
|
||||||
|
if version != "dev" && !strings.HasPrefix(Version, "v") {
|
||||||
|
version = "v" + Version
|
||||||
|
}
|
||||||
|
|
||||||
url := os.Getenv("OPENCODE_SERVER")
|
url := os.Getenv("OPENCODE_SERVER")
|
||||||
httpClient, err := client.NewClientWithResponses(url)
|
|
||||||
|
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||||
|
var appInfo client.AppInfo
|
||||||
|
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create client", "error", err)
|
slog.Error("Failed to unmarshal app info", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
paths, err := httpClient.PostPathGetWithResponse(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
|
|
||||||
|
|
||||||
|
logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
|
||||||
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
|
||||||
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
err := os.MkdirAll(filepath.Dir(logfile), 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,24 +49,26 @@ func main() {
|
|||||||
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
httpClient, err := client.NewClientWithResponses(url)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create client", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Create main context for the application
|
// Create main context for the application
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
version := Version
|
app_, err := app.New(ctx, version, appInfo, httpClient)
|
||||||
if version != "dev" && !strings.HasPrefix(Version, "v") {
|
|
||||||
version = "v" + Version
|
|
||||||
}
|
|
||||||
app_, err := app.New(ctx, version, httpClient)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the TUI
|
|
||||||
zone.NewGlobal()
|
|
||||||
program := tea.NewProgram(
|
program := tea.NewProgram(
|
||||||
tui.NewModel(app_),
|
tui.NewModel(app_),
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
|
tea.WithKeyboardEnhancements(),
|
||||||
|
tea.WithMouseCellMotion(),
|
||||||
)
|
)
|
||||||
|
|
||||||
eventClient, err := client.NewClient(url)
|
eventClient, err := client.NewClient(url)
|
||||||
@@ -86,136 +89,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 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():
|
|
||||||
slog.Info("TUI message handler shutting down")
|
|
||||||
return
|
|
||||||
case msg, ok := <-ch:
|
|
||||||
if !ok {
|
|
||||||
slog.Info("TUI message channel closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
program.Send(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Cleanup function for when the program exits
|
|
||||||
cleanup := func() {
|
|
||||||
// Cancel subscriptions first
|
|
||||||
cancelSubs()
|
|
||||||
|
|
||||||
// Then shutdown the app
|
|
||||||
app_.Shutdown()
|
|
||||||
|
|
||||||
// Then cancel TUI message handler
|
|
||||||
tuiCancel()
|
|
||||||
|
|
||||||
// Wait for TUI message handler to finish
|
|
||||||
tuiWg.Wait()
|
|
||||||
|
|
||||||
slog.Info("All goroutines cleaned up")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the TUI
|
// Run the TUI
|
||||||
result, err := program.Run()
|
result, err := program.Run()
|
||||||
cleanup()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("TUI error", "error", err)
|
slog.Error("TUI error", "error", err)
|
||||||
// return fmt.Errorf("TUI error: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("TUI exited", "result", result)
|
slog.Info("TUI exited", "result", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
if subCh == nil {
|
|
||||||
slog.Warn("subscription channel is nil", "name", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-subCh:
|
|
||||||
if !ok {
|
|
||||||
slog.Info("subscription channel closed", "name", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg tea.Msg = event
|
|
||||||
|
|
||||||
select {
|
|
||||||
case outputCh <- msg:
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
slog.Warn("message dropped due to slow consumer", "name", name)
|
|
||||||
case <-ctx.Done():
|
|
||||||
slog.Info("subscription cancelled", "name", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
slog.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, "status", app.Status.Subscribe, ch)
|
|
||||||
|
|
||||||
cleanupFunc := func() {
|
|
||||||
slog.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:
|
|
||||||
slog.Info("All subscription goroutines completed successfully")
|
|
||||||
close(ch) // Only close after all writers are confirmed done
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
slog.Warn("Timed out waiting for some subscription goroutines to complete")
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ch, cleanupFunc
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-13
@@ -4,16 +4,13 @@ go 1.24.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.5.0
|
||||||
github.com/alecthomas/chroma/v2 v2.15.0
|
github.com/alecthomas/chroma/v2 v2.18.0
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
||||||
github.com/catppuccin/go v0.3.0
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/glamour v0.10.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.4
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
|
||||||
github.com/charmbracelet/glamour v0.9.1
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0
|
github.com/charmbracelet/x/ansi v0.8.0
|
||||||
github.com/lithammer/fuzzysearch v1.1.8
|
github.com/lithammer/fuzzysearch v1.1.8
|
||||||
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231
|
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
|
||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
@@ -28,6 +25,10 @@ require (
|
|||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/atombender/go-jsonschema v0.20.0 // indirect
|
github.com/atombender/go-jsonschema v0.20.0 // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
|
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect
|
||||||
|
github.com/charmbracelet/x/windows v0.2.1 // indirect
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/getkin/kin-openapi v0.127.0 // indirect
|
github.com/getkin/kin-openapi v0.127.0 // indirect
|
||||||
@@ -57,18 +58,16 @@ require (
|
|||||||
github.com/atotto/clipboard v0.1.4
|
github.com/atotto/clipboard v0.1.4
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.3.1 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/disintegration/imaging v1.6.2
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
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/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
|||||||
+26
-29
@@ -7,8 +7,8 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6
|
|||||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
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/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.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
|
||||||
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
|
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
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/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||||
@@ -24,28 +24,32 @@ github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/
|
|||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
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/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||||
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
|
||||||
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY=
|
||||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
|
||||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||||
github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 h1:D9AJJuYTN5pvz6mpIGO1ijLKpfTYSHOtKGgwoTQ4Gog=
|
||||||
github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
|
||||||
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 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
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.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81/go.mod h1:poPFOXFTsJsnLbkV3H2KxAAXT7pdjxxLujLocWjkyzM=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||||
|
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 h1:fsWj8NF5njyMVzELc7++HsvRDvgz3VcgGAUgWBDWWWM=
|
||||||
|
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197/go.mod h1:xseGeVftoP9rVI+/8WKYrJFH6ior6iERGvklwwHz5+s=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
@@ -56,13 +60,11 @@ 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/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 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
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.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
||||||
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
@@ -118,16 +120,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||||
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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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-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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
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/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
@@ -259,7 +257,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
+222
-113
@@ -5,120 +5,201 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/sst/opencode/internal/commands"
|
||||||
|
"github.com/sst/opencode/internal/components/toast"
|
||||||
"github.com/sst/opencode/internal/config"
|
"github.com/sst/opencode/internal/config"
|
||||||
"github.com/sst/opencode/internal/fileutil"
|
|
||||||
"github.com/sst/opencode/internal/state"
|
|
||||||
"github.com/sst/opencode/internal/status"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var RootPath string
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
ConfigPath string
|
Info client.AppInfo
|
||||||
Config *config.Config
|
Version string
|
||||||
|
StatePath string
|
||||||
|
Config *client.ConfigInfo
|
||||||
Client *client.ClientWithResponses
|
Client *client.ClientWithResponses
|
||||||
|
State *config.State
|
||||||
Provider *client.ProviderInfo
|
Provider *client.ProviderInfo
|
||||||
Model *client.ProviderModel
|
Model *client.ModelInfo
|
||||||
Session *client.SessionInfo
|
Session *client.SessionInfo
|
||||||
Messages []client.MessageInfo
|
Messages []client.MessageInfo
|
||||||
Status status.Service
|
Commands commands.CommandRegistry
|
||||||
|
|
||||||
// UI state
|
|
||||||
filepickerOpen bool
|
|
||||||
completionDialogOpen bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInfo struct {
|
type SessionSelectedMsg = *client.SessionInfo
|
||||||
client.AppInfo
|
type ModelSelectedMsg struct {
|
||||||
Version string
|
Provider client.ProviderInfo
|
||||||
|
Model client.ModelInfo
|
||||||
|
}
|
||||||
|
type SessionClearedMsg struct{}
|
||||||
|
type CompactSessionMsg struct{}
|
||||||
|
type SendMsg struct {
|
||||||
|
Text string
|
||||||
|
Attachments []Attachment
|
||||||
|
}
|
||||||
|
type CompletionDialogTriggeredMsg struct {
|
||||||
|
InitialValue string
|
||||||
|
}
|
||||||
|
type OptimisticMessageAddedMsg struct {
|
||||||
|
Message client.MessageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
var Info AppInfo
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
version string,
|
||||||
|
appInfo client.AppInfo,
|
||||||
|
httpClient *client.ClientWithResponses,
|
||||||
|
) (*App, error) {
|
||||||
|
RootPath = appInfo.Path.Root
|
||||||
|
|
||||||
func New(ctx context.Context, version string, httpClient *client.ClientWithResponses) (*App, error) {
|
configResponse, err := httpClient.PostConfigGetWithResponse(ctx)
|
||||||
err := status.InitService()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to initialize status service", "error", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if configResponse.StatusCode() != 200 || configResponse.JSON200 == nil {
|
||||||
|
return nil, fmt.Errorf("failed to get config: %d", configResponse.StatusCode())
|
||||||
|
}
|
||||||
|
configInfo := configResponse.JSON200
|
||||||
|
if configInfo.Keybinds == nil {
|
||||||
|
leader := "ctrl+x"
|
||||||
|
keybinds := client.ConfigKeybinds{
|
||||||
|
Leader: &leader,
|
||||||
|
}
|
||||||
|
configInfo.Keybinds = &keybinds
|
||||||
|
}
|
||||||
|
|
||||||
appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
|
appStatePath := filepath.Join(appInfo.Path.State, "tui")
|
||||||
appInfo := appInfoResponse.JSON200
|
appState, err := config.LoadState(appStatePath)
|
||||||
Info = AppInfo{Version: version}
|
|
||||||
Info.Git = appInfo.Git
|
|
||||||
Info.Path = appInfo.Path
|
|
||||||
Info.Time = appInfo.Time
|
|
||||||
Info.User = appInfo.User
|
|
||||||
|
|
||||||
providersResponse, err := httpClient.PostProviderListWithResponse(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
appState = config.NewState()
|
||||||
|
config.SaveState(appStatePath, appState)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configInfo.Theme != nil {
|
||||||
|
appState.Theme = *configInfo.Theme
|
||||||
|
}
|
||||||
|
if configInfo.Model != nil {
|
||||||
|
splits := strings.Split(*configInfo.Model, "/")
|
||||||
|
appState.Provider = splits[0]
|
||||||
|
appState.Model = strings.Join(splits[1:], "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load themes from all directories
|
||||||
|
if err := theme.LoadThemesFromDirectories(
|
||||||
|
appInfo.Path.Config,
|
||||||
|
appInfo.Path.Root,
|
||||||
|
appInfo.Path.Cwd,
|
||||||
|
); err != nil {
|
||||||
|
slog.Warn("Failed to load themes from directories", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if appState.Theme != "" {
|
||||||
|
theme.SetTheme(appState.Theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Loaded config", "config", configInfo)
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
Info: appInfo,
|
||||||
|
Version: version,
|
||||||
|
StatePath: appStatePath,
|
||||||
|
Config: configInfo,
|
||||||
|
State: appState,
|
||||||
|
Client: httpClient,
|
||||||
|
Session: &client.SessionInfo{},
|
||||||
|
Messages: []client.MessageInfo{},
|
||||||
|
Commands: commands.LoadFromConfig(configInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) InitializeProvider() tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
providersResponse, err := a.Client.PostProviderListWithResponse(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to list providers", "error", err)
|
||||||
|
// TODO: notify user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if providersResponse != nil && providersResponse.StatusCode() != 200 {
|
||||||
|
slog.Error("failed to retrieve providers", "status", providersResponse.StatusCode(), "message", string(providersResponse.Body))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
providers := []client.ProviderInfo{}
|
providers := []client.ProviderInfo{}
|
||||||
var defaultProvider *client.ProviderInfo
|
var defaultProvider *client.ProviderInfo
|
||||||
var defaultModel *client.ProviderModel
|
var defaultModel *client.ModelInfo
|
||||||
|
|
||||||
for i, provider := range providersResponse.JSON200.Providers {
|
var anthropic *client.ProviderInfo
|
||||||
if i == 0 || provider.Id == "anthropic" {
|
for _, provider := range providersResponse.JSON200.Providers {
|
||||||
defaultProvider = &providersResponse.JSON200.Providers[i]
|
if provider.Id == "anthropic" {
|
||||||
if match, ok := providersResponse.JSON200.Default[provider.Id]; ok {
|
anthropic = &provider
|
||||||
model := defaultProvider.Models[match]
|
|
||||||
defaultModel = &model
|
|
||||||
} else {
|
|
||||||
for _, model := range provider.Models {
|
|
||||||
defaultModel = &model
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default to anthropic if available
|
||||||
|
if anthropic != nil {
|
||||||
|
defaultProvider = anthropic
|
||||||
|
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, provider := range providersResponse.JSON200.Providers {
|
||||||
|
if defaultProvider == nil || defaultModel == nil {
|
||||||
|
defaultProvider = &provider
|
||||||
|
defaultModel = getDefaultModel(providersResponse, provider)
|
||||||
}
|
}
|
||||||
providers = append(providers, provider)
|
providers = append(providers, provider)
|
||||||
}
|
}
|
||||||
if len(providers) == 0 {
|
if len(providers) == 0 {
|
||||||
return nil, fmt.Errorf("no providers found")
|
slog.Error("No providers configured")
|
||||||
}
|
return nil
|
||||||
|
|
||||||
appConfigPath := filepath.Join(Info.Path.Config, "tui.toml")
|
|
||||||
appConfig, err := config.LoadConfig(appConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
slog.Info("No TUI config found, using default values", "error", err)
|
|
||||||
appConfig = config.NewConfig("opencode", defaultProvider.Id, defaultModel.Id)
|
|
||||||
config.SaveConfig(appConfigPath, appConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentProvider *client.ProviderInfo
|
var currentProvider *client.ProviderInfo
|
||||||
var currentModel *client.ProviderModel
|
var currentModel *client.ModelInfo
|
||||||
for _, provider := range providers {
|
for _, provider := range providers {
|
||||||
if provider.Id == appConfig.Provider {
|
if provider.Id == a.State.Provider {
|
||||||
currentProvider = &provider
|
currentProvider = &provider
|
||||||
|
|
||||||
for _, model := range provider.Models {
|
for _, model := range provider.Models {
|
||||||
if model.Id == appConfig.Model {
|
if model.Id == a.State.Model {
|
||||||
currentModel = &model
|
currentModel = &model
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if currentProvider == nil || currentModel == nil {
|
||||||
app := &App{
|
currentProvider = defaultProvider
|
||||||
ConfigPath: appConfigPath,
|
currentModel = defaultModel
|
||||||
Config: appConfig,
|
|
||||||
Client: httpClient,
|
|
||||||
Provider: currentProvider,
|
|
||||||
Model: currentModel,
|
|
||||||
Session: &client.SessionInfo{},
|
|
||||||
Messages: []client.MessageInfo{},
|
|
||||||
Status: status.GetService(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
theme.SetTheme(appConfig.Theme)
|
// TODO: handle no provider or model setup, yet
|
||||||
fileutil.Init()
|
return ModelSelectedMsg{
|
||||||
|
Provider: *currentProvider,
|
||||||
|
Model: *currentModel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return app, nil
|
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
|
||||||
|
if match, ok := response.JSON200.Default[provider.Id]; ok {
|
||||||
|
model := provider.Models[match]
|
||||||
|
return &model
|
||||||
|
} else {
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
return &model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Attachment struct {
|
type Attachment struct {
|
||||||
@@ -137,8 +218,11 @@ func (a *App) IsBusy() bool {
|
|||||||
return lastMessage.Metadata.Time.Completed == nil
|
return lastMessage.Metadata.Time.Completed == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SaveConfig() {
|
func (a *App) SaveState() {
|
||||||
config.SaveConfig(a.ConfigPath, a.Config)
|
err := config.SaveState(a.StatePath, a.State)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to save state", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
||||||
@@ -146,31 +230,49 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
|||||||
|
|
||||||
session, err := a.CreateSession(ctx)
|
session, err := a.CreateSession(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.Error(err.Error())
|
// status.Error(err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Session = session
|
a.Session = session
|
||||||
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
|
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// TODO: Handle no provider or model setup, yet
|
|
||||||
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
|
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
|
||||||
SessionID: a.Session.Id,
|
SessionID: a.Session.Id,
|
||||||
ProviderID: a.Provider.Id,
|
ProviderID: a.Provider.Id,
|
||||||
ModelID: a.Model.Id,
|
ModelID: a.Model.Id,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.Error(err.Error())
|
slog.Error("Failed to initialize project", "error", err)
|
||||||
|
// status.Error(err.Error())
|
||||||
}
|
}
|
||||||
if response != nil && response.StatusCode != 200 {
|
if response != nil && response.StatusCode != 200 {
|
||||||
status.Error(fmt.Sprintf("failed to initialize project: %d", response.StatusCode))
|
slog.Error("Failed to initialize project", "error", response.StatusCode)
|
||||||
|
// status.Error(fmt.Sprintf("failed to initialize project: %d", response.StatusCode))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||||
|
go func() {
|
||||||
|
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||||
|
SessionID: a.Session.Id,
|
||||||
|
ProviderID: a.Provider.Id,
|
||||||
|
ModelID: a.Model.Id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to compact session", "error", err)
|
||||||
|
}
|
||||||
|
if response != nil && response.StatusCode() != 200 {
|
||||||
|
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
||||||
response, err := a.Client.PostAppInitialize(ctx)
|
response, err := a.Client.PostAppInitialize(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -200,17 +302,10 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
|||||||
if a.Session.Id == "" {
|
if a.Session.Id == "" {
|
||||||
session, err := a.CreateSession(ctx)
|
session, err := a.CreateSession(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.Error(err.Error())
|
return toast.NewErrorToast(err.Error())
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
a.Session = session
|
a.Session = session
|
||||||
cmds = append(cmds, util.CmdHandler(state.SessionSelectedMsg(session)))
|
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle attachments when API supports them
|
|
||||||
if len(attachments) > 0 {
|
|
||||||
// For now, ignore attachments
|
|
||||||
// return "", fmt.Errorf("attachments not supported yet")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
part := client.MessagePart{}
|
part := client.MessagePart{}
|
||||||
@@ -220,7 +315,26 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
|||||||
})
|
})
|
||||||
parts := []client.MessagePart{part}
|
parts := []client.MessagePart{part}
|
||||||
|
|
||||||
go func() {
|
optimisticMessage := client.MessageInfo{
|
||||||
|
Id: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||||
|
Role: client.User,
|
||||||
|
Parts: parts,
|
||||||
|
Metadata: client.MessageMetadata{
|
||||||
|
SessionID: a.Session.Id,
|
||||||
|
Time: struct {
|
||||||
|
Completed *float32 `json:"completed,omitempty"`
|
||||||
|
Created float32 `json:"created"`
|
||||||
|
}{
|
||||||
|
Created: float32(time.Now().Unix()),
|
||||||
|
},
|
||||||
|
Tool: make(map[string]client.MessageMetadata_Tool_AdditionalProperties),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Messages = append(a.Messages, optimisticMessage)
|
||||||
|
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
|
||||||
|
|
||||||
|
cmds = append(cmds, func() tea.Msg {
|
||||||
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
|
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
|
||||||
SessionID: a.Session.Id,
|
SessionID: a.Session.Id,
|
||||||
Parts: parts,
|
Parts: parts,
|
||||||
@@ -228,14 +342,17 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
|||||||
ModelID: a.Model.Id,
|
ModelID: a.Model.Id,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to send message", "error", err)
|
errormsg := fmt.Sprintf("failed to send message: %v", err)
|
||||||
status.Error(err.Error())
|
slog.Error(errormsg)
|
||||||
|
return toast.NewErrorToast(errormsg)()
|
||||||
}
|
}
|
||||||
if response != nil && response.StatusCode != 200 {
|
if response != nil && response.StatusCode != 200 {
|
||||||
slog.Error("Failed to send message", "error", fmt.Sprintf("failed to send message: %d", response.StatusCode))
|
errormsg := fmt.Sprintf("failed to send message: %d", response.StatusCode)
|
||||||
status.Error(fmt.Sprintf("failed to send message: %d", response.StatusCode))
|
slog.Error(errormsg)
|
||||||
|
return toast.NewErrorToast(errormsg)()
|
||||||
}
|
}
|
||||||
}()
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// The actual response will come through SSE
|
// The actual response will come through SSE
|
||||||
// For now, just return success
|
// For now, just return success
|
||||||
@@ -248,12 +365,12 @@ func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to cancel session", "error", err)
|
slog.Error("Failed to cancel session", "error", err)
|
||||||
status.Error(err.Error())
|
// status.Error(err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if response != nil && response.StatusCode != 200 {
|
if response != nil && response.StatusCode != 200 {
|
||||||
slog.Error("Failed to cancel session", "error", fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
slog.Error("Failed to cancel session", "error", fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
||||||
status.Error(fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
// status.Error(fmt.Sprintf("failed to cancel session: %d", response.StatusCode))
|
||||||
return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
|
return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -279,6 +396,19 @@ func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
|
|||||||
return sessions, nil
|
return sessions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
|
||||||
|
resp, err := a.Client.PostSessionDeleteWithResponse(ctx, client.PostSessionDeleteJSONRequestBody{
|
||||||
|
SessionID: sessionID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != 200 {
|
||||||
|
return fmt.Errorf("failed to delete session: %d", resp.StatusCode())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
|
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
|
||||||
resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
|
resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -310,27 +440,6 @@ func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error)
|
|||||||
return providers.Providers, nil
|
return providers.Providers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFilepickerOpen returns whether the filepicker is currently open
|
// func (a *App) loadCustomKeybinds() {
|
||||||
func (app *App) IsFilepickerOpen() bool {
|
//
|
||||||
return app.filepickerOpen
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
// SetFilepickerOpen sets the state of the filepicker
|
|
||||||
func (app *App) SetFilepickerOpen(open bool) {
|
|
||||||
app.filepickerOpen = open
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCompletionDialogOpen returns whether the completion dialog is currently open
|
|
||||||
func (app *App) IsCompletionDialogOpen() bool {
|
|
||||||
return app.completionDialogOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCompletionDialogOpen sets the state of the completion dialog
|
|
||||||
func (app *App) SetCompletionDialogOpen(open bool) {
|
|
||||||
app.completionDialogOpen = open
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown performs a clean shutdown of the application
|
|
||||||
func (app *App) Shutdown() {
|
|
||||||
// TODO: cleanup?
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/sst/opencode/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExecuteCommandMsg Command
|
||||||
|
type ExecuteCommandsMsg []Command
|
||||||
|
type CommandExecutedMsg Command
|
||||||
|
|
||||||
|
type Keybinding struct {
|
||||||
|
RequiresLeader bool
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Keybinding) Matches(msg tea.KeyPressMsg, leader bool) bool {
|
||||||
|
key := k.Key
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
return key == msg.String() && (k.RequiresLeader == leader)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandName string
|
||||||
|
type Command struct {
|
||||||
|
Name CommandName
|
||||||
|
Description string
|
||||||
|
Keybindings []Keybinding
|
||||||
|
Trigger string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Command) Keys() []string {
|
||||||
|
var keys []string
|
||||||
|
for _, k := range c.Keybindings {
|
||||||
|
keys = append(keys, k.Key)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandRegistry map[CommandName]Command
|
||||||
|
|
||||||
|
func (r CommandRegistry) Sorted() []Command {
|
||||||
|
var commands []Command
|
||||||
|
for _, command := range r {
|
||||||
|
commands = append(commands, command)
|
||||||
|
}
|
||||||
|
slices.SortFunc(commands, func(a, b Command) int {
|
||||||
|
if a.Name == AppExitCommand {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if b.Name == AppExitCommand {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return strings.Compare(string(a.Name), string(b.Name))
|
||||||
|
})
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
|
||||||
|
var matched []Command
|
||||||
|
for _, command := range r.Sorted() {
|
||||||
|
if command.Matches(msg, leader) {
|
||||||
|
matched = append(matched, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppHelpCommand CommandName = "app_help"
|
||||||
|
EditorOpenCommand CommandName = "editor_open"
|
||||||
|
SessionNewCommand CommandName = "session_new"
|
||||||
|
SessionListCommand CommandName = "session_list"
|
||||||
|
SessionShareCommand CommandName = "session_share"
|
||||||
|
SessionInterruptCommand CommandName = "session_interrupt"
|
||||||
|
SessionCompactCommand CommandName = "session_compact"
|
||||||
|
ToolDetailsCommand CommandName = "tool_details"
|
||||||
|
ModelListCommand CommandName = "model_list"
|
||||||
|
ThemeListCommand CommandName = "theme_list"
|
||||||
|
ProjectInitCommand CommandName = "project_init"
|
||||||
|
InputClearCommand CommandName = "input_clear"
|
||||||
|
InputPasteCommand CommandName = "input_paste"
|
||||||
|
InputSubmitCommand CommandName = "input_submit"
|
||||||
|
InputNewlineCommand CommandName = "input_newline"
|
||||||
|
HistoryPreviousCommand CommandName = "history_previous"
|
||||||
|
HistoryNextCommand CommandName = "history_next"
|
||||||
|
MessagesPageUpCommand CommandName = "messages_page_up"
|
||||||
|
MessagesPageDownCommand CommandName = "messages_page_down"
|
||||||
|
MessagesHalfPageUpCommand CommandName = "messages_half_page_up"
|
||||||
|
MessagesHalfPageDownCommand CommandName = "messages_half_page_down"
|
||||||
|
MessagesPreviousCommand CommandName = "messages_previous"
|
||||||
|
MessagesNextCommand CommandName = "messages_next"
|
||||||
|
MessagesFirstCommand CommandName = "messages_first"
|
||||||
|
MessagesLastCommand CommandName = "messages_last"
|
||||||
|
AppExitCommand CommandName = "app_exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool {
|
||||||
|
for _, binding := range k.Keybindings {
|
||||||
|
if binding.Matches(msg, leader) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBindings(bindings ...string) []Keybinding {
|
||||||
|
var parsedBindings []Keybinding
|
||||||
|
for _, binding := range bindings {
|
||||||
|
for p := range strings.SplitSeq(binding, ",") {
|
||||||
|
requireLeader := strings.HasPrefix(p, "<leader>")
|
||||||
|
keybinding := strings.ReplaceAll(p, "<leader>", "")
|
||||||
|
keybinding = strings.TrimSpace(keybinding)
|
||||||
|
parsedBindings = append(parsedBindings, Keybinding{
|
||||||
|
RequiresLeader: requireLeader,
|
||||||
|
Key: keybinding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedBindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
|
||||||
|
defaults := []Command{
|
||||||
|
{
|
||||||
|
Name: AppHelpCommand,
|
||||||
|
Description: "show help",
|
||||||
|
Keybindings: parseBindings("<leader>h"),
|
||||||
|
Trigger: "help",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: EditorOpenCommand,
|
||||||
|
Description: "open editor",
|
||||||
|
Keybindings: parseBindings("<leader>e"),
|
||||||
|
Trigger: "editor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionNewCommand,
|
||||||
|
Description: "new session",
|
||||||
|
Keybindings: parseBindings("<leader>n"),
|
||||||
|
Trigger: "new",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionListCommand,
|
||||||
|
Description: "list sessions",
|
||||||
|
Keybindings: parseBindings("<leader>l"),
|
||||||
|
Trigger: "sessions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionShareCommand,
|
||||||
|
Description: "share session",
|
||||||
|
Keybindings: parseBindings("<leader>s"),
|
||||||
|
Trigger: "share",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionInterruptCommand,
|
||||||
|
Description: "interrupt session",
|
||||||
|
Keybindings: parseBindings("esc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionCompactCommand,
|
||||||
|
Description: "compact the session",
|
||||||
|
Keybindings: parseBindings("<leader>c"),
|
||||||
|
Trigger: "compact",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ToolDetailsCommand,
|
||||||
|
Description: "toggle tool details",
|
||||||
|
Keybindings: parseBindings("<leader>d"),
|
||||||
|
Trigger: "details",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ModelListCommand,
|
||||||
|
Description: "list models",
|
||||||
|
Keybindings: parseBindings("<leader>m"),
|
||||||
|
Trigger: "models",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ThemeListCommand,
|
||||||
|
Description: "list themes",
|
||||||
|
Keybindings: parseBindings("<leader>t"),
|
||||||
|
Trigger: "themes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ProjectInitCommand,
|
||||||
|
Description: "create/update AGENTS.md",
|
||||||
|
Keybindings: parseBindings("<leader>i"),
|
||||||
|
Trigger: "init",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputClearCommand,
|
||||||
|
Description: "clear input",
|
||||||
|
Keybindings: parseBindings("ctrl+c"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputPasteCommand,
|
||||||
|
Description: "paste content",
|
||||||
|
Keybindings: parseBindings("ctrl+v"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputSubmitCommand,
|
||||||
|
Description: "submit message",
|
||||||
|
Keybindings: parseBindings("enter"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: InputNewlineCommand,
|
||||||
|
Description: "insert newline",
|
||||||
|
Keybindings: parseBindings("shift+enter", "ctrl+j"),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// Name: HistoryPreviousCommand,
|
||||||
|
// Description: "previous prompt",
|
||||||
|
// Keybindings: parseBindings("up"),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// Name: HistoryNextCommand,
|
||||||
|
// Description: "next prompt",
|
||||||
|
// Keybindings: parseBindings("down"),
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
Name: MessagesPageUpCommand,
|
||||||
|
Description: "page up",
|
||||||
|
Keybindings: parseBindings("pgup"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesPageDownCommand,
|
||||||
|
Description: "page down",
|
||||||
|
Keybindings: parseBindings("pgdown"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesHalfPageUpCommand,
|
||||||
|
Description: "half page up",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+u"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesHalfPageDownCommand,
|
||||||
|
Description: "half page down",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+d"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesPreviousCommand,
|
||||||
|
Description: "previous message",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+k"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesNextCommand,
|
||||||
|
Description: "next message",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+j"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesFirstCommand,
|
||||||
|
Description: "first message",
|
||||||
|
Keybindings: parseBindings("ctrl+g"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: MessagesLastCommand,
|
||||||
|
Description: "last message",
|
||||||
|
Keybindings: parseBindings("ctrl+alt+g"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: AppExitCommand,
|
||||||
|
Description: "exit the app",
|
||||||
|
Keybindings: parseBindings("ctrl+c", "<leader>q"),
|
||||||
|
Trigger: "exit",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
registry := make(CommandRegistry)
|
||||||
|
keybinds := map[string]string{}
|
||||||
|
marshalled, _ := json.Marshal(*config.Keybinds)
|
||||||
|
json.Unmarshal(marshalled, &keybinds)
|
||||||
|
for _, command := range defaults {
|
||||||
|
if keybind, ok := keybinds[string(command.Name)]; ok {
|
||||||
|
command.Keybindings = parseBindings(keybind)
|
||||||
|
}
|
||||||
|
registry[command.Name] = command
|
||||||
|
}
|
||||||
|
return registry
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/commands"
|
||||||
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandCompletionProvider struct {
|
||||||
|
app *app.App
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommandCompletionProvider(app *app.App) dialog.CompletionProvider {
|
||||||
|
return &CommandCompletionProvider{app: app}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetId() string {
|
||||||
|
return "commands"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetEntry() dialog.CompletionItemI {
|
||||||
|
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
|
Title: "Commands",
|
||||||
|
Value: "commands",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetEmptyMessage() string {
|
||||||
|
return "no matching commands"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
|
||||||
|
spacer := strings.Repeat(" ", space)
|
||||||
|
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||||
|
value := string(cmd.Name)
|
||||||
|
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||||
|
Title: title,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
commands := c.app.Commands
|
||||||
|
|
||||||
|
space := 1
|
||||||
|
for _, cmd := range c.app.Commands {
|
||||||
|
if lipgloss.Width(cmd.Trigger) > space {
|
||||||
|
space = lipgloss.Width(cmd.Trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
space += 2
|
||||||
|
|
||||||
|
sorted := commands.Sorted()
|
||||||
|
if query == "" {
|
||||||
|
// If no query, return all commands
|
||||||
|
items := []dialog.CompletionItemI{}
|
||||||
|
for _, cmd := range sorted {
|
||||||
|
if cmd.Trigger == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
space := space - lipgloss.Width(cmd.Trigger)
|
||||||
|
items = append(items, getCommandCompletionItem(cmd, space, t))
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fuzzy matching for commands
|
||||||
|
var commandNames []string
|
||||||
|
commandMap := make(map[string]dialog.CompletionItemI)
|
||||||
|
|
||||||
|
for _, cmd := range sorted {
|
||||||
|
if cmd.Trigger == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
space := space - lipgloss.Width(cmd.Trigger)
|
||||||
|
commandNames = append(commandNames, cmd.Trigger)
|
||||||
|
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find fuzzy matches
|
||||||
|
matches := fuzzy.RankFind(query, commandNames)
|
||||||
|
|
||||||
|
// Sort by score (best matches first)
|
||||||
|
sort.Sort(matches)
|
||||||
|
|
||||||
|
// Convert matches to completion items
|
||||||
|
items := []dialog.CompletionItemI{}
|
||||||
|
for _, match := range matches {
|
||||||
|
if item, ok := commandMap[match.Target]; ok {
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
package completions
|
package completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/fileutil"
|
|
||||||
"github.com/sst/opencode/internal/status"
|
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type filesAndFoldersContextGroup struct {
|
type filesAndFoldersContextGroup struct {
|
||||||
|
app *app.App
|
||||||
prefix string
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,143 +24,22 @@ func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func processNullTerminatedOutput(outputBytes []byte) []string {
|
func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string {
|
||||||
if len(outputBytes) > 0 && outputBytes[len(outputBytes)-1] == 0 {
|
return "no matching files"
|
||||||
outputBytes = outputBytes[:len(outputBytes)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(outputBytes) == 0 {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
split := bytes.Split(outputBytes, []byte{0})
|
|
||||||
matches := make([]string, 0, len(split))
|
|
||||||
|
|
||||||
for _, p := range split {
|
|
||||||
if len(p) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
path := string(p)
|
|
||||||
path = filepath.Join(".", path)
|
|
||||||
|
|
||||||
if !fileutil.SkipHidden(path) {
|
|
||||||
matches = append(matches, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
||||||
cmdRg := fileutil.GetRgCmd("") // No glob pattern for this use case
|
response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
|
||||||
cmdFzf := fileutil.GetFzfCmd(query)
|
Query: query,
|
||||||
|
})
|
||||||
var matches []string
|
|
||||||
// Case 1: Both rg and fzf available
|
|
||||||
if cmdRg != nil && cmdFzf != nil {
|
|
||||||
rgPipe, err := cmdRg.StdoutPipe()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get rg stdout pipe: %w", err)
|
return []string{}, err
|
||||||
}
|
}
|
||||||
defer rgPipe.Close()
|
if response.JSON200 == nil {
|
||||||
|
|
||||||
cmdFzf.Stdin = rgPipe
|
|
||||||
var fzfOut bytes.Buffer
|
|
||||||
var fzfErr bytes.Buffer
|
|
||||||
cmdFzf.Stdout = &fzfOut
|
|
||||||
cmdFzf.Stderr = &fzfErr
|
|
||||||
|
|
||||||
if err := cmdFzf.Start(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start fzf: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
errRg := cmdRg.Run()
|
|
||||||
errFzf := cmdFzf.Wait()
|
|
||||||
|
|
||||||
if errRg != nil {
|
|
||||||
status.Warn(fmt.Sprintf("rg command failed during pipe: %v", errRg))
|
|
||||||
}
|
|
||||||
|
|
||||||
if errFzf != nil {
|
|
||||||
if exitErr, ok := errFzf.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
|
||||||
return []string{}, nil // No matches from fzf
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", errFzf, fzfErr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
matches = processNullTerminatedOutput(fzfOut.Bytes())
|
|
||||||
|
|
||||||
// Case 2: Only rg available
|
|
||||||
} else if cmdRg != nil {
|
|
||||||
status.Debug("Using Ripgrep with fuzzy match fallback for file completions")
|
|
||||||
var rgOut bytes.Buffer
|
|
||||||
var rgErr bytes.Buffer
|
|
||||||
cmdRg.Stdout = &rgOut
|
|
||||||
cmdRg.Stderr = &rgErr
|
|
||||||
|
|
||||||
if err := cmdRg.Run(); err != nil {
|
|
||||||
return nil, fmt.Errorf("rg command failed: %w\nStderr: %s", err, rgErr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
allFiles := processNullTerminatedOutput(rgOut.Bytes())
|
|
||||||
matches = fuzzy.Find(query, allFiles)
|
|
||||||
|
|
||||||
// Case 3: Only fzf available
|
|
||||||
} else if cmdFzf != nil {
|
|
||||||
status.Debug("Using FZF with doublestar fallback for file completions")
|
|
||||||
files, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to list files for fzf: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
allFiles := make([]string, 0, len(files))
|
|
||||||
for _, file := range files {
|
|
||||||
if !fileutil.SkipHidden(file) {
|
|
||||||
allFiles = append(allFiles, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var fzfIn bytes.Buffer
|
|
||||||
for _, file := range allFiles {
|
|
||||||
fzfIn.WriteString(file)
|
|
||||||
fzfIn.WriteByte(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdFzf.Stdin = &fzfIn
|
|
||||||
var fzfOut bytes.Buffer
|
|
||||||
var fzfErr bytes.Buffer
|
|
||||||
cmdFzf.Stdout = &fzfOut
|
|
||||||
cmdFzf.Stderr = &fzfErr
|
|
||||||
|
|
||||||
if err := cmdFzf.Run(); err != nil {
|
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", err, fzfErr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
matches = processNullTerminatedOutput(fzfOut.Bytes())
|
return *response.JSON200, nil
|
||||||
|
|
||||||
// Case 4: Fallback to doublestar with fuzzy match
|
|
||||||
} else {
|
|
||||||
status.Debug("Using doublestar with fuzzy match for file completions")
|
|
||||||
allFiles, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to glob files: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredFiles := make([]string, 0, len(allFiles))
|
|
||||||
for _, file := range allFiles {
|
|
||||||
if !fileutil.SkipHidden(file) {
|
|
||||||
filteredFiles = append(filteredFiles, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matches = fuzzy.Find(query, filteredFiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||||
@@ -184,8 +60,9 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.C
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileAndFolderContextGroup() dialog.CompletionProvider {
|
func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider {
|
||||||
return &filesAndFoldersContextGroup{
|
return &filesAndFoldersContextGroup{
|
||||||
|
app: app,
|
||||||
prefix: "file",
|
prefix: "file",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompletionManager struct {
|
||||||
|
providers map[string]dialog.CompletionProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompletionManager(app *app.App) *CompletionManager {
|
||||||
|
return &CompletionManager{
|
||||||
|
providers: map[string]dialog.CompletionProvider{
|
||||||
|
"files": NewFileAndFolderContextGroup(app),
|
||||||
|
"commands": NewCommandCompletionProvider(app),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CompletionManager) DefaultProvider() dialog.CompletionProvider {
|
||||||
|
return m.providers["commands"]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
||||||
|
if strings.HasPrefix(input, "/") {
|
||||||
|
return m.providers["commands"]
|
||||||
|
}
|
||||||
|
return m.providers["files"]
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package chat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sst/opencode/internal/app"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SendMsg struct {
|
|
||||||
Text string
|
|
||||||
Attachments []app.Attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
func repo(width int) string {
|
|
||||||
repo := "github.com/sst/opencode"
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
|
|
||||||
return styles.BaseStyle().
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Width(width).
|
|
||||||
Render(repo)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user