mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
Compare commits
354 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f618e569ab | |||
| 7b394b91e2 | |||
| 6a7983a4ea | |||
| 737146fca1 | |||
| 688f3fd12f | |||
| 145df08444 | |||
| 8b400515ea | |||
| 289797f56d | |||
| be0811ecc3 | |||
| 0676bcd4fd | |||
| d076def561 | |||
| e0807d7317 | |||
| fa2723f2d0 | |||
| 87d62514db | |||
| 2f8cf9146b | |||
| 8e0ec6b037 | |||
| 6dc434cb83 | |||
| d972c27f03 | |||
| 9e2bb63688 | |||
| 49053b66a9 | |||
| 47497aef07 | |||
| 8455029de1 | |||
| 9f07f89384 | |||
| d840d43e8f | |||
| 9ead2f3dfb | |||
| f3742ddbb8 | |||
| b61a841aa8 | |||
| ebcf11e574 | |||
| 065f0aaddf | |||
| c0773dc7c5 | |||
| 1c3c74bd36 | |||
| 79bbf90b72 | |||
| 226a4a7f36 | |||
| df3b424830 | |||
| 3cfd9d80bc | |||
| e0553b8d2c | |||
| 391c837b37 | |||
| 5773d9d1a3 | |||
| ce611963c3 | |||
| f865cacfb8 | |||
| 2ec0611f42 | |||
| 334161a30e | |||
| dbb6e55226 | |||
| d0f9260559 | |||
| d2176064e1 | |||
| ed8d277e49 | |||
| 59b3268c64 | |||
| d043f67761 | |||
| 51bf193889 | |||
| f8b78f08b4 | |||
| a4f32d602b | |||
| dc3dd21cf3 | |||
| b4c2fcccf5 | |||
| e950ad5306 | |||
| 8ca713b737 | |||
| 5b54554fd5 | |||
| 4bc651f958 | |||
| 3b6976a9c8 | |||
| 863d5c1e8e | |||
| 97e19e9677 | |||
| b27851461f | |||
| 209687377a | |||
| 90face1c09 | |||
| 936e2ce48b | |||
| 16ee8ee379 | |||
| ac39308dad | |||
| 346b49219d | |||
| d84c1f20c7 | |||
| dfb8777555 | |||
| 008af18156 | |||
| ab23167f80 | |||
| b17ec46463 | |||
| 2e26b58d16 | |||
| 31b56e5a05 | |||
| 47c401cf25 | |||
| fab8dc9e6f | |||
| f39a2b1f16 | |||
| 66830ced4e | |||
| 9d3fad754d | |||
| dcd3131f58 | |||
| 3d02e07161 | |||
| 4dbc6a43a6 | |||
| 5394b5188b | |||
| 8e680b3957 | |||
| 1b8cd796d6 | |||
| 35fba793d0 | |||
| 5358d43b74 | |||
| f777347bac | |||
| 17c8b914df | |||
| 43b467dd12 | |||
| 0e0770921e | |||
| 8edbb74352 | |||
| e6bfa95758 | |||
| e4120b6287 | |||
| ccbc9e00f2 | |||
| 7d13baadc8 | |||
| 9acc83697f | |||
| db24bf87c0 | |||
| f4c0d2d2fd | |||
| d240f4c676 | |||
| 9c90cdbe08 | |||
| fc7af31fe5 | |||
| 2f8d23ec66 | |||
| 77ae3fb9b9 | |||
| 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 |
@@ -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 }}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.16
|
bun-version: 1.2.17
|
||||||
|
|
||||||
- name: Install makepkg
|
- name: Install makepkg
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -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,8 +1,22 @@
|
|||||||
[](https://github.com/sst/opencode)
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai">
|
||||||
|
<picture>
|
||||||
|
<source srcset="packages/web/src/assets/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<source srcset="packages/web/src/assets/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<img src="packages/web/src/assets/logo-ornate-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>
|
||||||
|
|
||||||
AI coding agent, built for the terminal.
|
[](https://opencode.ai)
|
||||||
|
|
||||||
⚠️ **Note:** version 0.1.x is a full rewrite and we do not have proper documentation for it yet. Should have this out week of June 17th 2025 📚
|
---
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -16,125 +30,51 @@ brew install sst/tap/opencode # macOS
|
|||||||
paru -S opencode-bin # Arch Linux
|
paru -S opencode-bin # Arch Linux
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Remove previous versions < 0.1.x first if installed
|
> **Note:** Remove versions older than 0.1.x before installing
|
||||||
|
|
||||||
### Providers
|
### Documentation
|
||||||
|
|
||||||
The recommended approach is to sign up for claude pro or max and do `opencode auth login` and select Anthropic. It is the most cost effective way to use this tool.
|
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||||
|
|
||||||
Additionally opencode is powered by the provider list at [models.dev](https://models.dev) so you can use `opencode auth login` to configure api keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ opencode auth login
|
|
||||||
|
|
||||||
┌ Add credential
|
|
||||||
│
|
|
||||||
◆ Select provider
|
|
||||||
│ ● Anthropic (recommended)
|
|
||||||
│ ○ OpenAI
|
|
||||||
│ ○ Google
|
|
||||||
│ ○ Amazon Bedrock
|
|
||||||
│ ○ Azure
|
|
||||||
│ ○ DeepSeek
|
|
||||||
│ ○ Groq
|
|
||||||
│ ...
|
|
||||||
└
|
|
||||||
```
|
|
||||||
|
|
||||||
The models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
|
|
||||||
|
|
||||||
If there are additional providers you want to use you can submit a PR to the [models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below
|
|
||||||
|
|
||||||
### Project Config
|
|
||||||
|
|
||||||
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and it will be loaded.
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "http://opencode.ai/config.json"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### MCP
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "http://opencode.ai/config.json",
|
|
||||||
"mcp": {
|
|
||||||
"localmcp": {
|
|
||||||
"type": "local",
|
|
||||||
"command": ["bun", "x", "my-mcp-command"],
|
|
||||||
"environment": {
|
|
||||||
"MY_ENV_VAR": "my_env_var_value"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"remotemcp": {
|
|
||||||
"type": "remote",
|
|
||||||
"url": "https://my-mcp-server.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Providers
|
|
||||||
|
|
||||||
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Use the npm package name as the key in your config. Note we use v5 of the ai-sdk and not all providers support that yet.
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"provider": {
|
|
||||||
"@ai-sdk/openai-compatible": {
|
|
||||||
"name": "ollama",
|
|
||||||
"options": {
|
|
||||||
"baseURL": "http://localhost:11434/v1"
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"llama2": {
|
|
||||||
"name": "llama2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
To run opencode locally you need
|
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.
|
||||||
|
|
||||||
- bun
|
To run opencode locally you need.
|
||||||
- golang 1.24.x
|
|
||||||
|
|
||||||
To run
|
- Bun
|
||||||
|
- Golang 1.24.x
|
||||||
|
|
||||||
```
|
And run.
|
||||||
|
|
||||||
|
```bash
|
||||||
$ bun install
|
$ bun install
|
||||||
$ cd packages/opencode
|
$ bun run packages/opencode/src/index.ts
|
||||||
$ bun run src/index.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Development Notes
|
||||||
|
|
||||||
|
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
|
|
||||||
#### How do I use this with OpenRouter
|
#### How is this different than Claude Code?
|
||||||
|
|
||||||
OpenRouter is not yet in the models.dev database but you can configure it manually.
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
```json title="opencode.json"
|
- 100% open source
|
||||||
{
|
- 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.
|
||||||
"$schema": "https://opencode.ai/config.json",
|
- 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.
|
||||||
"provider": {
|
- 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.
|
||||||
"@openrouter/ai-sdk-provider": {
|
|
||||||
"name": "OpenRouter",
|
#### What about Windows support?
|
||||||
"options": {
|
|
||||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
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.
|
||||||
},
|
|
||||||
"models": {
|
#### What's the other repo?
|
||||||
"anthropic/claude-3.5-sonnet": {
|
|
||||||
"name": "Claude 3.5 Sonnet"
|
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).
|
||||||
}
|
|
||||||
}
|
---
|
||||||
}
|
|
||||||
}
|
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -19,7 +19,10 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "0.0.0",
|
"version": "0.0.5",
|
||||||
|
"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,8 +46,11 @@
|
|||||||
"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",
|
||||||
@@ -68,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:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -86,6 +94,9 @@
|
|||||||
"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",
|
||||||
},
|
},
|
||||||
@@ -96,6 +107,10 @@
|
|||||||
"zod": "3.24.2",
|
"zod": "3.24.2",
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@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/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": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
"@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/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=="],
|
||||||
@@ -130,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=="],
|
||||||
@@ -410,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=="],
|
||||||
@@ -428,7 +461,7 @@
|
|||||||
|
|
||||||
"@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.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
|
"@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=="],
|
||||||
|
|
||||||
@@ -566,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.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
|
"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=="],
|
||||||
|
|
||||||
@@ -982,6 +1015,10 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -1418,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=="],
|
||||||
|
|
||||||
@@ -1488,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=="],
|
||||||
|
|
||||||
@@ -1648,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=="],
|
||||||
@@ -1674,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=="],
|
||||||
@@ -1744,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=="],
|
||||||
@@ -1810,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",
|
||||||
|
|||||||
+13
-10
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"provider": {
|
"experimental": {
|
||||||
"@ai-sdk/openai-compatible": {
|
"hook": {
|
||||||
"name": "ollama",
|
"file_edited": {
|
||||||
"options": {
|
".json": [
|
||||||
"baseURL": "http://localhost:11434/v1"
|
{
|
||||||
|
"command": ["bun", "run", "prettier", "$FILE"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"models": {
|
"session_completed": [
|
||||||
"qwen3": {},
|
{
|
||||||
"deepseek-r1": {},
|
"command": ["touch", "./node_modules/foo"]
|
||||||
"llama2": {}
|
}
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -5,7 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.2.14",
|
"packageManager": "bun@1.2.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "bun run --filter='*' typecheck"
|
"typecheck": "bun run --filter='*' typecheck",
|
||||||
|
"postinstall": "./scripts/hooks"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"sst": "3.17.4"
|
"sst": "3.17.6"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -37,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" },
|
||||||
|
|||||||
Vendored
+8
-8
@@ -6,20 +6,20 @@
|
|||||||
import "sst"
|
import "sst"
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
"Web": {
|
Web: {
|
||||||
"type": "sst.cloudflare.Astro"
|
type: "sst.cloudflare.Astro"
|
||||||
"url": string
|
url: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// cloudflare
|
// cloudflare
|
||||||
import * as cloudflare from "@cloudflare/workers-types";
|
import * as cloudflare from "@cloudflare/workers-types"
|
||||||
declare module "sst" {
|
declare module "sst" {
|
||||||
export interface Resource {
|
export interface Resource {
|
||||||
"Api": cloudflare.Service
|
Api: cloudflare.Service
|
||||||
"Bucket": cloudflare.R2Bucket
|
Bucket: cloudflare.R2Bucket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
export {}
|
export {}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@
|
|||||||
|
|
||||||
## IMPORTANT
|
## IMPORTANT
|
||||||
|
|
||||||
- Try to keep things in one function unless composable or reusablte
|
- 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 `try`/`catch` where possible
|
||||||
- AVOID else statements
|
- AVOID `else` statements
|
||||||
- AVOID using `any` type
|
- AVOID using `any` type
|
||||||
- AVOID let statements
|
- AVOID `let` statements
|
||||||
- PREFER single word variable names where possible
|
- PREFER single word variable names where possible
|
||||||
- Use as many bun apis as possible like Bun.file()
|
- Use as many bun apis as possible like Bun.file()
|
||||||
|
|
||||||
@@ -37,3 +37,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 stainless SDK. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, ask the user to generate a new client SDK to proceed with client-side changes.
|
||||||
|
|||||||
@@ -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%" %*
|
||||||
@@ -2,13 +2,155 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"$schema": {
|
"$schema": {
|
||||||
"type": "string"
|
"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": {
|
"provider": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"api": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -21,6 +163,9 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"npm": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
@@ -38,6 +183,9 @@
|
|||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"tool_call": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"cost": {
|
"cost": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -47,19 +195,14 @@
|
|||||||
"output": {
|
"output": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"inputCached": {
|
"cache_read": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"outputCached": {
|
"cache_write": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["input", "output"],
|
||||||
"input",
|
|
||||||
"output",
|
|
||||||
"inputCached",
|
|
||||||
"outputCached"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"limit": {
|
"limit": {
|
||||||
@@ -72,14 +215,15 @@
|
|||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["context", "output"],
|
||||||
"context",
|
|
||||||
"output"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -90,11 +234,10 @@
|
|||||||
"additionalProperties": {}
|
"additionalProperties": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["models"],
|
||||||
"models"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
},
|
||||||
|
"description": "Custom provider configurations and model overrides"
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -105,25 +248,25 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "local"
|
"const": "local",
|
||||||
|
"description": "Type of MCP server connection"
|
||||||
},
|
},
|
||||||
"command": {
|
"command": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
},
|
||||||
|
"description": "Command and arguments to run the MCP server"
|
||||||
},
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
},
|
||||||
|
"description": "Environment variables to set when running the MCP server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["type", "command"],
|
||||||
"type",
|
|
||||||
"command"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -131,22 +274,81 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "remote"
|
"const": "remote",
|
||||||
|
"description": "Type of MCP server connection"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "URL of the remote MCP server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": ["type", "url"],
|
||||||
"type",
|
|
||||||
"url"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"description": "MCP (Model Context Protocol) server configurations"
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hook": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"file_edited": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["command"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"session_completed": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["command"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "0.0.0",
|
"version": "0.0.5",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -8,13 +8,15 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"dev": "bun run ./src/index.ts"
|
"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",
|
||||||
@@ -44,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ for (const [os, arch] of targets) {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -142,7 +142,7 @@ if (!snapshot) {
|
|||||||
"# Maintainer: dax",
|
"# Maintainer: dax",
|
||||||
"# Maintainer: adam",
|
"# Maintainer: adam",
|
||||||
"",
|
"",
|
||||||
"pkgname='opencode-bin'",
|
"pkgname='${pkg}'",
|
||||||
`pkgver=${version.split("-")[0]}`,
|
`pkgver=${version.split("-")[0]}`,
|
||||||
"options=('!debug' '!strip')",
|
"options=('!debug' '!strip')",
|
||||||
"pkgrel=1",
|
"pkgrel=1",
|
||||||
@@ -166,14 +166,17 @@ if (!snapshot) {
|
|||||||
"",
|
"",
|
||||||
].join("\n")
|
].join("\n")
|
||||||
|
|
||||||
await $`rm -rf ./dist/aur-opencode-bin`
|
for (const pkg of ["opencode", "opencode-bin"]) {
|
||||||
|
await $`rm -rf ./dist/aur-${pkg}`
|
||||||
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`
|
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-${pkg}`
|
||||||
await Bun.file("./dist/aur-opencode-bin/PKGBUILD").write(pkgbuild)
|
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(
|
||||||
await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
|
pkgbuild.replace("${pkg}", pkg),
|
||||||
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`
|
)
|
||||||
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${version}"`
|
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
|
||||||
if (!dry) await $`cd ./dist/aur-opencode-bin && git push`
|
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
|
||||||
|
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
|
||||||
|
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
|
||||||
|
}
|
||||||
|
|
||||||
// Homebrew formula
|
// Homebrew formula
|
||||||
const homebrewFormula = [
|
const homebrewFormula = [
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -12,27 +13,39 @@ export namespace App {
|
|||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
user: z.string(),
|
user: z.string(),
|
||||||
|
hostname: z.string(),
|
||||||
git: z.boolean(),
|
git: z.boolean(),
|
||||||
path: z.object({
|
path: z.object({
|
||||||
config: z.string(),
|
config: z.string(),
|
||||||
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(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "App.Info",
|
ref: "App",
|
||||||
})
|
})
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
const ctx = Context.create<Awaited<ReturnType<typeof create>>>("app")
|
const ctx = Context.create<{
|
||||||
|
info: Info
|
||||||
|
services: Map<any, { state: any; shutdown?: (input: any) => Promise<void> }>
|
||||||
|
}>("app")
|
||||||
|
|
||||||
const APP_JSON = "app.json"
|
const APP_JSON = "app.json"
|
||||||
|
|
||||||
async function create(input: { cwd: string; version: string }) {
|
export type Input = {
|
||||||
|
cwd: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function provide<T>(
|
||||||
|
input: Input,
|
||||||
|
cb: (app: App.Info) => Promise<T>,
|
||||||
|
) {
|
||||||
log.info("creating", {
|
log.info("creating", {
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
})
|
})
|
||||||
@@ -44,14 +57,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<
|
||||||
@@ -62,26 +73,37 @@ export namespace App {
|
|||||||
}
|
}
|
||||||
>()
|
>()
|
||||||
|
|
||||||
|
const root = git ?? input.cwd
|
||||||
|
|
||||||
const info: Info = {
|
const info: Info = {
|
||||||
user: os.userInfo().username,
|
user: os.userInfo().username,
|
||||||
|
hostname: os.hostname(),
|
||||||
time: {
|
time: {
|
||||||
initialized: state.initialized,
|
initialized: state.initialized,
|
||||||
},
|
},
|
||||||
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,
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const result = {
|
const app = {
|
||||||
version: input.version,
|
|
||||||
services,
|
services,
|
||||||
info,
|
info,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return ctx.provide(app, async () => {
|
||||||
|
const result = await cb(app.info)
|
||||||
|
for (const [key, entry] of app.services.entries()) {
|
||||||
|
if (!entry.shutdown) continue
|
||||||
|
log.info("shutdown", { name: key })
|
||||||
|
await entry.shutdown?.(await entry.state)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function state<State>(
|
export function state<State>(
|
||||||
@@ -107,31 +129,22 @@ export namespace App {
|
|||||||
return ctx.use().info
|
return ctx.use().info
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function provide<T>(
|
|
||||||
input: { cwd: string; version: string },
|
|
||||||
cb: (app: Info) => Promise<T>,
|
|
||||||
) {
|
|
||||||
const app = await create(input)
|
|
||||||
return ctx.provide(app, async () => {
|
|
||||||
const result = await cb(app.info)
|
|
||||||
for (const [key, entry] of app.services.entries()) {
|
|
||||||
if (!entry.shutdown) continue
|
|
||||||
log.info("shutdown", { name: key })
|
|
||||||
await entry.shutdown?.(await entry.state)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Global } from "../global"
|
|||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
|
import { readableStreamToText } from "bun"
|
||||||
|
|
||||||
export namespace BunProc {
|
export namespace BunProc {
|
||||||
const log = Log.create({ service: "bun" })
|
const log = Log.create({ service: "bun" })
|
||||||
@@ -13,7 +14,7 @@ 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,
|
||||||
@@ -25,7 +26,14 @@ export namespace BunProc {
|
|||||||
BUN_BE_BUN: "1",
|
BUN_BE_BUN: "1",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const code = await result.exited
|
const code = await result.exited;
|
||||||
|
const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined
|
||||||
|
const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined
|
||||||
|
log.info("done", {
|
||||||
|
code,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
})
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
throw new Error(`Command failed with exit code ${result.exitCode}`)
|
throw new Error(`Command failed with exit code ${result.exitCode}`)
|
||||||
}
|
}
|
||||||
@@ -43,6 +51,7 @@ export namespace BunProc {
|
|||||||
version: z.string(),
|
version: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function install(pkg: string, version = "latest") {
|
export async function install(pkg: string, version = "latest") {
|
||||||
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
||||||
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
||||||
@@ -52,10 +61,10 @@ export namespace BunProc {
|
|||||||
if (parsed.dependencies[pkg] === version) return mod
|
if (parsed.dependencies[pkg] === version) return mod
|
||||||
parsed.dependencies[pkg] = version
|
parsed.dependencies[pkg] = version
|
||||||
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
||||||
await BunProc.run(["install"], {
|
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
|
||||||
cwd: Global.Path.cache,
|
cwd: Global.Path.cache,
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
new InstallFailedError(
|
throw new InstallFailedError(
|
||||||
{ pkg, version },
|
{ pkg, version },
|
||||||
{
|
{
|
||||||
cause: e,
|
cause: e,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export namespace Bus {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function publish<Definition extends EventDefinition>(
|
export async function publish<Definition extends EventDefinition>(
|
||||||
def: Definition,
|
def: Definition,
|
||||||
properties: z.output<Definition["properties"]>,
|
properties: z.output<Definition["properties"]>,
|
||||||
) {
|
) {
|
||||||
@@ -60,12 +60,14 @@ export namespace Bus {
|
|||||||
log.info("publishing", {
|
log.info("publishing", {
|
||||||
type: def.type,
|
type: def.type,
|
||||||
})
|
})
|
||||||
|
const pending = []
|
||||||
for (const key of [def.type, "*"]) {
|
for (const key of [def.type, "*"]) {
|
||||||
const match = state().subscriptions.get(key)
|
const match = state().subscriptions.get(key)
|
||||||
for (const sub of match ?? []) {
|
for (const sub of match ?? []) {
|
||||||
sub(payload)
|
pending.push(sub(payload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Promise.all(pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribe<Definition extends EventDefinition>(
|
export function subscribe<Definition extends EventDefinition>(
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { App } from "../app/app"
|
||||||
|
import { ConfigHooks } from "../config/hooks"
|
||||||
|
import { Format } from "../format"
|
||||||
|
import { Share } from "../share/share"
|
||||||
|
|
||||||
|
export async function bootstrap<T>(
|
||||||
|
input: App.Input,
|
||||||
|
cb: (app: App.Info) => Promise<T>,
|
||||||
|
) {
|
||||||
|
return App.provide(input, async (app) => {
|
||||||
|
Share.init()
|
||||||
|
Format.init()
|
||||||
|
ConfigHooks.init()
|
||||||
|
|
||||||
|
return cb(app)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,11 +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, sort, sortBy, values } from "remeda"
|
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",
|
||||||
@@ -16,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({
|
||||||
@@ -25,30 +29,61 @@ 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 providers = await ModelsDev.get()
|
const providers = await ModelsDev.get()
|
||||||
const priority: Record<string, number> = {
|
const priority: Record<string, number> = {
|
||||||
anthropic: 0,
|
anthropic: 0,
|
||||||
openai: 1,
|
"github-copilot": 1,
|
||||||
google: 2,
|
openai: 2,
|
||||||
|
google: 3,
|
||||||
}
|
}
|
||||||
let provider = await prompts.select({
|
let provider = await prompts.select({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
@@ -78,9 +113,16 @@ export const AuthLoginCommand = cmd({
|
|||||||
|
|
||||||
if (provider === "other") {
|
if (provider === "other") {
|
||||||
provider = await prompts.text({
|
provider = await prompts.text({
|
||||||
message: "Enter provider - must match @ai-sdk/<provider>",
|
message: "Enter provider id",
|
||||||
|
validate: (x) =>
|
||||||
|
x.match(/^[a-z-]+$/) ? undefined : "a-z and hyphens only",
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(provider)) throw new UI.CancelledError()
|
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") {
|
if (provider === "amazon-bedrock") {
|
||||||
@@ -115,7 +157,9 @@ export const AuthLoginCommand = cmd({
|
|||||||
try {
|
try {
|
||||||
await open(url)
|
await open(url)
|
||||||
} catch (e) {
|
} 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.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)
|
||||||
|
|
||||||
@@ -137,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"),
|
||||||
@@ -153,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))
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Server } from "../../server/server"
|
|||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import type { CommandModule } from "yargs"
|
import type { CommandModule } from "yargs"
|
||||||
import { Config } from "../../config/config"
|
|
||||||
|
|
||||||
export const GenerateCommand = {
|
export const GenerateCommand = {
|
||||||
command: "generate",
|
command: "generate",
|
||||||
|
|||||||
@@ -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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,136 +1,163 @@
|
|||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
import { App } from "../../app/app"
|
|
||||||
import { Bus } from "../../bus"
|
import { Bus } from "../../bus"
|
||||||
import { Provider } from "../../provider/provider"
|
import { Provider } from "../../provider/provider"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
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,
|
import { bootstrap } from "../bootstrap"
|
||||||
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 bootstrap({ cwd: process.cwd() }, async () => {
|
||||||
{
|
const session = await (async () => {
|
||||||
cwd: process.cwd(),
|
if (args.continue) {
|
||||||
version: VERSION,
|
const first = await Session.list().next()
|
||||||
},
|
if (first.done) return
|
||||||
async () => {
|
return first.value
|
||||||
await Share.init()
|
|
||||||
const session = args.session
|
|
||||||
? await Session.get(args.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/" +
|
|
||||||
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 (evt) => {
|
if (args.session) return Session.get(args.session)
|
||||||
if (evt.properties.sessionID !== session.id) return
|
|
||||||
const part = evt.properties.part
|
|
||||||
const message = await Session.getMessage(
|
|
||||||
evt.properties.sessionID,
|
|
||||||
evt.properties.messageID,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
return Session.create()
|
||||||
part.type === "tool-invocation" &&
|
})()
|
||||||
part.toolInvocation.state === "result"
|
|
||||||
) {
|
if (!session) {
|
||||||
const metadata =
|
UI.error("Session not found")
|
||||||
message.metadata.tool[part.toolInvocation.toolCallId]
|
return
|
||||||
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
|
}
|
||||||
part.toolInvocation.toolName,
|
|
||||||
UI.Style.TEXT_INFO_BOLD,
|
const isPiped = !process.stdout.isTTY
|
||||||
]
|
|
||||||
printEvent(color, tool, metadata.title)
|
UI.empty()
|
||||||
|
UI.println(UI.logo())
|
||||||
|
UI.empty()
|
||||||
|
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||||
|
UI.empty()
|
||||||
|
|
||||||
|
const cfg = await Config.get()
|
||||||
|
if (cfg.autoshare || Flag.OPENCODE_AUTO_SHARE || args.share) {
|
||||||
|
await Session.share(session.id)
|
||||||
|
UI.println(
|
||||||
|
UI.Style.TEXT_INFO_BOLD +
|
||||||
|
"~ https://opencode.ai/s/" +
|
||||||
|
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()
|
||||||
|
|
||||||
|
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 (evt) => {
|
||||||
|
if (evt.properties.sessionID !== session.id) return
|
||||||
|
const part = evt.properties.part
|
||||||
|
const message = await Session.getMessage(
|
||||||
|
evt.properties.sessionID,
|
||||||
|
evt.properties.messageID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
part.type === "tool-invocation" &&
|
||||||
|
part.toolInvocation.state === "result"
|
||||||
|
) {
|
||||||
|
const metadata = message.metadata.tool[part.toolInvocation.toolCallId]
|
||||||
|
const [tool, color] = TOOL[part.toolInvocation.toolName] ?? [
|
||||||
|
part.toolInvocation.toolName,
|
||||||
|
UI.Style.TEXT_INFO_BOLD,
|
||||||
|
]
|
||||||
|
printEvent(color, tool, metadata?.title || "Unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (part.type === "text") {
|
const result = await Session.chat({
|
||||||
if (part.text.includes("\n")) {
|
sessionID: session.id,
|
||||||
UI.empty()
|
providerID,
|
||||||
UI.println(part.text)
|
modelID,
|
||||||
UI.empty()
|
parts: [
|
||||||
return
|
{
|
||||||
}
|
type: "text",
|
||||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
text: message,
|
||||||
}
|
},
|
||||||
})
|
],
|
||||||
|
})
|
||||||
|
|
||||||
const { providerID, modelID } = await Provider.defaultModel()
|
if (isPiped) {
|
||||||
setTimeout(() => {
|
const match = result.parts.findLast((x) => x.type === "text")
|
||||||
Session.abort(session.id)
|
if (match) process.stdout.write(match.text)
|
||||||
}, 8000)
|
}
|
||||||
await Session.chat({
|
UI.empty()
|
||||||
sessionID: session.id,
|
})
|
||||||
providerID,
|
|
||||||
modelID,
|
|
||||||
parts: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
UI.empty()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { App } from "../../app/app"
|
import { App } from "../../app/app"
|
||||||
import { LSP } from "../../lsp"
|
import { LSP } from "../../lsp"
|
||||||
import { VERSION } from "../version"
|
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
|
||||||
export const ScrapCommand = cmd({
|
export const ScrapCommand = cmd({
|
||||||
@@ -8,7 +7,7 @@ export const ScrapCommand = cmd({
|
|||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("file", { type: "string", demandOption: true }),
|
yargs.positional("file", { type: "string", demandOption: true }),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await App.provide({ cwd: process.cwd(), version: VERSION }, async (app) => {
|
await App.provide({ cwd: process.cwd() }, async () => {
|
||||||
await LSP.touchFile(args.file, true)
|
await LSP.touchFile(args.file, true)
|
||||||
console.log(await LSP.diagnostics())
|
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,114 @@
|
|||||||
|
import { Global } from "../../global"
|
||||||
|
import { Provider } from "../../provider/provider"
|
||||||
|
import { Server } from "../../server/server"
|
||||||
|
import { bootstrap } from "../bootstrap"
|
||||||
|
import { UI } from "../ui"
|
||||||
|
import { cmd } from "./cmd"
|
||||||
|
import path from "path"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import { Installation } from "../../installation"
|
||||||
|
import { Config } from "../../config/config"
|
||||||
|
import { Bus } from "../../bus"
|
||||||
|
|
||||||
|
export const TuiCommand = cmd({
|
||||||
|
command: "$0 [project]",
|
||||||
|
describe: "start opencode tui",
|
||||||
|
builder: (yargs) =>
|
||||||
|
yargs.positional("project", {
|
||||||
|
type: "string",
|
||||||
|
describe: "path to start opencode in",
|
||||||
|
}),
|
||||||
|
handler: async (args) => {
|
||||||
|
while (true) {
|
||||||
|
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||||
|
try {
|
||||||
|
process.chdir(cwd)
|
||||||
|
} catch (e) {
|
||||||
|
UI.error("Failed to change directory to " + cwd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await bootstrap({ cwd }, async (app) => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
if (Object.keys(providers).length === 0) {
|
||||||
|
return "needs_provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = Server.listen({
|
||||||
|
port: 0,
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
})
|
||||||
|
|
||||||
|
let cmd = ["go", "run", "./main.go"]
|
||||||
|
let cwd = Bun.fileURLToPath(
|
||||||
|
new URL("../../../../tui/cmd/opencode", import.meta.url),
|
||||||
|
)
|
||||||
|
if (Bun.embeddedFiles.length > 0) {
|
||||||
|
const blob = Bun.embeddedFiles[0] as File
|
||||||
|
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)
|
||||||
|
if (!(await file.exists())) {
|
||||||
|
await Bun.write(file, blob, { mode: 0o755 })
|
||||||
|
await fs.chmod(binary, 0o755)
|
||||||
|
}
|
||||||
|
cwd = process.cwd()
|
||||||
|
cmd = [binary]
|
||||||
|
}
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: [...cmd, ...process.argv.slice(2)],
|
||||||
|
cwd,
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
stdin: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_SERVER: server.url.toString(),
|
||||||
|
OPENCODE_APP_INFO: JSON.stringify(app),
|
||||||
|
},
|
||||||
|
onExit: () => {
|
||||||
|
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
|
||||||
|
server.stop()
|
||||||
|
|
||||||
|
return "done"
|
||||||
|
})
|
||||||
|
if (result === "done") break
|
||||||
|
if (result === "needs_provider") {
|
||||||
|
UI.empty()
|
||||||
|
UI.println(UI.logo(" "))
|
||||||
|
const result = await Bun.spawn({
|
||||||
|
cmd: [process.execPath, "auth", "login"],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
stdin: "inherit",
|
||||||
|
}).exited
|
||||||
|
if (result !== 0) return
|
||||||
|
UI.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,184 +1,53 @@
|
|||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { VERSION } from "../version"
|
|
||||||
import path from "path"
|
|
||||||
import fs from "fs/promises"
|
|
||||||
import os from "os"
|
|
||||||
import * as prompts from "@clack/prompts"
|
import * as prompts from "@clack/prompts"
|
||||||
import { Global } from "../../global"
|
import { Installation } from "../../installation"
|
||||||
|
|
||||||
const API = "https://api.github.com/repos/sst/opencode"
|
|
||||||
|
|
||||||
interface Release {
|
|
||||||
tag_name: string
|
|
||||||
name: string
|
|
||||||
assets: Array<{
|
|
||||||
name: string
|
|
||||||
browser_download_url: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
function asset(): string {
|
|
||||||
const platform = os.platform()
|
|
||||||
const arch = os.arch()
|
|
||||||
|
|
||||||
if (platform === "darwin") {
|
|
||||||
return arch === "arm64"
|
|
||||||
? "opencode-darwin-arm64.zip"
|
|
||||||
: "opencode-darwin-x64.zip"
|
|
||||||
}
|
|
||||||
if (platform === "linux") {
|
|
||||||
return arch === "arm64"
|
|
||||||
? "opencode-linux-arm64.zip"
|
|
||||||
: "opencode-linux-x64.zip"
|
|
||||||
}
|
|
||||||
if (platform === "win32") {
|
|
||||||
return "opencode-windows-x64.zip"
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare(current: string, latest: string): number {
|
|
||||||
const a = current.replace(/^v/, "")
|
|
||||||
const b = latest.replace(/^v/, "")
|
|
||||||
|
|
||||||
const aParts = a.split(".").map(Number)
|
|
||||||
const bParts = b.split(".").map(Number)
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
||||||
const aPart = aParts[i] || 0
|
|
||||||
const bPart = bParts[i] || 0
|
|
||||||
|
|
||||||
if (aPart < bPart) return -1
|
|
||||||
if (aPart > bPart) return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
async function latest(): Promise<Release> {
|
|
||||||
const response = await fetch(`${API}/releases/latest`)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch latest release: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function specific(version: string): Promise<Release> {
|
|
||||||
const tag = version.startsWith("v") ? version : `v${version}`
|
|
||||||
const response = await fetch(`${API}/releases/tags/${tag}`)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch release ${tag}: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function download(url: string): Promise<string> {
|
|
||||||
const response = await fetch(url)
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to download: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await response.arrayBuffer()
|
|
||||||
const temp = path.join(Global.Path.cache, `opencode-update-${Date.now()}.zip`)
|
|
||||||
|
|
||||||
await Bun.write(temp, buffer)
|
|
||||||
|
|
||||||
const extractDir = path.join(
|
|
||||||
Global.Path.cache,
|
|
||||||
`opencode-extract-${Date.now()}`,
|
|
||||||
)
|
|
||||||
await fs.mkdir(extractDir, { recursive: true })
|
|
||||||
|
|
||||||
const proc = Bun.spawn(["unzip", "-o", temp, "-d", extractDir], {
|
|
||||||
stdout: "pipe",
|
|
||||||
stderr: "pipe",
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await proc.exited
|
|
||||||
if (result !== 0) {
|
|
||||||
throw new Error("Failed to extract update")
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.unlink(temp)
|
|
||||||
|
|
||||||
const binary = path.join(extractDir, "opencode")
|
|
||||||
await fs.chmod(binary, 0o755)
|
|
||||||
|
|
||||||
return binary
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UpgradeCommand = {
|
export const UpgradeCommand = {
|
||||||
command: "upgrade [target]",
|
command: "upgrade [target]",
|
||||||
describe: "Upgrade opencode to the latest version or a specific version",
|
describe: "upgrade opencode to the latest or a specific version",
|
||||||
builder: (yargs: Argv) => {
|
builder: (yargs: Argv) => {
|
||||||
return yargs.positional("target", {
|
return yargs
|
||||||
describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
|
.positional("target", {
|
||||||
type: "string",
|
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 }) => {
|
handler: async (args: { target?: string; method?: string }) => {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.logo(" "))
|
UI.println(UI.logo(" "))
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("upgrade")
|
prompts.intro("Upgrade")
|
||||||
|
const detectedMethod = await Installation.method()
|
||||||
if (!process.execPath.includes(path.join(".opencode", "bin")) && false) {
|
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||||
|
if (method === "unknown") {
|
||||||
prompts.log.error(
|
prompts.log.error(
|
||||||
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
|
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
|
||||||
)
|
)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
prompts.log.info("Using method: " + method)
|
||||||
const release = args.target ? await specific(args.target) : await latest()
|
const target = args.target ?? (await Installation.latest())
|
||||||
const target = release.tag_name
|
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||||
|
|
||||||
prompts.log.info(`Upgrade ${VERSION} → ${target}`)
|
|
||||||
|
|
||||||
if (VERSION !== "dev" && compare(VERSION, target) >= 0) {
|
|
||||||
prompts.log.success(`Already up to date`)
|
|
||||||
prompts.outro("Done")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = asset()
|
|
||||||
const found = release.assets.find((a) => a.name === name)
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
prompts.log.error(`No binary found for platform: ${name}`)
|
|
||||||
prompts.outro("Done")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const spinner = prompts.spinner()
|
const spinner = prompts.spinner()
|
||||||
spinner.start("Downloading update...")
|
spinner.start("Upgrading...")
|
||||||
|
const err = await Installation.upgrade(method, target).catch((err) => err)
|
||||||
let downloadPath: string
|
if (err) {
|
||||||
try {
|
spinner.stop("Upgrade failed")
|
||||||
downloadPath = await download(found.browser_download_url)
|
if (err instanceof Installation.UpgradeFailedError)
|
||||||
spinner.stop("Download complete")
|
prompts.log.error(err.data.stderr)
|
||||||
} catch (downloadError) {
|
else if (err instanceof Error) prompts.log.error(err.message)
|
||||||
spinner.stop("Download failed")
|
|
||||||
prompts.log.error(
|
|
||||||
`Download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`,
|
|
||||||
)
|
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
spinner.stop("Upgrade complete")
|
||||||
try {
|
|
||||||
await fs.rename(downloadPath, process.execPath)
|
|
||||||
prompts.log.success(`Successfully upgraded to ${target}`)
|
|
||||||
} catch (installError) {
|
|
||||||
prompts.log.error(
|
|
||||||
`Install failed: ${installError instanceof Error ? installError.message : String(installError)}`,
|
|
||||||
)
|
|
||||||
// Clean up downloaded file
|
|
||||||
await fs.unlink(downloadPath).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
prompts.outro("Done")
|
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,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,56 +1,160 @@
|
|||||||
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 { 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: "McpLocalConfig",
|
||||||
|
})
|
||||||
|
|
||||||
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: "McpRemoteConfig",
|
||||||
|
})
|
||||||
|
|
||||||
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 Keybinds = z
|
||||||
|
.object({
|
||||||
|
leader: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Leader key for keybind combinations"),
|
||||||
|
help: z.string().optional().describe("Show help dialog"),
|
||||||
|
editor_open: z.string().optional().describe("Open external editor"),
|
||||||
|
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()
|
||||||
|
.openapi({
|
||||||
|
ref: "KeybindsConfig",
|
||||||
|
})
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
$schema: z.string().optional(),
|
$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
|
provider: z
|
||||||
.record(
|
.record(
|
||||||
ModelsDev.Provider.partial().extend({
|
ModelsDev.Provider.partial().extend({
|
||||||
@@ -58,13 +162,98 @@ export namespace Config {
|
|||||||
options: z.record(z.any()).optional(),
|
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"),
|
||||||
|
experimental: z
|
||||||
|
.object({
|
||||||
|
hook: z
|
||||||
|
.object({
|
||||||
|
file_edited: z
|
||||||
|
.record(
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
command: z.string().array(),
|
||||||
|
environment: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
session_completed: z
|
||||||
|
.object({
|
||||||
|
command: z.string().array(),
|
||||||
|
environment: z.record(z.string(), z.string()).optional(),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
mcp: z.record(z.string(), Mcp).optional(),
|
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
.openapi({
|
||||||
|
ref: "Config",
|
||||||
|
})
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { App } from "../app/app"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { File } from "../file"
|
||||||
|
import { Session } from "../session"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
import { Config } from "./config"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export namespace ConfigHooks {
|
||||||
|
const log = Log.create({ service: "config.hooks" })
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
log.info("init")
|
||||||
|
const app = App.info()
|
||||||
|
|
||||||
|
Bus.subscribe(File.Event.Edited, async (payload) => {
|
||||||
|
const cfg = await Config.get()
|
||||||
|
const ext = path.extname(payload.properties.file)
|
||||||
|
for (const item of cfg.experimental?.hook?.file_edited?.[ext] ?? []) {
|
||||||
|
log.info("file_edited", {
|
||||||
|
file: payload.properties.file,
|
||||||
|
command: item.command,
|
||||||
|
})
|
||||||
|
Bun.spawn({
|
||||||
|
cmd: item.command.map((x) =>
|
||||||
|
x.replace("$FILE", payload.properties.file),
|
||||||
|
),
|
||||||
|
env: item.environment,
|
||||||
|
cwd: app.path.cwd,
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Bus.subscribe(Session.Event.Idle, async () => {
|
||||||
|
const cfg = await Config.get()
|
||||||
|
if (cfg.experimental?.hook?.session_completed) {
|
||||||
|
for (const item of cfg.experimental.hook.session_completed) {
|
||||||
|
log.info("session_completed", {
|
||||||
|
command: item.command,
|
||||||
|
})
|
||||||
|
Bun.spawn({
|
||||||
|
cmd: item.command,
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
env: item.environment,
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-3
@@ -116,11 +116,15 @@ export namespace Fzf {
|
|||||||
return filepath
|
return filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(cwd: string, query: string) {
|
export async function search(input: {
|
||||||
const results = await $`${await filepath()} --filter ${query}`
|
cwd: string
|
||||||
|
query: string
|
||||||
|
limit?: number
|
||||||
|
}) {
|
||||||
|
const results = await $`${await filepath()} --filter=${input.query}`
|
||||||
.quiet()
|
.quiet()
|
||||||
.throws(false)
|
.throws(false)
|
||||||
.cwd(cwd)
|
.cwd(input.cwd)
|
||||||
.text()
|
.text()
|
||||||
const split = results
|
const split = results
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
+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 +1,13 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
|
||||||
export namespace File {
|
export namespace File {
|
||||||
const glob = new Bun.Glob("**/*")
|
export const Event = {
|
||||||
export async function search(path: string, query: string) {
|
Edited: Bus.event(
|
||||||
for await (const entry of glob.scan({
|
"file.edited",
|
||||||
cwd: path,
|
z.object({
|
||||||
onlyFiles: true,
|
file: z.string(),
|
||||||
})) {
|
}),
|
||||||
}
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { App } from "../../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
export namespace FileTimes {
|
export namespace FileTime {
|
||||||
export const state = App.state("tool.filetimes", () => {
|
export const state = App.state("tool.filetimes", () => {
|
||||||
const read: {
|
const read: {
|
||||||
[sessionID: string]: {
|
[sessionID: string]: {
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { App } from "../app/app"
|
||||||
|
import { BunProc } from "../bun"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { File } from "../file"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export namespace Format {
|
||||||
|
const log = Log.create({ service: "format" })
|
||||||
|
|
||||||
|
const state = App.state("format", () => {
|
||||||
|
const enabled: Record<string, boolean> = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function isEnabled(item: Definition) {
|
||||||
|
const s = state()
|
||||||
|
let status = s.enabled[item.name]
|
||||||
|
if (status === undefined) {
|
||||||
|
status = await item.enabled()
|
||||||
|
s.enabled[item.name] = status
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFormatter(ext: string) {
|
||||||
|
const result = []
|
||||||
|
for (const item of FORMATTERS) {
|
||||||
|
if (!item.extensions.includes(ext)) continue
|
||||||
|
if (!isEnabled(item)) continue
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
log.info("init")
|
||||||
|
Bus.subscribe(File.Event.Edited, async (payload) => {
|
||||||
|
const file = payload.properties.file
|
||||||
|
log.info("formatting", { file })
|
||||||
|
const ext = path.extname(file)
|
||||||
|
|
||||||
|
for (const item of await getFormatter(ext)) {
|
||||||
|
log.info("running", { command: item.command })
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
env: item.environment,
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
const exit = await proc.exited
|
||||||
|
if (exit !== 0)
|
||||||
|
log.error("failed", {
|
||||||
|
command: item.command,
|
||||||
|
...item.environment,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Definition {
|
||||||
|
name: string
|
||||||
|
command: string[]
|
||||||
|
environment?: Record<string, string>
|
||||||
|
extensions: string[]
|
||||||
|
enabled(): Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
const FORMATTERS: Definition[] = [
|
||||||
|
{
|
||||||
|
name: "prettier",
|
||||||
|
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
|
||||||
|
environment: {
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
extensions: [
|
||||||
|
".js",
|
||||||
|
".jsx",
|
||||||
|
".mjs",
|
||||||
|
".cjs",
|
||||||
|
".ts",
|
||||||
|
".tsx",
|
||||||
|
".mts",
|
||||||
|
".cts",
|
||||||
|
".html",
|
||||||
|
".htm",
|
||||||
|
".css",
|
||||||
|
".scss",
|
||||||
|
".sass",
|
||||||
|
".less",
|
||||||
|
".vue",
|
||||||
|
".svelte",
|
||||||
|
".json",
|
||||||
|
".jsonc",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
".toml",
|
||||||
|
".xml",
|
||||||
|
".md",
|
||||||
|
".mdx",
|
||||||
|
".graphql",
|
||||||
|
".gql",
|
||||||
|
],
|
||||||
|
async enabled() {
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: [BunProc.which(), "run", "prettier", "--version"],
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
env: {
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
const exit = await proc.exited
|
||||||
|
return exit === 0
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix",
|
||||||
|
command: ["mix", "format", "$FILE"],
|
||||||
|
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
|
||||||
|
async enabled() {
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: ["mix", "--version"],
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
const exit = await proc.exited
|
||||||
|
return exit === 0
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gofmt",
|
||||||
|
command: ["gofmt", "-w", "$FILE"],
|
||||||
|
extensions: [".go"],
|
||||||
|
async enabled() {
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawn({
|
||||||
|
cmd: ["gofmt", "-h"],
|
||||||
|
cwd: App.info().path.cwd,
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "ignore",
|
||||||
|
})
|
||||||
|
const exit = await proc.exited
|
||||||
|
return exit === 0
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,107 @@
|
|||||||
import "zod-openapi/extend"
|
import "zod-openapi/extend"
|
||||||
import { App } from "./app/app"
|
|
||||||
import { Server } from "./server/server"
|
|
||||||
import fs from "fs/promises"
|
|
||||||
import path from "path"
|
|
||||||
import { Share } from "./share/share"
|
|
||||||
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 } from "./cli/cmd/auth"
|
||||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||||
import { Provider } from "./provider/provider"
|
import { ModelsCommand } from "./cli/cmd/models"
|
||||||
import { UI } from "./cli/ui"
|
import { UI } from "./cli/ui"
|
||||||
|
import { Installation } from "./installation"
|
||||||
|
import { NamedError } from "./util/error"
|
||||||
|
import { FormatError } from "./cli/error"
|
||||||
|
import { ServeCommand } from "./cli/cmd/serve"
|
||||||
|
import { TuiCommand } from "./cli/cmd/tui"
|
||||||
|
|
||||||
|
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(TuiCommand)
|
||||||
command: "$0 [project]",
|
|
||||||
describe: "Start opencode TUI",
|
|
||||||
builder: (yargs) =>
|
|
||||||
yargs.positional("project", {
|
|
||||||
type: "string",
|
|
||||||
describe: "path to start opencode in",
|
|
||||||
}),
|
|
||||||
handler: async (args) => {
|
|
||||||
while (true) {
|
|
||||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
|
||||||
process.chdir(cwd)
|
|
||||||
const result = await App.provide(
|
|
||||||
{ cwd, version: VERSION },
|
|
||||||
async () => {
|
|
||||||
const providers = await Provider.list()
|
|
||||||
if (Object.keys(providers).length === 0) {
|
|
||||||
return "needs_provider"
|
|
||||||
}
|
|
||||||
|
|
||||||
await Share.init()
|
|
||||||
const server = Server.listen()
|
|
||||||
|
|
||||||
let cmd = ["go", "run", "./main.go"]
|
|
||||||
let cwd = new URL("../../tui/cmd/opencode", import.meta.url)
|
|
||||||
.pathname
|
|
||||||
if (Bun.embeddedFiles.length > 0) {
|
|
||||||
const blob = Bun.embeddedFiles[0] as File
|
|
||||||
const binary = path.join(Global.Path.cache, "tui", blob.name)
|
|
||||||
const file = Bun.file(binary)
|
|
||||||
if (!(await file.exists())) {
|
|
||||||
await Bun.write(file, blob, { mode: 0o755 })
|
|
||||||
await fs.chmod(binary, 0o755)
|
|
||||||
}
|
|
||||||
cwd = process.cwd()
|
|
||||||
cmd = [binary]
|
|
||||||
}
|
|
||||||
const proc = Bun.spawn({
|
|
||||||
cmd: [...cmd, ...process.argv.slice(2)],
|
|
||||||
cwd,
|
|
||||||
stdout: "inherit",
|
|
||||||
stderr: "inherit",
|
|
||||||
stdin: "inherit",
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
OPENCODE_SERVER: server.url.toString(),
|
|
||||||
},
|
|
||||||
onExit: () => {
|
|
||||||
server.stop()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await proc.exited
|
|
||||||
await server.stop()
|
|
||||||
|
|
||||||
return "done"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (result === "done") break
|
|
||||||
if (result === "needs_provider") {
|
|
||||||
UI.empty()
|
|
||||||
UI.println(UI.logo(" "))
|
|
||||||
UI.empty()
|
|
||||||
await AuthLoginCommand.handler(args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.command(RunCommand)
|
.command(RunCommand)
|
||||||
.command(GenerateCommand)
|
.command(GenerateCommand)
|
||||||
.command(ScrapCommand)
|
.command(ScrapCommand)
|
||||||
.command(AuthCommand)
|
.command(AuthCommand)
|
||||||
.command(UpgradeCommand)
|
.command(UpgradeCommand)
|
||||||
.fail((msg, err) => {
|
.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)
|
let 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e instanceof ResolveMessage) {
|
||||||
|
Object.assign(data, {
|
||||||
|
name: e.name,
|
||||||
|
message: e.message,
|
||||||
|
code: e.code,
|
||||||
|
specifier: e.specifier,
|
||||||
|
referrer: e.referrer,
|
||||||
|
position: e.position,
|
||||||
|
importKind: e.importKind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
process.exitCode = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,32 +60,40 @@ export namespace LSPClient {
|
|||||||
})
|
})
|
||||||
connection.listen()
|
connection.listen()
|
||||||
|
|
||||||
await connection.sendRequest("initialize", {
|
log.info("sending initialize", { id: serverID })
|
||||||
processId: server.process.pid,
|
await Promise.race([
|
||||||
workspaceFolders: [
|
connection.sendRequest("initialize", {
|
||||||
{
|
processId: server.process.pid,
|
||||||
name: "workspace",
|
workspaceFolders: [
|
||||||
uri: "file://" + app.path.cwd,
|
{
|
||||||
},
|
name: "workspace",
|
||||||
],
|
uri: "file://" + app.path.cwd,
|
||||||
initializationOptions: {
|
|
||||||
...server.initialization,
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
workspace: {
|
|
||||||
configuration: true,
|
|
||||||
},
|
|
||||||
textDocument: {
|
|
||||||
synchronization: {
|
|
||||||
didOpen: true,
|
|
||||||
didChange: true,
|
|
||||||
},
|
},
|
||||||
publishDiagnostics: {
|
],
|
||||||
versionSupport: true,
|
initializationOptions: {
|
||||||
|
...server.initialization,
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
workspace: {
|
||||||
|
configuration: true,
|
||||||
|
},
|
||||||
|
textDocument: {
|
||||||
|
synchronization: {
|
||||||
|
didOpen: true,
|
||||||
|
didChange: true,
|
||||||
|
},
|
||||||
|
publishDiagnostics: {
|
||||||
|
versionSupport: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
})
|
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,8 +1,8 @@
|
|||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { lazy } from "../util/lazy"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { z } from "zod"
|
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" })
|
||||||
@@ -14,32 +14,36 @@ export namespace ModelsDev {
|
|||||||
attachment: z.boolean(),
|
attachment: z.boolean(),
|
||||||
reasoning: z.boolean(),
|
reasoning: z.boolean(),
|
||||||
temperature: z.boolean(),
|
temperature: z.boolean(),
|
||||||
|
tool_call: z.boolean(),
|
||||||
cost: z.object({
|
cost: z.object({
|
||||||
input: z.number(),
|
input: z.number(),
|
||||||
output: z.number(),
|
output: z.number(),
|
||||||
inputCached: z.number(),
|
cache_read: z.number().optional(),
|
||||||
outputCached: z.number(),
|
cache_write: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
limit: z.object({
|
limit: z.object({
|
||||||
context: z.number(),
|
context: z.number(),
|
||||||
output: z.number(),
|
output: z.number(),
|
||||||
}),
|
}),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
options: z.record(z.any()),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Model.Info",
|
ref: "Model",
|
||||||
})
|
})
|
||||||
export type Model = z.infer<typeof Model>
|
export type Model = z.infer<typeof Model>
|
||||||
|
|
||||||
export const Provider = z
|
export const Provider = z
|
||||||
.object({
|
.object({
|
||||||
|
api: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
env: z.array(z.string()),
|
env: z.array(z.string()),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
npm: z.string().optional(),
|
||||||
models: z.record(Model),
|
models: z.record(Model),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Provider.Info",
|
ref: "Provider",
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Provider = z.infer<typeof Provider>
|
export type Provider = z.infer<typeof Provider>
|
||||||
@@ -51,42 +55,15 @@ export namespace ModelsDev {
|
|||||||
refresh()
|
refresh()
|
||||||
return result as Record<string, Provider>
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const aisdk = lazy(async () => {
|
|
||||||
log.info("fetching ai-sdk")
|
|
||||||
const response = await fetch(
|
|
||||||
"https://registry.npmjs.org/-/v1/search?text=scope:@ai-sdk",
|
|
||||||
)
|
|
||||||
if (!response.ok)
|
|
||||||
throw new Error(
|
|
||||||
`Failed to fetch ai-sdk information: ${response.statusText}`,
|
|
||||||
)
|
|
||||||
const result = await response.json()
|
|
||||||
log.info("found ai-sdk", result.objects.length)
|
|
||||||
return result.objects
|
|
||||||
.filter((obj: any) => obj.package.name.startsWith("@ai-sdk/"))
|
|
||||||
.reduce((acc: any, obj: any) => {
|
|
||||||
acc[obj.package.name] = obj
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function pkg(providerID: string): Promise<[string, string]> {
|
|
||||||
const packages = await aisdk()
|
|
||||||
const match = packages[`@ai-sdk/${providerID}`]
|
|
||||||
if (match) return [match.package.name, "latest"]
|
|
||||||
return [providerID, "latest"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,48 +19,133 @@ 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" })
|
||||||
|
|
||||||
type CustomLoader = (
|
type CustomLoader = (
|
||||||
provider: ModelsDev.Provider,
|
provider: ModelsDev.Provider,
|
||||||
) => Promise<Record<string, any> | false>
|
api?: string,
|
||||||
|
) => Promise<{
|
||||||
|
autoload: boolean
|
||||||
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
|
options?: Record<string, any>
|
||||||
|
}>
|
||||||
|
|
||||||
type Source = "env" | "config" | "custom" | "api"
|
type Source = "env" | "config" | "custom" | "api"
|
||||||
|
|
||||||
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||||
async anthropic(provider) {
|
async anthropic(provider) {
|
||||||
const access = await AuthAnthropic.access()
|
const access = await AuthAnthropic.access()
|
||||||
if (!access) return false
|
if (!access) return { autoload: false }
|
||||||
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 {
|
||||||
apiKey: "",
|
autoload: true,
|
||||||
headers: {
|
options: {
|
||||||
authorization: `Bearer ${access}`,
|
apiKey: "",
|
||||||
"anthropic-beta": "oauth-2025-04-20",
|
async fetch(input: any, init: any) {
|
||||||
|
const access = await AuthAnthropic.access()
|
||||||
|
const headers = {
|
||||||
|
...init.headers,
|
||||||
|
authorization: `Bearer ${access}`,
|
||||||
|
"anthropic-beta": "oauth-2025-04-20",
|
||||||
|
}
|
||||||
|
delete headers["x-api-key"]
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"github-copilot": async (provider) => {
|
||||||
|
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 () => {
|
"amazon-bedrock": async () => {
|
||||||
if (!process.env["AWS_PROFILE"]) return false
|
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(
|
const { fromNodeProviderChain } = await import(
|
||||||
await BunProc.install("@aws-sdk/credential-providers")
|
await BunProc.install("@aws-sdk/credential-providers")
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
region: process.env["AWS_REGION"] ?? "us-east-1",
|
autoload: true,
|
||||||
credentialProvider: fromNodeProviderChain(),
|
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)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -73,6 +158,7 @@ export namespace Provider {
|
|||||||
[providerID: string]: {
|
[providerID: string]: {
|
||||||
source: Source
|
source: Source
|
||||||
info: ModelsDev.Provider
|
info: ModelsDev.Provider
|
||||||
|
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||||
options: Record<string, any>
|
options: Record<string, any>
|
||||||
}
|
}
|
||||||
} = {}
|
} = {}
|
||||||
@@ -82,32 +168,38 @@ export namespace Provider {
|
|||||||
>()
|
>()
|
||||||
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,
|
||||||
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, provider] of Object.entries(
|
const configProviders = Object.entries(config.provider ?? {})
|
||||||
config.provider ?? {},
|
|
||||||
)) {
|
for (const [providerID, provider] of configProviders) {
|
||||||
const existing = database[providerID]
|
const existing = database[providerID]
|
||||||
const parsed: ModelsDev.Provider = {
|
const parsed: ModelsDev.Provider = {
|
||||||
id: providerID,
|
id: providerID,
|
||||||
|
npm: provider.npm ?? existing?.npm,
|
||||||
name: provider.name ?? existing?.name ?? providerID,
|
name: provider.name ?? existing?.name ?? providerID,
|
||||||
env: provider.env ?? existing?.env ?? [],
|
env: provider.env ?? existing?.env ?? [],
|
||||||
models: existing?.models ?? {},
|
models: existing?.models ?? {},
|
||||||
@@ -121,13 +213,19 @@ export namespace Provider {
|
|||||||
attachment: model.attachment ?? existing?.attachment ?? false,
|
attachment: model.attachment ?? existing?.attachment ?? false,
|
||||||
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
reasoning: model.reasoning ?? existing?.reasoning ?? false,
|
||||||
temperature: model.temperature ?? existing?.temperature ?? false,
|
temperature: model.temperature ?? existing?.temperature ?? false,
|
||||||
cost: model.cost ??
|
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||||
existing?.cost ?? {
|
cost: {
|
||||||
input: 0,
|
...existing?.cost,
|
||||||
output: 0,
|
...model.cost,
|
||||||
inputCached: 0,
|
input: 0,
|
||||||
outputCached: 0,
|
output: 0,
|
||||||
},
|
cache_read: 0,
|
||||||
|
cache_write: 0,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
...existing?.options,
|
||||||
|
...model.options,
|
||||||
|
},
|
||||||
limit: model.limit ??
|
limit: model.limit ??
|
||||||
existing?.limit ?? {
|
existing?.limit ?? {
|
||||||
context: 0,
|
context: 0,
|
||||||
@@ -139,8 +237,12 @@ export namespace Provider {
|
|||||||
database[providerID] = parsed
|
database[providerID] = parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disabled = await Config.get().then(
|
||||||
|
(cfg) => new Set(cfg.disabled_providers ?? []),
|
||||||
|
)
|
||||||
// load env
|
// load env
|
||||||
for (const [providerID, provider] of Object.entries(database)) {
|
for (const [providerID, provider] of Object.entries(database)) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
if (provider.env.some((item) => process.env[item])) {
|
if (provider.env.some((item) => process.env[item])) {
|
||||||
mergeProvider(providerID, {}, "env")
|
mergeProvider(providerID, {}, "env")
|
||||||
}
|
}
|
||||||
@@ -148,6 +250,7 @@ export namespace Provider {
|
|||||||
|
|
||||||
// load apikeys
|
// load apikeys
|
||||||
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
for (const [providerID, provider] of Object.entries(await Auth.all())) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
if (provider.type === "api") {
|
if (provider.type === "api") {
|
||||||
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
mergeProvider(providerID, { apiKey: provider.key }, "api")
|
||||||
}
|
}
|
||||||
@@ -155,19 +258,29 @@ export namespace Provider {
|
|||||||
|
|
||||||
// load custom
|
// load custom
|
||||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||||
|
if (disabled.has(providerID)) continue
|
||||||
const result = await fn(database[providerID])
|
const result = await fn(database[providerID])
|
||||||
if (result) mergeProvider(providerID, result, "custom")
|
if (result && (result.autoload || providers[providerID])) {
|
||||||
|
mergeProvider(
|
||||||
|
providerID,
|
||||||
|
result.options ?? {},
|
||||||
|
"custom",
|
||||||
|
result.getModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// load config
|
// load config
|
||||||
for (const [providerID, provider] of Object.entries(
|
for (const [providerID, provider] of configProviders) {
|
||||||
config.provider ?? {},
|
|
||||||
)) {
|
|
||||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const providerID of Object.keys(providers)) {
|
for (const [providerID, provider] of Object.entries(providers)) {
|
||||||
log.info("loaded", { providerID })
|
if (Object.keys(provider.info.models).length === 0) {
|
||||||
|
delete providers[providerID]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.info("found", { providerID })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -181,19 +294,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 [pkg, version] = await ModelsDev.pkg(providerID)
|
const pkg = provider.npm ?? provider.id
|
||||||
const mod = await import(await BunProc.install(pkg, version))
|
const mod = await import(await BunProc.install(pkg, "latest"))
|
||||||
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 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +318,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,
|
||||||
})
|
})
|
||||||
@@ -211,13 +327,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,
|
||||||
@@ -254,7 +369,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")
|
||||||
@@ -264,6 +387,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,
|
||||||
@@ -279,14 +410,23 @@ export namespace Provider {
|
|||||||
// MultiEditTool,
|
// MultiEditTool,
|
||||||
WriteTool,
|
WriteTool,
|
||||||
TodoWriteTool,
|
TodoWriteTool,
|
||||||
TaskTool,
|
// 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()
|
||||||
@@ -298,6 +438,38 @@ export namespace Provider {
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,11 @@ import { z } from "zod"
|
|||||||
import { Message } from "../session/message"
|
import { Message } from "../session/message"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
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 { ModelsDev } from "../provider/models"
|
||||||
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
import { Config } from "../config/config"
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
400: {
|
400: {
|
||||||
@@ -68,12 +68,12 @@ export namespace Server {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/openapi",
|
"/doc",
|
||||||
openAPISpecs(app, {
|
openAPISpecs(app, {
|
||||||
documentation: {
|
documentation: {
|
||||||
info: {
|
info: {
|
||||||
title: "opencode",
|
title: "opencode",
|
||||||
version: "1.0.0",
|
version: "0.0.2",
|
||||||
description: "opencode api",
|
description: "opencode api",
|
||||||
},
|
},
|
||||||
openapi: "3.0.0",
|
openapi: "3.0.0",
|
||||||
@@ -120,8 +120,8 @@ export namespace Server {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.get(
|
||||||
"/app_info",
|
"/app",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Get app info",
|
description: "Get app info",
|
||||||
responses: {
|
responses: {
|
||||||
@@ -140,7 +140,7 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/app_initialize",
|
"/app/init",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Initialize the app",
|
description: "Initialize the app",
|
||||||
responses: {
|
responses: {
|
||||||
@@ -159,144 +159,27 @@ export namespace Server {
|
|||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.get(
|
||||||
"/session_initialize",
|
"/config",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Analyze the app and create an AGENTS.md file",
|
description: "Get config info",
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "200",
|
description: "Get config info",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(z.boolean()),
|
schema: resolver(Config.Info),
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
zValidator(
|
|
||||||
"json",
|
|
||||||
z.object({
|
|
||||||
sessionID: z.string(),
|
|
||||||
providerID: z.string(),
|
|
||||||
modelID: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const body = c.req.valid("json")
|
|
||||||
await Session.initialize(body)
|
|
||||||
return c.json(true)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.post(
|
|
||||||
"/path_get",
|
|
||||||
describeRoute({
|
|
||||||
description: "Get paths",
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "200",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(
|
|
||||||
z.object({
|
|
||||||
root: z.string(),
|
|
||||||
data: z.string(),
|
|
||||||
cwd: z.string(),
|
|
||||||
config: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const app = App.info()
|
return c.json(await Config.get())
|
||||||
return c.json({
|
|
||||||
root: app.path.root,
|
|
||||||
data: app.path.data,
|
|
||||||
cwd: app.path.cwd,
|
|
||||||
config: Global.Path.data,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.get(
|
||||||
"/session_create",
|
"/session",
|
||||||
describeRoute({
|
|
||||||
description: "Create a new session",
|
|
||||||
responses: {
|
|
||||||
...ERRORS,
|
|
||||||
200: {
|
|
||||||
description: "Successfully created session",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(Session.Info),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async (c) => {
|
|
||||||
const session = await Session.create()
|
|
||||||
return c.json(session)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.post(
|
|
||||||
"/session_share",
|
|
||||||
describeRoute({
|
|
||||||
description: "Share the session",
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Successfully shared 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.share(body.sessionID)
|
|
||||||
const session = await Session.get(body.sessionID)
|
|
||||||
return c.json(session)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.post(
|
|
||||||
"/session_messages",
|
|
||||||
describeRoute({
|
|
||||||
description: "Get messages for a session",
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Successfully created session",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(Message.Info.array()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
zValidator(
|
|
||||||
"json",
|
|
||||||
z.object({
|
|
||||||
sessionID: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const messages = await Session.messages(c.req.valid("json").sessionID)
|
|
||||||
return c.json(messages)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.post(
|
|
||||||
"/session_list",
|
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "List all sessions",
|
description: "List all sessions",
|
||||||
responses: {
|
responses: {
|
||||||
@@ -316,7 +199,89 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session_abort",
|
"/session",
|
||||||
|
describeRoute({
|
||||||
|
description: "Create a new session",
|
||||||
|
responses: {
|
||||||
|
...ERRORS,
|
||||||
|
200: {
|
||||||
|
description: "Successfully created session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Session.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const session = await Session.create()
|
||||||
|
return c.json(session)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/session/:id",
|
||||||
|
describeRoute({
|
||||||
|
description: "Delete a session and all its data",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully deleted session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
await Session.remove(c.req.valid("param").id)
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/session/:id/init",
|
||||||
|
describeRoute({
|
||||||
|
description: "Analyze the app and create an AGENTS.md file",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "200",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(z.boolean()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string().openapi({ description: "Session ID" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.object({
|
||||||
|
providerID: z.string(),
|
||||||
|
modelID: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const sessionID = c.req.valid("param").id
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
await Session.initialize({ ...body, sessionID })
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/session/:id/abort",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Abort a session",
|
description: "Abort a session",
|
||||||
responses: {
|
responses: {
|
||||||
@@ -331,23 +296,78 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
zValidator(
|
zValidator(
|
||||||
"json",
|
"param",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json")
|
return c.json(Session.abort(c.req.valid("param").id))
|
||||||
return c.json(Session.abort(body.sessionID))
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/session_summarize",
|
"/session/:id/share",
|
||||||
|
describeRoute({
|
||||||
|
description: "Share a session",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully shared session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Session.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const id = c.req.valid("param").id
|
||||||
|
await Session.share(id)
|
||||||
|
const session = await Session.get(id)
|
||||||
|
return c.json(session)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/session/:id/share",
|
||||||
|
describeRoute({
|
||||||
|
description: "Unshare the session",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Successfully unshared session",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Session.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const id = c.req.valid("param").id
|
||||||
|
await Session.unshare(id)
|
||||||
|
const session = await Session.get(id)
|
||||||
|
return c.json(session)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/session/:id/summarize",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Summarize the session",
|
description: "Summarize the session",
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Summarize the session",
|
description: "Summarized session",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(z.boolean()),
|
schema: resolver(z.boolean()),
|
||||||
@@ -356,27 +376,59 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string().openapi({ description: "Session ID" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
zValidator(
|
zValidator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
|
||||||
providerID: z.string(),
|
providerID: z.string(),
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
|
const id = c.req.valid("param").id
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
await Session.summarize(body)
|
await Session.summarize({ ...body, sessionID: id })
|
||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.get(
|
||||||
"/session_chat",
|
"/session/:id/message",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Chat with a model",
|
description: "List messages for a session",
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Chat with a model",
|
description: "List of messages",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Message.Info.array()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string().openapi({ description: "Session ID" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const messages = await Session.messages(c.req.valid("param").id)
|
||||||
|
return c.json(messages)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/session/:id/message",
|
||||||
|
describeRoute({
|
||||||
|
description: "Create and send a new message to a session",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Created message",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: resolver(Message.Info),
|
schema: resolver(Message.Info),
|
||||||
@@ -385,23 +437,29 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
zValidator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
id: z.string().openapi({ description: "Session ID" }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
zValidator(
|
zValidator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
|
||||||
providerID: z.string(),
|
providerID: z.string(),
|
||||||
modelID: z.string(),
|
modelID: z.string(),
|
||||||
parts: Message.Part.array(),
|
parts: Message.MessagePart.array(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
|
const sessionID = c.req.valid("param").id
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
const msg = await Session.chat(body)
|
const msg = await Session.chat({ ...body, sessionID })
|
||||||
return c.json(msg)
|
return c.json(msg)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.get(
|
||||||
"/provider_list",
|
"/config/providers",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "List all providers",
|
description: "List all providers",
|
||||||
responses: {
|
responses: {
|
||||||
@@ -433,8 +491,8 @@ export namespace Server {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.post(
|
.get(
|
||||||
"/file_search",
|
"/file",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
description: "Search for files",
|
description: "Search for files",
|
||||||
responses: {
|
responses: {
|
||||||
@@ -449,15 +507,19 @@ export namespace Server {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
zValidator(
|
zValidator(
|
||||||
"json",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
query: z.string(),
|
query: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json")
|
const query = c.req.valid("query").query
|
||||||
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,
|
||||||
|
limit: 10,
|
||||||
|
})
|
||||||
return c.json(result)
|
return c.json(result)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -480,10 +542,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"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
type LanguageModelUsage,
|
type LanguageModelUsage,
|
||||||
type CoreMessage,
|
type CoreMessage,
|
||||||
type UIMessage,
|
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"
|
||||||
@@ -29,6 +31,9 @@ import type { Tool } from "../tool/tool"
|
|||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import type { ModelsDev } from "../provider/models"
|
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" })
|
||||||
@@ -39,21 +44,31 @@ export namespace Session {
|
|||||||
parentID: Identifier.schema("session").optional(),
|
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(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "session.info",
|
ref: "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(),
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: "SessionShare",
|
||||||
|
})
|
||||||
|
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 +76,18 @@ export namespace Session {
|
|||||||
info: Info,
|
info: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
Deleted: Bus.event(
|
||||||
|
"session.deleted",
|
||||||
|
z.object({
|
||||||
|
info: Info,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Idle: Bus.event(
|
||||||
|
"session.idle",
|
||||||
|
z.object({
|
||||||
|
sessionID: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
Error: Bus.event(
|
Error: Bus.event(
|
||||||
"session.error",
|
"session.error",
|
||||||
z.object({
|
z.object({
|
||||||
@@ -69,19 +96,30 @@ export namespace Session {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = App.state("session", () => {
|
const state = App.state(
|
||||||
const sessions = new Map<string, Info>()
|
"session",
|
||||||
const messages = new Map<string, Message.Info[]>()
|
() => {
|
||||||
|
const sessions = new Map<string, 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(parentID?: string) {
|
export async function create(parentID?: string) {
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
id: Identifier.descending("session"),
|
id: Identifier.descending("session"),
|
||||||
|
version: Installation.VERSION,
|
||||||
parentID,
|
parentID,
|
||||||
title:
|
title:
|
||||||
(parentID ? "Child session - " : "New Session - ") +
|
(parentID ? "Child session - " : "New Session - ") +
|
||||||
@@ -94,7 +132,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)
|
||||||
if (!result.parentID && Flag.OPENCODE_AUTO_SHARE)
|
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
|
||||||
@@ -116,19 +155,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)
|
||||||
@@ -167,14 +222,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,
|
||||||
@@ -189,7 +277,7 @@ export namespace Session {
|
|||||||
sessionID: string
|
sessionID: string
|
||||||
providerID: string
|
providerID: string
|
||||||
modelID: string
|
modelID: string
|
||||||
parts: Message.Part[]
|
parts: Message.MessagePart[]
|
||||||
system?: string[]
|
system?: string[]
|
||||||
tools?: Tool.Info[]
|
tools?: Tool.Info[]
|
||||||
}) {
|
}) {
|
||||||
@@ -203,10 +291,16 @@ export namespace Session {
|
|||||||
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
|
Math.max(
|
||||||
|
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9,
|
||||||
|
0,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
await summarize({
|
await summarize({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -229,6 +323,7 @@ export namespace Session {
|
|||||||
if (msgs.length === 0 && !session.parentID) {
|
if (msgs.length === 0 && !session.parentID) {
|
||||||
generateText({
|
generateText({
|
||||||
maxTokens: input.providerID === "google" ? 1024 : 20,
|
maxTokens: input.providerID === "google" ? 1024 : 20,
|
||||||
|
providerOptions: model.info.options,
|
||||||
messages: [
|
messages: [
|
||||||
...SystemPrompt.title(input.providerID).map(
|
...SystemPrompt.title(input.providerID).map(
|
||||||
(x): CoreMessage => ({
|
(x): CoreMessage => ({
|
||||||
@@ -252,7 +347,7 @@ export namespace Session {
|
|||||||
draft.title = result.text
|
draft.title = result.text
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((e) => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
const msg: Message.Info = {
|
const msg: Message.Info = {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -270,7 +365,7 @@ export namespace Session {
|
|||||||
msgs.push(msg)
|
msgs.push(msg)
|
||||||
|
|
||||||
const system = input.system ?? SystemPrompt.provider(input.providerID)
|
const system = input.system ?? SystemPrompt.provider(input.providerID)
|
||||||
system.push(...(await SystemPrompt.environment(input.sessionID)))
|
system.push(...(await SystemPrompt.environment()))
|
||||||
system.push(...(await SystemPrompt.custom()))
|
system.push(...(await SystemPrompt.custom()))
|
||||||
|
|
||||||
const next: Message.Info = {
|
const next: Message.Info = {
|
||||||
@@ -289,6 +384,7 @@ export namespace Session {
|
|||||||
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,
|
||||||
@@ -315,6 +411,16 @@ export namespace Session {
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
abort: abort.signal,
|
abort: abort.signal,
|
||||||
messageID: next.id,
|
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,
|
||||||
@@ -381,11 +487,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)
|
||||||
@@ -399,8 +503,11 @@ export namespace Session {
|
|||||||
text = undefined
|
text = undefined
|
||||||
},
|
},
|
||||||
async onFinish(input) {
|
async onFinish(input) {
|
||||||
|
log.info("message finish", {
|
||||||
|
reason: input.finishReason,
|
||||||
|
})
|
||||||
const assistant = next.metadata!.assistant!
|
const assistant = next.metadata!.assistant!
|
||||||
const usage = getUsage(input.usage, model.info)
|
const usage = getUsage(model.info, input.usage, input.providerMetadata)
|
||||||
assistant.cost = usage.cost
|
assistant.cost = usage.cost
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
},
|
},
|
||||||
@@ -440,8 +547,10 @@ export namespace Session {
|
|||||||
// return step
|
// return step
|
||||||
// },
|
// },
|
||||||
toolCallStreaming: true,
|
toolCallStreaming: true,
|
||||||
|
maxTokens: model.info.limit.output || undefined,
|
||||||
abortSignal: abort.signal,
|
abortSignal: abort.signal,
|
||||||
maxSteps: 1000,
|
maxSteps: 1000,
|
||||||
|
providerOptions: model.info.options,
|
||||||
messages: [
|
messages: [
|
||||||
...system.map(
|
...system.map(
|
||||||
(x): CoreMessage => ({
|
(x): CoreMessage => ({
|
||||||
@@ -453,12 +562,25 @@ export namespace Session {
|
|||||||
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
|
temperature: model.info.temperature ? 0 : undefined,
|
||||||
tools: {
|
tools: model.info.tool_call === false ? undefined : tools,
|
||||||
...(await MCP.tools()),
|
model: wrapLanguageModel({
|
||||||
...tools,
|
model: model.language,
|
||||||
},
|
middleware: [
|
||||||
model: model.language,
|
{
|
||||||
|
async transformParams(args) {
|
||||||
|
if (args.type === "stream") {
|
||||||
|
args.params.prompt = ProviderTransform.message(
|
||||||
|
args.params.prompt,
|
||||||
|
input.providerID,
|
||||||
|
input.modelID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return args.params
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
for await (const value of result.fullStream) {
|
for await (const value of result.fullStream) {
|
||||||
@@ -518,7 +640,7 @@ 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
|
// for some reason ai sdk claims to not send this part but it does
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@@ -549,10 +671,26 @@ export namespace Session {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case "finish":
|
||||||
|
log.info("message finish", {
|
||||||
|
reason: value.finishReason,
|
||||||
|
})
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(
|
||||||
|
model.info,
|
||||||
|
value.usage,
|
||||||
|
value.providerMetadata,
|
||||||
|
)
|
||||||
|
assistant.cost = usage.cost
|
||||||
|
await updateMessage(next)
|
||||||
|
if (value.finishReason === "length")
|
||||||
|
throw new Message.OutputLengthError({})
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
l.info("unhandled", {
|
l.info("unhandled", {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
await updateMessage(next)
|
await updateMessage(next)
|
||||||
}
|
}
|
||||||
@@ -561,6 +699,9 @@ export namespace Session {
|
|||||||
error: e,
|
error: e,
|
||||||
})
|
})
|
||||||
switch (true) {
|
switch (true) {
|
||||||
|
case Message.OutputLengthError.isInstance(e):
|
||||||
|
next.metadata.error = e
|
||||||
|
break
|
||||||
case LoadAPIKeyError.isInstance(e):
|
case LoadAPIKeyError.isInstance(e):
|
||||||
next.metadata.error = new Provider.AuthError(
|
next.metadata.error = new Provider.AuthError(
|
||||||
{
|
{
|
||||||
@@ -639,6 +780,7 @@ export namespace Session {
|
|||||||
input: 0,
|
input: 0,
|
||||||
output: 0,
|
output: 0,
|
||||||
reasoning: 0,
|
reasoning: 0,
|
||||||
|
cache: { read: 0, write: 0 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
@@ -647,7 +789,9 @@ 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: [
|
messages: [
|
||||||
@@ -668,43 +812,95 @@ export namespace Session {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
onStepFinish: async (step) => {
|
||||||
|
const assistant = next.metadata!.assistant!
|
||||||
|
const usage = getUsage(model.info, step.usage, step.providerMetadata)
|
||||||
|
assistant.cost += usage.cost
|
||||||
|
assistant.tokens = usage.tokens
|
||||||
|
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)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
next.parts.push({
|
|
||||||
type: "text",
|
for await (const value of result.fullStream) {
|
||||||
text: result.text,
|
switch (value.type) {
|
||||||
})
|
case "text-delta":
|
||||||
const assistant = next.metadata!.assistant!
|
if (!text) {
|
||||||
const usage = getUsage(result.usage, model.info)
|
text = {
|
||||||
assistant.cost = usage.cost
|
type: "text",
|
||||||
assistant.tokens = usage.tokens
|
text: value.textDelta,
|
||||||
await updateMessage(next)
|
}
|
||||||
|
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)
|
||||||
|
Bus.publish(Event.Idle, {
|
||||||
|
sessionID,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsage(usage: LanguageModelUsage, model: ModelsDev.Model) {
|
function getUsage(
|
||||||
|
model: ModelsDev.Model,
|
||||||
|
usage: LanguageModelUsage,
|
||||||
|
metadata?: ProviderMetadata,
|
||||||
|
) {
|
||||||
const tokens = {
|
const tokens = {
|
||||||
input: usage.promptTokens ?? 0,
|
input: usage.promptTokens ?? 0,
|
||||||
output: usage.completionTokens ?? 0,
|
output: usage.completionTokens ?? 0,
|
||||||
reasoning: 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,
|
||||||
}
|
}
|
||||||
@@ -759,7 +955,7 @@ function toUIMessage(msg: Message.Info): UIMessage {
|
|||||||
throw new Error("not implemented")
|
throw new Error("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
function toParts(parts: Message.Part[]): UIMessage["parts"] {
|
function toParts(parts: Message.MessagePart[]): UIMessage["parts"] {
|
||||||
const result: UIMessage["parts"] = []
|
const result: UIMessage["parts"] = []
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
switch (part.type) {
|
switch (part.type) {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { Provider } from "../provider/provider"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
|
|
||||||
export namespace Message {
|
export namespace Message {
|
||||||
|
export const OutputLengthError = NamedError.create(
|
||||||
|
"MessageOutputLengthError",
|
||||||
|
z.object({}),
|
||||||
|
)
|
||||||
|
|
||||||
export const ToolCall = z
|
export const ToolCall = z
|
||||||
.object({
|
.object({
|
||||||
state: z.literal("call"),
|
state: z.literal("call"),
|
||||||
@@ -13,7 +18,7 @@ export namespace Message {
|
|||||||
args: z.custom<Required<unknown>>(),
|
args: z.custom<Required<unknown>>(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.ToolInvocation.ToolCall",
|
ref: "ToolCall",
|
||||||
})
|
})
|
||||||
export type ToolCall = z.infer<typeof ToolCall>
|
export type ToolCall = z.infer<typeof ToolCall>
|
||||||
|
|
||||||
@@ -26,7 +31,7 @@ export namespace Message {
|
|||||||
args: z.custom<Required<unknown>>(),
|
args: z.custom<Required<unknown>>(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.ToolInvocation.ToolPartialCall",
|
ref: "ToolPartialCall",
|
||||||
})
|
})
|
||||||
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
|
export type ToolPartialCall = z.infer<typeof ToolPartialCall>
|
||||||
|
|
||||||
@@ -40,14 +45,14 @@ export namespace Message {
|
|||||||
result: z.string(),
|
result: z.string(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.ToolInvocation.ToolResult",
|
ref: "ToolResult",
|
||||||
})
|
})
|
||||||
export type ToolResult = z.infer<typeof ToolResult>
|
export type ToolResult = z.infer<typeof ToolResult>
|
||||||
|
|
||||||
export const ToolInvocation = z
|
export const ToolInvocation = z
|
||||||
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
|
.discriminatedUnion("state", [ToolCall, ToolPartialCall, ToolResult])
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.ToolInvocation",
|
ref: "ToolInvocation",
|
||||||
})
|
})
|
||||||
export type ToolInvocation = z.infer<typeof ToolInvocation>
|
export type ToolInvocation = z.infer<typeof ToolInvocation>
|
||||||
|
|
||||||
@@ -57,7 +62,7 @@ export namespace Message {
|
|||||||
text: z.string(),
|
text: z.string(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Part.Text",
|
ref: "TextPart",
|
||||||
})
|
})
|
||||||
export type TextPart = z.infer<typeof TextPart>
|
export type TextPart = z.infer<typeof TextPart>
|
||||||
|
|
||||||
@@ -68,7 +73,7 @@ export namespace Message {
|
|||||||
providerMetadata: z.record(z.any()).optional(),
|
providerMetadata: z.record(z.any()).optional(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Part.Reasoning",
|
ref: "ReasoningPart",
|
||||||
})
|
})
|
||||||
export type ReasoningPart = z.infer<typeof ReasoningPart>
|
export type ReasoningPart = z.infer<typeof ReasoningPart>
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ export namespace Message {
|
|||||||
toolInvocation: ToolInvocation,
|
toolInvocation: ToolInvocation,
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Part.ToolInvocation",
|
ref: "ToolInvocationPart",
|
||||||
})
|
})
|
||||||
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
|
export type ToolInvocationPart = z.infer<typeof ToolInvocationPart>
|
||||||
|
|
||||||
@@ -91,7 +96,7 @@ export namespace Message {
|
|||||||
providerMetadata: z.record(z.any()).optional(),
|
providerMetadata: z.record(z.any()).optional(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Part.SourceUrl",
|
ref: "SourceUrlPart",
|
||||||
})
|
})
|
||||||
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
|
export type SourceUrlPart = z.infer<typeof SourceUrlPart>
|
||||||
|
|
||||||
@@ -103,7 +108,7 @@ export namespace Message {
|
|||||||
url: z.string(),
|
url: z.string(),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Part.File",
|
ref: "FilePart",
|
||||||
})
|
})
|
||||||
export type FilePart = z.infer<typeof FilePart>
|
export type FilePart = z.infer<typeof FilePart>
|
||||||
|
|
||||||
@@ -112,11 +117,11 @@ export namespace Message {
|
|||||||
type: z.literal("step-start"),
|
type: z.literal("step-start"),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Part.StepStart",
|
ref: "StepStartPart",
|
||||||
})
|
})
|
||||||
export type StepStartPart = z.infer<typeof StepStartPart>
|
export type StepStartPart = z.infer<typeof StepStartPart>
|
||||||
|
|
||||||
export const Part = z
|
export const MessagePart = z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
TextPart,
|
TextPart,
|
||||||
ReasoningPart,
|
ReasoningPart,
|
||||||
@@ -126,61 +131,68 @@ export namespace Message {
|
|||||||
StepStartPart,
|
StepStartPart,
|
||||||
])
|
])
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Part",
|
ref: "MessagePart",
|
||||||
})
|
})
|
||||||
export type Part = z.infer<typeof Part>
|
export type MessagePart = z.infer<typeof MessagePart>
|
||||||
|
|
||||||
export const Info = z
|
export const Info = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
role: z.enum(["user", "assistant"]),
|
role: z.enum(["user", "assistant"]),
|
||||||
parts: z.array(Part),
|
parts: z.array(MessagePart),
|
||||||
metadata: z.object({
|
metadata: z
|
||||||
time: z.object({
|
.object({
|
||||||
created: z.number(),
|
time: z.object({
|
||||||
completed: z.number().optional(),
|
created: z.number(),
|
||||||
}),
|
completed: z.number().optional(),
|
||||||
error: z
|
}),
|
||||||
.discriminatedUnion("name", [
|
error: z
|
||||||
Provider.AuthError.Schema,
|
.discriminatedUnion("name", [
|
||||||
NamedError.Unknown.Schema,
|
Provider.AuthError.Schema,
|
||||||
])
|
NamedError.Unknown.Schema,
|
||||||
.optional(),
|
OutputLengthError.Schema,
|
||||||
sessionID: z.string(),
|
])
|
||||||
tool: z.record(
|
.optional(),
|
||||||
z.string(),
|
sessionID: z.string(),
|
||||||
z
|
tool: z.record(
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
title: z.string(),
|
||||||
|
time: z.object({
|
||||||
|
start: z.number(),
|
||||||
|
end: z.number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.catchall(z.any()),
|
||||||
|
),
|
||||||
|
assistant: z
|
||||||
.object({
|
.object({
|
||||||
title: z.string(),
|
system: z.string().array(),
|
||||||
time: z.object({
|
modelID: z.string(),
|
||||||
start: z.number(),
|
providerID: z.string(),
|
||||||
end: z.number(),
|
path: z.object({
|
||||||
|
cwd: z.string(),
|
||||||
|
root: z.string(),
|
||||||
|
}),
|
||||||
|
cost: z.number(),
|
||||||
|
summary: z.boolean().optional(),
|
||||||
|
tokens: z.object({
|
||||||
|
input: z.number(),
|
||||||
|
output: z.number(),
|
||||||
|
reasoning: z.number(),
|
||||||
|
cache: z.object({
|
||||||
|
read: z.number(),
|
||||||
|
write: z.number(),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.catchall(z.any()),
|
.optional(),
|
||||||
),
|
})
|
||||||
assistant: z
|
.openapi({ ref: "MessageMetadata" }),
|
||||||
.object({
|
|
||||||
system: z.string().array(),
|
|
||||||
modelID: z.string(),
|
|
||||||
providerID: z.string(),
|
|
||||||
path: z.object({
|
|
||||||
cwd: z.string(),
|
|
||||||
root: z.string(),
|
|
||||||
}),
|
|
||||||
cost: z.number(),
|
|
||||||
summary: z.boolean().optional(),
|
|
||||||
tokens: z.object({
|
|
||||||
input: z.number(),
|
|
||||||
output: z.number(),
|
|
||||||
reasoning: z.number(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Message.Info",
|
ref: "Message",
|
||||||
})
|
})
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
@@ -193,7 +205,11 @@ export namespace Message {
|
|||||||
),
|
),
|
||||||
PartUpdated: Bus.event(
|
PartUpdated: Bus.event(
|
||||||
"message.part.updated",
|
"message.part.updated",
|
||||||
z.object({ part: Part, sessionID: z.string(), messageID: z.string() }),
|
z.object({
|
||||||
|
part: MessagePart,
|
||||||
|
sessionID: z.string(),
|
||||||
|
messageID: z.string(),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
import { ListTool } from "../tool/ls"
|
import { Ripgrep } from "../external/ripgrep"
|
||||||
|
import { Global } from "../global"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
|
||||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||||
@@ -22,8 +25,57 @@ export namespace SystemPrompt {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function environment(sessionID: string) {
|
export async function environment() {
|
||||||
const app = App.info()
|
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 [
|
return [
|
||||||
[
|
[
|
||||||
`Here is some useful information about the environment you are running in:`,
|
`Here is some useful information about the environment you are running in:`,
|
||||||
@@ -33,9 +85,9 @@ export namespace SystemPrompt {
|
|||||||
` Platform: ${process.platform}`,
|
` Platform: ${process.platform}`,
|
||||||
` Today's date: ${new Date().toDateString()}`,
|
` Today's date: ${new Date().toDateString()}`,
|
||||||
`</env>`,
|
`</env>`,
|
||||||
`<project>`,
|
// `<project>`,
|
||||||
` ${app.git ? await ListTool.execute({ path: app.path.cwd, ignore: [] }, { sessionID: sessionID, messageID: "", abort: AbortSignal.any([]) }).then((x) => x.output) : ""}`,
|
// ` ${app.git ? await tree() : ""}`,
|
||||||
`</project>`,
|
// `</project>`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -52,7 +104,17 @@ export namespace SystemPrompt {
|
|||||||
const matches = await Filesystem.findUp(item, cwd, root)
|
const matches = await Filesystem.findUp(item, cwd, root)
|
||||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||||
}
|
}
|
||||||
return Promise.all(found)
|
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) {
|
export function summarize(providerID: string) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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"
|
||||||
@@ -10,19 +10,14 @@ export namespace Share {
|
|||||||
let queue: Promise<void> = Promise.resolve()
|
let queue: Promise<void> = Promise.resolve()
|
||||||
const pending = new Map<string, any>()
|
const pending = new Map<string, any>()
|
||||||
|
|
||||||
const state = App.state("share", async () => {
|
|
||||||
Bus.subscribe(Storage.Event.Write, async (payload) => {
|
|
||||||
await sync(payload.properties.key, payload.properties.content)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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 () => {
|
||||||
@@ -50,12 +45,17 @@ export namespace Share {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function init() {
|
export function init() {
|
||||||
await state()
|
Bus.subscribe(Storage.Event.Write, async (payload) => {
|
||||||
|
await sync(payload.properties.key, payload.properties.content)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +65,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"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
|
// the approaches in this edit tool are sourced from
|
||||||
|
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
||||||
|
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
||||||
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { FileTimes } from "./util/file-times"
|
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { createTwoFilesPatch } from "diff"
|
import { createTwoFilesPatch } from "diff"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
import DESCRIPTION from "./edit.txt"
|
import DESCRIPTION from "./edit.txt"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
import { File } from "../file"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { FileTime } from "../file/time"
|
||||||
|
|
||||||
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,21 +27,25 @@ 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) {
|
||||||
throw new Error("filePath is required")
|
throw new Error("filePath is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.oldString === params.newString) {
|
||||||
|
throw new Error("oldString and newString must be different")
|
||||||
|
}
|
||||||
|
|
||||||
const app = App.info()
|
const app = App.info()
|
||||||
const filepath = path.isAbsolute(params.filePath)
|
const filepath = path.isAbsolute(params.filePath)
|
||||||
? params.filePath
|
? params.filePath
|
||||||
: 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: {
|
||||||
@@ -51,45 +61,38 @@ export const EditTool = Tool.define({
|
|||||||
if (params.oldString === "") {
|
if (params.oldString === "") {
|
||||||
contentNew = params.newString
|
contentNew = params.newString
|
||||||
await Bun.write(filepath, params.newString)
|
await Bun.write(filepath, params.newString)
|
||||||
|
await Bus.publish(File.Event.Edited, {
|
||||||
|
file: filepath,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = Bun.file(filepath)
|
const file = Bun.file(filepath)
|
||||||
if (!(await file.exists())) throw new Error(`File ${filepath} not found`)
|
const stats = await file.stat().catch(() => {})
|
||||||
const stats = await file.stat()
|
if (!stats) throw new Error(`File ${filepath} not found`)
|
||||||
if (stats.isDirectory())
|
if (stats.isDirectory())
|
||||||
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||||
await FileTimes.assert(ctx.sessionID, filepath)
|
await FileTime.assert(ctx.sessionID, filepath)
|
||||||
contentOld = await file.text()
|
contentOld = await file.text()
|
||||||
const index = contentOld.indexOf(params.oldString)
|
|
||||||
if (index === -1)
|
|
||||||
throw new Error(
|
|
||||||
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (params.replaceAll) {
|
|
||||||
contentNew = contentOld.replaceAll(params.oldString, params.newString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!params.replaceAll) {
|
|
||||||
const lastIndex = contentOld.lastIndexOf(params.oldString)
|
|
||||||
if (index !== lastIndex)
|
|
||||||
throw new Error(
|
|
||||||
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
|
|
||||||
)
|
|
||||||
|
|
||||||
contentNew =
|
|
||||||
contentOld.substring(0, index) +
|
|
||||||
params.newString +
|
|
||||||
contentOld.substring(index + params.oldString.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
contentNew = replace(
|
||||||
|
contentOld,
|
||||||
|
params.oldString,
|
||||||
|
params.newString,
|
||||||
|
params.replaceAll,
|
||||||
|
)
|
||||||
await file.write(contentNew)
|
await file.write(contentNew)
|
||||||
|
await Bus.publish(File.Event.Edited, {
|
||||||
|
file: filepath,
|
||||||
|
})
|
||||||
|
contentNew = await file.text()
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const diff = createTwoFilesPatch(filepath, filepath, contentOld, contentNew)
|
const diff = trimDiff(
|
||||||
|
createTwoFilesPatch(filepath, filepath, contentOld, contentNew),
|
||||||
|
)
|
||||||
|
|
||||||
FileTimes.read(ctx.sessionID, filepath)
|
FileTime.read(ctx.sessionID, filepath)
|
||||||
|
|
||||||
let output = ""
|
let output = ""
|
||||||
await LSP.touchFile(filepath, true)
|
await LSP.touchFile(filepath, true)
|
||||||
@@ -113,3 +116,398 @@ export const EditTool = Tool.define({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type Replacer = (
|
||||||
|
content: string,
|
||||||
|
find: string,
|
||||||
|
) => Generator<string, void, unknown>
|
||||||
|
|
||||||
|
export const SimpleReplacer: Replacer = function* (_content, find) {
|
||||||
|
yield find
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LineTrimmedReplacer: Replacer = function* (content, find) {
|
||||||
|
const originalLines = content.split("\n")
|
||||||
|
const searchLines = find.split("\n")
|
||||||
|
|
||||||
|
if (searchLines[searchLines.length - 1] === "") {
|
||||||
|
searchLines.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
||||||
|
let matches = true
|
||||||
|
|
||||||
|
for (let j = 0; j < searchLines.length; j++) {
|
||||||
|
const originalTrimmed = originalLines[i + j].trim()
|
||||||
|
const searchTrimmed = searchLines[j].trim()
|
||||||
|
|
||||||
|
if (originalTrimmed !== searchTrimmed) {
|
||||||
|
matches = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
let matchStartIndex = 0
|
||||||
|
for (let k = 0; k < i; k++) {
|
||||||
|
matchStartIndex += originalLines[k].length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchEndIndex = matchStartIndex
|
||||||
|
for (let k = 0; k < searchLines.length; k++) {
|
||||||
|
matchEndIndex += originalLines[i + k].length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
yield content.substring(matchStartIndex, matchEndIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
||||||
|
const originalLines = content.split("\n")
|
||||||
|
const searchLines = find.split("\n")
|
||||||
|
|
||||||
|
if (searchLines.length < 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchLines[searchLines.length - 1] === "") {
|
||||||
|
searchLines.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstLineSearch = searchLines[0].trim()
|
||||||
|
const lastLineSearch = searchLines[searchLines.length - 1].trim()
|
||||||
|
|
||||||
|
// Find blocks where first line matches the search first line
|
||||||
|
for (let i = 0; i < originalLines.length; i++) {
|
||||||
|
if (originalLines[i].trim() !== firstLineSearch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the matching last line after this first line
|
||||||
|
for (let j = i + 2; j < originalLines.length; j++) {
|
||||||
|
if (originalLines[j].trim() === lastLineSearch) {
|
||||||
|
// Found a potential block from i to j
|
||||||
|
let matchStartIndex = 0
|
||||||
|
for (let k = 0; k < i; k++) {
|
||||||
|
matchStartIndex += originalLines[k].length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchEndIndex = matchStartIndex
|
||||||
|
for (let k = 0; k <= j - i; k++) {
|
||||||
|
matchEndIndex += originalLines[i + k].length
|
||||||
|
if (k < j - i) {
|
||||||
|
matchEndIndex += 1 // Add newline character except for the last line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield content.substring(matchStartIndex, matchEndIndex)
|
||||||
|
break // Only match the first occurrence of the last line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
||||||
|
content,
|
||||||
|
find,
|
||||||
|
) {
|
||||||
|
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
|
||||||
|
const normalizedFind = normalizeWhitespace(find)
|
||||||
|
|
||||||
|
// Handle single line matches
|
||||||
|
const lines = content.split("\n")
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
if (normalizeWhitespace(line) === normalizedFind) {
|
||||||
|
yield line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for substring matches within lines
|
||||||
|
const normalizedLine = normalizeWhitespace(line)
|
||||||
|
if (normalizedLine.includes(normalizedFind)) {
|
||||||
|
// Find the actual substring in the original line that matches
|
||||||
|
const words = find.trim().split(/\s+/)
|
||||||
|
if (words.length > 0) {
|
||||||
|
const pattern = words
|
||||||
|
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||||
|
.join("\\s+")
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(pattern)
|
||||||
|
const match = line.match(regex)
|
||||||
|
if (match) {
|
||||||
|
yield match[0]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid regex pattern, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multi-line matches
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
if (findLines.length > 1) {
|
||||||
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||||
|
const block = lines.slice(i, i + findLines.length)
|
||||||
|
if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
|
||||||
|
yield block.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
||||||
|
const removeIndentation = (text: string) => {
|
||||||
|
const lines = text.split("\n")
|
||||||
|
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
|
||||||
|
if (nonEmptyLines.length === 0) return text
|
||||||
|
|
||||||
|
const minIndent = Math.min(
|
||||||
|
...nonEmptyLines.map((line) => {
|
||||||
|
const match = line.match(/^(\s*)/)
|
||||||
|
return match ? match[1].length : 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFind = removeIndentation(find)
|
||||||
|
const contentLines = content.split("\n")
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
|
||||||
|
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
||||||
|
const block = contentLines.slice(i, i + findLines.length).join("\n")
|
||||||
|
if (removeIndentation(block) === normalizedFind) {
|
||||||
|
yield block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
||||||
|
const unescapeString = (str: string): string => {
|
||||||
|
return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
||||||
|
switch (capturedChar) {
|
||||||
|
case "n":
|
||||||
|
return "\n"
|
||||||
|
case "t":
|
||||||
|
return "\t"
|
||||||
|
case "r":
|
||||||
|
return "\r"
|
||||||
|
case "'":
|
||||||
|
return "'"
|
||||||
|
case '"':
|
||||||
|
return '"'
|
||||||
|
case "`":
|
||||||
|
return "`"
|
||||||
|
case "\\":
|
||||||
|
return "\\"
|
||||||
|
case "\n":
|
||||||
|
return "\n"
|
||||||
|
case "$":
|
||||||
|
return "$"
|
||||||
|
default:
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const unescapedFind = unescapeString(find)
|
||||||
|
|
||||||
|
// Try direct match with unescaped find string
|
||||||
|
if (content.includes(unescapedFind)) {
|
||||||
|
yield unescapedFind
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try finding escaped versions in content that match unescaped find
|
||||||
|
const lines = content.split("\n")
|
||||||
|
const findLines = unescapedFind.split("\n")
|
||||||
|
|
||||||
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||||
|
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||||
|
const unescapedBlock = unescapeString(block)
|
||||||
|
|
||||||
|
if (unescapedBlock === unescapedFind) {
|
||||||
|
yield block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
||||||
|
// This replacer yields all exact matches, allowing the replace function
|
||||||
|
// to handle multiple occurrences based on replaceAll parameter
|
||||||
|
let startIndex = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const index = content.indexOf(find, startIndex)
|
||||||
|
if (index === -1) break
|
||||||
|
|
||||||
|
yield find
|
||||||
|
startIndex = index + find.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
|
||||||
|
const trimmedFind = find.trim()
|
||||||
|
|
||||||
|
if (trimmedFind === find) {
|
||||||
|
// Already trimmed, no point in trying
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the trimmed version
|
||||||
|
if (content.includes(trimmedFind)) {
|
||||||
|
yield trimmedFind
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try finding blocks where trimmed content matches
|
||||||
|
const lines = content.split("\n")
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
|
||||||
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||||
|
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||||
|
|
||||||
|
if (block.trim() === trimmedFind) {
|
||||||
|
yield block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContextAwareReplacer: Replacer = function* (content, find) {
|
||||||
|
const findLines = find.split("\n")
|
||||||
|
if (findLines.length < 3) {
|
||||||
|
// Need at least 3 lines to have meaningful context
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing empty line if present
|
||||||
|
if (findLines[findLines.length - 1] === "") {
|
||||||
|
findLines.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLines = content.split("\n")
|
||||||
|
|
||||||
|
// Extract first and last lines as context anchors
|
||||||
|
const firstLine = findLines[0].trim()
|
||||||
|
const lastLine = findLines[findLines.length - 1].trim()
|
||||||
|
|
||||||
|
// Find blocks that start and end with the context anchors
|
||||||
|
for (let i = 0; i < contentLines.length; i++) {
|
||||||
|
if (contentLines[i].trim() !== firstLine) continue
|
||||||
|
|
||||||
|
// Look for the matching last line
|
||||||
|
for (let j = i + 2; j < contentLines.length; j++) {
|
||||||
|
if (contentLines[j].trim() === lastLine) {
|
||||||
|
// Found a potential context block
|
||||||
|
const blockLines = contentLines.slice(i, j + 1)
|
||||||
|
const block = blockLines.join("\n")
|
||||||
|
|
||||||
|
// Check if the middle content has reasonable similarity
|
||||||
|
// (simple heuristic: at least 50% of non-empty lines should match when trimmed)
|
||||||
|
if (blockLines.length === findLines.length) {
|
||||||
|
let matchingLines = 0
|
||||||
|
let totalNonEmptyLines = 0
|
||||||
|
|
||||||
|
for (let k = 1; k < blockLines.length - 1; k++) {
|
||||||
|
const blockLine = blockLines[k].trim()
|
||||||
|
const findLine = findLines[k].trim()
|
||||||
|
|
||||||
|
if (blockLine.length > 0 || findLine.length > 0) {
|
||||||
|
totalNonEmptyLines++
|
||||||
|
if (blockLine === findLine) {
|
||||||
|
matchingLines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
totalNonEmptyLines === 0 ||
|
||||||
|
matchingLines / totalNonEmptyLines >= 0.5
|
||||||
|
) {
|
||||||
|
yield block
|
||||||
|
break // Only match the first occurrence
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replace(
|
||||||
|
content: string,
|
||||||
|
oldString: string,
|
||||||
|
newString: string,
|
||||||
|
replaceAll = false,
|
||||||
|
): string {
|
||||||
|
if (oldString === newString) {
|
||||||
|
throw new Error("oldString and newString must be different")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const replacer of [
|
||||||
|
SimpleReplacer,
|
||||||
|
LineTrimmedReplacer,
|
||||||
|
BlockAnchorReplacer,
|
||||||
|
WhitespaceNormalizedReplacer,
|
||||||
|
IndentationFlexibleReplacer,
|
||||||
|
EscapeNormalizedReplacer,
|
||||||
|
TrimmedBoundaryReplacer,
|
||||||
|
ContextAwareReplacer,
|
||||||
|
MultiOccurrenceReplacer,
|
||||||
|
]) {
|
||||||
|
for (const search of replacer(content, oldString)) {
|
||||||
|
const index = content.indexOf(search)
|
||||||
|
if (index === -1) continue
|
||||||
|
if (replaceAll) {
|
||||||
|
return content.replaceAll(search, newString)
|
||||||
|
}
|
||||||
|
const lastIndex = content.lastIndexOf(search)
|
||||||
|
if (index !== lastIndex) continue
|
||||||
|
return (
|
||||||
|
content.substring(0, index) +
|
||||||
|
newString +
|
||||||
|
content.substring(index + search.length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("oldString not found in content or was found multiple times")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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, dot: true })) {
|
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()
|
||||||
@@ -41,12 +43,11 @@ export const ListTool = Tool.define({
|
|||||||
const files = []
|
const files = []
|
||||||
|
|
||||||
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
|
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
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { z } from "zod"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as fs from "fs/promises"
|
import * as fs from "fs/promises"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { FileTimes } from "./util/file-times"
|
import { FileTime } from "../file/time"
|
||||||
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) => {
|
||||||
@@ -245,7 +244,7 @@ export const PatchTool = Tool.define({
|
|||||||
absPath = path.resolve(process.cwd(), absPath)
|
absPath = path.resolve(process.cwd(), absPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
await FileTimes.assert(ctx.sessionID, absPath)
|
await FileTime.assert(ctx.sessionID, absPath)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = await fs.stat(absPath)
|
const stats = await fs.stat(absPath)
|
||||||
@@ -352,7 +351,7 @@ export const PatchTool = Tool.define({
|
|||||||
totalAdditions += additions
|
totalAdditions += additions
|
||||||
totalRemovals += removals
|
totalRemovals += removals
|
||||||
|
|
||||||
FileTimes.read(ctx.sessionID, absPath)
|
FileTime.read(ctx.sessionID, absPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`
|
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as fs from "fs"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { FileTimes } from "./util/file-times"
|
import { FileTime } from "../file/time"
|
||||||
import DESCRIPTION from "./read.txt"
|
import DESCRIPTION from "./read.txt"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -90,7 +90,7 @@ export const ReadTool = Tool.define({
|
|||||||
|
|
||||||
// just warms the lsp client
|
// just warms the lsp client
|
||||||
await LSP.touchFile(filePath, true)
|
await LSP.touchFile(filePath, true)
|
||||||
FileTimes.read(ctx.sessionID, filePath)
|
FileTime.read(ctx.sessionID, filePath)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
output,
|
output,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { Tool } from "./tool"
|
|||||||
import DESCRIPTION from "./task.txt"
|
import DESCRIPTION from "./task.txt"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { Message } from "../session/message"
|
||||||
|
|
||||||
export const TaskTool = Tool.define({
|
export const TaskTool = Tool.define({
|
||||||
id: "opencode.task",
|
id: "task",
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
description: z
|
description: z
|
||||||
@@ -17,6 +19,31 @@ export const TaskTool = Tool.define({
|
|||||||
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
|
||||||
const metadata = msg.metadata.assistant!
|
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({
|
const result = await Session.chat({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
modelID: metadata.modelID,
|
modelID: metadata.modelID,
|
||||||
@@ -28,10 +55,11 @@ export const TaskTool = Tool.define({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
unsub()
|
||||||
return {
|
return {
|
||||||
metadata: {
|
metadata: {
|
||||||
title: params.description,
|
title: params.description,
|
||||||
|
summary: summary(result),
|
||||||
},
|
},
|
||||||
output: result.parts.findLast((x) => x.type === "text")!.text,
|
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,10 +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
|
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
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { FileTimes } from "./util/file-times"
|
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { Permission } from "../permission"
|
import { Permission } from "../permission"
|
||||||
import DESCRIPTION from "./write.txt"
|
import DESCRIPTION from "./write.txt"
|
||||||
import { App } from "../app/app"
|
import { App } from "../app/app"
|
||||||
|
import { Bus } from "../bus"
|
||||||
|
import { File } from "../file"
|
||||||
|
import { FileTime } from "../file/time"
|
||||||
|
|
||||||
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
|
||||||
@@ -26,10 +28,10 @@ export const WriteTool = Tool.define({
|
|||||||
|
|
||||||
const file = Bun.file(filepath)
|
const file = Bun.file(filepath)
|
||||||
const exists = await file.exists()
|
const exists = await file.exists()
|
||||||
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
if (exists) await FileTime.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
|
||||||
@@ -42,7 +44,10 @@ export const WriteTool = Tool.define({
|
|||||||
})
|
})
|
||||||
|
|
||||||
await Bun.write(filepath, params.content)
|
await Bun.write(filepath, params.content)
|
||||||
FileTimes.read(ctx.sessionID, filepath)
|
await Bus.publish(File.Event.Edited, {
|
||||||
|
file: filepath,
|
||||||
|
})
|
||||||
|
FileTime.read(ctx.sessionID, filepath)
|
||||||
|
|
||||||
let output = ""
|
let output = ""
|
||||||
await LSP.touchFile(filepath, true)
|
await LSP.touchFile(filepath, true)
|
||||||
|
|||||||
@@ -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 schema = z
|
||||||
|
.object({
|
||||||
|
name: z.literal(name),
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
ref: name,
|
||||||
|
})
|
||||||
const result = class extends NamedError {
|
const result = class extends NamedError {
|
||||||
public static readonly Schema = z
|
public static readonly Schema = schema
|
||||||
.object({
|
|
||||||
name: z.literal(name),
|
|
||||||
data: data,
|
|
||||||
})
|
|
||||||
.openapi({
|
|
||||||
ref: name,
|
|
||||||
})
|
|
||||||
|
|
||||||
public readonly name = name as Name
|
public readonly name = name as Name
|
||||||
|
|
||||||
@@ -29,10 +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,
|
|
||||||
cause: options?.cause?.toString(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static isInstance(input: any): input is InstanceType<typeof result> {
|
static isInstance(input: any): input is InstanceType<typeof result> {
|
||||||
@@ -40,7 +37,7 @@ export abstract class NamedError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schema() {
|
schema() {
|
||||||
return result.Schema
|
return schema
|
||||||
}
|
}
|
||||||
|
|
||||||
toObject() {
|
toObject() {
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ export function lazy<T>(fn: () => T) {
|
|||||||
return value as T
|
return value as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ 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()
|
||||||
@@ -68,13 +71,13 @@ export namespace Log {
|
|||||||
}
|
}
|
||||||
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
|
||||||
@@ -83,6 +86,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
|
||||||
|
|||||||
Vendored
+1
-1
@@ -6,4 +6,4 @@
|
|||||||
/// <reference path="../../sst-env.d.ts" />
|
/// <reference path="../../sst-env.d.ts" />
|
||||||
|
|
||||||
import "sst"
|
import "sst"
|
||||||
export {}
|
export {}
|
||||||
|
|||||||
@@ -0,0 +1,413 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { replace } from "../../src/tool/edit"
|
||||||
|
|
||||||
|
interface TestCase {
|
||||||
|
content: string
|
||||||
|
find: string
|
||||||
|
replace: string
|
||||||
|
all?: boolean
|
||||||
|
fail?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCases: TestCase[] = [
|
||||||
|
// SimpleReplacer cases
|
||||||
|
{
|
||||||
|
content: ["function hello() {", ' console.log("world");', "}"].join("\n"),
|
||||||
|
find: 'console.log("world");',
|
||||||
|
replace: 'console.log("universe");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"if (condition) {",
|
||||||
|
" doSomething();",
|
||||||
|
" doSomethingElse();",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [" doSomething();", " doSomethingElse();"].join("\n"),
|
||||||
|
replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// LineTrimmedReplacer cases
|
||||||
|
{
|
||||||
|
content: ["function test() {", ' console.log("hello");', "}"].join("\n"),
|
||||||
|
find: 'console.log("hello");',
|
||||||
|
replace: 'console.log("goodbye");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ["const x = 5; ", "const y = 10;"].join("\n"),
|
||||||
|
find: "const x = 5;",
|
||||||
|
replace: "const x = 15;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [" if (true) {", " return false;", " }"].join("\n"),
|
||||||
|
find: ["if (true) {", "return false;", "}"].join("\n"),
|
||||||
|
replace: ["if (false) {", "return true;", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// BlockAnchorReplacer cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" const temp = a + b;",
|
||||||
|
" const result = temp * 2;",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" // different middle content",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"class MyClass {",
|
||||||
|
" constructor() {",
|
||||||
|
" this.value = 0;",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" getValue() {",
|
||||||
|
" return this.value;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: ["class MyClass {", " // different implementation", "}"].join("\n"),
|
||||||
|
replace: [
|
||||||
|
"class MyClass {",
|
||||||
|
" constructor() {",
|
||||||
|
" this.value = 42;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// WhitespaceNormalizedReplacer cases
|
||||||
|
{
|
||||||
|
content: ["function test() {", '\tconsole.log("hello");', "}"].join("\n"),
|
||||||
|
find: ' console.log("hello");',
|
||||||
|
replace: ' console.log("world");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const x = 5;",
|
||||||
|
find: "const x = 5;",
|
||||||
|
replace: "const x = 10;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "if\t( condition\t) {",
|
||||||
|
find: "if ( condition ) {",
|
||||||
|
replace: "if (newCondition) {",
|
||||||
|
},
|
||||||
|
|
||||||
|
// IndentationFlexibleReplacer cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
" function nested() {",
|
||||||
|
' console.log("deeply nested");',
|
||||||
|
" return true;",
|
||||||
|
" }",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"function nested() {",
|
||||||
|
' console.log("deeply nested");',
|
||||||
|
" return true;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: [
|
||||||
|
"function nested() {",
|
||||||
|
' console.log("updated");',
|
||||||
|
" return false;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
" if (true) {",
|
||||||
|
' console.log("level 1");',
|
||||||
|
' console.log("level 2");',
|
||||||
|
" }",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"if (true) {",
|
||||||
|
'console.log("level 1");',
|
||||||
|
' console.log("level 2");',
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// replaceAll option cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
'console.log("test");',
|
||||||
|
'console.log("test");',
|
||||||
|
'console.log("test");',
|
||||||
|
].join("\n"),
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("updated");',
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ['console.log("test");', 'console.log("test");'].join("\n"),
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("updated");',
|
||||||
|
all: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Error cases
|
||||||
|
{
|
||||||
|
content: 'console.log("hello");',
|
||||||
|
find: "nonexistent string",
|
||||||
|
replace: "updated",
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ["test", "test", "different content", "test"].join("\n"),
|
||||||
|
find: "test",
|
||||||
|
replace: "updated",
|
||||||
|
all: false,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
{
|
||||||
|
content: "",
|
||||||
|
find: "",
|
||||||
|
replace: "new content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const regex = /[.*+?^${}()|[\\\\]\\\\\\\\]/g;",
|
||||||
|
find: "/[.*+?^${}()|[\\\\]\\\\\\\\]/g",
|
||||||
|
replace: "/\\\\w+/g",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'const message = "Hello 世界! 🌍";',
|
||||||
|
find: "Hello 世界! 🌍",
|
||||||
|
replace: "Hello World! 🌎",
|
||||||
|
},
|
||||||
|
|
||||||
|
// EscapeNormalizedReplacer cases
|
||||||
|
{
|
||||||
|
content: 'console.log("Hello\nWorld");',
|
||||||
|
find: 'console.log("Hello\\nWorld");',
|
||||||
|
replace: 'console.log("Hello\nUniverse");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const str = 'It's working';",
|
||||||
|
find: "const str = 'It\\'s working';",
|
||||||
|
replace: "const str = 'It's fixed';",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const template = `Hello ${name}`;",
|
||||||
|
find: "const template = `Hello \\${name}`;",
|
||||||
|
replace: "const template = `Hi ${name}`;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const path = 'C:\\Users\\test';",
|
||||||
|
find: "const path = 'C:\\\\Users\\\\test';",
|
||||||
|
replace: "const path = 'C:\\Users\\admin';",
|
||||||
|
},
|
||||||
|
|
||||||
|
// MultiOccurrenceReplacer cases (with replaceAll)
|
||||||
|
{
|
||||||
|
content: ["debug('start');", "debug('middle');", "debug('end');"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
find: "debug",
|
||||||
|
replace: "log",
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "const x = 1; const y = 1; const z = 1;",
|
||||||
|
find: "1",
|
||||||
|
replace: "2",
|
||||||
|
all: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// TrimmedBoundaryReplacer cases
|
||||||
|
{
|
||||||
|
content: [" function test() {", " return true;", " }"].join("\n"),
|
||||||
|
find: ["function test() {", " return true;", "}"].join("\n"),
|
||||||
|
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "\n const value = 42; \n",
|
||||||
|
find: "const value = 42;",
|
||||||
|
replace: "const value = 24;",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: ["", " if (condition) {", " doSomething();", " }", ""].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
find: ["if (condition) {", " doSomething();", "}"].join("\n"),
|
||||||
|
replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// ContextAwareReplacer cases
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" const temp = a + b;",
|
||||||
|
" const result = temp * 2;",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"function calculate(a, b) {",
|
||||||
|
" // some different content here",
|
||||||
|
" // more different content",
|
||||||
|
" return result;",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"class TestClass {",
|
||||||
|
" constructor() {",
|
||||||
|
" this.value = 0;",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" method() {",
|
||||||
|
" return this.value;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"class TestClass {",
|
||||||
|
" // different implementation",
|
||||||
|
" // with multiple lines",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Combined edge cases for new replacers
|
||||||
|
{
|
||||||
|
content: '\tconsole.log("test");\t',
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("updated");',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [" ", "function test() {", " return 'value';", "}", " "].join(
|
||||||
|
"\n",
|
||||||
|
),
|
||||||
|
find: ["function test() {", "return 'value';", "}"].join("\n"),
|
||||||
|
replace: ["function test() {", "return 'new value';", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test for same oldString and newString (should fail)
|
||||||
|
{
|
||||||
|
content: 'console.log("test");',
|
||||||
|
find: 'console.log("test");',
|
||||||
|
replace: 'console.log("test");',
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional tests for fixes made
|
||||||
|
|
||||||
|
// WhitespaceNormalizedReplacer - test regex special characters that could cause errors
|
||||||
|
{
|
||||||
|
content: 'const pattern = "test[123]";',
|
||||||
|
find: "test[123]",
|
||||||
|
replace: "test[456]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'const regex = "^start.*end$";',
|
||||||
|
find: "^start.*end$",
|
||||||
|
replace: "^begin.*finish$",
|
||||||
|
},
|
||||||
|
|
||||||
|
// EscapeNormalizedReplacer - test single backslash vs double backslash
|
||||||
|
{
|
||||||
|
content: 'const path = "C:\\Users";',
|
||||||
|
find: 'const path = "C:\\Users";',
|
||||||
|
replace: 'const path = "D:\\Users";',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'console.log("Line1\\nLine2");',
|
||||||
|
find: 'console.log("Line1\\nLine2");',
|
||||||
|
replace: 'console.log("First\\nSecond");',
|
||||||
|
},
|
||||||
|
|
||||||
|
// BlockAnchorReplacer - test edge case with exact newline boundaries
|
||||||
|
{
|
||||||
|
content: ["function test() {", " return true;", "}"].join("\n"),
|
||||||
|
find: ["function test() {", " // middle", "}"].join("\n"),
|
||||||
|
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// ContextAwareReplacer - test with trailing newline in find string
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
"class Test {",
|
||||||
|
" method1() {",
|
||||||
|
" return 1;",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
find: [
|
||||||
|
"class Test {",
|
||||||
|
" // different content",
|
||||||
|
"}",
|
||||||
|
"", // trailing empty line
|
||||||
|
].join("\n"),
|
||||||
|
replace: ["class Test {", " method2() { return 2; }", "}"].join("\n"),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test validation for empty strings with same oldString and newString
|
||||||
|
{
|
||||||
|
content: "",
|
||||||
|
find: "",
|
||||||
|
replace: "",
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test multiple occurrences with replaceAll=false (should fail)
|
||||||
|
{
|
||||||
|
content: ["const a = 1;", "const b = 1;", "const c = 1;"].join("\n"),
|
||||||
|
find: "= 1",
|
||||||
|
replace: "= 2",
|
||||||
|
all: false,
|
||||||
|
fail: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test whitespace normalization with multiple spaces and tabs mixed
|
||||||
|
{
|
||||||
|
content: "if\t \t( \tcondition\t )\t{",
|
||||||
|
find: "if ( condition ) {",
|
||||||
|
replace: "if (newCondition) {",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test escape sequences in template literals
|
||||||
|
{
|
||||||
|
content: "const msg = `Hello\\tWorld`;",
|
||||||
|
find: "const msg = `Hello\\tWorld`;",
|
||||||
|
replace: "const msg = `Hi\\tWorld`;",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("EditTool Replacers", () => {
|
||||||
|
test.each(testCases)("case %#", (testCase) => {
|
||||||
|
if (testCase.fail) {
|
||||||
|
expect(() => {
|
||||||
|
replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||||
|
}).toThrow()
|
||||||
|
} else {
|
||||||
|
const result = replace(
|
||||||
|
testCase.content,
|
||||||
|
testCase.find,
|
||||||
|
testCase.replace,
|
||||||
|
testCase.all,
|
||||||
|
)
|
||||||
|
expect(result).toContain(testCase.replace)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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" },
|
return await ListTool.execute(
|
||||||
async () => {
|
{ path: "./example", ignore: [".git"] },
|
||||||
return await ListTool.execute(
|
ctx,
|
||||||
{ path: "./example" },
|
)
|
||||||
{ sessionID: "test" },
|
})
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(result.output).toMatchSnapshot()
|
expect(result.output).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
opencode-test
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# TUI Agent Guidelines
|
||||||
|
|
||||||
|
## Build/Test Commands
|
||||||
|
|
||||||
|
- **Build**: `go build ./cmd/opencode` (builds main binary)
|
||||||
|
- **Test**: `go test ./...` (runs all tests)
|
||||||
|
- **Single test**: `go test ./internal/theme -run TestLoadThemesFromJSON` (specific test)
|
||||||
|
- **Release build**: Uses `.goreleaser.yml` configuration
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Language**: Go 1.24+ with standard formatting (`gofmt`)
|
||||||
|
- **Imports**: Group standard, third-party, local packages with blank lines
|
||||||
|
- **Naming**: Go conventions - PascalCase exports, camelCase private, ALL_CAPS constants
|
||||||
|
- **Error handling**: Return errors explicitly, use `fmt.Errorf` for wrapping
|
||||||
|
- **Structs**: Define clear interfaces, embed when appropriate
|
||||||
|
- **Testing**: Use table-driven tests, `t.TempDir()` for file operations
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **TUI Framework**: Bubble Tea v2 with Lipgloss v2 for styling
|
||||||
|
- **Client**: Generated OpenAPI client communicates with TypeScript server
|
||||||
|
- **Components**: Reusable UI components in `internal/components/`
|
||||||
|
- **Themes**: JSON-based theming system with override hierarchy
|
||||||
|
- **State**: Centralized app state with message passing
|
||||||
@@ -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,16 @@ 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/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
zone "github.com/lrstanley/bubblezone"
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
"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 +19,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 opencode.App
|
||||||
|
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,35 +51,34 @@ 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)
|
||||||
|
|
||||||
|
slog.Debug("TUI launched", "app", appInfo)
|
||||||
|
|
||||||
|
httpClient := opencode.NewClient(
|
||||||
|
option.WithBaseURL(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.WithMouseCellMotion(),
|
|
||||||
tea.WithKeyboardEnhancements(),
|
|
||||||
tea.WithAltScreen(),
|
tea.WithAltScreen(),
|
||||||
|
tea.WithKeyboardEnhancements(),
|
||||||
|
tea.WithMouseCellMotion(),
|
||||||
)
|
)
|
||||||
|
|
||||||
eventClient, err := client.NewClient(url)
|
evts, err := client.Event(httpClient, url, ctx)
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to create event client", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
evts, err := eventClient.Event(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to subscribe to events", "error", err)
|
slog.Error("Failed to subscribe to events", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -88,133 +90,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 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
|
|
||||||
}
|
|
||||||
|
|||||||
+8
-8
@@ -5,19 +5,19 @@ 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.18.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/charmbracelet/bubbles/v2 v2.0.0-beta.1
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3
|
||||||
github.com/charmbracelet/glamour v0.10.0
|
github.com/charmbracelet/glamour v0.10.0
|
||||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
|
||||||
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
|
||||||
github.com/oapi-codegen/runtime v1.1.1
|
github.com/oapi-codegen/runtime v1.1.1
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
|
||||||
|
github.com/sst/opencode-sdk-go v0.1.0-alpha.5
|
||||||
|
github.com/tidwall/gjson v1.14.4
|
||||||
rsc.io/qr v0.2.0
|
rsc.io/qr v0.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ 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/bubbletea v1.3.4 // indirect
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // 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/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect
|
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect
|
||||||
@@ -51,6 +50,9 @@ require (
|
|||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/sosodev/duration v1.3.1 // indirect
|
||||||
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
|
||||||
github.com/spf13/cobra v1.9.1 // indirect
|
github.com/spf13/cobra v1.9.1 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
golang.org/x/tools v0.31.0 // indirect
|
||||||
@@ -66,17 +68,15 @@ require (
|
|||||||
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.5 // 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
|
||||||
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
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
|||||||
+14
-13
@@ -24,12 +24,8 @@ 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/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
|
||||||
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
|
||||||
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY=
|
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY=
|
||||||
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
|
||||||
@@ -38,8 +34,8 @@ github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V
|
|||||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU=
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 h1:D9AJJuYTN5pvz6mpIGO1ijLKpfTYSHOtKGgwoTQ4Gog=
|
||||||
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
|
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
|
||||||
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.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg=
|
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg=
|
||||||
@@ -69,8 +65,6 @@ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
|
|||||||
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=
|
||||||
@@ -126,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=
|
||||||
@@ -201,6 +191,8 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx
|
|||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||||
|
github.com/sst/opencode-sdk-go v0.1.0-alpha.5 h1:iZjdSHLo6jOMjUbDH5JWi+44v76yNbEktsRqG/Qxrco=
|
||||||
|
github.com/sst/opencode-sdk-go v0.1.0-alpha.5/go.mod h1:uagorfAHZsVy6vf0xY6TlQraM4uCILdZ5tKKhl1oToM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@@ -208,6 +200,16 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||||
|
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
||||||
@@ -267,7 +269,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
+240
-172
@@ -5,126 +5,188 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
"github.com/sst/opencode/internal/commands"
|
"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/state"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var RootPath string
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
ConfigPath string
|
Info opencode.App
|
||||||
Config *config.Config
|
Version string
|
||||||
Client *client.ClientWithResponses
|
StatePath string
|
||||||
Provider *client.ProviderInfo
|
Config *opencode.Config
|
||||||
Model *client.ModelInfo
|
Client *opencode.Client
|
||||||
Session *client.SessionInfo
|
State *config.State
|
||||||
Messages []client.MessageInfo
|
Provider *opencode.Provider
|
||||||
Status status.Service
|
Model *opencode.Model
|
||||||
Commands commands.Registry
|
Session *opencode.Session
|
||||||
|
Messages []opencode.Message
|
||||||
|
Commands commands.CommandRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInfo struct {
|
type SessionSelectedMsg = *opencode.Session
|
||||||
client.AppInfo
|
type ModelSelectedMsg struct {
|
||||||
Version string
|
Provider opencode.Provider
|
||||||
|
Model opencode.Model
|
||||||
|
}
|
||||||
|
type SessionClearedMsg struct{}
|
||||||
|
type CompactSessionMsg struct{}
|
||||||
|
type SendMsg struct {
|
||||||
|
Text string
|
||||||
|
Attachments []Attachment
|
||||||
|
}
|
||||||
|
type CompletionDialogTriggeredMsg struct {
|
||||||
|
InitialValue string
|
||||||
|
}
|
||||||
|
type OptimisticMessageAddedMsg struct {
|
||||||
|
Message opencode.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
var Info AppInfo
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
version string,
|
||||||
|
appInfo opencode.App,
|
||||||
|
httpClient *opencode.Client,
|
||||||
|
) (*App, error) {
|
||||||
|
RootPath = appInfo.Path.Root
|
||||||
|
|
||||||
func New(ctx context.Context, version string, httpClient *client.ClientWithResponses) (*App, error) {
|
configInfo, err := httpClient.Config.Get(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
|
||||||
}
|
}
|
||||||
|
|
||||||
appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
|
if configInfo.Keybinds.Leader == "" {
|
||||||
appInfo := appInfoResponse.JSON200
|
configInfo.Keybinds.Leader = "ctrl+x"
|
||||||
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)
|
appStatePath := filepath.Join(appInfo.Path.State, "tui")
|
||||||
|
appState, err := config.LoadState(appStatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
appState = config.NewState()
|
||||||
|
config.SaveState(appStatePath, appState)
|
||||||
}
|
}
|
||||||
providers := []client.ProviderInfo{}
|
|
||||||
var defaultProvider *client.ProviderInfo
|
|
||||||
var defaultModel *client.ModelInfo
|
|
||||||
|
|
||||||
var anthropic *client.ProviderInfo
|
if configInfo.Theme != "" {
|
||||||
for _, provider := range providersResponse.JSON200.Providers {
|
appState.Theme = configInfo.Theme
|
||||||
if provider.Id == "anthropic" {
|
}
|
||||||
anthropic = &provider
|
|
||||||
|
if configInfo.Model != "" {
|
||||||
|
splits := strings.Split(configInfo.Model, "/")
|
||||||
|
appState.Provider = splits[0]
|
||||||
|
appState.Model = strings.Join(splits[1:], "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "" {
|
||||||
|
if appState.Theme == "system" && styles.Terminal != nil {
|
||||||
|
theme.UpdateSystemTheme(
|
||||||
|
styles.Terminal.Background,
|
||||||
|
styles.Terminal.BackgroundIsDark,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
theme.SetTheme(appState.Theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// default to anthropic if available
|
slog.Debug("Loaded config", "config", configInfo)
|
||||||
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)
|
|
||||||
}
|
|
||||||
if len(providers) == 0 {
|
|
||||||
return nil, fmt.Errorf("no providers found")
|
|
||||||
}
|
|
||||||
|
|
||||||
appConfigPath := filepath.Join(Info.Path.Config, "config")
|
|
||||||
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 currentModel *client.ModelInfo
|
|
||||||
for _, provider := range providers {
|
|
||||||
if provider.Id == appConfig.Provider {
|
|
||||||
currentProvider = &provider
|
|
||||||
|
|
||||||
for _, model := range provider.Models {
|
|
||||||
if model.Id == appConfig.Model {
|
|
||||||
currentModel = &model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
ConfigPath: appConfigPath,
|
Info: appInfo,
|
||||||
Config: appConfig,
|
Version: version,
|
||||||
Client: httpClient,
|
StatePath: appStatePath,
|
||||||
Provider: currentProvider,
|
Config: configInfo,
|
||||||
Model: currentModel,
|
State: appState,
|
||||||
Session: &client.SessionInfo{},
|
Client: httpClient,
|
||||||
Messages: []client.MessageInfo{},
|
Session: &opencode.Session{},
|
||||||
Status: status.GetService(),
|
Messages: []opencode.Message{},
|
||||||
Commands: commands.NewCommandRegistry(),
|
Commands: commands.LoadFromConfig(configInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
theme.SetTheme(appConfig.Theme)
|
|
||||||
|
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
|
func (a *App) InitializeProvider() tea.Cmd {
|
||||||
if match, ok := response.JSON200.Default[provider.Id]; ok {
|
return func() tea.Msg {
|
||||||
|
providersResponse, err := a.Client.Config.Providers(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to list providers", "error", err)
|
||||||
|
// TODO: notify user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
providers := providersResponse.Providers
|
||||||
|
var defaultProvider *opencode.Provider
|
||||||
|
var defaultModel *opencode.Model
|
||||||
|
|
||||||
|
var anthropic *opencode.Provider
|
||||||
|
for _, provider := range providers {
|
||||||
|
if provider.ID == "anthropic" {
|
||||||
|
anthropic = &provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default to anthropic if available
|
||||||
|
if anthropic != nil {
|
||||||
|
defaultProvider = anthropic
|
||||||
|
defaultModel = getDefaultModel(providersResponse, *anthropic)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, provider := range providers {
|
||||||
|
if defaultProvider == nil || defaultModel == nil {
|
||||||
|
defaultProvider = &provider
|
||||||
|
defaultModel = getDefaultModel(providersResponse, provider)
|
||||||
|
}
|
||||||
|
providers = append(providers, provider)
|
||||||
|
}
|
||||||
|
if len(providers) == 0 {
|
||||||
|
slog.Error("No providers configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentProvider *opencode.Provider
|
||||||
|
var currentModel *opencode.Model
|
||||||
|
for _, provider := range providers {
|
||||||
|
if provider.ID == a.State.Provider {
|
||||||
|
currentProvider = &provider
|
||||||
|
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if model.ID == a.State.Model {
|
||||||
|
currentModel = &model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if currentProvider == nil || currentModel == nil {
|
||||||
|
currentProvider = defaultProvider
|
||||||
|
currentModel = defaultModel
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModelSelectedMsg{
|
||||||
|
Provider: *currentProvider,
|
||||||
|
Model: *currentModel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultModel(response *opencode.ConfigProvidersResponse, provider opencode.Provider) *opencode.Model {
|
||||||
|
if match, ok := response.Default[provider.ID]; ok {
|
||||||
model := provider.Models[match]
|
model := provider.Models[match]
|
||||||
return &model
|
return &model
|
||||||
} else {
|
} else {
|
||||||
@@ -148,11 +210,14 @@ func (a *App) IsBusy() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastMessage := a.Messages[len(a.Messages)-1]
|
lastMessage := a.Messages[len(a.Messages)-1]
|
||||||
return lastMessage.Metadata.Time.Completed == nil
|
return lastMessage.Metadata.Time.Completed == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -160,96 +225,104 @@ 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
|
_, err := a.Client.Session.Init(ctx, a.Session.ID, opencode.SessionInitParams{
|
||||||
response, err := a.Client.PostSessionInitialize(ctx, client.PostSessionInitializeJSONRequestBody{
|
ProviderID: opencode.F(a.Provider.ID),
|
||||||
SessionID: a.Session.Id,
|
ModelID: opencode.F(a.Model.ID),
|
||||||
ProviderID: a.Provider.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 {
|
|
||||||
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() {
|
||||||
|
_, err := a.Client.Session.Summarize(ctx, a.Session.ID, opencode.SessionSummarizeParams{
|
||||||
|
ProviderID: opencode.F(a.Provider.ID),
|
||||||
|
ModelID: opencode.F(a.Model.ID),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to compact session", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
func (a *App) MarkProjectInitialized(ctx context.Context) error {
|
||||||
response, err := a.Client.PostAppInitialize(ctx)
|
_, err := a.Client.App.Init(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to mark project as initialized", "error", err)
|
slog.Error("Failed to mark project as initialized", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if response != nil && response.StatusCode != 200 {
|
|
||||||
return fmt.Errorf("failed to initialize project: %d", response.StatusCode)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CreateSession(ctx context.Context) (*client.SessionInfo, error) {
|
func (a *App) CreateSession(ctx context.Context) (*opencode.Session, error) {
|
||||||
resp, err := a.Client.PostSessionCreateWithResponse(ctx)
|
session, err := a.Client.Session.New(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp != nil && resp.StatusCode() != 200 {
|
|
||||||
return nil, fmt.Errorf("failed to create session: %d", resp.StatusCode())
|
|
||||||
}
|
|
||||||
session := resp.JSON200
|
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
|
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
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
|
optimisticMessage := opencode.Message{
|
||||||
if len(attachments) > 0 {
|
ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
|
||||||
// For now, ignore attachments
|
Role: opencode.MessageRoleUser,
|
||||||
// return "", fmt.Errorf("attachments not supported yet")
|
Parts: []opencode.MessagePart{{
|
||||||
|
Type: opencode.MessagePartTypeText,
|
||||||
|
Text: text,
|
||||||
|
}},
|
||||||
|
Metadata: opencode.MessageMetadata{
|
||||||
|
SessionID: a.Session.ID,
|
||||||
|
Time: opencode.MessageMetadataTime{
|
||||||
|
Created: float64(time.Now().Unix()),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
part := client.MessagePart{}
|
a.Messages = append(a.Messages, optimisticMessage)
|
||||||
part.FromMessagePartText(client.MessagePartText{
|
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
|
||||||
Type: "text",
|
|
||||||
Text: text,
|
|
||||||
})
|
|
||||||
parts := []client.MessagePart{part}
|
|
||||||
|
|
||||||
go func() {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
|
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
|
||||||
SessionID: a.Session.Id,
|
Parts: opencode.F([]opencode.MessagePartUnionParam{
|
||||||
Parts: parts,
|
opencode.TextPartParam{
|
||||||
ProviderID: a.Provider.Id,
|
Type: opencode.F(opencode.TextPartTypeText),
|
||||||
ModelID: a.Model.Id,
|
Text: opencode.F(text),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ProviderID: opencode.F(a.Provider.ID),
|
||||||
|
ModelID: opencode.F(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 {
|
return nil
|
||||||
slog.Error("Failed to send message", "error", fmt.Sprintf("failed to send message: %d", response.StatusCode))
|
})
|
||||||
status.Error(fmt.Sprintf("failed to send message: %d", response.StatusCode))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// The actual response will come through SSE
|
// The actual response will come through SSE
|
||||||
// For now, just return success
|
// For now, just return success
|
||||||
@@ -257,69 +330,64 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
func (a *App) Cancel(ctx context.Context, sessionID string) error {
|
||||||
response, err := a.Client.PostSessionAbort(ctx, client.PostSessionAbortJSONRequestBody{
|
_, err := a.Client.Session.Abort(ctx, sessionID)
|
||||||
SessionID: sessionID,
|
|
||||||
})
|
|
||||||
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 {
|
|
||||||
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))
|
|
||||||
return fmt.Errorf("failed to cancel session: %d", response.StatusCode)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
|
func (a *App) ListSessions(ctx context.Context) ([]opencode.Session, error) {
|
||||||
resp, err := a.Client.PostSessionListWithResponse(ctx)
|
response, err := a.Client.Session.List(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.StatusCode() != 200 {
|
if response == nil {
|
||||||
return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
|
return []opencode.Session{}, nil
|
||||||
}
|
}
|
||||||
if resp.JSON200 == nil {
|
sessions := *response
|
||||||
return []client.SessionInfo{}, nil
|
|
||||||
}
|
|
||||||
sessions := *resp.JSON200
|
|
||||||
|
|
||||||
sort.Slice(sessions, func(i, j int) bool {
|
sort.Slice(sessions, func(i, j int) bool {
|
||||||
return sessions[i].Time.Created-sessions[j].Time.Created > 0
|
return sessions[i].Time.Created-sessions[j].Time.Created > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
return sessions, nil
|
return sessions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
|
func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
|
||||||
resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
|
_, err := a.Client.Session.Delete(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to delete session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]opencode.Message, error) {
|
||||||
|
response, err := a.Client.Session.Messages(ctx, sessionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.StatusCode() != 200 {
|
if response == nil {
|
||||||
return nil, fmt.Errorf("failed to list messages: %d", resp.StatusCode())
|
return []opencode.Message{}, nil
|
||||||
}
|
}
|
||||||
if resp.JSON200 == nil {
|
messages := *response
|
||||||
return []client.MessageInfo{}, nil
|
|
||||||
}
|
|
||||||
messages := *resp.JSON200
|
|
||||||
return messages, nil
|
return messages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error) {
|
func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) {
|
||||||
resp, err := a.Client.PostProviderListWithResponse(ctx)
|
response, err := a.Client.Config.Providers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.StatusCode() != 200 {
|
if response == nil {
|
||||||
return nil, fmt.Errorf("failed to list sessions: %d", resp.StatusCode())
|
return []opencode.Provider{}, nil
|
||||||
}
|
|
||||||
if resp.JSON200 == nil {
|
|
||||||
return []client.ProviderInfo{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
providers := *resp.JSON200
|
providers := *response
|
||||||
return providers.Providers, nil
|
return providers.Providers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func (a *App) loadCustomKeybinds() {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1,71 +1,281 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command represents a user-triggerable action.
|
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 {
|
type Command struct {
|
||||||
// Name is the identifier used for slash commands (e.g., "new").
|
Name CommandName
|
||||||
Name string
|
|
||||||
// Description is a short explanation of what the command does.
|
|
||||||
Description string
|
Description string
|
||||||
// KeyBinding is the keyboard shortcut to trigger this command.
|
Keybindings []Keybinding
|
||||||
KeyBinding key.Binding
|
Trigger string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry holds all the available commands.
|
func (c Command) Keys() []string {
|
||||||
type Registry map[string]Command
|
var keys []string
|
||||||
|
for _, k := range c.Keybindings {
|
||||||
// ExecuteCommandMsg is a message sent when a command should be executed.
|
keys = append(keys, k.Key)
|
||||||
type ExecuteCommandMsg struct {
|
}
|
||||||
Name string
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandRegistry() Registry {
|
type CommandRegistry map[CommandName]Command
|
||||||
return Registry{
|
|
||||||
"help": {
|
func (r CommandRegistry) Sorted() []Command {
|
||||||
Name: "help",
|
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 *opencode.Config) CommandRegistry {
|
||||||
|
defaults := []Command{
|
||||||
|
{
|
||||||
|
Name: AppHelpCommand,
|
||||||
Description: "show help",
|
Description: "show help",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>h"),
|
||||||
key.WithKeys("f1", "super+/", "super+h"),
|
Trigger: "help",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"new": {
|
{
|
||||||
Name: "new",
|
Name: EditorOpenCommand,
|
||||||
|
Description: "open editor",
|
||||||
|
Keybindings: parseBindings("<leader>e"),
|
||||||
|
Trigger: "editor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SessionNewCommand,
|
||||||
Description: "new session",
|
Description: "new session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>n"),
|
||||||
key.WithKeys("f2", "super+n"),
|
Trigger: "new",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"sessions": {
|
{
|
||||||
Name: "sessions",
|
Name: SessionListCommand,
|
||||||
Description: "switch session",
|
Description: "list sessions",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>l"),
|
||||||
key.WithKeys("f3", "super+s"),
|
Trigger: "sessions",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"model": {
|
{
|
||||||
Name: "model",
|
Name: SessionShareCommand,
|
||||||
Description: "switch model",
|
Description: "share session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>s"),
|
||||||
key.WithKeys("f4", "super+m"),
|
Trigger: "share",
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"theme": {
|
{
|
||||||
Name: "theme",
|
Name: SessionInterruptCommand,
|
||||||
Description: "switch theme",
|
Description: "interrupt session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("esc"),
|
||||||
key.WithKeys("f5", "super+t"),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"quit": {
|
{
|
||||||
Name: "quit",
|
Name: SessionCompactCommand,
|
||||||
Description: "quit",
|
Description: "compact the session",
|
||||||
KeyBinding: key.NewBinding(
|
Keybindings: parseBindings("<leader>c"),
|
||||||
key.WithKeys("f10", "ctrl+c", "super+q"),
|
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 && keybind != "" {
|
||||||
|
command.Keybindings = parseBindings(keybind)
|
||||||
|
}
|
||||||
|
registry[command.Name] = command
|
||||||
|
}
|
||||||
|
return registry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package completions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss/v2"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
|
"github.com/sst/opencode/internal/commands"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/internal/styles"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandCompletionProvider struct {
|
type CommandCompletionProvider struct {
|
||||||
@@ -27,15 +32,42 @@ func (c *CommandCompletionProvider) GetEntry() dialog.CompletionItemI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 + styles.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) {
|
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 query == "" {
|
||||||
// If no query, return all commands
|
// If no query, return all commands
|
||||||
items := []dialog.CompletionItemI{}
|
items := []dialog.CompletionItemI{}
|
||||||
for _, cmd := range c.app.Commands {
|
for _, cmd := range sorted {
|
||||||
items = append(items, dialog.NewCompletionItem(dialog.CompletionItem{
|
if cmd.Trigger == "" {
|
||||||
Title: " /" + cmd.Name,
|
continue
|
||||||
Value: "/" + cmd.Name,
|
}
|
||||||
}))
|
space := space - lipgloss.Width(cmd.Trigger)
|
||||||
|
items = append(items, getCommandCompletionItem(cmd, space, t))
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
@@ -44,12 +76,13 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
|||||||
var commandNames []string
|
var commandNames []string
|
||||||
commandMap := make(map[string]dialog.CompletionItemI)
|
commandMap := make(map[string]dialog.CompletionItemI)
|
||||||
|
|
||||||
for _, cmd := range c.app.Commands {
|
for _, cmd := range sorted {
|
||||||
commandNames = append(commandNames, cmd.Name)
|
if cmd.Trigger == "" {
|
||||||
commandMap[cmd.Name] = dialog.NewCompletionItem(dialog.CompletionItem{
|
continue
|
||||||
Title: " /" + cmd.Name,
|
}
|
||||||
Value: "/" + cmd.Name,
|
space := space - lipgloss.Width(cmd.Trigger)
|
||||||
})
|
commandNames = append(commandNames, cmd.Trigger)
|
||||||
|
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find fuzzy matches
|
// Find fuzzy matches
|
||||||
@@ -65,7 +98,5 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
|||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package completions
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
"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 {
|
||||||
@@ -24,18 +24,19 @@ func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cg *filesAndFoldersContextGroup) GetEmptyMessage() string {
|
||||||
|
return "no matching files"
|
||||||
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
|
||||||
response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
|
files, err := cg.app.Client.File.Search(
|
||||||
Query: query,
|
context.Background(),
|
||||||
})
|
opencode.FileSearchParams{Query: opencode.F(query)},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
if response.JSON200 == nil {
|
return *files, nil
|
||||||
return []string{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return *response.JSON200, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ func NewCompletionManager(app *app.App) *CompletionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *CompletionManager) DefaultProvider() dialog.CompletionProvider {
|
||||||
|
return m.providers["commands"]
|
||||||
|
}
|
||||||
|
|
||||||
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
|
||||||
if strings.HasPrefix(input, "/") {
|
if strings.HasPrefix(input, "/") {
|
||||||
return m.providers["commands"]
|
return m.providers["commands"]
|
||||||
}
|
}
|
||||||
return m.providers["files"]
|
return m.providers["files"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user