An AI coding agent designed for large tasks and real world projects.
💻 Plandex is a terminal-based AI development tool that can **plan and execute** large coding tasks that span many steps and touch dozens of files. It can handle up to 2M tokens of context directly (~100k per file), and can index directories with 20M tokens or more using tree-sitter project maps.
🔬 **A cumulative diff review sandbox** keeps AI-generated changes separate from your project files until they are ready to go. Command execution is controlled so you can easily roll back and debug. Plandex helps you get the most out of AI without leaving behind a mess in your project.
🧠 **Combine the best models** from Anthropic, OpenAI, Google, and open source providers to build entire features and apps with a robust terminal-based workflow.
🚀 Plandex is capable of full autonomy—it can load relevant files, plan and implement changes, execute commands, and automatically debug—but it's also highly flexible and configurable, giving developers fine-grained control and a step-by-step review process when needed.
💪 Plandex is designed to be resilient to large projects and files. If you've found that others tools struggle once your project gets past a certain size or the changes are too complex, give Plandex a shot.
## Smart context management that works in big projects
- 🐘 **2M token effective context window** with default model pack. Plandex loads only what's needed for each step.
- 🗄️ **Reliable in large projects and files.** Easily generate, review, revise, and apply changes spanning dozens of files.
- 🗺️ **Fast project map generation** and syntax validation with tree-sitter. Supports 30+ languages.
- 💰 **Context caching** is used across the board for OpenAI, Anthropic, and Google models, reducing costs and latency.
## Tight control or full autonomy—it's up to you
- 🚦 **Configurable autonomy:** go from full auto mode to fine-grained control depending on the task.
- 🐞 **Automated debugging** of terminal commands (like builds, linters, tests, deployments, and scripts). If you have Chrome installed, you can also automatically debug browser applications.
## Tools that help you get production-ready results
- 💬 **A project-aware chat mode** that helps you flesh out ideas before moving to implementation. Also great for asking questions and learning about a codebase.
- 🧠 **Easily try + combine models** from multiple providers. Curated model packs offer different tradeoffs of capability, cost, and speed, as well as open source and provider-specific packs.
- 🛡️ **Reliable file edits** that prioritize correctness. While most edits are quick and cheap, Plandex validates both syntax and logic as needed, with multiple fallback layers when there are problems.
- 🔀 **Full-fledged version control** for every update to the plan, including branches for exploring multiple paths or comparing different models.
- 📂 **Git integration** with commit message generation and optional automatic commits.
## Dev-friendly, easy to install
- 🧑💻 **REPL mode** with fuzzy auto-complete for commands and file loading. Just run `plandex` in any project to get started.
- 🛠️ **CLI interface** for scripting or piping data into context.
- 📦 **One-line, zero dependency CLI install**. Dockerized local mode for easily self-hosting the server. Cloud-hosting options for extra reliability and convenience.
## Workflow 🔄
## Examples 🎥
## Install 📥
```bash
curl -sL https://plandex.ai/install.sh | bash
```
**Note:** Windows is supported via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). Plandex only works correctly on Windows in the WSL shell. It doesn't work in the Windows CMD prompt or PowerShell.
[More installation options.](https://docs.plandex.ai/install)
## Hosting ⚖️
| Option | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Plandex Cloud** | Winding down as of 10/3/2025 and no longer accepting new users. Learn more. |
| **Self-hosted/Local Mode** | • Run Plandex locally with Docker or host on your own server. • Use your own [OpenRouter.ai](https://openrouter.ai) key (or [other model provider](https://docs.plandex.ai/models/model-providers) accounts and API keys). • Follow the [local-mode quickstart](https://docs.plandex.ai/hosting/self-hosting/local-mode-quickstart) to get started. |
## Provider keys 🔑
```bash
export OPENROUTER_API_KEY=... # if using OpenRouter.ai
```
## Claude Pro/Max subscription 🖇️
If you have a Claude Pro or Max subscription, Plandex can use it when calling Anthropic models. You'll be asked if you want to connect a subscription the first time you run Plandex.
## Get started 🚀
First, `cd` into a **project directory** where you want to get something done or chat about the project. Make a new directory first with `mkdir your-project-dir` if you're starting on a new project.
```bash
cd your-project-dir
```
For a new project, you might also want to initialize a git repo. Plandex doesn't require that your project is in a git repo, but it does integrate well with git if you use it.
```bash
git init
```
Now start the Plandex REPL in your project:
```bash
plandex
```
or for short:
```bash
pdx
```
## Docs 🛠️
### [👉 Full documentation.](https://docs.plandex.ai/)
## Discussion and discord 💬
Please feel free to give your feedback, ask questions, report a bug, or just hang out:
- [Discord](https://discord.gg/plandex-ai)
- [Discussions](https://github.com/plandex-ai/plandex/discussions)
- [Issues](https://github.com/plandex-ai/plandex/issues)
## Follow and subscribe
- [Follow @PlandexAI](https://x.com/PlandexAI)
- [Follow @Danenania](https://x.com/Danenania) (Plandex's creator)
- [Subscribe on YouTube](https://x.com/PlandexAI)
## Contributors 👥
⭐️ Please star, fork, explore, and contribute to Plandex. There's a lot of work to do and so much that can be improved.
[Here's an overview on setting up a development environment.](https://docs.plandex.ai/development)
================================================
FILE: app/.dockerignore
================================================
cli/
plandex-server
================================================
FILE: app/.gitignore
================================================
.env
================================================
FILE: app/clear_local.sh
================================================
#!/usr/bin/env bash
# Get the absolute path to the script's directory, regardless of where it's run from
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Change to the app directory if we're not already there
cd "$SCRIPT_DIR"
echo "WARNING: This will delete all Plandex server data and reset the database."
echo "This action cannot be undone."
read -p "Are you sure you want to continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
echo "Reset cancelled."
exit 1
fi
echo "Resetting local mode..."
echo "Stopping containers and removing volumes..."
# Stop containers and remove volumes
docker compose down -v
echo "Database and data directories cleared. Server stopped."
================================================
FILE: app/cli/api/clients.go
================================================
package api
import (
"math"
"math/rand"
"net"
"net/http"
"os"
"plandex-cli/auth"
"plandex-cli/types"
"time"
)
const dialTimeout = 10 * time.Second
const fastReqTimeout = 30 * time.Second
const slowReqTimeout = 5 * time.Minute
type Api struct{}
var CloudApiHost string
var Client types.ApiClient = (*Api)(nil)
func init() {
if os.Getenv("PLANDEX_ENV") == "development" {
CloudApiHost = os.Getenv("PLANDEX_API_HOST")
if CloudApiHost == "" {
CloudApiHost = "http://localhost:8099"
}
} else {
CloudApiHost = "https://api-v2.plandex.ai"
}
}
func GetApiHost() string {
if auth.Current == nil {
return CloudApiHost
} else if auth.Current.IsCloud {
return CloudApiHost
} else {
return auth.Current.Host
}
}
type authenticatedTransport struct {
underlyingTransport http.RoundTripper
}
func (t *authenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
err := auth.SetAuthHeader(req)
if err != nil {
return nil, err
}
auth.SetVersionHeader(req)
return t.underlyingTransport.RoundTrip(req)
}
type unauthenticatedTransport struct {
underlyingTransport http.RoundTripper
}
func (t *unauthenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
auth.SetVersionHeader(req)
return t.underlyingTransport.RoundTrip(req)
}
type retryTransport struct {
Base http.RoundTripper
MaxRetries int
BaseDelay time.Duration
MaxDelay time.Duration
Jitter time.Duration
RetryStatuses map[int]bool
}
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.Base == nil {
t.Base = http.DefaultTransport
}
var resp *http.Response
var err error
for attempt := 0; attempt <= t.MaxRetries; attempt++ {
resp, err = t.Base.RoundTrip(req)
// If there's a low-level error (e.g. network), retry unless it's a timeout, as these are often transient.
if err != nil {
if netErr, ok := err.(net.Error); ok {
if netErr.Timeout() {
return resp, err
}
}
// continue to next attempt
} else {
// If status code not in our RetryStatuses, return immediately.
if !t.RetryStatuses[resp.StatusCode] {
return resp, nil
}
// Close the body before retrying.
_ = resp.Body.Close()
}
// If we reached the max, break out of loop (will return last resp).
if attempt == t.MaxRetries {
break
}
// Exponential backoff + jitter
backoff := float64(t.BaseDelay) * math.Pow(2, float64(attempt))
if backoff > float64(t.MaxDelay) {
backoff = float64(t.MaxDelay)
}
sleepDuration := time.Duration(backoff) + time.Duration(rand.Int63n(int64(t.Jitter)))
time.Sleep(sleepDuration)
}
return resp, err
}
var netDialer = &net.Dialer{
Timeout: dialTimeout,
}
var baseTransport = &http.Transport{
Dial: netDialer.Dial,
}
var sharedRetryTransport = &retryTransport{
Base: baseTransport,
MaxRetries: 3,
BaseDelay: 500 * time.Millisecond,
MaxDelay: 5 * time.Second,
Jitter: 300 * time.Millisecond,
RetryStatuses: map[int]bool{502: true, 503: true, 504: true},
}
var unauthenticatedClient = &http.Client{
Transport: &unauthenticatedTransport{
underlyingTransport: sharedRetryTransport,
},
Timeout: fastReqTimeout,
}
var authenticatedFastClient = &http.Client{
Transport: &authenticatedTransport{
underlyingTransport: sharedRetryTransport,
},
Timeout: fastReqTimeout,
}
var authenticatedSlowClient = &http.Client{
Transport: &authenticatedTransport{
underlyingTransport: sharedRetryTransport,
},
Timeout: slowReqTimeout,
}
var authenticatedStreamingClient = &http.Client{
Transport: &authenticatedTransport{
underlyingTransport: sharedRetryTransport,
},
}
================================================
FILE: app/cli/api/errors.go
================================================
package api
import (
"encoding/json"
"log"
"net/http"
"plandex-cli/auth"
"plandex-cli/term"
"strings"
shared "plandex-shared"
)
func HandleApiError(r *http.Response, errBody []byte) *shared.ApiError {
// Check if the response is JSON
if r.Header.Get("Content-Type") != "application/json" {
return &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: r.StatusCode,
Msg: strings.TrimSpace(string(errBody)),
}
}
var apiError shared.ApiError
if err := json.Unmarshal(errBody, &apiError); err != nil {
log.Printf("Error unmarshalling JSON: %v\n", err)
return &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: r.StatusCode,
Msg: strings.TrimSpace(string(errBody)),
}
}
// return error if token/auth refresh is needed
if apiError.Type == shared.ApiErrorTypeInvalidToken || apiError.Type == shared.ApiErrorTypeAuthOutdated {
return &apiError
}
term.HandleApiError(&apiError)
return &apiError
}
func refreshAuthIfNeeded(apiErr *shared.ApiError) (bool, *shared.ApiError) {
if apiErr.Type == shared.ApiErrorTypeInvalidToken {
err := auth.RefreshInvalidToken()
if err != nil {
return false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: "error refreshing invalid token"}
}
return true, nil
} else if apiErr.Type == shared.ApiErrorTypeAuthOutdated {
err := auth.RefreshAuth()
if err != nil {
return false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: "error refreshing auth"}
}
return true, nil
}
return false, apiErr
}
================================================
FILE: app/cli/api/methods.go
================================================
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"plandex-cli/types"
"strings"
shared "plandex-shared"
"github.com/shopspring/decimal"
)
func (a *Api) CreateCliTrialSession() (string, *shared.ApiError) {
serverUrl := CloudApiHost + "/accounts/cli_trial_session"
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", nil)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
return "", apiErr
}
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response: %v", err)}
}
return string(bytes), nil
}
func (a *Api) GetCliTrialSession(token string) (*shared.SessionResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/accounts/cli_trial_session/%s", CloudApiHost, token)
resp, err := unauthenticatedClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
if resp.StatusCode == 404 {
return nil, nil
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
return nil, apiErr
}
var session shared.SessionResponse
err = json.NewDecoder(resp.Body).Decode(&session)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &session, nil
}
func (a *Api) CreateProject(req shared.CreateProjectRequest) (*shared.CreateProjectResponse, *shared.ApiError) {
serverUrl := GetApiHost() + "/projects"
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.CreateProject(req)
}
return nil, apiErr
}
var respBody shared.CreateProjectResponse
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &respBody, nil
}
func (a *Api) ListProjects() ([]*shared.Project, *shared.ApiError) {
serverUrl := GetApiHost() + "/projects"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListProjects()
}
return nil, apiErr
}
var projects []*shared.Project
err = json.NewDecoder(resp.Body).Decode(&projects)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return projects, nil
}
func (a *Api) SetProjectPlan(projectId string, req shared.SetProjectPlanRequest) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/projects/%s/set_plan", GetApiHost(), projectId)
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.SetProjectPlan(projectId, req)
}
return apiErr
}
return nil
}
func (a *Api) RenameProject(projectId string, req shared.RenameProjectRequest) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/projects/%s/rename", GetApiHost(), projectId)
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.RenameProject(projectId, req)
}
return apiErr
}
return nil
}
func (a *Api) ListPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans?", GetApiHost())
parts := []string{}
for _, projectId := range projectIds {
parts = append(parts, fmt.Sprintf("projectId=%s", projectId))
}
serverUrl += strings.Join(parts, "&")
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.ListPlans(projectIds)
}
return nil, apiErr
}
var plans []*shared.Plan
err = json.NewDecoder(resp.Body).Decode(&plans)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return plans, nil
}
func (a *Api) ListArchivedPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/archive?", GetApiHost())
parts := []string{}
for _, projectId := range projectIds {
parts = append(parts, fmt.Sprintf("projectId=%s", projectId))
}
serverUrl += strings.Join(parts, "&")
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListArchivedPlans(projectIds)
}
return nil, apiErr
}
var plans []*shared.Plan
err = json.NewDecoder(resp.Body).Decode(&plans)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return plans, nil
}
func (a *Api) ListPlansRunning(projectIds []string, includeRecent bool) (*shared.ListPlansRunningResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/ps?", GetApiHost())
parts := []string{}
for _, projectId := range projectIds {
parts = append(parts, fmt.Sprintf("projectId=%s", projectId))
}
serverUrl += strings.Join(parts, "&")
if includeRecent {
serverUrl += "&recent=true"
}
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListPlansRunning(projectIds, includeRecent)
}
return nil, apiErr
}
var respBody *shared.ListPlansRunningResponse
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return respBody, nil
}
func (a *Api) GetCurrentBranchByPlanId(projectId string, req shared.GetCurrentBranchByPlanIdRequest) (map[string]*shared.Branch, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/projects/%s/plans/current_branches", GetApiHost(), projectId)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return nil, &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.GetCurrentBranchByPlanId(projectId, req)
}
return nil, apiErr
}
var respBody map[string]*shared.Branch
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return nil, &shared.ApiError{Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return respBody, nil
}
func (a *Api) CreatePlan(projectId string, req shared.CreatePlanRequest) (*shared.CreatePlanResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/projects/%s/plans", GetApiHost(), projectId)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.CreatePlan(projectId, req)
}
return nil, apiErr
}
var respBody shared.CreatePlanResponse
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &respBody, nil
}
func (a *Api) GetPlan(planId string) (*shared.Plan, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s", GetApiHost(), planId)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetPlan(planId)
}
return nil, apiErr
}
var plan shared.Plan
err = json.NewDecoder(resp.Body).Decode(&plan)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &plan, nil
}
func (a *Api) DeletePlan(planId string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s", GetApiHost(), planId)
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.DeletePlan(planId)
}
return apiErr
}
return nil
}
func (a *Api) DeleteAllPlans(projectId string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/projects/%s/plans", GetApiHost(), projectId)
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.DeleteAllPlans(projectId)
}
return apiErr
}
return nil
}
func (a *Api) TellPlan(planId, branch string, req shared.TellPlanRequest, onStream types.OnStreamPlan) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/tell", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
var client *http.Client
if req.ConnectStream {
client = authenticatedStreamingClient
} else {
client = authenticatedFastClient
}
resp, err := client.Do(request)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.TellPlan(planId, branch, req, onStream)
}
return apiErr
}
if req.ConnectStream {
log.Println("Connecting stream")
connectPlanRespStream(resp.Body, onStream)
} else {
// log.Println("Background exec - not connecting stream")
resp.Body.Close()
}
return nil
}
func (a *Api) BuildPlan(planId, branch string, req shared.BuildPlanRequest, onStream types.OnStreamPlan) *shared.ApiError {
log.Println("Calling BuildPlan")
serverUrl := fmt.Sprintf("%s/plans/%s/%s/build", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
var client *http.Client
if req.ConnectStream {
client = authenticatedStreamingClient
} else {
client = authenticatedFastClient
}
resp, err := client.Do(request)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
if resp.StatusCode >= 400 {
log.Println("Error response from build plan", resp.StatusCode)
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.BuildPlan(planId, branch, req, onStream)
}
return apiErr
}
if req.ConnectStream {
log.Println("Connecting stream")
connectPlanRespStream(resp.Body, onStream)
} else {
// log.Println("Background exec - not connecting stream")
resp.Body.Close()
}
return nil
}
func (a *Api) RespondMissingFile(planId, branch string, req shared.RespondMissingFileRequest) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/respond_missing_file", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.RespondMissingFile(planId, branch, req)
}
return apiErr
}
return nil
}
func (a *Api) ConnectPlan(planId, branch string, onStream types.OnStreamPlan) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/connect", GetApiHost(), planId, branch)
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedStreamingClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.ConnectPlan(planId, branch, onStream)
}
return apiErr
}
connectPlanRespStream(resp.Body, onStream)
return nil
}
func (a *Api) StopPlan(ctx context.Context, planId, branch string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/stop", GetApiHost(), planId, branch)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, serverUrl, nil)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.StopPlan(ctx, planId, branch)
}
return apiErr
}
return nil
}
func (a *Api) GetCurrentPlanState(planId, branch string) (*shared.CurrentPlanState, *shared.ApiError) {
return a.getCurrentPlanState(planId, branch, "")
}
func (a *Api) GetCurrentPlanStateAtSha(planId, sha string) (*shared.CurrentPlanState, *shared.ApiError) {
return a.getCurrentPlanState(planId, "", sha)
}
func (a *Api) getCurrentPlanState(planId, branch, sha string) (*shared.CurrentPlanState, *shared.ApiError) {
var serverUrl string
if sha != "" {
serverUrl = fmt.Sprintf("%s/plans/%s/current_plan/%s", GetApiHost(), planId, sha)
} else {
serverUrl = fmt.Sprintf("%s/plans/%s/%s/current_plan", GetApiHost(), planId, branch)
}
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.getCurrentPlanState(planId, branch, sha)
}
return nil, apiErr
}
var state shared.CurrentPlanState
err = json.NewDecoder(resp.Body).Decode(&state)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &state, nil
}
func (a *Api) ApplyPlan(planId, branch string, req shared.ApplyPlanRequest) (string, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/apply", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return "", &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return "", &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return "", &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.ApplyPlan(planId, branch, req)
}
return "", apiErr
}
// Reading the body on success
responseData, err := io.ReadAll(resp.Body)
if err != nil {
return "", &shared.ApiError{Msg: fmt.Sprintf("error reading response body: %v", err)}
}
return string(responseData), nil
}
func (a *Api) ArchivePlan(planId string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/archive", GetApiHost(), planId)
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.ArchivePlan(planId)
}
return apiErr
}
return nil
}
func (a *Api) UnarchivePlan(planId string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/unarchive", GetApiHost(), planId)
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.ArchivePlan(planId)
}
return apiErr
}
return nil
}
func (a *Api) RenamePlan(planId string, name string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/rename", GetApiHost(), planId)
reqBytes, err := json.Marshal(shared.RenamePlanRequest{Name: name})
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.RenamePlan(planId, name)
}
return apiErr
}
return nil
}
func (a *Api) RejectAllChanges(planId, branch string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/reject_all", GetApiHost(), planId, branch)
req, err := http.NewRequest(http.MethodPatch, serverUrl, nil)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
return a.RejectAllChanges(planId, branch)
}
return apiErr
}
return nil
}
func (a *Api) RejectFile(planId, branch, filePath string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/reject_file", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(shared.RejectFileRequest{FilePath: filePath})
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
req, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
req.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
a.RejectFile(planId, branch, filePath)
}
return apiErr
}
return nil
}
func (a *Api) RejectFiles(planId, branch string, paths []string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/reject_files", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(shared.RejectFilesRequest{Paths: paths})
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
req, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error creating request: %v", err)}
}
req.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
didRefresh, apiErr := refreshAuthIfNeeded(apiErr)
if didRefresh {
a.RejectFiles(planId, branch, paths)
}
return apiErr
}
return nil
}
func (a *Api) LoadContext(planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
// use the slow client since we may be uploading relatively large files
resp, err := authenticatedSlowClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.LoadContext(planId, branch, req)
}
return nil, apiErr
}
var loadContextResponse shared.LoadContextResponse
err = json.NewDecoder(resp.Body).Decode(&loadContextResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &loadContextResponse, nil
}
func (a *Api) UpdateContext(planId, branch string, req shared.UpdateContextRequest) (*shared.UpdateContextResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
// use the slow client since we may be uploading relatively large files
resp, err := authenticatedSlowClient.Do(request)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.UpdateContext(planId, branch, req)
}
return nil, apiErr
}
var updateContextResponse shared.UpdateContextResponse
err = json.NewDecoder(resp.Body).Decode(&updateContextResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &updateContextResponse, nil
}
func (a *Api) DeleteContext(planId, branch string, req shared.DeleteContextRequest) (*shared.DeleteContextResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodDelete, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.DeleteContext(planId, branch, req)
}
return nil, apiErr
}
var deleteContextResponse shared.DeleteContextResponse
err = json.NewDecoder(resp.Body).Decode(&deleteContextResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &deleteContextResponse, nil
}
func (a *Api) ListContext(planId, branch string) ([]*shared.Context, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context", GetApiHost(), planId, branch)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListContext(planId, branch)
}
return nil, apiErr
}
var contexts []*shared.Context
err = json.NewDecoder(resp.Body).Decode(&contexts)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return contexts, nil
}
func (a *Api) LoadCachedFileMap(planId, branch string, req shared.LoadCachedFileMapRequest) (*shared.LoadCachedFileMapResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/load_cached_file_map", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
return nil, apiErr
}
var loadResp shared.LoadCachedFileMapResponse
err = json.NewDecoder(resp.Body).Decode(&loadResp)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &loadResp, nil
}
func (a *Api) ListConvo(planId, branch string) ([]*shared.ConvoMessage, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/convo", GetApiHost(), planId, branch)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListConvo(planId, branch)
}
return nil, apiErr
}
var convos []*shared.ConvoMessage
err = json.NewDecoder(resp.Body).Decode(&convos)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return convos, nil
}
func (a *Api) GetPlanStatus(planId, branch string) (string, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/status", GetApiHost(), planId, branch)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetPlanStatus(planId, branch)
}
return "", apiErr
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response body: %v", err)}
}
return string(body), nil
}
func (a *Api) GetPlanDiffs(planId, branch string, plain bool) (string, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/diffs", GetApiHost(), planId, branch)
if plain {
serverUrl += "?plain=true"
}
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetPlanDiffs(planId, branch, plain)
}
return "", apiErr
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response body: %v", err)}
}
return string(body), nil
}
func (a *Api) ListLogs(planId, branch string) (*shared.LogResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/logs", GetApiHost(), planId, branch)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListLogs(planId, branch)
}
return nil, apiErr
}
var logs shared.LogResponse
err = json.NewDecoder(resp.Body).Decode(&logs)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &logs, nil
}
func (a *Api) RewindPlan(planId, branch string, req shared.RewindPlanRequest) (*shared.RewindPlanResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/rewind", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.RewindPlan(planId, branch, req)
}
return nil, apiErr
}
var rewindPlanResponse shared.RewindPlanResponse
err = json.NewDecoder(resp.Body).Decode(&rewindPlanResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &rewindPlanResponse, nil
}
func (a *Api) SignIn(req shared.SignInRequest, customHost string) (*shared.SessionResponse, *shared.ApiError) {
host := customHost
if host == "" {
host = CloudApiHost
}
serverUrl := host + "/accounts/sign_in"
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
return nil, apiErr
}
var sessionResponse shared.SessionResponse
err = json.NewDecoder(resp.Body).Decode(&sessionResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &sessionResponse, nil
}
func (a *Api) CreateAccount(req shared.CreateAccountRequest, customHost string) (*shared.SessionResponse, *shared.ApiError) {
host := customHost
if host == "" {
host = CloudApiHost
}
serverUrl := host + "/accounts"
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
return nil, apiErr
}
var sessionResponse shared.SessionResponse
err = json.NewDecoder(resp.Body).Decode(&sessionResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &sessionResponse, nil
}
func (a *Api) CreateOrg(req shared.CreateOrgRequest) (*shared.CreateOrgResponse, *shared.ApiError) {
serverUrl := GetApiHost() + "/orgs"
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.CreateOrg(req)
}
return nil, apiErr
}
var createOrgResponse shared.CreateOrgResponse
err = json.NewDecoder(resp.Body).Decode(&createOrgResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &createOrgResponse, nil
}
func (a *Api) GetOrgSession() (*shared.Org, *shared.ApiError) {
serverUrl := GetApiHost() + "/orgs/session"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetOrgSession()
}
return nil, apiErr
}
var org *shared.Org
err = json.NewDecoder(resp.Body).Decode(&org)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return org, nil
}
func (a *Api) ListOrgs() ([]*shared.Org, *shared.ApiError) {
serverUrl := GetApiHost() + "/orgs"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListOrgs()
}
return nil, apiErr
}
var orgs []*shared.Org
err = json.NewDecoder(resp.Body).Decode(&orgs)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return orgs, nil
}
func (a *Api) GetOrgUserConfig() (*shared.OrgUserConfig, *shared.ApiError) {
serverUrl := GetApiHost() + "/org_user_config"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetOrgUserConfig()
}
return nil, apiErr
}
var orgUserConfig shared.OrgUserConfig
err = json.NewDecoder(resp.Body).Decode(&orgUserConfig)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &orgUserConfig, nil
}
func (a *Api) UpdateOrgUserConfig(c shared.OrgUserConfig) *shared.ApiError {
serverUrl := GetApiHost() + "/org_user_config"
reqBytes, err := json.Marshal(c)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.UpdateOrgUserConfig(c)
}
return apiErr
}
return nil
}
func (a *Api) DeleteUser(userId string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/orgs/users/%s", GetApiHost(), userId)
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.DeleteUser(userId)
}
return apiErr
}
return nil
}
func (a *Api) ListOrgRoles() ([]*shared.OrgRole, *shared.ApiError) {
serverUrl := GetApiHost() + "/orgs/roles"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListOrgRoles()
}
return nil, apiErr
}
var roles []*shared.OrgRole
err = json.NewDecoder(resp.Body).Decode(&roles)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
}
return roles, nil
}
func (a *Api) InviteUser(req shared.InviteRequest) *shared.ApiError {
serverUrl := GetApiHost() + "/invites"
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.InviteUser(req)
}
return apiErr
}
return nil
}
func (a *Api) ListPendingInvites() ([]*shared.Invite, *shared.ApiError) {
serverUrl := GetApiHost() + "/invites/pending"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListPendingInvites()
}
return nil, apiErr
}
var invites []*shared.Invite
err = json.NewDecoder(resp.Body).Decode(&invites)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return invites, nil
}
func (a *Api) ListAcceptedInvites() ([]*shared.Invite, *shared.ApiError) {
serverUrl := GetApiHost() + "/invites/accepted"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListAcceptedInvites()
}
return nil, apiErr
}
var invites []*shared.Invite
err = json.NewDecoder(resp.Body).Decode(&invites)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return invites, nil
}
func (a *Api) ListAllInvites() ([]*shared.Invite, *shared.ApiError) {
serverUrl := GetApiHost() + "/invites/all"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListAllInvites()
}
return nil, apiErr
}
var invites []*shared.Invite
err = json.NewDecoder(resp.Body).Decode(&invites)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return invites, nil
}
func (a *Api) DeleteInvite(inviteId string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/invites/%s", GetApiHost(), inviteId)
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.DeleteInvite(inviteId)
}
return apiErr
}
return nil
}
func (a *Api) CreateEmailVerification(email, customHost, userId string) (*shared.CreateEmailVerificationResponse, *shared.ApiError) {
host := customHost
if host == "" {
host = CloudApiHost
}
serverUrl := host + "/accounts/email_verifications"
req := shared.CreateEmailVerificationRequest{Email: email, UserId: userId}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := unauthenticatedClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
return nil, HandleApiError(resp, errorBody)
}
var verificationResponse shared.CreateEmailVerificationResponse
err = json.NewDecoder(resp.Body).Decode(&verificationResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &verificationResponse, nil
}
func (a *Api) CreateSignInCode() (string, *shared.ApiError) {
serverUrl := GetApiHost() + "/accounts/sign_in_codes"
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", nil)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.CreateSignInCode()
}
return "", apiErr
}
var signInCode string
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error reading response body: %v", err)}
}
signInCode = string(body)
return signInCode, nil
}
func (a *Api) SignOut() *shared.ApiError {
serverUrl := GetApiHost() + "/accounts/sign_out"
req, err := http.NewRequest(http.MethodPost, serverUrl, nil)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
return HandleApiError(resp, errorBody)
}
return nil
}
func (a *Api) ListUsers() (*shared.ListUsersResponse, *shared.ApiError) {
serverUrl := GetApiHost() + "/users"
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListUsers()
}
return nil, apiErr
}
var r *shared.ListUsersResponse
err = json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return r, nil
}
func (a *Api) ListBranches(planId string) ([]*shared.Branch, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/branches", GetApiHost(), planId)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListBranches(planId)
}
return nil, apiErr
}
var branches []*shared.Branch
err = json.NewDecoder(resp.Body).Decode(&branches)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
}
return branches, nil
}
func (a *Api) CreateBranch(planId, branch string, req shared.CreateBranchRequest) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/branches", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %s", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.CreateBranch(planId, branch, req)
}
return apiErr
}
return nil
}
func (a *Api) DeleteBranch(planId, branch string) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/branches/%s", GetApiHost(), planId, branch)
req, err := http.NewRequest(http.MethodDelete, serverUrl, nil)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %s", err)}
}
resp, err := authenticatedFastClient.Do(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.DeleteBranch(planId, branch)
}
return apiErr
}
return nil
}
func (a *Api) GetSettings(planId, branch string) (*shared.PlanSettings, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/settings", GetApiHost(), planId, branch)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetSettings(planId, branch)
}
return nil, apiErr
}
var settings shared.PlanSettings
err = json.NewDecoder(resp.Body).Decode(&settings)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
}
return &settings, nil
}
func (a *Api) UpdateSettings(planId, branch string, req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/settings", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %s", err)}
}
// log.Println("UpdateSettings", string(reqBytes))
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %s", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.UpdateSettings(planId, branch, req)
}
return nil, apiErr
}
var updateRes shared.UpdateSettingsResponse
err = json.NewDecoder(resp.Body).Decode(&updateRes)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
}
return &updateRes, nil
}
func (a *Api) GetOrgDefaultSettings() (*shared.PlanSettings, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/default_settings", GetApiHost())
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetOrgDefaultSettings()
}
return nil, apiErr
}
var settings shared.PlanSettings
err = json.NewDecoder(resp.Body).Decode(&settings)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
}
return &settings, nil
}
func (a *Api) UpdateOrgDefaultSettings(req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/default_settings", GetApiHost())
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %s", err)}
}
// log.Println("UpdateSettings", string(reqBytes))
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %s", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %s", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.UpdateOrgDefaultSettings(req)
}
return nil, apiErr
}
var updateRes shared.UpdateSettingsResponse
err = json.NewDecoder(resp.Body).Decode(&updateRes)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %s", err)}
}
return &updateRes, nil
}
func (a *Api) GetPlanConfig(planId string) (*shared.PlanConfig, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/config", GetApiHost(), planId)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetPlanConfig(planId)
}
return nil, apiErr
}
var res shared.GetPlanConfigResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return res.Config, nil
}
func (a *Api) UpdatePlanConfig(planId string, req shared.UpdatePlanConfigRequest) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/plans/%s/config", GetApiHost(), planId)
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.UpdatePlanConfig(planId, req)
}
return apiErr
}
return nil
}
func (a *Api) GetDefaultPlanConfig() (*shared.PlanConfig, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/default_plan_config", GetApiHost())
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetDefaultPlanConfig()
}
return nil, apiErr
}
var res shared.GetDefaultPlanConfigResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return res.Config, nil
}
func (a *Api) UpdateDefaultPlanConfig(req shared.UpdateDefaultPlanConfigRequest) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/default_plan_config", GetApiHost())
reqBytes, err := json.Marshal(req)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
request, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
request.Header.Set("Content-Type", "application/json")
resp, err := authenticatedFastClient.Do(request)
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.UpdateDefaultPlanConfig(req)
}
return apiErr
}
return nil
}
func (a *Api) CreateCustomModels(input *shared.ModelsInput) *shared.ApiError {
serverUrl := fmt.Sprintf("%s/custom_models", GetApiHost())
body, err := json.Marshal(input)
if err != nil {
return &shared.ApiError{Msg: "Failed to marshal model"}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(body))
if err != nil {
return &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.CreateCustomModels(input)
}
return apiErr
}
return nil
}
func (a *Api) ListCustomModels() ([]*shared.CustomModel, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/custom_models", GetApiHost())
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListCustomModels()
}
return nil, apiErr
}
var models []*shared.CustomModel
err = json.NewDecoder(resp.Body).Decode(&models)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return models, nil
}
func (a *Api) ListCustomProviders() ([]*shared.CustomProvider, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/custom_providers", GetApiHost())
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListCustomProviders()
}
return nil, apiErr
}
var providers []*shared.CustomProvider
err = json.NewDecoder(resp.Body).Decode(&providers)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return providers, nil
}
func (a *Api) ListModelPacks() ([]*shared.ModelPack, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/model_sets", GetApiHost())
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.ListModelPacks()
}
return nil, apiErr
}
var sets []*shared.ModelPack
err = json.NewDecoder(resp.Body).Decode(&sets)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return sets, nil
}
func (a *Api) GetCreditsTransactions(pageSize, pageNum int, req shared.CreditsLogRequest) (*shared.CreditsLogResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/billing/credits_transactions?size=%d&page=%d", GetApiHost(), pageSize, pageNum)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetCreditsTransactions(pageSize, pageNum, req)
}
return nil, apiErr
}
var res *shared.CreditsLogResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return res, nil
}
func (a *Api) GetCreditsSummary(req shared.CreditsLogRequest) (*shared.CreditsSummaryResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/billing/credits_summary", GetApiHost())
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedFastClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetCreditsSummary(req)
}
return nil, apiErr
}
var res *shared.CreditsSummaryResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return res, nil
}
func (a *Api) GetBalance() (decimal.Decimal, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/billing/balance", GetApiHost())
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return decimal.Zero, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetBalance()
}
return decimal.Zero, apiErr
}
var res *shared.GetBalanceResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
return decimal.Zero, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return res.Balance, nil
}
func (a *Api) GetFileMap(req shared.GetFileMapRequest) (*shared.GetFileMapResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/file_map", GetApiHost())
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
resp, err := authenticatedSlowClient.Post(serverUrl, "application/json", bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetFileMap(req)
}
return nil, apiErr
}
var respBody shared.GetFileMapResponse
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &respBody, nil
}
func (a *Api) GetContextBody(planId, branch, contextId string) (*shared.GetContextBodyResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/context/%s/body", GetApiHost(), planId, branch, contextId)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetContextBody(planId, branch, contextId)
}
return nil, apiErr
}
var respBody shared.GetContextBodyResponse
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &respBody, nil
}
func (a *Api) AutoLoadContext(ctx context.Context, planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/auto_load_context", GetApiHost(), planId, branch)
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error marshalling request: %v", err)}
}
// Create a new request with context
httpReq, err := http.NewRequestWithContext(ctx, "POST", serverUrl, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error creating request: %v", err)}
}
// Set the content type header
httpReq.Header.Set("Content-Type", "application/json")
// Use the slow client since we may be uploading relatively large files
resp, err := authenticatedSlowClient.Do(httpReq)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.LoadContext(planId, branch, req)
}
return nil, apiErr
}
var loadContextResponse shared.LoadContextResponse
err = json.NewDecoder(resp.Body).Decode(&loadContextResponse)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &loadContextResponse, nil
}
func (a *Api) GetBuildStatus(planId, branch string) (*shared.GetBuildStatusResponse, *shared.ApiError) {
serverUrl := fmt.Sprintf("%s/plans/%s/%s/build_status", GetApiHost(), planId, branch)
resp, err := authenticatedFastClient.Get(serverUrl)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error sending request: %v", err)}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errorBody, _ := io.ReadAll(resp.Body)
apiErr := HandleApiError(resp, errorBody)
authRefreshed, apiErr := refreshAuthIfNeeded(apiErr)
if authRefreshed {
return a.GetBuildStatus(planId, branch)
}
return nil, apiErr
}
var respBody shared.GetBuildStatusResponse
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf("error decoding response: %v", err)}
}
return &respBody, nil
}
================================================
FILE: app/cli/api/stream.go
================================================
package api
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"plandex-cli/types"
"time"
shared "plandex-shared"
)
// 3 heartbeat misses = timeout
const HeartbeatTimeout = 16 * time.Second
func connectPlanRespStream(body io.ReadCloser, onStream types.OnStreamPlan) {
reader := bufio.NewReader(body)
timer := time.NewTimer(HeartbeatTimeout)
defer timer.Stop()
go func() {
for {
select {
case <-timer.C:
log.Println("Connection to plan stream timed out due to missing heartbeats")
onStream(types.OnStreamPlanParams{Msg: nil, Err: fmt.Errorf("connection to plan stream timed out due to missing heartbeats")})
body.Close()
return
default:
}
s, err := readUntilSeparator(reader, shared.STREAM_MESSAGE_SEPARATOR)
if err != nil {
log.Println("Error reading line:", err)
onStream(types.OnStreamPlanParams{Msg: nil, Err: err})
body.Close()
return
}
timer.Reset(HeartbeatTimeout)
// ignore heartbeats
if s == string(shared.StreamMessageHeartbeat) {
continue
}
var msg shared.StreamMessage
err = json.Unmarshal([]byte(s), &msg)
if err != nil {
log.Println("Error unmarshalling message:", err)
onStream(types.OnStreamPlanParams{Msg: nil, Err: err})
body.Close()
return
}
// log.Println("connectPlanRespStream: received message:", msg)
onStream(types.OnStreamPlanParams{Msg: &msg, Err: nil})
if msg.Type == shared.StreamMessageFinished || msg.Type == shared.StreamMessageError || msg.Type == shared.StreamMessageAborted {
body.Close()
return
}
}
}()
}
func readUntilSeparator(reader *bufio.Reader, separator string) (string, error) {
var result []byte
sepBytes := []byte(separator)
for {
b, err := reader.ReadByte()
if err != nil {
return string(result), err
}
result = append(result, b)
if len(result) >= len(sepBytes) && bytes.HasSuffix(result, sepBytes) {
return string(result[:len(result)-len(separator)]), nil
}
}
}
================================================
FILE: app/cli/auth/account.go
================================================
package auth
import (
"fmt"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
)
const AddAccountOption = "Add another account"
func SelectOrSignInOrCreate() error {
accounts, err := loadAccounts()
if err != nil {
return fmt.Errorf("error loading accounts: %v", err)
}
if len(accounts) == 0 {
err := promptSignInNewAccount()
if err != nil {
return fmt.Errorf("error signing in to new account: %v", err)
}
return nil
}
var options []string
for _, account := range accounts {
options = append(options, fmt.Sprintf("<%s> %s", account.UserName, account.Email))
}
options = append(options, AddAccountOption)
// either select from existing accounts or sign in/create account
selectedOpt, err := term.SelectFromList("Select an account:", options)
if err != nil {
return fmt.Errorf("error selecting account: %v", err)
}
if selectedOpt == AddAccountOption {
err := promptSignInNewAccount()
if err != nil {
return fmt.Errorf("error prompting for sign in to new account: %v", err)
}
return nil
}
var selected *shared.ClientAccount
for i, opt := range options {
if selectedOpt == opt {
selected = accounts[i]
break
}
}
if selected == nil {
return fmt.Errorf("error selecting account: account not found")
}
selectedAuth := *selected
setAuth(&shared.ClientAuth{
ClientAccount: selectedAuth,
})
term.StartSpinner("")
orgs, apiErr := apiClient.ListOrgs()
term.StopSpinner()
if apiErr != nil {
return fmt.Errorf("error listing orgs: %v", apiErr.Msg)
}
org, err := resolveOrgAuth(orgs, selectedAuth.IsLocalMode)
if err != nil {
return fmt.Errorf("error resolving org: %v", err)
}
err = setAuth(&shared.ClientAuth{
ClientAccount: *selected,
OrgId: org.Id,
OrgName: org.Name,
OrgIsTrial: org.IsTrial,
IntegratedModelsMode: org.IntegratedModelsMode,
})
if err != nil {
return fmt.Errorf("error setting auth: %v", err)
}
_, apiErr = apiClient.GetOrgSession()
if apiErr != nil {
return fmt.Errorf("error getting org session: %v", apiErr.Msg)
}
fmt.Printf("✅ Signed in as %s | Org: %s\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("<%s> %s", Current.UserName, Current.Email), color.New(term.ColorHiCyan).Sprint(Current.OrgName))
fmt.Println()
if !term.IsRepl {
term.PrintCmds("", "")
}
return nil
}
func SignInWithCode(code, host string) error {
term.StartSpinner("")
res, apiErr := apiClient.SignIn(shared.SignInRequest{
Pin: code,
IsSignInCode: true,
}, host)
term.StopSpinner()
if apiErr != nil {
return fmt.Errorf("error signing in: %v", apiErr.Msg)
}
return handleSignInResponse(res, host)
}
func promptInitialAuth() error {
fmt.Println("👋 Hey there!\nIt looks like this is your first time using Plandex on this computer.")
err := SelectOrSignInOrCreate()
if err != nil {
return fmt.Errorf("error selecting or signing in to account: %v", err)
}
return nil
}
const (
// SignInCloudOption = "Plandex Cloud"
SignInLocalOption = "Local mode host"
SignInOtherOption = "Another host"
)
func promptSignInNewAccount() error {
selected, err := term.SelectFromList("Use local mode or another host?", []string{SignInLocalOption, SignInOtherOption})
if err != nil {
return fmt.Errorf("error selecting sign in option: %v", err)
}
var host string
var email string
if selected == SignInLocalOption {
host, err = term.GetRequiredUserStringInputWithDefault("Host:", "http://localhost:8099")
} else {
host, err = term.GetRequiredUserStringInput("Host:")
}
if err != nil {
return fmt.Errorf("error prompting host: %v", err)
}
if selected == SignInLocalOption {
email = "local-admin@plandex.ai"
} else {
email, err = term.GetRequiredUserStringInput("Your email:")
}
if err != nil {
return fmt.Errorf("error prompting email: %v", err)
}
res, err := verifyEmail(email, host)
if err != nil {
return fmt.Errorf("error verifying email: %v", err)
}
if res.hasAccount {
err := signIn(email, res.pin, host)
if err != nil {
return fmt.Errorf("error signing in: %v", err)
}
} else {
err := createAccount(email, res.pin, host, res.isLocalMode)
if err != nil {
return fmt.Errorf("error creating account: %v", err)
}
}
if !term.IsRepl {
term.PrintCmds("", "")
}
return nil
}
type verifyEmailRes struct {
hasAccount bool
isLocalMode bool
pin string
}
func verifyEmail(email, host string) (*verifyEmailRes, error) {
term.StartSpinner("")
res, apiErr := apiClient.CreateEmailVerification(email, host, "")
term.StopSpinner()
if apiErr != nil {
return nil, fmt.Errorf("error creating email verification: %v", apiErr.Msg)
}
if res.IsLocalMode {
return &verifyEmailRes{
hasAccount: res.HasAccount,
isLocalMode: true,
pin: "",
}, nil
}
fmt.Println("✉️ You'll now receive a 6 character pin by email. It will be valid for 5 minutes.")
pin, err := term.GetUserPasswordInput("Please enter your pin:")
if err != nil {
return nil, fmt.Errorf("error prompting pin: %v", err)
}
return &verifyEmailRes{
hasAccount: res.HasAccount,
isLocalMode: false,
pin: pin,
}, nil
}
func signIn(email, pin, host string) error {
term.StartSpinner("")
res, apiErr := apiClient.SignIn(shared.SignInRequest{
Email: email,
Pin: pin,
}, host)
term.StopSpinner()
if apiErr != nil {
return fmt.Errorf("error signing in: %v", apiErr.Msg)
}
return handleSignInResponse(res, host)
}
func handleSignInResponse(res *shared.SessionResponse, host string) error {
isLocalMode := host != "" && res.IsLocalMode
err := setAuth(&shared.ClientAuth{
ClientAccount: shared.ClientAccount{
Email: res.Email,
UserId: res.UserId,
UserName: res.UserName,
Token: res.Token,
IsTrial: false,
IsCloud: host == "",
Host: host,
IsLocalMode: isLocalMode,
},
})
if err != nil {
return fmt.Errorf("error setting auth: %v", err)
}
org, err := resolveOrgAuth(res.Orgs, isLocalMode)
if err != nil {
return fmt.Errorf("error resolving org: %v", err)
}
Current.OrgId = org.Id
Current.OrgName = org.Name
Current.IntegratedModelsMode = org.IntegratedModelsMode
err = writeCurrentAuth()
if err != nil {
return fmt.Errorf("error writing auth: %v", err)
}
fmt.Printf("✅ Signed in as %s | Org: %s\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("<%s> %s", Current.UserName, Current.Email), color.New(term.ColorHiCyan).Sprint(Current.OrgName))
fmt.Println()
return nil
}
func createAccount(email, pin, host string, isLocalMode bool) error {
var name string
if isLocalMode {
name = "Local Admin"
} else {
var err error
name, err = term.GetUserStringInput("Your name:")
if err != nil {
return fmt.Errorf("error prompting name: %v", err)
}
}
term.StartSpinner("🌟 Creating account...")
res, apiErr := apiClient.CreateAccount(shared.CreateAccountRequest{
Email: email,
UserName: name,
Pin: pin,
}, host)
term.StopSpinner()
if apiErr != nil {
return fmt.Errorf("error creating account: %v", apiErr.Msg)
}
if res.IsLocalMode {
isLocalMode = true
}
err := setAuth(&shared.ClientAuth{
ClientAccount: shared.ClientAccount{
Email: res.Email,
UserId: res.UserId,
UserName: res.UserName,
Token: res.Token,
IsTrial: false,
IsCloud: host == "",
Host: host,
IsLocalMode: isLocalMode,
},
})
if err != nil {
return fmt.Errorf("error setting auth: %v", err)
}
org, err := resolveOrgAuth(res.Orgs, isLocalMode)
if err != nil {
return fmt.Errorf("error resolving org: %v", err)
}
if org == nil {
return fmt.Errorf("no org selected")
}
Current.OrgId = org.Id
Current.OrgName = org.Name
Current.IntegratedModelsMode = org.IntegratedModelsMode
err = writeCurrentAuth()
if err != nil {
return fmt.Errorf("error writing auth: %v", err)
}
fmt.Printf("✅ Signed in as %s | Org: %s\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("<%s> %s", Current.UserName, Current.Email), color.New(term.ColorHiCyan).Sprint(Current.OrgName))
fmt.Println()
return nil
}
================================================
FILE: app/cli/auth/api.go
================================================
package auth
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"plandex-cli/types"
"plandex-cli/version"
shared "plandex-shared"
)
var apiClient types.ApiClient
func SetApiClient(client types.ApiClient) {
apiClient = client
}
func SetAuthHeader(req *http.Request) error {
if Current == nil {
return fmt.Errorf("error setting auth header: auth not loaded")
}
hash := Current.ToHash()
authHeader := shared.AuthHeader{
Token: Current.Token,
OrgId: Current.OrgId,
Hash: hash,
}
bytes, err := json.Marshal(authHeader)
if err != nil {
return fmt.Errorf("error marshalling auth header: %v", err)
}
// base64 encode
token := base64.URLEncoding.EncodeToString(bytes)
req.Header.Set("Authorization", "Bearer "+token)
return nil
}
func SetVersionHeader(req *http.Request) {
req.Header.Set("X-Client-Version", version.Version)
}
================================================
FILE: app/cli/auth/auth.go
================================================
package auth
import (
"encoding/json"
"fmt"
"os"
"plandex-cli/fs"
"plandex-cli/term"
shared "plandex-shared"
)
var openUnauthenticatedCloudURL func(msg, path string)
var openAuthenticatedURL func(msg, path string)
func SetOpenUnauthenticatedCloudURLFn(fn func(msg, path string)) {
openUnauthenticatedCloudURL = fn
}
func SetOpenAuthenticatedURLFn(fn func(msg, path string)) {
openAuthenticatedURL = fn
}
func MustResolveAuthWithOrg() {
MustResolveAuth(true)
}
func MustResolveAuth(requireOrg bool) {
if apiClient == nil {
term.OutputErrorAndExit("error resolving auth: api client not set")
}
// load HomeAuthPath file into ClientAuth struct
bytes, err := os.ReadFile(fs.HomeAuthPath)
if err != nil {
if os.IsNotExist(err) {
err = promptInitialAuth()
if err != nil {
term.OutputErrorAndExit("error resolving auth: %v", err)
}
return
} else {
term.OutputErrorAndExit("error reading auth.json: %v", err)
}
}
var auth shared.ClientAuth
err = json.Unmarshal(bytes, &auth)
if err != nil {
term.OutputErrorAndExit("error unmarshalling auth.json: %v", err)
}
Current = &auth
if requireOrg && Current.OrgId == "" {
term.StartSpinner("")
orgs, apiErr := apiClient.ListOrgs()
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error listing orgs: %v", apiErr.Msg)
}
org, err := resolveOrgAuth(orgs, Current.IsLocalMode)
if err != nil {
term.OutputErrorAndExit("Error resolving org: %v", err)
}
if org.Id == "" {
// still no org--exit now
term.OutputErrorAndExit("No org")
}
Current.OrgId = org.Id
Current.OrgName = org.Name
Current.IntegratedModelsMode = org.IntegratedModelsMode
err = writeCurrentAuth()
if err != nil {
term.OutputErrorAndExit("Error writing auth: %v", err)
}
}
}
func RefreshInvalidToken() error {
if Current == nil {
return fmt.Errorf("error refreshing token: auth not loaded")
}
res, err := verifyEmail(Current.Email, Current.Host)
if err != nil {
return fmt.Errorf("error verifying email: %v", err)
}
if res.hasAccount {
return signIn(Current.Email, res.pin, Current.Host)
} else {
host := Current.Host
if host == "" {
host = "Plandex Cloud"
}
term.OutputErrorAndExit("Account %s not found on %s", Current.Email, host)
}
return nil
}
func RefreshAuth() error {
if Current == nil {
return fmt.Errorf("error refreshing auth: auth not loaded")
}
org, apiErr := apiClient.GetOrgSession()
if apiErr != nil {
return fmt.Errorf("error getting org session: %v", apiErr.Msg)
}
Current.OrgName = org.Name
Current.OrgIsTrial = org.IsTrial
Current.IntegratedModelsMode = org.IntegratedModelsMode
err := writeCurrentAuth()
if err != nil {
return fmt.Errorf("error writing auth: %v", err)
}
return nil
}
================================================
FILE: app/cli/auth/org.go
================================================
package auth
import (
"fmt"
"plandex-cli/term"
"strings"
shared "plandex-shared"
)
func resolveOrgAuth(orgs []*shared.Org, isLocalMode bool) (*shared.Org, error) {
var org *shared.Org
var err error
if len(orgs) == 0 {
if isLocalMode {
org, err = createOrg(isLocalMode)
} else {
org, err = promptNoOrgs()
}
if err != nil {
return nil, fmt.Errorf("error prompting no orgs: %v", err)
}
} else if len(orgs) == 1 {
org = orgs[0]
} else {
org, err = selectOrg(orgs, isLocalMode)
if err != nil {
return nil, fmt.Errorf("error selecting org: %v", err)
}
}
return org, nil
}
func promptNoOrgs() (*shared.Org, error) {
fmt.Println("🧐 You don't have access to any orgs yet.\n\nTo join an existing org, ask an admin to either invite you directly or give your whole email domain access.\n\nOtherwise, you can go ahead and create a new org.")
shouldCreate, err := term.ConfirmYesNo("Create a new org now?")
if err != nil {
return nil, fmt.Errorf("error prompting create org: %v", err)
}
if shouldCreate {
return createOrg(false)
}
return nil, nil
}
func createOrg(isLocalMode bool) (*shared.Org, error) {
var err error
var name string
var autoAddDomainUsers bool
if isLocalMode {
name = "Local Org"
} else {
name, err = term.GetRequiredUserStringInput("Org name:")
}
if err != nil {
return nil, fmt.Errorf("error prompting org name: %v", err)
}
if !isLocalMode {
autoAddDomainUsers, err = promptAutoAddUsersIfValid(Current.Email)
if err != nil {
return nil, fmt.Errorf("error prompting auto add domain users: %v", err)
}
}
term.StartSpinner("")
res, apiErr := apiClient.CreateOrg(shared.CreateOrgRequest{
Name: name,
AutoAddDomainUsers: autoAddDomainUsers,
})
term.StopSpinner()
if apiErr != nil {
return nil, fmt.Errorf("error creating org: %v", apiErr.Msg)
}
return &shared.Org{Id: res.Id, Name: name}, nil
}
func promptAutoAddUsersIfValid(email string) (bool, error) {
userDomain := strings.Split(email, "@")[1]
var autoAddDomainUsers bool
var err error
if !shared.IsEmailServiceDomain(userDomain) {
fmt.Println("With domain auto-join, you can allow any user with an email ending in @"+userDomain, "to auto-join this org.")
autoAddDomainUsers, err = term.ConfirmYesNo(fmt.Sprintf("Enable auto-join for %s?", userDomain))
if err != nil {
return false, err
}
}
return autoAddDomainUsers, nil
}
const CreateOrgOption = "Create a new org"
func selectOrg(orgs []*shared.Org, isLocalMode bool) (*shared.Org, error) {
var options []string
for _, org := range orgs {
options = append(options, org.Name)
}
options = append(options, CreateOrgOption)
selected, err := term.SelectFromList("Select an org:", options)
if err != nil {
return nil, fmt.Errorf("error selecting org: %v", err)
}
if selected == CreateOrgOption {
return createOrg(isLocalMode)
}
var selectedOrg *shared.Org
for _, org := range orgs {
if org.Name == selected {
selectedOrg = org
break
}
}
if selectedOrg == nil {
return nil, fmt.Errorf("error selecting org: org not found")
}
return selectedOrg, nil
}
================================================
FILE: app/cli/auth/state.go
================================================
package auth
import (
"encoding/json"
"fmt"
"os"
"plandex-cli/fs"
shared "plandex-shared"
)
var Current *shared.ClientAuth
func loadAccounts() ([]*shared.ClientAccount, error) {
bytes, err := os.ReadFile(fs.HomeAccountsPath)
if err != nil {
if os.IsNotExist(err) {
// no accounts
return []*shared.ClientAccount{}, nil
} else {
return nil, fmt.Errorf("error reading accounts.json: %v", err)
}
}
var accounts []*shared.ClientAccount
err = json.Unmarshal(bytes, &accounts)
if err != nil {
return nil, fmt.Errorf("error unmarshalling accounts.json: %v", err)
}
return accounts, nil
}
func setAuth(auth *shared.ClientAuth) error {
err := storeAccount(&auth.ClientAccount)
if err != nil {
return fmt.Errorf("error storing account: %v", err)
}
Current = auth
err = writeCurrentAuth()
if err != nil {
return fmt.Errorf("error writing auth: %v", err)
}
return nil
}
func storeAccount(toStore *shared.ClientAccount) error {
accounts, err := loadAccounts()
if err != nil {
return fmt.Errorf("error loading accounts: %v", err)
}
found := false
for i, account := range accounts {
if account.UserId == toStore.UserId {
accounts[i] = toStore
found = true
break
}
}
if !found {
accounts = append(accounts, toStore)
}
bytes, err := json.Marshal(accounts)
if err != nil {
return fmt.Errorf("error marshalling accounts: %v", err)
}
err = os.WriteFile(fs.HomeAccountsPath, bytes, os.ModePerm)
if err != nil {
return fmt.Errorf("error writing accounts: %v", err)
}
return nil
}
func writeCurrentAuth() error {
if Current == nil {
return fmt.Errorf("error writing auth: auth not loaded")
}
bytes, err := json.Marshal(Current)
if err != nil {
return fmt.Errorf("error marshalling auth: %v", err)
}
err = os.WriteFile(fs.HomeAuthPath, bytes, os.ModePerm)
if err != nil {
return fmt.Errorf("error writing auth: %v", err)
}
return nil
}
================================================
FILE: app/cli/auth/trial.go
================================================
package auth
import (
"fmt"
"plandex-cli/term"
"time"
)
func ConvertTrial() {
openAuthenticatedURL("Opening Plandex Cloud upgrade flow in your browser.", "/settings/billing?upgrade=1&cliUpgrade=1")
fmt.Println("\nCommand will continue automatically once you've upgraded...")
fmt.Println()
term.StartSpinner("")
startTime := time.Now()
expirationTime := startTime.Add(1 * time.Hour)
for time.Now().Before(expirationTime) {
org, apiErr := apiClient.GetOrgSession()
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("error getting org session: %s", apiErr.Msg)
}
if org != nil && !org.IsTrial {
term.StopSpinner()
fmt.Println("🚀 Trial upgraded")
fmt.Println()
return
}
time.Sleep(1500 * time.Millisecond)
}
term.StopSpinner()
term.OutputErrorAndExit("Timed out waiting for upgrade. Please try again. Email support@plandex.ai if the problem persists.")
}
func startTrial() {
term.StartSpinner("")
cliTrialToken, apiErr := apiClient.CreateCliTrialSession()
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("error starting trial: %s", apiErr.Msg)
}
openUnauthenticatedCloudURL(
"Opening Plandex Cloud trial flow in your browser.",
fmt.Sprintf("/start?cliTrialToken=%s", cliTrialToken),
)
fmt.Println("\nCommand will continue automatically once you've started your trial...")
fmt.Println()
term.StartSpinner("")
startTime := time.Now()
expirationTime := startTime.Add(1 * time.Hour)
for time.Now().Before(expirationTime) {
cliTrialSession, apiErr := apiClient.GetCliTrialSession(cliTrialToken)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("error getting cli trial session: %s", apiErr.Msg)
}
if cliTrialSession != nil {
// Trial session is valid, break the loop and sign in
term.StopSpinner()
fmt.Println("🚀 Trial started")
fmt.Println()
err := handleSignInResponse(cliTrialSession, "")
if err != nil {
term.OutputErrorAndExit("error signing in after trial started: %s", err)
}
return
}
time.Sleep(1500 * time.Millisecond)
}
term.StopSpinner()
term.OutputErrorAndExit("Timed out waiting for trial to start. Please try again. Email support@plandex.ai if the problem persists.")
}
================================================
FILE: app/cli/cmd/apply.go
================================================
package cmd
import (
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/plan_exec"
"plandex-cli/term"
"plandex-cli/types"
"github.com/spf13/cobra"
)
var autoCommit, skipCommit, autoExec bool
func init() {
initApplyFlags(applyCmd, false)
initExecScriptFlags(applyCmd)
RootCmd.AddCommand(applyCmd)
applyCmd.Flags().BoolVar(&fullAuto, "full", false, "Apply the plan and debug in full auto mode")
}
var applyCmd = &cobra.Command{
Use: "apply",
Aliases: []string{"ap"},
Short: "Apply a plan to the project",
Run: apply,
}
func apply(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if fullAuto {
term.StartSpinner("")
config := lib.MustGetCurrentPlanConfig()
_, updatedConfig, printFn := resolveAutoModeSilent(config)
lib.SetCachedPlanConfig(updatedConfig)
term.StopSpinner()
printFn()
}
mustSetPlanExecFlags(cmd, true)
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
applyFlags := types.ApplyFlags{
AutoConfirm: true,
AutoCommit: autoCommit,
NoCommit: skipCommit,
AutoExec: autoExec,
NoExec: noExec,
AutoDebug: autoDebug,
}
tellFlags := types.TellFlags{
TellBg: tellBg,
TellStop: tellStop,
TellNoBuild: tellNoBuild,
AutoContext: tellAutoContext,
ExecEnabled: !noExec,
AutoApply: tellAutoApply,
}
lib.MustApplyPlan(lib.ApplyPlanParams{
PlanId: lib.CurrentPlanId,
Branch: lib.CurrentBranch,
ApplyFlags: applyFlags,
TellFlags: tellFlags,
OnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),
})
}
================================================
FILE: app/cli/cmd/archive.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
"strings"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var archiveCmd = &cobra.Command{
Use: "archive [name-or-index]",
Aliases: []string{"arc"},
Short: "Archive a plan",
Args: cobra.MaximumNArgs(1),
Run: archive,
}
func init() {
RootCmd.AddCommand(archiveCmd)
}
func archive(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
var nameOrIdx string
if len(args) > 0 {
nameOrIdx = strings.TrimSpace(args[0])
}
var plan *shared.Plan
term.StartSpinner("")
plans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
}
if len(plans) == 0 {
fmt.Println("🤷♂️ No plans available to archive")
return
}
if nameOrIdx == "" {
opts := make([]string, len(plans))
for i, p := range plans {
opts[i] = p.Name
}
selected, err := term.SelectFromList("Select a plan to archive", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting plan: %v", err)
}
for _, p := range plans {
if p.Name == selected {
plan = p
break
}
}
} else {
idx, err := strconv.Atoi(nameOrIdx)
if err == nil && idx > 0 && idx <= len(plans) {
plan = plans[idx-1]
} else {
for _, p := range plans {
if p.Name == nameOrIdx {
plan = p
break
}
}
}
}
if plan == nil {
term.OutputErrorAndExit("Plan not found")
}
err := api.Client.ArchivePlan(plan.Id)
if err != nil {
term.OutputErrorAndExit("Error archiving plan: %v", err)
}
fmt.Printf("✅ Plan %s archived\n", color.New(color.Bold, term.ColorHiYellow).Sprint(plan.Name))
fmt.Println()
term.PrintCmds("", "plans --archived", "unarchive")
}
================================================
FILE: app/cli/cmd/billing.go
================================================
package cmd
import (
"plandex-cli/auth"
"plandex-cli/term"
"plandex-cli/ui"
"github.com/spf13/cobra"
)
var billingCmd = &cobra.Command{
Use: "billing",
Short: "Open the billing page in the browser",
Run: billing,
}
func init() {
RootCmd.AddCommand(billingCmd)
}
func billing(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
if !auth.Current.IsCloud {
term.OutputErrorAndExit("This command is only available for Plandex Cloud accounts.")
}
ui.OpenAuthenticatedURL("Opening billing page in your default browser...", "/settings/billing")
}
================================================
FILE: app/cli/cmd/branches.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/format"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var branchesCmd = &cobra.Command{
Use: "branches",
Aliases: []string{"br"},
Short: "List plan branches",
Run: branches,
}
func init() {
RootCmd.AddCommand(branchesCmd)
}
func branches(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
branches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting branches: %v", apiErr)
return
}
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{"#", "Name", "Updated" /* "Created",*/, "Context", "Convo"})
for i, b := range branches {
num := strconv.Itoa(i + 1)
if b.Name == lib.CurrentBranch {
num = color.New(color.Bold, term.ColorHiGreen).Sprint(num)
}
var name string
if b.Name == lib.CurrentBranch {
name = color.New(color.Bold, term.ColorHiGreen).Sprint(b.Name) + " 👈"
} else {
name = b.Name
}
row := []string{
num,
name,
format.Time(b.UpdatedAt),
// format.Time(b.CreatedAt),
strconv.Itoa(b.ContextTokens) + " 🪙",
strconv.Itoa(b.ConvoTokens) + " 🪙",
}
var style []tablewriter.Colors
if b.Name == lib.CurrentPlanId {
style = []tablewriter.Colors{
{tablewriter.FgGreenColor, tablewriter.Bold},
}
} else {
style = []tablewriter.Colors{
{tablewriter.Bold},
}
}
table.Rich(row, style)
}
table.Render()
fmt.Println()
term.PrintCmds("", "checkout", "delete-branch")
}
================================================
FILE: app/cli/cmd/browser.go
================================================
package cmd
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
"time"
chrome_log "github.com/chromedp/cdproto/log"
chrome_runtime "github.com/chromedp/cdproto/runtime"
"github.com/chromedp/cdproto/target"
"github.com/chromedp/chromedp"
"github.com/spf13/cobra"
)
var timeoutSeconds int
// browserCmd is our cobra command
var browserCmd = &cobra.Command{
Use: "browser [urls...]",
Short: "Open browser windows with given URLs, capturing console logs and exiting on JS errors",
RunE: browser,
}
func init() {
RootCmd.AddCommand(browserCmd)
browserCmd.Flags().IntVar(&timeoutSeconds, "timeout", 10, "Timeout in seconds for browser to load")
}
// browser is the main function for our command.
func browser(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("no URLs provided")
}
// See if we can find Chrome or Firefox in PATH:
chromePath, _ := findChrome()
if chromePath != "" {
fmt.Println("Using Chrome (not default browser, but found in PATH).")
return openChromeWithLogs(args)
}
// Fallback: open with OS default tool (open/xdg-open)
fmt.Println("No Chrome or Firefox found; falling back to OS default opener.")
return openWithOSDefault(args)
}
// findChrome returns the path to Chrome/Chromium if found, or "" if not found.
func findChrome() (string, error) {
switch runtime.GOOS {
case "darwin":
// macOS standard installation paths
macPaths := []string{
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
}
for _, p := range macPaths {
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
case "linux", "freebsd":
// Linux/FreeBSD (usually in PATH)
candidates := []string{
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
}
for _, c := range candidates {
if path, err := exec.LookPath(c); err == nil {
return path, nil
}
}
}
return "", errors.New("Chrome/Chromium not found")
}
var pages = map[target.ID]string{}
// openChromeWithLogs uses chromedp to open each URL in a visible Chrome browser,
// logs JS console messages, and exits on the first JS error (or if user presses Ctrl+C).
func openChromeWithLogs(urls []string) error {
if len(urls) == 0 {
return errors.New("no URLs provided")
}
fmt.Println("Launching Chrome with console log capture...")
// 1) Create a cancellable context that sets up a Chrome ExecAllocator
rootCtx, cancelAllocator := chromedp.NewExecAllocator(context.Background(),
chromedp.Flag("headless", false), // Visible, not headless
chromedp.Flag("disable-gpu", false),
chromedp.Flag("no-first-run", true), // Avoid "Welcome" dialog
chromedp.Flag("no-default-browser-check", true), // Avoid default browser dialog
chromedp.Flag("disable-default-apps", true), // Prevent default apps/extensions from loading
chromedp.Flag("remote-debugging-port", "0"), // Ensures separate debug session
)
defer cancelAllocator()
// 2) Create a new browser context from the allocator
browserCtx, cancelBrowser := chromedp.NewContext(rootCtx)
defer cancelBrowser()
// Add this to get the initial target
var initialTargetID target.ID
chromedp.ListenTarget(browserCtx, func(ev interface{}) {
if e, ok := ev.(*target.EventTargetCreated); ok {
// fmt.Printf("[DEBUG] Got event: %#v\n", e)
if e.TargetInfo.Type == "page" {
initialTargetID = e.TargetInfo.TargetID
}
}
})
if err := chromedp.Run(
browserCtx,
chrome_runtime.Enable(),
chrome_log.Enable(),
); err != nil {
log.Fatal(err)
}
// Listen for console API calls & JS exceptions
chromedp.ListenBrowser(browserCtx, func(ev interface{}) {
// fmt.Printf("[DEBUG] Got event: %#v\n", ev)
switch e := ev.(type) {
case *target.EventTargetCreated:
case *target.EventAttachedToTarget:
// This sometimes also includes target info
info := e.TargetInfo
if info.Type == "page" {
url := info.URL
if url == "about:blank" {
url = urls[0]
}
pages[info.TargetID] = url
}
case *target.EventTargetDestroyed:
// Something was destroyed
destroyedID := e.TargetID
// Check if it was a top-level page
if url, ok := pages[destroyedID]; ok {
fmt.Printf("[CHROME] Closed page: %s\n", url)
// remove from map
delete(pages, destroyedID)
// If that was your last page, do something:
if len(pages) == 0 {
cancelBrowser()
cancelAllocator()
os.Exit(0)
} else {
fmt.Printf("num pages left: %d\n", len(pages))
}
}
}
})
// Channels to handle signals & error detection
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
errChan := make(chan bool, 1)
// 3) Open all URLs in new tabs
for i, url := range urls {
// Reuse the initial browser tab for the first URL
var ctx context.Context
if i == 0 {
ctx, _ = chromedp.NewContext(browserCtx, chromedp.WithTargetID(initialTargetID))
} else {
// Create a new tab for each additional URL
ctx, _ = chromedp.NewContext(browserCtx)
}
tabCtx, _ := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
chromedp.ListenTarget(tabCtx, func(ev interface{}) {
switch e := ev.(type) {
case *chrome_runtime.EventConsoleAPICalled:
logType := e.Type.String()
for _, arg := range e.Args {
val := arg.Value
fmt.Printf("[CHROME:%s] %v\n", logType, val)
if logType == "error" {
fmt.Println("❌ Chrome console error")
errChan <- true
}
}
case *chrome_log.EventEntryAdded:
fmt.Printf("[CHROME:Log:%s] %s\n", e.Entry.Level, e.Entry.Text)
case *chrome_runtime.EventExceptionThrown:
fmt.Printf("[CHROME:Exception] %s\n", e.ExceptionDetails.Error())
fmt.Println("❌ JavaScript exception")
errChan <- true
}
})
if err := chromedp.Run(
tabCtx,
chrome_runtime.Enable(),
chrome_log.Enable(),
chromedp.Navigate(url),
chromedp.WaitReady("body", chromedp.ByQuery),
); err != nil {
fmt.Printf("Failed to load url %s: %v", url, err)
cancelBrowser()
cancelAllocator()
os.Exit(1)
} else {
fmt.Printf("Opened (%d/%d): %s\n", i+1, len(urls), url)
}
}
fmt.Println("Chrome is running, waiting for error or interrupt...")
// 4) Wait forever, or until an error or signal
select {
case <-sigChan:
fmt.Println("\n⚠️ Plandex browser process interrupted")
return nil
case <-rootCtx.Done():
fmt.Println("\n⚠️ Plandex browser process closed")
return nil
case <-errChan:
fmt.Println("\n❌ Plandex browser process exited due to JavaScript error")
cancelBrowser()
cancelAllocator()
os.Exit(1)
}
return nil
}
func openWithOSDefault(urls []string) error {
var openCmd string
switch runtime.GOOS {
case "darwin":
openCmd = "open"
case "linux", "freebsd":
openCmd = "xdg-open"
default:
return errors.New("unsupported OS for fallback")
}
for _, url := range urls {
cmd := exec.Command(openCmd, url)
if err := cmd.Start(); err != nil {
log.Printf("Failed to open %s with %s: %v", url, openCmd, err)
}
}
return nil
}
================================================
FILE: app/cli/cmd/build.go
================================================
package cmd
import (
"fmt"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/plan_exec"
"plandex-cli/term"
"plandex-cli/types"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var buildCmd = &cobra.Command{
Use: "build",
Aliases: []string{"b"},
Short: "Build pending changes",
// Long: ``,
Args: cobra.NoArgs,
Run: build,
}
func init() {
RootCmd.AddCommand(buildCmd)
initExecFlags(buildCmd, initExecFlagsParams{
omitFile: true,
omitNoBuild: true,
omitEditor: true,
omitStop: true,
omitAutoContext: true,
omitSmartContext: true,
})
}
func build(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
mustSetPlanExecFlags(cmd, false)
didBuild, err := plan_exec.Build(plan_exec.ExecParams{
CurrentPlanId: lib.CurrentPlanId,
CurrentBranch: lib.CurrentBranch,
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
auto := autoConfirm || tellAutoApply || tellAutoContext
return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
},
}, types.BuildFlags{
BuildBg: tellBg,
AutoApply: tellAutoApply,
})
if err != nil {
term.OutputErrorAndExit("Error building plan: %v", err)
}
if !didBuild {
fmt.Println()
term.PrintCmds("", "log", "tell", "continue")
return
}
if tellBg {
fmt.Println("🏗️ Building plan in the background")
fmt.Println()
term.PrintCmds("", "ps", "connect", "stop")
} else if tellAutoApply {
applyFlags := types.ApplyFlags{
AutoConfirm: true,
AutoCommit: autoCommit,
NoCommit: !autoCommit,
NoExec: noExec,
AutoExec: autoExec,
AutoDebug: autoDebug,
}
tellFlags := types.TellFlags{
AutoContext: tellAutoContext,
ExecEnabled: !noExec,
AutoApply: tellAutoApply,
}
lib.MustApplyPlan(lib.ApplyPlanParams{
PlanId: lib.CurrentPlanId,
Branch: lib.CurrentBranch,
ApplyFlags: applyFlags,
TellFlags: tellFlags,
OnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),
})
} else {
fmt.Println()
term.PrintCmds("", "diff", "diff --ui", "apply", "reject", "log")
}
}
================================================
FILE: app/cli/cmd/cd.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
"strings"
"time"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
func init() {
RootCmd.AddCommand(cdCmd)
}
var cdCmd = &cobra.Command{
Use: "cd [name-or-index]",
Aliases: []string{"set-plan"},
Short: "Set current plan by name or index",
Args: cobra.MaximumNArgs(1),
Run: cd,
}
func cd(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
var nameOrIdx string
if len(args) > 0 {
nameOrIdx = strings.TrimSpace(args[0])
}
var plan *shared.Plan
term.StartSpinner("")
plans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
}
if len(plans) == 0 {
fmt.Println("🤷♂️ No plans")
fmt.Println()
term.PrintCmds("", "new")
return
}
if nameOrIdx == "" {
opts := make([]string, len(plans))
for i, plan := range plans {
opts[i] = plan.Name
}
selected, err := term.SelectFromList("Select a plan", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting plan: %v", err)
}
for _, p := range plans {
if p.Name == selected {
plan = p
break
}
}
} else {
// see if it's an index
idx, err := strconv.Atoi(nameOrIdx)
if err == nil {
if idx > 0 && idx <= len(plans) {
plan = plans[idx-1]
} else {
term.OutputErrorAndExit("Plan index out of range")
}
} else {
for _, p := range plans {
if p.Name == nameOrIdx {
plan = p
break
}
}
}
}
if plan == nil {
term.OutputErrorAndExit("Plan not found")
}
err := lib.WriteCurrentPlan(plan.Id)
if err != nil {
term.OutputErrorAndExit("Error setting current plan: %v", err)
}
// reload current plan, which will also handle setting the right branch
lib.MustLoadCurrentPlan()
// fire and forget SetProjectPlan request (we don't care about the response or errors)
// this only matters for setting the current plan on a new device (i.e. when the current plan is not set)
go api.Client.SetProjectPlan(lib.CurrentProjectId, shared.SetProjectPlanRequest{PlanId: plan.Id})
// give the SetProjectPlan request some time to be sent before exiting
time.Sleep(50 * time.Millisecond)
fmt.Println("✅ Changed current plan to " + color.New(term.ColorHiGreen, color.Bold).Sprint(plan.Name))
fmt.Println()
term.PrintCmds("", "current")
}
================================================
FILE: app/cli/cmd/chat.go
================================================
package cmd
import (
"fmt"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/plan_exec"
"plandex-cli/types"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var chatCmd = &cobra.Command{
Use: "chat [prompt]",
Aliases: []string{"c"},
Short: "Chat without making changes",
// Long: ``,
Args: cobra.RangeArgs(0, 1),
Run: doChat,
}
func init() {
RootCmd.AddCommand(chatCmd)
initExecFlags(chatCmd, initExecFlagsParams{
omitNoBuild: true,
omitStop: true,
omitBg: true,
omitApply: true,
omitExec: true,
omitSmartContext: true,
omitSkipMenu: true,
})
}
func doChat(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
mustSetPlanExecFlags(cmd, false)
prompt := getTellPrompt(args)
if prompt == "" {
fmt.Println("🤷♂️ No prompt to send")
return
}
plan_exec.TellPlan(plan_exec.ExecParams{
CurrentPlanId: lib.CurrentPlanId,
CurrentBranch: lib.CurrentBranch,
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
auto := autoConfirm || tellAutoApply || tellAutoContext
return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
},
}, prompt, types.TellFlags{
IsChatOnly: true,
AutoContext: tellAutoContext,
SkipChangesMenu: tellSkipMenu,
})
}
================================================
FILE: app/cli/cmd/checkout.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
"strings"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
const (
OptCreateNewBranch = "Create a new branch"
)
var confirmCreateBranch bool
var checkoutCmd = &cobra.Command{
Use: "checkout [name-or-index]",
Aliases: []string{"co"},
Short: "Checkout an existing plan branch or create a new one",
Run: checkout,
Args: cobra.MaximumNArgs(1),
}
func init() {
RootCmd.AddCommand(checkoutCmd)
checkoutCmd.Flags().BoolVarP(&confirmCreateBranch, "yes", "y", false, "Confirm creating a new branch")
}
func checkout(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
branchName := ""
willCreate := false
var nameOrIdx string
if len(args) > 0 {
nameOrIdx = strings.TrimSpace(args[0])
}
term.StartSpinner("")
branches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting branches: %v", apiErr)
return
}
if nameOrIdx != "" {
idx, err := strconv.Atoi(nameOrIdx)
if err == nil {
if idx > 0 && idx <= len(branches) {
branchName = branches[idx-1].Name
} else {
term.OutputErrorAndExit("Branch %d not found", idx)
}
} else {
for _, b := range branches {
if b.Name == nameOrIdx {
branchName = b.Name
break
}
}
}
if branchName == "" {
fmt.Printf("🌱 Branch %s not found\n", color.New(color.Bold, term.ColorHiCyan).Sprint(nameOrIdx))
if confirmCreateBranch {
fmt.Println("✅ --yes flag set, will create branch")
branchName = nameOrIdx
willCreate = true
} else {
res, err := term.ConfirmYesNo("Create it now?")
if err != nil {
term.OutputErrorAndExit("Error getting user input: %v", err)
}
if res {
branchName = nameOrIdx
willCreate = true
} else {
return
}
}
}
}
if nameOrIdx == "" {
opts := make([]string, len(branches))
for i, branch := range branches {
opts[i] = branch.Name
}
opts = append(opts, OptCreateNewBranch)
selected, err := term.SelectFromList("Select a branch", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting branch: %v", err)
return
}
if selected == OptCreateNewBranch {
branchName, err = term.GetRequiredUserStringInput("Branch name")
if err != nil {
term.OutputErrorAndExit("Error getting branch name: %v", err)
return
}
willCreate = true
} else {
branchName = selected
}
}
if branchName == "" {
term.OutputErrorAndExit("Branch not found")
}
term.StartSpinner("")
if willCreate {
err := api.Client.CreateBranch(lib.CurrentPlanId, lib.CurrentBranch, shared.CreateBranchRequest{Name: branchName})
if err != nil {
term.OutputErrorAndExit("Error creating branch: %v", err)
return
}
// fmt.Printf("✅ Created branch %s\n", color.New(color.Bold, term.ColorHiGreen).Sprint(branchName))
}
err := lib.WriteCurrentBranch(branchName)
if err != nil {
term.OutputErrorAndExit("Error setting current branch: %v", err)
return
}
updatedModelSettings, err := lib.SaveLatestPlanModelSettingsIfNeeded()
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error saving model settings: %v", err)
}
term.StopSpinner()
fmt.Printf("✅ Checked out branch %s\n", color.New(color.Bold, term.ColorHiGreen).Sprint(branchName))
if updatedModelSettings {
fmt.Println()
fmt.Println("🧠 Model settings file updated → ", lib.GetPlanModelSettingsPath(lib.CurrentPlanId))
}
fmt.Println()
term.PrintCmds("", "load", "tell", "branches", "delete-branch")
}
================================================
FILE: app/cli/cmd/claude_max.go
================================================
package cmd
import (
"fmt"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var connectClaudeCmd = &cobra.Command{
Use: "connect-claude",
Short: "Connect your Claude Pro or Max subscription",
Run: connectClaude,
}
var disconnectClaudeCmd = &cobra.Command{
Use: "disconnect-claude",
Short: "Disconnect your Claude Pro or Max subscription",
Run: disconnectClaude,
}
var claudeStatusCmd = &cobra.Command{
Use: "claude-status",
Short: "Check the status of your Claude Pro or Max subscription",
Run: claudeStatus,
}
func connectClaude(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.ConnectClaudeMax()
}
func disconnectClaude(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.DisconnectClaudeMax()
}
func claudeStatus(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
creds, err := lib.GetAccountCredentials()
if err != nil {
term.OutputErrorAndExit("Error getting account credentials: %v", err)
}
orgUserConfig := lib.MustGetOrgUserConfig()
connected := creds.ClaudeMax != nil && orgUserConfig.UseClaudeSubscription
if connected {
fmt.Println("✅ Claude Pro or Max subscription is connected")
// if orgUserConfig.IsClaudeSubscriptionCooldownActive() {
if true {
fmt.Println()
color.New(term.ColorHiYellow, color.Bold).Println("⏳ You've reached your Claude Pro or Max subscription quota")
fmt.Println("The next provider with valid credentials will be used for Anthropic models until the quota resets")
fmt.Println()
}
term.PrintCmds("", "disconnect-claude")
} else {
fmt.Println("❌ No Claude Pro or Max subscription is connected")
term.PrintCmds("", "connect-claude")
}
}
func init() {
RootCmd.AddCommand(connectClaudeCmd)
RootCmd.AddCommand(disconnectClaudeCmd)
RootCmd.AddCommand(claudeStatusCmd)
}
================================================
FILE: app/cli/cmd/clear.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var clearCmd = &cobra.Command{
Use: "clear",
Short: "Clear all context",
Long: `Clear all context.`,
Run: clearAllContext,
}
func clearAllContext(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error retrieving context: %v", err)
}
deleteIds := map[string]bool{}
for _, context := range contexts {
deleteIds[context.Id] = true
}
if len(deleteIds) > 0 {
res, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{
Ids: deleteIds,
})
if err != nil {
term.OutputErrorAndExit("Error deleting context: %v", err)
}
fmt.Println("✅ " + res.Msg)
} else {
fmt.Println("🤷♂️ No context removed")
}
}
func init() {
RootCmd.AddCommand(clearCmd)
}
================================================
FILE: app/cli/cmd/config.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
func init() {
RootCmd.AddCommand(configCmd)
configCmd.AddCommand(defaultConfigCmd)
}
var configCmd = &cobra.Command{
Use: "config",
Short: "Show plan config",
Run: config,
}
var defaultConfigCmd = &cobra.Command{
Use: "default",
Short: "Show default config for new plans",
Run: defaultConfig,
}
func config(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
config, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error getting config: %v", apiErr.Msg)
return
}
term.StopSpinner()
color.New(color.Bold, term.ColorHiCyan).Println("⚙️ Plan Config")
lib.ShowPlanConfig(config, "")
fmt.Println()
term.PrintCmds("", "set-config", "config default", "set-config default")
}
func defaultConfig(cmd *cobra.Command, args []string) {
auth.MustResolveAuth(false)
term.StartSpinner("")
config, err := api.Client.GetDefaultPlanConfig()
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error getting default config: %v", err)
return
}
color.New(color.Bold, term.ColorHiCyan).Println("⚙️ Default Config")
lib.ShowPlanConfig(config, "")
fmt.Println()
term.PrintCmds("", "set-config default", "config", "set-config")
}
================================================
FILE: app/cli/cmd/connect.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/stream"
streamtui "plandex-cli/stream_tui"
"plandex-cli/term"
"github.com/spf13/cobra"
)
var connectCmd = &cobra.Command{
Use: "connect [stream-id-or-plan] [branch]",
Aliases: []string{"conn"},
Short: "Connect to an active stream",
// Long: ``,
Args: cobra.MaximumNArgs(2),
Run: connect,
}
func init() {
RootCmd.AddCommand(connectCmd)
}
func connect(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
planId, branch, shouldContinue := lib.SelectActiveStream(args)
if !shouldContinue {
return
}
term.StartSpinner("")
apiErr := api.Client.ConnectPlan(planId, branch, stream.OnStreamPlan)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error connecting to stream: %v", apiErr)
}
go func() {
err := streamtui.StartStreamUI("", false, true)
if err != nil {
term.OutputErrorAndExit("Error starting stream UI", err)
}
fmt.Println()
term.PrintCmds("", "diff", "diff --ui", "apply", "reject", "log")
os.Exit(0)
}()
// Wait for the stream to finish
select {}
}
================================================
FILE: app/cli/cmd/context_show.go
================================================
package cmd
import (
"fmt"
"log"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"strconv"
"github.com/spf13/cobra"
)
func init() {
RootCmd.AddCommand(contextShowCmd)
}
var contextShowCmd = &cobra.Command{
Use: "show [name-or-index]",
Short: "Show the body of a context by name or list index",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
nameOrIndex := args[0]
// Get list of contexts first
contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
if err != nil {
log.Printf("Error listing contexts: %v\n", err)
return fmt.Errorf("error listing contexts: %v", err)
}
var contextId string
// Try parsing as index first
if idx, err := strconv.Atoi(nameOrIndex); err == nil {
// Convert to 0-based index
idx--
if idx < 0 || idx >= len(contexts) {
return fmt.Errorf("invalid context index: %s", nameOrIndex)
}
contextId = contexts[idx].Id
} else {
// Try finding by name
found := false
for _, ctx := range contexts {
if ctx.Name == nameOrIndex || ctx.FilePath == nameOrIndex {
contextId = ctx.Id
found = true
break
}
}
if !found {
return fmt.Errorf("no context found with name: %s", nameOrIndex)
}
}
res, apiErr := api.Client.GetContextBody(lib.CurrentPlanId, lib.CurrentBranch, contextId)
if apiErr != nil {
log.Printf("Error getting context body: %v\n", apiErr)
return fmt.Errorf("error getting context body: %v", apiErr)
}
fmt.Println(res.Body)
return nil
},
}
================================================
FILE: app/cli/cmd/continue.go
================================================
package cmd
import (
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/plan_exec"
"plandex-cli/types"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var (
chatOnly bool
)
var continueCmd = &cobra.Command{
Use: "continue",
Aliases: []string{"c"},
Short: "Continue the plan",
Run: doContinue,
}
func init() {
RootCmd.AddCommand(continueCmd)
continueCmd.Flags().BoolVar(&chatOnly, "chat", false, "Continue in chat mode (no file changes)")
initExecFlags(continueCmd, initExecFlagsParams{
omitFile: true,
omitEditor: true,
})
}
func doContinue(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
mustSetPlanExecFlags(cmd, false)
tellFlags := types.TellFlags{
TellBg: tellBg,
TellStop: tellStop,
TellNoBuild: tellNoBuild,
IsUserContinue: true,
ExecEnabled: !noExec,
AutoContext: tellAutoContext,
SmartContext: tellSmartContext,
AutoApply: tellAutoApply,
IsChatOnly: chatOnly,
SkipChangesMenu: tellSkipMenu,
}
plan_exec.TellPlan(plan_exec.ExecParams{
CurrentPlanId: lib.CurrentPlanId,
CurrentBranch: lib.CurrentBranch,
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
auto := autoConfirm || tellAutoApply || tellAutoContext
return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
},
}, "", tellFlags)
if tellAutoApply {
applyFlags := types.ApplyFlags{
AutoConfirm: true,
AutoCommit: autoCommit,
NoCommit: !autoCommit,
AutoExec: autoExec,
NoExec: noExec,
AutoDebug: autoDebug,
}
lib.MustApplyPlan(lib.ApplyPlanParams{
PlanId: lib.CurrentPlanId,
Branch: lib.CurrentBranch,
ApplyFlags: applyFlags,
TellFlags: tellFlags,
OnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),
})
}
}
================================================
FILE: app/cli/cmd/convo.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"regexp"
"strings"
"time"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var plainTextOutput bool
var convoRaw bool
// convoCmd represents the convo command
var convoCmd = &cobra.Command{
Use: "convo [msg-range]",
Short: "Display complete conversation history",
Long: `Display complete conversation history. Optionally specify a message number or range of messages (e.g. '1' or '5' or '1-5' or '5-')`,
Run: convo,
}
func init() {
RootCmd.AddCommand(convoCmd)
convoCmd.Flags().BoolVarP(&plainTextOutput, "plain", "p", false, "Output conversation in plain text with no ANSI codes")
// for debugging output
convoCmd.Flags().BoolVar(&convoRaw, "raw", false, "Output conversation in raw format")
convoCmd.Flags().MarkHidden("raw")
}
const stoppedEarlyMsg = "You stopped the reply early"
func convo(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
term.StartSpinner("")
conversation, apiErr := api.Client.ListConvo(lib.CurrentPlanId, lib.CurrentBranch)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error loading conversation: %v", apiErr.Msg)
}
if len(conversation) == 0 {
fmt.Println("🤷♂️ No conversation history")
return
}
var msgRange string
var msgRangeStart, msgRangeEnd int
if len(args) > 0 {
msgRange = args[0]
}
if msgRange != "" {
// validate either a number or a range of numbers
if strings.Contains(msgRange, "-") {
_, err := fmt.Sscanf(msgRange, "%d-%d", &msgRangeStart, &msgRangeEnd)
if err != nil {
_, err := fmt.Sscanf(msgRange, "%d-", &msgRangeStart)
if err != nil {
term.OutputErrorAndExit("Invalid message range: %s", msgRange)
}
msgRangeEnd = len(conversation)
}
} else {
_, err := fmt.Sscanf(msgRange, "%d", &msgRangeStart)
if err != nil {
term.OutputErrorAndExit("Invalid message number: %s", msgRange)
}
msgRangeEnd = msgRangeStart
}
}
var convo string
var totalTokens int
var didCut bool
for i, msg := range conversation {
if msgRangeStart > 0 && msg.Num < msgRangeStart {
didCut = true
continue
}
if msgRangeEnd > 0 && msg.Num > msgRangeEnd {
didCut = true
break
}
var author string
if msg.Role == "assistant" {
author = "🤖 Plandex"
} else if msg.Role == "user" {
author = "💬 You"
} else {
author = msg.Role
}
replyTags := msg.Flags.GetReplyTags()
// format as above but start with day of week
formattedTs := msg.CreatedAt.Local().Format("Mon Jan 2, 2006 | 3:04pm MST")
// if it's today then use 'Today' instead of the date
if msg.CreatedAt.Day() == time.Now().Day() {
formattedTs = msg.CreatedAt.Local().Format("Today | 3:04pm MST")
}
// if it's yesterday then use 'Yesterday' instead of the date
if msg.CreatedAt.Day() == time.Now().AddDate(0, 0, -1).Day() {
formattedTs = msg.CreatedAt.Local().Format("Yesterday | 3:04pm MST")
}
var header string
if len(replyTags) > 0 {
header = fmt.Sprintf("#### %d | %s | %s | %s | %d 🪙 ", i+1,
author, strings.Join(replyTags, " | "), formattedTs, msg.Tokens)
} else {
header = fmt.Sprintf("#### %d | %s | %s | %d 🪙 ", i+1,
author, formattedTs, msg.Tokens)
}
txt := msg.Message
if !convoRaw {
txt = convertCodeBlocks(msg.Message)
}
if plainTextOutput {
convo += header + "\n" + txt + "\n\n"
} else {
md, err := term.GetMarkdown(header + "\n" + txt + "\n\n")
if err != nil {
term.OutputErrorAndExit("Error creating markdown representation: %v", err)
}
convo += md
}
if !didCut && msg.Stopped {
if plainTextOutput {
convo += fmt.Sprintf(" 🛑 %s\n\n", stoppedEarlyMsg)
} else {
convo += fmt.Sprintf(" 🛑 %s\n\n", color.New(color.Bold).Sprint(stoppedEarlyMsg))
}
}
totalTokens += msg.Tokens
}
if !plainTextOutput {
convo = strings.ReplaceAll(convo, stoppedEarlyMsg, color.New(term.ColorHiRed).Sprint(stoppedEarlyMsg))
}
output :=
fmt.Sprintf("\n%s", convo)
if !plainTextOutput && !didCut {
output += term.GetDivisionLine() +
color.New(color.Bold, term.ColorHiCyan).Sprint(" Conversation size →") + fmt.Sprintf(" %d 🪙", totalTokens) + "\n\n"
}
if plainTextOutput {
fmt.Println(output)
} else {
term.PageOutput(output)
fmt.Println()
term.PrintCmds("", "convo 1", "convo 2-5", "convo --plain", "log")
}
}
var codeBlockPattern = regexp.MustCompile(`([\s\S]+?)`)
func convertCodeBlocks(msg string) string {
return codeBlockPattern.ReplaceAllStringFunc(msg, func(match string) string {
// Extract language and content from the match
submatches := codeBlockPattern.FindStringSubmatch(match)
lang := submatches[1]
content := submatches[2]
// Escape any backticks in the content
content = strings.ReplaceAll(content, "```", "\\`\\`\\`")
// Return markdown code block format
return fmt.Sprintf("```%s%s```", lang, content)
})
}
================================================
FILE: app/cli/cmd/current.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var currentCmd = &cobra.Command{
Use: "current",
Aliases: []string{"cu"},
Short: "Get the current plan",
Run: current,
}
func init() {
RootCmd.AddCommand(currentCmd)
}
func current(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MaybeResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
plan, err := api.Client.GetPlan(lib.CurrentPlanId)
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error getting plan: %v", err)
return
}
currentBranchesByPlanId, err := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{
CurrentBranchByPlanId: map[string]string{
lib.CurrentPlanId: lib.CurrentBranch,
},
})
if err != nil {
term.OutputErrorAndExit("Error getting current branches: %v", err)
}
table := lib.GetCurrentPlanTable(plan, currentBranchesByPlanId, nil)
fmt.Println(table)
term.PrintCmds("", "tell", "ls", "plans")
}
================================================
FILE: app/cli/cmd/debug.go
================================================
package cmd
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/plan_exec"
"plandex-cli/term"
"plandex-cli/types"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
const DebugDefaultTries = 5
var debugCmd = &cobra.Command{
Use: "debug [tries] ",
Aliases: []string{"db"},
Short: "Debug a failing command with Plandex",
Args: cobra.MinimumNArgs(1),
Run: doDebug,
}
func init() {
RootCmd.AddCommand(debugCmd)
debugCmd.Flags().BoolVarP(&autoCommit, "commit", "c", false, "Commit changes after successful execution")
}
func doDebug(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
mustSetPlanExecFlags(cmd, false)
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
// Parse tries and command
tries := DebugDefaultTries
cmdArgs := args
// Check if first arg is tries count
if val, err := strconv.Atoi(args[0]); err == nil {
if val <= 0 {
term.OutputErrorAndExit("Tries must be greater than 0")
}
tries = val
cmdArgs = args[1:]
if len(cmdArgs) == 0 {
term.OutputErrorAndExit("No command specified")
}
}
// Get current working directory
cwd, err := os.Getwd()
if err != nil {
term.OutputErrorAndExit("Failed to get working directory: %v", err)
}
cmdStr := strings.Join(cmdArgs, " ")
// Execute command and handle retries
for attempt := 0; attempt < tries; attempt++ {
// Use shell to handle operators like && and |
shellCmdStr := "set -euo pipefail; " + cmdStr
execCmd := exec.Command("sh", "-c", shellCmdStr)
execCmd.Dir = cwd
execCmd.Env = os.Environ()
lib.SetPlatformSpecificAttrs(execCmd)
pipe, err := execCmd.StdoutPipe()
if err != nil {
term.StopSpinner()
term.OutputErrorAndExit("Failed to create pipe: %v", err)
}
execCmd.Stderr = execCmd.Stdout
if err := execCmd.Start(); err != nil {
term.StopSpinner()
term.OutputErrorAndExit("Failed to start command: %v", err)
}
maybeDeleteCgroup := lib.MaybeIsolateCgroup(execCmd)
ctx, cancel := context.WithCancel(context.Background())
var interrupted atomic.Bool
var interruptHandled atomic.Bool
var interruptWG sync.WaitGroup
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
interruptWG.Add(1)
go func() {
defer interruptWG.Done()
for {
select {
case sig := <-sigChan:
if interruptHandled.CompareAndSwap(false, true) {
fmt.Println()
color.New(term.ColorHiYellow, color.Bold).Println("\n👉 Caught interrupt. Exiting gracefully...")
interrupted.Store(true)
var sysSig syscall.Signal
switch sig {
case os.Interrupt:
// user pressed Ctrl+C
sysSig = syscall.SIGINT
case syscall.SIGTERM:
// a polite "kill" request
sysSig = syscall.SIGTERM
case syscall.SIGHUP:
sysSig = syscall.SIGHUP
case syscall.SIGQUIT:
sysSig = syscall.SIGQUIT
default:
sysSig = syscall.SIGINT
}
if err := lib.KillProcessGroup(execCmd, sysSig); err != nil {
log.Printf("Failed to send signal %s to process group: %v", sysSig, err)
}
select {
case <-time.After(2 * time.Second):
color.New(term.ColorHiYellow, color.Bold).Println("👉 Commands didn't exit after 2 seconds. Sending SIGKILL.")
if err := lib.KillProcessGroup(execCmd, syscall.SIGKILL); err != nil {
log.Printf("Failed to send SIGKILL to process group: %v", err)
}
maybeDeleteCgroup()
case <-ctx.Done():
maybeDeleteCgroup()
return
}
}
case <-ctx.Done():
maybeDeleteCgroup()
return
}
}
}()
var outputBuilder strings.Builder
scanner := bufio.NewScanner(pipe)
go func() {
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
outputBuilder.WriteString(line + "\n")
}
}()
waitErr := execCmd.Wait()
cancel()
interruptWG.Wait()
signal.Stop(sigChan)
close(sigChan)
if scanErr := scanner.Err(); scanErr != nil {
log.Printf("⚠️ Scanner error reading subprocess output: %v", scanErr)
}
term.StopSpinner()
outputStr := outputBuilder.String()
if outputStr == "" && waitErr != nil {
outputStr = waitErr.Error()
}
if outputStr != "" {
fmt.Println(outputStr)
}
didSucceed := waitErr == nil
if interrupted.Load() {
color.New(term.ColorHiYellow, color.Bold).Println("👉 Execution interrupted")
res, canceled, err := term.ConfirmYesNoCancel("Did the command succeed?")
if err != nil {
term.OutputErrorAndExit("Failed to get confirmation user input: %s", err)
}
didSucceed = res
if canceled {
os.Exit(0)
}
}
if didSucceed {
if attempt == 0 {
fmt.Printf("✅ Command %s succeeded on first try\n", color.New(color.Bold, term.ColorHiCyan).Sprintf(cmdStr))
} else {
lbl := "attempts"
if attempt == 1 {
lbl = "attempt"
}
fmt.Printf("✅ Command %s succeeded after %d fix %s\n", color.New(color.Bold, term.ColorHiCyan).Sprintf(cmdStr), attempt, lbl)
}
return
}
if attempt == tries-1 {
fmt.Printf("Command failed after %d tries\n", tries)
os.Exit(1)
}
// Prepare prompt for TellPlan
exitErr, ok := waitErr.(*exec.ExitError)
status := -1
if ok {
status = exitErr.ExitCode()
}
prompt := fmt.Sprintf("'%s' failed with exit status %d. Output:\n\n%s\n\n--\n\n",
strings.Join(cmdArgs, " "), status, outputStr)
tellFlags := types.TellFlags{
AutoContext: tellAutoContext,
ExecEnabled: false,
IsUserDebug: true,
}
plan_exec.TellPlan(plan_exec.ExecParams{
CurrentPlanId: lib.CurrentPlanId,
CurrentBranch: lib.CurrentBranch,
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
return lib.CheckOutdatedContextWithOutput(true, true, maybeContexts, projectPaths)
},
}, prompt, tellFlags)
applyFlags := types.ApplyFlags{
AutoConfirm: true,
AutoCommit: autoCommit,
NoCommit: !autoCommit,
NoExec: false,
AutoExec: true,
}
lib.MustApplyPlan(lib.ApplyPlanParams{
PlanId: lib.CurrentPlanId,
Branch: lib.CurrentBranch,
ApplyFlags: applyFlags,
TellFlags: tellFlags,
OnExecFail: plan_exec.GetOnApplyExecFailWithCommand(applyFlags, tellFlags, cmdStr),
ExecCommand: cmdStr,
})
}
}
================================================
FILE: app/cli/cmd/delete_branch.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var deleteBranchCmd = &cobra.Command{
Use: "delete-branch",
Aliases: []string{"dlb"},
Short: "Delete a plan branch by name or index",
Run: deleteBranch,
Args: cobra.MaximumNArgs(1),
}
func init() {
RootCmd.AddCommand(deleteBranchCmd)
}
func deleteBranch(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
var branch string
var nameOrIdx string
if len(args) > 0 {
nameOrIdx = strings.TrimSpace(args[0])
}
if nameOrIdx == "main" {
fmt.Println("🚨 Cannot delete main branch")
return
}
term.StartSpinner("")
branches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting branches: %v", apiErr)
return
}
if nameOrIdx == "" {
opts := make([]string, len(branches))
for i, branch := range branches {
if branch.Name == "main" {
continue
}
opts[i] = branch.Name
}
if len(opts) == 0 {
fmt.Println("🤷♂️ No branches to delete")
return
}
sel, err := term.SelectFromList("Select a branch to delete", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting branch: %v", err)
return
}
branch = sel
}
// see if it's an index
idx, err := strconv.Atoi(nameOrIdx)
if err == nil {
if idx > 0 && idx <= len(branches) {
branch = branches[idx-1].Name
} else {
term.OutputErrorAndExit("Branch index out of range")
}
} else {
for _, b := range branches {
if b.Name == nameOrIdx {
branch = b.Name
break
}
}
if branch == "" {
term.OutputErrorAndExit("Branch not found")
}
}
found := false
for _, b := range branches {
if b.Name == branch {
found = true
break
}
}
if !found {
fmt.Printf("🤷♂️ Branch %s does not exist\n", color.New(color.Bold, term.ColorHiCyan).Sprint(branch))
return
}
term.StartSpinner("")
apiErr = api.Client.DeleteBranch(lib.CurrentPlanId, branch)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error deleting branch: %v", apiErr)
return
}
fmt.Printf("✅ Deleted branch %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint(branch))
fmt.Println()
term.PrintCmds("", "branches")
}
================================================
FILE: app/cli/cmd/delete_plan.go
================================================
package cmd
import (
"fmt"
"path"
"strconv"
"strings"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var all bool
func init() {
rmCmd.Flags().BoolVar(&all, "all", false, "Delete all plans")
RootCmd.AddCommand(rmCmd)
}
// rmCmd represents the rm command
var rmCmd = &cobra.Command{
Use: "delete-plan [name-or-index]",
Aliases: []string{"dp"},
Short: "Delete a plan by name, index, range, or pattern, or select from a list. Delete all plans with --all flag.",
Args: cobra.RangeArgs(0, 1),
Run: del,
}
func matchPlansByPattern(pattern string, plans []*shared.Plan) []*shared.Plan {
var matched []*shared.Plan
for _, plan := range plans {
if isMatched, err := path.Match(pattern, plan.Name); err == nil && isMatched {
matched = append(matched, plan)
}
}
return matched
}
func del(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if all {
delAll()
return
}
term.StartSpinner("")
plans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
}
if len(plans) == 0 {
fmt.Println("🤷♂️ No plans")
fmt.Println()
term.PrintCmds("", "new")
return
}
var plansToDelete []*shared.Plan
if len(args) == 0 {
// Interactive selection
opts := make([]string, len(plans))
for i, plan := range plans {
opts[i] = plan.Name
}
selected, err := term.SelectFromList("Select a plan:", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting plan: %v", err)
}
for _, p := range plans {
if p.Name == selected {
plansToDelete = append(plansToDelete, p)
break
}
}
} else {
nameOrPattern := strings.TrimSpace(args[0])
// Check if it's a range of indices
if strings.Contains(nameOrPattern, "-") {
// Create single-element slice with the range pattern
rangeArgs := []string{nameOrPattern}
indices := parseIndices(rangeArgs)
for idx := range indices {
if idx >= 0 && idx < len(plans) {
plansToDelete = append(plansToDelete, plans[idx])
}
}
} else if strings.Contains(nameOrPattern, "*") {
// Wildcard pattern matching
plansToDelete = matchPlansByPattern(nameOrPattern, plans)
} else {
// Try as index first
idx, err := strconv.Atoi(nameOrPattern)
if err == nil {
if idx > 0 && idx <= len(plans) {
plansToDelete = append(plansToDelete, plans[idx-1])
} else {
term.OutputErrorAndExit("Plan index out of range")
}
} else {
// Try exact name match
for _, p := range plans {
if p.Name == nameOrPattern {
plansToDelete = append(plansToDelete, p)
break
}
}
}
}
}
if len(plansToDelete) == 0 {
term.OutputErrorAndExit("No matching plans found")
}
// Show confirmation with list of plans to be deleted
fmt.Printf("\nThe following %d plan(s) will be deleted:\n", len(plansToDelete))
for _, p := range plansToDelete {
fmt.Printf(" - %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint(p.Name))
}
fmt.Println()
confirmed, err := term.ConfirmYesNo("Are you sure you want to delete these plans?")
if err != nil {
term.OutputErrorAndExit("Error getting confirmation: %v", err)
}
if !confirmed {
fmt.Println("Operation cancelled")
return
}
// Delete the plans
term.StartSpinner("")
for _, p := range plansToDelete {
apiErr = api.Client.DeletePlan(p.Id)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error deleting plan %s: %s", p.Name, apiErr.Msg)
}
if lib.CurrentPlanId == p.Id {
err := lib.ClearCurrentPlan()
if err != nil {
term.OutputErrorAndExit("Error clearing current plan: %v", err)
}
}
}
term.StopSpinner()
if len(plansToDelete) == 1 {
fmt.Printf("✅ Deleted plan '%s'\n", plansToDelete[0].Name)
} else {
fmt.Printf("✅ Deleted %d plans\n", len(plansToDelete))
}
}
func delAll() {
term.StartSpinner("")
err := api.Client.DeleteAllPlans(lib.CurrentProjectId)
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error deleting all plans: %v", err)
}
fmt.Println("✅ Deleted all plans")
}
================================================
FILE: app/cli/cmd/diffs.go
================================================
package cmd
import (
"encoding/json"
"fmt"
"html/template"
"net"
"net/http"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"plandex-cli/ui"
"github.com/eiannone/keyboard"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var showDiffUi bool = true
var diffUiSideBySide = true
var diffUiLineByLine bool
var diffGit bool
var fromTellMenu bool
var diffsCmd = &cobra.Command{
Use: "diff",
Aliases: []string{"diffs"},
Short: "Review pending changes",
Run: diffs,
}
func init() {
RootCmd.AddCommand(diffsCmd)
diffsCmd.Flags().BoolVarP(&plainTextOutput, "plain", "p", false, "Output diffs in plain text with no ANSI codes")
diffsCmd.Flags().BoolVar(&showDiffUi, "ui", false, "Show diffs in a browser UI")
diffsCmd.Flags().BoolVar(&diffGit, "git", true, "Show diffs in git diff format")
diffsCmd.Flags().BoolVarP(&diffUiSideBySide, "side", "s", true, "Show diffs UI in side-by-side view")
diffsCmd.Flags().BoolVarP(&diffUiLineByLine, "line", "l", false, "Show diffs UI in line-by-line view")
diffsCmd.Flags().BoolVar(&fromTellMenu, "from-tell-menu", false, "Show diffs from the tell menu")
diffsCmd.Flags().MarkHidden("from-tell-menu")
}
func diffs(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MaybeResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
if showDiffUi {
diffGit = false
} else if diffGit || plainTextOutput {
showDiffUi = false
} else {
diffGit = true
}
diffs, err := api.Client.GetPlanDiffs(lib.CurrentPlanId, lib.CurrentBranch, plainTextOutput || showDiffUi)
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error getting plan diffs: %v", err)
return
}
if len(diffs) == 0 {
fmt.Println("🤷♂️ No pending changes")
return
}
if showDiffUi {
getNewListener := func() net.Listener {
outputFormat := "line-by-line"
if diffUiSideBySide {
outputFormat = "side-by-side"
} else if diffUiLineByLine {
outputFormat = "line-by-line"
}
// Properly escape the diff content for JavaScript
diffJSON, err := json.Marshal(diffs)
if err != nil {
term.OutputErrorAndExit("Error encoding diff content: %v", err)
}
// Create template data
data := struct {
DiffContent template.JS
OutputFormat string
}{
DiffContent: template.JS(diffJSON),
OutputFormat: outputFormat,
}
// Parse and execute the template
tmpl, err := template.New("diff").Parse(htmlTemplate)
if err != nil {
term.OutputErrorAndExit("Error parsing template: %v", err)
}
// Use :0 to let the OS pick an available port
listener, err := net.Listen("tcp", ":0")
if err != nil {
term.OutputErrorAndExit("Error starting server: %v", err)
}
// Get the actual port chosen
port := listener.Addr().(*net.TCPAddr).Port
// Start web server
go func() {
http.HandleFunc("/"+outputFormat, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
http.Serve(listener, nil)
}()
ui.OpenURL("Showing "+outputFormat+" diffs in your default browser...", fmt.Sprintf("http://localhost:%d/%s", port, outputFormat))
fmt.Println()
return listener
}
listener := getNewListener()
defer listener.Close()
var relaunch bool
for {
if relaunch {
listener.Close()
listener = getNewListener()
defer listener.Close()
relaunch = false
}
if diffUiLineByLine {
fmt.Printf("%s for side-by-side view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(s)"))
} else {
fmt.Printf("%s for line-by-line view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(l)"))
}
fmt.Printf("%s for git diff format\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(g)"))
// fmt.Printf("%s to quit\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(q)"))
s := "to exit menu/continue"
if fromTellMenu {
s = "to go back"
}
fmt.Printf("%s %s %s %s %s",
color.New(term.ColorHiMagenta, color.Bold).Sprint("Press a hotkey,"),
color.New(color.FgHiWhite, color.Bold).Sprintf("↓"),
color.New(term.ColorHiMagenta, color.Bold).Sprintf("to select, or"),
color.New(color.FgHiWhite, color.Bold).Sprintf("enter"),
color.New(term.ColorHiMagenta, color.Bold).Sprintf("%s>", s),
)
char, key, err := term.GetUserKeyInput()
if err != nil {
term.OutputErrorAndExit("Error getting key: %v", err)
return
}
fmt.Println()
if key == keyboard.KeyArrowDown {
options := []string{}
if diffUiLineByLine {
options = append(options, "side-by-side")
} else {
options = append(options, "line-by-line")
}
options = append(options, "git diff")
options = append(options, "exit menu")
selected, err := term.SelectFromList(
"Select an action",
options,
)
if err != nil {
term.OutputErrorAndExit("Error selecting action: %v", err)
return
}
if selected == "side-by-side" {
diffUiSideBySide = true
diffUiLineByLine = false
relaunch = true
} else if selected == "line-by-line" {
diffUiSideBySide = false
diffUiLineByLine = true
relaunch = true
} else if selected == "git diff" {
showGitDiff()
} else if selected == "exit menu" {
fmt.Println()
break
}
} else if string(char) == "g" {
showGitDiff()
} else if string(char) == "s" {
diffUiSideBySide = true
diffUiLineByLine = false
relaunch = true
} else if string(char) == "l" {
diffUiSideBySide = false
diffUiLineByLine = true
relaunch = true
} else if key == 13 || key == 10 || string(char) == "q" { // Check raw key codes for Enter/Return
if term.IsRepl {
fmt.Println()
break
} else {
fmt.Println()
break
}
} else if string(char) == "\x03" { // Ctrl+C
os.Exit(0)
} else {
fmt.Println()
term.OutputSimpleError("Invalid hotkey")
fmt.Println()
}
}
} else {
if plainTextOutput {
fmt.Println(diffs)
} else {
term.PageOutput(diffs)
}
fmt.Println()
}
}
func showGitDiff() {
_, err := lib.ExecPlandexCommandWithParams([]string{"diff", "--git"}, lib.ExecPlandexCommandParams{
DisableSuggestions: true,
})
if err != nil {
term.OutputErrorAndExit("Error showing git diff: %v", err)
}
}
var htmlTemplate = `
`
================================================
FILE: app/cli/cmd/invite.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/term"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var inviteCmd = &cobra.Command{
Use: "invite [email] [name] [org-role]",
Short: "Invite a new user to the org",
Run: invite,
Args: cobra.MaximumNArgs(3),
}
func init() {
RootCmd.AddCommand(inviteCmd)
}
func invite(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
email, name, orgRoleName := "", "", ""
if len(args) >= 1 {
email = args[0]
}
if len(args) >= 2 {
name = args[1]
}
if len(args) == 3 {
orgRoleName = args[2]
}
term.StartSpinner("")
orgRoles, err := api.Client.ListOrgRoles()
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Failed to list org roles: %v", err)
}
if email == "" {
var err error
email, err = term.GetRequiredUserStringInput("Email:")
if err != nil {
term.OutputErrorAndExit("Failed to get email: %v", err)
}
}
if name == "" {
var err error
name, err = term.GetRequiredUserStringInput("Name:")
if err != nil {
term.OutputErrorAndExit("Failed to get name: %v", err)
}
}
if orgRoleName == "" {
var orgRoleNames []string
for _, orgRole := range orgRoles {
orgRoleNames = append(orgRoleNames, orgRole.Label)
}
var err error
orgRoleName, err = term.SelectFromList("Org role:", orgRoleNames)
if err != nil {
term.OutputErrorAndExit("Failed to select org role: %v", err)
}
}
var orgRoleId string
for _, orgRole := range orgRoles {
if orgRole.Label == orgRoleName {
orgRoleId = orgRole.Id
break
}
}
if orgRoleId == "" {
term.OutputErrorAndExit("Org role '%s' not found", orgRoleName)
}
inviteRequest := shared.InviteRequest{
Email: email,
Name: name,
OrgRoleId: orgRoleId,
}
term.StartSpinner("")
apiErr := api.Client.InviteUser(inviteRequest)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Failed to invite user: %s", apiErr.Msg)
}
fmt.Println("✅ Invite sent")
}
================================================
FILE: app/cli/cmd/load.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"plandex-cli/types"
"github.com/sashabaranov/go-openai"
"github.com/spf13/cobra"
)
var (
recursive bool
namesOnly bool
note string
forceSkipIgnore bool
imageDetail string
defsOnly bool
)
var contextLoadCmd = &cobra.Command{
Use: "load [files-or-urls...]",
Aliases: []string{"l", "add"},
Short: "Load context from various inputs",
Long: `Load context from a file path, a directory, a URL, an image, a note, or piped data.`,
Run: contextLoad,
}
func init() {
contextLoadCmd.Flags().StringVarP(¬e, "note", "n", "", "Add a note to the context")
contextLoadCmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Search directories recursively")
contextLoadCmd.Flags().BoolVar(&namesOnly, "tree", false, "Load directory tree with file names only")
contextLoadCmd.Flags().BoolVarP(&forceSkipIgnore, "force", "f", false, "Load files even when ignored by .gitignore or .plandexignore")
contextLoadCmd.Flags().StringVarP(&imageDetail, "detail", "d", "high", "Image detail level (high or low)")
contextLoadCmd.Flags().BoolVar(&defsOnly, "map", false, "Load file maps (function/method/class signatures, variable names, types, etc.)")
RootCmd.AddCommand(contextLoadCmd)
}
func contextLoad(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
return
}
lib.MustLoadContext(args, &types.LoadContextParams{
Note: note,
Recursive: recursive,
NamesOnly: namesOnly,
ForceSkipIgnore: forceSkipIgnore,
ImageDetail: openai.ImageURLDetail(imageDetail),
DefsOnly: defsOnly,
SessionId: os.Getenv("PLANDEX_REPL_SESSION_ID"),
})
fmt.Println()
term.PrintCmds("", "ls", "tell", "debug")
}
================================================
FILE: app/cli/cmd/log.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"time"
"github.com/spf13/cobra"
)
// logCmd represents the log command
var logCmd = &cobra.Command{
Use: "log",
Aliases: []string{"history", "logs"},
Short: "Show plan history",
Long: `Show plan history`,
Args: cobra.NoArgs,
Run: runLog,
}
func init() {
// Add log command
RootCmd.AddCommand(logCmd)
}
func runLog(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
res, apiErr := api.Client.ListLogs(lib.CurrentPlanId, lib.CurrentBranch)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting logs: %v", apiErr)
}
withLocalTimestamps, err := convertTimestampsToLocal(res.Body)
if err != nil {
term.OutputErrorAndExit("Error converting timestamps: %v", err)
}
term.PageOutput(withLocalTimestamps)
fmt.Println()
term.PrintCmds("", "rewind", "continue", "convo", "convo 1", "convo 2-5")
}
func convertTimestampsToLocal(input string) (string, error) {
t := time.Now()
zone, _ := t.Zone()
re := lib.GitLogTimestampRegex
// Function to convert matched timestamps assuming they are in UTC to local time.
replaceFunc := func(match string) string {
t, err := time.Parse(lib.GitLogTimestampFormat, match)
if err != nil {
// In case of an error, return the original match.
return match
}
localDt := t.Local()
formattedTs := localDt.Format("Mon Jan 2, 2006 | 3:04:05pm")
if localDt.Day() == time.Now().Day() {
formattedTs = localDt.Format("Today | 3:04:05pm")
} else if localDt.Day() == time.Now().AddDate(0, 0, -1).Day() {
formattedTs = localDt.Format("Yesterday | 3:04:05pm")
}
// Convert to local time and format back to a string without the timezone to match the original format.
return formattedTs + " " + zone
}
// Find all matches and replace them.
result := re.ReplaceAllStringFunc(input, replaceFunc)
return result, nil
}
================================================
FILE: app/cli/cmd/ls.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/format"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var contextCmd = &cobra.Command{
Use: "ls",
Aliases: []string{"list-context"},
Short: "List everything in context",
Run: listContext,
}
func listContext(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
term.StartSpinner("")
contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
if err != nil {
term.OutputErrorAndExit("Error listing context: %v", err)
}
planConfig, err := api.Client.GetPlanConfig(lib.CurrentPlanId)
if err != nil {
term.OutputErrorAndExit("Error getting plan config: %v", err)
}
term.StopSpinner()
totalTokens := 0
totalPlannerTokens := 0
totalMapTokens := 0
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"#", "Name", "Type", "🪙", "Added", "Updated"})
table.SetAutoWrapText(false)
if len(contexts) == 0 {
fmt.Println("🤷♂️ No context")
fmt.Println()
term.PrintCmds("", "load")
return
}
for i, context := range contexts {
totalTokens += context.NumTokens
if context.ContextType == shared.ContextMapType {
totalMapTokens += context.NumTokens
} else {
totalPlannerTokens += context.NumTokens
}
t, icon := context.TypeAndIcon()
name := context.Name
if name == "" {
name = context.FilePath
}
if len(name) > 40 {
name = name[:20] + "⋯" + name[len(name)-20:]
}
row := []string{
strconv.Itoa(i + 1),
" " + icon + " " + name,
t,
strconv.Itoa(context.NumTokens), //+ " 🪙",
format.Time(context.CreatedAt),
format.Time(context.UpdatedAt),
}
table.Rich(row, []tablewriter.Colors{
{tablewriter.Bold},
{tablewriter.FgHiGreenColor, tablewriter.Bold},
})
}
table.Render()
tokensTbl := tablewriter.NewWriter(os.Stdout)
tokensTbl.SetAutoWrapText(false)
if planConfig.AutoLoadContext {
tokensTbl.Append([]string{
color.New(term.ColorHiCyan, color.Bold).Sprintf("Map tokens →") + color.New(color.Bold).Sprintf(" %d 🪙", totalMapTokens),
color.New(term.ColorHiCyan, color.Bold).Sprintf("Context tokens →") + color.New(color.Bold).Sprintf(" %d 🪙", totalPlannerTokens),
})
} else {
tokensTbl.Append([]string{color.New(term.ColorHiCyan, color.Bold).Sprintf("Total tokens →") + color.New(color.Bold).Sprintf(" %d 🪙", totalTokens)})
}
tokensTbl.Render()
fmt.Println()
term.PrintCmds("", "load", "rm", "clear")
}
func init() {
RootCmd.AddCommand(contextCmd)
}
================================================
FILE: app/cli/cmd/model_helpers.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
)
func warnModelsFileLocalChanges(path, cmd string) (bool, error) {
cmdPrefix := "\\"
if !term.IsRepl {
cmdPrefix = "plandex "
}
color.New(color.Bold, term.ColorHiYellow).Println("⚠️ The models file has local changes")
fmt.Println()
fmt.Println("Path → " + path)
fmt.Println()
fmt.Println("If you continue, local changes will be dropped in favor of the latest server state")
fmt.Println()
fmt.Printf("To keep the local version instead, quit and run %s\n", color.New(color.Bold, color.BgCyan, color.FgHiWhite).
Sprintf(" %s%s --save ", cmdPrefix, cmd))
fmt.Println()
return term.ConfirmYesNo("Drop local changes and continue?")
}
type maybePromptAndOpenModelsFileResult struct {
shouldReturn bool
jsonData []byte
}
func maybePromptAndOpenModelsFile(filePath, pathArg, cmd string, defaultConfig *shared.PlanConfig, planConfig *shared.PlanConfig) maybePromptAndOpenModelsFileResult {
printManual := func() {
cmdPrefix := "\\"
if !term.IsRepl {
cmdPrefix = "plandex "
}
fmt.Println("To save changes, run " +
fmt.Sprintf(" %s ", color.New(color.Bold, color.BgCyan, color.FgHiWhite).
Sprintf(" %s%s --save%s ", cmdPrefix, cmd, pathArg)))
}
selectedEditor := lib.MaybePromptAndOpen(filePath, defaultConfig, planConfig)
if selectedEditor {
fmt.Println("📝 Opened in editor")
fmt.Println()
confirmed, err := term.ConfirmYesNo("Ready to save?")
if err != nil {
term.OutputErrorAndExit("Error confirming: %v", err)
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
}
if !confirmed {
fmt.Println("🙅♂️ Update canceled")
fmt.Println()
printManual()
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
}
// get updated file state
jsonData, err := os.ReadFile(filePath)
if err != nil {
term.OutputErrorAndExit("Error reading JSON file: %v", err)
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
}
return maybePromptAndOpenModelsFileResult{shouldReturn: false, jsonData: jsonData}
} else {
// No editor available or user chose manual
fmt.Println("👨💻 Edit the file in your JSON editor of choice")
fmt.Println()
printManual()
return maybePromptAndOpenModelsFileResult{shouldReturn: true}
}
}
================================================
FILE: app/cli/cmd/model_packs.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var customModelPacksOnly bool
var modelPacksCmd = &cobra.Command{
Use: "model-packs",
Short: "List all model packs",
Run: listModelPacks,
}
var createModelPackCmd = &cobra.Command{
Use: "create",
Short: "Create a model pack",
Run: customModelsNotImplemented,
}
var deleteModelPackCmd = &cobra.Command{
Use: "delete",
Aliases: []string{"rm"},
Short: "Delete a model pack by name or index",
Run: customModelsNotImplemented,
}
var updateModelPackCmd = &cobra.Command{
Use: "update",
Short: "Update a model pack by name",
Run: customModelsNotImplemented,
}
var showModelPackCmd = &cobra.Command{
Use: "show [name]",
Short: "Show a model pack by name",
Args: cobra.MaximumNArgs(1),
Run: showModelPack,
}
func init() {
RootCmd.AddCommand(modelPacksCmd)
modelPacksCmd.AddCommand(createModelPackCmd)
modelPacksCmd.AddCommand(deleteModelPackCmd)
modelPacksCmd.AddCommand(updateModelPackCmd)
modelPacksCmd.AddCommand(showModelPackCmd)
modelPacksCmd.Flags().BoolVarP(&customModelPacksOnly, "custom", "c", false, "Only show custom model packs")
modelPacksCmd.Flags().BoolVarP(&allProperties, "all", "a", false, "Show all properties")
}
func listModelPacks(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
term.StartSpinner("")
builtInModelPacks := shared.BuiltInModelPacks
if auth.Current.IsCloud {
filtered := []*shared.ModelPack{}
for _, mp := range builtInModelPacks {
if mp.LocalProvider == "" {
filtered = append(filtered, mp)
}
}
builtInModelPacks = filtered
}
customModelPacks, err := api.Client.ListModelPacks()
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error fetching model packs: %v", err)
return
}
if !customModelPacksOnly {
color.New(color.Bold, term.ColorHiCyan).Println("🏠 Built-in Model Packs")
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(true)
table.SetRowLine(true)
table.SetHeader([]string{"Name", "Description"})
for _, set := range builtInModelPacks {
table.Append([]string{set.Name, set.Description})
}
table.Render()
fmt.Println()
}
if len(customModelPacks) > 0 {
color.New(color.Bold, term.ColorHiCyan).Println("🛠️ Custom Model Packs")
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(true)
table.SetRowLine(true)
table.SetHeader([]string{"#", "Name", "Description"})
for i, set := range customModelPacks {
table.Append([]string{fmt.Sprintf("%d", i+1), set.Name, set.Description})
}
table.Render()
fmt.Println()
} else if customModelPacksOnly {
fmt.Println("🤷♂️ No custom model packs")
fmt.Println()
}
if customModelPacksOnly && len(customModelPacks) > 0 {
term.PrintCmds("", "model-packs show", "models custom")
} else if len(customModelPacks) > 0 {
term.PrintCmds("", "model-packs --custom", "model-packs show", "models custom")
} else {
term.PrintCmds("", "model-packs show", "models custom")
}
}
func showModelPack(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
term.StartSpinner("")
customModelPacks, apiErr := api.Client.ListModelPacks()
if apiErr != nil {
term.OutputErrorAndExit("Error fetching models: %v", apiErr)
}
customModels, err := api.Client.ListCustomModels()
if err != nil {
term.OutputErrorAndExit("Error fetching custom models: %v", err)
}
customModelsById := make(map[shared.ModelId]*shared.CustomModel)
for _, m := range customModels {
customModelsById[m.ModelId] = m
}
term.StopSpinner()
modelPacks := []*shared.ModelPack{}
modelPacks = append(modelPacks, customModelPacks...)
builtInModelPacks := shared.BuiltInModelPacks
if auth.Current.IsCloud {
filtered := []*shared.ModelPack{}
for _, mp := range builtInModelPacks {
if mp.LocalProvider == "" {
filtered = append(filtered, mp)
}
}
builtInModelPacks = filtered
}
modelPacks = append(modelPacks, builtInModelPacks...)
var name string
if len(args) > 0 {
name = args[0]
}
var modelPack *shared.ModelPack
if name == "" {
opts := make([]string, len(modelPacks))
for i, mp := range modelPacks {
opts[i] = mp.Name
}
selected, err := term.SelectFromList("Select a model pack:", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting model pack: %v", err)
}
for _, mp := range modelPacks {
if mp.Name == selected {
modelPack = mp
break
}
}
} else {
for _, mp := range modelPacks {
if mp.Name == name {
modelPack = mp
break
}
}
}
if modelPack == nil {
term.OutputErrorAndExit("Model pack not found")
return
}
renderModelPack(modelPack, customModelsById, allProperties)
fmt.Println()
term.PrintCmds("", "set-model", "set-model default", "models custom")
}
// func getModelRoleConfig(customModels []*shared.CustomModel, modelRole shared.ModelRole) shared.ModelRoleConfig {
// _, modelConfig := getModelWithRoleConfig(customModels, modelRole)
// return modelConfig
// }
// func getModelWithRoleConfig(customModels []*shared.CustomModel, modelRole shared.ModelRole) (*shared.CustomModel, shared.ModelRoleConfig) {
// role := string(modelRole)
// modelId := getModelIdForRole(customModels, modelRole)
// temperatureStr, err := term.GetUserStringInputWithDefault("Temperature for "+role+":", fmt.Sprintf("%.1f", shared.DefaultConfigByRole[modelRole].Temperature))
// if err != nil {
// term.OutputErrorAndExit("Error reading temperature: %v", err)
// }
// temperature, err := strconv.ParseFloat(temperatureStr, 32)
// if err != nil {
// term.OutputErrorAndExit("Invalid number for temperature: %v", err)
// }
// topPStr, err := term.GetUserStringInputWithDefault("Top P for "+role+":", fmt.Sprintf("%.1f", shared.DefaultConfigByRole[modelRole].TopP))
// if err != nil {
// term.OutputErrorAndExit("Error reading top P: %v", err)
// }
// topP, err := strconv.ParseFloat(topPStr, 32)
// if err != nil {
// term.OutputErrorAndExit("Invalid number for top P: %v", err)
// }
// var reservedOutputTokens int
// if modelRole == shared.ModelRoleBuilder || modelRole == shared.ModelRolePlanner || modelRole == shared.ModelRoleWholeFileBuilder {
// reservedOutputTokensStr, err := term.GetUserStringInputWithDefault("Reserved output tokens for "+role+":", fmt.Sprintf("%d", model.ReservedOutputTokens))
// if err != nil {
// term.OutputErrorAndExit("Error reading reserved output tokens: %v", err)
// }
// reservedOutputTokens, err = strconv.Atoi(reservedOutputTokensStr)
// if err != nil {
// term.OutputErrorAndExit("Invalid number for reserved output tokens: %v", err)
// }
// }
// return model, shared.ModelRoleConfig{
// ModelId: model.ModelId,
// Role: modelRole,
// Temperature: float32(temperature),
// TopP: float32(topP),
// ReservedOutputTokens: reservedOutputTokens,
// }
// }
// func getPlannerRoleConfig(customModels []*shared.CustomModel) shared.PlannerRoleConfig {
// model, modelConfig := getModelWithRoleConfig(customModels, shared.ModelRolePlanner)
// return shared.PlannerRoleConfig{
// ModelRoleConfig: modelConfig,
// PlannerModelConfig: shared.PlannerModelConfig{
// MaxConvoTokens: model.DefaultMaxConvoTokens,
// },
// }
// }
// func getModelIdForRole(customModels []*shared.CustomModel, role shared.ModelRole) shared.ModelId {
// color.New(color.Bold).Printf("Select a model for the %s role 👇\n", role)
// return lib.SelectModelIdForRole(customModels, role)
// }
================================================
FILE: app/cli/cmd/model_providers.go
================================================
package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var customProvidersOnly bool
var providersCmd = &cobra.Command{
Use: "providers",
Short: "List built-in and custom model providers",
Run: listProviders,
}
var addProviderCmd = &cobra.Command{
Use: "add",
Aliases: []string{"create"},
Short: "Add a custom model provider",
Run: customModelsNotImplemented,
}
var updateProviderCmd = &cobra.Command{
Use: "update",
Aliases: []string{"edit"},
Short: "Update a custom model provider",
Run: customModelsNotImplemented,
}
func init() {
RootCmd.AddCommand(providersCmd)
providersCmd.Flags().BoolVarP(&customProvidersOnly, "custom", "c", false, "List custom providers only")
providersCmd.AddCommand(addProviderCmd)
providersCmd.AddCommand(updateProviderCmd)
}
func listProviders(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
var customProviders []*shared.CustomProvider
var apiErr *shared.ApiError
if customProvidersOnly && auth.Current.IsCloud {
term.OutputErrorAndExit("Custom providers are not supported on Plandex Cloud")
return
}
if !auth.Current.IsCloud {
term.StartSpinner("")
customProviders, apiErr = api.Client.ListCustomProviders()
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error fetching providers: %v", apiErr.Msg)
return
}
}
if customProvidersOnly && len(customProviders) == 0 {
fmt.Println("🤷♂️ No custom providers")
fmt.Println()
term.PrintCmds("", "models custom")
return
}
color.New(color.Bold, term.ColorHiCyan).Println("🏠 Built-in Providers")
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(true)
var header []string
if auth.Current.IsCloud {
header = []string{"ID", "API Key", "Other Vars"}
} else {
header = []string{"ID", "Base URL", "API Key", "Other Vars"}
}
table.SetHeader(header)
for _, p := range shared.AllModelProviders {
if p == shared.ModelProviderCustom {
continue
}
config := shared.BuiltInModelProviderConfigs[p]
if config.LocalOnly && auth.Current.IsCloud {
continue
}
var apiKey string
if config.ApiKeyEnvVar != "" {
apiKey = config.ApiKeyEnvVar
} else if config.SkipAuth {
apiKey = "No Auth"
} else if config.HasClaudeMaxAuth {
apiKey = "Claude Max Oauth"
}
extraVars := []string{}
if config.Provider == shared.ModelProviderAmazonBedrock {
extraVars = append(extraVars, "PLANDEX_AWS_PROFILE")
}
for _, v := range config.ExtraAuthVars {
extraVars = append(extraVars, v.Var)
}
if auth.Current.IsCloud {
table.Append([]string{
string(p),
apiKey,
strings.Join(extraVars, "\n"),
})
} else {
table.Append([]string{
string(p),
config.BaseUrl,
apiKey,
strings.Join(extraVars, "\n"),
})
}
}
table.Render()
fmt.Println()
if len(customProviders) > 0 {
color.New(color.Bold, term.ColorHiCyan).Println("🛠️ Custom Providers")
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{"#", "Name", "Base URL", "API Key", "Other Vars"})
for i, p := range customProviders {
extraVars := []string{}
for _, v := range p.ExtraAuthVars {
extraVars = append(extraVars, v.Var)
}
apiKey := p.ApiKeyEnvVar
if apiKey == "" && p.SkipAuth {
apiKey = "No Auth"
}
table.Append([]string{
strconv.Itoa(i + 1),
p.Name,
p.BaseUrl,
apiKey,
strings.Join(extraVars, "\n"),
})
}
table.Render()
fmt.Println()
}
fmt.Println(color.New(color.Bold, term.ColorHiCyan).Sprint("\n📖 Per-provider instructions"))
fmt.Println("Go to → " + color.New(color.Bold).Sprint("https://docs.plandex.ai/models/model-providers"))
fmt.Println()
term.PrintCmds("", "models custom")
}
================================================
FILE: app/cli/cmd/models.go
================================================
package cmd
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var customModelsOnly bool
var allProperties bool
var saveCustomModels bool
var customModelsPath string
func init() {
RootCmd.AddCommand(modelsCmd)
modelsCmd.Flags().BoolVarP(&allProperties, "all", "a", false, "Show all properties")
modelsCmd.AddCommand(listAvailableModelsCmd)
modelsCmd.AddCommand(addCustomModelCmd)
modelsCmd.AddCommand(updateCustomModelCmd)
modelsCmd.AddCommand(manageCustomModelsCmd)
modelsCmd.AddCommand(deleteCustomModelCmd)
modelsCmd.AddCommand(defaultModelsCmd)
manageCustomModelsCmd.Flags().BoolVar(&saveCustomModels, "save", false, "Save custom models")
manageCustomModelsCmd.Flags().StringVarP(&customModelsPath, "file", "f", "", "Path to custom models file")
listAvailableModelsCmd.Flags().BoolVarP(&customModelsOnly, "custom", "c", false, "List custom models only")
}
var modelsCmd = &cobra.Command{
Use: "models",
Short: "Show plan model settings",
Run: models,
}
var defaultModelsCmd = &cobra.Command{
Use: "default",
Short: "Show default model settings for new plans",
Run: defaultModels,
}
var listAvailableModelsCmd = &cobra.Command{
Use: "available",
Aliases: []string{"avail"},
Short: "List all available models",
Run: listAvailableModels,
}
var manageCustomModelsCmd = &cobra.Command{
Use: "custom",
Short: "Manage custom models, providers, and model packs",
Run: manageCustomModels,
}
var addCustomModelCmd = &cobra.Command{
Use: "add",
Aliases: []string{"create"},
Short: "Add a custom model",
Run: customModelsNotImplemented,
}
var updateCustomModelCmd = &cobra.Command{
Use: "update",
Aliases: []string{"edit"},
Short: "Update a custom model",
Run: customModelsNotImplemented,
}
var deleteCustomModelCmd = &cobra.Command{
Use: "rm",
Aliases: []string{"remove", "delete"},
Short: "Remove a custom model",
Args: cobra.MaximumNArgs(1),
Run: customModelsNotImplemented,
}
func manageCustomModels(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
term.StartSpinner("")
var serverModelsInput *shared.ModelsInput
var defaultConfig *shared.PlanConfig
errCh := make(chan error, 2)
go func() {
var err error
serverModelsInput, err = lib.GetServerModelsInput()
if err != nil {
errCh <- fmt.Errorf("error getting server models input: %v", err)
return
}
errCh <- nil
}()
go func() {
var apiErr *shared.ApiError
defaultConfig, apiErr = api.Client.GetDefaultPlanConfig()
if apiErr != nil {
errCh <- fmt.Errorf("error getting default config: %v", apiErr.Msg)
return
}
errCh <- nil
}()
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
term.OutputErrorAndExit(err.Error())
return
}
}
usingDefaultPath := false
if customModelsPath == "" {
usingDefaultPath = true
customModelsPath = lib.GetCustomModelsPath(auth.Current.UserId)
}
exists, err := fs.FileExists(customModelsPath)
if err != nil {
term.OutputErrorAndExit("Error checking custom models file: %v", err)
return
}
if saveCustomModels {
if !exists {
term.OutputErrorAndExit("File not found: %s", customModelsPath)
return
}
} else {
if serverModelsInput.IsEmpty() {
jsonData, err := json.MarshalIndent(getExampleTemplate(auth.Current.IsCloud, auth.Current.IntegratedModelsMode), "", " ")
if err != nil {
term.OutputErrorAndExit("Error marshalling template: %v", err)
return
}
err = os.MkdirAll(filepath.Dir(customModelsPath), 0755)
if err != nil {
term.OutputErrorAndExit("Error creating directory: %v", err)
return
}
err = os.WriteFile(customModelsPath, jsonData, 0644)
if err != nil {
term.OutputErrorAndExit("Error writing template file: %v", err)
return
}
term.StopSpinner()
fmt.Printf("🧠 Example models file → %s\n", customModelsPath)
fmt.Println("👨💻 Edit it, then come back here to save")
fmt.Println()
} else {
serverClientModelsInput := serverModelsInput.ToClientModelsInput()
serverClientModelsInput.PrepareUpdate()
var localModelsInput shared.ModelsInput
if exists {
// we only do a conflict check on the default path in the home dir
// if user specifies the path and the file exists, just open the file without checking for conflicts
if usingDefaultPath {
res, err := lib.CustomModelsCheckLocalChanges(customModelsPath)
if err != nil {
term.OutputErrorAndExit("Error checking local changes: %v", err)
return
}
localModelsInput = res.LocalModelsInput
if res.HasLocalChanges {
term.StopSpinner()
res, err := warnModelsFileLocalChanges(customModelsPath, "models custom")
if err != nil {
term.OutputErrorAndExit("Error confirming: %v", err)
return
}
if !res {
return
}
fmt.Println()
term.StartSpinner("")
}
}
}
if !serverModelsInput.Equals(localModelsInput) {
err := lib.WriteCustomModelsFile(customModelsPath, serverModelsInput)
if err != nil {
term.OutputErrorAndExit("Error saving custom models file: %v", err)
return
}
}
term.StopSpinner()
fmt.Printf("🧠 %s → %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Models file"), customModelsPath)
fmt.Println("👨💻 Edit it, then come back here to save")
fmt.Println()
}
pathArg := ""
if !usingDefaultPath {
pathArg = fmt.Sprintf(" --file %s", customModelsPath)
}
res := maybePromptAndOpenModelsFile(customModelsPath, pathArg, "models custom", defaultConfig, nil)
if res.shouldReturn {
return
}
}
didUpdate := lib.MustSyncCustomModels(customModelsPath, serverModelsInput)
if !didUpdate {
fmt.Println("🤷♂️ No changes to custom models/providers/model packs")
}
}
func models(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
plan, err := api.Client.GetPlan(lib.CurrentPlanId)
if err != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error getting plan: %v", err)
return
}
settings, err := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error getting settings: %v", err)
return
}
title := fmt.Sprintf("%s Model Settings", color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name))
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.Append([]string{title})
table.Render()
fmt.Println()
renderSettings(settings, allProperties)
term.PrintCmds("", "set-model", "models available", "models default", "models custom")
}
func defaultModels(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
term.StartSpinner("")
settings, err := api.Client.GetOrgDefaultSettings()
if err != nil {
term.OutputErrorAndExit("Error getting default model settings: %v", err)
return
}
term.StopSpinner()
title := fmt.Sprintf("%s Model Settings", color.New(color.Bold, term.ColorHiGreen).Sprint("Org-Wide Default"))
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.Append([]string{title})
table.Render()
fmt.Println()
renderSettings(settings, allProperties)
term.PrintCmds("", "set-model default", "models available", "models")
}
func listAvailableModels(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
if customModelsOnly && auth.Current.IntegratedModelsMode {
term.OutputErrorAndExit("Custom models are not supported in Integrated Models mode on Plandex Cloud")
return
}
term.StartSpinner("")
customModels, err := api.Client.ListCustomModels()
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error fetching custom models: %v", err)
return
}
if !customModelsOnly {
color.New(color.Bold, term.ColorHiCyan).Println("🏠 Built-in Models")
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{"Model", "Input", "Output", "Reserved"})
for _, model := range shared.BuiltInBaseModels {
if auth.Current.IsCloud && model.IsLocalOnly() {
continue
}
table.Append([]string{string(model.ModelId), fmt.Sprintf("%d 🪙", model.MaxTokens), fmt.Sprintf("%d 🪙", model.MaxOutputTokens), fmt.Sprintf("%d 🪙", model.ReservedOutputTokens)})
}
table.Render()
fmt.Println()
}
if len(customModels) > 0 {
color.New(color.Bold, term.ColorHiCyan).Println("🛠️ Custom Models")
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{"#", "ID", "🪙"})
for i, model := range customModels {
table.Append([]string{fmt.Sprintf("%d", i+1), string(model.ModelId), strconv.Itoa(model.MaxTokens)})
}
table.Render()
} else if customModelsOnly {
fmt.Println("🤷♂️ No custom models")
}
fmt.Println()
if customModelsOnly {
if len(customModels) > 0 {
term.PrintCmds("", "models", "set-model", "models custom")
} else {
term.PrintCmds("", "models custom")
}
} else {
term.PrintCmds("", "models available --custom", "models", "set-model", "models custom")
}
}
func renderSettings(settings *shared.PlanSettings, allProperties bool) {
modelPack := settings.GetModelPack()
color.New(color.Bold, term.ColorHiCyan).Println("🎛️ Current Model Pack")
renderModelPack(modelPack, settings.CustomModelsById, allProperties)
if allProperties {
color.New(color.Bold, term.ColorHiCyan).Println("🧠 Planner Defaults")
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{"Max Tokens", "Max Convo Tokens"})
table.Append([]string{
fmt.Sprintf("%d", modelPack.Planner.GetFinalLargeContextFallback().GetSharedBaseConfig(settings).MaxTokens),
fmt.Sprintf("%d", modelPack.Planner.GetMaxConvoTokens(settings)),
})
table.Render()
fmt.Println()
}
}
func renderModelPack(modelPack *shared.ModelPack, customModelsById map[shared.ModelId]*shared.CustomModel, allProperties bool) {
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoFormatHeaders(false)
table.SetAutoWrapText(true)
table.SetColWidth(64)
table.SetHeader([]string{modelPack.Name})
table.Append([]string{modelPack.Description})
table.Render()
fmt.Println()
color.New(color.Bold, term.ColorHiCyan).Println("🤖 Models")
table = tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
cols := []string{
"Role",
"Model",
}
align := []int{
tablewriter.ALIGN_LEFT, // Role
tablewriter.ALIGN_LEFT, // Model
}
if allProperties {
cols = append(cols, []string{
"Temperature",
"Top P",
"Max Input",
}...)
align = append(align, []int{
tablewriter.ALIGN_RIGHT, // Temperature
tablewriter.ALIGN_RIGHT, // Top P
tablewriter.ALIGN_RIGHT, // Max Input
}...)
}
table.SetHeader(cols)
table.SetColumnAlignment(align)
anyRoleParamsDisabled := false
var addModelRow func(role string, config shared.ModelRoleConfig, indent int)
addModelRow = func(role string, config shared.ModelRoleConfig, indent int) {
if indent > 0 {
role = "└─ " + role
for i := 0; i < indent-1; i++ {
role = " " + role
}
}
var temp float32
var topP float32
var disabled bool
sharedBaseConfig := config.GetSharedBaseConfigWithCustomModels(customModelsById)
if sharedBaseConfig.RoleParamsDisabled {
temp = 1
topP = 1
disabled = true
anyRoleParamsDisabled = true
} else {
temp = config.Temperature
topP = config.TopP
}
tempStr := fmt.Sprintf("%.1f", temp)
if disabled {
tempStr = "*" + tempStr
}
topPStr := fmt.Sprintf("%.1f", topP)
if disabled {
topPStr = "*" + topPStr
}
row := []string{
role,
string(config.GetModelId()),
}
if allProperties {
row = append(row, []string{
tempStr,
topPStr,
fmt.Sprintf("%d 🪙", sharedBaseConfig.MaxTokens-config.GetReservedOutputTokens(customModelsById)),
}...)
}
table.Append(row)
// Add large context and large output fallback(s) if present
if config.LargeContextFallback != nil {
addModelRow("large-context", *config.LargeContextFallback, indent+1)
}
if config.LargeOutputFallback != nil {
addModelRow("large-output", *config.LargeOutputFallback, indent+1)
}
if config.StrongModel != nil {
addModelRow("strong", *config.StrongModel, indent+1)
}
if config.ErrorFallback != nil {
addModelRow("error", *config.ErrorFallback, indent+1)
}
}
addModelRow(string(shared.ModelRolePlanner), modelPack.Planner.ModelRoleConfig, 0)
addModelRow(string(shared.ModelRoleArchitect), modelPack.GetArchitect(), 0)
addModelRow(string(shared.ModelRoleCoder), modelPack.GetCoder(), 0)
addModelRow(string(shared.ModelRolePlanSummary), modelPack.PlanSummary, 0)
addModelRow(string(shared.ModelRoleBuilder), modelPack.Builder, 0)
addModelRow(string(shared.ModelRoleWholeFileBuilder), modelPack.GetWholeFileBuilder(), 0)
addModelRow(string(shared.ModelRoleName), modelPack.Namer, 0)
addModelRow(string(shared.ModelRoleCommitMsg), modelPack.CommitMsg, 0)
addModelRow(string(shared.ModelRoleExecStatus), modelPack.ExecStatus, 0)
table.Render()
if anyRoleParamsDisabled && allProperties {
fmt.Println("* these models do not support changing temperature or top p")
}
fmt.Println()
}
func customModelsNotImplemented(cmd *cobra.Command, args []string) {
color.New(color.Bold, color.FgHiRed).Println("⛔️ Not implemented")
fmt.Println()
fmt.Println("Use " + color.New(color.BgCyan, color.FgHiWhite).Sprint(" plandex models custom ") + " to manage custom models, providers, and model packs")
os.Exit(1)
}
func getExampleTemplate(isCloud, isCloudIntegratedModels bool) shared.ClientModelsInput {
exampleProviderName := "togetherai"
var customProviders []*shared.CustomProvider
usesProviders := []shared.BaseModelUsesProvider{}
if !isCloud {
customProviders = append(customProviders, &shared.CustomProvider{
Name: exampleProviderName,
BaseUrl: "https://api.together.xyz/v1",
ApiKeyEnvVar: "TOGETHER_API_KEY",
})
usesProviders = append(usesProviders, shared.BaseModelUsesProvider{
Provider: shared.ModelProviderCustom,
CustomProvider: &exampleProviderName,
ModelName: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
})
}
usesProviders = append(usesProviders, shared.BaseModelUsesProvider{
Provider: shared.ModelProviderOpenRouter,
ModelName: "meta-llama/llama-4-maverick",
})
var customModels []*shared.CustomModel
if !isCloudIntegratedModels {
customModels = []*shared.CustomModel{
{
ModelId: shared.ModelId("meta-llama/llama-4-maverick"),
Publisher: shared.ModelPublisher("meta-llama"),
Description: "Meta Llama 4 Maverick",
BaseModelShared: shared.BaseModelShared{
DefaultMaxConvoTokens: 75000,
MaxTokens: 1048576,
MaxOutputTokens: 16000,
ReservedOutputTokens: 16000,
ModelCompatibility: shared.FullCompatibility,
PreferredOutputFormat: shared.ModelOutputFormatXml,
},
Providers: usesProviders,
},
}
}
lightModelId := "meta-llama/llama-4-maverick"
if len(customModels) == 0 {
lightModelId = "mistral/devstral-small"
}
return shared.ClientModelsInput{
SchemaUrl: shared.SchemaUrlInputConfig,
CustomProviders: customProviders,
CustomModels: customModels,
CustomModelPacks: []*shared.ClientModelPackSchema{
{
Name: "example-model-pack",
Description: "Example model pack",
ClientModelPackSchemaRoles: shared.ClientModelPackSchemaRoles{
Planner: "deepseek/r1",
Architect: "deepseek/r1",
Coder: &shared.ModelRoleConfigSchema{
ModelId: "deepseek/v3",
LargeContextFallback: &shared.ModelRoleConfigSchema{
ModelId: "google/gemini-2.5-pro",
},
ErrorFallback: &shared.ModelRoleConfigSchema{
ModelId: "deepseek/r1-hidden",
},
},
PlanSummary: lightModelId,
Builder: shared.ModelRoleConfigSchema{
ModelId: "deepseek/r1-hidden",
StrongModel: &shared.ModelRoleConfigSchema{
ModelId: "openai/o3-medium",
},
},
WholeFileBuilder: shared.ModelRoleConfigSchema{
ModelId: "deepseek/r1-hidden",
LargeContextFallback: &shared.ModelRoleConfigSchema{
ModelId: "google/gemini-2.5-pro",
LargeOutputFallback: &shared.ModelRoleConfigSchema{
ModelId: "openai/o3-low",
},
},
LargeOutputFallback: &shared.ModelRoleConfigSchema{
ModelId: "openai/o3-low",
},
},
ExecStatus: "deepseek/r1-hidden",
Namer: lightModelId,
CommitMsg: lightModelId,
},
},
},
}
}
================================================
FILE: app/cli/cmd/new.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"plandex-cli/types"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var name string
var contextBaseDir string
// newCmd represents the new command
var newCmd = &cobra.Command{
Use: "new",
Aliases: []string{"n"},
Short: "Start a new plan",
// Long: ``,
Args: cobra.ExactArgs(0),
Run: new,
}
func init() {
RootCmd.AddCommand(newCmd)
newCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the new plan")
newCmd.Flags().StringVar(&contextBaseDir, "context-dir", ".", "Base directory to auto-load context from")
AddNewPlanFlags(newCmd)
}
func new(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveOrCreateProject()
term.StartSpinner("")
errCh := make(chan error, 2)
var planId string
var config *shared.PlanConfig
go func() {
res, apiErr := api.Client.CreatePlan(lib.CurrentProjectId, shared.CreatePlanRequest{Name: name})
if apiErr != nil {
errCh <- fmt.Errorf("error creating plan: %v", apiErr.Msg)
return
}
planId = res.Id
errCh <- nil
}()
go func() {
var apiErr *shared.ApiError
config, apiErr = api.Client.GetDefaultPlanConfig()
if apiErr != nil {
errCh <- fmt.Errorf("error getting plan config: %v", apiErr.Msg)
return
}
errCh <- nil
}()
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
term.OutputErrorAndExit("Error: %v", err)
}
}
err := lib.WriteCurrentPlan(planId)
if err != nil {
term.OutputErrorAndExit("Error setting current plan: %v", err)
}
err = lib.WriteCurrentBranch("main")
if err != nil {
term.OutputErrorAndExit("Error setting current branch: %v", err)
}
if name == "" {
name = "draft"
}
term.StopSpinner()
fmt.Printf("✅ Started new plan %s and set it to current plan\n", color.New(color.Bold, term.ColorHiGreen).Sprint(name))
fmt.Printf("⚙️ Using default config\n")
resolveAutoMode(config)
resolveModelPack()
// autoModeLabel := shared.ConfigSettingsByKey["automode"].KeyToLabel(string(config.AutoMode))
// fmt.Println("⚡️ Auto-mode:", autoModeLabel)
if config.AutoLoadContext {
fmt.Println("📥 Automatic context loading is enabled")
baseDir := contextBaseDir
if baseDir == "" {
baseDir = "."
}
lib.MustLoadContext([]string{baseDir}, &types.LoadContextParams{
DefsOnly: true,
SkipIgnoreWarning: true,
AutoLoaded: true,
})
} else {
fmt.Println()
}
var cmds []string
if term.IsRepl {
cmds = []string{"config", "plans", "cd", "models"}
} else {
cmds = []string{"tell", "chat", "config"}
}
if !config.AutoLoadContext {
cmds = append([]string{"load"}, cmds...)
}
fmt.Println()
term.PrintCmds("", cmds...)
}
================================================
FILE: app/cli/cmd/plan_exec_helpers.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
shared "plandex-shared"
"github.com/spf13/cobra"
)
const (
EditorTypeVim string = "vim"
EditorTypeNano string = "nano"
)
var defaultEditor = EditorTypeVim
const defaultAutoDebugTries = 5
var autoConfirm bool
var tellPromptFile string
var tellBg bool
var tellStop bool
var tellNoBuild bool
var tellAutoApply bool
var tellAutoContext bool
var tellSmartContext bool
var tellSkipMenu bool
var noExec bool
var autoDebug int
var editor = EditorTypeVim // default to vim
var editorSetByFlag bool
func init() {
envEditor := os.Getenv("EDITOR")
if envEditor == "" {
envEditor = os.Getenv("VISUAL")
}
if envEditor != "" {
defaultEditor = envEditor
}
}
type initExecFlagsParams struct {
omitFile bool
omitNoBuild bool
omitEditor bool
omitStop bool
omitBg bool
omitApply bool
omitExec bool
omitAutoContext bool
omitSmartContext bool
omitSkipMenu bool
}
func initExecFlags(cmd *cobra.Command, params initExecFlagsParams) {
if !params.omitFile {
cmd.Flags().StringVarP(&tellPromptFile, "file", "f", "", "File containing prompt")
}
if !params.omitBg {
cmd.Flags().BoolVar(&tellBg, "bg", false, "Execute autonomously in the background")
}
if !params.omitStop {
cmd.Flags().BoolVarP(&tellStop, "stop", "s", false, "Stop after a single reply")
}
if !params.omitNoBuild {
cmd.Flags().BoolVarP(&tellNoBuild, "no-build", "n", false, "Don't build files")
}
cmd.Flags().BoolVar(&autoConfirm, "auto-update-context", false, shared.ConfigSettingsByKey["auto-update-context"].Desc)
if !params.omitAutoContext {
cmd.Flags().BoolVar(&tellAutoContext, "auto-load-context", false, shared.ConfigSettingsByKey["auto-load-context"].Desc)
}
if !params.omitSmartContext {
cmd.Flags().BoolVar(&tellSmartContext, "smart-context", false, shared.ConfigSettingsByKey["smart-context"].Desc)
}
if !params.omitApply {
cmd.Flags().BoolVar(&tellAutoApply, "apply", false, "Automatically apply changes")
initApplyFlags(cmd, true)
}
if !params.omitExec {
initExecScriptFlags(cmd)
}
if !params.omitEditor {
cmd.Flags().Var(newEditorValue(&editor), "editor", "Write prompt in system editor")
cmd.Flag("editor").NoOptDefVal = defaultEditor
}
if !params.omitSkipMenu {
cmd.Flags().BoolVar(&tellSkipMenu, "skip-menu", false, shared.ConfigSettingsByKey["skip-changes-menu"].Desc)
}
}
func initApplyFlags(cmd *cobra.Command, applyFlag bool) {
commitDesc := "Commit changes to git"
if applyFlag {
commitDesc += " when --apply is passed"
}
skipCommitDesc := "Skip committing changes to git"
if applyFlag {
skipCommitDesc += " when --apply is passed"
}
cmd.Flags().BoolVarP(&autoCommit, "commit", "c", false, commitDesc)
cmd.Flags().BoolVar(&skipCommit, "skip-commit", false, skipCommitDesc)
}
func initExecScriptFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&noExec, "no-exec", false, "Disable command execution")
cmd.Flags().BoolVar(&autoExec, "auto-exec", false, "Automatically execute commands without confirmation")
cmd.Flags().Var(newAutoDebugValue(&autoDebug), "debug", "Automatically execute and debug failing commands (optionally specify number of tries—default is 5)")
cmd.Flag("debug").NoOptDefVal = strconv.Itoa(defaultAutoDebugTries)
}
func validatePlanExecFlags(isApply bool) {
if autoDebug > 0 && noExec {
term.OutputErrorAndExit("--debug can't be used with --no-exec")
}
if tellAutoContext && tellBg {
term.OutputErrorAndExit("--auto-context/-c can't be used with --bg")
}
if !isApply {
if autoDebug > 0 && !tellAutoApply {
term.OutputErrorAndExit("--debug can only be used with --apply")
}
if autoExec && !tellAutoApply {
term.OutputErrorAndExit("--auto-exec can only be used with --apply")
}
if tellAutoApply && tellNoBuild {
term.OutputErrorAndExit("--apply can't be used with --no-build/-n")
}
if tellAutoApply && tellBg {
term.OutputErrorAndExit("--apply can't be used with --bg")
}
}
}
func mustSetPlanExecFlags(cmd *cobra.Command, isApply bool) {
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
config := lib.MustGetCurrentPlanConfig()
// Set flag vars from config when flags aren't explicitly set
if !cmd.Flags().Changed("stop") {
tellStop = !config.AutoContinue
}
if !cmd.Flags().Changed("no-build") {
tellNoBuild = !config.AutoBuild
}
if !cmd.Flags().Changed("auto-update-context") {
autoConfirm = config.AutoUpdateContext
}
if !cmd.Flags().Changed("apply") {
tellAutoApply = config.AutoApply
}
if !cmd.Flags().Changed("skip-commit") {
skipCommit = config.SkipCommit
}
if !cmd.Flags().Changed("commit") {
autoCommit = config.AutoCommit
}
if !cmd.Flags().Changed("auto-load-context") {
tellAutoContext = config.AutoLoadContext
}
if !cmd.Flags().Changed("smart-context") {
tellSmartContext = config.SmartContext
}
if !cmd.Flags().Changed("no-exec") {
noExec = !config.CanExec
}
if !cmd.Flags().Changed("auto-exec") {
autoExec = config.AutoExec
}
if !cmd.Flags().Changed("debug") {
autoDebug = config.AutoDebugTries
// Only set autoDebug if AutoDebug is enabled in config
if !config.AutoDebug {
autoDebug = 0
}
}
if !cmd.Flags().Changed("skip-menu") {
tellSkipMenu = config.SkipChangesMenu
}
// tell command editor is no longer tied to config *unless* it's set to vim or nano
// otherwise, the flag or EDITOR env var are used
// config.Editor is now used for mainly for JSON editing (and perhaps other purposes)
// this is because it's pretty rare to use the editor for writing prompts now rather than the REPL
if !editorSetByFlag && (config.Editor == shared.EditorTypeVim || config.Editor == shared.EditorTypeNano) {
editor = config.Editor
}
validatePlanExecFlags(isApply)
}
// AutoDebugValue implements the flag.Value interface
type autoDebugValue struct {
value *int
}
func newAutoDebugValue(p *int) *autoDebugValue {
*p = 0 // Default to 0 (disabled)
return &autoDebugValue{p}
}
func (f *autoDebugValue) Set(s string) error {
if s == "" {
*f.value = defaultAutoDebugTries
return nil
}
v, err := strconv.Atoi(s)
if err != nil {
return fmt.Errorf("invalid value for --debug: %v", err)
}
if v <= 0 {
return fmt.Errorf("--debug value must be greater than 0")
}
*f.value = v
return nil
}
func (f *autoDebugValue) String() string {
if f.value == nil {
return "0"
}
return strconv.Itoa(*f.value)
}
func (f *autoDebugValue) Type() string {
return "int"
}
// EditorValue implements the flag.Value interface
type editorValue struct {
value *string
}
func newEditorValue(p *string) *editorValue {
*p = defaultEditor
return &editorValue{p}
}
func (f *editorValue) Set(s string) error {
if s == "" {
*f.value = defaultEditor
return nil
}
*f.value = s
editorSetByFlag = true
return nil
}
func (f *editorValue) String() string {
if f.value == nil {
return ""
}
return *f.value
}
func (f *editorValue) Type() string {
return "string"
}
================================================
FILE: app/cli/cmd/plan_start_helpers.go
================================================
package cmd
import (
"os"
"plandex-cli/api"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var (
// Tier flags
noAuto bool
basicAuto bool
plusAuto bool
semiAuto bool
fullAuto bool
// Type flags
dailyModels bool
reasoningModels bool
strongModels bool
ossModels bool
cheapModels bool
geminiPlannerModels bool
o3PlannerModels bool
r1PlannerModels bool
perplexityPlannerModels bool
opusPlannerModels bool
)
func AddNewPlanFlags(cmd *cobra.Command) {
// Add tier flags
cmd.Flags().BoolVar(&noAuto, "no-auto", false, shared.AutoModeDescriptions[shared.AutoModeNone])
cmd.Flags().BoolVar(&basicAuto, "basic", false, shared.AutoModeDescriptions[shared.AutoModeBasic])
cmd.Flags().BoolVar(&plusAuto, "plus", false, shared.AutoModeDescriptions[shared.AutoModePlus])
cmd.Flags().BoolVar(&semiAuto, "semi", false, shared.AutoModeDescriptions[shared.AutoModeSemi])
cmd.Flags().BoolVar(&fullAuto, "full", false, shared.AutoModeDescriptions[shared.AutoModeFull])
// Add type flags
cmd.Flags().BoolVar(&dailyModels, "daily", false, shared.DailyDriverModelPack.Description)
cmd.Flags().BoolVar(&reasoningModels, "reasoning", false, shared.ReasoningModelPack.Description)
cmd.Flags().BoolVar(&strongModels, "strong", false, shared.StrongModelPack.Description)
cmd.Flags().BoolVar(&cheapModels, "cheap", false, shared.CheapModelPack.Description)
cmd.Flags().BoolVar(&ossModels, "oss", false, shared.OSSModelPack.Description)
cmd.Flags().BoolVar(&geminiPlannerModels, "gemini-planner", false, shared.GeminiPlannerModelPack.Description)
cmd.Flags().BoolVar(&o3PlannerModels, "o3-planner", false, shared.O3PlannerModelPack.Description)
cmd.Flags().BoolVar(&r1PlannerModels, "r1-planner", false, shared.R1PlannerModelPack.Description)
cmd.Flags().BoolVar(&perplexityPlannerModels, "perplexity-planner", false, shared.PerplexityPlannerModelPack.Description)
cmd.Flags().BoolVar(&opusPlannerModels, "opus-planner", false, shared.OpusPlannerModelPack.Description)
}
func resolveAutoMode(config *shared.PlanConfig) (bool, *shared.PlanConfig) {
didUpdate, updatedConfig, _ := resolveAutoModeWithArgs(config, false)
return didUpdate, updatedConfig
}
func resolveAutoModeSilent(config *shared.PlanConfig) (bool, *shared.PlanConfig, func()) {
return resolveAutoModeWithArgs(config, true)
}
func resolveAutoModeWithArgs(config *shared.PlanConfig, silent bool) (bool, *shared.PlanConfig, func()) {
currentAutoMode := config.AutoMode
var toSetAutoMode shared.AutoModeType
if noAuto {
toSetAutoMode = shared.AutoModeNone
} else if basicAuto {
toSetAutoMode = shared.AutoModeBasic
} else if plusAuto {
toSetAutoMode = shared.AutoModePlus
} else if semiAuto {
toSetAutoMode = shared.AutoModeSemi
} else if fullAuto {
toSetAutoMode = shared.AutoModeFull
}
if toSetAutoMode != "" && toSetAutoMode != currentAutoMode {
if !silent {
term.StartSpinner("")
}
_, updatedConfig := updateConfig([]string{"auto-mode", string(toSetAutoMode)}, config)
apiErr := api.Client.UpdatePlanConfig(lib.CurrentPlanId, shared.UpdatePlanConfigRequest{
Config: updatedConfig,
})
lib.SetCachedPlanConfig(updatedConfig)
if !silent {
term.StopSpinner()
}
if apiErr != nil {
term.OutputErrorAndExit("Error updating config auto-mode: %v", apiErr)
}
fn := func() {
printAutoModeTable(config)
}
if !silent {
fn()
return true, updatedConfig, fn
}
return true, updatedConfig, fn
}
return false, config, nil
}
func resolveModelPack() {
resolveModelPackWithArgs(nil, false)
}
func resolveModelPackSilent(settings *shared.PlanSettings) (*shared.PlanSettings, func()) {
return resolveModelPackWithArgs(settings, true)
}
func resolveModelPackWithArgs(settings *shared.PlanSettings, silent bool) (*shared.PlanSettings, func()) {
var originalSettings *shared.PlanSettings
var apiErr *shared.ApiError
if settings == nil {
if !silent {
term.StartSpinner("")
}
originalSettings, apiErr = api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)
} else {
originalSettings = settings
}
if apiErr != nil {
term.OutputErrorAndExit("Error getting current settings: %v", apiErr)
return nil, nil
}
var packName string
if ossModels {
packName = shared.OSSModelPack.Name
} else if strongModels {
packName = shared.StrongModelPack.Name
} else if cheapModels {
packName = shared.CheapModelPack.Name
} else if reasoningModels {
packName = shared.ReasoningModelPack.Name
} else if dailyModels {
packName = shared.DailyDriverModelPack.Name
} else if geminiPlannerModels {
packName = shared.GeminiPlannerModelPack.Name
} else if o3PlannerModels {
packName = shared.O3PlannerModelPack.Name
} else if r1PlannerModels {
packName = shared.R1PlannerModelPack.Name
} else if perplexityPlannerModels {
packName = shared.PerplexityPlannerModelPack.Name
} else if opusPlannerModels {
packName = shared.OpusPlannerModelPack.Name
}
if packName != "" && packName != originalSettings.GetModelPack().Name {
if !silent {
term.StartSpinner("")
}
updatedSettings := updateModelSettings([]string{packName}, originalSettings, "")
_, apiErr = api.Client.UpdateSettings(lib.CurrentPlanId, lib.CurrentBranch, shared.UpdateSettingsRequest{
ModelPackName: updatedSettings.ModelPackName,
ModelPack: updatedSettings.ModelPack,
})
if !silent {
term.StopSpinner()
}
if apiErr != nil {
term.OutputErrorAndExit("Error setting model pack: %v", apiErr)
return nil, nil
}
fn := func() {
printModelPackTable(packName)
}
if !silent {
fn()
return updatedSettings, fn
}
return updatedSettings, fn
} else {
if !silent {
term.StopSpinner()
}
fn := func() {
printModelPackTable(originalSettings.GetModelPack().Name)
}
if !silent {
fn()
return originalSettings, fn
}
return originalSettings, fn
}
}
func printModelPackTable(packName string) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"🧠 Model Pack"})
table.Append([]string{color.New(color.Bold, term.ColorHiMagenta).Sprint(packName)})
table.Render()
}
func printAutoModeTable(config *shared.PlanConfig) {
var contextMode string
if config.AutoLoadContext {
contextMode = "auto"
} else {
contextMode = "manual"
}
var applyMode string
if config.AutoApply {
applyMode = "auto"
} else {
applyMode = "approve"
}
var executionMode string
if config.AutoExec {
executionMode = "auto"
} else if config.CanExec {
executionMode = "approve"
} else {
executionMode = "disabled"
}
var commitMode string
if config.AutoCommit {
commitMode = "auto"
} else if config.SkipCommit {
commitMode = "skip"
} else {
commitMode = "manual"
}
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{
"🚀 Auto Mode",
"Context",
"Apply",
"Execution",
"Commits",
})
table.Append([]string{
color.New(color.Bold, term.ColorHiMagenta).Sprint(config.AutoMode),
contextMode,
applyMode,
executionMode,
commitMode,
})
table.Render()
}
================================================
FILE: app/cli/cmd/plans.go
================================================
package cmd
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/format"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"github.com/xlab/treeprint"
)
var archivedOnly bool
func init() {
RootCmd.AddCommand(plansCmd)
plansCmd.Flags().BoolVarP(&archivedOnly, "archived", "a", false, "List archived plans")
}
// plansCmd represents the list command
var plansCmd = &cobra.Command{
Use: "plans",
Aliases: []string{"pl"},
Short: "List plans",
Run: plans,
}
func plans(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MaybeResolveProject()
if archivedOnly {
listArchived()
} else {
listActive()
}
}
func listActive() {
errCh := make(chan error)
var parentProjectIdsWithPaths [][2]string
var childProjectIdsWithPaths [][2]string
go func() {
res, err := fs.GetParentProjectIdsWithPaths(auth.Current.UserId)
if err != nil {
errCh <- fmt.Errorf("error getting parent project ids with paths: %v", err)
return
}
parentProjectIdsWithPaths = res
errCh <- nil
}()
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
res, err := fs.GetChildProjectIdsWithPaths(ctx, auth.Current.UserId)
if err != nil {
log.Println(err.Error())
if err.Error() == "context timeout" {
errCh <- nil
return
}
errCh <- fmt.Errorf("error getting child project ids with paths: %v", err)
return
}
childProjectIdsWithPaths = res
errCh <- nil
}()
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
term.OutputErrorAndExit("%v", err)
}
}
var projectIds []string
if lib.CurrentProjectId != "" {
projectIds = append(projectIds, lib.CurrentProjectId)
}
for _, p := range parentProjectIdsWithPaths {
projectIds = append(projectIds, p[1])
}
for _, p := range childProjectIdsWithPaths {
projectIds = append(projectIds, p[1])
}
if len(projectIds) == 0 {
fmt.Println("🤷♂️ No plans")
fmt.Println()
term.PrintCmds("", "new")
return
}
term.StartSpinner("")
plans, apiErr := api.Client.ListPlans(projectIds)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
}
if len(plans) == 0 {
fmt.Println("🤷♂️ No plans")
fmt.Println()
term.PrintCmds("", "new")
return
}
plansByProjectId := make(map[string][]*shared.Plan)
var currentProjectPlanIds []string
for _, p := range plans {
plansByProjectId[p.ProjectId] = append(plansByProjectId[p.ProjectId], p)
if p.ProjectId == lib.CurrentProjectId {
currentProjectPlanIds = append(currentProjectPlanIds, p.Id)
}
}
for projectId, plans := range plansByProjectId {
if projectId != lib.CurrentProjectId {
// sort non-current-project plans alphabetically
sort.Slice(plans, func(i, j int) bool {
return plans[i].Name < plans[j].Name
})
}
}
// remove paths with no plans from parentProjectIdsWithPaths and childProjectIdsWithPaths
var parentProjectIdsWithPathsFiltered [][2]string
for _, p := range parentProjectIdsWithPaths {
if len(plansByProjectId[p[1]]) > 0 {
parentProjectIdsWithPathsFiltered = append(parentProjectIdsWithPathsFiltered, p)
}
}
parentProjectIdsWithPaths = parentProjectIdsWithPathsFiltered
var childProjectIdsWithPathsFiltered [][2]string
for _, p := range childProjectIdsWithPaths {
if len(plansByProjectId[p[1]]) > 0 {
childProjectIdsWithPathsFiltered = append(childProjectIdsWithPathsFiltered, p)
}
}
childProjectIdsWithPaths = childProjectIdsWithPathsFiltered
var b strings.Builder
if len(currentProjectPlanIds) > 0 {
currentBranchNamesByPlanId, err := lib.GetCurrentBranchNamesByPlanId(currentProjectPlanIds)
if err != nil {
term.OutputErrorAndExit("Error getting current branches: %v", err)
}
currentBranchesByPlanId, apiErr := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{
CurrentBranchByPlanId: currentBranchNamesByPlanId,
})
if apiErr != nil {
term.OutputErrorAndExit("Error getting current branches: %v", apiErr)
}
table := tablewriter.NewWriter(&b)
table.SetAutoWrapText(false)
table.SetHeader([]string{"#", "Name", "Updated" /*, "Created" /*"Branches",*/, "Branch", "Context", "Convo"})
currentProjectPlans := plansByProjectId[lib.CurrentProjectId]
if len(parentProjectIdsWithPaths) > 0 || len(childProjectIdsWithPaths) > 0 {
b.WriteString(color.New(color.Bold, term.ColorHiGreen).Sprint("Plans in current directory\n"))
} else {
b.WriteString("\n")
}
for i, p := range currentProjectPlans {
num := strconv.Itoa(i + 1)
if p.Id == lib.CurrentPlanId {
num = color.New(color.Bold, term.ColorHiGreen).Sprint(num)
}
var name string
if p.Id == lib.CurrentPlanId {
name = color.New(color.Bold, term.ColorHiGreen).Sprint(p.Name) + fmt.Sprint(" 👈")
} else {
name = p.Name
}
currentBranch := currentBranchesByPlanId[p.Id]
row := []string{
num,
name,
format.Time(p.UpdatedAt),
// format.Time(p.CreatedAt),
// strconv.Itoa(p.ActiveBranches),
currentBranch.Name,
strconv.Itoa(currentBranch.ContextTokens) + " 🪙",
strconv.Itoa(currentBranch.ConvoTokens) + " 🪙",
}
var style []tablewriter.Colors
if p.Name == lib.CurrentPlanId {
style = []tablewriter.Colors{
{tablewriter.FgHiGreenColor, tablewriter.Bold},
}
} else {
style = []tablewriter.Colors{
{tablewriter.Bold},
}
}
table.Rich(row, style)
}
table.Render()
} else {
b.WriteString("🤷♂️ No plans in current directory\n")
}
var addPathToTreeFn func(tree treeprint.Tree, basePath, localPath, projectId string, isParent bool)
addPathToTreeFn = func(tree treeprint.Tree, basePath, localPath, projectId string, isParent bool) {
var base string
var tail string
split := strings.Split(localPath, string(os.PathSeparator))
var baseBranch treeprint.Tree
for _, part := range split {
base = filepath.Join(base, part)
tail = strings.TrimPrefix(localPath, base+string(os.PathSeparator))
var searchBranch string
if isParent {
baseFull := filepath.Join(fs.HomeDir, basePath, base)
baseRel, _ := filepath.Rel(fs.Cwd, baseFull)
searchBranch = fmt.Sprintf("%s (%s)", base, baseRel)
// log.Println("Project root:", fs.Cwd)
// log.Println("searchBranch:", searchBranch)
// log.Println("base:", base)
// log.Println("tail:", tail)
// log.Println("basePath:", basePath)
// log.Println("baseFull:", baseFull)
// log.Println("baseRel:", baseRel)
} else {
searchBranch = base
}
baseBranch = tree.FindByValue(searchBranch)
if baseBranch != nil {
addPathToTreeFn(baseBranch, filepath.Join(basePath, base), tail, projectId, isParent)
return
}
}
if baseBranch == nil {
label := localPath
if isParent {
pathFull := filepath.Join(fs.HomeDir, basePath, localPath)
pathRel, _ := filepath.Rel(fs.Cwd, pathFull)
label = fmt.Sprintf("%s (%s)", localPath, pathRel)
}
branch := tree.AddBranch(label)
plans := plansByProjectId[projectId]
for _, p := range plans {
branch.AddNode(color.New(term.ColorHiCyan).Sprint(p.Name))
}
}
}
var c color.Attribute
if term.IsDarkBg {
c = color.FgWhite
} else {
c = color.FgBlack
}
if len(parentProjectIdsWithPaths) > 0 {
b.WriteString("\n")
b.WriteString(color.New(color.Bold).Sprint("Plans in parent directories\n"))
b.WriteString(color.New(c).Sprint("cd into a directory to work on a plan in that directory\n"))
parentTree := treeprint.NewWithRoot("~")
for i := len(parentProjectIdsWithPaths) - 1; i >= 0; i-- {
p := parentProjectIdsWithPaths[i]
rel, err := filepath.Rel(fs.HomeDir, p[0])
if err != nil {
term.OutputErrorAndExit("Error getting relative path: %v", err)
}
addPathToTreeFn(parentTree, "", rel, p[1], true)
}
b.WriteString(parentTree.String())
}
if len(childProjectIdsWithPaths) > 0 {
b.WriteString("\n")
b.WriteString(color.New(color.Bold).Sprint("Plans in child directories\n"))
b.WriteString(color.New(c).Sprint("cd into a directory to work on a plan in that directory\n"))
childTree := treeprint.New()
for _, p := range childProjectIdsWithPaths {
rel, err := filepath.Rel(fs.Cwd, p[0])
if err != nil {
term.OutputErrorAndExit("Error getting relative path: %v", err)
}
addPathToTreeFn(childTree, "", rel, p[1], false)
}
b.WriteString(childTree.String())
} else {
b.WriteString("\n")
}
term.PageOutput(b.String())
fmt.Println()
if len(currentProjectPlanIds) > 0 {
term.PrintCmds("", "new", "cd", "delete-plan", "plans --archived", "archive")
} else {
term.PrintCmds("", "new", "plans --archived")
}
}
func listArchived() {
var projectIds []string
if lib.CurrentProjectId != "" {
projectIds = append(projectIds, lib.CurrentProjectId)
}
term.StartSpinner("")
plans, apiErr := api.Client.ListArchivedPlans(projectIds)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting plans: %v", apiErr)
}
if len(plans) == 0 {
fmt.Println("🤷♂️ No archived plans")
fmt.Println()
term.PrintCmds("", "archive")
return
}
var b strings.Builder
table := tablewriter.NewWriter(&b)
table.SetAutoWrapText(false)
table.SetHeader([]string{"#", "Name", "Updated"})
for i, p := range plans {
num := strconv.Itoa(i + 1)
if p.Id == lib.CurrentPlanId {
num = color.New(color.Bold, term.ColorHiGreen).Sprint(num)
}
row := []string{
num,
p.Name,
format.Time(p.UpdatedAt),
}
var style []tablewriter.Colors
if p.Name == lib.CurrentPlanId {
style = []tablewriter.Colors{
{tablewriter.FgHiGreenColor, tablewriter.Bold},
}
} else {
style = []tablewriter.Colors{
{tablewriter.Bold},
}
}
table.Rich(row, style)
}
table.Render()
term.PageOutput(b.String())
fmt.Println()
term.PrintCmds("", "unarchive")
}
================================================
FILE: app/cli/cmd/ps.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/format"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var psCmd = &cobra.Command{
Use: "ps",
Short: "List plans with active or recently finished streams",
Run: ps,
}
func init() {
RootCmd.AddCommand(psCmd)
}
func ps(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
res, apiErr := api.Client.ListPlansRunning([]string{lib.CurrentProjectId}, true)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting running plans: %v", apiErr)
return
}
if len(res.Branches) == 0 {
fmt.Println("🤷♂️ No active or recently finished streams")
return
}
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader([]string{"Pid", "Plan", "Branch", "Started", "Status"})
for _, b := range res.Branches {
id := res.StreamIdByBranchId[b.Id]
plan := res.PlansById[b.PlanId]
status := "Active"
finishedAt := res.StreamFinishedAtByBranchId[b.Id]
switch b.Status {
case shared.PlanStatusFinished:
status = "Finished " + format.Time(finishedAt)
case shared.PlanStatusError:
status = "Error " + format.Time(finishedAt)
case shared.PlanStatusStopped:
status = "Stopped " + format.Time(finishedAt)
case shared.PlanStatusMissingFile:
status = "Missing file"
}
row := []string{
id[:4],
plan.Name,
b.Name,
format.Time(res.StreamStartedAtByBranchId[b.Id]),
status,
}
var style []tablewriter.Colors
if b.Name == lib.CurrentPlanId {
style = []tablewriter.Colors{
{tablewriter.FgGreenColor, tablewriter.Bold},
}
} else {
style = []tablewriter.Colors{
{tablewriter.Bold},
}
}
table.Rich(row, style)
}
table.Render()
fmt.Println()
term.PrintCmds("", "connect", "stop")
}
================================================
FILE: app/cli/cmd/reject.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"sort"
"github.com/plandex-ai/survey/v2"
"github.com/spf13/cobra"
)
var rejectAll bool
func init() {
RootCmd.AddCommand(rejectCmd)
rejectCmd.Flags().BoolVarP(&rejectAll, "all", "a", false, "Reject all pending changes")
}
var rejectCmd = &cobra.Command{
Use: "reject [files...]",
Aliases: []string{"rj"},
Short: "Reject pending changes",
Run: reject,
}
func reject(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
currentPlanState, apiErr := api.Client.GetCurrentPlanState(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error getting current plan state: %v", apiErr)
}
currentFiles := currentPlanState.CurrentPlanFiles.Files
if len(currentFiles) == 0 {
term.StopSpinner()
term.OutputErrorAndExit("No pending changes to reject")
}
if rejectAll {
numToReject := len(currentFiles)
suffix := ""
if numToReject > 1 {
suffix = "s"
}
apiErr := api.Client.RejectAllChanges(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error rejecting all changes: %v", apiErr)
}
term.StopSpinner()
fmt.Printf("✅ Rejected changes to %d file%s\n", numToReject, suffix)
sortedFiles := make([]string, 0, len(currentFiles))
for file := range currentFiles {
sortedFiles = append(sortedFiles, file)
}
sort.Strings(sortedFiles)
for _, file := range sortedFiles {
fmt.Printf("• 📄 %s\n", file)
}
return
}
if len(args) > 0 {
for _, path := range args {
if _, ok := currentFiles[path]; !ok {
term.StopSpinner()
term.OutputErrorAndExit("File %s not found in plan or has no changes to reject", path)
}
}
numToReject := len(args)
suffix := ""
if numToReject > 1 {
suffix = "s"
}
apiErr = api.Client.RejectFiles(lib.CurrentPlanId, lib.CurrentBranch, args)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error rejecting changes: %v", apiErr)
}
fmt.Printf("✅ Rejected changes to %d file%s\n", numToReject, suffix)
sortedFiles := append([]string{}, args...)
sort.Strings(sortedFiles)
for _, file := range sortedFiles {
fmt.Printf("• 📄 %s\n", file)
}
return
}
// No args provided - use survey multiselect
term.StopSpinner()
pathsToSort := make([]string, 0, len(currentFiles))
for path := range currentFiles {
pathsToSort = append(pathsToSort, path)
}
sort.Strings(pathsToSort)
var selectedFiles []string
prompt := &survey.MultiSelect{
Message: "Select files to reject:",
Options: pathsToSort,
}
err := survey.AskOne(prompt, &selectedFiles)
if err != nil {
term.OutputErrorAndExit("Error getting file selection: %v", err)
}
if len(selectedFiles) == 0 {
fmt.Println("No files selected")
return
}
term.StartSpinner("")
apiErr = api.Client.RejectFiles(lib.CurrentPlanId, lib.CurrentBranch, selectedFiles)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error rejecting changes: %v", apiErr)
}
suffix := ""
if len(selectedFiles) > 1 {
suffix = "s"
}
fmt.Printf("✅ Rejected changes to %d file%s\n", len(selectedFiles), suffix)
sortedFiles := append([]string{}, selectedFiles...)
sort.Strings(sortedFiles)
for _, file := range sortedFiles {
fmt.Printf("• 📄 %s\n", file)
}
}
================================================
FILE: app/cli/cmd/rename.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var renameCmd = &cobra.Command{
Use: "rename [new-name]",
Short: "Rename the current plan",
Args: cobra.MaximumNArgs(1),
Run: rename,
}
func init() {
RootCmd.AddCommand(renameCmd)
}
func rename(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
var newName string
if len(args) > 0 {
newName = args[0]
} else {
var err error
newName, err = term.GetRequiredUserStringInput("New name:")
if err != nil {
term.OutputErrorAndExit("Error reading new name: %v", err)
}
}
if newName == "" {
fmt.Println("🤷♂️ No new name provided")
return
}
term.StartSpinner("")
err := api.Client.RenamePlan(lib.CurrentPlanId, newName)
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error renaming plan: %v", err)
}
fmt.Printf("✅ Plan renamed to %s\n", color.New(color.Bold, term.ColorHiGreen).Sprint(newName))
}
================================================
FILE: app/cli/cmd/repl.go
================================================
package cmd
import (
"fmt"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/term"
"plandex-cli/types"
"plandex-cli/version"
shared "plandex-shared"
"regexp"
"sort"
"strings"
"unicode"
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"github.com/plandex-ai/go-prompt"
pstrings "github.com/plandex-ai/go-prompt/strings"
"github.com/lithammer/fuzzysearch/fuzzy"
)
var replCmd = &cobra.Command{
Use: "repl",
Short: "Start interactive Plandex REPL",
Run: runRepl,
}
var cliSuggestions []prompt.Suggest
var projectPaths *types.ProjectPaths
var currentPrompt *prompt.Prompt
var replConfig *shared.PlanConfig
var sessionId string
func init() {
RootCmd.AddCommand(replCmd)
replCmd.Flags().BoolP("chat", "c", false, "Start in chat mode")
replCmd.Flags().BoolP("tell", "t", false, "Start in tell mode")
AddNewPlanFlags(replCmd)
for _, config := range term.CliCommands {
if config.Repl {
desc := config.Desc
if config.Alias != "" {
desc = fmt.Sprintf("(\\%s) %s", config.Alias, desc)
}
cliSuggestions = append(cliSuggestions, prompt.Suggest{Text: "\\" + config.Cmd, Description: desc})
}
}
}
func setReplConfig() {
replConfig = lib.MustGetCurrentPlanConfig()
}
func runRepl(cmd *cobra.Command, args []string) {
sessionId = uuid.New().String()
term.SetIsRepl(true)
auth.MustResolveAuthWithOrg()
lib.MustResolveOrCreateProject()
term.StartSpinner("")
lib.LoadState()
chatFlag, err := cmd.Flags().GetBool("chat")
if err != nil {
term.OutputErrorAndExit("Error getting chat flag: %v", err)
}
tellFlag, err := cmd.Flags().GetBool("tell")
if err != nil {
term.OutputErrorAndExit("Error getting tell flag: %v", err)
}
if chatFlag && tellFlag {
term.OutputErrorAndExit("Cannot specify both --chat and --tell flags")
}
if chatFlag {
lib.CurrentReplState.Mode = lib.ReplModeChat
lib.WriteState()
} else if tellFlag {
lib.CurrentReplState.Mode = lib.ReplModeTell
lib.WriteState()
}
afterNew := false
if lib.CurrentPlanId == "" {
os.Setenv("PLANDEX_DISABLE_SUGGESTIONS", "1")
args := []string{}
if noAuto {
args = append(args, "--no-auto")
} else if basicAuto {
args = append(args, "--basic")
} else if plusAuto {
args = append(args, "--plus")
} else if semiAuto {
args = append(args, "--semi")
} else if fullAuto {
args = append(args, "--full")
}
if ossModels {
args = append(args, "--oss")
} else if strongModels {
args = append(args, "--strong")
} else if cheapModels {
args = append(args, "--cheap")
} else if dailyModels {
args = append(args, "--daily")
} else if reasoningModels {
args = append(args, "--reasoning")
} else if geminiPlannerModels {
args = append(args, "--gemini-planner")
} else if o3PlannerModels {
args = append(args, "--o3-planner")
} else if r1PlannerModels {
args = append(args, "--r1-planner")
} else if perplexityPlannerModels {
args = append(args, "--perplexity-planner")
} else if opusPlannerModels {
args = append(args, "--opus-planner")
}
newCmd.Run(newCmd, args)
os.Setenv("PLANDEX_DISABLE_SUGGESTIONS", "")
afterNew = true
}
setReplConfig()
lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode)
projectPaths, err = fs.GetProjectPaths(fs.Cwd)
if err != nil {
color.New(term.ColorHiRed).Printf("Error getting project paths: %v\n", err)
}
settings, apiErr := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.OutputErrorAndExit("Error getting settings: %v", apiErr.Msg)
}
var printAutoFn func()
var printModelFn func()
if !afterNew {
var didUpdateConfig bool
var updatedConfig *shared.PlanConfig
var updatedSettings *shared.PlanSettings
didUpdateConfig, updatedConfig, printAutoFn = resolveAutoModeSilent(replConfig)
updatedSettings, printModelFn = resolveModelPackSilent(settings)
if didUpdateConfig {
loadMapIfNeeded(replConfig, updatedConfig)
removeMapIfNeeded(replConfig, updatedConfig)
if updatedConfig != nil {
replConfig = updatedConfig
}
}
if updatedSettings != nil {
settings = updatedSettings
}
}
replWelcome(replWelcomeParams{
afterNew: afterNew,
isHelp: false,
printAutoFn: printAutoFn,
printModelFn: printModelFn,
config: replConfig,
packName: settings.GetModelPack().Name,
})
var p *prompt.Prompt
p = prompt.New(
func(in string) { executor(in, p) },
prompt.WithPrefixCallback(func() string {
// Get last part of current working directory
// cwd := fs.Cwd
// dirName := filepath.Base(cwd)
// Build prefix with directory and mode indicator
var modeIcon string
if lib.CurrentReplState.Mode == lib.ReplModeTell {
modeIcon = "⚡️"
if replConfig.AutoApply && replConfig.AutoExec {
modeIcon += "❗️" // warning reminder for auto apply and auto exec
}
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
modeIcon = "💬"
}
return fmt.Sprintf("%s ", modeIcon)
}),
prompt.WithTitle("Plandex "+version.Version),
prompt.WithSelectedSuggestionBGColor(prompt.LightGray),
prompt.WithSuggestionBGColor(prompt.DarkGray),
prompt.WithCompletionOnDown(),
prompt.WithCompleter(completer),
prompt.WithExecuteOnEnterCallback(executeOnEnter),
prompt.WithHistory(lib.GetHistory()),
)
currentPrompt = p
p.Run()
}
func getSuggestions() []prompt.Suggest {
suggestions := []prompt.Suggest{}
if lib.CurrentReplState.IsMulti {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\send", Description: "(\\s) Send the current prompt"},
{Text: "\\multi", Description: "(\\m) Turn multi-line mode off"},
{Text: "\\run", Description: "(\\r) Run a file through tell/chat based on current mode"},
{Text: "\\quit", Description: "(\\q) Exit the REPL"},
}...)
}
if lib.CurrentReplState.Mode == lib.ReplModeTell {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\chat", Description: "(\\ch) Switch to 'chat' mode to have a conversation without making changes"},
}...)
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\tell", Description: "(\\t) Switch to 'tell' mode for implementation"},
}...)
}
if !lib.CurrentReplState.IsMulti {
suggestions = append(suggestions, []prompt.Suggest{
{Text: "\\multi", Description: "(\\m) Turn multi-line mode on"},
{Text: "\\run", Description: "(\\r) Run a file through tell/chat based on current mode"},
{Text: "\\quit", Description: "(\\q) Exit the REPL"},
}...)
}
// Add help command suggestion
suggestions = append(suggestions, prompt.Suggest{Text: "\\help", Description: "(\\h) REPL info and list of commands"})
suggestions = append(suggestions, cliSuggestions...)
for path := range projectPaths.ActivePaths {
if path == "." {
continue
}
isDir := projectPaths.ActiveDirs[path]
if isDir {
path += "/"
}
suggestions = append(suggestions, prompt.Suggest{Text: "@" + path})
loadArgs := path
if isDir {
loadArgs += " -r"
}
suggestions = append(suggestions, prompt.Suggest{Text: "\\load " + loadArgs})
if isDir {
loadArgs = path
loadArgs += " --map"
suggestions = append(suggestions, prompt.Suggest{Text: "\\load " + loadArgs})
loadArgs = path
loadArgs += " --tree"
suggestions = append(suggestions, prompt.Suggest{Text: "\\load " + loadArgs})
}
if filepath.Ext(path) == ".md" || filepath.Ext(path) == ".txt" {
suggestions = append(suggestions, prompt.Suggest{Text: "\\run " + path})
}
}
return suggestions
}
func executeOnEnter(p *prompt.Prompt, indentSize int) (int, bool) {
input := p.Buffer().Text()
cmd, _ := parseCommand(input)
if cmd != "" {
return 0, true
}
if lib.CurrentReplState.IsMulti {
return 0, false
}
return 0, true
}
const cancelOpt = "Cancel"
func executor(in string, p *prompt.Prompt) {
defer lib.WriteHistory(in)
in = strings.TrimSpace(in)
lines := strings.Split(in, "\n")
lastLine := lines[len(lines)-1]
lastLine = strings.TrimSpace(lastLine)
trimmedInput := strings.TrimSpace(in)
if trimmedInput == "" {
return
}
// condense whitespace
condensedInput := strings.Join(strings.Fields(trimmedInput), " ")
// Handle plandex/pdx command prefix
if strings.HasPrefix(lastLine, "plandex ") || strings.HasPrefix(lastLine, "pdx ") {
fmt.Println()
parts := strings.Fields(lastLine)
if len(parts) > 1 {
args := parts[1:] // Skip the "plandex" or "pdx" command
_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing command: %v\n", err)
}
}
fmt.Println()
return
}
// Find the last \ or @ in the last line
lastBackslashIndex := strings.LastIndex(lastLine, "\\")
lastAtIndex := strings.LastIndex(lastLine, "@")
var preservedBuffer string
if len(lines) > 1 {
preservedBuffer = strings.Join(lines[:len(lines)-1], "\n") + "\n"
}
suggestions, _, _ := completer(prompt.Document{Text: in})
// Handle file references
if lastAtIndex != -1 && lastAtIndex > lastBackslashIndex {
paths := strings.Split(lastLine, "@")
numPaths := len(paths)
filteredPaths := []string{}
for i, path := range paths {
p := strings.TrimSpace(path)
if i == 0 {
// text before the @
preservedBuffer += p + " "
continue
}
if (p == "" || !projectPaths.ActivePaths[p]) && len(suggestions) > 0 && i == numPaths-1 {
p = strings.Replace(suggestions[0].Text, "@", "", 1)
filteredPaths = append(filteredPaths, p)
} else if projectPaths.ActivePaths[p] {
filteredPaths = append(filteredPaths, p)
}
}
if len(filteredPaths) > 0 {
args := []string{"load"}
args = append(args, filteredPaths...)
args = append(args, "-r")
fmt.Println()
_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing command: %v\n", err)
}
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return
}
}
// Handle commands
if lastBackslashIndex != -1 {
cmdString := strings.TrimSpace(lastLine[lastBackslashIndex+1:])
if cmdString == "" {
return
}
res := execWithInput(execWithInputParams{
cmdString: cmdString,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
if res.shouldReturn {
return
}
condensedInput = res.condensedInput
trimmedInput = res.trimmedInput
} else if len(lines) == 1 {
// Check for likely accidental command inputs (with no backslash) and confirm with user
var allCommands []string
for replCmd := range lib.ReplCmdAliases {
allCommands = append(allCommands, replCmd)
}
for _, config := range term.CliCommands {
if config.Repl {
allCommands = append(allCommands, config.Cmd)
}
}
// Only suggest commands if they're close enough matches
maybeCmds := findSimilarCommands(lastLine, allCommands)
if len(maybeCmds) > 0 {
res := suggestCmds(maybeCmds, getPromptOpt(lastLine))
if res.shouldReturn {
return
}
matchedCmd := res.matchedCmd
if matchedCmd != "" {
res := execWithInput(execWithInputParams{
cmdString: matchedCmd,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
if res.shouldReturn {
return
}
condensedInput = res.condensedInput
trimmedInput = res.trimmedInput
}
}
}
// Handle non-command input based on mode
if lib.CurrentReplState.Mode == lib.ReplModeTell {
fmt.Println()
args := []string{"tell", trimmedInput}
var err error
_, err = lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing tell: %v\n", err)
}
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
fmt.Println()
args := []string{"chat", trimmedInput}
output, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing chat: %v\n", err)
}
replacer := strings.NewReplacer("'", "", "\"", "", "*", "", "`", "", "_", "")
output = replacer.Replace(output)
rx := regexp.MustCompile(`(?i)(switch|start|continue|begin|change|chang|move|proceed|go|transition)(ing)?( to | with | into )?(tell|implementation|coding|development)( mode)?`)
if rx.MatchString(output) {
fmt.Println()
res, err := term.ConfirmYesNo("Switch to tell mode for implementation?")
if err != nil {
color.New(term.ColorHiRed).Printf("Error confirming yes/no: %v\n", err)
}
if res {
lib.CurrentReplState.Mode = lib.ReplModeTell
lib.WriteState()
fmt.Println()
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" ⚡️ Tell mode is enabled ")
fmt.Println()
fmt.Println("Now that you're in tell mode, you can either begin the implementation based on the conversation so far, or you can send another prompt to begin the implementation with additional information or instructions.")
fmt.Println()
beginImplOpt := "Begin implementation"
anotherPromptOpt := "Send another prompt"
sel, err := term.SelectFromList("What would you like to do?", []string{beginImplOpt, anotherPromptOpt})
if err != nil {
color.New(term.ColorHiRed).Printf("Error selecting from list: %v\n", err)
}
if sel == beginImplOpt {
fmt.Println()
args := []string{"tell", "--from-chat"}
_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing tell: %v\n", err)
}
}
}
}
}
fmt.Println()
}
func completer(in prompt.Document) ([]prompt.Suggest, pstrings.RuneNumber, pstrings.RuneNumber) {
// Don't show suggestions if we're navigating history
if currentPrompt.IsNavigatingHistory() {
return []prompt.Suggest{}, 0, 0
}
endIndex := in.CurrentRuneIndex()
lines := strings.Split(in.Text, "\n")
currentLineNum := strings.Count(in.TextBeforeCursor(), "\n")
// Don't show suggestions if we're not on the last line
if currentLineNum < len(lines)-1 {
return []prompt.Suggest{}, 0, 0
}
lastLine := lines[len(lines)-1]
if strings.TrimSpace(lastLine) == "" && len(lines) > 1 {
lastLine = lines[len(lines)-2]
}
// Handle plandex/pdx command prefix
if strings.HasPrefix(lastLine, "plandex ") || strings.HasPrefix(lastLine, "pdx ") {
parts := strings.Fields(lastLine)
var prefix string
if len(parts) > 1 {
prefix = parts[len(parts)-1]
}
startIndex := endIndex - pstrings.RuneNumber(len(prefix))
suggestions := []prompt.Suggest{}
for _, config := range term.CliCommands {
suggestions = append(suggestions, prompt.Suggest{
Text: config.Cmd,
Description: config.Desc,
})
}
filtered := prompt.FilterFuzzy(suggestions, prefix, true)
return filtered, startIndex, endIndex
}
// Find the last valid \ or @ in the current line
lastBackslashIndex := -1
lastAtIndex := -1
// Helper function to check if character at index is valid (start of line or after space)
isValidPosition := func(str string, index int) bool {
if index <= 0 {
return true // Start of line
}
return unicode.IsSpace(rune(str[index-1])) // After whitespace
}
// Find last valid backslash
for i := len(lastLine) - 1; i >= 0; i-- {
if lastLine[i] == '\\' && isValidPosition(lastLine, i) {
lastBackslashIndex = i
break
}
}
// Find last valid @
for i := len(lastLine) - 1; i >= 0; i-- {
if lastLine[i] == '@' && isValidPosition(lastLine, i) {
lastAtIndex = i
break
}
}
var w string
var startIndex pstrings.RuneNumber
if lastBackslashIndex == -1 && lastAtIndex == -1 {
return []prompt.Suggest{}, 0, 0
}
// Use the rightmost special character
if lastBackslashIndex > lastAtIndex {
// Get everything after the last backslash
w = lastLine[lastBackslashIndex:]
startIndex = endIndex - pstrings.RuneNumber(len(w))
} else if lastAtIndex != -1 {
// Get everything after the last @
w = lastLine[lastAtIndex:]
startIndex = endIndex - pstrings.RuneNumber(len(w))
}
// Verify this is at the end of the line (allowing for trailing spaces)
if !strings.HasSuffix(strings.TrimSpace(lastLine), strings.TrimSpace(w)) {
return []prompt.Suggest{}, 0, 0
}
wTrimmed := strings.TrimSpace(strings.TrimPrefix(w, "\\"))
parts := strings.Split(wTrimmed, " ")
wCmd := parts[0]
// For commands, verify it starts with an actual command
if strings.HasPrefix(w, "\\") {
isValidCommand := false
for _, config := range term.CliCommands {
if !config.Repl {
continue
}
if strings.HasPrefix(config.Cmd, wCmd) ||
(config.Alias != "" && strings.HasPrefix(config.Alias, wCmd)) {
isValidCommand = true
break
}
}
// Also check built-in REPL commands
if strings.HasPrefix("quit", wCmd) ||
strings.HasPrefix("multi", wCmd) ||
strings.HasPrefix("tell", wCmd) ||
strings.HasPrefix("chat", wCmd) ||
strings.HasPrefix("send", wCmd) ||
strings.HasPrefix("run", wCmd) {
isValidCommand = true
}
if !isValidCommand && wCmd != "" {
return []prompt.Suggest{}, 0, 0
}
}
fuzzySuggestions := prompt.FilterFuzzy(getSuggestions(), w, true)
prefixMatches := prompt.FilterHasPrefix(getSuggestions(), w, true)
runFilteredFuzzy := []prompt.Suggest{}
runFilteredPrefixMatches := []prompt.Suggest{}
for _, s := range fuzzySuggestions {
if strings.HasPrefix(s.Text, "\\run ") {
if wCmd == "run" {
runFilteredFuzzy = append(runFilteredFuzzy, s)
}
} else {
runFilteredFuzzy = append(runFilteredFuzzy, s)
}
}
for _, s := range prefixMatches {
if strings.HasPrefix(s.Text, "\\run ") {
if wCmd == "run" {
runFilteredPrefixMatches = append(runFilteredPrefixMatches, s)
}
} else {
runFilteredPrefixMatches = append(runFilteredPrefixMatches, s)
}
}
fuzzySuggestions = runFilteredFuzzy
prefixMatches = runFilteredPrefixMatches
loadFilteredFuzzy := []prompt.Suggest{}
loadFilteredPrefixMatches := []prompt.Suggest{}
for _, s := range fuzzySuggestions {
if strings.HasPrefix(s.Text, "\\load ") {
if wCmd == "load" {
loadFilteredFuzzy = append(loadFilteredFuzzy, s)
}
} else {
loadFilteredFuzzy = append(loadFilteredFuzzy, s)
}
}
for _, s := range prefixMatches {
if strings.HasPrefix(s.Text, "\\load ") {
if wCmd == "load" {
loadFilteredPrefixMatches = append(loadFilteredPrefixMatches, s)
}
} else {
loadFilteredPrefixMatches = append(loadFilteredPrefixMatches, s)
}
}
fuzzySuggestions = loadFilteredFuzzy
prefixMatches = loadFilteredPrefixMatches
if strings.TrimSpace(w) != "\\" {
sort.Slice(prefixMatches, func(i, j int) bool {
iTxt := prefixMatches[i].Text
jTxt := prefixMatches[j].Text
if iTxt == "\\chat" || iTxt == "\\tell" || iTxt == "\\multi" || iTxt == "\\quit" || iTxt == "\\send" || iTxt == "\\run" {
return true
}
if jTxt == "\\chat" || jTxt == "\\tell" || jTxt == "\\multi" || jTxt == "\\quit" || jTxt == "\\send" || jTxt == "\\run" {
return false
}
return prefixMatches[i].Text < prefixMatches[j].Text
})
}
if len(prefixMatches) > 0 {
// Remove prefix matches from fuzzy results to avoid duplicates
prefixMatchSet := make(map[string]bool)
for _, s := range prefixMatches {
prefixMatchSet[s.Text] = true
}
nonPrefixFuzzy := make([]prompt.Suggest, 0)
for _, s := range fuzzySuggestions {
if !prefixMatchSet[s.Text] {
nonPrefixFuzzy = append(nonPrefixFuzzy, s)
}
}
fuzzySuggestions = append(prefixMatches, nonPrefixFuzzy...)
}
var aliasMatch string
if lib.ReplCmdAliases[wTrimmed] != "" {
aliasMatch = "\\" + lib.ReplCmdAliases[wTrimmed]
} else {
for _, s := range term.CliCommands {
if s.Alias == wTrimmed {
aliasMatch = "\\" + s.Cmd
break
}
}
}
if aliasMatch != "" {
// put the suggestion with the alias match at the beginning
var matched prompt.Suggest
found := false
for _, s := range fuzzySuggestions {
if s.Text == aliasMatch {
matched = s
found = true
break
}
}
if found {
newSuggestions := []prompt.Suggest{}
newSuggestions = append(newSuggestions, matched)
for _, s := range fuzzySuggestions {
if s.Text != aliasMatch {
newSuggestions = append(newSuggestions, s)
}
}
fuzzySuggestions = newSuggestions
}
}
return fuzzySuggestions, startIndex, endIndex
}
type replWelcomeParams struct {
afterNew bool
isHelp bool
printAutoFn func()
printModelFn func()
packName string
config *shared.PlanConfig
}
func replWelcome(params replWelcomeParams) {
// print REPL welcome message and basic info
// have to make these requests serially in case re-authentication is needed
afterNew := params.afterNew
isHelp := params.isHelp
printAutoFn := params.printAutoFn
printModelFn := params.printModelFn
packName := params.packName
plan, apiErr := api.Client.GetPlan(lib.CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan: %v", apiErr.Msg)
}
config := params.config
if config == nil {
config, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan config: %v", apiErr.Msg)
}
}
currentBranchesByPlanId, err := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{
CurrentBranchByPlanId: map[string]string{
lib.CurrentPlanId: lib.CurrentBranch,
},
})
if err != nil {
term.OutputErrorAndExit("Error getting current branches: %v", err)
}
term.StopSpinner()
if !afterNew {
fmt.Println()
}
color.New(color.FgHiWhite, color.BgBlue, color.Bold).Print(" 👋 Welcome to Plandex ")
versionStr := version.Version
if versionStr != "development" {
color.New(color.FgHiWhite, color.BgHiBlack).Printf(" v%s ", versionStr)
}
fmt.Println()
fmt.Println()
fmt.Println(lib.GetCurrentPlanTable(plan, currentBranchesByPlanId, nil))
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(false)
var contextMode string
if config.AutoLoadContext {
contextMode = "auto"
} else {
contextMode = "manual"
}
filesStr := "%s for loading files into context"
if contextMode == "auto" {
filesStr += " manually (optional)"
}
filesStr += "\n"
color.New(color.FgHiWhite).Printf("%s for commands\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\"))
color.New(color.FgHiWhite).Printf(filesStr, color.New(term.ColorHiCyan, color.Bold).Sprint("@"))
color.New(color.FgHiWhite).Printf("%s (\\h) for help\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\help"))
color.New(color.FgHiWhite).Printf("%s (\\q) to exit\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\quit"))
fmt.Println()
if printAutoFn != nil {
printAutoFn()
} else {
printAutoModeTable(config)
}
color.New(color.FgHiWhite).Printf("%s to change auto mode\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\set-auto"))
color.New(color.FgHiWhite).Printf("%s to see config\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\config"))
color.New(color.FgHiWhite).Printf("%s to customize config\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\set-config"))
fmt.Println()
if printModelFn != nil {
printModelFn()
} else {
printModelPackTable(packName)
}
color.New(color.FgHiWhite).Printf("%s to see model details\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\models"))
color.New(color.FgHiWhite).Printf("%s to change models\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\set-model"))
showReplMode()
showMultiLineMode()
fmt.Println()
if !isHelp {
if lib.CurrentReplState.Mode == lib.ReplModeTell {
color.New(color.FgHiWhite, color.BgBlue, color.Bold).Println(" Describe a coding task 👇 ")
} else {
color.New(color.FgHiWhite, color.BgBlue, color.Bold).Println(" Ask a question or chat 👇 ")
}
fmt.Println()
}
}
func replHelp() {
replWelcome(replWelcomeParams{
afterNew: false,
isHelp: true,
})
term.PrintHelpAllCommands()
}
func showReplMode() {
fmt.Println()
if lib.CurrentReplState.Mode == lib.ReplModeTell {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" ⚡️ Tell mode is enabled ")
color.New(color.FgHiWhite).Printf("%s (\\ch) switch to chat mode to chat without writing code or making changes\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\chat"))
} else if lib.CurrentReplState.Mode == lib.ReplModeChat {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" 💬 Chat mode is enabled ")
color.New(color.FgHiWhite).Printf("%s (\\t) switch to tell mode to start writing code and implementing tasks\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\tell"))
}
fmt.Println()
}
func showMultiLineMode() {
if lib.CurrentReplState.IsMulti {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" 🔢 Multi-line mode is enabled ")
fmt.Printf("%s to exit multi-line mode\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\multi"))
fmt.Printf("%s for line breaks\n", color.New(term.ColorHiCyan, color.Bold).Sprint("enter"))
fmt.Printf("%s to send prompt\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\send"))
} else {
color.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(" 1️⃣ Multi-line mode is disabled ")
fmt.Printf("%s for multi-line editing mode\n", color.New(term.ColorHiCyan, color.Bold).Sprint("\\multi"))
fmt.Printf("%s to send prompt\n", color.New(term.ColorHiCyan, color.Bold).Sprint("enter"))
}
}
func parseCommand(in string) (string, string) {
in = strings.TrimSpace(in)
lines := strings.Split(in, "\n")
lastLine := lines[len(lines)-1]
lastLine = strings.TrimSpace(lastLine)
input := strings.TrimSpace(in)
if input == "" {
return "", ""
}
// Handle plandex/pdx command prefix
if strings.HasPrefix(lastLine, "plandex ") || strings.HasPrefix(lastLine, "pdx ") {
return lastLine, lastLine
}
// Find the last \ or @ in the last line
lastBackslashIndex := strings.LastIndex(lastLine, "\\")
lastAtIndex := strings.LastIndex(lastLine, "@")
suggestions, _, _ := completer(prompt.Document{Text: in})
// Handle file references
if lastAtIndex != -1 && lastAtIndex > lastBackslashIndex {
paths := strings.Split(lastLine, "@")
split2 := strings.SplitN(lastLine, "@", 2)
numPaths := len(paths)
filteredPaths := []string{}
for i, path := range paths {
p := strings.TrimSpace(path)
if i == 0 {
// text before the @
continue
}
if (p == "" || !projectPaths.ActivePaths[p]) && len(suggestions) > 0 && i == numPaths-1 {
p = strings.Replace(suggestions[0].Text, "@", "", 1)
filteredPaths = append(filteredPaths, p)
} else if projectPaths.ActivePaths[p] {
filteredPaths = append(filteredPaths, p)
}
}
if len(filteredPaths) > 0 {
res := ""
for _, p := range filteredPaths {
res += "@" + p + " "
}
return res, split2[1]
}
}
// Handle commands
if lastBackslashIndex != -1 {
cmdString := strings.TrimSpace(lastLine[lastBackslashIndex+1:])
if cmdString == "" {
return "", ""
}
// Split into command and args
parts := strings.Fields(cmdString)
cmd := parts[0]
args := parts[1:]
// Handle built-in REPL commands
switch cmd {
case "quit", lib.ReplCmdAliases["quit"]:
return "\\quit", "\\" + cmdString
case "help", lib.ReplCmdAliases["help"]:
return "\\help", "\\" + cmdString
case "multi", lib.ReplCmdAliases["multi"]:
return "\\multi", "\\" + cmdString
case "send", lib.ReplCmdAliases["send"]:
return "\\send", "\\" + cmdString
case "tell", lib.ReplCmdAliases["tell"]:
return "\\tell", "\\" + cmdString
case "chat", lib.ReplCmdAliases["chat"]:
return "\\chat", "\\" + cmdString
case "run", lib.ReplCmdAliases["run"]:
return "\\run", "\\" + cmdString
default:
// Check CLI commands
var matchedCmd string
for _, config := range term.CliCommands {
if (cmd == config.Cmd || (config.Alias != "" && cmd == config.Alias)) && config.Repl {
matchedCmd = config.Cmd
break
}
}
if matchedCmd == "" {
for _, config := range term.CliCommands {
if strings.HasPrefix(config.Cmd, cmd) && config.Repl {
matchedCmd = config.Cmd
break
}
}
}
if matchedCmd != "" {
res := matchedCmd
if len(args) > 0 {
res += " " + strings.Join(args, " ")
}
return res, "\\" + cmdString
}
}
}
return "", ""
}
func isFileInProjectPaths(filePath string) bool {
// Convert to absolute path
absPath, err := filepath.Abs(filePath)
if err != nil {
return false
}
// Check if file is within any project path
for path := range projectPaths.ActivePaths {
projectAbs, err := filepath.Abs(path)
if err != nil {
continue
}
if strings.HasPrefix(absPath, projectAbs) {
return true
}
}
return false
}
func handleRunCommand(args []string) error {
if len(args) != 1 {
return fmt.Errorf("run command requires exactly one file path argument")
}
filePath := args[0]
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return fmt.Errorf("file does not exist: %s", filePath)
}
// Build command based on current mode
var cmdArgs []string
if lib.CurrentReplState.Mode == lib.ReplModeTell {
cmdArgs = []string{"tell", "-f", filePath}
} else {
cmdArgs = []string{"chat", "-f", filePath}
}
// Execute the command
_, err := lib.ExecPlandexCommand(cmdArgs)
if err != nil {
return fmt.Errorf("error executing command: %v", err)
}
return nil
}
func getPromptOpt(cmd string) string {
asPrompt := cmd
if len(asPrompt) > 20 {
asPrompt = asPrompt[:20] + "..."
}
return fmt.Sprintf("Send '%s' as a prompt to the AI model", asPrompt)
}
type suggestCmdsResult struct {
shouldReturn bool
matchedCmd string
}
func suggestCmds(cmds []string, promptOpt string) suggestCmdsResult {
var matchedCmd string
fmt.Println()
opts := []string{}
for _, match := range cmds {
opts = append(opts, "\\"+match)
}
opts = append(opts, cancelOpt, promptOpt)
sel, err := term.SelectFromList("🤔 Did you mean to type one of these commands?", opts)
if err != nil {
color.New(term.ColorHiRed).Printf("Error selecting from list: %v\n", err)
}
if sel == cancelOpt {
return suggestCmdsResult{shouldReturn: true}
} else if sel != promptOpt {
matchedCmd = strings.Replace(sel, "\\", "", 1)
}
return suggestCmdsResult{matchedCmd: matchedCmd}
}
type execWithInputParams struct {
cmdString string
in string
lastBackslashIndex int
preservedBuffer string
p *prompt.Prompt
lastLine string
condensedInput string
trimmedInput string
lines []string
suggestions []prompt.Suggest
}
type execWithInputResult struct {
shouldReturn bool
condensedInput string
trimmedInput string
}
func execWithInput(params execWithInputParams) execWithInputResult {
cmdString := params.cmdString
in := params.in
lastBackslashIndex := params.lastBackslashIndex
preservedBuffer := params.preservedBuffer
lastLine := params.lastLine
p := params.p
condensedInput := params.condensedInput
trimmedInput := params.trimmedInput
lines := params.lines
suggestions := params.suggestions
// Split into command and args
parts := strings.Fields(cmdString)
cmd := parts[0]
args := parts[1:]
var fuzzyNEQCheckCmds []string
for replCmd := range lib.ReplCmdAliases {
if replCmd != cmd {
fuzzyNEQCheckCmds = append(fuzzyNEQCheckCmds, replCmd)
}
}
for _, config := range term.CliCommands {
if !config.Repl {
continue
}
if config.Cmd != cmd {
fuzzyNEQCheckCmds = append(fuzzyNEQCheckCmds, config.Cmd)
}
}
fuzzyNEQMatches := findSimilarCommands(cmd, fuzzyNEQCheckCmds)
// Handle built-in REPL commands
switch {
case cmd == "quit" || cmd == lib.ReplCmdAliases["quit"]:
lib.WriteHistory(in)
os.Exit(0)
case cmd == "help" || cmd == lib.ReplCmdAliases["help"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
replHelp()
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "multi" || cmd == lib.ReplCmdAliases["multi"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
fmt.Println()
lib.CurrentReplState.IsMulti = !lib.CurrentReplState.IsMulti
showMultiLineMode()
lib.WriteState()
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "send" || cmd == lib.ReplCmdAliases["send"]:
condensedSplit := strings.Split(condensedInput, "\\s")
condensedInput = strings.TrimSpace(condensedSplit[0])
condensedInput = strings.TrimSpace(condensedInput)
trimmedSplit := strings.Split(trimmedInput, "\\s")
trimmedInput = strings.TrimSpace(trimmedSplit[0])
trimmedInput = strings.TrimSpace(trimmedInput)
if condensedInput == "" {
fmt.Println()
fmt.Println("🤷♂️ No prompt to send")
fmt.Println()
return execWithInputResult{shouldReturn: true}
}
case cmd == "tell" || cmd == lib.ReplCmdAliases["tell"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
lib.CurrentReplState.Mode = lib.ReplModeTell
lib.WriteState()
showReplMode()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "chat" || cmd == lib.ReplCmdAliases["chat"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
lib.CurrentReplState.Mode = lib.ReplModeChat
lib.WriteState()
showReplMode()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
case cmd == "run" || cmd == lib.ReplCmdAliases["run"]:
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
fmt.Println()
if err := handleRunCommand(args); err != nil {
color.New(term.ColorHiRed).Printf("Run command failed: %v\n", err)
}
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
return execWithInputResult{shouldReturn: true}
default:
// Check CLI commands
var matchedCmd string
for _, config := range term.CliCommands {
if (cmd == config.Cmd || (config.Alias != "" && cmd == config.Alias)) && config.Repl {
matchedCmd = config.Cmd
break
}
}
if matchedCmd == "" && len(suggestions) > 0 {
matchedCmd = strings.Replace(suggestions[0].Text, "\\", "", 1)
return execWithInput(execWithInputParams{
cmdString: matchedCmd,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
}
if matchedCmd == "" {
promptOpt := getPromptOpt(cmd)
if len(fuzzyNEQMatches) > 0 {
res := suggestCmds(fuzzyNEQMatches, promptOpt)
if res.shouldReturn {
return execWithInputResult{shouldReturn: true}
}
matchedCmd = res.matchedCmd
return execWithInput(execWithInputParams{
cmdString: matchedCmd,
in: condensedInput,
lastBackslashIndex: lastBackslashIndex,
preservedBuffer: preservedBuffer,
p: p,
lastLine: lastLine,
condensedInput: condensedInput,
trimmedInput: trimmedInput,
lines: lines,
suggestions: suggestions,
})
} else if len(lines) == 1 && strings.HasPrefix(trimmedInput, "\\") {
showCmdsOpt := "Show available commands"
opts := []string{cancelOpt, showCmdsOpt, promptOpt}
sel, err := term.SelectFromList("🤔 Couldn't find a matching command. What do you want to do?", opts)
if err != nil {
color.New(term.ColorHiRed).Printf("Error selecting from list: %v\n", err)
}
if sel == cancelOpt {
return execWithInputResult{shouldReturn: true}
} else if sel == showCmdsOpt {
replHelp()
fmt.Println()
return execWithInputResult{shouldReturn: true}
}
}
}
if matchedCmd != "" {
// fmt.Println("> plandex " + config.Cmd)
if lastBackslashIndex > 0 {
preservedBuffer += lastLine[:lastBackslashIndex]
}
fmt.Println()
execArgs := []string{matchedCmd}
if matchedCmd == "continue" && chatOnly {
execArgs = append(execArgs, "--chat")
}
execArgs = append(execArgs, args...)
_, err := lib.ExecPlandexCommandWithParams(execArgs, lib.ExecPlandexCommandParams{
SessionId: sessionId,
})
if err != nil {
color.New(term.ColorHiRed).Printf("Error executing command: %v\n", err)
}
fmt.Println()
if preservedBuffer != "" {
p.InsertTextMoveCursor(preservedBuffer, true)
}
if strings.HasPrefix(matchedCmd, "set-auto") || strings.HasPrefix(matchedCmd, "set-config") {
term.StartSpinner("")
setReplConfig()
term.StopSpinner()
}
return execWithInputResult{shouldReturn: true}
}
}
return execWithInputResult{
condensedInput: condensedInput,
trimmedInput: trimmedInput,
}
}
func findSimilarCommands(input string, commands []string) []string {
input = strings.TrimSpace(input)
input = strings.ToLower(input)
input = strings.Trim(input, "/")
// Get ranked matches
ranks := fuzzy.RankFind(input, commands)
// Filter strictly by distance
var filtered []string
for _, rank := range ranks {
// include if either is a substring of the other
if strings.Contains(rank.Target, input) || strings.Contains(input, rank.Target) {
filtered = append(filtered, rank.Target)
continue
}
// Normalize threshold based on command length
maxLen := len(input)
if len(rank.Target) > maxLen {
maxLen = len(rank.Target)
}
threshold := 4 // Base threshold
if maxLen < 5 {
threshold = 1 // Stricter for very short commands
}
if rank.Distance <= threshold {
filtered = append(filtered, rank.Target)
}
}
return filtered
}
================================================
FILE: app/cli/cmd/revoke.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/term"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var revokeCmd = &cobra.Command{
Use: "revoke [email]",
Short: "Revoke an invite or remove a user from the org",
Run: revoke,
Args: cobra.MaximumNArgs(1),
}
func init() {
RootCmd.AddCommand(revokeCmd)
}
func revoke(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
email := ""
if len(args) > 0 {
email = args[0]
}
var userResp *shared.ListUsersResponse
var pendingInvites []*shared.Invite
errCh := make(chan error)
term.StartSpinner("")
go func() {
var err *shared.ApiError
userResp, err = api.Client.ListUsers()
if err != nil {
errCh <- fmt.Errorf("error fetching users: %s", err.Msg)
return
}
errCh <- nil
}()
go func() {
var err *shared.ApiError
pendingInvites, err = api.Client.ListPendingInvites()
if err != nil {
errCh <- fmt.Errorf("error fetching pending invites: %s", err.Msg)
return
}
errCh <- nil
}()
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
term.StopSpinner()
term.OutputErrorAndExit(err.Error())
}
}
term.StopSpinner()
type userInfo struct {
Id string
IsInvite bool
}
emailToUserMap := make(map[string]userInfo)
labelToEmail := make(map[string]string)
// Combine users and invites for selection
combinedList := make([]string, 0, len(userResp.Users)+len(pendingInvites))
for _, user := range userResp.Users {
label := fmt.Sprintf("%s <%s>", user.Name, user.Email)
labelToEmail[label] = user.Email
combinedList = append(combinedList, label)
emailToUserMap[user.Email] = userInfo{Id: user.Id, IsInvite: false}
}
for _, invite := range pendingInvites {
label := fmt.Sprintf("%s <%s> (invite pending)", invite.Name, invite.Email)
labelToEmail[label] = invite.Email
combinedList = append(combinedList, label)
emailToUserMap[invite.Email] = userInfo{Id: invite.Id, IsInvite: true}
}
if email == "" {
selected, err := term.SelectFromList("Select a user or invite:", combinedList)
if err != nil {
term.OutputErrorAndExit("Error selecting item to revoke: %v", err)
}
email = labelToEmail[selected]
}
if email == "" {
term.OutputErrorAndExit("No user or invite selected")
}
// Determine if email belongs to a user or an invite and revoke accordingly
if userInfo, exists := emailToUserMap[email]; exists {
if userInfo.IsInvite {
if err := api.Client.DeleteInvite(userInfo.Id); err != nil {
term.OutputErrorAndExit("Failed to revoke invite: %v", err)
}
fmt.Println("✅ Invite revoked")
} else {
if err := api.Client.DeleteUser(userInfo.Id); err != nil {
term.OutputErrorAndExit("Failed to remove user: %v", err)
}
fmt.Println("✅ User removed")
}
} else {
term.OutputErrorAndExit("No user or pending invite found for email '%s'", email)
}
}
================================================
FILE: app/cli/cmd/rewind.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/term"
"regexp"
"strconv"
"strings"
"time"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var rewindCmd = &cobra.Command{
Use: "rewind [steps-or-sha]",
Aliases: []string{"rw"},
Short: "Rewind plan state and optionally revert project files",
Long: `Rewind plan state and optionally revert project files to match.
You can pass a "steps" number or a commit sha. If a steps number is passed,
the plan will be rewound that many steps. If a commit sha is passed, the
plan will be rewound to that commit. If neither is passed, you will be
prompted to select a step from the history.
By default, you will be prompted whether to revert project files to match
the rewound plan state. You can use --revert to automatically revert
files, or configure this behavior with the 'auto-revert' plan config setting.
If project files have changes, you will always be prompted before updating.`,
Args: cobra.MaximumNArgs(1),
Run: rewind,
}
var revert bool
var skipRevert bool
func init() {
RootCmd.AddCommand(rewindCmd)
rewindCmd.Flags().BoolVar(&revert, "revert", false, "Also revert project files to match plan state")
rewindCmd.Flags().BoolVar(&skipRevert, "skip-revert", false, "Skip reverting project files to match plan state")
rewindCmd.Flags().BoolVar(&autoCommit, "commit", false, "Commit changes to git when --revert is passed")
rewindCmd.Flags().BoolVar(&skipCommit, "skip-commit", false, "Skip committing changes to git when --revert is passed")
}
func rewind(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
if skipRevert && revert {
term.OutputErrorAndExit("Cannot pass both --revert and --skip-revert")
}
// Get logs
term.StartSpinner("")
logsRes, apiErr := api.Client.ListLogs(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error getting logs: %v", apiErr)
}
var targetSha string
var steps int
var isSha bool
parseGitLog := func(log string) (string, string) {
// Parse the log entry
lines := strings.Split(log, "\n")
if len(lines) < 2 {
return "", ""
}
// Extract sha and timestamp from first line
parts := strings.Split(lines[0], "|")
if len(parts) < 2 {
return "", ""
}
shaLine := strings.TrimSpace(parts[0])
sha := regexp.MustCompile(`📝 Update (\w+)`).FindStringSubmatch(shaLine)[1]
msg := strings.TrimSpace(lines[1])
return sha, msg
}
logEntries := strings.Split(logsRes.Body, "\n\n")
if len(args) == 0 {
// No arguments - show selection list
options := make([]string, 0)
for _, log := range logEntries {
if log == "" {
continue
}
sha, msg := parseGitLog(log)
if sha == "" {
continue
}
// Format the two-line option
option := fmt.Sprintf("%s | %s", sha, formatLogMessage(msg))
options = append(options, option)
}
term.StopSpinner()
selected, err := term.SelectFromList("Select step to rewind to:", options)
if err != nil {
term.OutputErrorAndExit("Error selecting step: %v", err)
}
// Parse selected option to get sha
parts := strings.Split(selected, " | ")
if len(parts) < 2 {
term.OutputErrorAndExit("Invalid selection")
}
targetSha = parts[0]
isSha = true
} else {
// Arguments provided - use direct rewind logic
stepsOrSha := args[0]
steps, err := strconv.Atoi(stepsOrSha)
if err == nil && steps > 0 && steps < 999 {
// Rewind by the specified number of steps
targetSha = logsRes.Shas[steps]
} else if sha := stepsOrSha; sha != "" {
// Rewind to the specified Sha
targetSha = sha
isSha = true
} else {
term.OutputErrorAndExit("Invalid steps or sha. Steps must be a positive integer, and sha must be a valid commit hash.")
}
}
doRewind := func() {
var updatedModelSettings bool
term.StartSpinner("")
_, apiErr := api.Client.RewindPlan(lib.CurrentPlanId, lib.CurrentBranch, shared.RewindPlanRequest{
Sha: targetSha,
})
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error rewinding plan: %v", apiErr)
}
var err error
updatedModelSettings, err = lib.SaveLatestPlanModelSettingsIfNeeded()
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error saving model settings: %v", err)
}
var msg string
if isSha {
msg = "✅ Rewound to " + targetSha
} else {
postfix := "s"
if steps == 1 {
postfix = ""
}
msg = fmt.Sprintf("✅ Rewound %d step%s to %s", steps, postfix, targetSha)
}
fmt.Println(msg)
if updatedModelSettings {
fmt.Println()
fmt.Println("🧠 Model settings file updated → ", lib.GetPlanModelSettingsPath(lib.CurrentPlanId))
}
fmt.Println()
}
printNoChanges := func() {
fmt.Println("🙅♂️ No project files were modified")
fmt.Println()
}
printCmds := func() {
term.PrintCmds("", "log", "continue")
}
// get the timestamp of the target sha
var targetShaTimestamp time.Time
for _, log := range logEntries {
if log == "" {
continue
}
sha, _ := parseGitLog(log)
if sha == targetSha {
timestamp, err := lib.GetGitLogTimestamp(log)
if err != nil {
continue
}
targetShaTimestamp = timestamp
break
}
}
if targetShaTimestamp.IsZero() {
term.OutputErrorAndExit("Error getting timestamp for target sha: " + targetSha)
}
// Get current plan state to check for undone applies
term.StartSpinner("")
currentState, apiErr := api.Client.GetCurrentPlanState(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error getting plan state: %v", apiErr)
}
// Get list of applies that will be undone
undonePlanApplies := lib.GetUndonePlanApplies(currentState, targetShaTimestamp)
// If no applies are being undone, skip revert entirely
if len(undonePlanApplies) == 0 {
// Just do the rewind, no need for any file operations
doRewind()
printNoChanges()
printCmds()
return
}
// Get the set of affected file paths
// Determine if we should revert based on flag/config
var shouldRevert bool
needsPrompt := true
var config *shared.PlanConfig
if cmd.Flags().Changed("revert") || cmd.Flags().Changed("skip-revert") {
if skipRevert {
shouldRevert = false
} else {
shouldRevert = revert
}
needsPrompt = false
} else {
config, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan config: %v", apiErr)
}
shouldRevert = config.AutoRevertOnRewind
needsPrompt = false
}
var targetState *shared.CurrentPlanState
var analysis *lib.RewindAnalysis
if shouldRevert || needsPrompt {
// First preview the rewind to check for conflicts
targetState, apiErr = api.Client.GetCurrentPlanStateAtSha(lib.CurrentPlanId, targetSha)
if apiErr != nil {
term.OutputErrorAndExit("Error previewing rewind: %v", apiErr)
}
if targetState == nil {
term.OutputErrorAndExit("Error previewing rewind - no state found at sha: " + targetSha)
}
var err error
analysis, err = lib.AnalyzeRewind(targetState, currentState)
if err != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error analyzing rewind: %v", err)
}
if len(analysis.RequiredChanges) == 0 {
// No file changes - proceed with rewind
doRewind()
printNoChanges()
printCmds()
return
}
// Show file differences
term.StopSpinner()
// Group changes by type for display
toAdd := make([]string, 0)
toRemove := make([]string, 0)
toModify := make([]string, 0)
for path, content := range analysis.RequiredChanges {
if content == "" {
toRemove = append(toRemove, path)
} else if currentState.ContextsByPath[path] == nil {
toAdd = append(toAdd, path)
} else {
toModify = append(toModify, path)
}
}
if needsPrompt || len(analysis.Conflicts) > 0 {
s := "files"
if len(analysis.RequiredChanges) == 1 {
s = "file"
}
fmt.Printf("⏪ %d local project %s differ from target plan state\n", len(analysis.RequiredChanges), s)
fmt.Println()
fmt.Printf("Reverting the %s will make these changes locally 👇\n", s)
fmt.Println()
if len(toAdd) > 0 {
fmt.Println("To add:")
for _, path := range toAdd {
fmt.Printf(" • %s\n", path)
}
fmt.Println()
}
if len(toRemove) > 0 {
fmt.Println("To remove:")
for _, path := range toRemove {
fmt.Printf(" • %s\n", path)
}
fmt.Println()
}
if len(toModify) > 0 {
fmt.Println("To update:")
for _, path := range toModify {
fmt.Printf(" • %s\n", path)
}
fmt.Println()
}
if len(analysis.Conflicts) > 0 {
// Always prompt if there are conflicts
s := " These project files have"
if len(analysis.Conflicts) == 1 {
s = " A project file has"
}
fmt.Printf("⚠️ %s been updated outside of Plandex since the latest apply:\n", s)
for path := range analysis.Conflicts {
fmt.Printf(" • %s\n", path)
}
fmt.Println()
fmt.Println("If you revert, you will lose those changes.")
fmt.Println()
s = "files"
if len(analysis.RequiredChanges) == 1 {
s = "file"
}
options := []string{
fmt.Sprintf("Revert project %s to match rewound plan state (overwrite changes)", s),
fmt.Sprintf("Rewind plan, but skip reverting project %s", s),
"Cancel rewind",
}
selected, err := term.SelectFromList("What do you want to do?", options)
if err != nil {
term.OutputErrorAndExit("Error getting user input: %v", err)
}
switch selected {
case options[0]:
shouldRevert = true
case options[1]:
shouldRevert = false
case options[2]:
os.Exit(0)
}
needsPrompt = false
}
}
}
// Now that we've handled the file state decision, perform the actual rewind
doRewind()
didRevert := false
if shouldRevert || needsPrompt {
if needsPrompt {
term.StopSpinner()
s := "files"
if len(analysis.RequiredChanges) == 1 {
s = "file"
}
confirmed, err := term.ConfirmYesNo(fmt.Sprintf("Revert project %s to match rewound plan state?", s))
if err != nil {
term.OutputErrorAndExit("Error getting user confirmation: %v", err)
}
shouldRevert = confirmed
}
if shouldRevert && len(analysis.RequiredChanges) > 0 {
term.StartSpinner("")
err := lib.ApplyRewindChanges(analysis.RequiredChanges)
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error restoring file state: %v", err)
}
didRevert = true
s := "files were"
if len(analysis.RequiredChanges) == 1 {
s = "file was"
}
fmt.Printf("⏪ %d project %s reverted\n", len(analysis.RequiredChanges), s)
for path := range analysis.RequiredChanges {
fmt.Printf(" • %s\n", path)
}
fmt.Println()
}
}
if didRevert {
shouldCommit := false
needsPrompt := true
if !fs.ProjectRootIsGitRepo() {
shouldCommit = false
needsPrompt = false
} else {
if cmd.Flags().Changed("commit") || cmd.Flags().Changed("skip-commit") {
if skipCommit {
shouldCommit = false
} else {
shouldCommit = autoCommit
}
needsPrompt = false
} else {
if config == nil {
config, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan config: %v", apiErr)
}
}
shouldCommit = config.AutoCommit
needsPrompt = false
}
}
if needsPrompt {
term.StopSpinner()
confirmed, err := term.ConfirmYesNo("Commit changes to git?")
if err != nil {
term.OutputErrorAndExit("Error getting user confirmation: %v", err)
}
shouldCommit = confirmed
}
if shouldCommit {
msg := "🤖 Plandex → rewound plan state and reverted these changes:"
for _, apply := range undonePlanApplies {
msg += fmt.Sprintf("\n • %s", apply.CommitMsg)
}
paths := []string{}
for path := range analysis.RequiredChanges {
paths = append(paths, path)
}
err := lib.GitAddAndCommitPaths(fs.ProjectRoot, msg, paths, true)
if err != nil {
term.OutputErrorAndExit("Error committing changes: %v", err)
}
}
} else {
printNoChanges()
}
printCmds()
}
func formatLogMessage(msg string) string {
var res string
// Check for message type patterns
switch {
case strings.Contains(msg, "User prompt"):
res = "💬 " + msg
case strings.Contains(msg, "Plandex reply"):
if coins := regexp.MustCompile(`(\d+) 🪙`).FindStringSubmatch(msg); len(coins) >= 2 {
res = "🤖 AI Response | " + coins[1] + " 🪙"
}
res = "🤖 " + msg
case strings.Contains(msg, "Build pending"):
res = "🏗️ Building changes"
case strings.Contains(msg, "Loaded"):
res = "📚 " + msg
default:
res = msg
}
return res
}
================================================
FILE: app/cli/cmd/rm.go
================================================
package cmd
import (
"fmt"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
"strings"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var contextRmCmd = &cobra.Command{
Use: "rm",
Aliases: []string{"remove", "unload"},
Short: "Remove context",
Long: `Remove context by index, range, name, or glob.
plandex rm 1 # Remove by index in the 'plandex ls' list
plandex rm 1-3
plandex rm some-file.ts
plandex rm app/*.py
`,
Args: cobra.MinimumNArgs(1),
Run: contextRm,
}
func contextRm(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
if err != nil {
term.OutputErrorAndExit("Error retrieving context: %v", err)
}
deleteIds := map[string]bool{}
indices := parseIndices(args)
for i, context := range contexts {
if indices[i+1] {
deleteIds[context.Id] = true
continue
}
for _, id := range args {
if context.Name == id || context.FilePath == id || context.Url == id {
deleteIds[context.Id] = true
break
} else if context.FilePath != "" {
// Check if id is a glob pattern
matched, err := filepath.Match(id, context.FilePath)
if err != nil {
term.OutputErrorAndExit("Error matching glob pattern: %v", err)
}
if matched {
deleteIds[context.Id] = true
break
}
// Check if id is a parent directory
parentDir := context.FilePath
for parentDir != "." && parentDir != "/" && parentDir != "" {
if parentDir == id {
deleteIds[context.Id] = true
break
}
parentDir = filepath.Dir(parentDir) // Move up one directory
}
}
}
}
if len(deleteIds) > 0 {
res, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{
Ids: deleteIds,
})
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error deleting context: %v", err)
}
fmt.Println("✅ " + res.Msg)
} else {
term.StopSpinner()
fmt.Println("🤷♂️ No context removed")
}
}
func init() {
RootCmd.AddCommand(contextRmCmd)
}
func parseIndices(args []string) map[int]bool {
indices := map[int]bool{}
for _, arg := range args {
if strings.Contains(arg, "-") {
parts := strings.Split(arg, "-")
start, err1 := strconv.Atoi(parts[0])
end, err2 := strconv.Atoi(parts[1])
if err1 == nil && err2 == nil && start <= end {
for i := start; i <= end; i++ {
indices[i] = true
}
}
} else {
index, err := strconv.Atoi(arg)
if err == nil {
indices[index] = true
}
}
}
return indices
}
================================================
FILE: app/cli/cmd/root.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/term"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var helpShowAll bool
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: `plandex [command] [flags]`,
// Short: "Plandex: iterative development with AI",
SilenceErrors: true,
SilenceUsage: true,
Run: func(cmd *cobra.Command, args []string) {
run(cmd, args)
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
// if no arguments were passed, start the repl
if len(os.Args) == 1 ||
(len(os.Args) == 2 && strings.HasPrefix(os.Args[1], "--") && os.Args[1] != "--help") ||
(len(os.Args) == 3 && strings.HasPrefix(os.Args[1], "--") && os.Args[1] != "--help" && strings.HasPrefix(os.Args[2], "--") && os.Args[2] != "--help") {
// Instead of directly calling replCmd.Run, parse the flags first
replCmd.ParseFlags(os.Args[1:])
replCmd.Run(replCmd, []string{})
return
}
if err := RootCmd.Execute(); err != nil {
// term.OutputErrorAndExit("Error executing root command: %v", err)
// log.Fatalf("Error executing root command: %v", err)
// output the error message to stderr
term.OutputSimpleError("Error: %v", err)
fmt.Println()
color.New(color.Bold, color.BgGreen, color.FgHiWhite).Println(" Usage ")
color.New(color.Bold).Println(" plandex [command] [flags]")
color.New(color.Bold).Println(" pdx [command] [flags]")
fmt.Println()
color.New(color.Bold, color.BgGreen, color.FgHiWhite).Println(" Help ")
color.New(color.Bold).Println(" plandex help # show basic usage")
color.New(color.Bold).Println(" plandex help --all # show all commands")
color.New(color.Bold).Println(" plandex [command] --help")
fmt.Println()
color.New(color.Bold, color.BgGreen, color.FgHiWhite).Println(" Common Commands ")
color.New(color.Bold).Println(" plandex new # create a new plan")
color.New(color.Bold).Println(" plandex tell # tell the plan what to do")
color.New(color.Bold).Println(" plandex continue # continue the current plan")
color.New(color.Bold).Println(" plandex settings # show plan settings")
color.New(color.Bold).Println(" plandex set # update plan settings")
fmt.Println()
os.Exit(1)
}
}
func run(cmd *cobra.Command, args []string) {
}
func init() {
var helpCmd = &cobra.Command{
Use: "help",
Aliases: []string{"h"},
Short: "Display help for Plandex",
Long: `Display help for Plandex.`,
Run: func(cmd *cobra.Command, args []string) {
term.PrintCustomHelp(helpShowAll)
},
}
RootCmd.AddCommand(helpCmd)
RootCmd.AddCommand(connectClaudeCmd)
RootCmd.AddCommand(disconnectClaudeCmd)
// add an --all/-a flag
helpCmd.Flags().BoolVarP(&helpShowAll, "all", "a", false, "Show all commands")
}
================================================
FILE: app/cli/cmd/set_config.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"slices"
"strconv"
"strings"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
func init() {
RootCmd.AddCommand(setConfigCmd)
setConfigCmd.AddCommand(defaultSetConfigCmd)
RootCmd.AddCommand(setAutoCmd)
setAutoCmd.AddCommand(setAutoDefaultCmd)
}
var setConfigCmd = &cobra.Command{
Use: "set-config [setting] [value]",
Short: "Update current plan config",
Run: setConfig,
Args: cobra.MaximumNArgs(2),
}
var defaultSetConfigCmd = &cobra.Command{
Use: "default [setting] [value]",
Short: "Update default plan config",
Run: defaultSetConfig,
Args: cobra.MaximumNArgs(2),
}
var setAutoCmd = &cobra.Command{
Use: "set-auto [value]",
Short: "Update config auto-mode",
Run: setAuto,
Args: cobra.MaximumNArgs(1),
}
var setAutoDefaultCmd = &cobra.Command{
Use: "default [value]",
Short: "Update default config auto-mode",
Run: setAutoDefault,
Args: cobra.MaximumNArgs(1),
}
func setAuto(cmd *cobra.Command, args []string) {
setConfig(cmd, append([]string{"auto-mode"}, args...))
}
func setAutoDefault(cmd *cobra.Command, args []string) {
defaultSetConfig(cmd, append([]string{"auto-mode"}, args...))
}
func setConfig(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
term.StartSpinner("")
config, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting current config: %v", apiErr)
return
}
if config == nil {
config = &shared.PlanConfig{}
}
key, updatedConfig := updateConfig(args, config)
if updatedConfig == nil {
return
}
term.StartSpinner("")
apiErr = api.Client.UpdatePlanConfig(lib.CurrentPlanId, shared.UpdatePlanConfigRequest{
Config: updatedConfig,
})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error updating config: %v", apiErr)
return
}
fmt.Println("✅ Config updated")
lib.ShowPlanConfig(updatedConfig, key)
fmt.Println()
loadMapIfNeeded(config, updatedConfig)
removeMapIfNeeded(config, updatedConfig)
if !(config.AutoApply && config.AutoExec) && updatedConfig.AutoApply && updatedConfig.AutoExec {
color.New(term.ColorHiYellow, color.Bold).Println("⚠️ You enabled automatic apply and execution.")
fmt.Println()
} else if !config.AutoApply && updatedConfig.AutoApply {
color.New(term.ColorHiYellow, color.Bold).Println("⚠️ You enabled automatic apply.")
fmt.Println()
} else if !config.AutoExec && updatedConfig.AutoExec {
color.New(term.ColorHiYellow, color.Bold).Println("⚠️ You enabled automatic execution.")
fmt.Println()
}
term.StopSpinner()
term.PrintCmds("", "config", "config default", "set-config default")
}
func defaultSetConfig(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
term.StartSpinner("")
config, apiErr := api.Client.GetDefaultPlanConfig()
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting current config: %v", apiErr)
return
}
if config == nil {
config = &shared.PlanConfig{}
}
key, updatedConfig := updateConfig(args, config)
if updatedConfig == nil {
return
}
term.StartSpinner("")
apiErr = api.Client.UpdateDefaultPlanConfig(shared.UpdateDefaultPlanConfigRequest{
Config: updatedConfig,
})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error updating config: %v", apiErr)
return
}
fmt.Println("✅ Default config updated")
lib.ShowPlanConfig(updatedConfig, key)
fmt.Println()
term.PrintCmds("", "config default", "config", "set-config")
}
type sortableSetting struct {
sortKey string
cfg shared.ConfigSetting
}
func updateConfig(args []string, originalConfig *shared.PlanConfig) (string, *shared.PlanConfig) {
var setting, value string
if len(args) > 0 {
setting = strings.ToLower(strings.ReplaceAll(args[0], "-", ""))
}
if len(args) > 1 {
value = args[1]
}
if setting == "" {
var sorted []sortableSetting
for key, cfg := range shared.ConfigSettingsByKey {
var sortKey string
if cfg.SortKey != "" {
sortKey = cfg.SortKey
} else {
sortKey = key
}
sorted = append(sorted, sortableSetting{sortKey, cfg})
}
slices.SortFunc(sorted, func(a, b sortableSetting) int {
return strings.Compare(a.sortKey, b.sortKey)
})
var opts []string
for _, opt := range sorted {
opts = append(opts, fmt.Sprintf("%s → %s", opt.cfg.Name, opt.cfg.Desc))
}
selection, err := term.SelectFromList("Choose a setting to update:", opts)
if err != nil {
if err.Error() == "interrupt" {
return "", nil
}
term.OutputErrorAndExit("Error selecting setting: %v", err)
return "", nil
}
setting = strings.Split(selection, " →")[0]
setting = strings.ToLower(strings.ReplaceAll(setting, "-", ""))
}
config := *originalConfig
cfgSetting, exists := shared.ConfigSettingsByKey[setting]
if !exists {
term.OutputErrorAndExit("Unknown setting: %s\n", setting)
return "", nil
}
if value == "" {
if cfgSetting.BoolSetter != nil {
options := []string{"Enabled", "Disabled"}
selection, err := term.SelectFromList(fmt.Sprintf("Set %s:", cfgSetting.Name), options)
if err != nil {
if err.Error() == "interrupt" {
return "", nil
}
term.OutputErrorAndExit("Error selecting value: %v", err)
return "", nil
}
cfgSetting.BoolSetter(&config, selection == "Enabled")
} else if cfgSetting.IntSetter != nil {
value, err := term.GetRequiredUserStringInput(fmt.Sprintf("Set %s (number)", cfgSetting.Name))
if err != nil {
if err.Error() == "interrupt" {
return "", nil
}
term.OutputErrorAndExit("Error getting value: %v", err)
return "", nil
}
n, err := strconv.Atoi(value)
if err != nil {
term.OutputErrorAndExit("Invalid number value for %s (%s)", cfgSetting.Name, value)
return "", nil
}
cfgSetting.IntSetter(&config, n)
} else if cfgSetting.StringSetter != nil {
var selection string
var err error
choices := *cfgSetting.Choices
if len(choices) > 0 {
if cfgSetting.HasCustomChoice {
choices = append(choices, "Other")
}
selection, err = term.SelectFromList(fmt.Sprintf("Set %s:", cfgSetting.Name), choices)
if err != nil {
if err.Error() == "interrupt" {
return "", nil
}
term.OutputErrorAndExit("Error selecting value: %v", err)
return "", nil
}
if selection == "Other" {
selection, err = term.GetRequiredUserStringInput(fmt.Sprintf("Enter value for %s", cfgSetting.Name))
if err != nil {
if err.Error() == "interrupt" {
return "", nil
}
term.OutputErrorAndExit("Error getting value: %v", err)
return "", nil
}
} else if cfgSetting.ChoiceToKey != nil {
selection = cfgSetting.ChoiceToKey(selection)
}
} else {
selection, err = term.GetRequiredUserStringInput(fmt.Sprintf("Set %s", cfgSetting.Name))
if err != nil {
if err.Error() == "interrupt" {
return "", nil
}
term.OutputErrorAndExit("Error getting value: %v", err)
return "", nil
}
}
cfgSetting.StringSetter(&config, selection)
} else if cfgSetting.EditorSetter != nil {
editor := lib.SelectEditor(false)
cfgSetting.EditorSetter(&config, editor.Name, editor.Cmd, editor.Args)
}
} else {
if cfgSetting.BoolSetter != nil {
b, err := parseBooleanArg(value)
if err != nil {
term.OutputErrorAndExit("Invalid value for %s (%s)", cfgSetting.Name, value)
return "", nil
}
cfgSetting.BoolSetter(&config, b)
} else if cfgSetting.IntSetter != nil {
n, err := strconv.Atoi(value)
if err != nil {
term.OutputErrorAndExit("Invalid number value for %s (%s)", cfgSetting.Name, value)
return "", nil
}
cfgSetting.IntSetter(&config, n)
} else if cfgSetting.StringSetter != nil {
cfgSetting.StringSetter(&config, value)
} else if cfgSetting.EditorSetter != nil {
fields := strings.Fields(value)
cmd := fields[0]
var cmdArgs []string
if len(fields) > 1 {
cmdArgs = fields[1:]
}
cfgSetting.EditorSetter(&config, value, cmd, cmdArgs)
}
}
return setting, &config
}
func parseBooleanArg(value string) (bool, error) {
switch value {
case "enabled", "true", "t", "yes", "y", "1":
return true, nil
case "disabled", "false", "f", "no", "n", "0":
return false, nil
default:
return false, fmt.Errorf("invalid value: %s", value)
}
}
func loadMapIfNeeded(originalConfig, updatedConfig *shared.PlanConfig) {
if updatedConfig.AutoLoadContext && !originalConfig.AutoLoadContext {
hasMap := false
term.StartSpinner("")
context, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
if err == nil {
for _, c := range context {
if c.ContextType == shared.ContextMapType {
hasMap = true
break
}
}
if !hasMap {
lib.MustLoadAutoContextMap()
fmt.Println()
}
}
}
}
func removeMapIfNeeded(originalConfig, updatedConfig *shared.PlanConfig) {
if originalConfig.AutoLoadContext && !updatedConfig.AutoLoadContext {
term.StartSpinner("")
context, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
if err == nil {
for _, c := range context {
if c.ContextType == shared.ContextMapType && (c.AutoLoaded || c.FilePath == ".") {
res, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{
Ids: map[string]bool{c.Id: true},
})
term.StopSpinner()
if err != nil {
term.OutputErrorAndExit("Error deleting context: %v", err)
return
}
fmt.Println("✅ " + res.Msg)
break
}
}
}
}
}
================================================
FILE: app/cli/cmd/set_model.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/term"
"strings"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var setModelUseJsonFile bool
var setModelJsonFilePath string
var setModelSave bool
func init() {
RootCmd.AddCommand(modelsSetCmd)
modelsSetCmd.AddCommand(defaultModelSetCmd)
modelsSetCmd.Flags().BoolVar(&setModelUseJsonFile, "json", false, "Use a JSON file to set model settings")
modelsSetCmd.Flags().StringVarP(&setModelJsonFilePath, "file", "f", "", "Path to model settings JSON file")
modelsSetCmd.Flags().BoolVar(&setModelSave, "save", false, "Save model settings from JSON file")
defaultModelSetCmd.Flags().BoolVar(&setModelUseJsonFile, "json", false, "Use a JSON file to set model settings")
defaultModelSetCmd.Flags().StringVarP(&setModelJsonFilePath, "file", "f", "", "Path to model settings JSON file")
defaultModelSetCmd.Flags().BoolVar(&setModelSave, "save", false, "Save model settings from JSON file")
}
var modelsSetCmd = &cobra.Command{
Use: "set-model [model-pack-name]",
Aliases: []string{"set-models"},
Short: "Update current plan model settings",
Run: modelsSet,
Args: cobra.MaximumNArgs(1),
}
var defaultModelSetCmd = &cobra.Command{
Use: "default [model-pack-name]",
Short: "Update org-wide default model settings",
Run: defaultModelsSet,
Args: cobra.MaximumNArgs(1),
}
func modelsSet(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
term.StartSpinner("")
originalSettings, apiErr := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.OutputErrorAndExit("Error getting current settings: %v", apiErr)
return
}
defaultPath := lib.GetPlanModelSettingsPath(lib.CurrentPlanId)
settings := updateModelSettings(args, originalSettings, defaultPath)
if settings == nil {
return
}
res, apiErr := api.Client.UpdateSettings(
lib.CurrentPlanId,
lib.CurrentBranch,
shared.UpdateSettingsRequest{
ModelPackName: settings.ModelPackName,
ModelPack: settings.ModelPack,
})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error updating settings: %v", apiErr)
return
}
if res == nil {
return
}
fmt.Println(res.Msg)
fmt.Println()
term.PrintCmds("", "models", "set-model default", "log")
}
func defaultModelsSet(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
term.StartSpinner("")
originalSettings, apiErr := api.Client.GetOrgDefaultSettings()
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting current settings: %v", apiErr)
return
}
defaultPath := lib.DefaultModelSettingsPath
settings := updateModelSettings(args, originalSettings, defaultPath)
if settings == nil {
return
}
term.StartSpinner("")
res, apiErr := api.Client.UpdateOrgDefaultSettings(
shared.UpdateSettingsRequest{
ModelPackName: settings.ModelPackName,
ModelPack: settings.ModelPack,
})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error updating settings: %v", apiErr)
return
}
if res == nil {
return
}
fmt.Println(res.Msg)
fmt.Println()
term.PrintCmds("", "models", "set-model default", "log")
}
func updateModelSettings(args []string, originalSettings *shared.PlanSettings, defaultPath string) *shared.PlanSettings {
settings, err := originalSettings.DeepCopy()
if err != nil {
term.OutputErrorAndExit("Error copying settings: %v", err)
return nil
}
builtInModelPacks := shared.BuiltInModelPacks
if auth.Current.IsCloud {
filtered := []*shared.ModelPack{}
for _, ms := range builtInModelPacks {
if ms.LocalProvider == "" {
filtered = append(filtered, ms)
}
}
builtInModelPacks = filtered
}
var customModelPacks []*shared.ModelPack
var defaultConfig *shared.PlanConfig
var planConfig *shared.PlanConfig
errCh := make(chan error, 3)
go func() {
var apiErr *shared.ApiError
customModelPacks, apiErr = api.Client.ListModelPacks()
if apiErr != nil {
errCh <- fmt.Errorf("error getting custom model packs: %v", apiErr.Msg)
return
}
errCh <- nil
}()
go func() {
var apiErr *shared.ApiError
defaultConfig, apiErr = api.Client.GetDefaultPlanConfig()
if apiErr != nil {
errCh <- fmt.Errorf("error getting default config: %v", apiErr.Msg)
return
}
errCh <- nil
}()
go func() {
if lib.CurrentPlanId != "" {
var apiErr *shared.ApiError
planConfig, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)
if apiErr != nil {
errCh <- fmt.Errorf("error getting plan config: %v", apiErr.Msg)
return
}
}
errCh <- nil
}()
for i := 0; i < 3; i++ {
err := <-errCh
if err != nil {
term.OutputErrorAndExit(err.Error())
return nil
}
}
useJsonFile := setModelUseJsonFile || setModelSave
var nameArg string
if len(args) > 0 {
nameArg = args[0]
}
if !useJsonFile {
if nameArg == "" {
term.StopSpinner()
const modelPackOpt = "Select a model pack"
const jsonOpt = "Edit model settings JSON"
selection, err := term.SelectFromList("Select a model pack or edit settings?", []string{modelPackOpt, jsonOpt})
if err != nil {
if err.Error() == "interrupt" {
return nil
}
}
if selection == modelPackOpt {
useJsonFile = false
} else {
useJsonFile = true
term.StartSpinner("")
}
}
}
if useJsonFile {
usingDefaultPath := false
if setModelJsonFilePath == "" {
usingDefaultPath = true
setModelJsonFilePath = defaultPath
}
exists, err := fs.FileExists(setModelJsonFilePath)
if err != nil {
term.OutputErrorAndExit("Error checking model settings file: %v", err)
return nil
}
if setModelSave {
if !exists {
term.OutputErrorAndExit("File not found: %s", customModelsPath)
}
} else {
if usingDefaultPath && exists {
modelSettingsCheckLocalChangesResult, err := lib.ModelSettingsCheckLocalChanges(setModelJsonFilePath)
if err != nil {
term.OutputErrorAndExit("Error checking model settings file: %v", err)
return nil
}
if modelSettingsCheckLocalChangesResult.HasLocalChanges {
term.StopSpinner()
res, err := warnModelsFileLocalChanges(setModelJsonFilePath, "set-model")
if err != nil {
term.OutputErrorAndExit("Error confirming: %v", err)
return nil
}
if !res {
return nil
}
fmt.Println()
term.StartSpinner("")
}
}
err = lib.WriteModelSettingsFile(setModelJsonFilePath, originalSettings)
if err != nil {
term.OutputErrorAndExit("Error writing model settings file: %v", err)
return nil
}
term.StopSpinner()
fmt.Printf("🧠 %s → %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Models file"), setModelJsonFilePath)
fmt.Println("👨💻 Edit it, then come back here to save")
fmt.Println()
pathArg := ""
if !usingDefaultPath {
pathArg = fmt.Sprintf(" --file %s", setModelJsonFilePath)
}
res := maybePromptAndOpenModelsFile(setModelJsonFilePath, pathArg, "set-model", defaultConfig, planConfig)
if res.shouldReturn {
return nil
}
}
term.StartSpinner("")
settings, err = lib.ApplyModelSettings(setModelJsonFilePath, originalSettings)
if err != nil {
term.OutputErrorAndExit("Error applying model settings: %v", err)
return nil
}
} else {
if nameArg == "" {
var names []string
var opts []string
for _, ms := range builtInModelPacks {
names = append(names, ms.Name)
opts = append(opts, "Built-in | "+ms.Name)
}
for _, ms := range customModelPacks {
names = append(names, ms.Name)
opts = append(opts, "Custom | "+ms.Name)
}
term.StopSpinner()
selection, err := term.SelectFromList("Select a model pack:", opts)
if err != nil {
if err.Error() == "interrupt" {
return nil
}
}
for i, opt := range opts {
if opt == selection {
nameArg = names[i]
break
}
}
}
var modelPackName string
compare := strings.ToLower(strings.TrimSpace(nameArg))
if compare == "daily" {
compare = "daily-driver"
}
if compare == "opus-4-planner" {
compare = "opus-planner"
}
for _, ms := range builtInModelPacks {
if strings.EqualFold(ms.Name, compare) {
modelPackName = ms.Name
break
}
}
for _, ms := range customModelPacks {
if strings.EqualFold(ms.Name, compare) {
modelPackName = ms.Name
break
}
}
if modelPackName == "" {
term.StopSpinner()
term.OutputSimpleError("No model pack found with name '%s'", nameArg)
fmt.Println()
term.PrintCmds("", "model-packs")
os.Exit(1)
return nil
}
settings.SetModelPackByName(modelPackName)
// clear the default settings file and hash file if they exist, ignoring errors
os.Remove(defaultPath)
os.Remove(defaultPath + ".hash")
}
term.StopSpinner()
if originalSettings.Equals(settings) {
fmt.Println("🤷♂️ No model settings were updated")
return nil
} else {
return settings
}
}
================================================
FILE: app/cli/cmd/sign_in.go
================================================
package cmd
import (
"plandex-cli/auth"
"plandex-cli/term"
"github.com/spf13/cobra"
)
var pin string
var signInCmd = &cobra.Command{
Use: "sign-in",
Short: "Sign in to a Plandex account",
Args: cobra.NoArgs,
Run: signIn,
}
func init() {
RootCmd.AddCommand(signInCmd)
signInCmd.Flags().StringVar(&pin, "pin", "", "Sign in with a pin from the Plandex Cloud web UI")
}
func signIn(cmd *cobra.Command, args []string) {
if pin != "" {
err := auth.SignInWithCode(pin, "")
if err != nil {
term.OutputErrorAndExit("Error signing in: %v", err)
}
return
}
err := auth.SelectOrSignInOrCreate()
if err != nil {
term.OutputErrorAndExit("Error signing in: %v", err)
}
}
================================================
FILE: app/cli/cmd/stop.go
================================================
package cmd
import (
"context"
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"github.com/spf13/cobra"
)
var stopCmd = &cobra.Command{
Use: "stop [stream-id-or-plan] [branch]",
Short: "Connect to an active stream",
// Long: ``,
Args: cobra.MaximumNArgs(2),
Run: stop,
}
func init() {
RootCmd.AddCommand(stopCmd)
}
func stop(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
if lib.CurrentPlanId == "" {
term.OutputNoCurrentPlanErrorAndExit()
}
planId, branch, shouldContinue := lib.SelectActiveStream(args)
if !shouldContinue {
return
}
term.StartSpinner("")
apiErr := api.Client.StopPlan(context.Background(), planId, branch)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error stopping stream: %v", apiErr.Msg)
}
fmt.Println("✅ Plan stream stopped")
fmt.Println()
term.PrintCmds("", "convo", "log")
}
================================================
FILE: app/cli/cmd/summary.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"github.com/spf13/cobra"
)
var summaryPlain bool
var statusCmd = &cobra.Command{
Use: "summary",
Short: "Show the latest summary of the current plan",
Run: status,
}
func init() {
RootCmd.AddCommand(statusCmd)
statusCmd.Flags().BoolVarP(&summaryPlain, "plain", "p", false, "Output summary in plain text with no ANSI codes")
}
func status(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
term.StartSpinner("")
status, apiErr := api.Client.GetPlanStatus(lib.CurrentPlanId, lib.CurrentBranch)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error loading conversation: %v", apiErr.Msg)
}
if status == "" {
fmt.Println("🤷♂️ No summary available")
}
if summaryPlain {
fmt.Println(status)
return
}
md, err := term.GetMarkdown(status)
if err != nil {
term.OutputErrorAndExit("Error formatting markdown: %v", err)
}
fmt.Println(md)
}
================================================
FILE: app/cli/cmd/tell.go
================================================
package cmd
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/plan_exec"
"plandex-cli/term"
"plandex-cli/types"
"strings"
shared "plandex-shared"
"github.com/spf13/cobra"
)
var isImplementationOfChat bool
// tellCmd represents the prompt command
var tellCmd = &cobra.Command{
Use: "tell [prompt]",
Aliases: []string{"t"},
Short: "Send a prompt for the current plan",
// Long: ``,
Args: cobra.RangeArgs(0, 1),
Run: doTell,
}
func init() {
RootCmd.AddCommand(tellCmd)
initExecFlags(tellCmd, initExecFlagsParams{})
tellCmd.Flags().BoolVar(&isImplementationOfChat, "from-chat", false, "Begin implementation based on conversation so far")
}
func doTell(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
mustSetPlanExecFlags(cmd, false)
if isImplementationOfChat && len(args) > 0 {
term.OutputErrorAndExit("Error: --from-chat cannot be used with a prompt")
}
var prompt string
if !isImplementationOfChat {
prompt = getTellPrompt(args)
if prompt == "" {
fmt.Println("🤷♂️ No prompt to send")
return
}
}
tellFlags := types.TellFlags{
TellBg: tellBg,
TellStop: tellStop,
TellNoBuild: tellNoBuild,
AutoContext: tellAutoContext,
SmartContext: tellSmartContext,
ExecEnabled: !noExec,
AutoApply: tellAutoApply,
IsImplementationOfChat: isImplementationOfChat,
SkipChangesMenu: tellSkipMenu,
}
plan_exec.TellPlan(plan_exec.ExecParams{
CurrentPlanId: lib.CurrentPlanId,
CurrentBranch: lib.CurrentBranch,
AuthVars: lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
auto := autoConfirm || tellAutoApply || tellAutoContext
return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
},
}, prompt, tellFlags)
if tellAutoApply {
applyFlags := types.ApplyFlags{
AutoConfirm: true,
AutoCommit: autoCommit,
NoCommit: !autoCommit,
NoExec: noExec,
AutoExec: autoExec || autoDebug > 0,
AutoDebug: autoDebug,
}
lib.MustApplyPlan(lib.ApplyPlanParams{
PlanId: lib.CurrentPlanId,
Branch: lib.CurrentBranch,
ApplyFlags: applyFlags,
TellFlags: tellFlags,
OnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),
})
}
}
func getTellPrompt(args []string) string {
var prompt string
var pipedData string
if len(args) > 0 {
prompt = args[0]
} else if tellPromptFile != "" {
bytes, err := os.ReadFile(tellPromptFile)
if err != nil {
term.OutputErrorAndExit("Error reading prompt file: %v", err)
}
prompt = string(bytes)
}
// Check if there's piped input
fileInfo, err := os.Stdin.Stat()
if err != nil {
term.OutputErrorAndExit("Failed to stat stdin: %v", err)
}
if fileInfo.Mode()&os.ModeNamedPipe != 0 {
reader := bufio.NewReader(os.Stdin)
pipedDataBytes, err := io.ReadAll(reader)
if err != nil {
term.OutputErrorAndExit("Failed to read piped data: %v", err)
}
pipedData = string(pipedDataBytes)
}
if prompt == "" && pipedData == "" {
prompt = getEditorPrompt()
} else if pipedData != "" {
if prompt != "" {
prompt = fmt.Sprintf("%s\n\n---\n\n%s", prompt, pipedData)
} else {
prompt = pipedData
}
}
return prompt
}
func prepareEditorCommand(editor string, filename string) *exec.Cmd {
switch editor {
case "vim":
return exec.Command(editor, "+normal G$", "+startinsert!", filename)
case "nano":
return exec.Command(editor, "+99999999", filename)
default:
return exec.Command(editor, filename)
}
}
func getEditorInstructions() string {
if editor == EditorTypeVim {
return "👉 Write your prompt below, then save and exit to send it to Plandex.\n• To save and exit, press ESC, then type :wq! and press ENTER.\n• To exit without saving, press ESC, then type :q! and press ENTER.\n\n\n"
}
if editor == EditorTypeNano {
return "👉 Write your prompt below, then save and exit to send it to Plandex.\n• To save and exit, press Ctrl+X, then Y, then ENTER.\n• To exit without saving, press Ctrl+X, then N.\n\n\n"
}
return "👉 Write your prompt below, then save and exit to send it to Plandex.\n\n\n"
}
func getEditorPrompt() string {
tempFile, err := os.CreateTemp(os.TempDir(), "plandex_prompt_*")
if err != nil {
term.OutputErrorAndExit("Failed to create temporary file: %v", err)
}
instructions := getEditorInstructions()
filename := tempFile.Name()
err = os.WriteFile(filename, []byte(instructions), 0644)
if err != nil {
term.OutputErrorAndExit("Failed to write instructions to temporary file: %v", err)
}
editorCmd := prepareEditorCommand(editor, filename)
editorCmd.Stdin = os.Stdin
editorCmd.Stdout = os.Stdout
editorCmd.Stderr = os.Stderr
err = editorCmd.Run()
if err != nil {
term.OutputErrorAndExit("Error opening editor: %v", err)
}
bytes, err := os.ReadFile(tempFile.Name())
if err != nil {
term.OutputErrorAndExit("Error reading temporary file: %v", err)
}
prompt := string(bytes)
err = os.Remove(tempFile.Name())
if err != nil {
term.OutputErrorAndExit("Error removing temporary file: %v", err)
}
prompt = strings.TrimPrefix(prompt, strings.TrimSpace(instructions))
prompt = strings.TrimSpace(prompt)
return prompt
}
// func maybeShowDiffs() {
// diffs, err := api.Client.GetPlanDiffs(lib.CurrentPlanId, lib.CurrentBranch, plainTextOutput || showDiffUi)
// if err != nil {
// term.OutputErrorAndExit("Error getting plan diffs: %v", err)
// return
// }
// if len(diffs) > 0 {
// cmd := exec.Command(os.Args[0], "diffs", "--ui")
// // Create a context that's cancelled when the program exits
// ctx, cancel := context.WithCancel(context.Background())
// // Ensure cleanup on program exit
// go func() {
// // Wait for program exit signal
// c := make(chan os.Signal, 1)
// signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// <-c
// // Cancel context and kill the process
// cancel()
// if cmd.Process != nil {
// cmd.Process.Kill()
// }
// }()
// go func() {
// if err := cmd.Start(); err != nil {
// fmt.Fprintf(os.Stderr, "Error starting diffs command: %v\n", err)
// return
// }
// // Wait in a separate goroutine
// go cmd.Wait()
// // Wait for either context cancellation or process completion
// <-ctx.Done()
// if cmd.Process != nil {
// cmd.Process.Kill()
// }
// }()
// // Give the UI a moment to start
// time.Sleep(100 * time.Millisecond)
// }
// }
================================================
FILE: app/cli/cmd/unarchive.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"strconv"
"strings"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var unarchiveCmd = &cobra.Command{
Use: "unarchive [name-or-index]",
Aliases: []string{"unarc"},
Short: "Unarchive a plan",
Args: cobra.MaximumNArgs(1),
Run: unarchive,
}
func init() {
RootCmd.AddCommand(unarchiveCmd)
}
func unarchive(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
var nameOrIdx string
if len(args) > 0 {
nameOrIdx = strings.TrimSpace(args[0])
}
var plan *shared.Plan
term.StartSpinner("")
plans, apiErr := api.Client.ListArchivedPlans([]string{lib.CurrentProjectId})
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting archived plans: %v", apiErr)
}
if len(plans) == 0 {
fmt.Println("🤷♂️ No archived plans")
return
}
if nameOrIdx == "" {
opts := make([]string, len(plans))
for i, p := range plans {
opts[i] = p.Name
}
selected, err := term.SelectFromList("Select a plan:", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting plan: %v", err)
}
for _, p := range plans {
if p.Name == selected {
plan = p
break
}
}
} else {
idx, err := strconv.Atoi(nameOrIdx)
if err == nil && idx > 0 && idx <= len(plans) {
plan = plans[idx-1]
} else {
for _, p := range plans {
if p.Name == nameOrIdx {
plan = p
break
}
}
}
}
if plan == nil {
term.OutputErrorAndExit("Plan not found")
}
err := api.Client.UnarchivePlan(plan.Id)
if err != nil {
term.OutputErrorAndExit("Error unarchiving plan: %v", err)
}
fmt.Printf("✅ Plan %s unarchived\n", color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name))
fmt.Println()
term.PrintCmds("", "plans", "current")
}
================================================
FILE: app/cli/cmd/update.go
================================================
package cmd
import (
"fmt"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/term"
"github.com/spf13/cobra"
)
var updateCmd = &cobra.Command{
Use: "update ",
Aliases: []string{"u"},
Short: "Update outdated context",
Args: cobra.MaximumNArgs(1),
Run: update,
}
func init() {
RootCmd.AddCommand(updateCmd)
}
func update(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
lib.MustResolveProject()
term.StartSpinner("")
contexts, apiErr := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("failed to list context: %s", apiErr)
}
paths, err := fs.GetProjectPaths(fs.ProjectRoot)
if err != nil {
term.OutputErrorAndExit("error getting project paths: %v", err)
}
outdated, err := lib.CheckOutdatedContext(contexts, paths)
if err != nil {
term.StopSpinner()
term.OutputErrorAndExit("failed to check outdated context: %s", err)
}
if len(outdated.UpdatedContexts) == 0 {
term.StopSpinner()
fmt.Println("✅ Context is up to date")
return
}
lib.UpdateContextWithOutput(lib.UpdateContextParams{
Contexts: contexts,
OutdatedRes: *outdated,
ReqFn: outdated.ReqFn,
})
}
================================================
FILE: app/cli/cmd/usage.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"sort"
"strconv"
"strings"
"time"
"unicode"
"github.com/eiannone/keyboard"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/shopspring/decimal"
"github.com/spf13/cobra"
)
const MaxCreditsLogPageSize = 500
var logCreditsPageSize int
var logCreditsPage int
var logCreditsDebitsOnly bool
var logCreditsCreditsOnly bool
var showUsageLog bool
var creditsSession bool
var creditsToday bool
var creditsMonth bool
var creditsCurrentPlan bool
var usageCmd = &cobra.Command{
Use: "usage",
Short: "Display credits balance and usage report",
Run: usage,
}
func init() {
RootCmd.AddCommand(usageCmd)
usageCmd.Flags().BoolVar(&showUsageLog, "log", false, "Show usage log")
usageCmd.Flags().IntVarP(&logCreditsPageSize, "page-size", "s", 100, "Number of transactions to display per page")
usageCmd.Flags().IntVarP(&logCreditsPage, "page", "p", 1, "Page number to display")
usageCmd.Flags().BoolVar(&logCreditsDebitsOnly, "debits", false, "Show only debits in the log")
usageCmd.Flags().BoolVar(&logCreditsCreditsOnly, "purchases", false, "Show only purchases in the log")
usageCmd.Flags().BoolVar(&creditsToday, "today", false, "Show usage for today")
usageCmd.Flags().BoolVar(&creditsMonth, "month", false, "Show usage for current billing month")
usageCmd.Flags().BoolVar(&creditsCurrentPlan, "plan", false, "Show usage for the current plan")
}
func usage(cmd *cobra.Command, args []string) {
if showUsageLog {
showLog(cmd, args)
} else {
showUsage()
}
}
func showUsage() {
auth.MustResolveAuthWithOrg()
term.StartSpinner("")
if !(creditsSession || creditsToday || creditsMonth || creditsCurrentPlan) {
if os.Getenv("PLANDEX_REPL_SESSION_ID") != "" {
creditsSession = true
} else {
creditsToday = true
}
}
var sessionId string
if creditsSession {
sessionId = os.Getenv("PLANDEX_REPL_SESSION_ID")
if sessionId == "" {
term.OutputErrorAndExit("Session ID is not set. The --session flag should be used in the Plandex REPL.")
}
}
var dayStart *time.Time
if creditsToday {
now := time.Now()
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
dayStart = &midnight
}
var planId string
var currentPlanName string
if creditsCurrentPlan {
lib.MustResolveProject()
planId = lib.CurrentPlanId
plan, apiErr := api.Client.GetPlan(planId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan: %v", apiErr)
}
currentPlanName = plan.Name
}
req := shared.CreditsLogRequest{
SessionId: sessionId,
DayStart: dayStart,
Month: creditsMonth,
PlanId: planId,
}
res, apiErr := api.Client.GetCreditsSummary(req)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting credits summary: %v", apiErr)
}
builder := strings.Builder{}
balance := res.Balance
balanceStr := formatSpend(balance)
spendLbl := "💸 Spent"
if creditsSession {
spendLbl += " This Session"
} else if creditsToday {
spendLbl += " Today"
} else if creditsMonth {
spendLbl += " This Billing Month"
spendLbl += fmt.Sprintf(" (since %s)", res.MonthStart.Format("Jan 2"))
} else if creditsCurrentPlan {
spendLbl += fmt.Sprintf(" On Plan 📋 %s", currentPlanName)
}
var spendStr string
if res.TotalSpend.IsZero() {
spendStr = "$0.00"
} else {
spendStr = formatSpend(res.TotalSpend)
}
table := tablewriter.NewWriter(&builder)
table.SetAutoWrapText(false)
table.SetHeader([]string{"💰 Current Balance", spendLbl})
table.Append([]string{balanceStr, spendStr})
table.Render()
fmt.Fprintln(&builder)
if !res.CacheSavings.IsZero() {
table := tablewriter.NewWriter(&builder)
table.SetAutoWrapText(false)
table.SetHeader([]string{"🎯 Cache Savings"})
table.Append([]string{formatSpend(res.CacheSavings)})
table.Render()
fmt.Fprintln(&builder)
}
amountByStr := map[string]float64{}
if len(res.ByPlanId) > 0 {
if !creditsCurrentPlan {
table := tablewriter.NewWriter(&builder)
table.SetAutoWrapText(false)
table.SetHeader([]string{"📋 Plan", "💸 Spent"})
rows := [][]string{}
for id, spend := range res.ByPlanId {
name := res.PlanNamesById[id]
spendStr := formatSpend(spend)
rows = append(rows, []string{name, spendStr})
amountByStr[spendStr] = spend.InexactFloat64()
}
sort.Slice(rows, func(i, j int) bool {
return amountByStr[rows[i][1]] > amountByStr[rows[j][1]]
})
for _, row := range rows {
table.Append(row)
}
table.Render()
fmt.Fprintln(&builder)
}
}
if len(res.ByPurpose) > 0 {
table = tablewriter.NewWriter(&builder)
table.SetAutoWrapText(false)
table.SetHeader([]string{"⚡️ Purpose", "💸 Spent"})
rows := [][]string{}
for name, spend := range res.ByPurpose {
spendStr := formatSpend(spend)
rows = append(rows, []string{name, spendStr})
amountByStr[spendStr] = spend.InexactFloat64()
}
sort.Slice(rows, func(i, j int) bool {
return amountByStr[rows[i][1]] > amountByStr[rows[j][1]]
})
for _, row := range rows {
table.Append(row)
}
table.Render()
fmt.Fprintln(&builder)
}
if len(res.ByModelName) > 0 {
table = tablewriter.NewWriter(&builder)
table.SetAutoWrapText(false)
table.SetHeader([]string{"🤖 Model", "💸 Spent"})
rows := [][]string{}
for name, spend := range res.ByModelName {
spendStr := formatSpend(spend)
rows = append(rows, []string{name, spendStr})
amountByStr[spendStr] = spend.InexactFloat64()
}
sort.Slice(rows, func(i, j int) bool {
return amountByStr[rows[i][1]] > amountByStr[rows[j][1]]
})
for _, row := range rows {
table.Append(row)
}
table.Render()
fmt.Fprintln(&builder)
}
term.PageOutput(builder.String())
term.PrintCmds("", "usage", "billing")
}
func showLog(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
if !(creditsSession || creditsToday || creditsMonth || creditsCurrentPlan) {
if os.Getenv("PLANDEX_REPL_SESSION_ID") != "" {
creditsSession = true
} else {
creditsToday = true
}
}
term.StartSpinner("")
var transactionType shared.CreditsTransactionType
if logCreditsDebitsOnly {
transactionType = shared.CreditsTransactionTypeDebit
} else if logCreditsCreditsOnly {
transactionType = shared.CreditsTransactionTypeCredit
}
var sessionId string
if creditsSession {
sessionId = os.Getenv("PLANDEX_REPL_SESSION_ID")
if sessionId == "" {
term.OutputErrorAndExit("Session ID is not set. The --session flag should be used in the Plandex REPL.")
}
}
var dayStart *time.Time
if creditsToday {
now := time.Now()
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
dayStart = &midnight
}
var planId string
var planName string
if creditsCurrentPlan {
lib.MustResolveProject()
planId = lib.CurrentPlanId
plan, err := api.Client.GetPlan(planId)
if err != nil {
term.OutputErrorAndExit("Error getting plan: %v", err)
return
}
planName = plan.Name
}
req := shared.CreditsLogRequest{
TransactionType: transactionType,
SessionId: sessionId,
DayStart: dayStart,
Month: creditsMonth,
PlanId: planId,
}
res, apiErr := api.Client.GetCreditsTransactions(logCreditsPageSize, logCreditsPage, req)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting credits transactions: %v", apiErr)
return
}
transactions := res.Transactions
if len(transactions) == 0 {
lbl := "🤷♂️ No usage"
if sessionId != "" {
lbl = "🤷♂️ No usage so far this session"
} else if creditsToday {
tz, _ := time.Now().Zone()
lbl = fmt.Sprintf("🤷♂️ No usage so far today (since midnight %s)", tz)
} else if creditsMonth {
lbl = fmt.Sprintf("🤷♂️ No usage so far this billing month (since %s)", res.MonthStart.Format("Jan 2"))
} else if creditsCurrentPlan {
lbl = "🤷♂️ No usage so far for current plan 👉 " + planName
}
fmt.Println(lbl)
return
}
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetAutoWrapText(false)
table.SetHeader([]string{"Amount", "Balance", "Transaction"})
for _, transaction := range transactions {
var sign string
var c color.Attribute
desc := transaction.CreatedAt.Local().Format("2006-01-02 15:04:05.000 EST") + "\n"
if transaction.TransactionType == "debit" {
sign = "-"
c = term.ColorHiRed
if transaction.DebitPlanId != nil {
planName := res.PlanNamesById[*transaction.DebitPlanId]
desc += fmt.Sprintf("Plan → %s\n", planName)
}
surchargePct := transaction.DebitSurcharge.Div(*transaction.DebitBaseAmount)
inputPrice := transaction.DebitModelInputPricePerToken.Mul(decimal.NewFromInt(1000000)).Mul(surchargePct.Add(decimal.NewFromInt(1))).StringFixed(4)
outputPrice := transaction.DebitModelOutputPricePerToken.Mul(decimal.NewFromInt(1000000)).Mul(surchargePct.Add(decimal.NewFromInt(1))).StringFixed(4)
var cacheDiscountStr string
var cacheDiscountPct float64
if transaction.DebitCacheDiscount != nil {
cacheDiscountStr = transaction.DebitCacheDiscount.StringFixed(4)
totalAmount := transaction.DebitBaseAmount.Add(*transaction.DebitCacheDiscount)
cacheDiscountPct = transaction.DebitCacheDiscount.Div(totalAmount).Mul(decimal.NewFromInt(100)).InexactFloat64()
}
for i := 0; i < 2; i++ {
inputPrice = strings.TrimSuffix(inputPrice, "0")
outputPrice = strings.TrimSuffix(outputPrice, "0")
cacheDiscountStr = strings.TrimSuffix(cacheDiscountStr, "0")
}
desc += fmt.Sprintf("⚡️ %s\n", *transaction.DebitPurpose)
desc += fmt.Sprintf("🧠 %s\n", transaction.ModelString())
desc += fmt.Sprintf("💳 Price → $%s input / $%s output per 1M\n", inputPrice, outputPrice)
desc += fmt.Sprintf("🪙 Used → %d input / %d output\n", *transaction.DebitInputTokens, *transaction.DebitOutputTokens)
if cacheDiscountStr != "" {
desc += fmt.Sprintf("🎯 Cache discount → $%s (%d%%)\n", cacheDiscountStr, int(cacheDiscountPct))
}
} else {
sign = "+"
c = term.ColorHiGreen
switch *transaction.CreditType {
case shared.CreditTypeGrant:
desc += "Monthly subscription payment"
case shared.CreditTypeTrial:
desc += "Started trial"
case shared.CreditTypePurchase:
desc += "Purchased credits"
case shared.CreditTypeSwitch:
desc += "Switched to Integrated Models mode"
}
desc += "\n"
}
amountStr := transaction.Amount.StringFixed(6)
for i := 0; i < 4; i++ {
amountStr = strings.TrimSuffix(amountStr, "0")
}
balanceStr := transaction.EndBalance.StringFixed(4)
for i := 0; i < 2; i++ {
balanceStr = strings.TrimSuffix(balanceStr, "0")
}
table.Append([]string{
color.New(c).Sprint(sign + "$" + amountStr),
"$" + balanceStr,
desc,
})
}
table.Render()
var output string
var pageLine string
if res.NumPages > 1 {
pageLine = fmt.Sprintf("Page size %d. Showing page %d of %d", logCreditsPageSize, logCreditsPage, res.NumPages)
if res.NumPagesMax {
pageLine = "+"
}
output = pageLine + "\n\n" + tableString.String()
} else {
output = tableString.String()
}
term.PageOutput(output)
var inputFn func()
inputFn = func() {
fmt.Println("\n" + pageLine)
prompts := []string{}
if res.NumPages > 1 && logCreditsPage < res.NumPages {
prompts = append(prompts, "Press 'n' for next page")
}
if logCreditsPage > 1 {
prompts = append(prompts, "Press 'p' for previous page")
}
prompts = append(prompts, "Type any number and press enter to jump to a page")
prompts = append(prompts, "Press 'q' to quit")
color.New(term.ColorHiMagenta, color.Bold).Println(strings.Join(prompts, "\n"))
color.New(term.ColorHiMagenta, color.Bold).Print("> ")
char, _, err := term.GetUserKeyInput()
if err != nil {
term.OutputErrorAndExit("Failed to get user input: %v", err)
}
// Check if the input is a digit
if unicode.IsDigit(char) {
var numberInput strings.Builder
numberInput.WriteRune(char)
fmt.Print(string(char)) // Show the initial digit
for {
char, key, err := term.GetUserKeyInput()
if err != nil {
term.OutputErrorAndExit("Failed to get user input: %v", err)
}
// If Enter is pressed, commit the input
if key == keyboard.KeyEnter {
pageNumber, err := strconv.Atoi(numberInput.String())
if err != nil {
fmt.Println("Invalid page number.")
return
}
// Check if the page number is valid
if pageNumber >= 1 && (pageNumber <= res.NumPages || res.NumPagesMax) {
logCreditsPage = pageNumber
showLog(cmd, args) // Re-run the log command with the new page
} else {
fmt.Println()
fmt.Println("Invalid page number.")
inputFn()
}
return
}
// If another digit is pressed, add it to the input
if unicode.IsDigit(char) {
numberInput.WriteRune(char)
fmt.Print(string(char)) // Show the digit
} else if key == keyboard.KeyBackspace || key == keyboard.KeyBackspace2 {
// Handle backspace
if numberInput.Len() > 0 {
// Remove the last rune
input := numberInput.String()
numberInput.Reset()
numberInput.WriteString(input[:len(input)-1])
fmt.Print("\b \b") // Erase the digit
}
} else {
// Handle invalid input while typing a number
fmt.Println()
fmt.Println("\nInvalid input. Please enter a valid page number.")
inputFn()
return
}
}
}
// Handle non-digit hotkeys
fmt.Print(string(char))
switch char {
case 'n':
if logCreditsPage < res.NumPages || res.NumPagesMax {
logCreditsPage++
showLog(cmd, args)
} else {
fmt.Println()
fmt.Println("Already on last page.")
inputFn()
}
case 'p':
if logCreditsPage > 1 {
logCreditsPage--
showLog(cmd, args)
} else {
fmt.Println()
fmt.Println("Already on first page.")
inputFn()
}
case 'q':
fmt.Println()
return
default:
fmt.Println()
fmt.Println("Invalid input.")
inputFn()
}
}
if res.NumPages > 1 {
inputFn()
}
}
func formatSpend(spend decimal.Decimal) string {
if spend.IsZero() {
return "$0.00"
}
spendStr := fmt.Sprintf("$%s", spend.StringFixed(4))
for i := 0; i < 2; i++ {
if strings.HasSuffix(spendStr, "0") {
spendStr = spendStr[:len(spendStr)-1]
}
}
if spendStr == "$0.00" {
return "<$0.0001"
}
return spendStr
}
================================================
FILE: app/cli/cmd/users.go
================================================
package cmd
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/term"
shared "plandex-shared"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
var usersCmd = &cobra.Command{
Use: "users",
Short: "List all users and pending invites and the current org",
Run: listUsersAndInvites,
}
func init() {
RootCmd.AddCommand(usersCmd)
}
func listUsersAndInvites(cmd *cobra.Command, args []string) {
auth.MustResolveAuthWithOrg()
var userResp *shared.ListUsersResponse
var pendingInvites []*shared.Invite
var orgRoles []*shared.OrgRole
errCh := make(chan error)
term.StartSpinner("")
go func() {
var err *shared.ApiError
userResp, err = api.Client.ListUsers()
if err != nil {
errCh <- fmt.Errorf("error fetching users: %s", err.Msg)
return
}
errCh <- nil
}()
go func() {
var err *shared.ApiError
pendingInvites, err = api.Client.ListPendingInvites()
if err != nil {
errCh <- fmt.Errorf("error fetching pending invites: %s", err.Msg)
return
}
errCh <- nil
}()
go func() {
var err *shared.ApiError
orgRoles, err = api.Client.ListOrgRoles()
if err != nil {
errCh <- fmt.Errorf("error fetching org roles: %s", err.Msg)
return
}
errCh <- nil
}()
for i := 0; i < 3; i++ {
err := <-errCh
if err != nil {
term.StopSpinner()
term.OutputErrorAndExit("%v", err)
}
}
term.StopSpinner()
orgRolesById := make(map[string]*shared.OrgRole)
for _, role := range orgRoles {
orgRolesById[role.Id] = role
}
// Display users and pending invites in a table
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Email", "Name", "Role", "Status"})
for _, user := range userResp.Users {
table.Append([]string{user.Email, user.Name, orgRolesById[userResp.OrgUsersByUserId[user.Id].OrgRoleId].Label, "Active"})
}
for _, invite := range pendingInvites {
table.Append([]string{invite.Email, invite.Name, orgRolesById[invite.OrgRoleId].Label, "Pending"})
}
table.Render()
}
================================================
FILE: app/cli/cmd/version.go
================================================
package cmd
import (
"fmt"
"plandex-cli/version"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Plandex",
Long: `All software has versions. This is Plandex's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(version.Version)
},
}
func init() {
RootCmd.AddCommand(versionCmd)
}
================================================
FILE: app/cli/dev.sh
================================================
#!/usr/bin/env bash
OUT="${PLANDEX_DEV_CLI_OUT_DIR:-/usr/local/bin}"
NAME="${PLANDEX_DEV_CLI_NAME:-plandex-dev}"
ALIAS="${PLANDEX_DEV_CLI_ALIAS:-pdxd}"
# Double quote to prevent globbing and word splitting.
go build -o "$NAME" &&
rm -f "$OUT"/"$NAME" &&
cp "$NAME" "$OUT"/"$NAME" &&
ln -sf "$OUT"/"$NAME" "$OUT"/"$ALIAS" &&
echo built "$NAME" cli and added "$ALIAS" alias to "$OUT"
================================================
FILE: app/cli/format/file.go
================================================
package format
import (
"path/filepath"
"strings"
)
func GetFileNameWithoutExt(path string) string {
name := path[:len(path)-len(filepath.Ext(path))]
name = strings.ToLower(name)
name = strings.ReplaceAll(name, "_", "-")
name = strings.ReplaceAll(name, " ", "-")
name = strings.ReplaceAll(name, ".", "-")
name = strings.ReplaceAll(name, "/", "-")
name = strings.ReplaceAll(name, "\\", "-")
name = strings.ReplaceAll(name, "'", "")
name = strings.ReplaceAll(name, "`", "")
name = strings.ReplaceAll(name, "\"", "")
return name
}
================================================
FILE: app/cli/format/time.go
================================================
// Adapted from https://raw.githubusercontent.com/dustin/go-humanize/master/times.go
package format
import (
"fmt"
"sort"
"time"
)
// Seconds-based time units
const (
Day = 24 * time.Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
LongTime = 37 * Year
)
// Time formats a time into a relative string.
//
// Time(someT) -> "3 weeks ago"
func Time(then time.Time) string {
return relTime(then.UTC(), time.Now().UTC(), "ago", "from now")
}
// A relTimeMagnitude struct contains a relative time point at which
// the relative format of time will switch to a new format string. A
// slice of these in ascending order by their "D" field is passed to
// CustomRelTime to format durations.
//
// The Format field is a string that may contain a "%s" which will be
// replaced with the appropriate signed label (e.g. "ago" or "from
// now") and a "%d" that will be replaced by the quantity.
//
// The DivBy field is the amount of time the time difference must be
// divided by in order to display correctly.
//
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
// DivBy should be time.Minute so whatever the duration is will be
// expressed in minutes.
type relTimeMagnitude struct {
D time.Duration
Fn func(diff time.Duration, lbl string) string
DivBy time.Duration
}
var defaultMagnitudes = []relTimeMagnitude{
{time.Second, func(diff time.Duration, lbl string) string { return "just now" }, time.Second},
{2 * time.Second, func(diff time.Duration, lbl string) string { return fmt.Sprintf("1s %s", lbl) }, 1},
{time.Minute, func(diff time.Duration, lbl string) string { return fmt.Sprintf("%ds %s", diff, lbl) }, time.Second},
{2 * time.Minute, func(diff time.Duration, lbl string) string { return fmt.Sprintf("1m %s", lbl) }, 1},
{time.Hour, func(diff time.Duration, lbl string) string { return fmt.Sprintf("%dm %s", diff, lbl) }, time.Minute},
{2 * time.Hour, func(diff time.Duration, lbl string) string { return fmt.Sprintf("1h %s", lbl) }, 1},
{Day, func(diff time.Duration, lbl string) string { return fmt.Sprintf("%dh %s", diff, lbl) }, time.Hour},
{2 * Day, func(diff time.Duration, lbl string) string { return fmt.Sprintf("1d %s", lbl) }, 1},
{Week, func(diff time.Duration, lbl string) string { return fmt.Sprintf("%dd %s", diff, lbl) }, Day},
{2 * Week, func(diff time.Duration, lbl string) string { return fmt.Sprintf("1w %s", lbl) }, 1},
{Month, func(diff time.Duration, lbl string) string { return fmt.Sprintf("%dw %s", diff, lbl) }, Week},
}
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func relTime(a, b time.Time, albl, blbl string) string {
return customRelTime(a, b, albl, blbl, defaultMagnitudes)
}
func customRelTime(a, b time.Time, albl, blbl string, magnitudes []relTimeMagnitude) string {
lbl := albl
diff := b.Sub(a)
if a.After(b) {
lbl = blbl
diff = a.Sub(b)
}
// Find the largest magnitude
largestMagnitude := magnitudes[len(magnitudes)-1].D
// If the difference is greater than the largest magnitude, format the date in local time
if diff >= largestMagnitude {
return a.Local().Format("Jan 2 2006")
}
n := sort.Search(len(magnitudes), func(i int) bool {
return magnitudes[i].D > diff
})
// If no magnitude is large enough, use the largest magnitude available
if n >= len(magnitudes) {
n = len(magnitudes) - 1
}
mag := magnitudes[n]
if mag.DivBy == 1 {
return mag.Fn(diff, lbl)
}
return mag.Fn(diff/mag.DivBy, lbl)
}
================================================
FILE: app/cli/fs/fs.go
================================================
package fs
import (
"os"
"os/exec"
"path/filepath"
"plandex-cli/term"
)
var Cwd string
var PlandexDir string
var ProjectRoot string
var HomePlandexDir string
var CacheDir string
var HomeDir string
var HomeAuthPath string
var HomeAccountsPath string
func init() {
var err error
Cwd, err = os.Getwd()
if err != nil {
term.OutputErrorAndExit("Error getting current working directory: %v", err)
}
home, err := os.UserHomeDir()
if err != nil {
term.OutputErrorAndExit("Couldn't find home dir: %v", err.Error())
}
HomeDir = home
if os.Getenv("PLANDEX_ENV") == "development" {
HomePlandexDir = filepath.Join(home, ".plandex-home-dev-v2")
} else {
HomePlandexDir = filepath.Join(home, ".plandex-home-v2")
}
// Create the home plandex directory if it doesn't exist
err = os.MkdirAll(HomePlandexDir, os.ModePerm)
if err != nil {
term.OutputErrorAndExit(err.Error())
}
CacheDir = filepath.Join(HomePlandexDir, "cache")
HomeAuthPath = filepath.Join(HomePlandexDir, "auth.json")
HomeAccountsPath = filepath.Join(HomePlandexDir, "accounts.json")
err = os.MkdirAll(filepath.Join(CacheDir, "tiktoken"), os.ModePerm)
if err != nil {
term.OutputErrorAndExit(err.Error())
}
err = os.Setenv("TIKTOKEN_CACHE_DIR", CacheDir)
if err != nil {
term.OutputErrorAndExit(err.Error())
}
FindPlandexDir()
if PlandexDir != "" {
ProjectRoot = Cwd
}
}
func FindOrCreatePlandex() (string, bool, error) {
FindPlandexDir()
if PlandexDir != "" {
ProjectRoot = Cwd
return PlandexDir, false, nil
}
// Determine the directory path
var dir string
if os.Getenv("PLANDEX_ENV") == "development" {
dir = filepath.Join(Cwd, ".plandex-dev-v2")
} else {
dir = filepath.Join(Cwd, ".plandex-v2")
}
err := os.Mkdir(dir, os.ModePerm)
if err != nil {
return "", false, err
}
PlandexDir = dir
ProjectRoot = Cwd
return dir, true, nil
}
func ProjectRootIsGitRepo() bool {
if ProjectRoot == "" {
return false
}
return IsGitRepo(ProjectRoot)
}
func IsGitRepo(dir string) bool {
isGitRepo := false
if isCommandAvailable("git") {
// check whether we're in a git repo
cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree")
cmd.Dir = dir
err := cmd.Run()
if err == nil {
isGitRepo = true
}
}
return isGitRepo
}
func FindPlandexDir() {
PlandexDir = findPlandex(Cwd)
}
func findPlandex(baseDir string) string {
var dir string
if os.Getenv("PLANDEX_ENV") == "development" {
dir = filepath.Join(baseDir, ".plandex-dev-v2")
} else {
dir = filepath.Join(baseDir, ".plandex-v2")
}
if _, err := os.Stat(dir); !os.IsNotExist(err) {
return dir
}
return ""
}
func isCommandAvailable(name string) bool {
cmd := exec.Command(name, "--version")
if err := cmd.Run(); err != nil {
return false
}
return true
}
================================================
FILE: app/cli/fs/paths.go
================================================
package fs
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"plandex-cli/types"
"strings"
"sync"
shared "plandex-shared"
ignore "github.com/sabhiram/go-gitignore"
)
func GetProjectPaths(baseDir string) (*types.ProjectPaths, error) {
if ProjectRoot == "" {
return nil, fmt.Errorf("no project root found")
}
return GetPaths(baseDir, ProjectRoot)
}
func GetPaths(baseDir, currentDir string) (*types.ProjectPaths, error) {
ignored, err := GetPlandexIgnore(currentDir)
if err != nil {
return nil, err
}
allPaths := map[string]bool{}
activePaths := map[string]bool{}
allDirs := map[string]bool{}
activeDirs := map[string]bool{}
gitIgnoredDirs := map[string]bool{}
isGitRepo := IsGitRepo(baseDir)
errCh := make(chan error)
var mu sync.Mutex
numRoutines := 0
deletedFiles := map[string]bool{}
if isGitRepo {
// Use git status to find deleted files
numRoutines++
go func() {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
output, err := cmd.Output()
if err != nil {
errCh <- fmt.Errorf("error getting git root: %s", err)
return
}
repoRoot := strings.TrimSpace(string(output))
cmd = exec.Command("git", "status", "--porcelain")
cmd.Dir = baseDir
out, err := cmd.Output()
if err != nil {
errCh <- fmt.Errorf("error getting git status: %s", err)
}
lines := strings.Split(string(out), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "D ") {
path := strings.TrimSpace(line[2:])
absPath := filepath.Join(repoRoot, path)
relPath, err := filepath.Rel(currentDir, absPath)
if err != nil {
errCh <- fmt.Errorf("error getting relative path: %s", err)
return
}
deletedFiles[relPath] = true
}
}
errCh <- nil
}()
// combine `git ls-files` and `git ls-files --others --exclude-standard`
// to get all files in the repo
numRoutines++
go func() {
// get all tracked files in the repo
cmd := exec.Command("git", "ls-files")
cmd.Dir = baseDir
out, err := cmd.Output()
if err != nil {
errCh <- fmt.Errorf("error getting files in git repo: %s", err)
return
}
files := strings.Split(string(out), "\n")
mu.Lock()
defer mu.Unlock()
for _, file := range files {
absFile := filepath.Join(baseDir, file)
relFile, err := filepath.Rel(currentDir, absFile)
if err != nil {
errCh <- fmt.Errorf("error getting relative path: %s", err)
return
}
if ignored != nil && ignored.MatchesPath(relFile) {
continue
}
activePaths[relFile] = true
parentDir := relFile
for parentDir != "." && parentDir != "/" && parentDir != "" {
parentDir = filepath.Dir(parentDir)
activeDirs[parentDir] = true
}
}
errCh <- nil
}()
// get all untracked non-ignored files in the repo
numRoutines++
go func() {
cmd := exec.Command("git", "ls-files", "--others", "--exclude-standard")
cmd.Dir = baseDir
out, err := cmd.Output()
if err != nil {
errCh <- fmt.Errorf("error getting untracked files in git repo: %s", err)
return
}
files := strings.Split(string(out), "\n")
mu.Lock()
defer mu.Unlock()
for _, file := range files {
absFile := filepath.Join(baseDir, file)
relFile, err := filepath.Rel(currentDir, absFile)
if err != nil {
errCh <- fmt.Errorf("error getting relative path: %s", err)
return
}
if ignored != nil && ignored.MatchesPath(relFile) {
continue
}
activePaths[relFile] = true
parentDir := relFile
for parentDir != "." && parentDir != "/" && parentDir != "" {
parentDir = filepath.Dir(parentDir)
activeDirs[parentDir] = true
}
}
errCh <- nil
}()
// get all ignored paths/dirs in the repo
// in some cases when entire directories are ignored, git will just list the directory and not the files within it
numRoutines++
go func() {
cmd := exec.Command("git", "ls-files", "--others", "--ignored", "--exclude-standard")
cmd.Dir = baseDir
out, err := cmd.Output()
if err != nil {
errCh <- fmt.Errorf("error getting untracked files in git repo: %s", err)
return
}
paths := strings.Split(string(out), "\n")
mu.Lock()
defer mu.Unlock()
for _, file := range paths {
absFile := filepath.Join(baseDir, file)
relFile, err := filepath.Rel(currentDir, absFile)
if err != nil {
errCh <- fmt.Errorf("error getting relative path: %s", err)
return
}
// check if git is ignoring the entire directory, meaning it won't list the files within it
if strings.HasSuffix(file, "/") {
allDirs[relFile] = true
allPaths[relFile] = true
gitIgnoredDirs[relFile+"/"] = true
} else {
allPaths[relFile] = true
}
}
errCh <- nil
}()
} else {
// get all paths in the directory
numRoutines++
go func() {
err = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if ShouldSkipDir(info.Name()) {
return filepath.SkipDir
}
relPath, err := filepath.Rel(currentDir, path)
if err != nil {
return err
}
allDirs[relPath] = true
if ignored != nil && ignored.MatchesPath(relPath) {
return filepath.SkipDir
}
} else {
relPath, err := filepath.Rel(currentDir, path)
if err != nil {
return err
}
allPaths[relPath] = true
if ignored != nil && ignored.MatchesPath(relPath) {
return nil
}
if !isGitRepo {
mu.Lock()
defer mu.Unlock()
activePaths[relPath] = true
parentDir := relPath
for parentDir != "." && parentDir != "/" && parentDir != "" {
parentDir = filepath.Dir(parentDir)
activeDirs[parentDir] = true
}
}
}
return nil
})
if err != nil {
errCh <- fmt.Errorf("error walking directory: %s", err)
return
}
errCh <- nil
}()
}
for i := 0; i < numRoutines; i++ {
err := <-errCh
if err != nil {
return nil, err
}
}
for dir := range activeDirs {
allDirs[dir] = true
}
for dir := range allDirs {
allPaths[dir] = true
}
for dir := range activeDirs {
activePaths[dir] = true
}
removeDirs := map[string]bool{}
for dir := range allDirs {
if ShouldSkipDir(dir) {
removeDirs[dir] = true
}
}
for dir := range removeDirs {
delete(allDirs, dir)
delete(activeDirs, dir)
delete(allPaths, dir)
delete(activePaths, dir)
}
// remove deleted files from active paths
for path := range deletedFiles {
delete(activePaths, path)
}
for path := range activePaths {
allPaths[path] = true
}
removePaths := map[string]bool{}
for path := range allPaths {
if IsInSkippedDir(path) {
removePaths[path] = true
}
}
for path := range removePaths {
delete(allPaths, path)
delete(activePaths, path)
delete(activeDirs, path)
delete(allDirs, path)
}
ignoredPaths := map[string]string{}
for path := range allPaths {
if _, ok := activePaths[path]; !ok {
if ignored != nil && ignored.MatchesPath(path) {
ignoredPaths[path] = "plandex"
} else {
ignoredPaths[path] = "git"
}
}
}
return &types.ProjectPaths{
ActivePaths: activePaths,
AllPaths: allPaths,
ActiveDirs: activeDirs,
AllDirs: allDirs,
PlandexIgnored: ignored,
IgnoredPaths: ignoredPaths,
GitIgnoredDirs: gitIgnoredDirs,
}, nil
}
func GetPlandexIgnore(dir string) (*ignore.GitIgnore, error) {
ignorePath := filepath.Join(dir, ".plandexignore")
if _, err := os.Stat(ignorePath); err == nil {
ignored, err := ignore.CompileIgnoreFile(ignorePath)
if err != nil {
return nil, fmt.Errorf("error reading .plandexignore file: %s", err)
}
return ignored, nil
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("error checking for .plandexignore file: %s", err)
}
return nil, nil
}
func GetBaseDirForContexts(contexts []*shared.Context) string {
var paths []string
for _, context := range contexts {
if context.FilePath != "" {
paths = append(paths, context.FilePath)
}
}
return GetBaseDirForFilePaths(paths)
}
func GetBaseDirForFilePaths(paths []string) string {
baseDir := ProjectRoot
dirsUp := 0
for _, path := range paths {
currentDir := ProjectRoot
pathSplit := strings.Split(path, string(os.PathSeparator))
n := 0
for _, p := range pathSplit {
if p == ".." {
n++
currentDir = filepath.Dir(currentDir)
} else {
break
}
}
if n > dirsUp {
dirsUp = n
baseDir = currentDir
}
}
return baseDir
}
// isSubpathOf checks if 'child' is within 'parent' (same path or deeper).
// Both 'parent' and 'child' can be absolute or relative to 'baseDir';
// we’ll convert them to absolute paths based on 'baseDir' and then compare.
func IsSubpathOf(parent, child, baseDir string) (bool, error) {
// Convert 'parent' -> absolute
absParent := parent
if !filepath.IsAbs(parent) {
absParent = filepath.Join(baseDir, parent)
}
absParent = filepath.Clean(absParent)
// Convert 'child' -> absolute
absChild := child
if !filepath.IsAbs(child) {
absChild = filepath.Join(baseDir, child)
}
absChild = filepath.Clean(absChild)
// filepath.Rel(absParent, absChild) will be something like:
// - ".", "foo", "foo/bar", or ".." references
rel, err := filepath.Rel(absParent, absChild)
if err != nil {
// If there's some I/O error or invalid path, just fail safe
return false, fmt.Errorf("error getting relative path: %s", err)
}
// If rel starts with "..", then absChild is outside of absParent
// or at a higher level (e.g. absParent/../sibling).
if strings.HasPrefix(rel, "..") {
return false, nil
}
// If we want "absChild == absParent" to count as inside,
// then !HasPrefix(rel, "..") is enough.
// This means child == parent or child is deeper within parent.
return true, nil
}
func IsIgnored(paths *types.ProjectPaths, path, baseDir string) (bool, string, error) {
if !paths.AllPaths[path] {
// if the path isn't in AllPaths, it either:
// 1. doesn't exist (in which case we shouldn't be calling this function)
// 2. is a subpath of a git ignored dir
for dir := range paths.GitIgnoredDirs {
subpath, err := IsSubpathOf(dir, path, baseDir)
if err != nil {
return false, "", fmt.Errorf("error checking if %s is a subpath of %s: %s", path, dir, err)
}
if subpath {
return true, "git", nil
}
}
return false, "", fmt.Errorf("path %s is not in the project", path)
}
if paths.ActivePaths[path] {
return false, "", nil
}
if paths.PlandexIgnored != nil && paths.PlandexIgnored.MatchesPath(path) {
return true, "plandex", nil
}
return true, "git", nil
}
var skipDirs = map[string]bool{
".git": true,
"node_modules": true,
"venv": true,
".cache": true,
"__pycache__": true,
"cue.mod": true,
"_build": true,
".build": true,
"DerivedData": true,
".gradle": true,
".terraform": true,
".terragrunt-cache": true,
".next": true,
".nuxt": true,
".bundle": true,
".rvm": true,
".rbenv": true,
".pyenv": true,
".nodenv": true,
".plenv": true,
".nvm": true,
"vendor": true,
".plandex": true,
".plandex-dev": true,
".plandex-v2": true,
".plandex-dev-v2": true,
}
func ShouldSkipDir(path string) bool {
if skipDirs[path] {
return true
}
for k := range skipDirs {
splitPath := strings.Split(path, string(os.PathSeparator))
for _, p := range splitPath {
if p == k {
return true
}
}
}
return false
}
func IsInSkippedDir(path string) bool {
// Check the direct parent directory
dirName := filepath.Dir(path)
if skipDirs[dirName] {
return true
}
// Handle cases where the directory might include path separators
pathComponents := strings.Split(dirName, string(os.PathSeparator))
for _, component := range pathComponents {
if skipDirs[component] {
return true
}
}
return false
}
================================================
FILE: app/cli/fs/projects.go
================================================
package fs
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"plandex-cli/term"
"plandex-cli/types"
"strings"
)
func GetParentProjectIdsWithPaths(currentUserId string) ([][2]string, error) {
var parentProjectIds [][2]string
currentDir := filepath.Dir(Cwd)
for currentDir != "/" {
plandexDir := findPlandex(currentDir)
projectSettingsPath := filepath.Join(plandexDir, "projects-v2.json")
if _, err := os.Stat(projectSettingsPath); err == nil {
bytes, err := os.ReadFile(projectSettingsPath)
if err != nil {
return nil, fmt.Errorf("error reading projectId file: %s", err)
}
var settingsByAccount types.CurrentProjectSettingsByAccount
err = json.Unmarshal(bytes, &settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error unmarshalling projects-v2.json: %v", err)
}
settings := settingsByAccount[currentUserId]
if settings == nil {
return parentProjectIds, nil
}
projectId := string(settings.Id)
parentProjectIds = append(parentProjectIds, [2]string{currentDir, projectId})
}
currentDir = filepath.Dir(currentDir)
}
return parentProjectIds, nil
}
func GetChildProjectIdsWithPaths(ctx context.Context, currentUserId string) ([][2]string, error) {
var childProjectIds [][2]string
err := filepath.Walk(Cwd, func(path string, info os.FileInfo, err error) error {
if err != nil {
// if permission denied, skip the path
if os.IsPermission(err) {
if info.IsDir() {
return filepath.SkipDir
} else {
return nil
}
}
return err
}
if strings.HasPrefix(info.Name(), ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("context timeout")
default:
}
if info.IsDir() && path != Cwd {
plandexDir := findPlandex(path)
projectSettingsPath := filepath.Join(plandexDir, "projects-v2.json")
if _, err := os.Stat(projectSettingsPath); err == nil {
bytes, err := os.ReadFile(projectSettingsPath)
if err != nil {
return fmt.Errorf("error reading projectId file: %s", err)
}
var settingsByAccount types.CurrentProjectSettingsByAccount
err = json.Unmarshal(bytes, &settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error unmarshalling projects-v2.json: %v", err)
}
settings := settingsByAccount[currentUserId]
if settings == nil {
return nil
}
projectId := string(settings.Id)
childProjectIds = append(childProjectIds, [2]string{path, projectId})
}
}
return nil
})
if err != nil {
if err.Error() == "context timeout" {
return childProjectIds, nil
}
return nil, fmt.Errorf("error walking the path %s: %s", Cwd, err)
}
return childProjectIds, nil
}
================================================
FILE: app/cli/fs/utils.go
================================================
package fs
import (
"fmt"
"os"
)
func FileExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
} else if os.IsNotExist(err) {
return false, nil
} else {
return false, fmt.Errorf("error checking if file exists: %v", err)
}
}
================================================
FILE: app/cli/go.mod
================================================
module plandex-cli
go 1.23.3
require (
github.com/aws/aws-sdk-go-v2/config v1.29.14
github.com/charmbracelet/lipgloss v1.0.0
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8
github.com/chromedp/chromedp v0.13.3
github.com/coreos/go-systemd/v22 v22.5.0
github.com/davecgh/go-spew v1.1.1
github.com/fatih/color v1.18.0
github.com/godbus/dbus/v5 v5.1.0
github.com/google/uuid v1.6.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
github.com/olekukonko/tablewriter v0.0.5
github.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c
github.com/plandex-ai/survey/v2 v2.3.7
github.com/sashabaranov/go-openai v1.38.1
github.com/shopspring/decimal v1.4.0
github.com/spf13/cobra v1.8.0
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/term v0.19.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/cqroot/multichoose v0.1.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-tty v0.0.3 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pkg/term v1.2.0-beta.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/yuin/goldmark v1.6.0 // indirect
github.com/yuin/goldmark-emoji v1.0.2 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.30.0 // indirect
)
require (
github.com/Masterminds/semver v1.5.0
github.com/PuerkitoBio/goquery v1.8.1
github.com/briandowns/spinner v1.23.0
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.3.0
github.com/charmbracelet/glamour v0.6.0
github.com/charmbracelet/glow v1.5.1
github.com/cqroot/prompt v0.9.4
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
github.com/spf13/pflag v1.0.5 // indirect
github.com/xlab/treeprint v1.2.0
golang.org/x/image v0.25.0 // indirect
golang.org/x/text v0.23.0 // indirect
plandex-shared v0.0.0-00010101000000-000000000000
)
replace plandex-shared => ../shared
================================================
FILE: app/cli/go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=
cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=
cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=
cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=
cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=
cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=
cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=
cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=
cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=
cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=
cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=
cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=
cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=
cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=
cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=
cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=
cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=
cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=
cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=
cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=
cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=
cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=
cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=
cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=
cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=
cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=
cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=
cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=
cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=
cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=
cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=
cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=
cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=
cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=
cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=
cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=
cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=
cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=
cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=
cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=
cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=
cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=
cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=
cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=
cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=
cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=
cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=
cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=
cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=
cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=
cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=
cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=
cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=
cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=
cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=
cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=
cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=
cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=
cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=
cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=
cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=
cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=
cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=
cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=
cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=
cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=
cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=
cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=
cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=
cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=
cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=
cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=
cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=
cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=
cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=
cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=
cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=
cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=
cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=
cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=
cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=
cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/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/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
github.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.7.5/go.mod h1:IRTORFvhEI6OUH7WhN2Ks8Z8miNGimk1BE6cmHijOkM=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
github.com/charmbracelet/bubbletea v1.3.0 h1:fPMyirm0u3Fou+flch7hlJN9krlnVURrkUVDwqXjoAc=
github.com/charmbracelet/bubbletea v1.3.0/go.mod h1:eTaHfqbIwvBhFQM/nlT1NsGc4kp8jhF8LfUK67XiTDM=
github.com/charmbracelet/charm v0.8.7/go.mod h1:ApJYwJljEjODkOYJgFDzbUqztLrCWQct9zyPD+xcVr4=
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/glow v1.5.1 h1:o1mwT4xXXpkfUhJG6euQayNxLZf9yKctOCNHLztrwdE=
github.com/charmbracelet/glow v1.5.1/go.mod h1:rGgop0a2/4gXWiAxUW1iEQseoE+9Ctpb7M4sM9cY9CU=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
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/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0=
github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cqroot/multichoose v0.1.1 h1:diGuKYKea9ePOTwUyUDor9zKRqKFWXGkYGqUa9+firU=
github.com/cqroot/multichoose v0.1.1/go.mod h1:BJzIGqbQZNADPDuA3IzhmTMpRc2F3fZKysMRYP+Ydw8=
github.com/cqroot/prompt v0.9.4 h1:uFRlhXuOP3CSD+Pii0Z8VJhgXpavSloFf7/KAERwjz8=
github.com/cqroot/prompt v0.9.4/go.mod h1:6BVZiEv7XkW1K64y1k2wdzToDwspL3n/RkUIyPjQ808=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY=
github.com/hashicorp/consul/sdk v0.11.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/meowgorithm/babyenv v1.3.0/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=
github.com/meowgorithm/babyenv v1.3.1/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/gitcha v0.2.0/go.mod h1:Ri8m9TZS4+ORG4JVmVKUQcWZuxDvUW3UKxMdQfzG2zI=
github.com/muesli/go-app-paths v0.2.1/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a/go.mod h1:+XG0ne5zXWBTSbbe7Z3/RWxaT8PZY6zaZ1dX6KjprYY=
github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c h1:bki/wkg5iFBOv3jCPUDNuH5yLngUPUdEJCSuvc2tiQ0=
github.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c/go.mod h1:SqEsJfsIr0GYUyLatvezDOBe6XsCw64E7v33QzeH5PM=
github.com/plandex-ai/survey/v2 v2.3.7 h1:u1o6bflbaBpW8i8krm+91Z2cOcvZcMVS+AjV+rgR8Rk=
github.com/plandex-ai/survey/v2 v2.3.7/go.mod h1:RiBOKRDB5fOQrOzsiAPAN57hYqFKPkCxgSK7twcDOys=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
github.com/sagikazarmark/crypt v0.8.0/go.mod h1:TmKwZAo97S4Fy4sfMH/HX/cQP5D+ijra2NyLpNNmttY=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4=
go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/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-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
================================================
FILE: app/cli/install.sh
================================================
#!/usr/bin/env bash
set -e
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PLATFORM=
ARCH=
VERSION=
RELEASES_URL="https://github.com/plandex-ai/plandex/releases/download"
# Ensure cleanup happens on exit and on specific signals
trap cleanup EXIT
trap cleanup INT TERM
cleanup () {
cd "${SCRIPT_DIR}"
rm -rf plandex_install_tmp
}
# Set platform
case "$(uname -s)" in
Darwin)
PLATFORM='darwin'
;;
Linux)
PLATFORM='linux'
;;
FreeBSD)
PLATFORM='freebsd'
;;
CYGWIN*|MINGW*|MSYS*)
PLATFORM='windows'
;;
*)
echo "Platform may or may not be supported. Will attempt to install."
PLATFORM='linux'
;;
esac
if [[ "$PLATFORM" == "windows" ]]; then
echo "🚨 Windows is only supported via WSL. It doesn't work in the Windows CMD prompt or PowerShell."
echo "How to install WSL 👉 https://learn.microsoft.com/en-us/windows/wsl/about"
exit 1
fi
# Set arch
if [[ "$(uname -m)" == 'x86_64' ]]; then
ARCH="amd64"
elif [[ "$(uname -m)" == 'arm64' || "$(uname -m)" == 'aarch64' ]]; then
ARCH="arm64"
fi
if [[ "$(cat /proc/1/cgroup 2> /dev/null | grep docker | wc -l)" > 0 ]] || [ -f /.dockerenv ]; then
IS_DOCKER=true
else
IS_DOCKER=false
fi
# Set Version
if [[ -z "${PLANDEX_VERSION}" ]]; then
VERSION=$(curl -sL https://plandex.ai/v2/cli-version.txt)
else
VERSION=$PLANDEX_VERSION
echo "Using custom version $VERSION"
fi
welcome_plandex () {
echo ""
echo "$(printf '%*s' "$(tput cols)" '' | tr ' ' -)"
echo ""
echo "🚀 Plandex v$VERSION • Quick Install"
echo ""
echo "$(printf '%*s' "$(tput cols)" '' | tr ' ' -)"
echo ""
}
download_plandex () {
ENCODED_TAG="cli%2Fv${VERSION}"
url="${RELEASES_URL}/${ENCODED_TAG}/plandex_${VERSION}_${PLATFORM}_${ARCH}.tar.gz"
mkdir -p plandex_install_tmp
cd plandex_install_tmp
echo "📥 Downloading Plandex tarball"
echo ""
echo "👉 $url"
echo ""
curl -s -L -o plandex.tar.gz "${url}"
tar zxf plandex.tar.gz 1> /dev/null
should_sudo=false
if [ "$PLATFORM" == "darwin" ] || $IS_DOCKER ; then
if [[ -d /usr/local/bin ]]; then
if ! mv plandex /usr/local/bin/ 2>/dev/null; then
echo "Permission denied when attempting to move Plandex to /usr/local/bin."
if hash sudo 2>/dev/null; then
should_sudo=true
echo "Attempting to use sudo to complete installation."
sudo mv plandex /usr/local/bin/
if [[ $? -eq 0 ]]; then
echo "✅ Plandex is installed in /usr/local/bin"
echo ""
else
echo "Failed to install Plandex using sudo. Please manually move Plandex to a directory in your PATH."
exit 1
fi
else
echo "sudo not found. Please manually move Plandex to a directory in your PATH."
exit 1
fi
else
echo "✅ Plandex is installed in /usr/local/bin"
fi
else
echo >&2 'Error: /usr/local/bin does not exist. Create this directory with appropriate permissions, then re-install.'
exit 1
fi
else
if [ $UID -eq 0 ]
then
# we are root
mv plandex /usr/local/bin/
elif hash sudo 2>/dev/null;
then
# not root, but can sudo
sudo mv plandex /usr/local/bin/
should_sudo=true
else
echo "ERROR: This script must be run as root or be able to sudo to complete the installation."
exit 1
fi
echo "✅ Plandex is installed in /usr/local/bin"
fi
# create 'pdx' alias, but don't overwrite existing pdx command
if [ ! -x "$(command -v pdx)" ]; then
echo "🎭 Creating pdx alias..."
LOC=$(which plandex)
BIN_DIR=$(dirname "$LOC")
if [ "$should_sudo" = true ]; then
sudo ln -s "$LOC" "$BIN_DIR/pdx" && \
echo "✅ Successfully created 'pdx' alias with sudo." || \
echo "⚠️ Failed to create 'pdx' alias even with sudo. Please create it manually."
else
ln -s "$LOC" "$BIN_DIR/pdx" && \
echo "✅ Successfully created 'pdx' alias." || \
echo "⚠️ Failed to create 'pdx' alias. Please create it manually."
fi
fi
}
check_existing_installation () {
if command -v plandex >/dev/null 2>&1; then
existing_version=$(plandex version 2>/dev/null || echo "unknown")
# Check if version starts with 1.x.x
if [[ "$existing_version" =~ ^1\. ]]; then
echo "Found existing Plandex v1.x installation ($existing_version). Renaming to 'plandex1' before installing v2..."
# Get the location of existing binary
existing_binary=$(which plandex)
binary_dir=$(dirname "$existing_binary")
# Rename plandex to plandex1
if ! mv "$existing_binary" "${binary_dir}/plandex1" 2>/dev/null; then
sudo mv "$existing_binary" "${binary_dir}/plandex1"
fi
# Rename pdx to pdx1 if it exists
if [ -L "${binary_dir}/pdx" ]; then
if ! mv "${binary_dir}/pdx" "${binary_dir}/pdx1" 2>/dev/null; then
sudo mv "${binary_dir}/pdx" "${binary_dir}/pdx1"
fi
echo "Renamed 'pdx' alias to 'pdx1'"
fi
echo "Your v1.x installation is now accessible as 'plandex1' and 'pdx1'"
fi
fi
}
welcome_plandex
check_existing_installation
download_plandex
echo ""
echo "🎉 Installation complete"
echo ""
echo "$(printf '%*s' "$(tput cols)" '' | tr ' ' -)"
echo ""
echo "⚡️ Run 'plandex' or 'pdx' in any project directory and start building!"
echo ""
echo "$(printf '%*s' "$(tput cols)" '' | tr ' ' -)"
echo ""
echo "📚 Need help? 👉 https://docs.plandex.ai"
echo ""
echo "👋 Join a community of AI builders 👉 https://discord.gg/plandex-ai"
echo ""
echo "$(printf '%*s' "$(tput cols)" '' | tr ' ' -)"
echo ""
================================================
FILE: app/cli/lib/active_stream.go
================================================
package lib
import (
"fmt"
"plandex-cli/api"
"plandex-cli/term"
shared "plandex-shared"
)
func SelectActiveStream(args []string) (string, string, bool) {
term.StartSpinner("")
res, apiErr := api.Client.ListPlansRunning([]string{CurrentProjectId}, false)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting running plans: %v", apiErr)
return "", "", false
}
if len(res.Branches) == 0 {
fmt.Println("🤷♂️ No active plan stream")
fmt.Println()
term.PrintCmds("", "ps")
return "", "", false
}
var planId string
var branch string
var streamIdOrPlan string
if len(args) > 0 {
streamIdOrPlan = args[0]
}
if streamIdOrPlan != "" {
for _, b := range res.Branches {
id := res.StreamIdByBranchId[b.Id]
plan := res.PlansById[b.PlanId]
if id == streamIdOrPlan || plan.Name == streamIdOrPlan {
planId = b.PlanId
branch = b.Name
break
}
}
}
if planId == "" {
if len(res.PlansById) == 1 {
for _, p := range res.PlansById {
if p.Id == CurrentPlanId {
planId = p.Id
break
}
}
}
if planId == "" {
var opts []string
addedPlans := make(map[string]bool)
for _, plan := range res.PlansById {
if addedPlans[plan.Id] {
continue
}
opts = append(opts, plan.Name)
addedPlans[plan.Id] = true
}
selected, err := term.SelectFromList("Select an active plan", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting plan: %v", err)
}
for _, p := range res.PlansById {
if p.Name == selected {
planId = p.Id
break
}
}
}
}
if planId == "" {
fmt.Println("🤷♂️ No active plan stream")
fmt.Println()
term.PrintCmds("", "ps")
return "", "", false
}
var planBranches []*shared.Branch
for _, b := range res.Branches {
if b.PlanId == planId {
planBranches = append(planBranches, b)
}
}
if len(planBranches) == 0 {
fmt.Println("🤷♂️ No active plan stream")
fmt.Println()
term.PrintCmds("", "ps")
return "", "", false
}
if len(args) > 1 {
maybeBranch := args[1]
for _, b := range planBranches {
if b.Name == maybeBranch {
branch = maybeBranch
break
}
}
}
if branch == "" {
if len(planBranches) == 1 {
name := planBranches[0].Name
if name == CurrentBranch {
branch = name
}
}
if branch == "" {
opts := make([]string, len(planBranches))
for i, b := range planBranches {
opts[i] = b.Name
}
selected, err := term.SelectFromList("Select a branch", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting branch: %v", err)
}
branch = selected
}
}
if branch == "" {
fmt.Println("🤷♂️ No active plan stream")
fmt.Println()
term.PrintCmds("", "ps")
return "", "", false
}
return planId, branch, true
}
================================================
FILE: app/cli/lib/apply.go
================================================
package lib
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/term"
"plandex-cli/types"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
shared "plandex-shared"
"github.com/fatih/color"
)
type ApplyPlanParams struct {
PlanId string
Branch string
ApplyFlags types.ApplyFlags
TellFlags types.TellFlags
OnExecFail types.OnApplyExecFailFn
ExecCommand string
}
func MustApplyPlan(
params ApplyPlanParams,
) {
MustApplyPlanAttempt(params, 0)
}
func MustApplyPlanAttempt(
params ApplyPlanParams,
attempt int,
) {
log.Println("Applying plan")
applyFlags := params.ApplyFlags
planId := params.PlanId
branch := params.Branch
onExecFail := params.OnExecFail
autoConfirm := applyFlags.AutoConfirm
autoCommit := applyFlags.AutoCommit
noCommit := applyFlags.NoCommit
noExec := applyFlags.NoExec
term.StartSpinner("")
err := PromptSyncModelsIfNeeded()
if err != nil {
term.OutputErrorAndExit("Error syncing models: %v", err)
}
term.StartSpinner("")
currentPlanState, apiErr := api.Client.GetCurrentPlanState(planId, branch)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error getting current plan state: %v", apiErr)
}
if currentPlanState.HasPendingBuilds() {
plansRunningRes, apiErr := api.Client.ListPlansRunning([]string{CurrentProjectId}, false)
if apiErr != nil {
term.StopSpinner()
term.OutputErrorAndExit("Error getting running plans: %v", apiErr)
}
for _, b := range plansRunningRes.Branches {
if b.PlanId == planId && b.Name == branch {
fmt.Println("This plan is currently active. Please wait for it to finish before applying.")
fmt.Println()
term.PrintCmds("", "ps", "connect")
return
}
}
term.StopSpinner()
fmt.Println("This plan has changes that need to be built before applying")
fmt.Println()
shouldBuild, err := term.ConfirmYesNo("Build changes now?")
if err != nil {
term.OutputErrorAndExit("failed to get confirmation user input: %s", err)
}
if !shouldBuild {
fmt.Println("Apply plan canceled")
os.Exit(0)
}
_, err = buildPlanInlineFn(autoConfirm, nil)
if err != nil {
term.OutputErrorAndExit("failed to build plan: %v", err)
}
}
paths, err := fs.GetProjectPaths(fs.ProjectRoot)
if err != nil {
term.OutputErrorAndExit("error getting project paths: %v", err)
}
anyOutdated, didUpdate, err := CheckOutdatedContextWithOutput(true, autoConfirm, nil, paths)
if err != nil {
term.OutputErrorAndExit("error checking outdated context: %v", err)
}
if anyOutdated && !didUpdate {
term.StopSpinner()
fmt.Println("Apply plan canceled")
os.Exit(0)
}
term.ResumeSpinner()
currentPlanFiles := currentPlanState.CurrentPlanFiles
isRepo := fs.ProjectRootIsGitRepo()
toApply := currentPlanFiles.Files
toRemove := currentPlanFiles.Removed
hasExec := currentPlanFiles.Files["_apply.sh"] != ""
log.Printf("Files to apply: %d, Has exec script: %v", len(toApply), hasExec)
if len(toApply) == 0 && !hasExec {
term.StopSpinner()
fmt.Println("🤷♂️ No changes to apply")
return
}
hasFileChanges := !hasExec || len(toApply) > 1
var toRollback *types.ApplyRollbackPlan
var updatedFiles []string
onErr := func(errMsg string, errArgs ...interface{}) {
term.StopSpinner()
// if toRollback != nil && toRollback.HasChanges() {
// Rollback(toRollback, true)
// }
term.OutputErrorAndExit(errMsg, errArgs...)
}
onGitErr := func(errMsg, unformattedErrMsg string) {
term.StopSpinner()
fmt.Println()
term.OutputSimpleError(errMsg, unformattedErrMsg)
}
log.Println("Has file changes:", hasFileChanges)
if hasFileChanges {
if !autoConfirm {
log.Println("Asking user to confirm applying changes")
term.StopSpinner()
numToApply := len(toApply)
suffix := ""
if numToApply > 1 {
suffix = "s"
}
shouldContinue, err := term.ConfirmYesNo("Apply changes to %d file%s?", numToApply, suffix)
if err != nil {
term.OutputErrorAndExit("failed to get confirmation user input: %s", err)
}
if !shouldContinue {
os.Exit(0)
}
term.ResumeSpinner()
}
log.Println("Applying plan files")
if hasExec {
term.StopSpinner()
fmt.Println("🔄 Tentatively applying changes")
term.ResumeSpinner()
}
updatedFiles, toRollback, err = ApplyFiles(toApply, toRemove, paths)
if err != nil {
onErr("failed to apply files: %s", err)
}
log.Println("Applying plan files complete")
}
onExecSuccess := func() {
term.StartSpinner("")
commitSummary, err := apiApplyPlan(planId, branch)
if err != nil {
onErr("apply plan server error: %s", err)
}
if len(updatedFiles) == 0 {
term.StopSpinner()
fmt.Println("✅ Applied changes, but no files were updated")
} else {
appliedMsgFn := func() {
suffix := ""
if len(updatedFiles) > 1 {
suffix = "s"
}
fmt.Printf("✅ Applied changes, %d file%s updated\n", len(updatedFiles), suffix)
for _, file := range updatedFiles {
fmt.Println(" • 📄 " + file)
}
}
if isRepo && !noCommit {
term.StopSpinner()
gitErr := commitApplied(autoCommit, commitSummary, updatedFiles, currentPlanState)
appliedMsgFn()
if gitErr != nil {
onGitErr("Failed to commit changes:", gitErr.Error())
}
} else {
term.StopSpinner()
appliedMsgFn()
}
}
}
if _, ok := toApply["_apply.sh"]; ok && !noExec {
handleApplyScript(params, toApply, onErr, toRollback, onExecFail, attempt, onExecSuccess)
} else {
onExecSuccess()
}
}
func handleApplyScript(
params ApplyPlanParams,
toApply map[string]string,
onErr types.OnErrFn,
toRollback *types.ApplyRollbackPlan,
onExecFail types.OnApplyExecFailFn,
attempt int,
onSuccess func(),
) {
log.Println("Handling apply script")
term.StopSpinner()
color.New(term.ColorHiCyan, color.Bold).Println("🚀 Commands to execute 👇")
var content string
if params.ExecCommand != "" {
content = params.ExecCommand
} else {
content = toApply["_apply.sh"]
}
md, err := term.GetMarkdown("```bash\n" + content + "\n```")
if err != nil {
onErr("failed to get markdown representation: %s", err)
}
fmt.Println(strings.TrimSpace(md))
log.Println("Asking user to confirm executing apply script")
var confirmed bool
if params.ApplyFlags.AutoExec {
confirmed = true
} else {
confirmed, err = term.ConfirmYesNo("Execute now?")
if err != nil {
onErr("failed to get confirmation user input: %s", err)
}
}
if confirmed {
log.Println("Executing apply script")
execApplyScript(params, toApply, onErr, toRollback, onExecFail, attempt, onSuccess)
} else {
if toRollback != nil && toRollback.HasChanges() {
res, err := term.SelectFromList("Skipping execution. Apply file changes or roll back?", []string{string(types.ApplyRollbackOptionKeep), string(types.ApplyRollbackOptionRollback)})
if err != nil {
onErr("failed to get rollback confirmation user input: %s", err)
}
if res == string(types.ApplyRollbackOptionRollback) {
Rollback(toRollback, true)
os.Exit(0)
} else {
onSuccess()
}
} else {
fmt.Println("🙅♂️ Skipped execution")
fmt.Println("🤷♂️ No changes to apply")
}
}
}
var shellShebangs = map[string]string{
"/bin/bash": `#!/bin/bash
`,
"/bin/zsh": `#!/bin/zsh
`,
}
var applyScriptErrorHandling = map[string]string{
"/bin/bash": `set -euo pipefail`,
"/bin/zsh": `set -euo pipefail`,
}
func execApplyScript(
params ApplyPlanParams,
toApply map[string]string,
onErr types.OnErrFn,
toRollback *types.ApplyRollbackPlan,
onExecFail types.OnApplyExecFailFn,
attempt int,
onSuccess func(),
) {
log.Println("Executing apply script")
color.New(term.ColorHiYellow, color.Bold).Println("👉 For long-running commands, use ctrl+c to exit")
color.New(term.ColorHiCyan, color.Bold).Println("🚀 Executing... output below 👇")
fmt.Println()
var content string
if params.ExecCommand != "" {
content = params.ExecCommand
} else {
content = toApply["_apply.sh"]
}
scriptPath := filepath.Join(fs.ProjectRoot, "_apply.sh")
lines := strings.Split(content, "\n")
filteredLines := []string{}
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#!/") {
continue
}
if strings.HasPrefix(trimmed, "set -") || strings.HasSuffix(trimmed, "pipefail") {
continue
}
if strings.HasPrefix(trimmed, "trap") {
continue
}
filteredLines = append(filteredLines, line)
}
// Detect shell
shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/bash" // fallback
}
// Get appropriate header
shebang := shellShebangs[shell]
if shebang == "" {
shebang = shellShebangs["/bin/bash"] // fallback if shell not supported
}
errorHandling := applyScriptErrorHandling[shell]
if errorHandling == "" {
errorHandling = applyScriptErrorHandling["/bin/bash"] // fallback if shell not supported
}
header := shebang + "\n" + errorHandling
content = header + "\n" + strings.Join(filteredLines, "\n")
err := os.WriteFile(scriptPath, []byte(content), 0755)
if err != nil {
onErr("failed to write _apply.sh: %s", err)
}
execCmd := exec.Command(shell, "-c", scriptPath)
execCmd.Dir = fs.ProjectRoot
execCmd.Env = os.Environ()
execCmd.Stdin = os.Stdin
// Create a pipe for both stdout and stderr
pipe, err := execCmd.StdoutPipe()
if err != nil {
// best effort cleanup
os.Remove(scriptPath)
onErr("failed to create stdout pipe: %s", err)
}
execCmd.Stderr = execCmd.Stdout
// Set platform-specific process attributes
SetPlatformSpecificAttrs(execCmd)
if err := execCmd.Start(); err != nil {
// best effort cleanup
os.Remove(scriptPath)
onErr("failed to start command: %s", err)
}
maybeDeleteCgroup := MaybeIsolateCgroup(execCmd)
pgid, err := syscall.Getpgid(execCmd.Process.Pid)
if err != nil {
log.Printf("Getpgid error: %v", err)
} else {
log.Printf("Child PID=%d PGID=%d", execCmd.Process.Pid, pgid)
}
// Create a context that we can cancel
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Use atomic variable to prevent data races
var interrupted atomic.Bool
// Handle SIGINT and SIGTERM
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
var interruptHandled atomic.Bool
var interruptWG sync.WaitGroup
// Start the interrupt handler goroutine
interruptWG.Add(1)
go func() {
defer interruptWG.Done()
for {
select {
case sig := <-sigChan:
if interruptHandled.CompareAndSwap(false, true) {
fmt.Println()
color.New(term.ColorHiYellow, color.Bold).Println("👉 Caught interrupt. Exiting gracefully...")
interrupted.Store(true)
var sysSig syscall.Signal
switch sig {
case os.Interrupt:
// user pressed Ctrl+C
sysSig = syscall.SIGINT
case syscall.SIGTERM:
// a polite "kill" request
sysSig = syscall.SIGTERM
case syscall.SIGHUP:
sysSig = syscall.SIGHUP
case syscall.SIGQUIT:
sysSig = syscall.SIGQUIT
default:
sysSig = syscall.SIGINT
}
if err := KillProcessGroup(execCmd, sysSig); err != nil {
log.Printf("Failed to send signal %s to process group: %v", sysSig, err)
}
select {
case <-time.After(2 * time.Second):
color.New(term.ColorHiYellow, color.Bold).Println("👉 Commands didn't exit after 2 seconds. Sending SIGKILL.")
if err := KillProcessGroup(execCmd, syscall.SIGKILL); err != nil {
log.Printf("Failed to terminate process group: %v", err)
}
pipe.Close()
if maybeDeleteCgroup != nil {
maybeDeleteCgroup()
}
case <-ctx.Done():
if maybeDeleteCgroup != nil {
maybeDeleteCgroup()
}
return
}
}
case <-ctx.Done():
// If no interrupts occurred, this will be the normal exit path
if maybeDeleteCgroup != nil {
maybeDeleteCgroup()
}
return
}
}
}()
// Read and display output in real-time
scanner := bufio.NewScanner(pipe)
var outputBuilder strings.Builder
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
outputBuilder.WriteString(line + "\n")
}
// Check for scanner errors
if scanErr := scanner.Err(); scanErr != nil {
log.Printf("⚠️ Scanner error reading subprocess output: %v", scanErr)
}
err = execCmd.Wait()
// Ensure interrupt handler fully completes before proceeding
cancel() // cancel the context, if not already
interruptWG.Wait() // wait until the interrupt handler goroutine finishes
signal.Stop(sigChan)
close(sigChan)
success := err == nil
if interrupted.Load() {
os.Remove(scriptPath)
color.New(term.ColorHiYellow, color.Bold).Println("👉 Execution interrupted")
didSucceed, canceled, err := term.ConfirmYesNoCancel("Did the commands succeed?")
if err != nil {
onErr("failed to get confirmation user input: %s", err)
}
success = didSucceed
if canceled {
// rollback and exit
Rollback(toRollback, true)
os.Exit(0)
}
}
// remove _apply.sh without overwriting err val
{
err := os.Remove(scriptPath)
if err != nil && !os.IsNotExist(err) {
onErr("failed to remove _apply.sh: %s", err)
}
}
if !success {
fmt.Println()
color.New(term.ColorHiRed, color.Bold).Println("🚨 Commands failed")
exitErr, ok := err.(*exec.ExitError)
status := -1
if ok {
status = exitErr.ExitCode()
}
onExecFail(status, outputBuilder.String(), attempt, toRollback, onErr, onSuccess)
} else {
fmt.Println()
fmt.Println("✅ Commands succeeded")
onSuccess()
}
}
func apiApplyPlan(planId, branch string) (string, error) {
authVars := MustVerifyAuthVarsSilent(auth.Current.IntegratedModelsMode)
var commitSummary string
log.Println("Applying plan with API call")
commitSummary, apiErr := api.Client.ApplyPlan(planId, branch, shared.ApplyPlanRequest{
AuthVars: authVars,
})
if apiErr != nil {
return "", fmt.Errorf("failed to set pending results applied: %s", apiErr.Msg)
}
return commitSummary, nil
}
func commitApplied(autoCommit bool, commitSummary string, updatedFiles []string, currentPlanState *shared.CurrentPlanState) (err error) {
confirmed := autoCommit
if !autoCommit {
fmt.Println("✏️ Plandex can commit these updates with an automatically generated message.")
fmt.Println()
// fmt.Println("ℹ️ Only the files that Plandex is updating will be included the commit. Any other changes, staged or unstaged, will remain exactly as they are.")
// fmt.Println()
confirmed, err = term.ConfirmYesNo("Commit Plandex updates now?")
if err != nil {
return fmt.Errorf("failed to get confirmation user input: %s", err)
}
}
if confirmed {
// Commit the changes
msg := currentPlanState.PendingChangesSummaryForApply(commitSummary)
// log.Println("Committing changes with message:")
// log.Println(msg)
// spew.Dump(currentPlanState)
err = GitAddAndCommitPaths(fs.ProjectRoot, msg, updatedFiles, true)
if err != nil {
return fmt.Errorf("failed to commit changes: %s", err.Error())
}
}
return nil
}
func ApplyFiles(toApply map[string]string, toRemove map[string]bool, projectPaths *types.ProjectPaths) ([]string, *types.ApplyRollbackPlan, error) {
var updatedFiles []string
toRevert := map[string]types.ApplyReversion{}
var toRemoveOnRollback []string
var mu sync.Mutex
totalOps := len(toApply) + len(toRemove)
errCh := make(chan error, totalOps)
for path, content := range toApply {
if path == "_apply.sh" {
errCh <- nil
continue
}
go func(path, content string) {
// Compute destination path
dstPath := filepath.Join(fs.ProjectRoot, path)
content = strings.ReplaceAll(content, "\\`\\`\\`", "```")
// Check if the file exists
var exists bool
var mode os.FileMode
info, err := os.Stat(dstPath)
if err == nil {
exists = true
mode = info.Mode()
} else {
if os.IsNotExist(err) {
exists = false
} else {
errCh <- fmt.Errorf("failed to check if %s exists: %s", dstPath, err.Error())
return
}
}
if exists {
// read file content
bytes, err := os.ReadFile(dstPath)
if err != nil {
errCh <- fmt.Errorf("failed to read %s: %s", dstPath, err.Error())
return
}
// Check if the file has changed
if string(bytes) == content {
// log.Println("File is unchanged, skipping")
errCh <- nil
return
} else {
mu.Lock()
updatedFiles = append(updatedFiles, path)
toRevert[dstPath] = types.ApplyReversion{Content: string(bytes), Mode: mode}
mu.Unlock()
}
} else {
mu.Lock()
updatedFiles = append(updatedFiles, path)
toRemoveOnRollback = append(toRemoveOnRollback, dstPath)
mu.Unlock()
// Create the directory if it doesn't exist
err := os.MkdirAll(filepath.Dir(dstPath), 0755)
if err != nil {
errCh <- fmt.Errorf("failed to create directory %s: %s", filepath.Dir(dstPath), err.Error())
return
}
}
// Write the file
err = os.WriteFile(dstPath, []byte(content), 0644)
if err != nil {
errCh <- fmt.Errorf("failed to write %s: %s", dstPath, err.Error())
return
}
errCh <- nil
}(path, content)
}
for path, remove := range toRemove {
go func(path string, remove bool) {
if !remove {
errCh <- nil
return
}
// Compute destination path
dstPath := filepath.Join(fs.ProjectRoot, path)
// Check if the file exists
var exists bool
var mode os.FileMode
info, err := os.Stat(dstPath)
if err == nil {
exists = true
mode = info.Mode()
} else {
if os.IsNotExist(err) {
exists = false
} else {
errCh <- fmt.Errorf("failed to check if %s exists: %s", dstPath, err.Error())
return
}
}
if exists {
content, err := os.ReadFile(dstPath)
if err != nil {
errCh <- fmt.Errorf("failed to read %s: %s", dstPath, err.Error())
return
}
err = os.Remove(dstPath)
if err != nil && !os.IsNotExist(err) {
errCh <- fmt.Errorf("failed to remove %s: %s", dstPath, err.Error())
return
}
mu.Lock()
toRevert[dstPath] = types.ApplyReversion{Content: string(content), Mode: mode}
mu.Unlock()
}
errCh <- nil
}(path, remove)
}
for i := 0; i < totalOps; i++ {
err := <-errCh
if err != nil {
return nil, nil, err
}
}
return updatedFiles, &types.ApplyRollbackPlan{
PreviousProjectPaths: projectPaths,
ToRevert: toRevert,
ToRemove: toRemoveOnRollback,
}, nil
}
func Rollback(rollbackPlan *types.ApplyRollbackPlan, msg bool) error {
numRoutines := len(rollbackPlan.ToRevert) + len(rollbackPlan.ToRemove) + 1
errCh := make(chan error, numRoutines)
for path, revert := range rollbackPlan.ToRevert {
go func(path string, revert types.ApplyReversion) {
err := os.WriteFile(path, []byte(revert.Content), revert.Mode)
if err != nil {
errCh <- fmt.Errorf("failed to write %s: %s", path, err.Error())
return
}
errCh <- nil
}(path, revert)
}
for _, path := range rollbackPlan.ToRemove {
go func(path string) {
err := os.Remove(path)
if err != nil {
errCh <- fmt.Errorf("failed to remove %s: %s", path, err.Error())
return
}
errCh <- nil
}(path)
}
go func() {
var err error
updatedProjectPaths, err := fs.GetProjectPaths(fs.ProjectRoot)
if err != nil {
errCh <- fmt.Errorf("failed to get project paths: %v", err)
}
var toRemove []string
for path := range updatedProjectPaths.AllPaths {
if _, ok := rollbackPlan.PreviousProjectPaths.AllPaths[path]; !ok {
toRemove = append(toRemove, path)
}
}
pathsErrCh := make(chan error, len(toRemove))
for _, path := range toRemove {
go func(path string) {
err := os.Remove(path)
pathsErrCh <- err
}(path)
}
for range toRemove {
err := <-pathsErrCh
if err != nil {
errCh <- fmt.Errorf("failed to remove %s: %s", toRemove, err.Error())
return
}
}
errCh <- nil
}()
errs := []error{}
for i := 0; i < numRoutines; i++ {
err := <-errCh
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("failed to rollback: %s", errs)
}
if msg {
fmt.Println("🚫 Rolled back all changes")
}
return nil
}
================================================
FILE: app/cli/lib/apply_cgroup_linux.go
================================================
//go:build linux
// +build linux
package lib
import (
"context"
"fmt"
"log"
"os/exec"
"time"
systemdDbus "github.com/coreos/go-systemd/v22/dbus"
"github.com/godbus/dbus/v5"
"github.com/google/uuid"
)
const cgroupCallTimeout = 1 * time.Second
func MaybeIsolateCgroup(cmd *exec.Cmd) (deleteFn func()) {
noop := func() {}
pid := cmd.Process.Pid
// 1. Connect to the user manager (no prompt on typical distros).
ctx, _ := context.WithTimeout(context.Background(), cgroupCallTimeout)
conn, err := systemdDbus.NewUserConnectionContext(ctx)
if err != nil {
log.Printf("⚠️ Could not connect to user systemd manager. No cgroup isolation for PID %d. Error: %v", pid, err)
return noop
}
// We'll keep 'conn' open while scope is active. The scope isn't strictly tied
// to the connection's lifetime, but it's nice to keep it in case we want to stop the unit.
scopeName := fmt.Sprintf("plandex-%s.scope", uuid.New().String())
props := []systemdDbus.Property{
systemdDbus.PropDescription("Plandex user-scope isolation"),
// Under system manager, user.slice means “treat it as a user process.”
// If this is truly in the user manager, it may ignore the slice or map it differently.
systemdDbus.PropSlice("user.slice"),
// KillMode=control-group: stopping the scope kills all processes in cgroup.
systemdDbus.Property{Name: "KillMode", Value: dbus.MakeVariant("control-group")},
// Attach the existing process by PID
systemdDbus.PropPids(uint32(pid)),
// Optional: auto-remove the scope once no processes remain.
systemdDbus.Property{Name: "CollectMode", Value: dbus.MakeVariant("inactive-or-failed")},
}
_, err = conn.StartTransientUnitContext(ctx, scopeName, "replace", props, nil)
if err != nil {
// Fallback, no isolation
log.Printf("⚠️ Failed to start transient scope for PID %d: %v", pid, err)
return noop
}
return func() {
// Close the connection to the user manager.
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), cgroupCallTimeout)
defer cancel()
// Attempt to stop the scope (killing all processes if any remain).
_, stopErr := conn.StopUnitContext(ctx, scopeName, "replace", nil)
if stopErr != nil {
log.Printf("⚠️ Failed to stop scope %s: %v", scopeName, stopErr)
}
}
}
================================================
FILE: app/cli/lib/apply_cgroup_other.go
================================================
//go:build !linux
// +build !linux
package lib
import "os/exec"
func MaybeIsolateCgroup(cmd *exec.Cmd) (deleteFn func()) {
return func() {}
}
================================================
FILE: app/cli/lib/apply_proc.go
================================================
package lib
import (
"os/exec"
"syscall"
)
func SetPlatformSpecificAttrs(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
}
func KillProcessGroup(cmd *exec.Cmd, signal syscall.Signal) error {
return syscall.Kill(-cmd.Process.Pid, signal)
}
================================================
FILE: app/cli/lib/build.go
================================================
package lib
import shared "plandex-shared"
var buildPlanInlineFn func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error)
func SetBuildPlanInlineFn(fn func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error)) {
buildPlanInlineFn = fn
}
================================================
FILE: app/cli/lib/claude_max.go
================================================
package lib
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"plandex-cli/term"
"plandex-cli/types"
"plandex-cli/ui"
shared "plandex-shared"
"strings"
"time"
"github.com/fatih/color"
)
const claudeMaxClientId = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
const claudeMaxScopes = "org:create_api_key user:profile user:inference"
const claudeMaxRedirect = "https://console.anthropic.com/oauth/code/callback"
const claudeMaxTokenUrl = "https://console.anthropic.com/v1/oauth/token"
func hasAnthropicModels(opts shared.ModelProviderOptions) bool {
for _, opt := range opts {
if opt.Config.Provider == shared.ModelProviderAnthropic {
return true
}
}
return false
}
func promptClaudeMaxIfNeeded() bool {
orgUserConfig := MustGetOrgUserConfig()
if orgUserConfig.PromptedClaudeMax {
return false
}
term.StopSpinner()
fmt.Println("ℹ️ The current model pack uses Anthropic models.\n\nIf you have a " + color.New(color.FgHiGreen, color.Bold).Sprint("Claude Pro or Max Subscription") + ", you can connect to it.\n\nPlandex will then use your Claude subscription for Anthropic model calls up to your limit.\n")
res, err := term.ConfirmYesNo("Connect your Claude subscription?")
if err != nil {
term.OutputErrorAndExit("Error confirming claude connection: %v", err)
}
// update org user config to avoid prompting again
orgUserConfig.PromptedClaudeMax = true
MustUpdateOrgUserConfig(*orgUserConfig)
if !res {
fmt.Println()
fmt.Println("To connect a Claude subscription later, run:\n" + term.ShowCmd("connect-claude"))
fmt.Println()
return false
}
ConnectClaudeMax()
return true
}
func connectClaudeMaxIfNeeded() bool {
accountCreds, err := GetAccountCredentials()
if err != nil {
term.OutputErrorAndExit("Error getting account credentials: %v", err)
}
if accountCreds == nil || accountCreds.ClaudeMax == nil {
term.StopSpinner()
fmt.Println("ℹ️ You connected a " + color.New(color.FgHiGreen, color.Bold).Sprint("Claude Pro or Max subscription,") + "\nbut credentials weren't found on this device.\n")
res, err := term.ConfirmYesNo("Connect your Claude subscription?")
if err != nil {
term.OutputErrorAndExit("Error confirming claude connection: %v", err)
}
if res {
ConnectClaudeMax()
return true
}
}
return false
}
func refreshClaudeMaxCredsIfNeeded() {
accountCreds, err := GetAccountCredentials()
if err != nil {
term.OutputErrorAndExit("Error getting account credentials: %v", err)
}
if accountCreds == nil || accountCreds.ClaudeMax == nil {
return
}
if !needsRefresh(accountCreds.ClaudeMax) || accountCreds.ClaudeMax.RefreshToken == "" {
return
}
_, status, err := refreshCreds(accountCreds)
if err != nil {
if status == http.StatusUnauthorized {
term.StopSpinner()
color.New(color.FgHiYellow, color.Bold).Println("⚠️ Your Claude subscription's connection has been lost")
fmt.Println()
res, err := term.ConfirmYesNo("Reconnect your Claude subscription?")
if err != nil {
term.OutputErrorAndExit("Error confirming claude connection: %v", err)
}
if !res {
accountCreds.ClaudeMax = nil
if err := SetAccountCredentials(accountCreds); err != nil {
term.OutputErrorAndExit("Error clearing Claude credentials: %v", err)
}
return
}
ConnectClaudeMax()
return
}
term.OutputErrorAndExit("Error refreshing Claude credentials: %v", err)
}
}
func ConnectClaudeMax() {
connectClaudeMaxOauth()
term.StartSpinner("")
orgUserConfig := MustGetOrgUserConfig()
orgUserConfig.UseClaudeSubscription = true
MustUpdateOrgUserConfig(*orgUserConfig)
term.StopSpinner()
fmt.Println()
fmt.Println("✅ Your Claude subscription is now connected")
fmt.Println()
fmt.Println("To disconnect, run:\n" + term.ShowCmd("disconnect-claude"))
fmt.Println()
}
func DisconnectClaudeMax() {
term.StartSpinner("")
orgUserConfig := MustGetOrgUserConfig()
orgUserConfig.UseClaudeSubscription = false
MustUpdateOrgUserConfig(*orgUserConfig)
accountCreds, err := GetAccountCredentials()
if err != nil {
term.OutputErrorAndExit("Error getting account credentials: %v", err)
}
if accountCreds != nil {
accountCreds.ClaudeMax = nil
if err := SetAccountCredentials(accountCreds); err != nil {
term.OutputErrorAndExit("Error clearing Claude credentials: %v", err)
}
}
term.StopSpinner()
fmt.Println("✅ Your Claude subscription has been disconnected")
fmt.Println()
fmt.Println("To reconnect, run:\n" + term.ShowCmd("connect-claude"))
fmt.Println()
}
func connectClaudeMaxOauth() {
verifier, err := genCodeVerifier()
if err != nil {
term.OutputErrorAndExit("Error generating code verifier: %v", err)
}
challenge := sha256Base64(verifier)
state, err := genCodeVerifier()
if err != nil {
term.OutputErrorAndExit("Error generating state: %v", err)
}
authURL := fmt.Sprintf(
"https://claude.ai/oauth/authorize?code=true&client_id=%s&response_type=code&scope=%s&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256&state=%s",
claudeMaxClientId, url.QueryEscape(claudeMaxScopes), url.QueryEscape(claudeMaxRedirect), challenge, state,
)
term.StopSpinner()
fmt.Println()
ui.OpenURL("Opening Claude authentication page in your default browser...", authURL)
fmt.Println()
color.New(color.FgHiGreen, color.Bold).Println("📋 Click 'Authorize', copy the Authentication Code, then paste it below.")
fmt.Println()
pastedCode, err := term.GetUserPasswordInput("Authentication Code:")
if err != nil {
term.OutputErrorAndExit("Error reading pasted authentication code: %v", err)
}
split := strings.SplitN(pastedCode, "#", 2)
if len(split) != 2 {
term.OutputErrorAndExit("Invalid authentication code: %s", pastedCode)
}
code := split[0]
pastedState := split[1]
if code == "" || pastedState != state {
term.OutputErrorAndExit("Claude authentication failed: missing or mismatched oauth code/state")
}
term.StartSpinner("")
tokens, err := exchangeCode(code, verifier, state)
if err != nil {
term.OutputErrorAndExit("Error exchanging code: %v", err)
}
creds := types.AccountCredentials{
ClaudeMax: &types.OauthCreds{
OauthResponse: *tokens,
ExpiresAt: time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second),
},
}
if err := SetAccountCredentials(&creds); err != nil {
term.OutputErrorAndExit("Error setting account credentials: %v", err)
}
}
func exchangeCode(code, verifier, state string) (*types.OauthResponse, error) {
body, _ := json.Marshal(map[string]any{
"grant_type": "authorization_code",
"code": code,
"state": state,
"code_verifier": verifier,
"redirect_uri": claudeMaxRedirect,
"client_id": claudeMaxClientId,
})
req, err := http.NewRequest("POST", claudeMaxTokenUrl, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("token exchange failed - error creating request: %s", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-beta", shared.AnthropicClaudeMaxBetaHeader)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("token exchange failed - error reading body: %s", err)
}
return nil, fmt.Errorf("token exchange failed - status: %d, body: %s", resp.StatusCode, b)
}
var t types.OauthResponse
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
return nil, err
}
return &t, nil
}
func genCodeVerifier() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func sha256Base64(verifier string) string {
sum := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(sum[:])
}
func needsRefresh(creds *types.OauthCreds) bool {
// refresh an hour early so we can make multiple calls before it expires
return time.Now().After(creds.ExpiresAt.Add(-1 * time.Hour))
}
func refreshCreds(accountCreds *types.AccountCredentials) (*types.OauthCreds, int, error) {
creds := accountCreds.ClaudeMax
if creds == nil {
return nil, 0, fmt.Errorf("no stored Claude credentials")
}
body, err := json.Marshal(map[string]any{
"grant_type": "refresh_token",
"refresh_token": creds.RefreshToken,
"client_id": claudeMaxClientId,
})
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - marshal: %w", err)
}
req, err := http.NewRequest("POST", claudeMaxTokenUrl, bytes.NewReader(body))
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-beta", shared.AnthropicClaudeMaxBetaHeader)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - http: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, fmt.Errorf("refresh failed - read body: %w", err)
}
return nil, resp.StatusCode, fmt.Errorf("refresh failed - status %d: %s", resp.StatusCode, b)
}
var r types.OauthResponse
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return nil, 0, fmt.Errorf("refresh failed - decode: %w", err)
}
newCreds := &types.OauthCreds{
OauthResponse: r,
ExpiresAt: time.Now().Add(time.Duration(r.ExpiresIn) * time.Second),
}
// persist updated creds
accountCreds.ClaudeMax = newCreds
if err := SetAccountCredentials(accountCreds); err != nil {
return nil, 0, fmt.Errorf("refresh failed - save: %w", err)
}
return newCreds, resp.StatusCode, nil
}
================================================
FILE: app/cli/lib/context_auto_load.go
================================================
package lib
import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"plandex-cli/api"
"plandex-cli/types"
shared "plandex-shared"
"sync"
"github.com/sashabaranov/go-openai"
)
func AutoLoadContextFiles(ctx context.Context, files []string) (string, error) {
contexts, err := api.Client.ListContext(CurrentPlanId, CurrentBranch)
if err != nil {
return "", fmt.Errorf("failed to get contexts: %v", err)
}
var totalSize int64
totalContexts := len(contexts)
for _, context := range contexts {
totalSize += context.BodySize
}
loadContextReqsByIndex := make(map[int]*shared.LoadContextParams)
filesSkippedTooLarge := []filePathWithSize{}
filesSkippedAfterSizeLimit := []string{}
var mu sync.Mutex
errCh := make(chan error, len(files))
for i, path := range files {
totalContexts++
if totalContexts > shared.MaxContextCount {
log.Println("Skipping file", path, "because it would exceed the max context count", totalContexts)
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
errCh <- nil
continue
}
go func(index int, path string) {
fileInfo, err := os.Stat(path)
if err != nil {
errCh <- fmt.Errorf("failed to get file info for %s: %v", path, err)
return
}
if fileInfo.IsDir() {
log.Println("Skipping directory", path)
errCh <- nil // skip directories
return
}
size := fileInfo.Size()
mu.Lock()
if size > shared.MaxContextBodySize {
log.Println("Skipping file", path, "because it's too large", size)
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: path, Size: size})
mu.Unlock()
errCh <- nil
return
}
if totalSize+size > shared.MaxTotalContextSize {
log.Println("Skipping file", path, "because it would exceed the max context body size", totalSize+size)
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
mu.Unlock()
errCh <- nil
return
}
totalSize += size
mu.Unlock()
b, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("failed to read file %s: %v", path, err)
return
}
var contextType shared.ContextType
isImage := shared.IsImageFile(path)
if isImage {
contextType = shared.ContextImageType
} else {
contextType = shared.ContextFileType
}
var imageDetail openai.ImageURLDetail
if isImage {
imageDetail = openai.ImageURLDetailHigh
}
var body string
if isImage {
body = base64.StdEncoding.EncodeToString(b)
} else {
body = string(shared.NormalizeEOL(b))
}
mu.Lock()
loadContextReqsByIndex[index] = &shared.LoadContextParams{
ContextType: contextType,
FilePath: path,
Name: path,
Body: body,
AutoLoaded: true,
ImageDetail: imageDetail,
}
mu.Unlock()
errCh <- nil
}(i, path)
}
for range files {
if e := <-errCh; e != nil {
return "", fmt.Errorf("failed to load context: %v", e)
}
}
// Convert map back to ordered slice
loadContextReqs := make(shared.LoadContextRequest, 0, len(loadContextReqsByIndex))
for i := 0; i < len(files); i++ {
if req := loadContextReqsByIndex[i]; req != nil {
loadContextReqs = append(loadContextReqs, req)
}
}
// even if there are no files to load, we still need to hit the API endpoint because the stream is waiting on a channel for the autoload to finish
res, apiErr := api.Client.AutoLoadContext(ctx, CurrentPlanId, CurrentBranch, loadContextReqs)
if apiErr != nil {
return "", fmt.Errorf("failed to load context: %v", apiErr.Msg)
}
if res.MaxTokensExceeded {
overage := res.TotalTokens - res.MaxTokens
return "", fmt.Errorf("update would add %d 🪙 and exceed token limit (%d) by %d 🪙", res.TokensAdded, res.MaxTokens, overage)
}
msg := res.Msg
// Print skip info if any
if len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 {
msg += "\n\n" + getSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit, nil, nil)
}
return msg, nil
}
func MustLoadAutoContextMap() {
MustLoadContext([]string{"."}, &types.LoadContextParams{
DefsOnly: true,
SkipIgnoreWarning: true,
AutoLoaded: true,
})
}
================================================
FILE: app/cli/lib/context_conflict.go
================================================
package lib
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/term"
"github.com/fatih/color"
)
func checkContextConflicts(filesByPath map[string]string) (bool, error) {
// log.Println("Checking for context conflicts.")
// log.Println(spew.Sdump(filesByPath))
currentPlan, err := api.Client.GetCurrentPlanState(CurrentPlanId, CurrentBranch)
if err != nil {
return false, fmt.Errorf("error getting current plan state: %v", err)
}
conflictedPaths := currentPlan.PlanResult.FileResultsByPath.ConflictedPaths(filesByPath)
// log.Println("Conflicted paths:", conflictedPaths)
if len(conflictedPaths) > 0 {
term.StopSpinner()
color.New(color.Bold, term.ColorHiYellow).Println("⚠️ Some updates conflict with pending changes:")
for path := range conflictedPaths {
fmt.Println("📄 " + path)
}
fmt.Println()
res, err := term.ConfirmYesNo("Update context and rebuild changes?")
if err != nil {
return false, fmt.Errorf("error confirming update and rebuild: %v", err)
}
if !res {
fmt.Println("Context update canceled")
os.Exit(0)
}
}
return len(conflictedPaths) > 0, nil
}
================================================
FILE: app/cli/lib/context_display.go
================================================
package lib
import shared "plandex-shared"
func GetContextLabelAndIcon(contextType shared.ContextType) (string, string) {
var icon string
var lbl string
switch contextType {
case shared.ContextFileType:
icon = "📄"
lbl = "file"
case shared.ContextURLType:
icon = "🌎"
lbl = "url"
case shared.ContextDirectoryTreeType:
icon = "🗂 "
lbl = "tree"
case shared.ContextNoteType:
icon = "✏️ "
lbl = "note"
case shared.ContextPipedDataType:
icon = "↔️ "
lbl = "piped"
case shared.ContextImageType:
icon = "🖼️ "
lbl = "image"
case shared.ContextMapType:
icon = "🗺️ "
lbl = "map"
}
return lbl, icon
}
func FindContextByIndex(contexts []*shared.Context, index int) *shared.Context {
// Convert to 0-based index
index--
if index < 0 || index >= len(contexts) {
return nil
}
return contexts[index]
}
func FindContextByName(contexts []*shared.Context, name string) *shared.Context {
for _, ctx := range contexts {
if ctx.Name == name || ctx.FilePath == name {
return ctx
}
}
return nil
}
================================================
FILE: app/cli/lib/context_load.go
================================================
package lib
import (
"bufio"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/term"
"plandex-cli/types"
"plandex-cli/url"
"strings"
"sync"
shared "plandex-shared"
"github.com/fatih/color"
)
const maxSkippedFileList = 20
func MustLoadContext(resources []string, params *types.LoadContextParams) {
if params.DefsOnly {
// while caching is set up to work with multiple map paths, it can end up in a partially loaded state if token limits are exceeded, so better to just load one at a time
if len(resources) > 1 {
term.OutputErrorAndExit("Please load a single map directory at a time")
}
term.LongSpinnerWithWarning("🗺️ Building project map...", "🗺️ This can take a while in larger projects...")
} else if params.NamesOnly {
term.LongSpinnerWithWarning("🌳 Loading directory tree...", "🌳 This can take a while in larger projects...")
} else {
term.StartSpinner("📥 Loading context...")
}
onErr := func(err error) {
term.StopSpinner()
term.OutputErrorAndExit("Failed to load context: %v", err)
}
var loadContextReq shared.LoadContextRequest
fileInfo, err := os.Stdin.Stat()
if err != nil {
onErr(fmt.Errorf("failed to stat stdin: %v", err))
}
var authVars map[string]string
var openAIBase string
if params.Note != "" || fileInfo.Mode()&os.ModeNamedPipe != 0 {
authVars = MustVerifyAuthVarsSilent(auth.Current.IntegratedModelsMode)
}
if params.Note != "" {
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
ContextType: shared.ContextNoteType,
Body: params.Note,
ApiKeys: authVars,
OpenAIBase: openAIBase,
OpenAIOrgId: os.Getenv("OPENAI_ORG_ID"),
SessionId: params.SessionId,
AutoLoaded: params.AutoLoaded,
})
}
if fileInfo.Mode()&os.ModeNamedPipe != 0 {
reader := bufio.NewReader(os.Stdin)
pipedData, err := io.ReadAll(reader)
if err != nil {
onErr(fmt.Errorf("failed to read piped data: %v", err))
}
if len(pipedData) > 0 {
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
ContextType: shared.ContextPipedDataType,
Body: string(pipedData),
ApiKeys: authVars,
OpenAIBase: openAIBase,
OpenAIOrgId: os.Getenv("OPENAI_ORG_ID"),
SessionId: params.SessionId,
AutoLoaded: params.AutoLoaded,
})
}
}
var inputUrls []string
var inputFilePaths []string
if len(resources) > 0 {
for _, resource := range resources {
// so far resources are either files or urls
if url.IsValidURL(resource) {
inputUrls = append(inputUrls, resource)
} else {
if strings.HasPrefix(resource, "."+string(os.PathSeparator)) {
resource = resource[2:]
}
inputFilePaths = append(inputFilePaths, resource)
}
}
}
var contextMu sync.Mutex
errCh := make(chan error)
ignoredPaths := make(map[string]string)
mapFilesTruncatedTooLarge := []filePathWithSize{}
mapFilesSkippedAfterSizeLimit := []string{}
// We'll reuse these for all skipping, including directory-tree partial skipping and URLs
filesSkippedTooLarge := []filePathWithSize{}
filesSkippedAfterSizeLimit := []string{}
var totalSize int64
numRoutines := 0
// filter out already loaded contexts
alreadyLoadedByComposite := make(map[string]*shared.Context)
existingContexts, apiErr := api.Client.ListContext(CurrentPlanId, CurrentBranch)
if apiErr != nil {
onErr(fmt.Errorf("failed to list contexts: %v", apiErr.Msg))
}
existsByComposite := make(map[string]*shared.Context)
for _, context := range existingContexts {
switch context.ContextType {
case shared.ContextFileType, shared.ContextDirectoryTreeType, shared.ContextMapType, shared.ContextImageType:
existsByComposite[strings.Join([]string{string(context.ContextType), context.FilePath}, "|")] = context
case shared.ContextURLType:
existsByComposite[strings.Join([]string{string(context.ContextType), context.Url}, "|")] = context
}
}
var cachedMapPaths map[string]bool
var cachedMapLoadRes *shared.LoadContextResponse
mapInputShas := map[string]string{}
mapInputTokens := map[string]int{}
mapInputSizes := map[string]int64{}
toLoadMapPaths := []string{}
mapInputPathsForPaths := map[string]string{}
currentMapInputBatch := shared.FileMapInputs{}
mapInputBatches := []shared.FileMapInputs{currentMapInputBatch}
sem := make(chan struct{}, ContextMapMaxClientConcurrency)
if len(inputFilePaths) > 0 {
var mapSize int64
if params.DefsOnly {
for _, inputFilePath := range inputFilePaths {
composite := strings.Join([]string{string(shared.ContextMapType), inputFilePath}, "|")
if existsByComposite[composite] != nil {
alreadyLoadedByComposite[composite] = existsByComposite[composite]
continue
}
toLoadMapPaths = append(toLoadMapPaths, inputFilePath)
}
var uncachedMapPaths []string
res, err := api.Client.LoadCachedFileMap(CurrentPlanId, CurrentBranch, shared.LoadCachedFileMapRequest{
FilePaths: toLoadMapPaths,
})
if err != nil {
onErr(fmt.Errorf("error checking cached file map: %v", err))
}
if res.LoadRes != nil {
if res.LoadRes.MaxTokensExceeded {
term.StopSpinner()
overage := res.LoadRes.TotalTokens - res.LoadRes.MaxTokens
term.OutputErrorAndExit("Update would add %d 🪙 and exceed token limit (%d) by %d 🪙\n", res.LoadRes.TokensAdded, res.LoadRes.MaxTokens, overage)
}
cachedMapLoadRes = res.LoadRes
cachedMapPaths = res.CachedByPath
for _, path := range toLoadMapPaths {
if !cachedMapPaths[path] {
uncachedMapPaths = append(uncachedMapPaths, path)
}
}
} else {
uncachedMapPaths = toLoadMapPaths
}
toLoadMapPaths = uncachedMapPaths
inputFilePaths = toLoadMapPaths
}
if len(inputFilePaths) > 0 {
baseDir := fs.GetBaseDirForFilePaths(inputFilePaths)
paths, err := fs.GetProjectPaths(baseDir)
if err != nil {
onErr(fmt.Errorf("failed to get project paths: %v", err))
}
if !params.ForceSkipIgnore {
var filteredPaths []string
for _, inputFilePath := range inputFilePaths {
if _, ok := paths.ActivePaths[inputFilePath]; !ok {
ignored, reason, err := fs.IsIgnored(paths, inputFilePath, baseDir)
if err != nil {
onErr(fmt.Errorf("failed to check if %s is ignored: %v", inputFilePath, err))
}
if ignored {
ignoredPaths[inputFilePath] = reason
}
} else {
filteredPaths = append(filteredPaths, inputFilePath)
}
}
inputFilePaths = filteredPaths
}
if params.NamesOnly {
// "params.NamesOnly" => we create directory-tree contexts (ContextDirectoryTreeType)
// Partial skipping of subpaths
for _, inputFilePath := range inputFilePaths {
composite := strings.Join([]string{string(shared.ContextDirectoryTreeType), inputFilePath}, "|")
if existsByComposite[composite] != nil {
alreadyLoadedByComposite[composite] = existsByComposite[composite]
continue
}
numRoutines++
go func(inputFilePath string) {
sem <- struct{}{}
defer func() { <-sem }()
flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{
FileOrDirPaths: []string{inputFilePath},
BaseDir: baseDir,
ProjectPaths: paths,
LoadParams: params,
})
if err != nil {
errCh <- fmt.Errorf("failed to parse input paths: %v", err)
return
}
if !params.ForceSkipIgnore {
var filteredPaths []string
for _, path := range flattenedPaths {
if _, ok := paths.ActivePaths[path]; ok {
filteredPaths = append(filteredPaths, path)
} else {
ignored, reason, err := fs.IsIgnored(paths, path, baseDir)
if err != nil {
errCh <- fmt.Errorf("failed to check if %s is ignored: %v", path, err)
return
}
if ignored {
ignoredPaths[path] = reason
}
}
}
flattenedPaths = filteredPaths
}
// PARTIAL skipping of subpaths
var keptPaths []string
for _, p := range flattenedPaths {
lineSize := int64(len(p))
contextMu.Lock()
if lineSize > shared.MaxContextBodySize {
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: p, Size: lineSize})
contextMu.Unlock()
continue
}
if totalSize+lineSize > shared.MaxContextBodySize {
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, p)
contextMu.Unlock()
continue
}
totalSize += lineSize
contextMu.Unlock()
keptPaths = append(keptPaths, p)
}
body := strings.Join(keptPaths, "\n")
name := inputFilePath
if name == "." {
name = "cwd"
}
if name == ".." {
name = "parent"
}
contextMu.Lock()
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
ContextType: shared.ContextDirectoryTreeType,
Name: name,
Body: body,
FilePath: inputFilePath,
ForceSkipIgnore: params.ForceSkipIgnore,
AutoLoaded: params.AutoLoaded,
})
contextMu.Unlock()
errCh <- nil
}(inputFilePath)
}
} else {
flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{
FileOrDirPaths: inputFilePaths,
BaseDir: baseDir,
ProjectPaths: paths,
LoadParams: params,
})
if err != nil {
onErr(fmt.Errorf("failed to parse input paths: %v", err))
}
if !params.ForceSkipIgnore {
var filteredPaths []string
for _, path := range flattenedPaths {
if _, ok := paths.ActivePaths[path]; ok {
filteredPaths = append(filteredPaths, path)
} else {
ignored, reason, err := fs.IsIgnored(paths, path, baseDir)
if err != nil {
onErr(fmt.Errorf("failed to check if %s is ignored: %v", path, err))
}
if ignored {
ignoredPaths[path] = reason
}
}
}
flattenedPaths = filteredPaths
}
var numPaths int
if params.DefsOnly {
filtered := []string{}
for _, path := range flattenedPaths {
if shared.HasFileMapSupport(path) {
numPaths++
if numPaths > shared.MaxContextMapPaths {
mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, path)
continue
}
filtered = append(filtered, path)
}
}
flattenedPaths = filtered
} else if params.NamesOnly {
filtered := []string{}
for _, path := range flattenedPaths {
numPaths++
if numPaths > shared.MaxContextMapPaths {
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
continue
}
filtered = append(filtered, path)
}
flattenedPaths = filtered
} else {
filtered := []string{}
for _, path := range flattenedPaths {
numPaths++
if numPaths > shared.MaxContextCount {
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
continue
}
filtered = append(filtered, path)
}
flattenedPaths = filtered
}
inputFilePaths = flattenedPaths
for _, path := range flattenedPaths {
var mapInputPath string
if params.DefsOnly {
for _, inputPath := range toLoadMapPaths {
absPath, err := filepath.Abs(path)
if err != nil {
continue
}
absInputPath, err := filepath.Abs(inputPath)
if err != nil {
continue
}
if absPath == absInputPath ||
strings.HasPrefix(absPath+string(os.PathSeparator), absInputPath+string(os.PathSeparator)) {
mapInputPath = inputPath
break
}
}
if mapInputPath == "" {
continue
}
mapInputPathsForPaths[path] = mapInputPath
}
var contextType shared.ContextType
isImage := shared.IsImageFile(path)
if isImage {
contextType = shared.ContextImageType
} else if params.DefsOnly {
contextType = shared.ContextMapType
} else {
contextType = shared.ContextFileType
}
if !params.DefsOnly {
composite := strings.Join([]string{string(contextType), path}, "|")
if existsByComposite[composite] != nil {
alreadyLoadedByComposite[composite] = existsByComposite[composite]
continue
}
}
numRoutines++
go func(path string) {
sem <- struct{}{}
defer func() { <-sem }()
var size int64
fileInfo, err := os.Stat(path)
if err != nil {
errCh <- fmt.Errorf("failed to get file info for %s: %v", path, err)
return
}
size = fileInfo.Size()
if !params.DefsOnly && size > shared.MaxContextBodySize {
contextMu.Lock()
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: path, Size: size})
contextMu.Unlock()
errCh <- nil
return
}
if !params.DefsOnly {
contextMu.Lock()
if totalSize+size > shared.MaxContextBodySize {
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)
contextMu.Unlock()
errCh <- nil
return
}
totalSize += size
contextMu.Unlock()
}
if params.DefsOnly {
res, err := getMapFileDetails(path, size, totalSize)
if err != nil {
errCh <- fmt.Errorf("failed to get map file details for %s: %v", path, err)
return
}
contextMu.Lock()
defer contextMu.Unlock()
if currentMapInputBatch.NumFiles()+1 > shared.ContextMapMaxBatchSize || currentMapInputBatch.TotalSize()+size > shared.ContextMapMaxBatchBytes {
currentMapInputBatch = shared.FileMapInputs{}
mapInputBatches = append(mapInputBatches, currentMapInputBatch)
}
currentMapInputBatch[path] = res.mapContent
mapSize += res.size
mapInputShas[path] = res.shaVal
mapInputTokens[path] = res.tokens
mapInputSizes[path] = res.size
if len(res.mapFilesTruncatedTooLarge) > 0 {
mapFilesTruncatedTooLarge = append(mapFilesTruncatedTooLarge, res.mapFilesTruncatedTooLarge...)
}
if len(res.mapFilesSkippedAfterSizeLimit) > 0 {
mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, res.mapFilesSkippedAfterSizeLimit...)
}
} else if isImage {
fileContent, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("failed to read the file %s: %v", path, err)
return
}
contextMu.Lock()
defer contextMu.Unlock()
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
ContextType: shared.ContextImageType,
Name: path,
Body: base64.StdEncoding.EncodeToString(fileContent),
FilePath: path,
ImageDetail: params.ImageDetail,
AutoLoaded: params.AutoLoaded,
})
} else {
fileContent, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("failed to read the file %s: %v", path, err)
return
}
fileContent = shared.NormalizeEOL(fileContent)
contextMu.Lock()
defer contextMu.Unlock()
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
ContextType: shared.ContextFileType,
Name: path,
Body: string(fileContent),
FilePath: path,
AutoLoaded: params.AutoLoaded,
})
}
errCh <- nil
}(path)
}
}
}
}
if len(inputUrls) > 0 {
for _, u := range inputUrls {
composite := strings.Join([]string{string(shared.ContextURLType), u}, "|")
if existsByComposite[composite] != nil {
alreadyLoadedByComposite[composite] = existsByComposite[composite]
continue
}
numRoutines++
go func(u string) {
sem <- struct{}{}
defer func() { <-sem }()
body, err := url.FetchURLContent(u)
if err != nil {
errCh <- fmt.Errorf("failed to fetch content from URL %s: %v", u, err)
return
}
name := url.SanitizeURL(u)
// show the first 20 characters, then ellipsis then the last 20 characters of 'name'
if len(name) > 40 {
name = name[:20] + "⋯" + name[len(name)-20:]
}
// Check the size of the URL body, just like a file:
size := int64(len(body))
contextMu.Lock()
defer contextMu.Unlock()
if size > shared.MaxContextBodySize {
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: u, Size: size})
errCh <- nil
return
}
if totalSize+size > shared.MaxContextBodySize {
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, u)
errCh <- nil
return
}
totalSize += size
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
ContextType: shared.ContextURLType,
Name: name,
Body: body,
Url: u,
AutoLoaded: params.AutoLoaded,
})
errCh <- nil
}(u)
}
}
for i := 0; i < numRoutines; i++ {
err := <-errCh
if err != nil {
onErr(err)
}
}
if params.DefsOnly {
allMapBodies, err := processMapBatches(mapInputBatches)
if err != nil {
onErr(fmt.Errorf("failed to process map batches: %v", err))
}
for _, inputPath := range toLoadMapPaths {
var name string
if inputPath == "." {
name = "cwd"
} else if inputPath == ".." {
name = "parent"
} else {
name = inputPath
}
pathBodies := shared.FileMapBodies{}
pathShas := map[string]string{}
pathTokens := map[string]int{}
pathSizes := map[string]int64{}
for path, body := range allMapBodies {
mapInputPath := mapInputPathsForPaths[path]
if mapInputPath == inputPath {
pathBodies[path] = body
pathShas[path] = mapInputShas[path]
pathTokens[path] = mapInputTokens[path]
pathSizes[path] = mapInputSizes[path]
}
}
// load the map even if it's empty (no paths)
// it needs to exist so it can be updated later
loadContextReq = append(loadContextReq, &shared.LoadContextParams{
ContextType: shared.ContextMapType,
Name: name,
MapBodies: pathBodies,
InputShas: pathShas,
InputTokens: pathTokens,
InputSizes: pathSizes,
FilePath: inputPath,
AutoLoaded: params.AutoLoaded,
})
}
}
filesToLoad := map[string]string{}
for _, context := range loadContextReq {
if context.ContextType == shared.ContextFileType {
filesToLoad[context.FilePath] = context.Body
}
}
hasConflicts, err := checkContextConflicts(filesToLoad)
if err != nil {
onErr(fmt.Errorf("failed to check context conflicts: %v", err))
}
if len(loadContextReq)+len(cachedMapPaths) == 0 {
term.StopSpinner()
fmt.Println("🤷♂️ No context loaded")
didOutputReason := false
if len(alreadyLoadedByComposite) > 0 {
printAlreadyLoadedMsg(alreadyLoadedByComposite)
didOutputReason = true
}
if len(ignoredPaths) > 0 && !params.SkipIgnoreWarning {
printIgnoredMsg()
didOutputReason = true
}
if !didOutputReason {
fmt.Println()
fmt.Printf("Use %s to load a file or URL:", color.New(color.BgCyan, color.FgHiWhite).Sprint(" plandex load [file-path|url] "))
fmt.Println()
fmt.Println("plandex load file.c file.h")
fmt.Println("plandex load https://github.com/some-org/some-repo/README.md")
fmt.Println()
fmt.Printf("%s with the --recursive/-r flag:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Load a whole directory"))
fmt.Println("plandex load app/src -r")
fmt.Println()
fmt.Printf("%s with the --tree flag:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Load a directory layout (file names only)"))
fmt.Println()
fmt.Printf("%s file paths are relative to the current directory\n", color.New(color.Bold, term.ColorHiYellow).Sprint("Note:"))
fmt.Println()
fmt.Printf("%s with the -n flag:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Load a note"))
fmt.Println("plandex load -n 'Some note here'")
fmt.Println()
fmt.Printf("%s from any command:\n", color.New(color.Bold, term.ColorHiCyan).Sprint("Pipe data in"))
fmt.Println("npm test | plandex load")
}
os.Exit(0)
}
var res *shared.LoadContextResponse
if cachedMapLoadRes != nil {
res = cachedMapLoadRes
} else {
res, apiErr = api.Client.LoadContext(CurrentPlanId, CurrentBranch, loadContextReq)
if apiErr != nil {
onErr(fmt.Errorf("failed to load context: %v", apiErr.Msg))
}
}
term.StopSpinner()
if hasConflicts {
term.StartSpinner("🏗️ Starting build...")
_, err := buildPlanInlineFn(false, nil)
if err != nil {
onErr(fmt.Errorf("failed to build plan: %v", err))
}
fmt.Println()
}
fmt.Println("✅ " + res.Msg)
if len(alreadyLoadedByComposite) > 0 {
printAlreadyLoadedMsg(alreadyLoadedByComposite)
}
if len(ignoredPaths) > 0 && !params.SkipIgnoreWarning {
printIgnoredMsg()
}
if len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 ||
len(mapFilesTruncatedTooLarge) > 0 || len(mapFilesSkippedAfterSizeLimit) > 0 {
printSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit,
mapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit)
}
}
func printAlreadyLoadedMsg(alreadyLoadedByComposite map[string]*shared.Context) {
fmt.Println()
pronoun := "they're"
if len(alreadyLoadedByComposite) == 1 {
pronoun = "it's"
}
fmt.Printf("🙅♂️ Skipped because %s already in context:\n", pronoun)
for _, context := range alreadyLoadedByComposite {
_, icon := context.TypeAndIcon()
fmt.Printf(" • %s %s\n", icon, context.Name)
}
}
func printIgnoredMsg() {
fmt.Println()
fmt.Println("ℹ️ " + color.New(color.FgWhite).Sprint("Due to .gitignore or .plandexignore, some paths weren't loaded.\nUse --force / -f to load ignored paths."))
}
================================================
FILE: app/cli/lib/context_paths.go
================================================
package lib
import (
"fmt"
"plandex-cli/fs"
"plandex-cli/types"
)
type ParseInputPathsParams struct {
FileOrDirPaths []string
BaseDir string
ProjectPaths *types.ProjectPaths
LoadParams *types.LoadContextParams
}
func ParseInputPaths(params ParseInputPathsParams) ([]string, error) {
fileOrDirPaths := params.FileOrDirPaths
baseDir := params.BaseDir
projectPaths := params.ProjectPaths
loadParams := params.LoadParams
resPaths := []string{}
for path := range projectPaths.AllPaths {
// see if it's a child of any of the fileOrDirPaths
found := false
for _, p := range fileOrDirPaths {
var err error
found, err = fs.IsSubpathOf(p, path, baseDir)
if err != nil {
return nil, fmt.Errorf("error checking if %s is a subpath of %s: %s", path, p, err)
}
if found {
break
}
}
if !found {
continue
}
if projectPaths.AllDirs[path] {
if !(loadParams.Recursive || loadParams.NamesOnly || loadParams.DefsOnly) {
// log.Println("path", path, "info.Name()", info.Name())
return nil, fmt.Errorf("cannot process directory %s: requires --recursive/-r, --tree, or --map flag", path)
}
// calculate directory depth from base
// depth := strings.Count(path[len(p):], string(filepath.Separator))
// if params.MaxDepth != -1 && depth > params.MaxDepth {
// return filepath.SkipDir
// }
if loadParams.NamesOnly {
// add directory name to results
resPaths = append(resPaths, path)
}
} else {
// add file path to results
resPaths = append(resPaths, path)
}
}
return resPaths, nil
}
================================================
FILE: app/cli/lib/context_shared.go
================================================
package lib
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"plandex-cli/api"
"strings"
"sync"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
const ContextMapMaxClientConcurrency = 250
type filePathWithSize struct {
Path string
Size int64
}
type mapFileDetails struct {
size int64
tokens int
shaVal string
mapFilesSkippedAfterSizeLimit []string
mapFilesTruncatedTooLarge []filePathWithSize
mapContent string
}
func getMapFileDetails(path string, size, mapSize int64) (mapFileDetails, error) {
var isImage bool
var totalMapSizeExceeded bool
res := mapFileDetails{
size: size,
mapFilesSkippedAfterSizeLimit: []string{},
mapFilesTruncatedTooLarge: []filePathWithSize{},
}
if !shared.HasFileMapSupport(path) {
if shared.IsImageFile(path) {
isImage = true
var err error
res.tokens, err = readImageTokensForDefsOnly(path, size, openai.ImageURLDetailHigh, 8*1024)
if err != nil {
return mapFileDetails{}, fmt.Errorf("failed to read image tokens for %s: %v", path, err)
}
} else {
res.tokens = shared.GetBytesToTokensEstimate(size)
}
} else {
var truncated bool
if size > shared.MaxContextMapSingleInputSize {
size = shared.MaxContextMapSingleInputSize
truncated = true
res.tokens = shared.GetBytesToTokensEstimate(size)
}
// should go in either skip list *or* truncated list, not both
if mapSize+size > shared.MaxContextMapTotalInputSize {
totalMapSizeExceeded = true
res.mapFilesSkippedAfterSizeLimit = append(res.mapFilesSkippedAfterSizeLimit, path)
res.tokens = shared.GetBytesToTokensEstimate(size)
} else if truncated {
res.mapFilesTruncatedTooLarge = append(res.mapFilesTruncatedTooLarge, filePathWithSize{Path: path, Size: size})
}
}
if totalMapSizeExceeded || !shared.HasFileMapSupport(path) || isImage {
shaVal := sha256.Sum256([]byte(fmt.Sprintf("%d", res.tokens)))
res.shaVal = hex.EncodeToString(shaVal[:])
res.mapContent = ""
res.size = 0
} else {
// partial read for the map
contentRes, err := getMapFileContent(path)
if err != nil {
return mapFileDetails{}, fmt.Errorf("failed to read file %s: %v", path, err)
}
res.mapContent = contentRes.content
res.shaVal = contentRes.shaVal
if contentRes.truncated {
res.mapFilesTruncatedTooLarge = append(res.mapFilesTruncatedTooLarge, filePathWithSize{Path: path, Size: shared.MaxContextMapSingleInputSize})
res.size = shared.MaxContextMapSingleInputSize
res.tokens = shared.GetBytesToTokensEstimate(shared.MaxContextMapSingleInputSize)
} else {
// do the actual token count if we didn't truncate
res.tokens = shared.GetNumTokensEstimate(res.mapContent)
}
}
return res, nil
}
type mapFileContent struct {
mapData []byte
content string
shaVal string
truncated bool
}
func getMapFileContent(path string) (mapFileContent, error) {
f, err := os.Open(path)
if err != nil {
return mapFileContent{}, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return mapFileContent{}, err
}
size := info.Size()
limit := int64(shared.MaxContextMapSingleInputSize)
truncated := size > limit
limitReader := io.LimitReader(f, limit)
bytes, err := io.ReadAll(limitReader)
if err != nil {
return mapFileContent{}, err
}
sum := sha256.Sum256(bytes)
shaVal := hex.EncodeToString(sum[:])
return mapFileContent{mapData: bytes, content: string(bytes), shaVal: shaVal, truncated: truncated}, nil
}
func processMapBatches(mapInputBatches []shared.FileMapInputs) (shared.FileMapBodies, error) {
allMapBodies := shared.FileMapBodies{}
var mapMu sync.Mutex
errCh := make(chan error, len(mapInputBatches))
for _, batch := range mapInputBatches {
if len(batch) == 0 {
errCh <- nil
continue
}
go func(batch shared.FileMapInputs) {
mapRes, apiErr := api.Client.GetFileMap(shared.GetFileMapRequest{
MapInputs: batch,
})
if apiErr != nil {
errCh <- fmt.Errorf("failed to get file map: %v", apiErr)
return
}
mapMu.Lock()
for path, bodies := range mapRes.MapBodies {
allMapBodies[path] = bodies
}
mapMu.Unlock()
errCh <- nil
}(batch)
}
for i := 0; i < len(mapInputBatches); i++ {
err := <-errCh
if err != nil {
return nil, err
}
}
return allMapBodies, nil
}
func readImageTokensForDefsOnly(path string, size int64, detail openai.ImageURLDetail, headerBytes int64) (int, error) {
file, err := os.Open(path)
if err != nil {
return 0, fmt.Errorf("failed to open file %s: %w", path, err)
}
defer file.Close()
tokens, err := shared.GetImageTokensFromHeader(file, detail, headerBytes)
if err != nil {
tokens = shared.GetImageTokensEstimateFromBytes(size)
}
return tokens, nil
}
func printSkippedFilesMsg(
filesSkippedTooLarge []filePathWithSize,
filesSkippedAfterSizeLimit []string,
mapFilesTruncatedTooLarge []filePathWithSize,
mapFilesSkippedAfterSizeLimit []string,
) {
fmt.Println()
fmt.Println(getSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit, mapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit))
}
func getSkippedFilesMsg(
filesSkippedTooLarge []filePathWithSize,
filesSkippedAfterSizeLimit []string,
mapFilesTruncatedTooLarge []filePathWithSize,
mapFilesSkippedAfterSizeLimit []string,
) string {
var builder strings.Builder
if len(filesSkippedTooLarge) > 0 {
fmt.Fprintf(&builder, "ℹ️ These files were skipped because they're too large:\n")
for i, file := range filesSkippedTooLarge {
if i >= maxSkippedFileList {
fmt.Fprintf(&builder, " • and %d more\n", len(filesSkippedTooLarge)-maxSkippedFileList)
break
}
fmt.Fprintf(&builder, " • %s - %d MB\n", file.Path, file.Size/1024/1024)
}
}
if len(mapFilesTruncatedTooLarge) > 0 {
fmt.Fprintf(&builder, "ℹ️ These files were truncated because they're too large to map fully:\n")
for i, file := range mapFilesTruncatedTooLarge {
if i >= maxSkippedFileList {
fmt.Fprintf(&builder, " • and %d more\n", len(mapFilesTruncatedTooLarge)-maxSkippedFileList)
break
}
if file.Size > 1024*1024 {
fmt.Fprintf(&builder, " • %s - %d MB\n", file.Path, file.Size/1024/1024)
} else {
fmt.Fprintf(&builder, " • %s - %d KB\n", file.Path, file.Size/1024)
}
}
if len(mapFilesTruncatedTooLarge) > 0 {
fmt.Fprintf(&builder, "They will still be included in the map, but only the first %d KB will be mapped.\n", shared.MaxContextMapSingleInputSize/1024)
}
}
if len(filesSkippedAfterSizeLimit) > 0 {
fmt.Fprintf(&builder, "ℹ️ These files were skipped because the total size limit was exceeded:\n")
for i, file := range filesSkippedAfterSizeLimit {
if i >= maxSkippedFileList {
fmt.Fprintf(&builder, " • and %d more\n", len(filesSkippedAfterSizeLimit)-maxSkippedFileList)
break
}
fmt.Fprintf(&builder, " • %s\n", file)
}
}
if len(mapFilesSkippedAfterSizeLimit) > 0 {
fmt.Fprintf(&builder, "ℹ️ These files were skipped because the total map size limit was exceeded:\n")
for i, file := range mapFilesSkippedAfterSizeLimit {
if i >= maxSkippedFileList {
fmt.Fprintf(&builder, " • and %d more\n", len(mapFilesSkippedAfterSizeLimit)-maxSkippedFileList)
break
}
fmt.Fprintf(&builder, " • %s\n", file)
}
if len(mapFilesSkippedAfterSizeLimit) > 0 {
fmt.Fprintf(&builder, "They will still be included in the map as paths in the project, but no maps will be generated for them.\n")
}
}
return builder.String()
}
================================================
FILE: app/cli/lib/context_update.go
================================================
package lib
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/fs"
"plandex-cli/term"
"plandex-cli/types"
"plandex-cli/url"
"strconv"
"strings"
"sync"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
)
func CheckOutdatedContextWithOutput(quiet, autoConfirm bool, maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (contextOutdated, updated bool, err error) {
if !quiet {
term.StartSpinner("🔬 Checking context...")
}
var contexts []*shared.Context
if maybeContexts != nil {
contexts = maybeContexts
} else {
res, err := api.Client.ListContext(CurrentPlanId, CurrentBranch)
if err != nil {
term.StopSpinner()
return false, false, fmt.Errorf("failed to list context: %s", err)
}
contexts = res
}
outdatedRes, err := CheckOutdatedContext(contexts, projectPaths)
if err != nil {
term.StopSpinner()
return false, false, fmt.Errorf("failed to check outdated context: %s", err)
}
if !quiet {
term.StopSpinner()
}
if len(outdatedRes.UpdatedContexts) == 0 && len(outdatedRes.RemovedContexts) == 0 {
if !quiet {
fmt.Println("✅ Context is up to date")
}
return false, false, nil
}
if len(outdatedRes.UpdatedContexts) > 0 {
types := []string{}
if outdatedRes.NumFiles > 0 {
lbl := "file"
if outdatedRes.NumFiles > 1 {
lbl = "files"
}
lbl = strconv.Itoa(outdatedRes.NumFiles) + " " + lbl
types = append(types, lbl)
}
if outdatedRes.NumUrls > 0 {
lbl := "url"
if outdatedRes.NumUrls > 1 {
lbl = "urls"
}
lbl = strconv.Itoa(outdatedRes.NumUrls) + " " + lbl
types = append(types, lbl)
}
if outdatedRes.NumTrees > 0 {
lbl := "directory tree"
if outdatedRes.NumTrees > 1 {
lbl = "directory trees"
}
lbl = strconv.Itoa(outdatedRes.NumTrees) + " " + lbl
types = append(types, lbl)
}
if outdatedRes.NumMaps > 0 {
lbl := "map"
if outdatedRes.NumMaps > 1 {
lbl = "maps"
}
lbl = strconv.Itoa(outdatedRes.NumMaps) + " " + lbl
types = append(types, lbl)
}
var msg string
if len(types) <= 2 {
msg += strings.Join(types, " and ")
} else {
for i, add := range types {
if i == len(types)-1 {
msg += ", and " + add
} else {
msg += ", " + add
}
}
}
phrase := "have been"
if len(outdatedRes.UpdatedContexts) == 1 {
phrase = "has been"
}
if !quiet {
term.StopSpinner()
color.New(term.ColorHiCyan, color.Bold).Printf("%s in context %s modified 👇\n\n", msg, phrase)
tableString := tableForContextOutdated(outdatedRes.UpdatedContexts, outdatedRes.TokenDiffsById)
fmt.Println(tableString)
}
}
if len(outdatedRes.RemovedContexts) > 0 {
types := []string{}
if outdatedRes.NumFilesRemoved > 0 {
lbl := "file"
if outdatedRes.NumFilesRemoved > 1 {
lbl = "files"
}
lbl = strconv.Itoa(outdatedRes.NumFilesRemoved) + " " + lbl
types = append(types, lbl)
}
if outdatedRes.NumTreesRemoved > 0 {
lbl := "directory tree"
if outdatedRes.NumTreesRemoved > 1 {
lbl = "directory trees"
}
lbl = strconv.Itoa(outdatedRes.NumTreesRemoved) + " " + lbl
types = append(types, lbl)
}
var msg string
if len(types) <= 2 {
msg += strings.Join(types, " and ")
} else {
for i, add := range types {
if i == len(types)-1 {
msg += ", and " + add
} else {
msg += ", " + add
}
}
}
phrase := "have been"
if len(outdatedRes.RemovedContexts) == 1 {
phrase = "has been"
}
if !quiet {
term.StopSpinner()
color.New(term.ColorHiCyan, color.Bold).Printf("%s in context %s removed 👇\n\n", msg, phrase)
tableString := tableForContextOutdated(outdatedRes.RemovedContexts, outdatedRes.TokenDiffsById)
fmt.Println(tableString)
}
}
confirmed := autoConfirm
if !autoConfirm {
confirmed, err = term.ConfirmYesNo("Update context now?")
if err != nil {
term.OutputErrorAndExit("failed to get user input: %s", err)
}
}
if confirmed {
reqFn := outdatedRes.ReqFn
if reqFn == nil {
return false, false, fmt.Errorf("no update request function provided")
}
_, err = UpdateContextWithOutput(UpdateContextParams{
Contexts: contexts,
OutdatedRes: *outdatedRes,
ReqFn: reqFn,
})
if err != nil {
return false, false, fmt.Errorf("error updating context: %v", err)
}
return true, true, nil
} else {
return true, false, nil
}
}
type UpdateContextParams struct {
Contexts []*shared.Context
OutdatedRes types.ContextOutdatedResult
ReqFn func() (map[string]*shared.UpdateContextParams, error)
}
type UpdateContextResult struct {
HasConflicts bool
Msg string
}
func UpdateContextWithOutput(params UpdateContextParams) (UpdateContextResult, error) {
term.StartSpinner("🔄 Updating context...")
updateRes, err := UpdateContext(params)
if err != nil {
return UpdateContextResult{}, err
}
term.StopSpinner()
fmt.Println("✅ " + updateRes.Msg)
return updateRes, nil
}
func UpdateContext(params UpdateContextParams) (UpdateContextResult, error) {
var err error
reqFn := params.ReqFn
if reqFn == nil {
return UpdateContextResult{}, fmt.Errorf("no update request function provided")
}
req, err := reqFn()
if err != nil {
return UpdateContextResult{}, fmt.Errorf("error getting update request: %v", err)
}
var hasConflicts bool
var msg string
contextsById := map[string]*shared.Context{}
for _, context := range params.Contexts {
contextsById[context.Id] = context
}
deleteIds := map[string]bool{}
for _, context := range params.OutdatedRes.RemovedContexts {
deleteIds[context.Id] = true
}
filesToLoad := map[string]string{}
for id := range req {
context := contextsById[id]
if context.ContextType == shared.ContextFileType {
filesToLoad[context.FilePath] = context.Body
}
}
for id := range deleteIds {
context := contextsById[id]
if context.ContextType == shared.ContextFileType {
filesToLoad[context.FilePath] = ""
}
}
hasConflicts, err = checkContextConflicts(filesToLoad)
if err != nil {
return UpdateContextResult{}, fmt.Errorf("failed to check context conflicts: %v", err)
}
if len(req) > 0 {
res, apiErr := api.Client.UpdateContext(CurrentPlanId, CurrentBranch, req)
if apiErr != nil {
return UpdateContextResult{}, fmt.Errorf("failed to update context: %v", apiErr)
}
msg = res.Msg
}
if len(deleteIds) > 0 {
res, apiErr := api.Client.DeleteContext(CurrentPlanId, CurrentBranch, shared.DeleteContextRequest{
Ids: deleteIds,
})
if apiErr != nil {
return UpdateContextResult{}, fmt.Errorf("failed to delete contexts: %v", apiErr)
}
msg += " " + res.Msg
}
return UpdateContextResult{
HasConflicts: hasConflicts,
Msg: strings.TrimSpace(msg),
}, nil
}
// CheckOutdatedContext is where we replicate your partial-read logic for map files
// so that large map files or newly added map files do not read more than MaxContextMapSingleInputSize
func CheckOutdatedContext(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (*types.ContextOutdatedResult, error) {
return checkOutdatedAndMaybeUpdateContext(false, maybeContexts, projectPaths)
}
type mapState struct {
removedMapPaths []string
mapInputShas map[string]string
mapInputTokens map[string]int
mapInputSizes map[string]int64
totalMapSize int64
currentMapInputBatch shared.FileMapInputs
mapInputBatches []shared.FileMapInputs
}
func checkOutdatedAndMaybeUpdateContext(doUpdate bool, maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (*types.ContextOutdatedResult, error) {
var contexts []*shared.Context
if maybeContexts == nil {
contextsRes, apiErr := api.Client.ListContext(CurrentPlanId, CurrentBranch)
if apiErr != nil {
return nil, fmt.Errorf("error retrieving context: %v", apiErr)
}
contexts = contextsRes
} else {
contexts = maybeContexts
}
totalTokens := 0
for _, c := range contexts {
totalTokens += c.NumTokens
}
var errs []error
reqFns := map[string]func() (*shared.UpdateContextParams, error){}
var updatedContexts []*shared.Context
var tokenDiffsById = map[string]int{}
var numFiles int
var numUrls int
var numTrees int
var numMaps int
var numFilesRemoved int
var numTreesRemoved int
var mu sync.Mutex
var wg sync.WaitGroup
contextsById := make(map[string]*shared.Context)
deleteIds := make(map[string]bool)
paths := projectPaths
// We track skipped items for final warnings
var filesSkippedTooLarge []filePathWithSize
var filesSkippedAfterSizeLimit []string
var mapFilesTruncatedTooLarge []filePathWithSize
var mapFilesSkippedAfterSizeLimit []string
mapFilesTruncatedSet := map[string]bool{}
mapFilesSkippedAfterSizeLimitSet := map[string]bool{}
mapFileInfoByPath := map[string]os.FileInfo{}
mapFileRemovedByPath := map[string]bool{}
var totalSize int64
var totalBodySize int64
var totalContextCount int
sem := make(chan struct{}, ContextMapMaxClientConcurrency)
for _, c := range contexts {
contextsById[c.Id] = c
}
for _, context := range contexts {
switch context.ContextType {
case shared.ContextFileType:
wg.Add(1)
go func(ctx *shared.Context) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
if _, err := os.Stat(ctx.FilePath); os.IsNotExist(err) {
mu.Lock()
defer mu.Unlock()
deleteIds[ctx.Id] = true
numFilesRemoved++
tokenDiffsById[ctx.Id] = -ctx.NumTokens
return
}
fileContent, err := os.ReadFile(ctx.FilePath)
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, fmt.Errorf("failed to read the file %s: %v", ctx.FilePath, err))
return
}
fileContent = shared.NormalizeEOL(fileContent)
fileInfo, err := os.Stat(ctx.FilePath)
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, fmt.Errorf("failed to get file info for %s: %v", ctx.FilePath, err))
return
}
size := fileInfo.Size()
// Individual skip checks
if size > shared.MaxContextBodySize {
mu.Lock()
defer mu.Unlock()
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: ctx.FilePath, Size: size})
return
}
if totalSize+size > shared.MaxContextBodySize {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)
return
}
// Compare new sha
hash := sha256.Sum256(fileContent)
sha := hex.EncodeToString(hash[:])
if sha != ctx.Sha {
if totalContextCount >= shared.MaxContextCount {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)
return
}
oldBodySize := int64(len(ctx.Body))
newBodySize := int64(len(fileContent))
if totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)
return
}
// Accept
mu.Lock()
totalSize += size
totalContextCount++
totalBodySize += (newBodySize - oldBodySize)
mu.Unlock()
var numTokens int
if shared.IsImageFile(ctx.FilePath) {
tokens, err := shared.GetImageTokens(base64.StdEncoding.EncodeToString(fileContent), ctx.ImageDetail)
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, fmt.Errorf("failed to get image tokens for %s: %v", ctx.FilePath, err))
return
}
numTokens = tokens
} else {
numTokens = shared.GetNumTokensEstimate(string(fileContent))
}
mu.Lock()
defer mu.Unlock()
tokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens
numFiles++
updatedContexts = append(updatedContexts, ctx)
reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {
return &shared.UpdateContextParams{
Body: string(fileContent),
}, nil
}
}
}(context)
case shared.ContextDirectoryTreeType:
wg.Add(1)
go func(ctx *shared.Context) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
if _, err := os.Stat(ctx.FilePath); os.IsNotExist(err) {
mu.Lock()
deleteIds[ctx.Id] = true
numTreesRemoved++
tokenDiffsById[ctx.Id] = -ctx.NumTokens
mu.Unlock()
return
}
baseDir := fs.GetBaseDirForFilePaths([]string{ctx.FilePath})
flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{
FileOrDirPaths: []string{ctx.FilePath},
BaseDir: baseDir,
ProjectPaths: paths,
LoadParams: &types.LoadContextParams{
NamesOnly: true,
ForceSkipIgnore: ctx.ForceSkipIgnore,
},
})
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, fmt.Errorf("failed to get directory tree %s: %v", ctx.FilePath, err))
return
}
if !ctx.ForceSkipIgnore && paths != nil {
var filtered []string
for _, p := range flattenedPaths {
if _, ok := paths.ActivePaths[p]; ok {
filtered = append(filtered, p)
}
}
flattenedPaths = filtered
}
// Partial skipping for sub-paths
var kept []string
mu.Lock()
for _, p := range flattenedPaths {
lineSize := int64(len(p))
// If line is individually too large, skip
if lineSize > shared.MaxContextBodySize {
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: p, Size: lineSize})
continue
}
if totalSize+lineSize > shared.MaxContextBodySize {
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, p)
continue
}
// Accept
totalSize += lineSize
kept = append(kept, p)
}
mu.Unlock()
body := strings.Join(kept, "\n")
newHash := sha256.Sum256([]byte(body))
newSha := hex.EncodeToString(newHash[:])
if newSha != ctx.Sha {
if totalContextCount >= shared.MaxContextCount {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)
return
}
oldBodySize := int64(len(ctx.Body))
newBodySize := int64(len(body))
if totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)
return
}
numTokens := shared.GetNumTokensEstimate(body)
mu.Lock()
defer mu.Unlock()
totalContextCount++
totalBodySize += (newBodySize - oldBodySize)
tokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens
numTrees++
updatedContexts = append(updatedContexts, ctx)
reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {
return &shared.UpdateContextParams{
Body: body,
}, nil
}
}
}(context)
case shared.ContextMapType:
// Instead of reading all files in the same goroutine,
// we now spawn one goroutine per map-file to mirror the loading logic concurrency.
wg.Add(1)
go func(ctx *shared.Context) {
defer wg.Done()
// We collect paths from the existing map
var mapPaths []string
for path := range ctx.MapShas {
mapPaths = append(mapPaths, path)
}
// Next, see if there are newly added files
baseDir := fs.GetBaseDirForFilePaths([]string{ctx.FilePath})
flattenedPaths, err := ParseInputPaths(ParseInputPathsParams{
FileOrDirPaths: []string{ctx.FilePath},
BaseDir: baseDir,
ProjectPaths: projectPaths,
LoadParams: &types.LoadContextParams{Recursive: true},
})
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, fmt.Errorf("failed to get the directory tree %s: %v", ctx.FilePath, err))
return
}
var filtered []string
if projectPaths != nil {
for _, p := range flattenedPaths {
if _, ok := projectPaths.ActivePaths[p]; ok {
filtered = append(filtered, p)
}
}
flattenedPaths = filtered
}
// If a path was not already in the map, it's newly added
for _, p := range flattenedPaths {
if _, ok := ctx.MapShas[p]; !ok {
mapPaths = append(mapPaths, p)
}
}
totalMapPaths := len(mapPaths)
currentMapInputBatch := shared.FileMapInputs{}
state := mapState{
removedMapPaths: []string{},
mapInputShas: map[string]string{},
mapInputTokens: map[string]int{},
mapInputSizes: map[string]int64{},
totalMapSize: 0,
currentMapInputBatch: currentMapInputBatch,
mapInputBatches: []shared.FileMapInputs{currentMapInputBatch},
}
innerExistenceErrCh := make(chan error, len(mapPaths))
// Existence: check each path in its own goroutine:
for _, path := range mapPaths {
go func(path string) {
sem <- struct{}{}
defer func() { <-sem }()
var removed bool
var hasFileInfo bool
mu.Lock()
if _, ok := mapFileInfoByPath[path]; ok {
hasFileInfo = true
} else if _, ok := mapFileRemovedByPath[path]; ok {
removed = true
}
mu.Unlock()
if !(hasFileInfo || removed) {
fileInfo, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
removed = true
} else {
innerExistenceErrCh <- fmt.Errorf("failed to stat map file %s: %v", path, err)
return
}
}
mu.Lock()
prevTokens := ctx.MapTokens[path]
prevSize := ctx.MapSizes[path]
if removed {
mapFileRemovedByPath[path] = true
totalMapPaths--
if _, existed := ctx.MapShas[path]; existed {
state.removedMapPaths = append(state.removedMapPaths, path)
tokenDiffsById[ctx.Id] -= prevTokens
state.totalMapSize -= prevSize
}
} else {
mapFileInfoByPath[path] = fileInfo
}
mu.Unlock()
}
innerExistenceErrCh <- nil
}(path)
}
for range mapPaths {
err := <-innerExistenceErrCh
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, err)
return
}
}
// Updates: check each path in its own goroutine:
innerUpdatesErrCh := make(chan error, len(mapPaths))
for _, path := range mapPaths {
go func(path string) {
sem <- struct{}{}
defer func() { <-sem }()
var removed bool
var fileInfo os.FileInfo
var hasFileInfo bool
mu.Lock()
if _, ok := mapFileInfoByPath[path]; ok {
fileInfo = mapFileInfoByPath[path]
hasFileInfo = true
} else if _, ok := mapFileRemovedByPath[path]; ok {
removed = true
}
mu.Unlock()
if removed {
innerUpdatesErrCh <- nil
return
}
if !hasFileInfo {
innerUpdatesErrCh <- fmt.Errorf("failed to get map file info for %s - should already be set", path)
return
}
size := fileInfo.Size()
var totalMapSize int64
mu.Lock()
prevTokens := ctx.MapTokens[path]
prevSize := ctx.MapSizes[path]
prevSha := ctx.MapShas[path]
totalMapSize = state.totalMapSize
mu.Unlock()
res, err := getMapFileDetails(path, size, totalMapSize)
if err != nil {
innerUpdatesErrCh <- fmt.Errorf("failed to get map file details for %s: %v", path, err)
return
}
if res.shaVal == prevSha {
// no change
innerUpdatesErrCh <- nil
return
}
mu.Lock()
totalMapPaths++
if totalMapPaths > shared.MaxContextMapPaths {
if _, ok := mapFilesSkippedAfterSizeLimitSet[path]; !ok {
mapFilesSkippedAfterSizeLimitSet[path] = true
mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, path)
}
mu.Unlock()
innerUpdatesErrCh <- nil
return
}
defer mu.Unlock()
if state.currentMapInputBatch.NumFiles()+1 > shared.ContextMapMaxBatchSize || state.totalMapSize+res.size > shared.ContextMapMaxBatchBytes {
state.currentMapInputBatch = shared.FileMapInputs{}
state.mapInputBatches = append(state.mapInputBatches, state.currentMapInputBatch)
}
sizeChange := int64(res.size) - prevSize
state.totalMapSize += sizeChange
tokenDiffsById[ctx.Id] += (res.tokens - prevTokens)
state.mapInputShas[path] = res.shaVal
state.mapInputTokens[path] = res.tokens
state.currentMapInputBatch[path] = res.mapContent
state.mapInputSizes[path] = res.size
if len(res.mapFilesTruncatedTooLarge) > 0 {
for _, file := range res.mapFilesTruncatedTooLarge {
if _, ok := mapFilesTruncatedSet[file.Path]; !ok {
mapFilesTruncatedSet[file.Path] = true
mapFilesTruncatedTooLarge = append(mapFilesTruncatedTooLarge, file)
}
}
}
if len(res.mapFilesSkippedAfterSizeLimit) > 0 {
for _, file := range res.mapFilesSkippedAfterSizeLimit {
if _, ok := mapFilesSkippedAfterSizeLimitSet[file]; !ok {
mapFilesSkippedAfterSizeLimitSet[file] = true
mapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, file)
}
}
}
innerUpdatesErrCh <- nil
}(path)
}
for range mapPaths {
err := <-innerUpdatesErrCh
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, err)
return
}
}
hasAnyUpdate := len(state.removedMapPaths) > 0 || len(state.mapInputShas) > 0
if hasAnyUpdate {
mu.Lock()
defer mu.Unlock()
updatedContexts = append(updatedContexts, ctx)
numMaps++
reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {
updatedMapBodies, err := processMapBatches(state.mapInputBatches)
if err != nil {
return nil, fmt.Errorf("failed to process map batches: %v", err)
}
return &shared.UpdateContextParams{
MapBodies: updatedMapBodies,
InputShas: state.mapInputShas,
InputTokens: state.mapInputTokens,
InputSizes: state.mapInputSizes,
RemovedMapPaths: state.removedMapPaths,
}, nil
}
}
}(context)
case shared.ContextURLType:
wg.Add(1)
go func(ctx *shared.Context) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
body, err := url.FetchURLContent(ctx.Url)
if err != nil {
mu.Lock()
defer mu.Unlock()
errs = append(errs, fmt.Errorf("failed to fetch the URL %s: %v", ctx.Url, err))
return
}
size := int64(len(body))
if size > shared.MaxContextBodySize {
mu.Lock()
defer mu.Unlock()
filesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: ctx.Url, Size: size})
return
}
if totalSize+size > shared.MaxContextBodySize {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url)
return
}
hash := sha256.Sum256([]byte(body))
newSha := hex.EncodeToString(hash[:])
if newSha != ctx.Sha {
if totalContextCount >= shared.MaxContextCount {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url)
return
}
oldBodySize := int64(len(ctx.Body))
newBodySize := size
if totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize {
mu.Lock()
defer mu.Unlock()
filesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url)
return
}
numTokens := shared.GetNumTokensEstimate(string(body))
mu.Lock()
defer mu.Unlock()
totalSize += size
totalContextCount++
totalBodySize += (newBodySize - oldBodySize)
tokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens
numUrls++
updatedContexts = append(updatedContexts, ctx)
reqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {
return &shared.UpdateContextParams{
Body: string(body),
}, nil
}
}
}(context)
}
}
wg.Wait()
if len(errs) > 0 {
return nil, fmt.Errorf("failed to check context outdated: %v", errs)
}
// Identify contexts to remove
var removedContexts []*shared.Context
for id := range deleteIds {
removedContexts = append(removedContexts, contextsById[id])
}
// If nothing changed
if len(reqFns) == 0 && len(removedContexts) == 0 {
return &types.ContextOutdatedResult{
Msg: "Context is up to date",
}, nil
}
reqFn := func() (map[string]*shared.UpdateContextParams, error) {
req := map[string]*shared.UpdateContextParams{}
var mu sync.Mutex
errCh := make(chan error, len(reqFns))
for id, fn := range reqFns {
go func(id string, fn func() (*shared.UpdateContextParams, error)) {
res, err := fn()
if err != nil {
errCh <- err
return
}
mu.Lock()
req[id] = res
mu.Unlock()
errCh <- nil
}(id, fn)
}
for i := 0; i < len(reqFns); i++ {
err := <-errCh
if err != nil {
return nil, err
}
}
return req, nil
}
// Build final result
outdatedRes := types.ContextOutdatedResult{
UpdatedContexts: updatedContexts,
RemovedContexts: removedContexts,
TokenDiffsById: tokenDiffsById,
NumFiles: numFiles,
NumUrls: numUrls,
NumTrees: numTrees,
NumMaps: numMaps,
NumFilesRemoved: numFilesRemoved,
NumTreesRemoved: numTreesRemoved,
ReqFn: reqFn,
}
var hasConflicts bool
var msg string
if doUpdate {
res, err := UpdateContext(UpdateContextParams{
Contexts: contexts,
OutdatedRes: outdatedRes,
ReqFn: reqFn,
})
if err != nil {
return nil, fmt.Errorf("failed to update context: %v", err)
}
hasConflicts = res.HasConflicts
msg = res.Msg
outdatedRes.Msg = msg
} else {
var tokensDiff int
for _, diff := range tokenDiffsById {
tokensDiff += diff
}
newTotal := totalTokens + tokensDiff
outdatedRes.Msg = shared.SummaryForUpdateContext(shared.SummaryForUpdateContextParams{
NumFiles: numFiles,
NumTrees: numTrees,
NumUrls: numUrls,
NumMaps: numMaps,
TokensDiff: tokensDiff,
TotalTokens: newTotal,
})
}
if hasConflicts {
term.StartSpinner("🏗️ Starting build...")
_, err := buildPlanInlineFn(false, nil)
term.StopSpinner()
fmt.Println()
if err != nil {
return nil, fmt.Errorf("failed to build plan: %v", err)
}
}
// Warn about any items skipped
if len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 ||
len(mapFilesTruncatedTooLarge) > 0 || len(mapFilesSkippedAfterSizeLimit) > 0 {
printSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit,
mapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit)
}
return &outdatedRes, nil
}
func tableForContextOutdated(updatedContexts []*shared.Context, tokenDiffsById map[string]int) string {
if len(updatedContexts) == 0 {
return ""
}
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetHeader([]string{"Name", "Type", "🪙"})
table.SetAutoWrapText(false)
for _, ctx := range updatedContexts {
t, icon := ctx.TypeAndIcon()
diff := tokenDiffsById[ctx.Id]
diffStr := "+" + strconv.Itoa(diff)
tableColor := tablewriter.FgHiGreenColor
if diff < 0 {
diffStr = strconv.Itoa(diff)
tableColor = tablewriter.FgHiRedColor
}
row := []string{
" " + icon + " " + ctx.Name,
t,
diffStr,
}
table.Rich(row, []tablewriter.Colors{
{tableColor, tablewriter.Bold},
{tableColor},
{tableColor},
})
}
table.Render()
return tableString.String()
}
================================================
FILE: app/cli/lib/current.go
================================================
package lib
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/format"
"plandex-cli/fs"
"plandex-cli/term"
"plandex-cli/types"
"strconv"
"strings"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
)
var CurrentProjectId string
var CurrentPlanId string
var CurrentBranch string
var HomeCurrentProjectDir string
var HomeCurrentPlanPath string
func MustResolveOrCreateProject() {
resolveProject(true, true)
}
func MustResolveProject() {
resolveProject(true, false)
}
func MaybeResolveProject() {
resolveProject(false, false)
}
func resolveProject(mustResolve, shouldCreate bool) {
if fs.PlandexDir == "" {
var err error
if shouldCreate {
_, _, err = fs.FindOrCreatePlandex()
} else {
fs.FindPlandexDir()
}
if err != nil && mustResolve {
term.OutputErrorAndExit("error finding or creating plandex: %v", err)
}
}
if (fs.PlandexDir == "" || fs.ProjectRoot == "") && mustResolve {
fmt.Printf(
"🤷♂️ No plans in current directory\nTry %s to create a plan or %s to see plans in nearby directories\n",
color.New(color.Bold, term.ColorHiCyan).Sprint("plandex new"),
color.New(color.Bold, term.ColorHiCyan).Sprint("plandex plans"))
os.Exit(0)
}
if fs.PlandexDir == "" {
return
}
MigrateLegacyProjectFile(auth.Current.UserId)
// check if projects-v2.json exists in PlandexDir
path := filepath.Join(fs.PlandexDir, "projects-v2.json")
_, err := os.Stat(path)
if os.IsNotExist(err) {
log.Println("projects-v2.json does not exist")
log.Println("Initializing project")
mustInitProject(nil)
} else if err != nil {
term.OutputErrorAndExit("error checking if projects-v2.json exists: %v", err)
}
var settings *types.CurrentProjectSettings
var loadProjectSettings func()
loadProjectSettings = func() {
// read projects-v2.json
bytes, err := os.ReadFile(path)
if err != nil {
term.OutputErrorAndExit("error reading projects-v2.json: %v", err)
}
var settingsByAccount types.CurrentProjectSettingsByAccount
err = json.Unmarshal(bytes, &settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error unmarshalling projects-v2.json: %v", err)
}
settings = settingsByAccount[auth.Current.UserId]
if settings == nil {
mustInitProject(&settingsByAccount)
loadProjectSettings()
}
}
loadProjectSettings()
CurrentProjectId = settings.Id
MigrateLegacyCurrentPlanFile(auth.Current.UserId)
HomeCurrentProjectDir = filepath.Join(fs.HomePlandexDir, CurrentProjectId)
HomeCurrentPlanPath = filepath.Join(HomeCurrentProjectDir, "current-plans-v2.json")
err = os.MkdirAll(HomeCurrentProjectDir, os.ModePerm)
if err != nil {
term.OutputErrorAndExit("error creating project dir: %v", err)
}
MustLoadCurrentPlan()
MigrateLegacyPlanSettingsFile(auth.Current.UserId)
}
func MustLoadCurrentPlan() {
if CurrentProjectId == "" {
term.OutputErrorAndExit("No current project")
}
// Check if the file exists
_, err := os.Stat(HomeCurrentPlanPath)
if os.IsNotExist(err) {
return
} else if err != nil {
term.OutputErrorAndExit("error checking if current-plans-v2.json exists: %v", err)
}
// Read the contents of the file
fileBytes, err := os.ReadFile(HomeCurrentPlanPath)
if err != nil {
term.OutputErrorAndExit("error reading current-plans-v2.json: %v", err)
}
var currentPlansByAccount types.CurrentPlanSettingsByAccount
err = json.Unmarshal(fileBytes, ¤tPlansByAccount)
if err != nil {
term.OutputErrorAndExit("error unmarshalling current-plans-v2.json: %v", err)
}
currentPlan := currentPlansByAccount[auth.Current.UserId]
if currentPlan != nil {
CurrentPlanId = currentPlan.Id
}
if CurrentPlanId != "" {
err = loadCurrentBranch()
if err != nil {
term.OutputErrorAndExit("error loading current branch: %v", err)
}
if CurrentBranch == "" {
err = WriteCurrentBranch("main")
if err != nil {
term.OutputErrorAndExit("error setting current branch: %v", err)
}
}
}
}
func loadCurrentBranch() error {
// Load plan-specific settings
if CurrentPlanId == "" {
return fmt.Errorf("no current plan")
}
path := filepath.Join(HomeCurrentProjectDir, CurrentPlanId, "settings-v2.json")
// Check if the file exists
_, err := os.Stat(path)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("error checking if settings-v2.json exists: %v", err)
}
fileBytes, err := os.ReadFile(path)
if err != nil {
term.OutputErrorAndExit("error reading settings-v2.json: %v", err)
}
var settingsByAccount types.PlanSettingsByAccount
err = json.Unmarshal(fileBytes, &settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error unmarshalling settings-v2.json: %v", err)
}
settings := settingsByAccount[auth.Current.UserId]
if settings == nil {
return nil
}
CurrentBranch = settings.Branch
return nil
}
func GetCurrentPlanTable(plan *shared.Plan, currentBranchesByPlanId map[string]*shared.Branch, onlyCols []string) string {
b := &strings.Builder{}
table := tablewriter.NewWriter(b)
table.SetAutoWrapText(false)
var cols []string
if onlyCols == nil {
cols = []string{"Current Plan", "Updated", "Created" /*"Branches",*/, "Branch", "Context", "Convo"}
} else {
cols = onlyCols
}
table.SetHeader(cols)
name := color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name)
branch := currentBranchesByPlanId[CurrentPlanId]
var row []string
for _, col := range cols {
switch col {
case "Current Plan":
row = append(row, name)
case "Updated":
row = append(row, format.Time(plan.UpdatedAt))
case "Created":
row = append(row, format.Time(plan.CreatedAt))
case "Branch":
row = append(row, CurrentBranch)
case "Context":
row = append(row, strconv.Itoa(branch.ContextTokens)+" 🪙")
case "Convo":
row = append(row, strconv.Itoa(branch.ConvoTokens)+" 🪙")
}
}
style := []tablewriter.Colors{
{tablewriter.FgGreenColor, tablewriter.Bold},
}
table.Rich(row, style)
table.Render()
return b.String()
}
func mustInitProject(existingSettings *types.CurrentProjectSettingsByAccount) {
res, apiErr := api.Client.CreateProject(shared.CreateProjectRequest{Name: filepath.Base(fs.ProjectRoot)})
if apiErr != nil {
term.OutputErrorAndExit("error creating project: %v", apiErr.Msg)
}
log.Println("Project created:", res.Id)
CurrentProjectId = res.Id
var settingsByAccount types.CurrentProjectSettingsByAccount
if existingSettings != nil {
settingsByAccount = *existingSettings
} else {
settingsByAccount = types.CurrentProjectSettingsByAccount{}
}
settingsByAccount[auth.Current.UserId] = &types.CurrentProjectSettings{
Id: CurrentProjectId,
}
// write projects-v2.json
path := filepath.Join(fs.PlandexDir, "projects-v2.json")
bytes, err := json.Marshal(settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error marshalling project settings: %v", err)
}
err = os.WriteFile(path, bytes, os.ModePerm)
if err != nil {
term.OutputErrorAndExit("error writing projects-v2.json: %v", err)
}
log.Println("Wrote projects-v2.json")
// write current-plans-v2.json to PlandexHomeDir/[projectId]/current-plans-v2.json
dir := filepath.Join(fs.HomePlandexDir, CurrentProjectId)
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
term.OutputErrorAndExit("error creating project dir: %v", err)
}
path = filepath.Join(dir, "current-plans-v2.json")
bytes, err = json.Marshal(types.CurrentPlanSettingsByAccount{
auth.Current.UserId: &types.CurrentPlanSettings{
Id: "",
},
})
if err != nil {
term.OutputErrorAndExit("error marshalling plan settings: %v", err)
}
err = os.WriteFile(path, bytes, os.ModePerm)
if err != nil {
term.OutputErrorAndExit("error writing current-plans-v2.json: %v", err)
}
log.Println("Wrote current-plans-v2.json")
}
================================================
FILE: app/cli/lib/custom_models.go
================================================
package lib
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/schema"
"plandex-cli/term"
shared "plandex-shared"
"strings"
"github.com/fatih/color"
)
type CustomModelsCheckLocalChangesResult struct {
HasLocalChanges bool
LocalModelsInput shared.ModelsInput
}
func GetCustomModelsPath(userId string) string {
return filepath.Join(fs.HomePlandexDir, "accounts", userId, "custom-models.json")
}
func GetServerModelsInput() (*shared.ModelsInput, error) {
errCh := make(chan *shared.ApiError, 3)
var (
customModels []*shared.CustomModel
customProviders []*shared.CustomProvider
customModelPacks []*shared.ModelPackSchema
)
go func() {
models, apiErr := api.Client.ListCustomModels()
if apiErr != nil {
errCh <- apiErr
return
}
customModels = models
errCh <- nil
}()
go func() {
// custom providers are not supported on cloud
if auth.Current.IsCloud {
errCh <- nil
return
}
providers, apiErr := api.Client.ListCustomProviders()
if apiErr != nil {
errCh <- apiErr
return
}
customProviders = providers
errCh <- nil
}()
go func() {
modelPacks, apiErr := api.Client.ListModelPacks()
if apiErr != nil {
errCh <- apiErr
return
}
schemas := make([]*shared.ModelPackSchema, len(modelPacks))
for i, modelPack := range modelPacks {
schemas[i] = modelPack.ToModelPackSchema()
}
customModelPacks = schemas
errCh <- nil
}()
for i := 0; i < 3; i++ {
err := <-errCh
if err != nil {
return nil, fmt.Errorf("error fetching custom models: %v", err.Msg)
}
}
serverModelsInput := &shared.ModelsInput{
CustomModels: customModels,
CustomProviders: customProviders,
CustomModelPacks: customModelPacks,
}
return serverModelsInput, nil
}
func CustomModelsCheckLocalChanges(path string) (CustomModelsCheckLocalChangesResult, error) {
hashPath := path + ".hash"
exists, err := fs.FileExists(path)
if err != nil {
return CustomModelsCheckLocalChangesResult{}, err
}
if !exists {
return CustomModelsCheckLocalChangesResult{}, nil
}
localJsonData, err := os.ReadFile(path)
if err != nil {
return CustomModelsCheckLocalChangesResult{}, fmt.Errorf("error reading JSON file: %v", err)
}
var localClientModelsInput shared.ClientModelsInput
err = json.Unmarshal(localJsonData, &localClientModelsInput)
if err != nil {
return CustomModelsCheckLocalChangesResult{}, fmt.Errorf("error unmarshalling JSON file: %v", err)
}
localModelsInput := localClientModelsInput.ToModelsInput()
lastSavedHash, err := os.ReadFile(hashPath)
if err != nil && !os.IsNotExist(err) {
return CustomModelsCheckLocalChangesResult{}, fmt.Errorf("error reading hash file: %v", err)
}
currentHash, err := localModelsInput.Hash()
if err != nil {
return CustomModelsCheckLocalChangesResult{}, fmt.Errorf("error hashing models: %v", err)
}
return CustomModelsCheckLocalChangesResult{
HasLocalChanges: currentHash != string(lastSavedHash),
LocalModelsInput: localModelsInput,
}, nil
}
func WriteCustomModelsFile(path string, modelsInput *shared.ModelsInput) error {
err := os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
return fmt.Errorf("error creating directory: %v", err)
}
clientModelsInput := modelsInput.ToClientModelsInput()
clientModelsInput.PrepareUpdate()
jsonData, err := json.MarshalIndent(clientModelsInput, "", " ")
if err != nil {
return fmt.Errorf("error marshalling models: %v", err)
}
err = os.WriteFile(path, jsonData, 0644)
if err != nil {
return fmt.Errorf("error writing file: %v", err)
}
err = SaveCustomModelsHash(path, modelsInput)
if err != nil {
return fmt.Errorf("error saving hash file: %v", err)
}
return nil
}
func SaveCustomModelsHash(basePath string, modelsInput *shared.ModelsInput) error {
hashPath := basePath + ".hash"
hash, err := modelsInput.Hash()
if err != nil {
return fmt.Errorf("error hashing models: %v", err)
}
err = os.WriteFile(hashPath, []byte(hash), 0644)
if err != nil {
return fmt.Errorf("error writing hash file: %v", err)
}
return nil
}
func MustSyncCustomModels(path string, serverModelsInput *shared.ModelsInput) bool {
term.StartSpinner("")
jsonData, err := os.ReadFile(path)
if err != nil {
term.OutputErrorAndExit("Error reading custom models file: %v", err)
return false
}
clientModelsInput, err := schema.ValidateModelsInputJSON(jsonData)
if err != nil {
term.StopSpinner()
color.New(color.Bold, term.ColorHiRed).Println("🚨 Error validating custom models file")
fmt.Println(err.Error())
return false
}
modelsInput := clientModelsInput.ToModelsInput()
noDuplicates, errMsg := modelsInput.CheckNoDuplicates()
if !noDuplicates {
term.StopSpinner()
color.New(color.Bold, term.ColorHiRed).Println("🚨 Some items in custom models file are duplicated:")
fmt.Println()
fmt.Println(errMsg)
return false
}
if modelsInput.Equals(*serverModelsInput) {
term.StopSpinner()
return false
}
apiErr := api.Client.CreateCustomModels(&modelsInput)
if apiErr != nil {
term.OutputErrorAndExit("Error importing models: %v", apiErr.Msg)
return false
}
err = SaveCustomModelsHash(path, &modelsInput)
if err != nil {
term.OutputErrorAndExit("Error saving hash file: %v", err)
return false
}
inputModelIds := map[string]bool{}
inputProviderNames := map[string]bool{}
inputModelPackNames := map[string]bool{}
for _, model := range clientModelsInput.CustomModels {
inputModelIds[string(model.ModelId)] = true
}
for _, provider := range clientModelsInput.CustomProviders {
inputProviderNames[provider.Name] = true
}
for _, modelPack := range clientModelsInput.CustomModelPacks {
inputModelPackNames[modelPack.Name] = true
}
updatedModelsInput := modelsInput.FilterUnchanged(serverModelsInput)
customModels := serverModelsInput.CustomModels
customProviders := serverModelsInput.CustomProviders
customModelPacks := serverModelsInput.CustomModelPacks
term.StopSpinner()
added := strings.Builder{}
updated := strings.Builder{}
deleted := strings.Builder{}
existsById := map[string]bool{}
for _, model := range customModels {
existsById[string(model.ModelId)] = true
}
for _, provider := range customProviders {
existsById[provider.Name] = true
}
for _, modelPack := range customModelPacks {
existsById[modelPack.Name] = true
}
for _, provider := range updatedModelsInput.CustomProviders {
action := "✅ Added"
builder := &added
if existsById[provider.Name] {
action = "🔄 Updated"
builder = &updated
}
builder.WriteString(fmt.Sprintf("%s custom %s → %s\n",
action,
color.New(term.ColorHiCyan).Sprint("provider"),
color.New(color.Bold, term.ColorHiGreen).Sprint(provider.Name)))
}
for _, provider := range customProviders {
if !inputProviderNames[provider.Name] {
deleted.WriteString(fmt.Sprintf("❌ Removed custom %s → %s\n",
color.New(term.ColorHiCyan).Sprint("provider"),
color.New(color.Bold, term.ColorHiRed).Sprint(provider.Name)))
}
}
for _, model := range updatedModelsInput.CustomModels {
action := "✅ Added"
builder := &added
if existsById[string(model.ModelId)] {
action = "🔄 Updated"
builder = &updated
}
builder.WriteString(fmt.Sprintf("%s custom %s → %s\n",
action,
color.New(term.ColorHiCyan).Sprint("model"),
color.New(color.Bold, term.ColorHiGreen).Sprint(string(model.ModelId))))
}
for _, model := range customModels {
if !inputModelIds[string(model.ModelId)] {
deleted.WriteString(fmt.Sprintf("❌ Removed custom %s → %s\n",
color.New(term.ColorHiCyan).Sprint("model"),
color.New(color.Bold, term.ColorHiRed).Sprint(string(model.ModelId))))
}
}
for _, modelPack := range updatedModelsInput.CustomModelPacks {
action := "✅ Added"
builder := &added
if existsById[modelPack.Name] {
action = "🔄 Updated"
builder = &updated
}
builder.WriteString(fmt.Sprintf("%s custom %s → %s\n",
action,
color.New(term.ColorHiCyan).Sprint("model pack"),
color.New(color.Bold, term.ColorHiGreen).Sprint(modelPack.Name)))
}
for _, modelPack := range customModelPacks {
if !inputModelPackNames[modelPack.Name] {
deleted.WriteString(fmt.Sprintf("❌ Removed custom %s → %s\n",
color.New(term.ColorHiCyan).Sprint("model pack"),
color.New(color.Bold, term.ColorHiRed).Sprint(modelPack.Name)))
}
}
if updated.Len()+added.Len()+deleted.Len() == 0 {
return false
}
fmt.Print(added.String())
fmt.Print(updated.String())
fmt.Print(deleted.String())
return true
}
func SyncCustomModels() error {
userId := auth.Current.UserId
if userId == "" {
return fmt.Errorf("auth.Current.UserId is empty")
}
serverModelsInput, err := GetServerModelsInput()
if err != nil {
return fmt.Errorf("error getting server models input: %v", err)
}
MustSyncCustomModels(GetCustomModelsPath(userId), serverModelsInput)
return nil
}
================================================
FILE: app/cli/lib/editor.go
================================================
package lib
import (
"os"
"os/exec"
"path/filepath"
"plandex-cli/api"
"plandex-cli/term"
shared "plandex-shared"
"sort"
"strings"
)
func MaybePromptAndOpen(path string, defaultConfig *shared.PlanConfig, planConfig *shared.PlanConfig) bool {
var cmd string
var args []string
var openManually bool
var checkConfig *shared.PlanConfig
if planConfig != nil {
checkConfig = planConfig
} else if defaultConfig != nil {
checkConfig = defaultConfig
} else {
term.OutputErrorAndExit("Missing config")
}
if checkConfig.EditorOpenManually {
return false
}
if checkConfig.EditorCommand == "" {
editorRes := SelectEditor(true)
cmd = editorRes.Cmd
args = editorRes.Args
openManually = editorRes.OpenManually
// update the default editor config
toUpdateDefault := *defaultConfig
toUpdateDefault.Editor = editorRes.Name
toUpdateDefault.EditorCommand = cmd
toUpdateDefault.EditorArgs = args
toUpdateDefault.EditorOpenManually = openManually
apiErr := api.Client.UpdateDefaultPlanConfig(shared.UpdateDefaultPlanConfigRequest{
Config: &toUpdateDefault,
})
if apiErr != nil {
term.OutputErrorAndExit("Error updating default config: %v", apiErr)
}
// also update the current plan config
if planConfig != nil {
toUpdate := *planConfig
toUpdate.Editor = editorRes.Name
toUpdate.EditorCommand = cmd
toUpdate.EditorArgs = args
toUpdate.EditorOpenManually = openManually
apiErr = api.Client.UpdatePlanConfig(CurrentPlanId, shared.UpdatePlanConfigRequest{
Config: &toUpdate,
})
if apiErr != nil {
term.OutputErrorAndExit("Error updating plan config: %v", apiErr)
}
}
} else {
cmd = checkConfig.EditorCommand
args = checkConfig.EditorArgs
}
if openManually {
return false
}
err := exec.Command(cmd, append(args, path)...).Start()
if err != nil {
term.OutputErrorAndExit("Error opening template: %v", err)
}
return true
}
type SelectEditorResult struct {
Name string
Cmd string
Args []string
OpenManually bool
}
func SelectEditor(includeOpenManuallyOpt bool) SelectEditorResult {
editors := detectEditors()
opts := []string{}
for _, c := range editors {
opts = append(opts, c.name)
}
const otherOpt = "Other (custom command)"
opts = append(opts, otherOpt)
const openManuallyOpt = "Open files manually"
if includeOpenManuallyOpt {
opts = append(opts, openManuallyOpt)
}
choice, err := term.SelectFromList("What's your preferred editor?", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting editor: %v", err)
}
var name string
var cmd string
var args []string
if choice == otherOpt {
choice, err = term.GetRequiredUserStringInput("Enter the command to open the editor")
if err != nil {
term.OutputErrorAndExit("Error getting editor command: %v", err)
}
name = choice
parts := strings.Fields(choice)
if len(parts) == 0 {
term.OutputErrorAndExit("Invalid editor command: %s", choice)
}
cmd = parts[0]
if len(parts) > 1 {
args = parts[1:]
}
} else if choice == openManuallyOpt {
return SelectEditorResult{
Name: "Open manually",
Cmd: "",
Args: []string{},
OpenManually: true,
}
} else {
var candidate editorCandidate
for _, c := range editors {
if c.name == choice {
candidate = c
break
}
}
name = candidate.name
cmd = candidate.cmd
args = candidate.args
}
return SelectEditorResult{
Name: name,
Cmd: cmd,
Args: args,
}
}
type editorCandidate struct {
name string
cmd string
args []string
isJetBrains bool
}
const maxEditorOpts = 5
func detectEditors() []editorCandidate {
guess := []editorCandidate{
// Popular non-JetBrains launchers
{"VS Code", "code", nil, false},
{"Cursor", "cursor", nil, false},
{"Zed", "zed", nil, false},
{"Neovim", "nvim", nil, false},
// JetBrains IDE-specific launchers
{"IntelliJ IDEA", "idea", nil, true},
{"GoLand", "goland", nil, true},
{"PyCharm", "pycharm", nil, true},
{"CLion", "clion", nil, true},
{"WebStorm", "webstorm", nil, true},
{"PhpStorm", "phpstorm", nil, true},
{"DataGrip", "datagrip", nil, true},
{"RubyMine", "rubymine", nil, true},
{"Rider", "rider", nil, true},
{"DataSpell", "dataspell", nil, true},
// JetBrains universal CLI (2023.2+)
{"JetBrains (jb)", "jb", []string{"open"}, true},
{"Vim", "vim", nil, false},
{"Nano", "nano", nil, false},
{"Helix", "hx", nil, false},
{"Micro", "micro", nil, false},
{"Sublime Text", "subl", nil, false},
{"TextMate", "mate", nil, false},
{"Kakoune", "kak", nil, false},
{"Emacs", "emacs", nil, false},
{"Kate", "kate", nil, false},
}
pref := map[string]bool{}
for _, env := range []string{"VISUAL", "EDITOR"} {
if v := os.Getenv(env); v != "" {
// keep only the binary name, drop path/flags
cmd := filepath.Base(strings.Fields(v)[0])
pref[cmd] = true
}
}
_, err := exec.LookPath("jb") // true if universal launcher exists
jbOnPath := err == nil
var found []editorCandidate
for _, c := range guess {
if _, err := exec.LookPath(c.cmd); err != nil {
continue // not on PATH
}
// If jb is present, drop per-IDE launchers *unless* this exact cmd
// is marked preferred by VISUAL/EDITOR.
if jbOnPath && c.isJetBrains && !pref[c.cmd] {
continue
}
found = append(found, c)
}
for cmd := range pref {
if _, err := exec.LookPath(cmd); err == nil {
already := false
for _, c := range found {
if c.cmd == cmd {
already = true
break
}
}
if !already {
found = append(found, editorCandidate{name: cmd, cmd: cmd})
}
}
}
sort.SliceStable(found, func(i, j int) bool {
pi, pj := pref[found[i].cmd], pref[found[j].cmd]
if pi == pj {
return false // keep original order
}
return pi // true → i comes before j
})
if len(found) > maxEditorOpts {
found = found[:maxEditorOpts]
}
return found
}
================================================
FILE: app/cli/lib/git.go
================================================
package lib
import (
"fmt"
"log"
"os/exec"
"regexp"
"strings"
"sync"
"time"
)
var gitMutex sync.Mutex
func GitAddAndCommit(dir, message string, lockMutex bool) error {
if lockMutex {
gitMutex.Lock()
defer gitMutex.Unlock()
}
err := GitAdd(dir, ".", false)
if err != nil {
return fmt.Errorf("error adding files to git repository for dir: %s, err: %v", dir, err)
}
err = GitCommit(dir, message, nil, false)
if err != nil {
return fmt.Errorf("error committing files to git repository for dir: %s, err: %v", dir, err)
}
return nil
}
func GitAddAndCommitPaths(dir, message string, paths []string, lockMutex bool) error {
if len(paths) == 0 {
return nil
}
if lockMutex {
gitMutex.Lock()
defer gitMutex.Unlock()
}
for _, path := range paths {
err := GitAdd(dir, path, false)
if err != nil {
return fmt.Errorf("error adding file %s to git repository for dir: %s, err: %v", path, dir, err)
}
}
err := GitCommit(dir, message, paths, false)
if err != nil {
return fmt.Errorf("error committing files to git repository for dir: %s, err: %v", dir, err)
}
return nil
}
func GitAdd(repoDir, path string, lockMutex bool) error {
if lockMutex {
gitMutex.Lock()
defer gitMutex.Unlock()
}
res, err := exec.Command("git", "-C", repoDir, "add", path).CombinedOutput()
if err != nil {
return fmt.Errorf("error adding files to git repository for dir: %s, err: %v, output: %s", repoDir, err, string(res))
}
return nil
}
func GitCommit(repoDir, commitMsg string, paths []string, lockMutex bool) error {
if lockMutex {
gitMutex.Lock()
defer gitMutex.Unlock()
}
args := []string{"-C", repoDir, "commit", "-m", commitMsg, "--allow-empty"}
if len(paths) > 0 {
args = append(args, paths...)
}
res, err := exec.Command("git", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("error committing files to git repository for dir: %s, err: %v, output: %s", repoDir, err, string(res))
}
return nil
}
func CheckUncommittedChanges() (bool, error) {
gitMutex.Lock()
defer gitMutex.Unlock()
// Check if there are any changes
res, err := exec.Command("git", "status", "--porcelain").CombinedOutput()
if err != nil {
return false, fmt.Errorf("error checking for uncommitted changes: %v, output: %s", err, string(res))
}
// If there's output, there are uncommitted changes
return strings.TrimSpace(string(res)) != "", nil
}
func GitStashCreate(message string) error {
gitMutex.Lock()
defer gitMutex.Unlock()
res, err := exec.Command("git", "stash", "push", "--include-untracked", "-m", message).CombinedOutput()
if err != nil {
return fmt.Errorf("error creating git stash: %v, output: %s", err, string(res))
}
return nil
}
// this matches output for git version 2.39.3
// need to test on other versions and check for more variations
// there isn't any structured way to get stash conflicts from git, unfortunately
const PopStashConflictMsg = "overwritten by merge"
const ConflictMsgFilesEnd = "commit your changes"
func GitStashPop(forceOverwrite bool) error {
gitMutex.Lock()
defer gitMutex.Unlock()
res, err := exec.Command("git", "stash", "pop").CombinedOutput()
// we should no longer have conflicts since we are forcing an update before
// running the 'apply' command as well as resetting any files with uncommitted change
// still leaving this though in case something goes wrong
if err != nil {
log.Println("Error popping git stash:", string(res))
if strings.Contains(string(res), PopStashConflictMsg) {
log.Println("Conflicts detected")
if !forceOverwrite {
return fmt.Errorf("conflict popping git stash: %s", string(res))
}
// Parse the output to find which files have conflicts
conflictFiles := parseConflictFiles(string(res))
log.Println("Conflicting files:", conflictFiles)
for _, file := range conflictFiles {
// Reset each conflicting file individually
checkoutRes, err := exec.Command("git", "checkout", "--ours", file).CombinedOutput()
if err != nil {
return fmt.Errorf("error resetting file %s: %v", file, string(checkoutRes))
}
}
dropRes, err := exec.Command("git", "stash", "drop").CombinedOutput()
if err != nil {
return fmt.Errorf("error dropping git stash: %v", string(dropRes))
}
return nil
} else {
log.Println("No conflicts detected")
return fmt.Errorf("error popping git stash: %v", string(res))
}
}
return nil
}
func GitClearUncommittedChanges() error {
gitMutex.Lock()
defer gitMutex.Unlock()
// Reset staged changes
res, err := exec.Command("git", "reset", "--hard").CombinedOutput()
if err != nil {
return fmt.Errorf("error resetting staged changes | err: %v, output: %s", err, string(res))
}
// Clean untracked files
res, err = exec.Command("git", "clean", "-d", "-f").CombinedOutput()
if err != nil {
return fmt.Errorf("error cleaning untracked files | err: %v, output: %s", err, string(res))
}
return nil
}
func GitFileHasUncommittedChanges(path string) (bool, error) {
gitMutex.Lock()
defer gitMutex.Unlock()
res, err := exec.Command("git", "status", "--porcelain", path).CombinedOutput()
if err != nil {
return false, fmt.Errorf("error checking for uncommitted changes for file %s | err: %v, output: %s", path, err, string(res))
}
return strings.TrimSpace(string(res)) != "", nil
}
func GitCheckoutFile(path string) error {
gitMutex.Lock()
defer gitMutex.Unlock()
res, err := exec.Command("git", "checkout", path).CombinedOutput()
if err != nil {
log.Println("Error checking out file:", string(res))
return fmt.Errorf("error checking out file %s | err: %v, output: %s", path, err, string(res))
}
return nil
}
const GitLogTimestampFormat = "Mon Jan 2, 2006 | 3:04:05pm"
var GitLogTimestampRegex = regexp.MustCompile(`\w{3} \w{3} \d{1,2}, \d{4} \| \d{1,2}:\d{2}:\d{2}(am|pm) UTC`)
func GetGitLogTimestamp(log string) (time.Time, error) {
matches := GitLogTimestampRegex.FindStringSubmatch(log)
if len(matches) < 2 {
return time.Time{}, fmt.Errorf("no timestamp found in log")
}
return time.Parse(GitLogTimestampFormat, strings.TrimSuffix(matches[0], " UTC"))
}
func parseConflictFiles(gitOutput string) []string {
var conflictFiles []string
lines := strings.Split(gitOutput, "\n")
inFilesSection := false
for _, line := range lines {
if inFilesSection {
file := strings.TrimSpace(line)
if file == "" {
continue
}
conflictFiles = append(conflictFiles, strings.TrimSpace(line))
} else if strings.Contains(line, PopStashConflictMsg) {
inFilesSection = true
} else if strings.Contains(line, ConflictMsgFilesEnd) {
break
}
}
return conflictFiles
}
================================================
FILE: app/cli/lib/legacy_files.go
================================================
package lib
import (
"encoding/json"
"log"
"os"
"path/filepath"
"plandex-cli/fs"
"plandex-cli/term"
"plandex-cli/types"
)
func MigrateLegacyProjectFile(currentUserId string) {
if fs.PlandexDir == "" {
return
}
if currentUserId == "" {
return
}
// Migrate project.json to projects-v2.json
// New formats map to a user id so we can handle multiple accounts in the same plandex dir
projectPath := filepath.Join(fs.PlandexDir, "project.json")
if _, err := os.Stat(projectPath); err == nil {
var settings types.CurrentProjectSettings
bytes, err := os.ReadFile(projectPath)
if err != nil {
term.OutputErrorAndExit("error reading project.json: %v", err)
}
err = json.Unmarshal(bytes, &settings)
if err != nil {
term.OutputErrorAndExit("error unmarshalling project.json: %v", err)
}
v2Path := filepath.Join(fs.PlandexDir, "projects-v2.json")
settingsByAccount := types.CurrentProjectSettingsByAccount{
currentUserId: &settings,
}
bytes, err = json.Marshal(settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error marshalling projects-v2.json: %v", err)
}
err = os.WriteFile(v2Path, bytes, 0644)
if err != nil {
term.OutputErrorAndExit("error writing projects-v2.json: %v", err)
}
// Delete the v1 file after successful migration
if err := os.Remove(projectPath); err != nil {
term.OutputErrorAndExit("could not delete old project.json: %v", err)
}
log.Println("Migrated project.json to projects-v2.json")
} else if !os.IsNotExist(err) {
term.OutputErrorAndExit("error checking for project.json: %v", err)
}
}
func MigrateLegacyCurrentPlanFile(currentUserId string) {
if fs.PlandexDir == "" {
return
}
if currentUserId == "" {
return
}
if CurrentProjectId == "" {
return
}
// Migrate current_plan.json to current-plans-v2.json
planPath := filepath.Join(fs.HomePlandexDir, CurrentProjectId, "current_plan.json")
if _, err := os.Stat(planPath); err == nil {
var settings types.CurrentPlanSettings
bytes, err := os.ReadFile(planPath)
if err != nil {
term.OutputErrorAndExit("error reading current_plan.json: %v", err)
}
err = json.Unmarshal(bytes, &settings)
if err != nil {
term.OutputErrorAndExit("error unmarshalling current_plan.json: %v", err)
}
v2Path := filepath.Join(fs.HomePlandexDir, CurrentProjectId, "current-plans-v2.json")
settingsByAccount := types.CurrentPlanSettingsByAccount{
currentUserId: &settings,
}
bytes, err = json.Marshal(settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error marshalling current-plans-v2.json: %v", err)
}
err = os.WriteFile(v2Path, bytes, 0644)
if err != nil {
term.OutputErrorAndExit("error writing current-plans-v2.json: %v", err)
}
// Delete the v1 file after successful migration
if err := os.Remove(planPath); err != nil {
term.OutputErrorAndExit("could not delete old current_plan.json: %v", err)
}
log.Println("Migrated current_plan.json to current-plans-v2.json")
} else if !os.IsNotExist(err) {
term.OutputErrorAndExit("error checking for current_plan.json: %v", err)
}
}
func MigrateLegacyPlanSettingsFile(currentUserId string) {
if fs.PlandexDir == "" {
return
}
if currentUserId == "" {
return
}
if CurrentPlanId == "" {
return
}
// Migrate settings.json to settings-v2.json for current plan
settingsPath := filepath.Join(fs.HomePlandexDir, CurrentProjectId, CurrentPlanId, "settings.json")
if _, err := os.Stat(settingsPath); err == nil {
var settings types.PlanSettings
bytes, err := os.ReadFile(settingsPath)
if err != nil {
term.OutputErrorAndExit("error reading settings.json: %v", err)
}
err = json.Unmarshal(bytes, &settings)
if err != nil {
term.OutputErrorAndExit("error unmarshalling settings.json: %v", err)
}
v2Path := filepath.Join(fs.HomePlandexDir, CurrentProjectId, CurrentPlanId, "settings-v2.json")
settingsByAccount := types.PlanSettingsByAccount{
currentUserId: &settings,
}
bytes, err = json.Marshal(settingsByAccount)
if err != nil {
term.OutputErrorAndExit("error marshalling settings-v2.json: %v", err)
}
err = os.WriteFile(v2Path, bytes, 0644)
if err != nil {
term.OutputErrorAndExit("error writing settings-v2.json: %v", err)
}
// Delete the v1 file after successful migration
if err := os.Remove(settingsPath); err != nil {
term.OutputErrorAndExit("could not delete old settings.json: %v", err)
}
log.Println("Migrated settings.json to settings-v2.json")
} else if !os.IsNotExist(err) {
term.OutputErrorAndExit("error checking for settings.json: %v", err)
}
}
================================================
FILE: app/cli/lib/log_format.go
================================================
package lib
import (
"regexp"
"strings"
"time"
)
// Regular expressions for parsing log entries
var (
// Match the update ID and timestamp
updateRegex = regexp.MustCompile(`📝 Update ([a-f0-9]+)\[0;22m \| \[36m(.*?)\[0m`)
// Match message number and type
messageRegex = regexp.MustCompile(`Message #(\d+) \| (.+?) \|`)
// Match coin count
coinRegex = regexp.MustCompile(`(\d+) 🪙`)
// Match context load summary
contextRegex = regexp.MustCompile(`Loaded (\d+) .+ into context`)
)
// LogEntry represents a parsed log entry
type LogEntry struct {
ID string
Timestamp string
Type string
Message string
}
// ParseLogEntry parses a raw log entry string into a structured LogEntry
func ParseLogEntry(raw string) LogEntry {
lines := strings.Split(raw, "\n")
entry := LogEntry{}
// Parse first line for update ID and timestamp
if matches := updateRegex.FindStringSubmatch(lines[0]); len(matches) >= 3 {
entry.ID = matches[1]
entry.Timestamp = parseTimestamp(matches[2])
}
// Parse second line for message type and details
if len(lines) > 1 {
if matches := messageRegex.FindStringSubmatch(lines[1]); len(matches) >= 3 {
entry.Type = cleanType(matches[2])
entry.Message = lines[1]
} else if matches := contextRegex.FindStringSubmatch(lines[1]); len(matches) >= 2 {
entry.Type = "Context Load"
entry.Message = lines[1]
}
}
return entry
}
// FormatCompactSummary creates a compact one-line summary of the log entry
func FormatCompactSummary(entry LogEntry) string {
var summary strings.Builder
// Add timestamp
summary.WriteString(entry.Timestamp)
summary.WriteString(" | ")
// Add type indicator and summary based on type
switch {
case strings.Contains(entry.Type, "User prompt"):
summary.WriteString("💬 User: ")
msg := extractFirstLine(entry.Message)
if len(msg) > 40 {
msg = msg[:37] + "..."
}
summary.WriteString(msg)
case strings.Contains(entry.Type, "Plandex reply"):
summary.WriteString("🤖 AI: ")
if coins := coinRegex.FindStringSubmatch(entry.Message); len(coins) >= 2 {
summary.WriteString(coins[1] + "🪙")
}
case strings.Contains(entry.Type, "Context Load"):
summary.WriteString("📚 ")
if matches := contextRegex.FindStringSubmatch(entry.Message); len(matches) >= 2 {
summary.WriteString("Loaded " + matches[1] + " items")
}
case strings.Contains(entry.Type, "Build"):
summary.WriteString("🏗️ Build changes")
default:
summary.WriteString(entry.Type)
}
return summary.String()
}
// Helper functions
func parseTimestamp(ts string) string {
// Convert timestamp to a consistent format
ts = strings.TrimSpace(ts)
if ts == "Today" {
return time.Now().Format("15:04:05")
}
if ts == "Yesterday" {
return "Yesterday"
}
return ts
}
func cleanType(t string) string {
// Remove ANSI color codes and clean up type string
t = strings.TrimSpace(t)
return strings.ReplaceAll(t, "[0m", "")
}
func extractFirstLine(s string) string {
if idx := strings.Index(s, "\n"); idx != -1 {
return s[:idx]
}
return s
}
================================================
FILE: app/cli/lib/model_credentials.go
================================================
package lib
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/term"
"plandex-cli/types"
shared "plandex-shared"
"sort"
"strings"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/fatih/color"
)
type ProviderAuthStatus int
const (
FullySatisfied ProviderAuthStatus = iota
PartiallySatisfied
FullyMissing
)
type ProviderCredentialStatus struct {
ProviderComposite string
Status ProviderAuthStatus
MissingVars []string
}
type PublisherCredentialStatus struct {
Publisher shared.ModelPublisher
SelectedProvider *ProviderCredentialStatus
PartialProviders []ProviderCredentialStatus
}
type CredentialCheckResult struct {
AllSatisfied bool
Publishers []PublisherCredentialStatus
AuthVars map[string]string
}
func CheckCredentialStatus(opts shared.ModelProviderOptions, claudeMaxEnabled bool) (CredentialCheckResult, error) {
publishersToProviders := groupProvidersByPublisher(opts)
selectedAuthVars := map[string]string{}
var publisherStatuses []PublisherCredentialStatus
allSatisfied := true
for publisher, providers := range publishersToProviders {
var selectedProvider *ProviderCredentialStatus
partialProviders := []ProviderCredentialStatus{}
for _, provider := range providers {
if provider.Config.HasClaudeMaxAuth && !claudeMaxEnabled {
continue
}
authVars, err := ResolveProviderAuthVars(provider.Config)
if err != nil {
return CredentialCheckResult{}, fmt.Errorf("error checking API keys/credentials: %v", err)
}
status, missingVars, err := checkProviderCredentialStatus(provider.Config, authVars)
if err != nil {
return CredentialCheckResult{}, fmt.Errorf("error checking API keys/credentials: %v", err)
}
providerStatus := ProviderCredentialStatus{
ProviderComposite: provider.Config.ToComposite(),
Status: status,
MissingVars: missingVars,
}
if status == FullySatisfied {
selectedProvider = &providerStatus
mergeAuthVars(selectedAuthVars, authVars)
break // first fully satisfied provider found, stop looking further
} else if status == PartiallySatisfied {
partialProviders = append(partialProviders, providerStatus)
}
// otherwise,if fully missing, don't set selected provider
}
if selectedProvider == nil {
allSatisfied = false
}
publisherStatuses = append(publisherStatuses, PublisherCredentialStatus{
Publisher: publisher,
SelectedProvider: selectedProvider,
PartialProviders: partialProviders,
})
}
return CredentialCheckResult{
AllSatisfied: allSatisfied,
Publishers: publisherStatuses,
AuthVars: selectedAuthVars,
}, nil
}
func groupProvidersByPublisher(opts shared.ModelProviderOptions) map[shared.ModelPublisher][]shared.ModelProviderOption {
grouped := map[shared.ModelPublisher][]shared.ModelProviderOption{}
for _, option := range opts {
for pub := range option.Publishers {
grouped[pub] = append(grouped[pub], option)
}
}
// stable priority sort
for pub := range grouped {
sort.SliceStable(grouped[pub], func(i, j int) bool {
return grouped[pub][i].Priority < grouped[pub][j].Priority
})
}
return grouped
}
func checkProviderCredentialStatus(cfg *shared.ModelProviderConfigSchema, authVars map[string]string) (ProviderAuthStatus, []string, error) {
var missing []string
if cfg.SkipAuth {
return FullySatisfied, nil, nil
}
if cfg.HasClaudeMaxAuth {
creds, err := GetAccountCredentials()
if err != nil {
return FullyMissing, nil, fmt.Errorf("error getting account credentials: %v", err)
}
if creds == nil || creds.ClaudeMax == nil {
return FullyMissing, nil, nil
}
}
if cfg.ApiKeyEnvVar != "" && authVars[cfg.ApiKeyEnvVar] == "" {
missing = append(missing, cfg.ApiKeyEnvVar)
}
for _, extra := range cfg.ExtraAuthVars {
if extra.Required && authVars[extra.Var] == "" {
missing = append(missing, extra.Var)
}
}
numRequired := 0
for _, extra := range cfg.ExtraAuthVars {
if extra.Required {
numRequired++
}
}
if cfg.ApiKeyEnvVar != "" {
numRequired++
}
switch {
case len(missing) == 0:
return FullySatisfied, nil, nil
case len(missing) == numRequired:
return FullyMissing, missing, nil
default:
return PartiallySatisfied, missing, nil
}
}
func MustVerifyAuthVars(integratedModels bool) map[string]string {
return mustVerifyAuthVars(integratedModels, false)
}
func MustVerifyAuthVarsSilent(integratedModels bool) map[string]string {
return mustVerifyAuthVars(integratedModels, true)
}
func mustVerifyAuthVars(integratedModels, silent bool) map[string]string {
if !silent {
term.StartSpinner("")
}
planSettings, apiErr := api.Client.GetSettings(CurrentPlanId, CurrentBranch)
if apiErr != nil {
term.OutputErrorAndExit("Error getting settings: %v", apiErr)
}
orgUserConfig := MustGetOrgUserConfig()
opts := planSettings.GetModelProviderOptions()
if !silent {
if hasAnthropicModels(opts) {
didConnect := promptClaudeMaxIfNeeded()
term.StartSpinner("")
if !didConnect && orgUserConfig.UseClaudeSubscription {
didConnect = connectClaudeMaxIfNeeded()
if !didConnect {
refreshClaudeMaxCredsIfNeeded()
}
}
}
}
// For IntegratedModelsMode on Cloud, we only send the connected Claude subscription api key—nothing else
// If we're in IntegratedModelsMode and there's no connected Claude sub, return nil
if integratedModels {
if orgUserConfig.UseClaudeSubscription {
creds, err := GetAccountCredentials()
if err != nil {
term.OutputErrorAndExit("Error getting Claude subscription credentials: %v", err)
}
if creds != nil && creds.ClaudeMax != nil {
return map[string]string{
shared.AnthropicClaudeMaxTokenEnvVar: creds.ClaudeMax.AccessToken,
}
}
}
return nil
}
checkResult, err := CheckCredentialStatus(opts, orgUserConfig.UseClaudeSubscription)
if err != nil {
term.OutputErrorAndExit("Error checking API keys/credentials: %v", err)
}
if checkResult.AllSatisfied {
return checkResult.AuthVars
}
showCredentialErrorMessage(checkResult, opts)
os.Exit(1)
return nil
}
func ResolveProviderAuthVars(cfg *shared.ModelProviderConfigSchema) (map[string]string, error) {
authVars := map[string]string{}
if cfg.SkipAuth {
return authVars, nil
}
if cfg.HasAWSAuth {
// PLANDEX_AWS_PROFILE enables credential file loading from ~/.aws/credentials
// this ensures it's opt-in so we only use bedrock if user explicitly intends to
profile := os.Getenv("PLANDEX_AWS_PROFILE")
if profile != "" {
os.Setenv("AWS_PROFILE", profile)
if err := loadAWSVars(authVars); err == nil {
return authVars, nil
}
}
// if no PLANDEX_AWS_PROFILE is set OR loading aws vars fails, just silently fall through to the default env var checks
// because we're disabling the EC2 metadata service, the aws check will fail unless appropriate env vars or credentials file is found, but it's not actually a problem—just indicates AWS creds aren't set
}
if cfg.HasClaudeMaxAuth {
creds, err := GetAccountCredentials()
if err != nil {
return nil, fmt.Errorf("error getting account credentials: %v", err)
}
if creds != nil && creds.ClaudeMax != nil {
token := creds.ClaudeMax.AccessToken
authVars[shared.AnthropicClaudeMaxTokenEnvVar] = token
}
}
if cfg.ApiKeyEnvVar != "" {
val := os.Getenv(cfg.ApiKeyEnvVar)
if val != "" {
authVars[cfg.ApiKeyEnvVar] = val
}
}
for _, extra := range cfg.ExtraAuthVars {
val := os.Getenv(extra.Var)
if val == "" && extra.Default != "" {
val = extra.Default
}
if extra.MaybeJSONFilePath {
if val == "" {
continue
}
content, err := maybeLoadFile(val)
if err != nil {
return nil, fmt.Errorf("failed to load file for %s: %v", extra.Var, err)
}
authVars[extra.Var] = content
} else if val != "" {
authVars[extra.Var] = val
}
}
return authVars, nil
}
func maybeLoadFile(pathOrJson string) (string, error) {
if strings.HasPrefix(strings.TrimSpace(pathOrJson), "{") {
// var contains json directly, so we can return it as is
return pathOrJson, nil
}
// see if it's base64 encoded json
decoded, err := base64.StdEncoding.DecodeString(pathOrJson)
if err == nil {
s := string(decoded)
if strings.HasPrefix(strings.TrimSpace(s), "{") {
return s, nil
}
}
content, err := os.ReadFile(pathOrJson)
if err != nil {
return "", err
}
return string(content), nil
}
func loadAWSVars(vars map[string]string) error {
// disable IMDS to prevent slow request
os.Setenv("AWS_EC2_METADATA_DISABLED", "true")
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
return fmt.Errorf("failed to load AWS config: %v", err)
}
creds, err := cfg.Credentials.Retrieve(context.Background())
if err != nil {
return fmt.Errorf("failed to retrieve AWS credentials: %v", err)
}
vars["AWS_ACCESS_KEY_ID"] = creds.AccessKeyID
vars["AWS_SECRET_ACCESS_KEY"] = creds.SecretAccessKey
vars["AWS_REGION"] = cfg.Region
if creds.SessionToken != "" {
vars["AWS_SESSION_TOKEN"] = creds.SessionToken
}
return nil
}
func mergeAuthVars(dest, src map[string]string) {
for k, v := range src {
dest[k] = v
}
}
func showCredentialErrorMessage(res CredentialCheckResult, opts shared.ModelProviderOptions) {
term.StopSpinner()
boldRed := color.New(color.Bold, term.ColorHiRed)
cyanChip := color.New(color.BgCyan, color.FgHiWhite)
fmt.Println(boldRed.Sprint("🚨 Required API key(s) or model credentials are missing"))
someOK, someMissing := false, false
for _, p := range res.Publishers {
if p.SelectedProvider != nil && p.SelectedProvider.Status == FullySatisfied {
someOK = true
} else {
someMissing = true
}
}
if someOK && someMissing {
fmt.Println()
fmt.Println(color.New(color.Bold, term.ColorHiYellow).Sprint("⚠️ Some models are missing a provider"))
sorted := make([]PublisherCredentialStatus, 0, len(res.Publishers))
for _, p := range res.Publishers {
sorted = append(sorted, p)
}
sort.Slice(sorted, func(i, j int) bool {
readyI := sorted[i].SelectedProvider != nil && sorted[i].SelectedProvider.Status == FullySatisfied
readyJ := sorted[j].SelectedProvider != nil && sorted[j].SelectedProvider.Status == FullySatisfied
return readyI && !readyJ
})
for _, p := range sorted {
ready := p.SelectedProvider != nil && p.SelectedProvider.Status == FullySatisfied
var lbl string
if ready {
lbl = p.SelectedProvider.ProviderComposite
} else {
lbl = "missing"
}
fmt.Printf("%s %s models → %s\n", mark(ready), p.Publisher, lbl)
}
}
var partialLines []string
added := map[string]bool{}
for _, pub := range res.Publishers {
partialProviders := pub.PartialProviders
for _, sp := range partialProviders {
// already added this provider
if added[sp.ProviderComposite] {
continue
}
// compute vars set vs missing
opt, ok := opts[sp.ProviderComposite]
if !ok {
continue
}
req := requiredVars(opt.Config)
var setVars []string
for _, v := range req {
missing := false
for _, mv := range sp.MissingVars {
if mv == v {
missing = true
break
}
}
if !missing {
setVars = append(setVars, v)
}
}
added[sp.ProviderComposite] = true
partialLines = append(partialLines,
fmt.Sprintf("%s\n set → %s\n missing → %s",
color.New(color.Bold).Sprint(sp.ProviderComposite),
strings.Join(setVars, ", "),
strings.Join(sp.MissingVars, ", "),
))
}
}
if len(partialLines) > 0 {
sort.Strings(partialLines)
fmt.Println()
fmt.Println(color.New(term.ColorHiYellow, color.Bold).Sprint("⚠️ Providers with partial credentials"))
for _, l := range partialLines {
fmt.Println(l)
}
}
byPub := providersByPublisher(opts)
byPubWithoutOpenRouter := map[shared.ModelPublisher][]shared.ModelProvider{}
for pub, providers := range byPub {
nonOrProviders := []shared.ModelProvider{}
for _, provider := range providers {
if provider != shared.ModelProviderOpenRouter {
nonOrProviders = append(nonOrProviders, provider)
}
}
if len(nonOrProviders) > 0 {
byPubWithoutOpenRouter[pub] = nonOrProviders
}
}
allPublishersHaveOpenRouter := allPublishersHaveProvider(byPub, shared.ModelProviderOpenRouter)
if allPublishersHaveOpenRouter {
fmt.Println()
fmt.Println(color.New(term.ColorHiCyan, color.Bold).Sprint("🚀 Quick option → OpenRouter.ai"))
fmt.Println("OpenRouter allows you to use all models in the current model pack with a single account and API key. To get started:")
fmt.Println()
step := func(n int, txt string) { fmt.Printf("%d. %s\n", n, txt) }
step(1, "Sign up at "+color.New(color.Bold).Sprint("https://openrouter.ai/sign-up"))
step(2, "Buy some credits at "+color.New(color.Bold).Sprint("https://openrouter.ai/settings/credits"))
step(3, "Generate an API key at "+color.New(color.Bold).Sprint("https://openrouter.ai/settings/keys"))
if term.IsRepl {
step(4, "Quit the REPL with "+cyanChip.Sprint(" \\quit "))
step(5, "Run "+cyanChip.Sprint(" export OPENROUTER_API_KEY=… "))
step(6, "Restart the REPL with "+cyanChip.Sprint(" plandex "))
} else {
step(4, "Run "+cyanChip.Sprint(" export OPENROUTER_API_KEY=… "))
}
}
if len(byPubWithoutOpenRouter) > 0 {
fmt.Println()
fmt.Println(color.New(term.ColorHiCyan, color.Bold).Sprint("🔑 Other model providers"))
if allPublishersHaveOpenRouter {
fmt.Println("You can also use the following providers for the current model pack:")
} else {
fmt.Println("You can use the following providers for the current model pack:")
}
fmt.Println()
pubs := make([]string, 0, len(byPubWithoutOpenRouter))
for p := range byPubWithoutOpenRouter {
pubs = append(pubs, string(p))
}
sort.Strings(pubs)
for _, p := range pubs {
providers := byPubWithoutOpenRouter[shared.ModelPublisher(p)]
providerNames := make([]string, 0, len(providers))
for _, provider := range providers {
providerNames = append(providerNames, string(provider))
}
fmt.Printf("%s → %s\n", color.New(color.Bold).Sprint(p+" models"), strings.Join(providerNames, ", "))
}
fmt.Println(color.New(color.Bold, term.ColorHiCyan).Sprint("\n📖 Per-provider instructions"))
fmt.Println("For details on the API key/credentials required for each provider, go to:\n" + color.New(color.Bold).Sprint("https://docs.plandex.ai/models/model-providers"))
}
fmt.Println()
}
// return required env‑var names (API key + required extras)
func requiredVars(cfg *shared.ModelProviderConfigSchema) []string {
var vars []string
if cfg.ApiKeyEnvVar != "" {
vars = append(vars, cfg.ApiKeyEnvVar)
}
for _, ex := range cfg.ExtraAuthVars {
if ex.Required {
vars = append(vars, ex.Var)
}
}
return vars
}
func providersByPublisher(opts shared.ModelProviderOptions) map[shared.ModelPublisher][]shared.ModelProvider {
byPub := map[shared.ModelPublisher][]shared.ModelProvider{}
sortedOpts := make([]shared.ModelProviderOption, 0, len(opts))
for _, opt := range opts {
sortedOpts = append(sortedOpts, opt)
}
sort.Slice(sortedOpts, func(i, j int) bool {
return sortedOpts[i].Priority < sortedOpts[j].Priority
})
for _, opt := range sortedOpts {
for pub := range opt.Publishers {
byPub[pub] = append(byPub[pub], opt.Config.Provider)
}
}
return byPub
}
func allPublishersHaveProvider(byPub map[shared.ModelPublisher][]shared.ModelProvider, p shared.ModelProvider) bool {
if len(byPub) == 0 {
return false
}
for _, providers := range byPub {
found := false
for _, provider := range providers {
if provider == p {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// emoji for satisfied vs missing
func mark(ok bool) string {
if ok {
return "✅"
}
return "❌"
}
var cachedAccountCredentials *types.AccountCredentials
func SetAccountCredentials(creds *types.AccountCredentials) error {
if auth.Current == nil {
return fmt.Errorf("no authenticated user")
}
dir := filepath.Join(fs.HomePlandexDir, auth.Current.UserId, auth.Current.OrgId)
err := os.MkdirAll(dir, 0700)
if err != nil {
return fmt.Errorf("error creating account credentials directory: %v", err)
}
path := filepath.Join(dir, "creds.json")
bytes, err := json.MarshalIndent(creds, "", " ")
if err != nil {
return fmt.Errorf("error marshalling account credentials: %v", err)
}
err = os.WriteFile(path, bytes, 0600)
if err != nil {
return fmt.Errorf("error writing account credentials: %v", err)
}
cachedAccountCredentials = creds
return nil
}
func GetAccountCredentials() (*types.AccountCredentials, error) {
if cachedAccountCredentials != nil {
return cachedAccountCredentials, nil
}
if auth.Current == nil {
return nil, fmt.Errorf("no authenticated user")
}
path := filepath.Join(fs.HomePlandexDir, auth.Current.UserId, auth.Current.OrgId, "creds.json")
bytes, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
var creds types.AccountCredentials
err = json.Unmarshal(bytes, &creds)
if err != nil {
return nil, err
}
cachedAccountCredentials = &creds
return &creds, nil
}
================================================
FILE: app/cli/lib/model_settings.go
================================================
package lib
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/fs"
"plandex-cli/schema"
"plandex-cli/term"
shared "plandex-shared"
"github.com/fatih/color"
)
var DefaultModelSettingsPath string
func init() {
DefaultModelSettingsPath = filepath.Join(fs.HomePlandexDir, "default-model-settings.json")
}
func GetPlanModelSettingsPath(planId string) string {
return filepath.Join(fs.HomePlandexDir, planId, "model-settings.json")
}
type ModelSettingsCheckLocalChangesResult struct {
HasLocalChanges bool
LocalModelPackSchemaRoles *shared.ModelPackSchemaRoles
}
func ModelSettingsCheckLocalChanges(path string) (ModelSettingsCheckLocalChangesResult, error) {
hashPath := path + ".hash"
exists, err := fs.FileExists(path)
if err != nil {
return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error checking model settings file: %v", err)
}
if !exists {
return ModelSettingsCheckLocalChangesResult{}, nil
}
lastSavedHash, err := os.ReadFile(hashPath)
if err != nil && !os.IsNotExist(err) {
return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error reading hash file: %v", err)
}
localJsonData, err := os.ReadFile(path)
if err != nil {
return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error reading JSON file: %v", err)
}
var clientModelPackSchemaRoles *shared.ClientModelPackSchemaRoles
err = json.Unmarshal(localJsonData, &clientModelPackSchemaRoles)
if err != nil {
return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error unmarshalling JSON file: %v", err)
}
currentHash, err := clientModelPackSchemaRoles.ToModelPackSchemaRoles().Hash()
if err != nil {
return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error hashing model pack: %v", err)
}
modelPackSchemaRoles := clientModelPackSchemaRoles.ToModelPackSchemaRoles()
return ModelSettingsCheckLocalChangesResult{
HasLocalChanges: currentHash != string(lastSavedHash),
LocalModelPackSchemaRoles: &modelPackSchemaRoles,
}, nil
}
func WriteModelSettingsFile(path string, originalSettings *shared.PlanSettings) error {
err := os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
return fmt.Errorf("error creating directory: %v", err)
}
modelPackSchemaRoles := originalSettings.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles
clientModelPackRoles := modelPackSchemaRoles.ToClientModelPackSchemaRoles()
bytes, err := json.MarshalIndent(clientModelPackRoles, "", " ")
if err != nil {
return fmt.Errorf("error marshalling model pack: %v", err)
}
err = os.WriteFile(path, bytes, 0644)
if err != nil {
return fmt.Errorf("error writing JSON file: %v", err)
}
err = SaveModelPackRolesHash(path, &modelPackSchemaRoles)
if err != nil {
return fmt.Errorf("error saving model pack roles hash: %v", err)
}
return nil
}
func SaveModelPackRolesHash(basePath string, serverModelPack *shared.ModelPackSchemaRoles) error {
hashPath := basePath + ".hash"
hash, err := serverModelPack.Hash()
if err != nil {
return fmt.Errorf("error hashing model pack: %v", err)
}
err = os.WriteFile(hashPath, []byte(hash), 0644)
if err != nil {
return fmt.Errorf("error writing hash file: %v", err)
}
return nil
}
func ApplyModelSettings(path string, originalSettings *shared.PlanSettings) (*shared.PlanSettings, error) {
jsonData, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading JSON file: %v", err)
}
settings, err := originalSettings.DeepCopy()
if err != nil {
return nil, fmt.Errorf("error copying settings: %v", err)
}
clientModelPackRoles, err := schema.ValidateModelPackInlineJSON(jsonData)
if err != nil {
term.StopSpinner()
color.New(color.Bold, term.ColorHiRed).Println("🚨 Error validating JSON file")
fmt.Println(err.Error())
os.Exit(1)
}
modelPackRoles := clientModelPackRoles.ToModelPackSchemaRoles()
modelPackSchema := shared.ModelPackSchema{
Name: "custom",
Description: "Model pack with custom settings",
ModelPackSchemaRoles: modelPackRoles,
}
modelPack := modelPackSchema.ToModelPack()
settings.SetCustomModelPack(&modelPack)
err = SaveModelPackRolesHash(path, &modelPackRoles)
if err != nil {
return nil, fmt.Errorf("error saving model settings hash: %v", err)
}
return settings, nil
}
func SaveLatestPlanModelSettingsIfNeeded() (bool, error) {
path := GetPlanModelSettingsPath(CurrentPlanId)
jsonData, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("error reading JSON file: %v", err)
}
var clientModelPackSchemaRoles *shared.ClientModelPackSchemaRoles
err = json.Unmarshal(jsonData, &clientModelPackSchemaRoles)
if err != nil {
return false, fmt.Errorf("error unmarshalling JSON file: %v", err)
}
modelPackSchemaRoles := clientModelPackSchemaRoles.ToModelPackSchemaRoles()
localHash, err := modelPackSchemaRoles.Hash()
if err != nil {
return false, fmt.Errorf("error hashing model pack: %v", err)
}
settings, apiErr := api.Client.GetSettings(CurrentPlanId, CurrentBranch)
if apiErr != nil {
return false, fmt.Errorf("error getting settings: %v", apiErr)
}
serverHash, err := settings.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles.Hash()
if err != nil {
return false, fmt.Errorf("error hashing model pack: %v", err)
}
if localHash == serverHash {
return false, nil
}
err = WriteModelSettingsFile(path, settings)
if err != nil {
return false, fmt.Errorf("error writing model settings file: %v", err)
}
return true, nil
}
// save settings in file to server
func SyncPlanModelSettings() error {
settings, err := api.Client.GetSettings(CurrentPlanId, CurrentBranch)
if err != nil {
return fmt.Errorf("error getting settings: %v", err)
}
updatedSettings, apiErr := ApplyModelSettings(GetPlanModelSettingsPath(CurrentPlanId), settings)
if apiErr != nil {
return fmt.Errorf("error applying model settings: %v", err)
}
res, updateErr := api.Client.UpdateSettings(CurrentPlanId, CurrentBranch, shared.UpdateSettingsRequest{
ModelPackName: updatedSettings.ModelPackName,
ModelPack: updatedSettings.ModelPack,
})
if updateErr != nil {
return fmt.Errorf("error updating settings: %v", err)
}
if res == nil {
return nil
}
fmt.Println(res.Msg)
return nil
}
func SyncDefaultModelSettings() error {
settings, err := api.Client.GetOrgDefaultSettings()
if err != nil {
return fmt.Errorf("error getting settings: %v", err)
}
updatedSettings, apiErr := ApplyModelSettings(DefaultModelSettingsPath, settings)
if apiErr != nil {
return fmt.Errorf("error applying model settings: %v", err)
}
res, updateErr := api.Client.UpdateOrgDefaultSettings(shared.UpdateSettingsRequest{
ModelPackName: updatedSettings.ModelPackName,
ModelPack: updatedSettings.ModelPack,
})
if updateErr != nil {
return fmt.Errorf("error updating settings: %v", err)
}
if res == nil {
return nil
}
fmt.Println(res.Msg)
return nil
}
================================================
FILE: app/cli/lib/models_sync.go
================================================
package lib
import (
"fmt"
"plandex-cli/auth"
"plandex-cli/term"
"github.com/fatih/color"
)
func PromptSyncModelsIfNeeded() error {
var changes []string
var onApprove []func() error
userId := auth.Current.UserId
if userId == "" {
return fmt.Errorf("auth.Current.UserId is empty")
}
customModelsPath := GetCustomModelsPath(userId)
customModelsRes, err := CustomModelsCheckLocalChanges(customModelsPath)
if err != nil {
return fmt.Errorf("error checking custom models: %v", err)
}
if customModelsRes.HasLocalChanges {
changes = append(
changes,
fmt.Sprintf("%s → %s", color.New(term.ColorHiCyan, color.Bold).Sprint("Custom models"), customModelsPath))
onApprove = append(onApprove, SyncCustomModels)
}
defaultModelSettingsRes, err := ModelSettingsCheckLocalChanges(DefaultModelSettingsPath)
if err != nil {
return fmt.Errorf("error checking default model settings: %v", err)
}
if defaultModelSettingsRes.HasLocalChanges {
changes = append(
changes,
fmt.Sprintf("%s → %s", color.New(term.ColorHiCyan, color.Bold).Sprint("Default model settings"), DefaultModelSettingsPath))
onApprove = append(onApprove, SyncDefaultModelSettings)
}
planModelSettingsRes, err := ModelSettingsCheckLocalChanges(GetPlanModelSettingsPath(CurrentPlanId))
if err != nil {
return fmt.Errorf("error checking plan model settings: %v", err)
}
if planModelSettingsRes.HasLocalChanges {
changes = append(
changes,
fmt.Sprintf("%s → %s", color.New(term.ColorHiCyan, color.Bold).Sprint("Plan model settings"), GetPlanModelSettingsPath(CurrentPlanId)))
onApprove = append(onApprove, SyncPlanModelSettings)
}
if len(changes) == 0 {
return nil
}
term.StopSpinner()
color.New(color.Bold, term.ColorHiYellow).Println("⚠️ Model settings have local changes")
fmt.Println()
for _, change := range changes {
fmt.Println(change)
}
fmt.Println()
shouldSave, err := term.ConfirmYesNo("Save changes now?")
if err != nil {
return fmt.Errorf("error confirming: %v", err)
}
if !shouldSave {
return nil
}
for _, fn := range onApprove {
err := fn()
if err != nil {
return fmt.Errorf("error syncing models: %v", err)
}
}
return nil
}
================================================
FILE: app/cli/lib/org_user_config.go
================================================
package lib
import (
"plandex-cli/api"
"plandex-cli/term"
shared "plandex-shared"
)
var cachedOrgUserConfig *shared.OrgUserConfig
func MustGetOrgUserConfig() *shared.OrgUserConfig {
if cachedOrgUserConfig != nil {
return cachedOrgUserConfig
}
orgUserConfig, err := api.Client.GetOrgUserConfig()
if err != nil {
term.OutputErrorAndExit("Error getting org user config: %v", err)
}
cachedOrgUserConfig = orgUserConfig
return orgUserConfig
}
func MustUpdateOrgUserConfig(orgUserConfig shared.OrgUserConfig) {
err := api.Client.UpdateOrgUserConfig(orgUserConfig)
if err != nil {
term.OutputErrorAndExit("Error updating org user config: %v", err)
}
SetCachedOrgUserConfig(&orgUserConfig)
}
func SetCachedOrgUserConfig(orgUserConfig *shared.OrgUserConfig) {
cachedOrgUserConfig = orgUserConfig
}
================================================
FILE: app/cli/lib/plan_config.go
================================================
package lib
import (
"os"
"plandex-cli/api"
"plandex-cli/term"
"sort"
shared "plandex-shared"
"github.com/olekukonko/tablewriter"
)
var cachedPlanConfig *shared.PlanConfig
func MustGetCurrentPlanConfig() *shared.PlanConfig {
if cachedPlanConfig != nil {
return cachedPlanConfig
}
planConfig, apiErr := api.Client.GetPlanConfig(CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan config: %v", apiErr)
}
cachedPlanConfig = planConfig
return planConfig
}
func SetCachedPlanConfig(planConfig *shared.PlanConfig) {
cachedPlanConfig = planConfig
}
func ShowPlanConfig(config *shared.PlanConfig, key string) {
table := tablewriter.NewWriter(os.Stdout)
table.SetAutoWrapText(true)
table.SetHeader([]string{"Name", "Value", "Description"})
numVisibleSettings := 0
for k, setting := range shared.ConfigSettingsByKey {
if key != "" && k != key {
continue
}
if setting.Visible == nil || setting.Visible(config) {
numVisibleSettings++
}
}
numOutput := 0
sortedSettings := make([][]string, 0, len(shared.ConfigSettingsByKey))
for k, setting := range shared.ConfigSettingsByKey {
if key != "" && k != key {
continue
}
if setting.Visible == nil || setting.Visible(config) {
var sortKey string
if setting.SortKey != "" {
sortKey = setting.SortKey
} else {
sortKey = k
}
sortedSettings = append(sortedSettings, []string{sortKey, setting.Name, setting.Getter(config), setting.Desc})
}
}
sort.Slice(sortedSettings, func(i, j int) bool {
return sortedSettings[i][0] < sortedSettings[j][0]
})
for _, row := range sortedSettings {
table.Append(row[1:])
numOutput++
if numOutput < numVisibleSettings {
table.Append([]string{"", "", ""})
}
}
table.Render()
}
================================================
FILE: app/cli/lib/plans.go
================================================
package lib
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/types"
"sync"
)
func WriteCurrentPlan(id string) error {
if fs.HomePlandexDir == "" {
return fmt.Errorf("HomePlandexDir not set")
}
if CurrentProjectId == "" || HomeCurrentPlanPath == "" {
return fmt.Errorf("no current project")
}
var currentPlanSettingsByAccount *types.CurrentPlanSettingsByAccount
bytes, err := os.ReadFile(HomeCurrentPlanPath)
if err == nil {
err = json.Unmarshal(bytes, ¤tPlanSettingsByAccount)
if err != nil {
return fmt.Errorf("error unmarshalling current-plans-v2.json: %v", err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("error checking if current-plans-v2.json exists: %v", err)
}
if currentPlanSettingsByAccount == nil {
currentPlanSettingsByAccount = &types.CurrentPlanSettingsByAccount{}
}
settings := types.CurrentPlanSettings{
Id: id,
}
(*currentPlanSettingsByAccount)[auth.Current.UserId] = &settings
bytes, err = json.Marshal(currentPlanSettingsByAccount)
if err != nil {
return fmt.Errorf("error marshalling current plan: %v", err)
}
err = os.WriteFile(HomeCurrentPlanPath, bytes, 0644)
if err != nil {
return fmt.Errorf("error writing current plan: %v", err)
}
CurrentPlanId = id
return nil
}
func ClearCurrentPlan() error {
if fs.HomePlandexDir == "" {
return fmt.Errorf("HomePlandexDir not set")
}
if CurrentProjectId == "" || HomeCurrentPlanPath == "" {
return fmt.Errorf("no current project")
}
var currentPlanSettingsByAccount *types.CurrentPlanSettingsByAccount
bytes, err := os.ReadFile(HomeCurrentPlanPath)
if err == nil {
err = json.Unmarshal(bytes, ¤tPlanSettingsByAccount)
if err != nil {
return fmt.Errorf("error unmarshalling current-plans-v2.json: %v", err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("error checking if current-plans-v2.json exists: %v", err)
}
if currentPlanSettingsByAccount != nil {
delete(*currentPlanSettingsByAccount, auth.Current.UserId)
}
bytes, err = json.Marshal(currentPlanSettingsByAccount)
if err != nil {
return fmt.Errorf("error marshalling current plan: %v", err)
}
err = os.WriteFile(HomeCurrentPlanPath, bytes, 0644)
if err != nil {
return fmt.Errorf("error writing current plan: %v", err)
}
CurrentPlanId = ""
return nil
}
func WriteCurrentBranch(branch string) error {
if fs.HomePlandexDir == "" {
return fmt.Errorf("HomePlandexDir not set")
}
if CurrentProjectId == "" || HomeCurrentPlanPath == "" {
return fmt.Errorf("no current project")
}
if CurrentPlanId == "" {
return fmt.Errorf("no current plan")
}
dir := filepath.Join(fs.HomePlandexDir, CurrentProjectId, CurrentPlanId)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return fmt.Errorf("error creating plan dir: %v", err)
}
path := filepath.Join(dir, "settings-v2.json")
var settingsByAccount *types.PlanSettingsByAccount
bytes, err := os.ReadFile(path)
if err == nil {
err = json.Unmarshal(bytes, &settingsByAccount)
if err != nil {
return fmt.Errorf("error unmarshalling settings-v2.json: %v", err)
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("error checking if settings-v2.json exists: %v", err)
}
if settingsByAccount == nil {
settingsByAccount = &types.PlanSettingsByAccount{}
}
existingSettings := (*settingsByAccount)[auth.Current.UserId]
if existingSettings == nil {
existingSettings = &types.PlanSettings{}
}
existingSettings.Branch = branch
(*settingsByAccount)[auth.Current.UserId] = existingSettings
bytes, err = json.Marshal(settingsByAccount)
if err != nil {
return fmt.Errorf("error marshalling current plan settings: %v", err)
}
err = os.WriteFile(path, bytes, 0644)
if err != nil {
return fmt.Errorf("error writing current plan settings: %v", err)
}
CurrentBranch = branch
return nil
}
func GetCurrentBranchNamesByPlanId(planIds []string) (map[string]string, error) {
if fs.HomePlandexDir == "" {
return nil, fmt.Errorf("HomePlandexDir not set")
}
if CurrentProjectId == "" || HomeCurrentPlanPath == "" {
return nil, fmt.Errorf("no current project")
}
var mu sync.Mutex
branches := make(map[string]string)
errCh := make(chan error, len(planIds))
for _, planId := range planIds {
go func(planId string) {
branch, err := getPlanCurrentBranch(planId)
if err != nil {
errCh <- fmt.Errorf("error getting plan current branch: %v", err)
} else {
mu.Lock()
defer mu.Unlock()
branches[planId] = branch
errCh <- nil
}
}(planId)
}
for i := 0; i < len(planIds); i++ {
err := <-errCh
if err != nil {
return nil, err
}
}
return branches, nil
}
func getPlanCurrentBranch(planId string) (string, error) {
if fs.HomePlandexDir == "" {
return "", fmt.Errorf("HomePlandexDir not set")
}
if CurrentProjectId == "" || HomeCurrentPlanPath == "" {
return "", fmt.Errorf("no current project")
}
v2Path := filepath.Join(fs.HomePlandexDir, CurrentProjectId, planId, "settings-v2.json")
var settings *types.PlanSettings
// check if settings-v2.json exists
_, err := os.Stat(v2Path)
if err == nil {
// read settings-v2.json
var settingsByAccount types.PlanSettingsByAccount
bytes, err := os.ReadFile(v2Path)
if err != nil {
return "", fmt.Errorf("error reading settings-v2.json: %v", err)
}
err = json.Unmarshal(bytes, &settingsByAccount)
if err != nil {
return "", fmt.Errorf("error unmarshalling settings-v2.json: %v", err)
}
settings = settingsByAccount[auth.Current.UserId]
} else if os.IsNotExist(err) {
return "main", nil
} else {
return "", fmt.Errorf("error checking if settings-v2.json exists: %v", err)
}
if settings == nil {
return "main", nil
}
return settings.Branch, nil
}
================================================
FILE: app/cli/lib/repl.go
================================================
package lib
import (
"encoding/json"
"os"
"os/exec"
"os/signal"
"path/filepath"
"plandex-cli/fs"
"plandex-cli/term"
"strconv"
"syscall"
)
var ReplSettingsDir string
type ReplMode string
const (
ReplModeTell ReplMode = "tell"
ReplModeChat ReplMode = "chat"
)
type ReplState struct {
Mode ReplMode
IsMulti bool
}
var CurrentReplState = ReplState{
Mode: ReplModeChat,
IsMulti: false,
}
type ReplSettings struct {
State ReplState
History []string
}
var ReplCmdAliases = map[string]string{
"chat": "ch",
"tell": "t",
"multi": "m",
"quit": "q",
"help": "h",
"run": "r",
"send": "s",
}
func init() {
ReplSettingsDir = filepath.Join(fs.HomePlandexDir, "repl_settings")
}
func EnsureReplSettingsFile() {
if err := os.MkdirAll(ReplSettingsDir, os.ModePerm); err != nil {
term.OutputErrorAndExit("Error creating repl history directory: %v", err)
}
settingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+".json")
if _, err := os.Stat(settingsFile); os.IsNotExist(err) {
file, err := os.Create(settingsFile)
if err != nil {
term.OutputErrorAndExit("Error creating history file: %v", err)
}
defer file.Close()
// Write empty settings object
var settings ReplSettings
data, err := json.Marshal(settings)
if err != nil {
term.OutputErrorAndExit("Error converting settings to JSON: %v", err)
}
if _, err := file.Write(data); err != nil {
term.OutputErrorAndExit("Error writing to history file: %v", err)
}
}
}
func writeSettings(settings *ReplSettings) {
settingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+".json")
data, err := json.Marshal(settings)
if err != nil {
term.OutputErrorAndExit("Error converting settings to JSON: %v", err)
}
if err := os.WriteFile(settingsFile, data, 0644); err != nil {
term.OutputErrorAndExit("Error writing settings file: %v", err)
}
}
func getSettings() *ReplSettings {
EnsureReplSettingsFile()
settingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+".json")
// Read existing settings
data, err := os.ReadFile(settingsFile)
if err != nil {
term.OutputErrorAndExit("Error reading history file: %v", err)
}
// Parse JSON
var settings ReplSettings
if err := json.Unmarshal(data, &settings); err != nil {
term.OutputErrorAndExit("Error parsing history file: %v", err)
}
return &settings
}
func LoadState() {
settings := getSettings()
if settings.State.Mode != "" {
CurrentReplState = settings.State
} else {
// Write default state
WriteState()
}
}
func WriteState() {
settings := getSettings()
settings.State = CurrentReplState
writeSettings(settings)
}
func WriteHistory(input string) {
settings := getSettings()
// Add new input
settings.History = append(settings.History, input)
writeSettings(settings)
}
func GetHistory() []string {
settings := getSettings()
return settings.History
}
// ExecPlandexCommand spawns the same binary, wiring std streams directly so you
// don't have to capture output. Any os.Exit calls in the child won't kill your REPL.
func ExecPlandexCommand(args []string) (string, error) {
return ExecPlandexCommandWithParams(args, ExecPlandexCommandParams{})
}
type ExecPlandexCommandParams struct {
DisableSuggestions bool
SessionId string
}
func ExecPlandexCommandWithParams(args []string, params ExecPlandexCommandParams) (string, error) {
// Create temp file
tmpFile, err := os.CreateTemp("", "plandex-output-*")
if err != nil {
return "", err
}
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
var env []string = os.Environ()
if os.Getenv("PLANDEX_REPL") == "" {
columns := term.GetTerminalWidth()
hasDarkBackground := term.HasDarkBackground()
streamForegroundColor := term.GetStreamForegroundColor()
var glamourStyle string
if hasDarkBackground {
glamourStyle = "dark"
} else {
glamourStyle = "light"
}
// Set env vars
env = append(env,
"PLANDEX_REPL=1",
"PLANDEX_REPL_OUTPUT_FILE="+tmpPath,
"PLANDEX_COLUMNS="+strconv.Itoa(columns),
"PLANDEX_STREAM_FOREGROUND_COLOR="+streamForegroundColor.Sequence(false),
"GLAMOUR_STYLE="+glamourStyle,
"PLANDEX_SKIP_UPGRADE=1",
)
if params.SessionId != "" {
env = append(env, "PLANDEX_REPL_SESSION_ID="+params.SessionId)
}
}
if params.DisableSuggestions {
env = append(env, "PLANDEX_DISABLE_SUGGESTIONS=1")
}
// Run command
cmd := exec.Command(os.Args[0], args...)
cmd.Env = env
// Connect stdin directly
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
return "", err
}
signal.Ignore(syscall.SIGINT)
defer signal.Reset(syscall.SIGINT)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-sigChan
syscall.Kill(-cmd.Process.Pid, sig.(syscall.Signal))
}()
err = cmd.Wait()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
return "", nil
}
return "", err
}
// Read output from temp file
output, err := os.ReadFile(tmpPath)
if err != nil {
return "", err
}
return string(output), nil
}
================================================
FILE: app/cli/lib/rewind.go
================================================
package lib
import (
"fmt"
"os"
"path/filepath"
"plandex-cli/fs"
shared "plandex-shared"
"sort"
"sync"
"time"
)
// GetUndonePlanApplies returns the list of PlanApplies that will be undone by rewinding to targetSHA.
// An apply is considered "undone" if its timestamp is after OR equal to the target SHA's timestamp,
// since we want to revert to the state before the target SHA.
func GetUndonePlanApplies(currentState *shared.CurrentPlanState, timestamp time.Time) []*shared.PlanApply {
if currentState == nil {
return nil
}
var undoneApplies []*shared.PlanApply
for _, apply := range currentState.PlanApplies {
// Include applies after OR equal to target time
if !apply.CreatedAt.Before(timestamp) {
undoneApplies = append(undoneApplies, apply)
}
}
// Sort by creation time ascending to ensure proper order
sort.Slice(undoneApplies, func(i, j int) bool {
return undoneApplies[i].CreatedAt.Before(undoneApplies[j].CreatedAt)
})
return undoneApplies
}
// GetAffectedFilePaths extracts the set of file paths that were modified by the given PlanApplies.
// It looks up each PlanFileResultId in the current state to get the actual file paths.
func GetAffectedFilePaths(currentState *shared.CurrentPlanState, applies []*shared.PlanApply) map[string]bool {
if currentState == nil || currentState.PlanResult == nil {
return nil
}
// First collect all file result IDs
fileResultIds := make(map[string]bool)
for _, apply := range applies {
if apply == nil {
continue
}
for _, fileId := range apply.PlanFileResultIds {
if fileId != "" {
fileResultIds[fileId] = true
}
}
}
// Then get the actual file paths from the plan result
affectedPaths := make(map[string]bool)
for _, result := range currentState.PlanResult.Results {
if result == nil {
continue
}
// Skip if this result wasn't part of an undone apply
if !fileResultIds[result.Id] {
continue
}
// Skip if the result was rejected
if result.RejectedAt != nil {
continue
}
// Skip if the result was never applied
if result.AppliedAt == nil {
continue
}
// Validate path
if result.Path == "" {
continue
}
// Check if path is in plan context
if currentState.ContextsByPath[result.Path] == nil {
continue
}
affectedPaths[result.Path] = true
}
return affectedPaths
}
// RewindAnalysis captures all the information about a potential rewind operation
type RewindAnalysis struct {
// Files that need to be modified when rewinding from current plan state to target plan state
RequiredChanges map[string]string
// Files that have been modified on disk relative to current plan state (potential conflicts)
Conflicts map[string]bool
}
// AnalyzeRewind examines the three states (disk, current plan, target plan) to determine:
// 1. What files need to be changed to reach target state
// 2. Which of those changes would conflict with user modifications
func AnalyzeRewind(targetState, currentState *shared.CurrentPlanState) (*RewindAnalysis, error) {
if targetState == nil || currentState == nil {
return nil, fmt.Errorf("both target and current states must be provided")
}
// First determine what files need to be changed between current and target plan states
requiredChanges := make(map[string]string)
// Track all paths we need to examine for either changes or conflicts
allPaths := make(map[string]bool)
// Add paths from both states
for path, context := range targetState.ContextsByPath {
if context.ContextType != shared.ContextFileType {
continue
}
allPaths[path] = true
}
for path, context := range currentState.ContextsByPath {
if context.ContextType != shared.ContextFileType {
continue
}
allPaths[path] = true
}
// For each path, check if content differs between current and target states
for path := range allPaths {
targetContent := ""
if ctx := targetState.ContextsByPath[path]; ctx != nil {
targetContent = ctx.Body
}
currentContent := ""
if ctx := currentState.ContextsByPath[path]; ctx != nil {
currentContent = ctx.Body
}
// If content differs between plan states, this is a required change
if targetContent != currentContent {
if targetContent == "" {
// File should be removed
requiredChanges[path] = ""
} else {
// File should be added or modified
requiredChanges[path] = targetContent
}
}
}
// Now check for conflicts by comparing disk state with current plan state
// A conflict exists if a file that needs to be changed has been modified on disk
conflicts := make(map[string]bool)
var mu sync.Mutex
errCh := make(chan error, len(requiredChanges))
for path := range requiredChanges {
go func(path string) {
var outErr error
defer func() { errCh <- outErr }()
// Get the content from current plan state
currentContent := ""
if ctx := currentState.ContextsByPath[path]; ctx != nil {
currentContent = ctx.Body
}
// Get the actual file content from disk
dstPath := filepath.Join(fs.ProjectRoot, path)
diskContent, err := os.ReadFile(dstPath)
if err != nil {
if os.IsNotExist(err) {
// If file doesn't exist on disk and current state has no content,
// there's no conflict
if currentContent == "" {
return
}
// Otherwise it's a conflict because file was deleted
mu.Lock()
conflicts[path] = true
mu.Unlock()
return
}
outErr = fmt.Errorf("failed to read %s: %w", path, err)
return
}
// If disk content differs from current plan state, we have a conflict
if string(diskContent) != currentContent {
mu.Lock()
conflicts[path] = true
mu.Unlock()
}
}(path)
}
// Collect any errors from goroutines
for i := 0; i < len(requiredChanges); i++ {
if err := <-errCh; err != nil {
return nil, err
}
}
return &RewindAnalysis{
RequiredChanges: requiredChanges,
Conflicts: conflicts,
}, nil
}
// RemoveEmptyDirs recursively removes empty directories starting from the given path
func RemoveEmptyDirs(path string, baseDir string) error {
// Check if the path is a directory
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !info.IsDir() {
return nil
}
// List directory contents
entries, err := os.ReadDir(path)
if err != nil {
return err
}
// If directory has contents, leave it alone
if len(entries) > 0 {
return nil
}
// Directory is empty, remove it if it's not the base dir
if path != baseDir {
if err := os.Remove(path); err != nil {
return err
}
}
// Recursively check parent directory
parent := filepath.Dir(path)
if parent != baseDir && parent != path {
return RemoveEmptyDirs(parent, baseDir)
}
return nil
}
// ApplyRewindChanges updates files on disk to match target state
func ApplyRewindChanges(requiredChanges map[string]string) error {
if len(requiredChanges) == 0 {
return nil
}
// Track directories that might need cleanup
dirsToCheck := make(map[string]bool)
var mu sync.Mutex
errCh := make(chan error, len(requiredChanges))
for path, content := range requiredChanges {
go func(path, content string) {
dstPath := filepath.Join(fs.ProjectRoot, path)
if content == "" {
// Remove the file
err := os.Remove(dstPath)
if err != nil && !os.IsNotExist(err) {
errCh <- fmt.Errorf("failed to remove %s: %w", path, err)
return
}
// Mark parent directory for cleanup
parentDir := filepath.Dir(dstPath)
mu.Lock()
dirsToCheck[parentDir] = true
mu.Unlock()
errCh <- nil
return
}
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
errCh <- fmt.Errorf("failed to create directory for %s: %w", path, err)
return
}
// Write the file
if err := os.WriteFile(dstPath, []byte(content), 0644); err != nil {
errCh <- fmt.Errorf("failed to write %s: %w", path, err)
return
}
errCh <- nil
}(path, content)
}
// Collect any errors
for i := 0; i < len(requiredChanges); i++ {
if err := <-errCh; err != nil {
return err
}
}
// Clean up empty directories
for dir := range dirsToCheck {
if err := RemoveEmptyDirs(dir, fs.ProjectRoot); err != nil {
// Log but don't fail the operation for directory cleanup errors
fmt.Printf("Warning: failed to clean up directory %s: %v\n", dir, err)
}
}
return nil
}
================================================
FILE: app/cli/main.go
================================================
package main
import (
"log"
"os"
"path/filepath"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/cmd"
"plandex-cli/fs"
"plandex-cli/lib"
"plandex-cli/plan_exec"
"plandex-cli/term"
"plandex-cli/types"
"plandex-cli/ui"
shared "plandex-shared"
"gopkg.in/natefinch/lumberjack.v2"
)
func init() {
// inter-package dependency injections to avoid circular imports
auth.SetApiClient(api.Client)
auth.SetOpenUnauthenticatedCloudURLFn(ui.OpenUnauthenticatedCloudURL)
auth.SetOpenAuthenticatedURLFn(ui.OpenAuthenticatedURL)
term.SetOpenAuthenticatedURLFn(ui.OpenAuthenticatedURL)
term.SetOpenUnauthenticatedCloudURLFn(ui.OpenUnauthenticatedCloudURL)
term.SetConvertTrialFn(auth.ConvertTrial)
plan_exec.SetPromptSyncModelsIfNeeded(lib.PromptSyncModelsIfNeeded)
lib.SetBuildPlanInlineFn(func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error) {
authVars := lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode)
return plan_exec.Build(plan_exec.ExecParams{
CurrentPlanId: lib.CurrentPlanId,
CurrentBranch: lib.CurrentBranch,
AuthVars: authVars,
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
return lib.CheckOutdatedContextWithOutput(true, autoConfirm, maybeContexts, projectPaths)
},
}, types.BuildFlags{})
})
// set up a rotating file logger
logger := &lumberjack.Logger{
Filename: filepath.Join(fs.HomePlandexDir, "plandex.log"),
MaxSize: 10, // megabytes before rotation
MaxBackups: 3, // number of backups to keep
MaxAge: 28, // days to keep old logs
Compress: true, // compress rotated files
}
// Set the output of the logger
log.SetOutput(logger)
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
// log.Println("Starting Plandex - logging initialized")
}
func main() {
// Manually check for help flags at the root level
if len(os.Args) == 2 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
// Display your custom help here
term.PrintCustomHelp(true)
os.Exit(0)
}
var firstArg string
if len(os.Args) > 1 {
firstArg = os.Args[1]
}
if firstArg != "version" && firstArg != "browser" && firstArg != "help" && firstArg != "h" {
checkForUpgrade()
}
cmd.Execute()
}
================================================
FILE: app/cli/nodemon.json
================================================
{
"watch": [
".",
"../shared"
],
"ext": "go,mod,sum",
"exec": "./dev.sh"
}
================================================
FILE: app/cli/plan.json
================================================
{
"name": "draft",
"proposalId": "",
"rootId": "",
"createdAt": "2023-11-04T09:44:29.147Z",
"updatedAt": "2023-11-04T09:44:29.147Z",
"description": null,
"contextTokens": 0,
"convoTokens": 0,
"convoSummarizedTokens": 0
}
================================================
FILE: app/cli/plan_exec/action_menu.go
================================================
package plan_exec
import (
"fmt"
"os"
"plandex-cli/api"
"plandex-cli/lib"
"plandex-cli/term"
shared "plandex-shared"
"strings"
"github.com/eiannone/keyboard"
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
)
type hotkeyOption struct {
char string
key keyboard.Key
command string
description string
replOnly bool
terminalOnly bool
dropdownOnly bool
applyScriptOnly bool
}
var allHotkeyOptions = []hotkeyOption{
{
char: "d",
command: "diff ui",
description: "Review diffs in browser UI",
replOnly: false,
terminalOnly: false,
},
{
char: "g",
command: "git diff format",
description: "Review diffs in git diff format",
replOnly: false,
terminalOnly: false,
},
{
char: "r",
command: "reject",
description: "Reject some or all pending changes",
replOnly: false,
terminalOnly: false,
},
{
char: "a",
command: "apply",
description: "Apply all pending changes",
replOnly: false,
terminalOnly: false,
},
{
char: "f",
command: "full auto",
description: "Apply and debug in full auto mode",
replOnly: true,
terminalOnly: false,
applyScriptOnly: true,
},
{
char: "q",
key: keyboard.KeyEnter,
command: "exit menu",
description: "Exit menu",
dropdownOnly: true,
},
}
func showHotkeyMenu(diffs []string) {
hasApplyScript := false
for _, diff := range diffs {
if diff == "_apply.sh" {
hasApplyScript = true
break
}
}
numDiffs := len(diffs)
if hasApplyScript {
numDiffs--
}
if numDiffs > 0 {
s := "files have"
if numDiffs == 1 {
s = "file has"
}
color.New(color.Bold, term.ColorHiGreen).Printf("🧐 %d %s pending changes\n", numDiffs, s)
for _, diff := range diffs {
if diff == "_apply.sh" {
continue
}
fmt.Printf("• 📄 %s\n", diff)
}
fmt.Println()
}
if hasApplyScript {
color.New(color.Bold, term.ColorHiYellow).Println("🚀 Commands pending")
fmt.Println()
}
var b strings.Builder
table := tablewriter.NewWriter(&b)
table.SetAutoWrapText(false)
table.SetHeaderLine(false)
table.SetAlignment(tablewriter.ALIGN_LEFT)
for _, opt := range allHotkeyOptions {
if (opt.terminalOnly && term.IsRepl) || (opt.replOnly && !term.IsRepl) || opt.dropdownOnly || (opt.applyScriptOnly && !hasApplyScript) {
continue
}
c := color.New(term.ColorHiCyan, color.Bold)
if opt.command == "apply" {
c = color.New(term.ColorHiGreen, color.Bold)
} else if opt.command == "reject" {
c = color.New(term.ColorHiRed, color.Bold)
} else if opt.command == "full auto" {
c = color.New(term.ColorHiYellow, color.Bold)
}
table.Append([]string{
c.Sprintf("(%s)", opt.char),
opt.command,
opt.description,
})
}
table.Render()
fmt.Print(b.String())
fmt.Printf("%s %s %s %s %s",
color.New(term.ColorHiMagenta, color.Bold).Sprint("Press a hotkey,"),
color.New(color.FgHiWhite, color.Bold).Sprintf("↓"),
color.New(term.ColorHiMagenta, color.Bold).Sprintf("to select, or"),
color.New(color.FgHiWhite, color.Bold).Sprintf("enter"),
color.New(term.ColorHiMagenta, color.Bold).Sprintf("to exit menu/keep iterating>"),
)
}
func handleHotkey(diffs []string, params ExecParams) {
char, key, err := term.GetUserKeyInput()
if err != nil {
fmt.Printf("\nError getting key: %v\n", err)
showHotkeyMenu(diffs)
handleHotkey(diffs, params)
}
if key == keyboard.KeyArrowDown {
options := []string{}
for _, opt := range allHotkeyOptions {
if (opt.terminalOnly && term.IsRepl) || (opt.replOnly && !term.IsRepl) {
continue
}
options = append(options, opt.description)
}
selected, err := term.SelectFromList(
"Select an action",
options,
)
if err != nil {
fmt.Printf("\nError selecting action: %v\n", err)
showHotkeyMenu(diffs)
handleHotkey(diffs, params)
}
if selected != "" {
var option hotkeyOption
for _, opt := range allHotkeyOptions {
if opt.description == selected {
if (opt.terminalOnly && term.IsRepl) || (opt.replOnly && !term.IsRepl) {
continue
}
option = opt
break
}
}
handleHotkeyOption(option, diffs, params)
}
}
handleHotkeyOption(hotkeyOption{char: string(char), key: key}, diffs, params)
}
func handleHotkeyOption(option hotkeyOption, diffs []string, params ExecParams) {
exitUnlessDiffs := func() {
diffs, apiErr := getDiffs(params)
if apiErr != nil {
fmt.Printf("\nError getting plan diffs: %v\n", apiErr.Msg)
os.Exit(0)
}
if len(diffs) == 0 {
os.Exit(0)
}
showHotkeyMenu(diffs)
handleHotkey(diffs, params)
}
fmt.Println()
if option.char == "d" {
fmt.Println()
_, err := lib.ExecPlandexCommandWithParams([]string{"diffs", "--ui", "--from-tell-menu"}, lib.ExecPlandexCommandParams{
DisableSuggestions: true,
})
if err != nil {
fmt.Printf("\nError showing diffs: %v\n", err)
}
fmt.Println()
} else if option.char == "g" {
fmt.Println()
_, err := lib.ExecPlandexCommandWithParams([]string{"diffs", "--git"}, lib.ExecPlandexCommandParams{
DisableSuggestions: true,
})
if err != nil {
fmt.Printf("\nError showing diffs: %v\n", err)
}
fmt.Println()
} else if option.char == "a" {
fmt.Print("(a)")
fmt.Println()
_, err := lib.ExecPlandexCommand([]string{"apply"})
if err != nil {
fmt.Printf("\nError applying changes: %v\n", err)
}
fmt.Println()
os.Exit(0)
} else if option.char == "r" {
fmt.Println()
_, err := lib.ExecPlandexCommand([]string{"reject"})
if err != nil {
fmt.Printf("\nError rejecting changes: %v\n", err)
}
fmt.Println()
exitUnlessDiffs()
} else if option.char == "f" {
fmt.Print("(f)")
fmt.Println()
color.New(term.ColorHiYellow, color.Bold).Println("⚠️ Full auto mode allows automatic apply, execution, and multiple rounds of debugging without review.")
fmt.Println()
_, err := lib.ExecPlandexCommand([]string{"apply", "--full"})
if err != nil {
fmt.Printf("\nError applying changes: %v\n", err)
}
fmt.Println()
os.Exit(0)
} else if option.char == "q" || option.key == keyboard.KeyEnter {
os.Exit(0)
} else {
fmt.Println("\nInvalid hotkey")
}
showHotkeyMenu(diffs)
handleHotkey(diffs, params)
}
func getDiffs(params ExecParams) ([]string, *shared.ApiError) {
currentPlan, apiErr := api.Client.GetCurrentPlanState(params.CurrentPlanId, params.CurrentBranch)
if apiErr != nil {
return nil, apiErr
}
return currentPlan.PlanResult.SortedPaths, nil
}
================================================
FILE: app/cli/plan_exec/apply_exec.go
================================================
package plan_exec
import (
"fmt"
"log"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/lib"
"plandex-cli/term"
"plandex-cli/types"
shared "plandex-shared"
"github.com/fatih/color"
)
func GetOnApplyExecFail(applyFlags types.ApplyFlags, tellFlags types.TellFlags) types.OnApplyExecFailFn {
return getOnApplyExecFail(applyFlags, tellFlags, "")
}
func GetOnApplyExecFailWithCommand(applyFlags types.ApplyFlags, tellFlags types.TellFlags, execCommand string) types.OnApplyExecFailFn {
return getOnApplyExecFail(applyFlags, tellFlags, execCommand)
}
func getOnApplyExecFail(applyFlags types.ApplyFlags, tellFlags types.TellFlags, execCommand string) types.OnApplyExecFailFn {
var onExecFail types.OnApplyExecFailFn
onExecFail = func(status int, output string, attempt int, toRollback *types.ApplyRollbackPlan, onErr types.OnErrFn, onSuccess func()) {
var proceed bool
resetAttempts := false
if applyFlags.AutoDebug > 0 {
if attempt >= applyFlags.AutoDebug {
timesLbl := "times"
if attempt == 1 {
timesLbl = "time"
}
color.New(term.ColorHiRed, color.Bold).Printf("Commands failed %d %s.\n", attempt, timesLbl)
} else {
proceed = true
}
}
if !proceed {
const (
DebugAndRetry = "Debug and retry once"
DebugInFullAutoMode = "Debug in full auto mode"
RollbackChangesAndExit = "Rollback changes and exit"
ApplyChangesAndExit = "Apply changes and exit"
)
opts := []string{
DebugAndRetry,
DebugInFullAutoMode,
RollbackChangesAndExit,
ApplyChangesAndExit,
}
selection, err := term.SelectFromList("What do you want to do?", opts)
if err != nil {
term.OutputErrorAndExit("failed to get confirmation user input: %s", err)
}
switch selection {
case DebugAndRetry:
proceed = true
case DebugInFullAutoMode:
proceed = true
resetAttempts = true
term.StartSpinner("")
config, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)
if apiErr != nil {
term.OutputErrorAndExit("failed to get plan config: %s", apiErr)
}
if config.AutoMode != shared.AutoModeFull {
config.SetAutoMode(shared.AutoModeFull)
apiErr = api.Client.UpdatePlanConfig(lib.CurrentPlanId, shared.UpdatePlanConfigRequest{
Config: config,
})
if apiErr != nil {
term.OutputErrorAndExit("failed to update plan config: %s", apiErr)
}
lib.SetCachedPlanConfig(config)
applyFlags.AutoCommit = true
applyFlags.AutoConfirm = true
applyFlags.AutoExec = true
applyFlags.AutoDebug = config.AutoDebugTries
tellFlags.AutoApply = true
tellFlags.AutoContext = true
tellFlags.ExecEnabled = true
tellFlags.SmartContext = true
term.StopSpinner()
fmt.Println()
fmt.Println("✅ Full auto mode enabled")
fmt.Println()
} else {
term.StopSpinner()
}
case RollbackChangesAndExit:
if toRollback != nil {
lib.Rollback(toRollback, true)
}
os.Exit(1)
case ApplyChangesAndExit:
onSuccess()
return
}
}
if proceed {
if toRollback != nil && toRollback.HasChanges() {
lib.Rollback(toRollback, true)
}
authVars := lib.MustVerifyAuthVarsSilent(auth.Current.IntegratedModelsMode)
prompt := fmt.Sprintf("Execution failed with exit status %d. Output:\n\n%s\n\n--\n\n",
status, output)
tellFlags.IsUserContinue = false
if execCommand != "" {
tellFlags.IsApplyDebug = false
tellFlags.ExecEnabled = false
tellFlags.IsUserDebug = true
} else {
tellFlags.IsApplyDebug = true
tellFlags.ExecEnabled = true
tellFlags.IsUserDebug = false
}
log.Printf("Calling TellPlan for next debug attempt")
TellPlan(ExecParams{
CurrentPlanId: lib.CurrentPlanId,
CurrentBranch: lib.CurrentBranch,
AuthVars: authVars,
CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
return lib.CheckOutdatedContextWithOutput(true, true, maybeContexts, projectPaths)
},
}, prompt, tellFlags)
log.Printf("Applying plan after tell")
if resetAttempts {
attempt = 0
}
lib.MustApplyPlanAttempt(lib.ApplyPlanParams{
PlanId: lib.CurrentPlanId,
Branch: lib.CurrentBranch,
ApplyFlags: applyFlags,
TellFlags: tellFlags,
OnExecFail: onExecFail,
ExecCommand: execCommand,
}, attempt+1)
}
}
return onExecFail
}
================================================
FILE: app/cli/plan_exec/build.go
================================================
package plan_exec
import (
"fmt"
"log"
"plandex-cli/api"
"plandex-cli/fs"
"plandex-cli/stream"
streamtui "plandex-cli/stream_tui"
"plandex-cli/term"
"plandex-cli/types"
shared "plandex-shared"
)
func Build(params ExecParams, flags types.BuildFlags) (bool, error) {
buildBg := flags.BuildBg
term.StartSpinner("")
err := PromptSyncModelsIfNeeded()
if err != nil {
term.OutputErrorAndExit("Error syncing models: %v", err)
}
term.StartSpinner("")
contexts, apiErr := api.Client.ListContext(params.CurrentPlanId, params.CurrentBranch)
if apiErr != nil {
term.OutputErrorAndExit("Error getting context: %v", apiErr)
}
paths, err := fs.GetProjectPaths(fs.GetBaseDirForContexts(contexts))
if err != nil {
return false, fmt.Errorf("error getting project paths: %v", err)
}
anyOutdated, didUpdate, err := params.CheckOutdatedContext(contexts, paths)
if err != nil {
term.OutputErrorAndExit("error checking outdated context: %v", err)
}
if anyOutdated && !didUpdate {
term.StopSpinner()
log.Println("Build canceled")
return false, nil
}
apiErr = api.Client.BuildPlan(params.CurrentPlanId, params.CurrentBranch, shared.BuildPlanRequest{
ConnectStream: !buildBg,
ProjectPaths: paths.ActivePaths,
AuthVars: params.AuthVars,
}, stream.OnStreamPlan)
term.StopSpinner()
if apiErr != nil {
if apiErr.Msg == shared.NoBuildsErr {
fmt.Println("🤷♂️ This plan has no pending changes to build")
return false, nil
}
return false, fmt.Errorf("error building plan: %v", apiErr.Msg)
}
if !buildBg {
ch := make(chan error)
go func() {
err := streamtui.StartStreamUI("", true, !flags.AutoApply)
if err != nil {
ch <- fmt.Errorf("error starting stream UI: %v", err)
return
}
ch <- nil
}()
// Wait for the stream to finish
err := <-ch
if err != nil {
return false, err
}
}
return true, nil
}
================================================
FILE: app/cli/plan_exec/params.go
================================================
package plan_exec
import (
"plandex-cli/types"
shared "plandex-shared"
)
type ExecParams struct {
CurrentPlanId string
CurrentBranch string
AuthVars map[string]string
CheckOutdatedContext func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error)
}
var PromptSyncModelsIfNeeded func() error
func SetPromptSyncModelsIfNeeded(fn func() error) {
PromptSyncModelsIfNeeded = fn
}
================================================
FILE: app/cli/plan_exec/tell.go
================================================
package plan_exec
import (
"fmt"
"log"
"os"
"plandex-cli/api"
"plandex-cli/auth"
"plandex-cli/fs"
"plandex-cli/stream"
streamtui "plandex-cli/stream_tui"
"plandex-cli/term"
"plandex-cli/types"
"plandex-cli/ui"
"time"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/shopspring/decimal"
)
// For cloud trials in Integrated Models mode, we warn after the stream finishes when the balance is less than $1
const CloudTrialBalanceWarningThreshold = 1
func TellPlan(
params ExecParams,
prompt string,
flags types.TellFlags,
) {
tellBg := flags.TellBg
tellStop := flags.TellStop
tellNoBuild := flags.TellNoBuild
isUserContinue := flags.IsUserContinue
isDebugCmd := flags.IsUserDebug
isChatOnly := flags.IsChatOnly
autoContext := flags.AutoContext
smartContext := flags.SmartContext
execEnabled := flags.ExecEnabled
autoApply := flags.AutoApply
isApplyDebug := flags.IsApplyDebug
isImplementationOfChat := flags.IsImplementationOfChat
skipChangesMenu := flags.SkipChangesMenu
done := make(chan struct{})
if prompt == "" && isImplementationOfChat {
prompt = "Go ahead with the plan based on what we've discussed so far."
}
outputPromptIfTell := func() {
if isUserContinue || prompt == "" {
return
}
term.StopSpinner()
// print prompt so it isn't lost
color.New(term.ColorHiCyan, color.Bold).Println("\nYour prompt 👇")
fmt.Println()
fmt.Println(prompt)
fmt.Println()
}
term.StartSpinner("")
err := PromptSyncModelsIfNeeded()
if err != nil {
outputPromptIfTell()
term.OutputErrorAndExit("Error syncing models: %v", err)
}
term.StartSpinner("")
contexts, apiErr := api.Client.ListContext(params.CurrentPlanId, params.CurrentBranch)
if apiErr != nil {
outputPromptIfTell()
term.OutputErrorAndExit("Error getting context: %v", apiErr)
}
paths, err := fs.GetProjectPaths(fs.GetBaseDirForContexts(contexts))
if err != nil {
outputPromptIfTell()
term.OutputErrorAndExit("Error getting project paths: %v", err)
}
anyOutdated, didUpdate, err := params.CheckOutdatedContext(contexts, paths)
if err != nil {
outputPromptIfTell()
term.OutputErrorAndExit("Error checking outdated context: %v", err)
}
if anyOutdated && !didUpdate {
term.StopSpinner()
if isUserContinue {
log.Println("Plan won't continue")
} else {
log.Println("Prompt not sent")
}
outputPromptIfTell()
color.New(term.ColorHiRed, color.Bold).Println("🛑 Plan won't continue due to outdated context")
os.Exit(0)
}
var fn func() bool
fn = func() bool {
var buildMode shared.BuildMode
if tellNoBuild || isChatOnly {
buildMode = shared.BuildModeNone
} else {
buildMode = shared.BuildModeAuto
}
// if isUserContinue {
// term.StartSpinner("⚡️ Continuing plan...")
// } else {
// term.StartSpinner("💬 Sending prompt...")
// }
term.StartSpinner("")
var osDetails string
if execEnabled {
osDetails = term.GetOsDetails()
}
isGitRepo := fs.ProjectRootIsGitRepo()
apiErr := api.Client.TellPlan(params.CurrentPlanId, params.CurrentBranch, shared.TellPlanRequest{
Prompt: prompt,
ConnectStream: !tellBg,
AutoContinue: !tellStop,
ProjectPaths: paths.ActivePaths,
BuildMode: buildMode,
IsUserContinue: isUserContinue,
IsUserDebug: isDebugCmd,
IsChatOnly: isChatOnly,
AutoContext: autoContext,
SmartContext: smartContext,
ExecEnabled: execEnabled,
OsDetails: osDetails,
AuthVars: params.AuthVars,
IsImplementationOfChat: isImplementationOfChat,
IsGitRepo: isGitRepo,
SessionId: os.Getenv("PLANDEX_REPL_SESSION_ID"),
}, stream.OnStreamPlan)
term.StopSpinner()
if apiErr != nil {
if apiErr.Type == shared.ApiErrorTypeTrialMessagesExceeded {
fmt.Fprintf(os.Stderr, "\n🚨 You've reached the Plandex Cloud trial limit of %d messages per plan\n", apiErr.TrialMessagesExceededError.MaxReplies)
res, err := term.ConfirmYesNo("Upgrade now?")
if err != nil {
outputPromptIfTell()
term.OutputErrorAndExit("Error prompting upgrade trial: %v", err)
}
if res {
auth.ConvertTrial()
// retry action after converting trial
return fn()
}
outputPromptIfTell()
return false
}
outputPromptIfTell()
term.OutputErrorAndExit("Prompt error: %v", apiErr.Msg)
} else if apiErr != nil && isUserContinue && apiErr.Type == shared.ApiErrorTypeContinueNoMessages {
fmt.Println("🤷♂️ There's no plan yet to continue")
fmt.Println()
term.PrintCmds("", "tell")
os.Exit(0)
}
if !tellBg {
go func() {
err := streamtui.StartStreamUI(
prompt,
false,
!(autoApply || autoContext || isApplyDebug || isDebugCmd),
)
if err != nil {
outputPromptIfTell()
term.OutputErrorAndExit("Error starting stream UI: %v", err)
}
if auth.Current.IsCloud && auth.Current.IntegratedModelsMode && auth.Current.OrgIsTrial {
term.StartSpinner("")
balance, apiErr := api.Client.GetBalance()
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting balance: %v", apiErr.Msg)
return
}
if balance.LessThan(decimal.NewFromInt(CloudTrialBalanceWarningThreshold)) {
color.New(term.ColorHiYellow, color.Bold).Printf("\n⚠️ Your Plandex Cloud trial has $%s in credits remaining\n\n", balance.StringFixed(2))
const continueOpt = "Continue"
const billingSettingsOpt = "Go to billing settings (then continue)"
opts := []string{continueOpt, billingSettingsOpt}
choice, err := term.SelectFromList("What do you want to do?", opts)
if err != nil {
term.OutputErrorAndExit("Error selecting option: %v", err)
}
if choice == billingSettingsOpt {
ui.OpenAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing")
}
}
}
if isChatOnly {
term.StopSpinner()
if !term.IsRepl {
term.PrintCmds("", "tell", "convo", "summary", "log")
}
} else if autoApply || isDebugCmd || isApplyDebug {
term.StopSpinner()
// do nothing, allow auto apply to run
} else if skipChangesMenu {
term.StopSpinner()
// script mode, don't show menu
} else {
term.StartSpinner("")
// sleep a little to prevent lock contention on server
time.Sleep(500 * time.Millisecond)
diffs, apiErr := getDiffs(params)
term.StopSpinner()
if apiErr != nil {
term.OutputErrorAndExit("Error getting plan diffs: %v", apiErr.Msg)
return
}
numDiffs := len(diffs)
hasDiffs := numDiffs > 0
fmt.Println()
if tellStop && hasDiffs {
if hasDiffs {
// term.PrintCmds("", "continue", "diff", "diff --ui", "apply", "reject", "log")
showHotkeyMenu(diffs)
handleHotkey(diffs, params)
} else {
term.PrintCmds("", "continue", "log")
}
} else if hasDiffs {
// term.PrintCmds("", "diff", "diff --ui", "apply", "reject", "log")
showHotkeyMenu(diffs)
handleHotkey(diffs, params)
}
}
close(done)
}()
}
return true
}
shouldContinue := fn()
if !shouldContinue {
return
}
if tellBg {
outputPromptIfTell()
fmt.Println("✅ Plan is active in the background")
fmt.Println()
term.PrintCmds("", "ps", "connect", "stop")
} else {
<-done
}
}
================================================
FILE: app/cli/schema/json-schemas/definitions/auto-modes.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/definitions/auto-modes.schema.json",
"title": "Auto Mode Enum",
"description": "Reusable enum for auto modes",
"enum": [
"full",
"semi",
"plus",
"basic",
"none",
"custom"
]
}
================================================
FILE: app/cli/schema/json-schemas/definitions/local-providers.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/definitions/local-providers.schema.json",
"title": "Local Provider Enum",
"description": "Reusable enum for local providers",
"enum": [
"ollama"
]
}
================================================
FILE: app/cli/schema/json-schemas/definitions/model-providers.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/definitions/model-providers.schema.json",
"title": "Model Providers",
"description": "The built-in model providers that Plandex supports. Use 'custom' for a provider that is not built-in.",
"enum": [
"openrouter",
"openai",
"anthropic",
"google-ai-studio",
"google-vertex",
"azure-openai",
"deepseek",
"perplexity",
"custom"
]
}
================================================
FILE: app/cli/schema/json-schemas/model-config.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/model-config.schema.json",
"title": "Model Config",
"description": "Config for a model",
"type": "object",
"properties": {
"modelId": {
"type": "string",
"description": "Unique identifier for the model on the Plandex side.\n\nIt's distinct from the 'modelName', which is associated with a specific provider. Different modelIds can be used when the same model is called with different settings. Examples: 'openai/o3-high', 'openai/o3-low', 'anthropic/claude-sonnet-4'."
},
"publisher": {
"type": "string",
"description": "The publisher of the model, e.g. 'OpenAI', 'Anthropic', 'Google', 'DeepSeek', etc.\n\nNot necessarily the same as the provider—for example, the 'google-vertex' provider serves models published by Google, but also models published by Anthropic and others."
},
"description": {
"type": "string",
"description": "A human-readable description of the model, e.g. 'OpenAI o3'."
},
"defaultMaxConvoTokens": {
"type": "number",
"description": "The default maximum number of conversation tokens that are allowed before Plandex starts using gradual summarization to shorten the conversation."
},
"maxTokens": {
"type": "number",
"description": "The maximum number of input tokens the model can be called with."
},
"maxOutputTokens": {
"type": "number",
"description": "The maximum number of output tokens the model can produce."
},
"reservedOutputTokens": {
"type": "number",
"description": "How many tokens are set aside in context for the model to use in its output.\n\nIt's more of a realistic output limit than 'maxOutputTokens', since for some models, the hard maximum 'MaxTokens' is actually equal to the input limit, which would leave no room for input. The effective input limit is 'MaxTokens' - 'ReservedOutputTokens'.\n\nFor example, OpenAI o3 models have a MaxTokens of 200k and a MaxOutputTokens of 100k. But in practice, we are very unlikely to use all the output tokens, and we want to leave more space for input. So we set ReservedOutputTokens to 40k, allowing ~25k for reasoning tokens, as well as ~15k for real output tokens, which is enough for most use cases. The new effective input limit is therefore 200k - 40k = 160k.\n\nNote that these are not passed through as hard limits. So if we have a smaller amount of input (under 100k) the model could still use up to the full 100k output tokens if necessary."
},
"preferredOutputFormat": {
"type": "string",
"description": "The preferred output format for the model—currently either 'xml' or 'tool-call-json'.\n\nOpenAI models like JSON (and benefit from strict JSON schemas), while most other providers are unreliable for JSON generation and do better with XML, even when they claim to support JSON.",
"enum": [
"xml",
"tool-call-json"
]
},
"systemPromptDisabled": {
"type": "boolean",
"description": "Whether the model's system prompt is disabled. This is used to disable the system prompt for the model. Some OpenAI models, for example, don't allow system prompts."
},
"roleParamsDisabled": {
"type": "boolean",
"description": "Whether the model's role-based parameters (mainly temperature and topP) are disabled. Some OpenAI models, for example, don't allow changes to these parameters."
},
"stopDisabled": {
"type": "boolean",
"description": "Whether the model's 'stop token' parameter is disabled. Some OpenAI models, for example, don't allow the 'stop token' parameter. When this is true, Plandex uses its own stop token implementation."
},
"predictedOutputEnabled": {
"type": "boolean",
"description": "Whether the model's 'predicted output' parameter is enabled. This is used to enable predicted output for the model (currently only supported by OpenAI's gpt-4o). Not currently used by Plandex, but could be in the future."
},
"includeReasoning": {
"type": "boolean",
"description": "For reasoning models, whether the reasoning should be included in the output. If set to false, the reasoning will be hidden from the user."
},
"reasoningBudget": {
"type": "number",
"description": "For reasoning models, the maximum number of tokens that can be used for reasoning. This is used to limit the reasoning budget for the model.\n\nSome reasoning models use 'reasoningBudget' to control reasoning output (e.g. Anthropic Claude Sonnet 4, Google Gemini 2.5 Pro), while others use 'reasoningEffort' (e.g. OpenAI o3)."
},
"hasImageSupport": {
"type": "boolean",
"description": "Whether the model is multi-modal and supports images in context."
},
"reasoningEffortEnabled": {
"type": "boolean",
"description": "For reasoning models, whether the 'reasoningEffort' parameter is enabled. This is used in conjunction with 'reasoningEffort' to control the reasoning budget for the model.\n\nSome reasoning models use 'reasoningEffort' to control reasoning output (e.g. OpenAI o3), while others use 'reasoningBudget' (e.g. Anthropic Claude Sonnet 4, Google Gemini 2.5 Pro)."
},
"reasoningEffort": {
"type": "string",
"enum": [
"low",
"medium",
"high"
],
"description": "For reasoning models that use 'reasoningEffort' to control reasoning output (e.g. OpenAI o3), this is the reasoning effort to use."
},
"supportsCacheControl": {
"type": "boolean",
"description": "Whether the model supports cache control breakpoints for caching (e.g. Anthropic models). Models with implicit caching (e.g. OpenAI models) do not support this."
},
"singleMessageNoSystemPrompt": {
"type": "boolean",
"description": "Whether the model rejects a single message that is a system prompt (e.g. Anthropic models)."
},
"tokenEstimatePaddingPct": {
"type": "number",
"description": "The percentage of tokens to add to the token estimate, which uses the OpenAI tokenizer. This helps to account for other provider's tokenizers, which may be slightly different."
},
"providers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"provider": {
"description": "The model provider. Use 'custom' for a provider that is not built-in.",
"$ref": "./definitions/model-providers.schema.json"
},
"customProvider": {
"type": "string",
"description": "If the provider is 'custom', this is the name of the custom provider."
},
"modelName": {
"type": "string",
"description": "The name of the model on the provider's side. It must exactly match the model name as it appears on the provider's website or documentation."
}
},
"required": [
"provider",
"modelName"
],
"allOf": [
{
"if": {
"properties": {
"provider": {
"const": "custom"
}
}
},
"then": {
"required": [
"customProvider"
]
},
"else": {
"not": {
"required": [
"customProvider"
]
}
}
}
]
},
"minItems": 1
}
},
"required": [
"modelId",
"defaultMaxConvoTokens",
"maxTokens",
"maxOutputTokens",
"reservedOutputTokens",
"preferredOutputFormat",
"providers"
],
"additionalProperties": false
}
================================================
FILE: app/cli/schema/json-schemas/model-pack-base-config.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/model-pack-base-config.schema.json",
"title": "Base Model Pack Config",
"description": "Base config for a model pack",
"type": "object",
"allOf": [
{
"$ref": "./model-pack-roles.schema.json"
}
],
"properties": {
"$schema": {
"type": "string"
},
"name": {
"type": "string",
"description": "The name of the model pack"
},
"description": {
"type": "string",
"description": "The human-friendly description of the model pack"
}
},
"required": [
"name",
"description"
]
}
================================================
FILE: app/cli/schema/json-schemas/model-pack-config.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/model-pack-config.schema.json",
"title": "Model Pack Config",
"allOf": [
{
"$ref": "./model-pack-base-config.schema.json"
}
],
"properties": {
"$schema": true,
"name": true,
"description": true,
"localProvider": true,
"planner": true,
"coder": true,
"architect": true,
"summarizer": true,
"builder": true,
"wholeFileBuilder": true,
"names": true,
"commitMessages": true,
"autoContinue": true
},
"additionalProperties": false
}
================================================
FILE: app/cli/schema/json-schemas/model-pack-inline.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/model-pack-inline.schema.json",
"title": "Inline Model Pack Config",
"description": "Inline model pack config for plan model settings or default model settings",
"allOf": [
{
"$ref": "./model-pack-roles.schema.json"
}
],
"properties": {
"$schema": true,
"name": true,
"description": true,
"localProvider": true,
"planner": true,
"coder": true,
"architect": true,
"summarizer": true,
"builder": true,
"wholeFileBuilder": true,
"names": true,
"commitMessages": true,
"autoContinue": true
},
"additionalProperties": false
}
================================================
FILE: app/cli/schema/json-schemas/model-pack-roles.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/model-pack-roles.schema.json",
"title": "Model Pack Roles",
"type": "object",
"description": "Config for a model pack's roles",
"definitions": {
"roleRef": {
"description": "Can be a string like 'openai/o3-high' or an object with model config if you want to defined role properties like temperature/topP, or fallbacks like 'largeContextFallback', 'largeOutputFallback', 'errorFallback', 'strongModel'",
"oneOf": [
{
"type": "string",
"minLength": 1
},
{
"$ref": "./model-role-config.schema.json"
}
]
}
},
"properties": {
"$schema": {
"type": "string"
},
"localProvider": {
"description": "The local provider for the model pack",
"$ref": "./definitions/local-providers.schema.json"
},
"planner": {
"description": "This is the 'main' role that replies to prompts and makes plans.",
"$ref": "#/definitions/roleRef"
},
"coder": {
"description": "This role writes code to implement each step of the plan made by the 'planner' role during the planning stage.\n\nInstruction-following is important for this role as it needs to follow specific formatting rules.",
"$ref": "#/definitions/roleRef"
},
"architect": {
"description": "When auto-context is enabled, this role makes a high-level plan using the project map, then determines what context to provide for the 'planner' role.",
"$ref": "#/definitions/roleRef"
},
"summarizer": {
"description": "Summarizes conversations to stay under the limit set in the model's 'defaultMaxConvoTokens'.",
"$ref": "#/definitions/roleRef"
},
"builder": {
"description": "Builds the proposed changes described by the `planner` role into pending file updates.",
"$ref": "#/definitions/roleRef"
},
"wholeFileBuilder": {
"description": "Builds the proposed changes described by the `planner` role into pending file updates by writing the entire file. Used as a fallback if more targeted edits fail.\n\nThis role is optional. It falls back to the `builder` role if not set.",
"$ref": "#/definitions/roleRef"
},
"names": {
"description": "Gives automatically-generated names to plans and context.",
"$ref": "#/definitions/roleRef"
},
"commitMessages": {
"description": "Automatically generates commit messages for a set of pending updates.",
"$ref": "#/definitions/roleRef"
},
"autoContinue": {
"description": "Determines whether a plan is finished or should automatically continue based on the previous response.",
"$ref": "#/definitions/roleRef"
}
},
"required": [
"planner",
"summarizer",
"builder",
"names",
"commitMessages",
"autoContinue"
]
}
================================================
FILE: app/cli/schema/json-schemas/model-provider-config.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/model-provider-config.schema.json",
"title": "Model Provider Config",
"description": "Config for a custom model provider",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the model provider. This is used to reference the model provider in a model's 'providers' array."
},
"baseUrl": {
"type": "string",
"description": "The base URL for the model provider. This is used to construct the full URL for the model. For example, 'https://api.openai.com/v1' for OpenAI."
},
"skipAuth": {
"type": "boolean",
"default": false,
"description": "Whether to skip authentication for the model provider. If set to true, the model provider will not require an API key or other authentication. Mainly used for local models (ollama, etc.)."
},
"apiKeyEnvVar": {
"type": "string",
"description": "The environment variable that contains the API key for the model provider. This is used to authenticate the model provider. For example, 'OPENAI_API_KEY' for OpenAI."
},
"extraAuthVars": {
"type": "array",
"description": "Extra authentication variables for the model provider. In some cases these are used for authentication in place of an API key (e.g. AWS Bedrock). In other cases, they provide additional data on top of the API key (AZURE_API_VERSION, OPENAI_ORG_ID, etc.).",
"items": {
"type": "object",
"properties": {
"var": {
"type": "string",
"description": "The name of the environment variable that contains the authentication variable. For example, 'OPENAI_ORG_ID' for OpenAI."
},
"maybeJSONFilePath": {
"type": "boolean",
"description": "Whether the variable can be a JSON file path. If set to true, the value can be read from a JSON file path *OR* an environment variable. For example, 'true' for Google Vertex's GOOGLE_APPLICATION_CREDENTIALS."
},
"required": {
"type": "boolean",
"description": "Whether the variable is required. If set to true, the authentication variable is required. For example, 'true' for OpenAI."
},
"default": {
"type": "string"
}
},
"required": [
"var"
]
},
"minItems": 1
}
},
"required": [
"name",
"baseUrl"
],
"anyOf": [
{
"required": [
"apiKeyEnvVar"
]
},
{
"required": [
"extraAuthVars"
]
},
{
"required": [
"skipAuth"
],
"properties": {
"skipAuth": {
"const": true
}
}
}
],
"additionalProperties": false
}
================================================
FILE: app/cli/schema/json-schemas/model-role-config.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/model-role-config.schema.json",
"title": "Model Role Config",
"description": "Config for a defined model role within a model pack",
"type": "object",
"definitions": {
"roleRef": {
"oneOf": [
{
"type": "string",
"minLength": 1
},
{
"$ref": "#"
}
]
}
},
"properties": {
"modelId": {
"type": "string",
"description": "The 'modelId' of the built-in or custom model to use for this role."
},
"temperature": {
"type": "number",
"description": "The temperature to use for the model. This is used to control the randomness of the model's output. A sensible default is used by Plandex depending on the role, but you can override it here."
},
"topP": {
"type": "number",
"description": "The topP to use for the model. This is used to control the randomness of the model's output. A sensible default is used by Plandex depending on the role, but you can override it here."
},
"reservedOutputTokens": {
"type": "number"
},
"maxConvoTokens": {
"type": "number"
},
"largeContextFallback": {
"$ref": "#/definitions/roleRef"
},
"largeOutputFallback": {
"$ref": "#/definitions/roleRef"
},
"errorFallback": {
"$ref": "#/definitions/roleRef"
},
"strongModel": {
"$ref": "#/definitions/roleRef"
}
},
"required": [
"modelId"
],
"additionalProperties": false
}
================================================
FILE: app/cli/schema/json-schemas/models-input.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/models-input.schema.json",
"title": "Models Input",
"description": "Input schema for custom models, providers, and model packs",
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"models": {
"type": "array",
"description": "Custom models to import into Plandex. They can be referenced by 'modelId' in model packs.",
"items": {
"$ref": "./model-config.schema.json"
}
},
"providers": {
"type": "array",
"description": "Custom model providers to import into Plandex. They can be referenced in a custom model's 'providers' array.",
"items": {
"$ref": "./model-provider-config.schema.json"
}
},
"modelPacks": {
"type": "array",
"description": "Model packs to import into Plandex. These define which models to use for each of Plandex's roles.",
"items": {
"$ref": "./model-pack-config.schema.json"
}
}
},
"additionalProperties": false
}
================================================
FILE: app/cli/schema/json-schemas/plan-config.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://plandex.ai/schemas/plan-config.schema.json",
"title": "Plan Config",
"description": "Config for a plan",
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"autoMode": {
"$ref": "./definitions/auto-modes.schema.json"
}
},
"additionalProperties": false
}
================================================
FILE: app/cli/schema/schemas.go
================================================
package schema
import (
"bytes"
"embed"
"encoding/json"
"errors"
"fmt"
"path"
"strings"
shared "plandex-shared"
gojsonreference "github.com/xeipuuv/gojsonreference"
"github.com/xeipuuv/gojsonschema"
)
const scheme = "embed://"
type SchemaPath string
const (
SchemaPathInputConfig SchemaPath = "json-schemas/models-input.schema.json"
SchemaPathPlanConfig SchemaPath = "json-schemas/plan-config.schema.json"
SchemaPathModelPackInline SchemaPath = "json-schemas/model-pack-inline.schema.json"
)
//go:embed json-schemas/*.schema.json json-schemas/definitions/*.schema.json
var schemaFS embed.FS
type embeddedSchemaLoader struct {
source string
fs embed.FS
}
func ValidateModelsInputJSON(jsonData []byte) (shared.ClientModelsInput, error) {
return validateJSON[shared.ClientModelsInput](jsonData, SchemaPathInputConfig)
}
func ValidateModelPackInlineJSON(jsonData []byte) (shared.ClientModelPackSchemaRoles, error) {
return validateJSON[shared.ClientModelPackSchemaRoles](jsonData, SchemaPathModelPackInline)
}
func validateJSON[T any](jsonData []byte, schemaPath SchemaPath) (T, error) {
var zero T
// strip meta-keywords that break additionalProperties ──
var tmp interface{}
if err := json.Unmarshal(jsonData, &tmp); err != nil {
return zero, fmt.Errorf("invalid json: %w", err)
}
if obj, ok := tmp.(map[string]interface{}); ok {
delete(obj, "$schema") // ignore top-level $schema
var err error
jsonData, err = json.Marshal(obj)
if err != nil {
return zero, fmt.Errorf("error marshalling json: %w", err)
}
}
schemaLoader := newEmbeddedSchemaLoader(schemaPath)
documentLoader := gojsonschema.NewBytesLoader(jsonData)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return zero, err
}
if !result.Valid() {
var msgs []string
for _, d := range result.Errors() {
msgs = append(msgs, "• "+d.String())
}
return zero, errors.New(strings.Join(msgs, "\n"))
}
var v T
if err := json.Unmarshal(jsonData, &v); err != nil {
return zero, fmt.Errorf("unmarshal error: %w", err)
}
return v, nil
}
func newEmbeddedSchemaLoader(source SchemaPath) *embeddedSchemaLoader {
return &embeddedSchemaLoader{
source: string(source),
fs: schemaFS,
}
}
func (l *embeddedSchemaLoader) JsonSource() interface{} {
return l.source
}
func (l *embeddedSchemaLoader) LoadJSON() (interface{}, error) {
// remove both "./" and the scheme prefix
source := strings.TrimPrefix(l.source, "./")
source = strings.TrimPrefix(source, scheme)
// convert absolute Plandex URLs to our embed path
const webPrefix = "https://plandex.ai/schemas/"
if strings.HasPrefix(source, webPrefix) {
source = path.Join("json-schemas", strings.TrimPrefix(source, webPrefix))
}
if strings.HasSuffix(source, ".schema.json") {
schemaPath := source
// for schemas with relative path references, add the json-schemas prefix
if !strings.HasPrefix(schemaPath, "json-schemas/") {
schemaPath = path.Join("json-schemas", schemaPath)
}
data, err := l.fs.ReadFile(schemaPath)
if err != nil {
return nil, fmt.Errorf("error reading embedded schema %s: %v", schemaPath, err)
}
var v interface{}
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber() // use numbers instead of floats
if err := dec.Decode(&v); err != nil {
return nil, fmt.Errorf("error parsing embedded schema %s: %v", source, err)
}
return v, nil
}
var v interface{}
dec := json.NewDecoder(bytes.NewReader([]byte(source)))
dec.UseNumber() // use numbers instead of floats
if err := dec.Decode(&v); err != nil {
return nil, fmt.Errorf("error parsing schema JSON: %v", err)
}
return v, nil
}
func (l *embeddedSchemaLoader) JsonReference() (gojsonreference.JsonReference, error) {
return gojsonreference.NewJsonReference(scheme + l.source)
}
type embeddedLoaderFactory struct{}
func (embeddedLoaderFactory) New(source string) gojsonschema.JSONLoader {
source = strings.TrimPrefix(source, scheme)
return newEmbeddedSchemaLoader(SchemaPath(source))
}
func (l *embeddedSchemaLoader) LoaderFactory() gojsonschema.JSONLoaderFactory {
return embeddedLoaderFactory{}
}
================================================
FILE: app/cli/stream/stream.go
================================================
package stream
import (
"log"
"plandex-cli/api"
"plandex-cli/lib"
streamtui "plandex-cli/stream_tui"
"plandex-cli/term"
"plandex-cli/types"
"strings"
shared "plandex-shared"
)
var OnStreamPlan types.OnStreamPlan
func init() {
OnStreamPlan = func(params types.OnStreamPlanParams) {
if params.Err != nil {
if strings.Contains(params.Err.Error(), "missing heartbeats") || strings.Contains(strings.ToLower(params.Err.Error()), "eof") {
log.Println("Error in stream:", params.Err)
streamtui.Send(shared.StreamMessage{
Type: shared.StreamMessageError,
Error: &shared.ApiError{
Msg: "Stream error: " + params.Err.Error(),
},
})
// try to reconnect
term.StartSpinner("Reconnecting...")
apiErr := api.Client.ConnectPlan(lib.CurrentPlanId, lib.CurrentBranch, OnStreamPlan)
term.StopSpinner()
if apiErr != nil {
log.Println("Error reconnecting to stream:", apiErr)
}
}
return
}
if params.Msg.Type == shared.StreamMessageStart {
log.Println("Stream started")
return
}
// log.Println("Stream message:")
// log.Println(spew.Sdump(*params.Msg))
streamtui.Send(*params.Msg)
}
}
================================================
FILE: app/cli/stream_tui/debouncer.go
================================================
package streamtui
import (
"sync"
"time"
)
// UpdateDebouncer helps prevent visual glitches from rapid updates
type UpdateDebouncer struct {
mu sync.Mutex
lastUpdate time.Time
minInterval time.Duration
pending bool
}
func NewUpdateDebouncer(minInterval time.Duration) *UpdateDebouncer {
return &UpdateDebouncer{
minInterval: minInterval,
}
}
// ShouldUpdate returns true if enough time has passed since the last update
func (d *UpdateDebouncer) ShouldUpdate() bool {
d.mu.Lock()
defer d.mu.Unlock()
now := time.Now()
if now.Sub(d.lastUpdate) < d.minInterval {
d.pending = true
return false
}
d.lastUpdate = now
d.pending = false
return true
}
================================================
FILE: app/cli/stream_tui/model.go
================================================
package streamtui
import (
"context"
"log"
"sync"
"time"
shared "plandex-shared"
bubbleKey "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
MissingFileLoadLabel = "Load the file into context"
MissingFileSkipLabel = "Skip generating this file"
MissingFileOverwriteLabel = "Allow Plandex to overwrite this file"
)
var promptChoices = []shared.RespondMissingFileChoice{
shared.RespondMissingFileChoiceLoad,
shared.RespondMissingFileChoiceSkip,
shared.RespondMissingFileChoiceOverwrite,
}
var missingFileSelectOpts = []string{
MissingFileLoadLabel,
MissingFileSkipLabel,
MissingFileOverwriteLabel,
}
var stateMu sync.RWMutex
type streamUIModel struct {
buildOnly bool
canSendToBg bool
keymap keymap
reply string
mainDisplay string
mainViewport viewport.Model
processing bool
starting bool
spinner spinner.Model
buildSpinner spinner.Model
sharedTicker *time.Ticker
building bool
tokensByPath map[string]int
finishedByPath map[string]bool
removedByPath map[string]bool
ready bool
width int
height int
atScrollBottom bool
promptingMissingFile bool
missingFilePath string
missingFileSelectedIdx int
promptedMissingFile bool
autoLoadedMissingFile bool
missingFileContent string
missingFileTokens int
prompt string
stopped bool
background bool
finished bool
err error
apiErr *shared.ApiError
updateDebouncer *UpdateDebouncer
autoLoadContextCancelFn context.CancelFunc
buildViewCollapsed bool
userToggledBuild bool
}
type keymap = struct {
stop,
scrollUp,
scrollDown,
pageUp,
pageDown,
start,
end,
up,
down,
quit,
background,
enter bubbleKey.Binding
}
func (m streamUIModel) Init() tea.Cmd {
log.Println("Model Init start")
m.mainViewport.MouseWheelEnabled = true
return tea.Batch(
m.Tick(),
m.pollBuildStatus(),
)
}
type buildStatusPollMsg time.Time
func (m streamUIModel) pollBuildStatus() tea.Cmd {
return tea.Every(5*time.Second, func(t time.Time) tea.Msg {
return buildStatusPollMsg(t)
})
}
func initialModel(prestartReply, prompt string, buildOnly bool, canSendToBg bool) *streamUIModel {
sharedTicker := time.NewTicker(100 * time.Millisecond)
s := spinner.New()
s.Spinner = spinner.Points
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
buildSpinner := spinner.New()
buildSpinner.Spinner = spinner.MiniDot
initialState := streamUIModel{
buildOnly: buildOnly,
canSendToBg: canSendToBg,
buildViewCollapsed: false,
prompt: prompt,
reply: prestartReply,
keymap: keymap{
quit: bubbleKey.NewBinding(
bubbleKey.WithKeys("ctrl+c"),
bubbleKey.WithHelp("ctrl+c", "quit"),
),
background: bubbleKey.NewBinding(
bubbleKey.WithKeys("b"),
bubbleKey.WithHelp("b", "background"),
),
stop: bubbleKey.NewBinding(
bubbleKey.WithKeys("s"),
bubbleKey.WithHelp("s", "stop"),
),
scrollDown: bubbleKey.NewBinding(
bubbleKey.WithKeys("j"),
bubbleKey.WithHelp("j", "scroll down"),
),
scrollUp: bubbleKey.NewBinding(
bubbleKey.WithKeys("k"),
bubbleKey.WithHelp("k", "scroll up"),
),
pageDown: bubbleKey.NewBinding(
bubbleKey.WithKeys("d", "pageDown"),
bubbleKey.WithHelp("d", "page down"),
),
pageUp: bubbleKey.NewBinding(
bubbleKey.WithKeys("u", "pageUp"),
bubbleKey.WithHelp("u", "page up"),
),
up: bubbleKey.NewBinding(
bubbleKey.WithKeys("up"),
bubbleKey.WithHelp("up", "prev"),
),
down: bubbleKey.NewBinding(
bubbleKey.WithKeys("down"),
bubbleKey.WithHelp("down", "next"),
),
enter: bubbleKey.NewBinding(
bubbleKey.WithKeys("enter"),
bubbleKey.WithHelp("enter", "select"),
),
start: bubbleKey.NewBinding(
bubbleKey.WithKeys("g", "home"),
bubbleKey.WithHelp("g", "start"),
),
end: bubbleKey.NewBinding(
bubbleKey.WithKeys("G", "end"),
bubbleKey.WithHelp("G", "end"),
),
},
tokensByPath: make(map[string]int),
finishedByPath: make(map[string]bool),
removedByPath: make(map[string]bool),
spinner: s,
buildSpinner: buildSpinner,
sharedTicker: sharedTicker,
atScrollBottom: true,
starting: true,
updateDebouncer: NewUpdateDebouncer(8 * time.Millisecond),
}
return &initialState
}
func (m streamUIModel) Tick() tea.Cmd {
return func() tea.Msg {
<-m.sharedTicker.C
return spinner.TickMsg{}
}
}
func (m *streamUIModel) cleanup() {
log.Println("Cleaning up stream UI model")
m.updateState(func() {
m.sharedTicker.Stop()
})
}
func (m *streamUIModel) readState() streamUIModel {
stateMu.RLock()
defer stateMu.RUnlock()
return *m
}
func (m *streamUIModel) updateState(updateFn func()) {
stateMu.Lock()
defer stateMu.Unlock()
updateFn()
}
================================================
FILE: app/cli/stream_tui/run.go
================================================
package streamtui
import (
"fmt"
"log"
"os"
"plandex-cli/term"
"sync"
shared "plandex-shared"
tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
)
var ui *tea.Program
var mu sync.Mutex
var wg sync.WaitGroup
var prestartReply string
var prestartErr *shared.ApiError
var prestartAbort bool
func StartStreamUI(prompt string, buildOnly, canSendToBg bool) error {
if prestartErr != nil {
log.Println("stream UI - prestart error: ", prestartErr)
term.HandleApiError(prestartErr)
}
if prestartAbort {
fmt.Println("🛑 Stopped early")
os.Exit(0)
}
log.Println("Starting stream UI")
initial := initialModel(prestartReply, prompt, buildOnly, canSendToBg)
mu.Lock()
ui = tea.NewProgram(initial, tea.WithAltScreen())
mu.Unlock()
log.Println("Running bubbletea program")
wg.Add(1)
m, err := ui.Run()
log.Println("Bubbletea program finished")
wg.Done()
log.Println("Stream UI finished")
if err != nil {
return fmt.Errorf("error running stream UI: %v", err)
}
var mod *streamUIModel
c, ok := m.(*streamUIModel)
if ok {
mod = c
} else {
c := m.(streamUIModel)
mod = &c
}
fmt.Println()
if !mod.buildOnly {
fmt.Println(mod.mainDisplay)
}
if len(mod.finishedByPath) > 0 || len(mod.tokensByPath) > 0 {
fmt.Println(mod.renderStaticBuild())
}
if mod.err != nil {
log.Println("stream UI - error: ", mod.err)
fmt.Println()
term.OutputErrorAndExit(mod.err.Error())
}
if mod.apiErr != nil {
log.Println("stream UI - api error: ", mod.apiErr)
fmt.Println()
term.HandleApiError(mod.apiErr)
}
if mod.stopped {
fmt.Println()
color.New(color.BgBlack, color.Bold, color.FgHiRed).Println(" 🛑 Stopped early ")
fmt.Println()
term.PrintCmds("", "log", "rewind", "tell")
os.Exit(0)
} else if mod.background {
fmt.Println()
color.New(color.BgBlack, color.Bold, color.FgHiGreen).Println(" ✅ Plan is active in the background ")
fmt.Println()
term.PrintCmds("", "ps", "connect", "stop")
os.Exit(0)
}
if os.Getenv("PLANDEX_REPL") != "" && os.Getenv("PLANDEX_REPL_OUTPUT_FILE") != "" {
// write output to file
err := os.WriteFile(os.Getenv("PLANDEX_REPL_OUTPUT_FILE"), []byte(mod.reply), 0644)
if err != nil {
log.Println("stream UI - error writing output to repl temp file: ", err)
}
}
return nil
}
func Quit() {
if ui == nil {
log.Println("stream UI is nil, can't quit")
return
}
mu.Lock()
if ui != nil {
ui.Quit()
}
mu.Unlock()
wg.Wait() // Wait for the UI to fully terminate
}
func Send(msg shared.StreamMessage) {
if ui == nil {
log.Println("stream ui is nil")
if msg.Type == shared.StreamMessageError {
prestartErr = msg.Error
} else if msg.Type == shared.StreamMessageAborted {
} else if msg.Type == shared.StreamMessageReply {
prestartReply += msg.ReplyChunk
}
return
}
mu.Lock()
defer mu.Unlock()
ui.Send(msg)
}
func ToggleVisibility(hide bool) {
if ui == nil {
return
}
mu.Lock()
defer mu.Unlock()
if hide {
ui.Send(tea.ExitAltScreen())
} else {
ui.Send(tea.EnterAltScreen())
}
}
================================================
FILE: app/cli/stream_tui/update.go
================================================
package streamtui
import (
"context"
"fmt"
"log"
"os"
"plandex-cli/api"
"plandex-cli/lib"
"plandex-cli/term"
"strings"
"time"
shared "plandex-shared"
bubbleKey "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/davecgh/go-spew/spew"
"github.com/fatih/color"
)
func (m streamUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// log.Println("Stream TUI - Update received message:", spew.Sdump(msg))
switch msg := msg.(type) {
case spinner.TickMsg:
state := m.readState()
if state.processing || state.starting {
m.updateState(func() {
spinnerModel, _ := m.spinner.Update(msg)
m.spinner = spinnerModel
})
}
if state.building {
m.updateState(func() {
buildSpinnerModel, _ := m.buildSpinner.Update(msg)
m.buildSpinner = buildSpinnerModel
})
}
return m, m.Tick()
case tea.WindowSizeMsg:
m.windowResized(msg.Width, msg.Height)
case shared.StreamMessage:
return m.streamUpdate(&msg, false)
case contextLoadDoneMsg:
if msg.err != nil {
log.Println("failed to auto load context files:", msg.err)
m.updateState(func() {
m.err = msg.err
m.processing = false
})
return m, tea.Quit
}
// We have the loaded content in msg.text
m.updateState(func() {
if msg.text != "" {
m.reply += "\n\n" + msg.text + "\n\n"
}
// and keep processing
m.processing = true
})
m.updateReplyDisplay()
return m, m.Tick()
case delayFileRestartMsg:
m.updateState(func() {
m.finishedByPath[msg.path] = false
})
// Scroll wheel doesn't seem to work--not sure why
// case tea.MouseMsg:
// if !m.promptingMissingFile {
// if msg.Type == tea.MouseWheelUp {
// m.mainViewport.LineUp(3)
// } else if msg.Type == tea.MouseWheelDown {
// m.mainViewport.LineDown(3)
// }
// }
case tea.KeyMsg:
switch {
case bubbleKey.Matches(msg, m.keymap.stop) || bubbleKey.Matches(msg, m.keymap.quit):
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
apiErr := api.Client.StopPlan(ctx, lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
log.Println("stop plan api error:", apiErr)
m.updateState(func() {
m.apiErr = apiErr
})
}
m.updateState(func() {
m.stopped = true
})
return m, tea.Quit
case bubbleKey.Matches(msg, m.keymap.background):
state := m.readState()
if state.canSendToBg {
m.updateState(func() {
m.background = true
})
return m, tea.Quit
}
case bubbleKey.Matches(msg, m.keymap.scrollDown) && !m.promptingMissingFile:
m.scrollDown()
case bubbleKey.Matches(msg, m.keymap.scrollUp) && !m.promptingMissingFile:
m.scrollUp()
case bubbleKey.Matches(msg, m.keymap.pageDown) && !m.promptingMissingFile:
m.pageDown()
case bubbleKey.Matches(msg, m.keymap.pageUp) && !m.promptingMissingFile:
m.pageUp()
case bubbleKey.Matches(msg, m.keymap.up) && m.building:
m.up()
case bubbleKey.Matches(msg, m.keymap.down) && m.building:
m.down()
case bubbleKey.Matches(msg, m.keymap.start) && !m.promptingMissingFile:
m.scrollStart()
case bubbleKey.Matches(msg, m.keymap.end) && !m.promptingMissingFile:
m.scrollEnd()
case m.promptingMissingFile && bubbleKey.Matches(msg, m.keymap.enter):
return m.selectedMissingFileOpt()
default:
m.resolveEscapeSequence(msg.String())
}
case buildStatusPollMsg:
state := m.readState()
numPaths := len(m.tokensByPath)
numFinished := 0
for _, isBuilt := range m.finishedByPath {
if isBuilt {
numFinished++
}
}
if !state.finished && !state.stopped && !state.background && numPaths > 0 && numPaths != numFinished {
status, apiErr := api.Client.GetBuildStatus(lib.CurrentPlanId, lib.CurrentBranch)
if apiErr != nil {
return m, m.pollBuildStatus()
}
m.updateState(func() {
for path, isBuilt := range status.BuiltFiles {
isBuilding := status.IsBuildingByPath[path]
if isBuilt && !isBuilding {
m.finishedByPath[path] = true
}
}
})
}
return m, m.pollBuildStatus()
}
return m, nil
}
func (m *streamUIModel) windowResized(w, h int) {
m.updateState(func() {
m.width = w
m.height = h
})
state := m.readState()
_, viewportHeight := state.getViewportDimensions()
if state.ready {
m.updateViewportDimensions()
} else {
m.updateState(func() {
m.mainViewport = viewport.New(w, viewportHeight)
m.mainViewport.Style = lipgloss.NewStyle().Padding(0, 1, 0, 1)
})
m.updateReplyDisplay()
m.updateState(func() {
m.ready = true
})
}
}
func (m *streamUIModel) updateReplyDisplay() {
state := m.readState()
if state.buildOnly {
return
}
s := ""
if state.prompt != "" {
promptTxt := term.GetPlain(state.prompt)
s += color.New(color.BgGreen, color.Bold, color.FgHiWhite).Sprintf(" 💬 User prompt 👇 ")
s += "\n\n" + strings.TrimSpace(promptTxt) + "\n"
}
if state.reply != "" {
replyMd, _ := term.GetMarkdown(state.reply)
s += "\n" + color.New(color.BgBlue, color.Bold, color.FgHiWhite).Sprintf(" 🤖 Plandex reply 👇 ")
s += "\n\n" + strings.TrimSpace(replyMd)
} else {
s += "\n"
}
m.updateState(func() {
m.mainDisplay = s
m.mainViewport.SetContent(s)
})
m.updateViewportDimensions()
if state.atScrollBottom {
m.updateState(func() {
m.mainViewport.GotoBottom()
})
}
}
func (m *streamUIModel) updateViewportDimensions() {
state := m.readState()
w, h := state.getViewportDimensions()
m.updateState(func() {
m.mainViewport.Width = w
m.mainViewport.Height = h
})
}
func (m *streamUIModel) getViewportDimensions() (int, int) {
w := m.width
h := m.height
helpHeight := lipgloss.Height(m.renderHelp())
var buildHeight int
if m.building {
if m.buildViewCollapsed {
buildHeight = 3
} else {
buildHeight = len(m.getRows(false))
}
}
var processingHeight int
if m.starting || m.processing {
processingHeight = lipgloss.Height(m.renderProcessing())
}
maxViewportHeight := h - (helpHeight + processingHeight + buildHeight)
if maxViewportHeight < 0 {
maxViewportHeight = 0
}
viewportHeight := min(maxViewportHeight, lipgloss.Height(m.mainDisplay))
viewportWidth := w
return viewportWidth, viewportHeight
}
func (m streamUIModel) replyScrollable() bool {
return m.mainViewport.TotalLineCount() > m.mainViewport.VisibleLineCount()
}
func (m *streamUIModel) scrollDown() {
state := m.readState()
if state.replyScrollable() {
m.updateState(func() {
m.mainViewport.LineDown(1)
})
}
state = m.readState()
m.updateState(func() {
m.atScrollBottom = !state.replyScrollable() || state.mainViewport.AtBottom()
})
}
func (m *streamUIModel) scrollUp() {
state := m.readState()
if state.replyScrollable() {
m.updateState(func() {
m.mainViewport.LineUp(1)
m.atScrollBottom = false
})
}
}
func (m *streamUIModel) pageDown() {
state := m.readState()
if state.replyScrollable() {
m.updateState(func() {
m.mainViewport.ViewDown()
})
}
state = m.readState()
m.updateState(func() {
m.atScrollBottom = !state.replyScrollable() || state.mainViewport.AtBottom()
})
}
func (m *streamUIModel) pageUp() {
state := m.readState()
if state.replyScrollable() {
m.updateState(func() {
m.mainViewport.ViewUp()
m.atScrollBottom = false
})
}
}
func (m *streamUIModel) scrollStart() {
state := m.readState()
if state.replyScrollable() {
m.updateState(func() {
m.mainViewport.GotoTop()
m.atScrollBottom = false
})
}
}
func (m *streamUIModel) scrollEnd() {
state := m.readState()
if state.replyScrollable() {
m.updateState(func() {
m.mainViewport.GotoBottom()
m.atScrollBottom = true
})
}
}
func (m *streamUIModel) streamUpdate(msg *shared.StreamMessage, deferUIUpdate bool) (tea.Model, tea.Cmd) {
switch msg.Type {
case shared.StreamMessageMulti:
cmds := []tea.Cmd{}
for _, subMsg := range msg.StreamMessages {
teaModel, cmd := m.streamUpdate(&subMsg, true)
m = teaModel.(*streamUIModel)
if cmd != nil {
cmds = append(cmds, cmd)
}
}
m.updateReplyDisplay()
m.updateViewportDimensions()
return m, tea.Batch(cmds...)
case shared.StreamMessageConnectActive:
if msg.InitPrompt != "" {
m.updateState(func() {
m.prompt = msg.InitPrompt
})
}
if msg.InitBuildOnly {
m.updateState(func() {
m.buildOnly = true
})
}
if len(msg.InitReplies) > 0 {
m.updateState(func() {
m.reply = strings.Join(msg.InitReplies, "\n\n👇\n\n")
})
}
m.updateReplyDisplay()
return m.checkMissingFile(msg)
case shared.StreamMessagePromptMissingFile:
return m.checkMissingFile(msg)
case shared.StreamMessageReply:
// ignore empty reply messages
if msg.ReplyChunk == "" {
return m, nil
}
state := m.readState()
if state.starting {
m.updateState(func() {
m.starting = false
})
}
if state.processing {
log.Println("Non-empty message reply, setting processing to false")
m.updateState(func() {
m.processing = false
if state.promptedMissingFile || state.autoLoadedMissingFile {
log.Println("Prompted missing file or auto loaded missing file, resetting (and skipping 👇 marker)")
m.promptedMissingFile = false
m.autoLoadedMissingFile = false
} else {
log.Println("Not prompted missing file or auto loaded missing file, adding 👇 marker")
m.reply += "\n\n👇\n\n"
}
})
}
m.updateState(func() {
m.reply += msg.ReplyChunk
})
if !deferUIUpdate {
m.updateReplyDisplay()
}
case shared.StreamMessageBuildInfo:
state := m.readState()
if state.starting {
m.updateState(func() {
m.starting = false
})
}
m.updateState(func() {
m.building = true
})
wasFinished := state.finishedByPath[msg.BuildInfo.Path]
nowFinished := msg.BuildInfo.Finished
m.updateState(func() {
if msg.BuildInfo.Removed {
m.removedByPath[msg.BuildInfo.Path] = true
} else {
m.removedByPath[msg.BuildInfo.Path] = false
}
})
if msg.BuildInfo.Finished {
m.updateState(func() {
m.tokensByPath[msg.BuildInfo.Path] = 0
m.finishedByPath[msg.BuildInfo.Path] = true
})
} else {
if wasFinished && !nowFinished {
// delay for a second before marking not finished again (so check flashes green prior to restarting build)
log.Println("Stream message build info - delaying for 1 second before marking not finished again")
return m, startDelay(msg.BuildInfo.Path, time.Second*1)
} else {
m.updateState(func() {
m.finishedByPath[msg.BuildInfo.Path] = false
})
}
m.updateState(func() {
m.tokensByPath[msg.BuildInfo.Path] += msg.BuildInfo.NumTokens
})
}
// Auto-collapse if build info takes up too much space
state = m.readState()
if !state.userToggledBuild && state.building {
rows := len(m.getRows(false))
m.updateState(func() {
m.buildViewCollapsed = rows > 3
})
}
if !deferUIUpdate {
m.updateViewportDimensions()
}
return m, m.Tick()
case shared.StreamMessageDescribing:
log.Println("Message describing, setting processing to true")
m.updateState(func() {
m.processing = true
})
return m, m.Tick()
// Instead of blocking here, we'll spawn a command
case shared.StreamMessageLoadContext:
m.updateState(func() {
m.processing = true
})
return m, tea.Batch(
loadContextCmd(msg.LoadContextFiles),
tea.Tick(time.Second/10, func(t time.Time) tea.Msg {
return spinner.TickMsg{}
}),
)
case shared.StreamMessageError:
log.Println("Stream message error:", spew.Sdump(msg))
state := m.readState()
if state.autoLoadContextCancelFn != nil {
state.autoLoadContextCancelFn()
}
m.updateState(func() {
m.apiErr = msg.Error
})
return m, tea.Quit
case shared.StreamMessageFinished:
m.updateState(func() {
m.finished = true
})
return m, tea.Quit
case shared.StreamMessageAborted:
m.updateState(func() {
m.stopped = true
})
return m, tea.Quit
case shared.StreamMessageRepliesFinished:
log.Println("Replies finished, setting processing to false")
state := m.readState()
m.updateState(func() {
m.processing = false
})
if state.building {
return m, m.Tick()
}
}
return m, nil
}
type delayFileRestartMsg struct {
path string
}
func startDelay(path string, delay time.Duration) tea.Cmd {
return func() tea.Msg {
time.Sleep(delay)
return delayFileRestartMsg{path: path}
}
}
var escReceivedAt time.Time
var escSeq string
func (m *streamUIModel) resolveEscapeSequence(val string) {
if val == "esc" || val == "alt+[" {
escReceivedAt = time.Now()
go func() {
time.Sleep(51 * time.Millisecond)
escReceivedAt = time.Time{}
escSeq = ""
}()
}
if !escReceivedAt.IsZero() {
elapsed := time.Since(escReceivedAt)
if elapsed < 50*time.Millisecond {
escSeq += val
if escSeq == "esc[A" || escSeq == "alt+[A" {
m.up()
escReceivedAt = time.Time{}
escSeq = ""
} else if escSeq == "esc[B" || escSeq == "alt+[B" {
m.down()
escReceivedAt = time.Time{}
escSeq = ""
}
}
}
}
func (m *streamUIModel) up() {
state := m.readState()
if state.promptingMissingFile {
m.updateState(func() {
m.missingFileSelectedIdx = max(m.missingFileSelectedIdx-1, 0)
})
} else {
m.updateState(func() {
m.buildViewCollapsed = false
m.userToggledBuild = true
})
}
}
func (m *streamUIModel) down() {
state := m.readState()
if state.promptingMissingFile {
m.updateState(func() {
m.missingFileSelectedIdx = min(m.missingFileSelectedIdx+1, len(missingFileSelectOpts)-1)
})
} else {
m.updateState(func() {
m.buildViewCollapsed = true
m.userToggledBuild = true
})
}
}
func (m *streamUIModel) selectedMissingFileOpt() (tea.Model, tea.Cmd) {
state := m.readState()
choice := promptChoices[state.missingFileSelectedIdx]
if choice == "" {
return m, nil
}
apiErr := api.Client.RespondMissingFile(lib.CurrentPlanId, lib.CurrentBranch, shared.RespondMissingFileRequest{
Choice: choice,
FilePath: m.missingFilePath,
Body: m.missingFileContent,
})
if apiErr != nil {
log.Println("missing file prompt api error:", apiErr)
m.updateState(func() {
m.apiErr = apiErr
})
return m, nil
}
if choice == shared.RespondMissingFileChoiceSkip {
replyLines := strings.Split(state.reply, "\n")
m.updateState(func() {
m.reply = strings.Join(replyLines[:len(replyLines)-3], "\n")
})
m.updateReplyDisplay()
}
m.updateState(func() {
m.promptingMissingFile = false
m.missingFilePath = ""
m.missingFileSelectedIdx = 0
m.missingFileContent = ""
m.missingFileTokens = 0
m.promptedMissingFile = true
m.processing = true
})
return m, func() tea.Msg {
<-m.sharedTicker.C
return spinner.TickMsg{}
}
}
func (m *streamUIModel) checkMissingFile(msg *shared.StreamMessage) (tea.Model, tea.Cmd) {
if msg.MissingFilePath != "" {
log.Println("checkMissingFile - received missing file message | path:", msg.MissingFilePath)
if msg.MissingFileAutoContext {
log.Println("checkMissingFile - received missing file message | auto context")
m.updateState(func() {
m.processing = true
m.autoLoadedMissingFile = true
})
return m, tea.Batch(
func() tea.Msg {
<-m.sharedTicker.C
return spinner.TickMsg{}
},
func() tea.Msg {
bytes, err := os.ReadFile(msg.MissingFilePath)
if err != nil {
log.Println("failed to read file:", err)
m.err = fmt.Errorf("failed to read file: %w", err)
return tea.Quit
}
content := string(shared.NormalizeEOL(bytes))
log.Println("checkMissingFile - calling RespondMissingFile")
apiErr := api.Client.RespondMissingFile(lib.CurrentPlanId, lib.CurrentBranch, shared.RespondMissingFileRequest{
Choice: shared.RespondMissingFileChoiceLoad,
FilePath: msg.MissingFilePath,
Body: content,
})
if apiErr != nil {
log.Println("missing file prompt api error:", apiErr)
m.updateState(func() {
m.apiErr = apiErr
})
return tea.Quit
}
log.Println("checkMissingFile - RespondMissingFile success")
return nil
},
)
}
m.updateState(func() {
m.promptingMissingFile = true
m.missingFilePath = msg.MissingFilePath
})
bytes, err := os.ReadFile(m.missingFilePath)
if err != nil {
log.Println("failed to read file:", err)
m.updateState(func() {
m.err = fmt.Errorf("failed to read file: %w", err)
})
return m, nil
}
missingFileContent := string(bytes)
m.updateState(func() {
m.missingFileContent = missingFileContent
})
numTokens := shared.GetNumTokensEstimate(missingFileContent)
m.updateState(func() {
m.missingFileTokens = numTokens
})
}
return m, nil
}
// contextLoadDoneMsg is sent when the long-running AutoLoadContextFiles completes
type contextLoadDoneMsg struct {
text string
err error
}
func loadContextCmd(loadContextFiles []string) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Run the long operation directly
text, err := lib.AutoLoadContextFiles(ctx, loadContextFiles)
// Return the result as a message
return contextLoadDoneMsg{
text: text,
err: err,
}
}
}
================================================
FILE: app/cli/stream_tui/view.go
================================================
package streamtui
import (
"fmt"
"sort"
"strings"
"plandex-cli/term"
"github.com/charmbracelet/lipgloss"
"github.com/fatih/color"
)
var borderColor = lipgloss.Color("#444")
var helpTextColor = lipgloss.Color("#ddd")
func (m streamUIModel) View() string {
if m.promptingMissingFile {
return m.renderMissingFilePrompt()
}
views := []string{}
if !m.buildOnly {
views = append(views, m.renderMainView())
}
if m.processing || m.starting {
views = append(views, m.renderProcessing())
}
if m.building {
views = append(views, m.renderBuild())
}
views = append(views, m.renderHelp())
return lipgloss.JoinVertical(lipgloss.Left, views...)
}
func (m streamUIModel) renderMainView() string {
return m.mainViewport.View()
}
func (m streamUIModel) renderHelp() string {
style := lipgloss.NewStyle().Width(m.width).Foreground(lipgloss.Color(helpTextColor)).BorderStyle(lipgloss.NormalBorder()).BorderTop(true).BorderForeground(lipgloss.Color(borderColor))
if m.buildOnly {
s := " (s)top"
if m.canSendToBg {
s += " • (b)ackground"
}
return style.Render(s)
} else {
s := " (s)top"
if m.canSendToBg {
s += " • (b)ackground"
}
s += " • (j/k) scroll • (d/u) page • (g/G) start/end"
return style.Render(s)
}
}
func (m streamUIModel) renderProcessing() string {
if m.starting || m.processing {
return "\n " + m.spinner.View()
} else {
return ""
}
}
func (m streamUIModel) renderBuild() string {
return m.doRenderBuild(false)
}
func (m streamUIModel) renderStaticBuild() string {
return m.doRenderBuild(true)
}
func (m streamUIModel) doRenderBuild(outputStatic bool) string {
if !m.building && !outputStatic {
return ""
}
if outputStatic && len(m.finishedByPath) == 0 && len(m.tokensByPath) == 0 {
return ""
}
var style lipgloss.Style
if m.buildOnly {
style = lipgloss.NewStyle().Width(m.width)
} else {
style = lipgloss.NewStyle().Width(m.width).BorderStyle(lipgloss.NormalBorder()).BorderTop(true).BorderForeground(lipgloss.Color(borderColor))
}
if !outputStatic && m.buildViewCollapsed {
// Render collapsed view
inProgress := 0
total := len(m.tokensByPath)
for path := range m.tokensByPath {
if path == "_apply.sh" {
total--
continue
}
if !m.finishedByPath[path] {
inProgress++
}
}
_, hasApplyScript := m.tokensByPath["_apply.sh"]
applyScriptFinished := m.finishedByPath["_apply.sh"]
lbl := "file"
if total > 1 {
lbl = "files"
}
var summary string
if total > 0 {
summary = fmt.Sprintf(" 📄 %d %s", total, lbl)
}
if inProgress > 0 {
summary += fmt.Sprintf(" • 📝 editing %d %s", inProgress, m.buildSpinner.View())
}
if hasApplyScript {
if total > 0 {
summary += " •"
}
if applyScriptFinished {
summary += " 🚀 wrote commands"
} else {
summary += fmt.Sprintf(" 🚀 editing commands %s", m.buildSpinner.View())
}
}
head := m.getBuildHeader(outputStatic)
return style.Render(lipgloss.JoinVertical(lipgloss.Left, head, summary))
}
resRows := m.getRows(outputStatic)
res := style.Render(strings.Join(resRows, "\n"))
return res
}
func (m streamUIModel) didBuild() bool {
return !(m.stopped || m.err != nil || m.apiErr != nil)
}
func (m streamUIModel) getBuildHeader(static bool) string {
lbl := "Building plan "
bgColor := color.BgGreen
if static {
if !m.didBuild() {
lbl = "Build incomplete "
bgColor = color.BgRed
} else {
lbl = "Built plan "
}
}
head := color.New(bgColor, color.FgHiWhite, color.Bold).Sprint(" 🏗 ") + color.New(bgColor, color.FgHiWhite).Sprint(lbl)
// Add collapse/expand hint
var hint string
if !static {
hint = "(↓) collapse"
if m.buildViewCollapsed {
hint = "(↑) expand"
}
}
padding := m.width - lipgloss.Width(head) - lipgloss.Width(hint) - 1 // 1 for space
if padding > 0 {
head += strings.Repeat(" ", padding) + hint
}
return head
}
func (m streamUIModel) getRows(static bool) []string {
built := m.didBuild() && static
head := m.getBuildHeader(static)
// Gather file paths, _apply.sh last
filePaths := make([]string, 0, len(m.tokensByPath))
for filePath := range m.tokensByPath {
if filePath == "_apply.sh" {
continue
}
filePaths = append(filePaths, filePath)
}
sort.Strings(filePaths)
if _, ok := m.tokensByPath["_apply.sh"]; ok {
filePaths = append(filePaths, "_apply.sh")
}
var rows [][]string
lineWidth := 0
lineNum := -1
rowIdx := 0
for _, filePath := range filePaths {
tokens := m.tokensByPath[filePath]
finished := m.finished || m.finishedByPath[filePath] || built
removed := m.removedByPath[filePath]
// Basic block label
icon := "📄"
label := filePath
if filePath == "_apply.sh" {
icon = "🚀"
label = "commands"
}
block := fmt.Sprintf("%s %s", icon, label)
// Mark removed/finished/tokens
switch {
case removed:
block += " ❌"
case finished:
block += " ✅"
case tokens > 0:
block += fmt.Sprintf(" %d 🪙", tokens)
default:
block += " " + m.buildSpinner.View()
}
// Truncate if needed
blockWidth := lipgloss.Width(block)
if blockWidth > m.width {
maxWidth := m.width - lipgloss.Width("⋯")
if maxWidth < 4 {
block = string([]rune(block)[0:1]) + "⋯"
} else {
half := maxWidth / 2
runes := []rune(block)
block = string(runes[:half]) + "⋯" + string(runes[len(runes)-half:])
}
}
// Build the "prefix + block" text tentatively:
prefix := ""
if rowIdx > 0 {
prefix = " | "
}
candidate := prefix + block
candidateWidth := lipgloss.Width(candidate)
// Check if we have no row or it won't fit with the prefix
if lineNum == -1 || lineWidth+candidateWidth > m.width {
// Start a new row
rows = append(rows, []string{})
lineNum++
rowIdx = 0
lineWidth = 0
// In a new row, there's no prefix
candidate = block
candidateWidth = lipgloss.Width(candidate)
}
rows[lineNum] = append(rows[lineNum], candidate)
lineWidth += candidateWidth
rowIdx++
}
// If empty row left at the end, strip it
if len(rows) > 0 && len(rows[len(rows)-1]) == 0 {
rows = rows[:len(rows)-1]
}
// Final output lines
resRows := make([]string, len(rows)+1)
resRows[0] = head
for i, row := range rows {
resRows[i+1] = lipgloss.JoinHorizontal(lipgloss.Left, row...)
}
return resRows
}
func (m streamUIModel) renderMissingFilePrompt() string {
style := lipgloss.NewStyle().Padding(1).BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(borderColor)).Width(m.width - 2).Height(m.height - 2)
prompt := "📄 " + color.New(color.Bold, term.ColorHiYellow).Sprint(m.missingFilePath) + " isn't in context."
prompt += "\n\n"
desc := "This file exists in your project, but isn't loaded into context. Unless you load it into context or skip generating it, Plandex will fully overwrite the existing file rather than applying updates."
words := strings.Split(desc, " ")
for i, word := range words {
words[i] = color.New(color.FgWhite).Sprint(word)
}
prompt += strings.Join(words, " ")
prompt += "\n\n" + color.New(term.ColorHiMagenta, color.Bold).Sprintln("🧐 What do you want to do?")
for i, opt := range missingFileSelectOpts {
if i == m.missingFileSelectedIdx {
prompt += color.New(term.ColorHiCyan, color.Bold).Sprint(" > " + opt)
} else {
prompt += " " + opt
}
if opt == MissingFileLoadLabel {
prompt += fmt.Sprintf(" | %d 🪙", m.missingFileTokens)
}
prompt += "\n"
}
return style.Render(prompt)
}
================================================
FILE: app/cli/term/color.go
================================================
package term
import (
"github.com/fatih/color"
"github.com/muesli/termenv"
)
var IsDarkBg = termenv.HasDarkBackground()
var ColorHiGreen color.Attribute
var ColorHiMagenta color.Attribute
var ColorHiRed color.Attribute
var ColorHiYellow color.Attribute
var ColorHiCyan color.Attribute
var ColorHiBlue color.Attribute
func init() {
if IsDarkBg {
ColorHiGreen = color.FgHiGreen
ColorHiMagenta = color.FgHiMagenta
ColorHiRed = color.FgHiRed
ColorHiYellow = color.FgHiYellow
ColorHiCyan = color.FgHiCyan
ColorHiBlue = color.FgHiBlue
} else {
ColorHiGreen = color.FgGreen
ColorHiMagenta = color.FgMagenta
ColorHiRed = color.FgRed
ColorHiYellow = color.FgYellow
ColorHiCyan = color.FgCyan
ColorHiBlue = color.FgBlue
}
}
================================================
FILE: app/cli/term/errors.go
================================================
package term
import (
"encoding/json"
"fmt"
"os"
"strings"
shared "plandex-shared"
"github.com/fatih/color"
)
var openUnauthenticatedCloudURL func(msg, path string)
var openAuthenticatedURL func(msg, path string)
var convertTrial func()
func SetOpenUnauthenticatedCloudURLFn(fn func(msg, path string)) {
openUnauthenticatedCloudURL = fn
}
func SetOpenAuthenticatedURLFn(fn func(msg, path string)) {
openAuthenticatedURL = fn
}
func SetConvertTrialFn(fn func()) {
convertTrial = fn
}
func OutputSimpleError(msg string, args ...interface{}) {
msg = fmt.Sprintf(msg, args...)
fmt.Fprintln(os.Stderr, color.New(ColorHiRed, color.Bold).Sprint("🚨 "+shared.Capitalize(msg)))
}
func OutputErrorAndExit(msg string, args ...interface{}) {
StopSpinner()
msg = fmt.Sprintf(msg, args...)
msg = strings.ReplaceAll(msg, "status code:", "status code")
msg = strings.ReplaceAll(msg, ", body:", ":")
displayMsg := ""
errorParts := strings.Split(msg, ": ")
addedErrors := map[string]bool{}
if len(errorParts) > 1 {
var lastPart string
i := 0
for idx, part := range errorParts {
// don't repeat the same error message
if _, ok := addedErrors[strings.ToLower(part)]; ok {
continue
}
tail := strings.Join(errorParts[idx:], ": ")
if maybeJSON(tail) {
prettyJSON := prettyJSON(tail)
indent := strings.Repeat(" ", i)
// prepend indent to **each** line in the pretty JSON
indentedJSON := strings.ReplaceAll(prettyJSON, "\n", "\n"+indent+" ")
// now write the block
displayMsg += "\n" + indent + "→ " + indentedJSON
break
}
if len(lastPart) < 10 && i > 0 {
lastPart = lastPart + ": " + part
displayMsg += ": " + part
addedErrors[strings.ToLower(lastPart)] = true
addedErrors[strings.ToLower(part)] = true
continue
}
if i != 0 {
displayMsg += "\n"
}
// indent the error message
for n := 0; n < i; n++ {
displayMsg += " "
}
if i > 0 {
displayMsg += "→ "
}
s := shared.Capitalize(part)
if i == 0 {
s = color.New(ColorHiRed, color.Bold).Sprint("🚨 " + s)
}
displayMsg += s
addedErrors[strings.ToLower(part)] = true
lastPart = part
i++
}
} else {
displayMsg = color.New(ColorHiRed, color.Bold).Sprint("🚨 " + msg)
}
fmt.Fprintln(os.Stderr, color.New(ColorHiRed, color.Bold).Sprint(displayMsg))
os.Exit(1)
}
func OutputUnformattedErrorAndExit(msg string) {
StopSpinner()
fmt.Fprintln(os.Stderr, msg)
os.Exit(1)
}
func OutputNoCurrentPlanErrorAndExit() {
fmt.Println("🤷♂️ No current plan")
fmt.Println()
PrintCmds("", "new", "cd")
os.Exit(1)
}
func HandleApiError(apiError *shared.ApiError) {
if apiError.Type == shared.ApiErrorTypeCloudSubscriptionPaused {
if apiError.BillingError.HasBillingPermission {
StopSpinner()
fmt.Println("Your org's Plandex Cloud subscription is paused.")
res, err := ConfirmYesNo("Go to billing settings?")
if err != nil {
OutputErrorAndExit("error getting confirmation")
}
if res {
openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing")
os.Exit(0)
} else {
os.Exit(0)
}
} else {
OutputErrorAndExit("Your org's subscription is paused. Please contact an org owner to continue.")
}
}
if apiError.Type == shared.ApiErrorTypeCloudSubscriptionOverdue {
if apiError.BillingError.HasBillingPermission {
StopSpinner()
OutputSimpleError("Your org's Plandex Cloud subscription is overdue.")
res, err := ConfirmYesNo("Go to billing settings?")
if err != nil {
OutputErrorAndExit("error getting confirmation")
}
if res {
openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing")
os.Exit(0)
} else {
os.Exit(0)
}
} else {
OutputErrorAndExit("Your org's subscription is overdue. Please contact an org owner to continue.")
}
}
if apiError.Type == shared.ApiErrorTypeCloudMonthlyMaxReached {
if apiError.BillingError.HasBillingPermission {
StopSpinner()
OutputSimpleError("Your org has reached its monthly limit for Plandex Cloud.")
res, err := ConfirmYesNo("Go to billing settings?")
if err != nil {
OutputErrorAndExit("error getting confirmation")
}
if res {
openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing")
os.Exit(0)
} else {
os.Exit(0)
}
} else {
OutputErrorAndExit("Your org has reached its monthly limit for Plandex Cloud.")
}
}
if apiError.Type == shared.ApiErrorTypeCloudInsufficientCredits {
if apiError.BillingError.HasBillingPermission {
StopSpinner()
OutputSimpleError("Insufficient credits")
res, err := ConfirmYesNo("Go to billing settings?")
if err != nil {
OutputErrorAndExit("error getting confirmation")
}
if res {
openAuthenticatedURL("Opening billing settings in your browser.", "/settings/billing")
os.Exit(0)
} else {
os.Exit(0)
}
} else {
OutputErrorAndExit("Insufficient credits")
}
}
if apiError.Type == shared.ApiErrorTypeTrialMessagesExceeded {
StopSpinner()
fmt.Fprintf(os.Stderr, "\n🚨 You've reached the Plandex Cloud trial limit of %d messages per plan\n", apiError.TrialMessagesExceededError.MaxReplies)
res, err := ConfirmYesNo("Upgrade now?")
if err != nil {
OutputErrorAndExit("Error prompting upgrade trial: %v", err)
}
if res {
convertTrial()
PrintCmds("", "continue")
os.Exit(0)
}
}
StopSpinner()
OutputErrorAndExit(apiError.Msg)
}
func maybeJSON(s string) bool {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") {
return true
}
if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {
return true
}
return false
}
func prettyJSON(s string) string {
var v any
if err := json.Unmarshal([]byte(s), &v); err != nil {
return s // not JSON
}
out, _ := json.MarshalIndent(v, "", " ")
return string(out)
}
================================================
FILE: app/cli/term/format.go
================================================
package term
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glow/utils"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/termenv"
)
func init() {
// pre-cache the glamour renderer
getGlamourRenderer()
}
var (
cachedGlamourRenderer *glamour.TermRenderer
cachedGlamourRendererWidth int
)
func getGlamourRenderer() (*glamour.TermRenderer, error) {
width := GetTerminalWidth()
if cachedGlamourRenderer != nil && cachedGlamourRendererWidth == width {
return cachedGlamourRenderer, nil
}
// Build the renderer options.
var opts []glamour.TermRendererOption
// Check for a GLAMOUR_STYLE env variable.
if style, ok := os.LookupEnv("GLAMOUR_STYLE"); ok && style != "" {
opts = append(opts, glamour.WithStandardStyle(style))
} else {
// Fallback to auto style detection.
opts = append(opts, glamour.WithAutoStyle())
}
// Always set word wrap and preserved newlines.
opts = append(opts,
glamour.WithWordWrap(min(width, 80)),
glamour.WithPreservedNewLines(),
)
r, err := glamour.NewTermRenderer(opts...)
if err != nil {
return nil, fmt.Errorf("failed to create glamour renderer: %w", err)
}
cachedGlamourRenderer = r
cachedGlamourRendererWidth = width
return r, nil
}
func GetMarkdown(input string) (string, error) {
inputBytes := utils.RemoveFrontmatter([]byte(input))
r, err := getGlamourRenderer()
if err != nil {
return "", err
}
out, err := r.RenderBytes(inputBytes)
if err != nil {
return "", err
}
return string(out), nil
}
func GetPlain(input string) string {
width := GetTerminalWidth()
s := wordwrap.String(input, min(width-2, 80))
// add padding
lines := strings.Split(s, "\n")
// for i := range lines {
// lines[i] = " " + lines[i]
// }
s = strings.Join(lines, "\n")
s = termenv.String(s).Foreground(GetStreamForegroundColor()).String()
return s
}
================================================
FILE: app/cli/term/help.go
================================================
package term
import (
"fmt"
"io"
"os"
"strings"
"github.com/fatih/color"
)
type CmdConfig struct {
Cmd string
Alias string
Desc string
Repl bool
}
var CliCommands = []CmdConfig{
{"", "", "start the Plandex REPL", false},
// {"--full", "", fmt.Sprintf("start the Plandex REPL with auto-mode %s", "'full'"), false},
// {"--semi", "", fmt.Sprintf("start the Plandex REPL with auto-mode %s", "'semi'"), false},
// {"--plus", "", fmt.Sprintf("start the Plandex REPL with auto-mode %s", "'plus'"), false},
// {"--basic", "", fmt.Sprintf("start the Plandex REPL with auto-mode %s", "'basic'"), false},
// {"--none", "", fmt.Sprintf("start the Plandex REPL with auto-mode %s", "'none'"), false},
// {"--daily", "", fmt.Sprintf("start the Plandex REPL with %s model pack", "'daily-driver'"), false},
// {"--strong", "", fmt.Sprintf("start the Plandex REPL with %s model pack", "'strong'"), false},
// {"--cheap", "", fmt.Sprintf("start the Plandex REPL with %s model pack", "'cheap'"), false},
// {"--oss", "", fmt.Sprintf("start the Plandex REPL with %s model pack", "'oss'"), false},
{"new", "", "start a new plan", true},
{"new --full", "", fmt.Sprintf("start a new plan with auto-mode %s", "'full'"), true},
{"new --semi", "", fmt.Sprintf("start a new plan with auto-mode %s", "'semi'"), true},
{"new --plus", "", fmt.Sprintf("start a new plan with auto-mode %s", "'plus'"), true},
{"new --basic", "", fmt.Sprintf("start a new plan with auto-mode %s", "'basic'"), true},
{"new --none", "", fmt.Sprintf("start a new plan with auto-mode %s", "'none'"), true},
{"new --daily", "", fmt.Sprintf("start a new plan with %s model pack", "'daily-driver'"), true},
{"new --reasoning", "", fmt.Sprintf("start a new plan with %s model pack", "'reasoning'"), true},
{"new --strong", "", fmt.Sprintf("start a new plan with %s model pack", "'strong'"), true},
{"new --cheap", "", fmt.Sprintf("start a new plan with %s model pack", "'cheap'"), true},
{"new --oss", "", fmt.Sprintf("start a new plan with %s model pack", "'oss'"), true},
{"new --gemini-planner", "", fmt.Sprintf("start a new plan with %s model pack", "'gemini-planner'"), true},
{"new --o3-planner", "", fmt.Sprintf("start a new plan with %s model pack", "'o3-planner'"), true},
{"new --r1-planner", "", fmt.Sprintf("start a new plan with %s model pack", "'r1-planner'"), true},
{"new --perplexity-planner", "", fmt.Sprintf("start a new plan with %s model pack", "'perplexity-planner'"), true},
{"new --opus-planner", "", fmt.Sprintf("start a new plan with %s model pack", "'opus-planner'"), true},
{"plans", "pl", "list plans", true},
{"cd", "", "set current plan by name or index", true},
{"current", "cu", "show current plan", true},
{"rename", "", "rename the current plan", true},
{"delete-plan", "dp", "delete plan by name or index", true},
{"config", "", "show current plan config", true},
{"set-config", "", "update current plan config", true},
{"config default", "", "show the default config for new plans", true},
{"set-config default", "", "update the default config for new plans", true},
{"set-auto", "", "update auto-mode (autonomy level) for current plan", true},
{"set-auto none", "", fmt.Sprintf("set auto-mode to %s", "'none'"), true},
{"set-auto basic", "", fmt.Sprintf("set auto-mode to %s", "'basic'"), true},
{"set-auto plus", "", fmt.Sprintf("set auto-mode to %s", "'plus'"), true},
{"set-auto semi", "", fmt.Sprintf("set auto-mode to %s", "'semi'"), true},
{"set-auto full", "", fmt.Sprintf("set auto-mode to %s", "'full'"), true},
{"set-auto default", "", "set the default auto-mode for new plans", true},
{"tell", "t", "describe a task to complete", false},
{"chat", "ch", "ask a question or chat", false},
{"load", "l", "load files/dirs/urls/notes/images or pipe data into context", true},
{"ls", "", "list everything in context", true},
{"rm", "", "remove context by index, range, name, or glob", true},
{"clear", "", "remove all context", true},
{"update", "u", "update outdated context", true},
{"show", "", "show current context by name or index", true},
{"diff --ui", "", "review pending changes in a browser UI", true},
{"diff", "", "review pending changes in 'git diff' format", true},
{"diff --plain", "", "review pending changes in 'git diff' format with no color formatting", false},
{"summary", "", "show the latest summary of the current plan", true},
{"apply", "ap", "apply pending changes to project files", true},
{"reject", "rj", "reject pending changes to one or more project files", true},
{"log", "", "show log of plan updates", true},
{"rewind", "rw", "rewind to a previous state", true},
{"continue", "c", "continue the plan", true},
{"debug", "db", "repeatedly run a command and auto-apply fixes until it succeeds", true},
{"build", "b", "build any pending changes", true},
{"convo", "", "show plan conversation", true},
{"convo 1", "", "show a specific message in the conversation", false},
{"convo 2-5", "", "show a range of messages in the conversation", false},
{"convo --plain", "", "show conversation in plain text", false},
{"branches", "br", "list plan branches", true},
{"checkout", "co", "checkout or create a branch", true},
{"delete-branch", "dlb", "delete a branch by name or index", true},
{"plans --archived", "", "list archived plans", true},
{"archive", "arc", "archive a plan", true},
{"unarchive", "unarc", "unarchive a plan", true},
{"models", "", "show current plan model settings", true},
{"models default", "", "show the default model settings for new plans", true},
{"models available", "", "show all available models", true},
{"models available --custom", "", "show available custom models only", true},
{"models custom", "", "manage custom models, providers, and model packs", true},
{"providers", "", "show all available model providers", true},
{"providers --custom", "", "show available custom model providers only", true},
{"model-packs", "", "show all available model packs", true},
{"model-packs --custom", "", "show custom model packs only", true},
{"model-packs show", "", "show a built-in or custom model pack's settings", true},
{"set-model", "", "update current plan model settings", true},
{"set-model default", "", "update the default model settings for new plans", true},
{"set-model daily", "", fmt.Sprintf("Use %s model pack", "'daily-driver'"), true},
{"set-model reasoning", "", fmt.Sprintf("Use %s model pack", "'reasoning'"), true},
{"set-model strong", "", fmt.Sprintf("Use %s model pack", "'strong'"), true},
{"set-model cheap", "", fmt.Sprintf("Use %s model pack", "'cheap'"), true},
{"set-model oss", "", fmt.Sprintf("Use %s model pack", "'oss'"), true},
{"set-model gemini-planner", "", fmt.Sprintf("Use %s model pack", "'gemini-planner'"), true},
{"set-model o3-planner", "", fmt.Sprintf("Use %s model pack", "'o3-planner'"), true},
{"set-model r1-planner", "", fmt.Sprintf("Use %s model pack", "'r1-planner'"), true},
{"set-model perplexity-planner", "", fmt.Sprintf("Use %s model pack", "'perplexity-planner'"), true},
{"set-model opus-planner", "", fmt.Sprintf("Use %s model pack", "'opus-planner'"), true},
{"ps", "", "list active and recently finished plan streams", true},
{"stop", "", "stop an active plan stream", true},
{"connect", "conn", "connect to an active plan stream", true},
{"sign-in", "", "sign in, accept an invite, or create an account", true},
{"invite", "", "invite a user to join your org", true},
{"revoke", "", "revoke an invite or remove a user from your org", true},
{"users", "", "list users and pending invites in your org", true},
{"connect-claude", "", "connect your Claude Pro or Max subscription", true},
{"disconnect-claude", "", "disconnect your Claude Pro or Max subscription", true},
{"claude-status", "", "status of your Claude Pro or Max subscription connection", true},
{"usage", "", "show Plandex Cloud current balance and usage report", true},
{"usage --today", "", "show Plandex Cloud usage for the day so far", true},
{"usage --month", "", "show Plandex Cloud usage for the current billing month", true},
{"usage --plan", "", "show Plandex Cloud usage for the current plan", true},
{"usage --log", "", "show Plandex Cloud transaction log", true},
{"billing", "", "show Plandex Cloud billing settings", true},
}
var CmdDesc = map[string]CmdConfig{}
func init() {
for _, cmd := range CliCommands {
CmdDesc[cmd.Cmd] = cmd
}
}
func PrintCmds(prefix string, cmds ...string) {
printCmds(os.Stderr, prefix, []color.Attribute{color.Bold, color.FgHiWhite, color.BgCyan, color.FgHiWhite}, cmds...)
}
func PrintCmdsWithColors(prefix string, colors []color.Attribute, cmds ...string) {
printCmds(os.Stderr, prefix, colors, cmds...)
}
func printCmds(w io.Writer, prefix string, colors []color.Attribute, cmds ...string) {
if os.Getenv("PLANDEX_DISABLE_SUGGESTIONS") != "" {
return
}
for _, cmd := range cmds {
config, ok := CmdDesc[cmd]
if !ok {
continue
}
if IsRepl && !config.Repl {
continue
}
alias := config.Alias
desc := config.Desc
if alias != "" {
if IsRepl {
cmd = fmt.Sprintf("%s (\\%s)", cmd, alias)
} else {
containsFull := strings.Contains(cmd, alias)
if containsFull {
cmd = strings.Replace(cmd, alias, fmt.Sprintf("(%s)", alias), 1)
} else {
cmd = fmt.Sprintf("%s (%s)", cmd, alias)
}
}
// desc += color.New(color.FgWhite).Sprintf(" • alias → %s", fmt.Sprint(a'lias'))
}
var styled string
if IsRepl {
styled = color.New(colors...).Sprintf(" \\%s ", cmd)
} else if cmd == "" { // special case for the repl
styled = color.New(colors...).Sprintf(" plandex ")
} else {
styled = color.New(colors...).Sprintf(" plandex %s ", cmd)
}
fmt.Fprintf(w, "%s%s 👉 %s\n", prefix, styled, desc)
}
}
func PrintCustomCmd(prefix, cmd, alias, desc string) {
cmd = strings.Replace(cmd, alias, fmt.Sprintf("(%s)", alias), 1)
// desc += color.New(color.FgWhite).Sprintf(" • alias → %s", fmt.Sprint(a'lias'))
styled := color.New(color.Bold, color.FgHiWhite, color.BgCyan, color.FgHiWhite).Sprintf(" plandex %s ", cmd)
fmt.Printf("%s%s 👉 %s\n", prefix, styled, desc)
}
// PrintCustomHelp prints the custom help output for the Plandex CLI
func PrintCustomHelp(all bool) {
builder := &strings.Builder{}
color.New(color.Bold, color.BgGreen, color.FgHiWhite).Fprintln(builder, " Usage ")
color.New(color.Bold).Fprintln(builder, " plandex [command] [flags]")
color.New(color.Bold).Fprintln(builder, " pdx [command] [flags]")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgGreen, color.FgHiWhite).Fprintln(builder, " Help ")
color.New(color.Bold).Fprintln(builder, " plandex help # show basic usage")
color.New(color.Bold).Fprintln(builder, " plandex help --all # show all commands")
color.New(color.Bold).Fprintln(builder, " plandex [command] --help")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgMagenta, color.FgHiWhite).Fprintln(builder, " Getting Started ")
fmt.Fprintln(builder)
fmt.Fprintf(builder, " 🚀 Start the Plandex REPL in a project directory with %s or %s\n", color.New(color.Bold, color.BgCyan, color.FgHiWhite).Sprint(" plandex "), color.New(color.Bold, color.BgCyan, color.FgHiWhite).Sprint(" pdx "))
fmt.Fprintln(builder)
fmt.Fprintf(builder, " 💻 You can also use any command outside the REPL with %s or %s\n", color.New(color.Bold, color.BgCyan, color.FgHiWhite).Sprint(" plandex [command] "), color.New(color.Bold, color.BgCyan, color.FgHiWhite).Sprint(" pdx [command] "))
fmt.Fprintln(builder)
color.New(color.Bold, color.BgMagenta, color.FgHiWhite).Fprintln(builder, " REPL Options ")
fmt.Fprintln(builder)
// Add REPL startup flags
fmt.Fprintln(builder, color.New(color.Bold, color.FgHiBlue).Sprint(" Mode "))
fmt.Fprintln(builder, " --chat, -c Start in chat mode (for conversation without making changes)")
fmt.Fprintln(builder, " --tell, -t Start in tell mode (for implementation)")
fmt.Fprintln(builder)
fmt.Fprintln(builder, color.New(color.Bold, color.FgHiBlue).Sprint(" Autonomy "))
fmt.Fprintln(builder, " --no-auto None → step-by-step, no automation")
fmt.Fprintln(builder, " --basic Basic → auto-continue plans")
fmt.Fprintln(builder, " --plus Plus → auto-update context, smart context, auto-commit changes")
fmt.Fprintln(builder, " --semi Semi-Auto → auto-load context")
fmt.Fprintln(builder, " --full Full-Auto → auto-apply, auto-exec, auto-debug")
fmt.Fprintln(builder)
fmt.Fprintln(builder, color.New(color.Bold, color.FgHiBlue).Sprint(" Models "))
fmt.Fprintln(builder, " --daily Daily driver pack")
fmt.Fprintln(builder, " --reasoning Reasoning pack")
fmt.Fprintln(builder, " --strong Strong pack")
fmt.Fprintln(builder, " --cheap Cheap pack")
fmt.Fprintln(builder, " --oss Open source pack")
fmt.Fprintln(builder)
if all {
fmt.Print(builder.String())
PrintHelpAllCommands()
} else {
fmt.Print(builder.String())
// in the same style as 'getting started' section, output See All Commands
color.New(color.Bold, color.BgHiBlue, color.FgHiWhite).Fprintln(builder, " Use 'plandex help --all' or 'plandex help -a' for a list of all commands ")
fmt.Fprintln(builder)
fmt.Print(builder.String())
}
}
func PrintHelpAllCommands() {
builder := &strings.Builder{}
color.New(color.Bold, color.BgMagenta, color.FgHiWhite).Fprintln(builder, " Key Commands ")
printCmds(builder, " ", []color.Attribute{color.Bold, color.FgHiMagenta}, "new", "load", "tell", "diff", "diff --ui", "apply", "reject", "debug", "chat")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Plans ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "new", "plans", "cd", "current", "delete-plan", "rename", "archive", "plans --archived", "unarchive")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Changes ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "diff", "diff --ui", "diff --plain", "apply", "reject")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Context ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "load", "ls", "rm", "update", "clear")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Branches ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "branches", "checkout", "delete-branch")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " History ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "log", "rewind", "convo", "convo 1", "convo 2-5", "convo --plain", "summary")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Control ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "tell", "continue", "build", "debug", "chat")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Streams ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "ps", "connect", "stop")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Config ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "config", "set-config", "config default", "set-config default")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Autonomy ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "set-auto", "set-auto default", "set-auto full", "set-auto semi", "set-auto plus", "set-auto basic", "set-auto none")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " AI Models ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "models", "models default", "model-packs", "set-model", "set-model daily", "set-model reasoning", "set-model strong", "set-model cheap", "set-model oss", "set-model default")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Custom Models ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan},
"models custom", "models available", "models available --custom", "providers", "providers --custom", "model-packs", "model-packs --custom", "model-packs show")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Accounts ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "sign-in", "invite", "revoke", "users")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Integrations ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "connect-claude", "disconnect-claude", "claude-status")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " Cloud ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "usage", "usage --today", "usage --month", "usage --plan", "usage --log", "billing")
fmt.Fprintln(builder)
color.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, " New Plan Shortcuts ")
printCmds(builder, " ", []color.Attribute{color.Bold, ColorHiCyan}, "new --full", "new --semi", "new --plus", "new --basic", "new --none", "new --daily", "new --reasoning", "new --strong", "new --cheap", "new --oss", "new --gemini-planner", "new --o3-planner", "new --r1-planner", "new --perplexity-planner", "new --opus-planner")
fmt.Fprintln(builder)
fmt.Print(builder.String())
}
func ShowCmd(cmd string) string {
if IsRepl {
cmd = fmt.Sprintf("\\%s", cmd)
} else {
cmd = fmt.Sprintf("plandex %s", cmd)
}
return color.New(color.Bold, color.BgCyan, color.FgHiWhite).Sprintf(" %s ", cmd)
}
================================================
FILE: app/cli/term/os.go
================================================
package term
import (
"fmt"
"os"
"runtime"
)
func GetOsDetails() string {
return fmt.Sprintf(
"OS: %s\nArchitecture: %s\nCPUs: %d\nShell: %s",
runtime.GOOS,
runtime.GOARCH,
runtime.NumCPU(),
os.Getenv("SHELL"),
)
}
================================================
FILE: app/cli/term/prompt.go
================================================
package term
import (
"fmt"
"os"
"github.com/cqroot/prompt"
"github.com/cqroot/prompt/input"
"github.com/eiannone/keyboard"
"github.com/fatih/color"
)
func GetRequiredUserStringInput(msg string) (string, error) {
res, err := GetUserStringInput(msg)
if err != nil {
return "", fmt.Errorf("failed to get user input: %s", err)
}
if res == "" {
color.New(color.Bold, ColorHiRed).Println("🚨 This input is required")
return GetRequiredUserStringInput(msg)
}
return res, nil
}
func GetUserStringInput(msg string) (string, error) {
return GetUserStringInputWithDefault(msg, "")
}
func GetUserStringInputWithDefault(msg, def string) (string, error) {
disableBracketedPaste()
defer enableBracketedPaste()
res, err := prompt.New().Ask(msg).Input(def)
if err != nil && err.Error() == "user quit prompt" {
os.Exit(0)
}
return res, err
}
func GetRequiredUserStringInputWithDefault(msg, def string) (string, error) {
res, err := GetUserStringInputWithDefault(msg, def)
if err != nil {
return "", fmt.Errorf("failed to get user input: %s", err)
}
if res == "" {
color.New(color.Bold, ColorHiRed).Println("🚨 This input is required")
return GetRequiredUserStringInputWithDefault(msg, def)
}
return res, nil
}
func GetUserPasswordInput(msg string) (string, error) {
disableBracketedPaste()
defer enableBracketedPaste()
res, err := prompt.New().Ask(msg).Input("", input.WithEchoMode(input.EchoPassword))
if err != nil && err.Error() == "user quit prompt" {
os.Exit(0)
}
return res, err
}
func GetUserKeyInput() (rune, keyboard.Key, error) {
if err := keyboard.Open(); err != nil {
return 0, 0, fmt.Errorf("failed to open keyboard: %s", err)
}
defer func() {
_ = keyboard.Close()
}()
char, key, err := keyboard.GetKey()
if err != nil {
return 0, 0, fmt.Errorf("failed to read keypress: %s", err)
}
return char, key, nil
}
func ConfirmYesNo(fmtStr string, fmtArgs ...interface{}) (bool, error) {
color.New(ColorHiMagenta, color.Bold).Printf(fmtStr+" (y)es | (n)o", fmtArgs...)
color.New(ColorHiMagenta, color.Bold).Print("> ")
char, key, err := GetUserKeyInput()
if err != nil {
return false, fmt.Errorf("failed to get user input: %s", err)
}
// ctrl+c == no
if key == keyboard.KeyCtrlC {
return false, nil
}
fmt.Println(string(char))
if char == 'y' || char == 'Y' {
return true, nil
} else if char == 'n' || char == 'N' {
return false, nil
} else {
fmt.Println()
color.New(ColorHiRed, color.Bold).Print("Invalid input.\nEnter 'y' for yes or 'n' for no.\n\n")
return ConfirmYesNo(fmtStr, fmtArgs...)
}
}
func ConfirmYesNoCancel(fmtStr string, fmtArgs ...interface{}) (bool, bool, error) {
color.New(ColorHiMagenta, color.Bold).Printf(fmtStr+" (y)es | (n)o | (c)ancel", fmtArgs...)
color.New(ColorHiMagenta, color.Bold).Print("> ")
char, key, err := GetUserKeyInput()
if err != nil {
return false, false, fmt.Errorf("failed to get user input: %s", err)
}
// ctrl+c == cancel
if key == keyboard.KeyCtrlC {
return false, true, nil
}
fmt.Println(string(char))
if char == 'y' || char == 'Y' {
return true, false, nil
} else if char == 'n' || char == 'N' {
return false, false, nil
} else if char == 'c' || char == 'C' {
return false, true, nil
} else {
fmt.Println()
color.New(ColorHiRed, color.Bold).Print("Invalid input.\nEnter 'y' for yes, 'n' for no, or 'c' for cancel.\n\n")
return ConfirmYesNoCancel(fmtStr, fmtArgs...)
}
}
func disableBracketedPaste() {
fmt.Print("\033[?2004l")
}
func enableBracketedPaste() {
fmt.Print("\033[?2004h")
}
================================================
FILE: app/cli/term/repl.go
================================================
package term
import "os"
var IsRepl = os.Getenv("PLANDEX_REPL") != ""
func SetIsRepl(value bool) {
IsRepl = value
}
================================================
FILE: app/cli/term/select.go
================================================
package term
import (
"fmt"
"os"
"github.com/fatih/color"
"github.com/plandex-ai/survey/v2"
)
func SelectFromList(msg string, options []string) (string, error) {
var selected string
prompt := &survey.Select{
Message: color.New(ColorHiMagenta, color.Bold).Sprint(msg),
Options: convertToStringSlice(options),
FilterMessage: "",
}
err := survey.AskOne(prompt, &selected)
if err != nil {
if err.Error() == "interrupt" {
os.Exit(0)
}
return "", err
}
return selected, nil
}
func convertToStringSlice[T any](input []T) []string {
var result []string
for _, v := range input {
result = append(result, fmt.Sprint(v))
}
return result
}
================================================
FILE: app/cli/term/spinner.go
================================================
package term
import (
"sync/atomic"
"time"
"github.com/briandowns/spinner"
)
const withMessageMinDuration = 700 * time.Millisecond
const withoutMessageMinDuration = 350 * time.Millisecond
var s = spinner.New(spinner.CharSets[33], 100*time.Millisecond)
var startedAt time.Time
var lastMessage string
var active bool
var currentWarningLoop int32
func StartSpinner(msg string) {
if active {
if msg == lastMessage {
return
}
s.Stop()
}
startedAt = time.Now()
s.Prefix = msg + " "
lastMessage = msg
s.Start()
active = true
}
func StopSpinner() {
elapsed := time.Since(startedAt)
if lastMessage != "" && elapsed < withMessageMinDuration {
time.Sleep(withMessageMinDuration - elapsed)
} else if elapsed < withoutMessageMinDuration {
time.Sleep(withoutMessageMinDuration - elapsed)
}
s.Stop()
ClearCurrentLine()
active = false
}
func ResumeSpinner() {
if !active {
StartSpinner(lastMessage)
}
}
func LongSpinnerWithWarning(msg, warning string) {
atomic.AddInt32(¤tWarningLoop, 1)
currentLoop := currentWarningLoop
StartSpinner(msg)
var flashWarning func()
flashWarning = func() {
go func() {
time.Sleep(3 * time.Second)
if !active || atomic.LoadInt32(¤tWarningLoop) != currentLoop {
return
}
StartSpinner(warning)
time.Sleep(2 * time.Second)
if !active || atomic.LoadInt32(¤tWarningLoop) != currentLoop {
return
}
StartSpinner(msg)
flashWarning()
}()
}
flashWarning()
}
================================================
FILE: app/cli/term/utils.go
================================================
package term
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/muesli/termenv"
"golang.org/x/term"
)
func init() {
// pre-cache terminal settings
IsTerminal()
GetTerminalWidth()
GetStreamForegroundColor()
HasDarkBackground()
}
func AlternateScreen() {
// Switch to alternate screen and hide the cursor
fmt.Print("\x1b[?1049h\x1b[?25l")
}
func ClearScreen() {
fmt.Print("\x1b[2J")
}
func MoveCursorToTopLeft() {
fmt.Print("\x1b[H")
}
func ClearCurrentLine() {
fmt.Print("\033[2K")
}
func MoveUpLines(numLines int) {
fmt.Printf("\033[%dA", numLines)
}
func BackToMain() {
// Switch back to main screen and show the cursor on exit
fmt.Print("\x1b[?1049l\x1b[?25h")
}
func PageOutput(output string) {
cmd := exec.Command("less", "-R")
cmd.Env = append(os.Environ(), "LESS=FRX", "LESSCHARSET=utf-8")
cmd.Stdin = strings.NewReader(output)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
OutputErrorAndExit("Failed to page output: %v", err)
}
}
func PageOutputReverse(output string) {
cmd := exec.Command("less", "-RX", "+G")
cmd.Stdin = strings.NewReader(output)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Set the environment variables specifically for the less command
cmd.Env = append(os.Environ(), "LESS=FRX", "LESSCHARSET=utf-8")
if err := cmd.Run(); err != nil {
OutputErrorAndExit("Failed to page output: %v", err)
}
}
func GetDivisionLine() string {
// Get the terminal width
terminalWidth := GetTerminalWidth()
return strings.Repeat("─", terminalWidth)
}
var envReplCols int
var envDefaultCols int
func GetTerminalWidth() int {
if envReplCols != 0 {
return envReplCols
}
if os.Getenv("PLANDEX_COLUMNS") != "" {
w, err := strconv.Atoi(os.Getenv("PLANDEX_COLUMNS"))
if err == nil {
envReplCols = w
return w
}
}
if IsTerminal() {
// Try to get terminal size
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {
return w
}
}
if envDefaultCols != 0 {
return envDefaultCols
}
// Not running in a TTY or GetSize failed; use a default.
// Try to get width from environment variable
if w, err := strconv.Atoi(os.Getenv("COLUMNS")); err == nil {
envDefaultCols = w
return w
}
// Fallback to default width
return 80
}
var envStreamForegroundColor termenv.Color
func GetStreamForegroundColor() termenv.Color {
if envStreamForegroundColor != nil {
return envStreamForegroundColor
}
if os.Getenv("PLANDEX_STREAM_FOREGROUND_COLOR") != "" {
envStreamForegroundColor = termenv.ANSI256.Color(os.Getenv("PLANDEX_STREAM_FOREGROUND_COLOR"))
return envStreamForegroundColor
}
c := "234"
if HasDarkBackground() {
c = "251"
}
envStreamForegroundColor = termenv.ANSI256.Color(c)
return envStreamForegroundColor
}
var envHasDarkBackground bool
var cachedHasDarkBackground bool
func HasDarkBackground() bool {
if cachedHasDarkBackground {
return envHasDarkBackground
}
envHasDarkBackground = termenv.HasDarkBackground()
cachedHasDarkBackground = true
return envHasDarkBackground
}
var envIsTerminal bool
var cachedIsTerminal bool
func IsTerminal() bool {
if cachedIsTerminal {
return envIsTerminal
}
envIsTerminal = term.IsTerminal(int(os.Stdout.Fd()))
cachedIsTerminal = true
return envIsTerminal
}
================================================
FILE: app/cli/types/api.go
================================================
package types
import (
"context"
shared "plandex-shared"
"github.com/shopspring/decimal"
)
type OnStreamPlanParams struct {
Msg *shared.StreamMessage
Err error
}
type OnStreamPlan func(params OnStreamPlanParams)
type ApiClient interface {
CreateCliTrialSession() (string, *shared.ApiError)
GetCliTrialSession(token string) (*shared.SessionResponse, *shared.ApiError)
CreateEmailVerification(email, customHost, userId string) (*shared.CreateEmailVerificationResponse, *shared.ApiError)
CreateSignInCode() (string, *shared.ApiError)
CreateAccount(req shared.CreateAccountRequest, customHost string) (*shared.SessionResponse, *shared.ApiError)
SignIn(req shared.SignInRequest, customHost string) (*shared.SessionResponse, *shared.ApiError)
SignOut() *shared.ApiError
GetOrgSession() (*shared.Org, *shared.ApiError)
ListOrgs() ([]*shared.Org, *shared.ApiError)
CreateOrg(req shared.CreateOrgRequest) (*shared.CreateOrgResponse, *shared.ApiError)
GetOrgUserConfig() (*shared.OrgUserConfig, *shared.ApiError)
UpdateOrgUserConfig(req shared.OrgUserConfig) *shared.ApiError
ListUsers() (*shared.ListUsersResponse, *shared.ApiError)
DeleteUser(userId string) *shared.ApiError
ListOrgRoles() ([]*shared.OrgRole, *shared.ApiError)
InviteUser(req shared.InviteRequest) *shared.ApiError
ListPendingInvites() ([]*shared.Invite, *shared.ApiError)
ListAcceptedInvites() ([]*shared.Invite, *shared.ApiError)
ListAllInvites() ([]*shared.Invite, *shared.ApiError)
DeleteInvite(inviteId string) *shared.ApiError
CreateProject(req shared.CreateProjectRequest) (*shared.CreateProjectResponse, *shared.ApiError)
ListProjects() ([]*shared.Project, *shared.ApiError)
SetProjectPlan(projectId string, req shared.SetProjectPlanRequest) *shared.ApiError
RenameProject(projectId string, req shared.RenameProjectRequest) *shared.ApiError
ListPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError)
ListArchivedPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError)
ListPlansRunning(projectIds []string, includeRecent bool) (*shared.ListPlansRunningResponse, *shared.ApiError)
GetCurrentBranchByPlanId(projectId string, req shared.GetCurrentBranchByPlanIdRequest) (map[string]*shared.Branch, *shared.ApiError)
GetPlan(planId string) (*shared.Plan, *shared.ApiError)
CreatePlan(projectId string, req shared.CreatePlanRequest) (*shared.CreatePlanResponse, *shared.ApiError)
TellPlan(planId, branch string, req shared.TellPlanRequest, onStreamPlan OnStreamPlan) *shared.ApiError
BuildPlan(planId, branch string, req shared.BuildPlanRequest, onStreamPlan OnStreamPlan) *shared.ApiError
RespondMissingFile(planId, branch string, req shared.RespondMissingFileRequest) *shared.ApiError
DeletePlan(planId string) *shared.ApiError
DeleteAllPlans(projectId string) *shared.ApiError
ConnectPlan(planId, branch string, onStreamPlan OnStreamPlan) *shared.ApiError
StopPlan(ctx context.Context, planId, branch string) *shared.ApiError
ArchivePlan(planId string) *shared.ApiError
UnarchivePlan(planId string) *shared.ApiError
RenamePlan(planId string, name string) *shared.ApiError
GetCurrentPlanState(planId, branch string) (*shared.CurrentPlanState, *shared.ApiError)
GetCurrentPlanStateAtSha(planId, sha string) (*shared.CurrentPlanState, *shared.ApiError)
ApplyPlan(planId, branch string, req shared.ApplyPlanRequest) (string, *shared.ApiError)
RejectAllChanges(planId, branch string) *shared.ApiError
RejectFile(planId, branch, filePath string) *shared.ApiError
RejectFiles(planId, branch string, paths []string) *shared.ApiError
GetPlanDiffs(planId, branch string, plain bool) (string, *shared.ApiError)
LoadContext(planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError)
UpdateContext(planId, branch string, req shared.UpdateContextRequest) (*shared.UpdateContextResponse, *shared.ApiError)
DeleteContext(planId, branch string, req shared.DeleteContextRequest) (*shared.DeleteContextResponse, *shared.ApiError)
ListContext(planId, branch string) ([]*shared.Context, *shared.ApiError)
LoadCachedFileMap(planId, branch string, req shared.LoadCachedFileMapRequest) (*shared.LoadCachedFileMapResponse, *shared.ApiError)
ListConvo(planId, branch string) ([]*shared.ConvoMessage, *shared.ApiError)
GetPlanStatus(planId, branch string) (string, *shared.ApiError)
ListLogs(planId, branch string) (*shared.LogResponse, *shared.ApiError)
RewindPlan(planId, branch string, req shared.RewindPlanRequest) (*shared.RewindPlanResponse, *shared.ApiError)
ListBranches(planId string) ([]*shared.Branch, *shared.ApiError)
DeleteBranch(planId, branch string) *shared.ApiError
CreateBranch(planId, branch string, req shared.CreateBranchRequest) *shared.ApiError
GetSettings(planId, branch string) (*shared.PlanSettings, *shared.ApiError)
UpdateSettings(planId, branch string, req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError)
GetOrgDefaultSettings() (*shared.PlanSettings, *shared.ApiError)
UpdateOrgDefaultSettings(req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError)
GetPlanConfig(planId string) (*shared.PlanConfig, *shared.ApiError)
UpdatePlanConfig(planId string, req shared.UpdatePlanConfigRequest) *shared.ApiError
GetDefaultPlanConfig() (*shared.PlanConfig, *shared.ApiError)
UpdateDefaultPlanConfig(req shared.UpdateDefaultPlanConfigRequest) *shared.ApiError
CreateCustomModels(input *shared.ModelsInput) *shared.ApiError
ListCustomModels() ([]*shared.CustomModel, *shared.ApiError)
ListCustomProviders() ([]*shared.CustomProvider, *shared.ApiError)
ListModelPacks() ([]*shared.ModelPack, *shared.ApiError)
GetCreditsTransactions(pageSize, pageNum int, req shared.CreditsLogRequest) (*shared.CreditsLogResponse, *shared.ApiError)
GetCreditsSummary(req shared.CreditsLogRequest) (*shared.CreditsSummaryResponse, *shared.ApiError)
GetBalance() (decimal.Decimal, *shared.ApiError)
GetFileMap(req shared.GetFileMapRequest) (*shared.GetFileMapResponse, *shared.ApiError)
GetContextBody(planId, branch, contextId string) (*shared.GetContextBodyResponse, *shared.ApiError)
AutoLoadContext(ctx context.Context, planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError)
GetBuildStatus(planId, branch string) (*shared.GetBuildStatusResponse, *shared.ApiError)
}
================================================
FILE: app/cli/types/apply.go
================================================
package types
import (
"os"
)
type ApplyFlags struct {
AutoConfirm bool
AutoCommit bool
NoCommit bool
AutoExec bool
NoExec bool
AutoDebug int
}
type ApplyRollbackOption string
const (
ApplyRollbackOptionKeep ApplyRollbackOption = "Apply file changes"
ApplyRollbackOptionRollback ApplyRollbackOption = "Roll back file changes"
)
type OnApplyExecFailFn func(status int, output string, attempt int, toRollback *ApplyRollbackPlan, onErr OnErrFn, onSuccess func())
type ApplyReversion struct {
Content string
Mode os.FileMode
}
type ApplyRollbackPlan struct {
ToRevert map[string]ApplyReversion
ToRemove []string
PreviousProjectPaths *ProjectPaths
}
func (r *ApplyRollbackPlan) HasChanges() bool {
return len(r.ToRevert) > 0 || len(r.ToRemove) > 0
}
================================================
FILE: app/cli/types/exec.go
================================================
package types
type TellFlags struct {
TellBg bool
TellStop bool
TellNoBuild bool
IsUserContinue bool
IsUserDebug bool
IsApplyDebug bool
IsChatOnly bool
AutoContext bool
SmartContext bool
ContinuedAfterAction bool
ExecEnabled bool
AutoApply bool
IsImplementationOfChat bool
SkipChangesMenu bool
}
type BuildFlags struct {
BuildBg bool
AutoApply bool
}
================================================
FILE: app/cli/types/fs.go
================================================
package types
import ignore "github.com/sabhiram/go-gitignore"
type ProjectPaths struct {
ActivePaths map[string]bool
AllPaths map[string]bool
ActiveDirs map[string]bool
AllDirs map[string]bool
PlandexIgnored *ignore.GitIgnore
IgnoredPaths map[string]string
GitIgnoredDirs map[string]bool
}
================================================
FILE: app/cli/types/types.go
================================================
package types
import (
shared "plandex-shared"
"time"
"github.com/sashabaranov/go-openai"
)
type LoadContextParams struct {
Note string
Recursive bool
NamesOnly bool
ForceSkipIgnore bool
ImageDetail openai.ImageURLDetail
DefsOnly bool
SkipIgnoreWarning bool
AutoLoaded bool
SessionId string
}
type ContextOutdatedResult struct {
Msg string
UpdatedContexts []*shared.Context
RemovedContexts []*shared.Context
TokenDiffsById map[string]int
NumFiles int
NumUrls int
NumTrees int
NumMaps int
NumFilesRemoved int
NumTreesRemoved int
ReqFn func() (map[string]*shared.UpdateContextParams, error)
}
const (
PlanOutdatedStrategyOverwrite string = "Clear the modifications and then apply"
PlanOutdatedStrategyApplyUnmodified string = "Apply only new and unmodified files"
PlanOutdatedStrategyApplyNoConflicts string = "Apply anyway since there are no conflicts"
PlanOutdatedStrategyRebuild string = "Rebuild the plan with updated context"
PlanOutdatedStrategyCancel string = "Cancel"
)
type CurrentPlanSettings struct {
Id string `json:"id"`
}
type PlanSettings struct {
Branch string `json:"branch"`
}
type CurrentProjectSettings struct {
Id string `json:"id"`
}
type CurrentPlanSettingsByAccount map[string]*CurrentPlanSettings
type PlanSettingsByAccount map[string]*PlanSettings
type CurrentProjectSettingsByAccount map[string]*CurrentProjectSettings
type ChangesUIScrollReplacement struct {
OldContent string
NewContent string
NumLinesPrepended int
}
type ChangesUIViewportsUpdate struct {
ScrollReplacement *ChangesUIScrollReplacement
}
type OnErrFn func(errMsg string, errArgs ...interface{})
type OauthResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
}
type OauthCreds struct {
OauthResponse
ExpiresAt time.Time `json:"expires_at"`
}
type AccountCredentials struct {
ClaudeMax *OauthCreds `json:"claudeMax"`
}
================================================
FILE: app/cli/ui/ui.go
================================================
package ui
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"plandex-cli/api"
"plandex-cli/term"
"strings"
"github.com/fatih/color"
"github.com/pkg/browser"
shared "plandex-shared"
)
func OpenAuthenticatedURL(msg, path string) {
signInCode, apiErr := api.Client.CreateSignInCode()
if apiErr != nil {
log.Fatalf("Error creating sign in code: %v", apiErr)
}
apiHost := api.GetApiHost()
appHost := strings.Replace(apiHost, "api-v2.", "app.", 1)
appHost = strings.Replace(appHost, "api.", "app.", 1)
token := shared.UiSignInToken{
Pin: signInCode,
RedirectTo: path,
}
jsonToken, err := json.Marshal(token)
if err != nil {
log.Fatalf("Error marshalling token: %v", err)
}
encodedToken := base64.URLEncoding.EncodeToString(jsonToken)
url := fmt.Sprintf("%s/auth/%s", appHost, encodedToken)
OpenURL(msg, url)
}
func OpenUnauthenticatedCloudURL(msg, path string) {
apiHost := api.GetApiHost()
appHost := strings.Replace(apiHost, "api-v2.", "app.", 1)
url := fmt.Sprintf("%s%s", appHost, path)
OpenURL(msg, url)
}
func OpenURL(msg, url string) {
fmt.Printf(
"%s\n\nIf it doesn't open automatically, use this URL:\n%s\n",
color.New(term.ColorHiGreen).Sprintf(msg),
url,
)
err := browser.OpenURL(url)
if err != nil {
fmt.Printf("Failed to open URL automatically: %v\n", err)
fmt.Println("Please open the URL manually in your browser.")
}
}
================================================
FILE: app/cli/upgrade.go
================================================
package main
import (
"archive/tar"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"plandex-cli/term"
"plandex-cli/version"
"runtime"
"strings"
"time"
"github.com/Masterminds/semver"
"github.com/fatih/color"
"github.com/inconshreveable/go-update"
)
func checkForUpgrade() {
if os.Getenv("PLANDEX_SKIP_UPGRADE") != "" {
return
}
if version.Version == "development" {
return
}
term.StartSpinner("")
defer term.StopSpinner()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
latestVersionURL := "https://plandex.ai/v2/cli-version.txt"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, latestVersionURL, nil)
if err != nil {
log.Println("Error creating request:", err)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Println("Error checking latest version:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("Error reading response body:", err)
return
}
versionStr := string(body)
versionStr = strings.TrimSpace(versionStr)
latestVersion, err := semver.NewVersion(versionStr)
if err != nil {
log.Println("Error parsing latest version:", err)
return
}
currentVersion, err := semver.NewVersion(version.Version)
if err != nil {
log.Println("Error parsing current version:", err)
return
}
if latestVersion.GreaterThan(currentVersion) {
term.StopSpinner()
fmt.Println("A new version of Plandex is available:", color.New(color.Bold, term.ColorHiGreen).Sprint(versionStr))
fmt.Printf("Current version: %s\n", color.New(color.Bold, term.ColorHiCyan).Sprint(version.Version))
confirmed, err := term.ConfirmYesNo("Upgrade to the latest version?")
if err != nil {
log.Println("Error reading input:", err)
return
}
if confirmed {
term.ResumeSpinner()
err := doUpgrade(latestVersion.String())
if err != nil {
term.OutputErrorAndExit("Failed to upgrade: %v", err)
return
}
term.StopSpinner()
restartPlandex()
} else {
fmt.Println("Note: set PLANDEX_SKIP_UPGRADE=1 to stop upgrade prompts")
}
}
}
func doUpgrade(version string) error {
tag := fmt.Sprintf("cli/v%s", version)
escapedTag := url.QueryEscape(tag)
downloadURL := fmt.Sprintf("https://github.com/plandex-ai/plandex/releases/download/%s/plandex_%s_%s_%s.tar.gz", escapedTag, version, runtime.GOOS, runtime.GOARCH)
resp, err := http.Get(downloadURL)
if err != nil {
return fmt.Errorf("failed to download the update: %w", err)
}
defer resp.Body.Close()
// Create a temporary file to save the downloaded archive
tempFile, err := os.CreateTemp("", "*.tar.gz")
if err != nil {
return fmt.Errorf("failed to create temporary file: %w", err)
}
defer os.Remove(tempFile.Name()) // Clean up file afterwards
// Copy the response body to the temporary file
_, err = io.Copy(tempFile, resp.Body)
if err != nil {
return fmt.Errorf("failed to save the downloaded archive: %w", err)
}
_, err = tempFile.Seek(0, 0)
if err != nil {
return fmt.Errorf("failed to seek in temporary file: %w", err)
}
// Now, extract the binary from the tempFile
gzr, err := gzip.NewReader(tempFile)
if err != nil {
return fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzr.Close()
tarReader := tar.NewReader(gzr)
for {
header, err := tarReader.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return fmt.Errorf("failed to read tar header: %w", err)
}
// Check if the current file is the binary
if header.Typeflag == tar.TypeReg && (header.Name == "plandex" || header.Name == "plandex.exe") {
err = update.Apply(tarReader, update.Options{})
if err != nil {
if errors.Is(err, fs.ErrPermission) {
return fmt.Errorf("failed to apply update due to permission error; please try running your command again with 'sudo': %w", err)
}
return fmt.Errorf("failed to apply update: %w", err)
}
break
}
}
return nil
}
func restartPlandex() {
exe, err := os.Executable()
if err != nil {
term.OutputErrorAndExit("Failed to determine executable path: %v", err)
}
cmd := exec.Command(exe, os.Args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
term.OutputErrorAndExit("Failed to restart: %v", err)
}
err = cmd.Wait()
// If the process exited with an error, exit with the same error code
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
} else if err != nil {
term.OutputErrorAndExit("Failed to restart: %v", err)
}
os.Exit(0)
}
================================================
FILE: app/cli/url/url.go
================================================
package url
import (
"errors"
"io"
"net/http"
"net/url"
"plandex-cli/term"
"regexp"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
)
const (
// Constants for fetchURLContent function
maxRedirections = 10
httpTimeout = 30 * time.Second
maxContentSizeInMB = 10
)
func FetchURLContent(url string) (string, error) {
client := &http.Client{
Timeout: httpTimeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= maxRedirections {
return errors.New("stopped after too many redirects")
}
return nil
},
}
resp, err := client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", errors.New("non-2xx HTTP response status: " + resp.Status)
}
// Limit the response reader to a maximum amount
limitedReader := io.LimitReader(resp.Body, maxContentSizeInMB*1024*1024)
content, err := io.ReadAll(limitedReader)
if err != nil {
return "", err
}
contentType := resp.Header.Get("Content-Type")
if strings.Contains(contentType, "text/html") {
return ExtractTextualContent(string(content)), nil
} else {
return string(content), nil
}
}
func ExtractTextualContent(htmlContent string) string {
r := strings.NewReader(htmlContent)
doc, err := goquery.NewDocumentFromReader(r)
if err != nil {
term.OutputErrorAndExit("Failed to parse HTML: %v", err)
}
return doc.Text()
}
func SanitizeURL(url string) string {
// remove protocol portion with a regex
re := regexp.MustCompile(`^.*?://`)
url = re.ReplaceAllString(url, "")
// Replace common invalid filename characters. You can extend this list as needed.
sanitized := strings.ReplaceAll(url, ":", "_")
sanitized = strings.ReplaceAll(sanitized, "/", "_")
sanitized = strings.ReplaceAll(sanitized, "?", "_")
sanitized = strings.ReplaceAll(sanitized, "&", "_")
sanitized = strings.ReplaceAll(sanitized, "=", "_")
sanitized = strings.ReplaceAll(sanitized, "#", "_")
sanitized = strings.ReplaceAll(sanitized, "%", "_")
sanitized = strings.ReplaceAll(sanitized, "*", "_")
sanitized = strings.ReplaceAll(sanitized, " ", "_")
return sanitized
}
func IsValidURL(str string) bool {
u, err := url.Parse(str)
return err == nil && u.Scheme != "" && u.Host != ""
}
================================================
FILE: app/cli/utils/utils.go
================================================
package utils
import (
"time"
)
func EnsureMinDuration(start time.Time, minDuration time.Duration) {
elapsed := time.Since(start)
if elapsed < minDuration {
time.Sleep(minDuration - elapsed)
}
}
================================================
FILE: app/cli/version/version.go
================================================
package version
// Version will be set at build time using -ldflags
var Version = "development"
================================================
FILE: app/cli/version.txt
================================================
2.2.1
================================================
FILE: app/docker-compose.yml
================================================
services:
plandex-postgres:
image: postgres:latest
restart: always
environment:
POSTGRES_PASSWORD: plandex
POSTGRES_USER: plandex
POSTGRES_DB: plandex
ports:
- "5432:5432"
volumes:
- plandex-db:/var/lib/postgresql/data
networks:
- plandex-network
plandex-server:
image: plandexai/plandex-server:latest
# build:
# context: .
# dockerfile: server/Dockerfile
volumes:
- plandex-files:/plandex-server
ports:
- "8099:8099"
- "4000:4000"
environment:
DATABASE_URL: "postgres://plandex:plandex@plandex-postgres:5432/plandex?sslmode=disable"
GOENV: development
LOCAL_MODE: 1
PLANDEX_BASE_DIR: /plandex-server
OLLAMA_BASE_URL: http://host.docker.internal:11434
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- plandex-network
depends_on:
- plandex-postgres
command: [ "/bin/sh", "-c", "/scripts/wait-for-it.sh plandex-postgres:5432 -- ./plandex-server" ]
networks:
plandex-network:
driver: bridge
volumes:
plandex-db:
plandex-files:
================================================
FILE: app/plans/credits-cmd.txt
================================================
Add a cmd/credits.go file with a command that calls GetOrgSession to get the current org and then if IntegratedModelsMode is true, displays the current credit balance formatted in dollars, to 4 decimal places.
If IntegratedModelsMode isn't true, output that the org isn't using integrated models mode and nothing else.
Display the current balance nicely in a table.
================================================
FILE: app/plans/credits-log-cmd.txt
================================================
I want to add a 'plandex credits log' command to the 'cli/cmd' directory. I want it to show all debits and credits from the credits_transactions table in a nicely formated way.
You will need to add an API handler and route for this endpoint in server/cloud/ and also do the CLI side of the endpoint in cli/types/api.go and cli/api/methods.go.
We will also need a client-side version of the CreditsTransaction type in shared/data_models.go that includes appropriate attributes, as well as a ToApi method for the server-side CreditsTransaction type in server/cloud/types/credits.go
Think through anything else we will need to make this work. Also think about the best way to format and display this data for maximum developer-friendliness.
================================================
FILE: app/plans/json-prompts-to-xml.md
================================================
dI want to update a number of LLM calls to potentially use XML instead of JSON-based tool calls based on their configuration in `AvailableModels` in `shared/ai_models.go`.
Here are the calls I want to update:
- commit message ('genPlanDescription' call)
- exec status ('execStatusShouldContinue' call)
- naming functions - 'GenPlanName', 'GenPipedDataName', and 'GenNoteName'
Look at 'build_exec.go' for an example of how to extract XML from a response.
For each of the LLM calls:
- Before the call, you'll need to check the model config to see if the 'PreferredOutputFormat' field is set to xml/tool call json. We'll need to branch all the logic and prompts based on this.
- Add new prompting to output the same data using xml tags instead of a JSON function call if the model's 'PreferredOutputFormat' field is set to 'Xml'. do not use XML attributes, just simple tags. if there are multiple results in the json schema for the function call, update the prompt to output multiple tags. keep the rest of the prompts exactly the same. You can refactor shared aspects of the prompts between the xml version and the tool call json version.
- Look at the corresponding prompts for the build (in prompt/build.go) and use similar language for outputting xml tags in the xml versions of prompts.
- Update the post LLM call handling to extract the appropriate data using xml tags instead of json.
- Apart from the updated prompts, do not change other parameters in the LLM calls (like model, temperature, etc.)
- I don't want to have any nesting in the xml. the response should just contain multiple tags at the top level if multiple tags are needed. also, it must be clear in all cases that the output should be the content of the tag and not an attribute... brief examples must be included in every prompt as well for the xml versions.
================================================
FILE: app/plans/plan-config.md
================================================
I want to add a plan config feature. It should be loosely modeled
on the plan model settings (often referred to also as just plan settings),
and the 'model packs' feature. I want to store a 'plan_config' JSON field on
the 'plans' table. I also want a 'settings' and a 'set' CLI command.
'settings' shows the current config (like 'models' cmd) and 'set' allows
updating the config just like 'set-model'. the server-side updates can also
work similarly to models and model sets. it shouldn't be added to the file
system or use git at all-it should just be stored in the database. These are *new* commands separate from the 'models' and 'set-model' commands and should have their own files.
For prompting and user input in the 'set' command, use the same approach as the 'set-model' command. Don't introduce any new dependencies.
it should have these properties to start:
AutoApply bool AutoCommit bool AutoContext bool NoExec bool AutoDebug bool AutoDebugTries int
Apart from the plan config, I also want a default user-level config with the
same properties. similar to how there's 'set-model' and 'set-model default'-
this should also have a 'set default' command. again use model settings as a
guide.
On the server side, to keep things neater, create new files for the API and DB handlers rather than including them in the existing plan settings handlers.
On the client side, update the API interface and implementation for the new api calls.
Also update the CLI 'tell', 'continue', 'build', and 'chat' commands to use config settings by default (they can be overridden with the flags that already exist).
Server-side, it needs api handlers, db handlers, and db up/down migration.
Also add request/response types.
For the 'new' command, show the default settings to the user after the plan is created (in a nicely formatted way, similar to the 'settings' command).
Update the CLI help output accordingly. Also add the appropriate command suggestions to the 'settings' and 'set' commands. Also add suggestions to the 'new' command to demonstrate the new config settings commands.
================================================
FILE: app/reset_local.sh
================================================
#!/usr/bin/env bash
# Get the absolute path to the script's directory, regardless of where it's run from
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Change to the app directory if we're not already there
cd "$SCRIPT_DIR"
echo "Clearing local mode..."
./clear_local.sh
echo "Starting local mode..."
./start_local.sh
================================================
FILE: app/scripts/cmd/gen/gen.go
================================================
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"text/template"
)
func main() {
if len(os.Args) < 2 {
log.Fatalf("Usage: %s ", os.Args[0])
}
dirPath := os.Args[1]
dirName := filepath.Base(dirPath)
// Create the main directory
if err := os.MkdirAll(dirPath, 0755); err != nil {
log.Fatalf("Error creating directory: %s", err)
}
f, err := os.Create(fmt.Sprintf("%s/%s", dirPath, "promptfooconfig.yaml"))
if err != nil {
log.Fatalf("Error creating file: %s", err)
}
f.Close()
// Create files inside the directory
files := []string{
"parameters.json",
"config.properties",
"prompt.txt",
}
for _, file := range files {
f, err := os.Create(fmt.Sprintf("%s/%s.%s", dirPath, dirName, file))
if err != nil {
log.Fatalf("Error creating file: %s", err)
}
f.Close()
}
// Create assets and tests directories
subDirs := []string{"assets", "tests"}
for _, subDir := range subDirs {
if err := os.Mkdir(fmt.Sprintf("%s/%s", dirPath, subDir), 0755); err != nil {
log.Fatalf("Error creating subdirectory: %s", err)
}
}
// Template for promptfooconfig.yaml
ymlTemplate := `description: "{{ .Name }}"
prompts:
- file://{{ .Name }}.prompt.txt
providers:
- file://{{ .Name }}.provider.yml
tests: tests/*.tests.yml
`
// Populate promptfooconfig.yaml
promptFooConfigTmpl, err := template.New("yml").Parse(ymlTemplate)
if err != nil {
log.Fatalf("Error creating template: %s", err)
}
// Template for config.properties
propertiesTemplate := `provider_id=openai:gpt-4o
temperature=
max_tokens=
top_p=
response_format=
function_name=
tool_type=function
function_param_type=object
tool_choice_type=function
tool_choice_function_name=
nested_parameters_json={{ .Name }}.parameters.json
`
// Populate config.properties
configPropertiesTmpl, err := template.New("properties").Parse(propertiesTemplate)
if err != nil {
log.Fatalf("Error creating template: %s", err)
}
configFile, err := os.Create(fmt.Sprintf("%s/%s.%s", dirPath, dirName, "config.properties"))
if err != nil {
log.Fatalf("Error creating config.properties: %s", err)
}
defer configFile.Close()
file, err := os.Create(fmt.Sprintf("%s/promptfooconfig.yaml", dirPath))
if err != nil {
log.Fatalf("Error creating promptfooconfig.yaml: %s", err)
}
defer file.Close()
data := struct {
Name string
}{
Name: dirName,
}
if err := promptFooConfigTmpl.Execute(file, data); err != nil {
log.Fatalf("Error executing template: %s", err)
}
if err := configPropertiesTmpl.Execute(configFile, data); err != nil {
log.Fatalf("Error executing template: %s", err)
}
fmt.Println("Directory created successfully!")
fmt.Println("Please check the contents of the directory and proceed with the implementation.")
}
================================================
FILE: app/scripts/cmd/provider/gen_provider.go
================================================
package main
import (
"encoding/json"
"log"
"os"
"path/filepath"
"strings"
"text/template"
)
var testDir = "test/evals/promptfoo-poc"
var templFile = testDir + "/templates/" + "/provider.template.yml"
func main() {
testAbsPath, _ := filepath.Abs(testDir)
templAbsPath, _ := filepath.Abs(templFile)
// Function to walk through directories and find required values
err := filepath.Walk(testAbsPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".properties" {
dirName := filepath.Base(filepath.Dir(path))
outputFileName := filepath.Join(filepath.Dir(path), dirName+".provider.yml")
// Read the template file
templateContent, err := os.ReadFile(templAbsPath)
if err != nil {
log.Fatalf("Error reading template file: %v", err)
}
// Prepare variables (this assumes properties file is a simple key=value format)
variables := map[string]interface{}{}
properties, err := os.ReadFile(path)
if err != nil {
log.Fatalf("Error reading properties file: %v", err)
}
for _, line := range strings.Split(string(properties), "\n") {
if len(line) == 0 {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) > 2 {
log.Fatalf("Invalid line in properties file: %s", line)
}
if len(parts) < 2 {
log.Fatalf("Invalid line in properties file: %s", line)
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if key != "nested_parameters_json" {
variables[key] = value
continue
}
// Read the file path from the nested_parameters_json key
parametersJsonFile := filepath.Join(filepath.Dir(path), value)
jsonParameters, err := os.ReadFile(parametersJsonFile)
if err != nil {
log.Fatalf("Error reading nested parameters JSON file: %v", err)
}
// Parse the JSON string
var nestedParameters map[string]interface{}
// We marshal and unmarshal the JSON to ensure that the nested properties are properly formatted
// for the template, and to ensure that the data is correct json
err = json.Unmarshal(jsonParameters, &nestedParameters)
if err != nil {
log.Fatalf("Error un-marshalling nested parameters JSON: %v", err)
}
parameters, err := json.Marshal(nestedParameters)
if err != nil {
log.Fatalf("Error marshalling nested parameters JSON: %v", err)
}
// Add the nested properties to the variables
variables["parameters"] = string(parameters)
}
// Parse and execute the template
tmpl, err := template.New("yamlTemplate").Parse(string(templateContent))
if err != nil {
log.Fatalf("Error parsing template: %v", err)
}
outputFile, err := os.Create(outputFileName)
if err != nil {
log.Fatalf("Error creating output file: %v", err)
}
defer outputFile.Close()
err = tmpl.Execute(outputFile, variables)
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
log.Printf("Template rendered and saved to '%s'", outputFileName)
}
return nil
})
if err != nil {
log.Fatalf("Error walking the path: %v", err)
}
}
================================================
FILE: app/scripts/dev.sh
================================================
#!/usr/bin/env bash
# Detect zsh and trigger it if its the shell
if [ -n "$ZSH_VERSION" ]; then
# shell is zsh
echo "Detected zsh"
zsh -c "source ~/.zshrc && $*"
fi
# Get the directory of the script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Change to the script directory
cd "$SCRIPT_DIR" || exit 1
# Install Python deps
"$SCRIPT_DIR/litellm_deps.sh"
# Update PATH for python venv
export PATH="$SCRIPT_DIR/../litellm-venv/bin:$PATH"
# Detect if reflex is installed and install it if not
if ! [ -x "$(command -v reflex)" ]; then
# Check if the $GOPATH is empty
if [ -z "$GOPATH" ]; then
echo "Error: GOPATH is not set. Please set it to continue..." >&2
exit 1
fi
echo 'Error: reflex is not installed. Installing it now...' >&2
go install github.com/cespare/reflex@latest
fi
terminate() {
pkill -f 'plandex-server' # Assuming plandex-server is the name of your process
kill -TERM "$pid1" 2>/dev/null
kill -TERM "$pid2" 2>/dev/null
}
trap terminate SIGTERM SIGINT
(cd .. && cd cli && ./dev.sh)
cd ../
export DATABASE_URL=postgres://ds:@localhost/plandex_local?sslmode=disable
export GOENV=development
export LOCAL_MODE=1
reflex -r '^(cli|shared)/.*\.(go|mod|sum)$' -- sh -c 'cd cli && ./dev.sh' &
pid1=$!
reflex -r '^(server|shared)/.*\.(go|mod|sum|py)$' -s -- sh -c 'cd server && go build && ./plandex-server' &
pid2=$!
wait $pid1
wait $pid2
================================================
FILE: app/scripts/litellm_deps.sh
================================================
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$SCRIPT_DIR/../litellm-venv"
REQUIRED_PYTHON="python3"
REQUIRED_PACKAGES=("litellm==1.72.6" "fastapi==0.115.12" "uvicorn==0.34.1" "google-cloud-aiplatform==1.96.0" "boto3==1.38.40" "botocore==1.38.40")
if ! command -v "$REQUIRED_PYTHON" &>/dev/null; then
echo "Python3 not found. Please install it and run this script again."
exit 1
fi
if [ ! -d "$VENV_DIR" ]; then
echo "Creating Python virtual environment at $VENV_DIR..."
"$REQUIRED_PYTHON" -m venv "$VENV_DIR"
fi
source "$VENV_DIR/bin/activate"
is_installed() {
python -c "import pkg_resources; pkg_resources.require('$1')" &>/dev/null
}
for package in "${REQUIRED_PACKAGES[@]}"; do
if ! is_installed "$package"; then
echo "Installing Python package: $package"
pip install "$package"
else
echo "Python package $package already installed"
fi
done
deactivate
================================================
FILE: app/scripts/wait-for-it.sh
================================================
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
# Check if busybox timeout uses -t flag
# (recent Alpine versions don't support -t anymore)
if timeout &>/dev/stdout | grep -q -e '-t '; then
WAITFORIT_BUSYTIMEFLAG="-t"
fi
else
WAITFORIT_ISBUSY=0
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi
================================================
FILE: app/server/.gitignore
================================================
cloud/
================================================
FILE: app/server/Dockerfile
================================================
FROM golang:1.23.3
# Update and install necessary packages including build tools for Tree-sitter
RUN apt-get update && \
apt-get install -y git gcc g++ make python3 python3-venv
# Install Python and create a virtual environment for litellm passthrough
RUN python3 -m venv /opt/venv
# Activate the virtual environment for all following RUN commands
ENV PATH="/opt/venv/bin:$PATH"
# Now install litellm passthrough dependencies in the virtual environment
RUN pip install --no-cache-dir "litellm==1.72.6" "fastapi==0.115.12" "uvicorn==0.34.1" "google-cloud-aiplatform==1.96.0" "boto3==1.38.40" "botocore==1.38.40"
WORKDIR /app
# Copy go.mod and go.sum for shared and server, and install dependencies
COPY ./shared/go.mod ./shared/go.sum ./shared/
RUN cd shared && go mod download
COPY ./server/go.mod ./server/go.sum ./server/
RUN cd server && go mod download
# Copy the actual source code
COPY ./server ./server
COPY ./shared ./shared
COPY ./scripts /scripts
# Set working directory to server
WORKDIR /app/server
# Build the application
RUN rm -f plandex-server && go build -o plandex-server .
# Set the port and expose it
ENV PORT=8099
EXPOSE 8099
# Command to run the executable
CMD ["./plandex-server"]
================================================
FILE: app/server/db/account_helpers.go
================================================
package db
import (
"fmt"
"os"
"github.com/jmoiron/sqlx"
)
type CreateAccountResult struct {
User *User
OrgId string
Token string
}
func CreateAccount(name, email, emailVerificationId string, tx *sqlx.Tx) (*CreateAccountResult, error) {
isLocalMode := (os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1")
// create user
user, err := CreateUser(name, email, tx)
if err != nil {
return nil, fmt.Errorf("error creating user: %v", err)
}
userId := user.Id
domain := user.Domain
// create auth token
token, authTokenId, err := CreateAuthToken(userId, tx)
if err != nil {
return nil, fmt.Errorf("error creating auth token: %v", err)
}
// skipping email verification in local mode
if !isLocalMode {
// update email verification with user and auth token ids
_, err = tx.Exec("UPDATE email_verifications SET user_id = $1, auth_token_id = $2 WHERE id = $3", userId, authTokenId, emailVerificationId)
if err != nil {
return nil, fmt.Errorf("error updating email verification: %v", err)
}
}
// add to org matching domain if one exists and auto add domain users is true for that org
orgId, err := AddToOrgForDomain(domain, userId, tx)
if err != nil {
return nil, fmt.Errorf("error adding user to org for domain: %v", err)
}
return &CreateAccountResult{
User: user,
OrgId: orgId,
Token: token,
}, nil
}
================================================
FILE: app/server/db/ai_model_helpers.go
================================================
package db
================================================
FILE: app/server/db/auth_helpers.go
================================================
package db
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
const tokenExpirationDays = 90 // (trial tokens don't expire)
func CreateAuthToken(userId string, tx *sqlx.Tx) (token, id string, err error) {
uid := uuid.New()
bytes := uid[:]
hashBytes := sha256.Sum256(bytes)
hash := hex.EncodeToString(hashBytes[:])
err = tx.QueryRow("INSERT INTO auth_tokens (user_id, token_hash) VALUES ($1, $2) RETURNING id", userId, hash).Scan(&id)
if err != nil {
return "", "", fmt.Errorf("error creating auth token: %v", err)
}
return uid.String(), id, nil
}
func ValidateAuthToken(token string) (*AuthToken, error) {
uid, err := uuid.Parse(token)
if err != nil {
log.Println("error parsing token", err)
return nil, errors.New("invalid token")
}
bytes := uid[:]
hashBytes := sha256.Sum256(bytes)
tokenHash := hex.EncodeToString(hashBytes[:])
var authToken AuthToken
err = Conn.Get(&authToken, "SELECT * FROM auth_tokens WHERE token_hash = $1 AND created_at > $2 AND deleted_at IS NULL", tokenHash, time.Now().AddDate(0, 0, -tokenExpirationDays))
if err != nil {
if err == sql.ErrNoRows {
log.Println("auth token error - no rows found")
return nil, errors.New("invalid token")
}
return nil, fmt.Errorf("error validating token: %v", err)
}
return &authToken, nil
}
func CreateEmailVerification(email string, userId, pinHash string) error {
var err error
if userId == "" {
_, err = Conn.Exec("INSERT INTO email_verifications (email, pin_hash) VALUES ($1, $2)", email, pinHash)
} else {
_, err = Conn.Exec("INSERT INTO email_verifications (email, pin_hash, user_id) VALUES ($1, $2, $3)", email, pinHash, userId)
}
if err != nil {
return fmt.Errorf("error creating email verification: %v", err)
}
return nil
}
// email verifications expire in 5 minutes
const emailVerificationExpirationMinutes = 5
const InvalidOrExpiredPinError = "invalid or expired pin"
func ValidateEmailVerification(email, pin string) (id string, err error) {
return validateEmailVerification(email, pin, true, true)
}
func ValidateEmailPreviouslyVerified(email, pin string) (id string, err error) {
return validateEmailVerification(email, pin, false, false)
}
func validateEmailVerification(email, pin string, enforceExpiration bool, errOnAlreadyVerified bool) (id string, err error) {
pinHashBytes := sha256.Sum256([]byte(pin))
pinHash := hex.EncodeToString(pinHashBytes[:])
var authTokenId *string
query := `SELECT id, auth_token_id
FROM email_verifications
WHERE pin_hash = $1
AND email = $2`
if enforceExpiration {
query += ` AND created_at > $3`
err = Conn.QueryRow(query, pinHash, email, time.Now().Add(-emailVerificationExpirationMinutes*time.Minute)).Scan(&id, &authTokenId)
} else {
err = Conn.QueryRow(query, pinHash, email).Scan(&id, &authTokenId)
}
if err != nil {
if err == sql.ErrNoRows {
return "", errors.New(InvalidOrExpiredPinError)
}
return "", fmt.Errorf("error validating email verification: %v", err)
}
if authTokenId != nil && errOnAlreadyVerified {
return "", errors.New("pin already verified")
} else if authTokenId == nil && !errOnAlreadyVerified {
return "", errors.New("pin not previously verified")
}
return id, nil
}
func CreateSignInCode(userId, orgId, pinHash string) error {
_, err := Conn.Exec("INSERT INTO sign_in_codes (user_id, org_id, pin_hash) VALUES ($1, $2, $3)", userId, orgId, pinHash)
if err != nil {
return fmt.Errorf("error creating sign in code: %v", err)
}
return nil
}
const signInCodeExpirationMinutes = 5
type ValidateSignInCodeRes struct {
Id string
OrgId string
UserId string
}
func ValidateSignInCode(pin string) (*ValidateSignInCodeRes, error) {
pinHashBytes := sha256.Sum256([]byte(pin))
pinHash := hex.EncodeToString(pinHashBytes[:])
res := &ValidateSignInCodeRes{}
var authTokenId *string
query := `SELECT id, org_id, user_id, auth_token_id FROM sign_in_codes WHERE pin_hash = $1 AND created_at > $2`
err := Conn.QueryRow(query, pinHash, time.Now().Add(-signInCodeExpirationMinutes*time.Minute)).Scan(&res.Id, &res.OrgId, &res.UserId, &authTokenId)
if err != nil {
return nil, fmt.Errorf("error validating sign in code: %v", err)
}
if authTokenId != nil {
return nil, errors.New("sign in code already used")
}
return res, nil
}
func GetUserPermissions(userId, orgId string) ([]string, error) {
var permissions []string
query := `
SELECT p.name, p.resource_id
FROM permissions p
JOIN org_roles_permissions orp ON p.id = orp.permission_id
JOIN orgs_users ou ON orp.org_role_id = ou.org_role_id
WHERE ou.user_id = $1 AND ou.org_id = $2
`
rows, err := Conn.Query(query, userId, orgId)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var permission string
var resourceId sql.NullString
if err := rows.Scan(&permission, &resourceId); err != nil {
return nil, err
}
toAdd := permission
if resourceId.Valid {
toAdd = toAdd + "|" + resourceId.String
}
permissions = append(permissions, toAdd)
}
// Check for errors from iterating over rows.
if err = rows.Err(); err != nil {
return nil, err
}
return permissions, nil
}
================================================
FILE: app/server/db/branch_helpers.go
================================================
package db
import (
"context"
"database/sql"
"fmt"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
func CreateBranch(repo *GitRepo, plan *Plan, parentBranch *Branch, name string, tx *sqlx.Tx) (*Branch, error) {
query := `INSERT INTO branches (org_id, owner_id, plan_id, parent_branch_id, name, status, context_tokens, convo_tokens)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, created_at, updated_at`
var (
contextTokens int
convoTokens int
parentBranchId *string
)
if parentBranch != nil {
parentBranchId = &parentBranch.Id
contextTokens = parentBranch.ContextTokens
convoTokens = parentBranch.ConvoTokens
}
branch := &Branch{
OrgId: plan.OrgId,
OwnerId: plan.OwnerId,
PlanId: plan.Id,
ParentBranchId: parentBranchId,
Name: name,
Status: shared.PlanStatusDraft,
}
var err error
if tx == nil {
err = Conn.QueryRow(
query,
branch.OrgId,
branch.OwnerId,
branch.PlanId,
branch.ParentBranchId,
branch.Name,
branch.Status,
contextTokens,
convoTokens,
).Scan(
&branch.Id,
&branch.CreatedAt,
&branch.UpdatedAt,
)
} else {
err = tx.QueryRow(
query,
branch.OrgId,
branch.OwnerId,
branch.PlanId,
branch.ParentBranchId,
branch.Name,
branch.Status,
contextTokens,
convoTokens,
).Scan(
&branch.Id,
&branch.CreatedAt,
&branch.UpdatedAt,
)
}
if err != nil {
return nil, fmt.Errorf("error creating branch: %v", err)
}
// Create the git branch (except for main, which is created by default on repo init)
if name != "main" {
// parentBranchName := "main"
// if parentBranch != nil {
// parentBranchName = parentBranch.Name
// }
err = repo.GitCreateBranch(name)
if err != nil {
return nil, fmt.Errorf("error creating git branch: %v", err)
}
}
err = IncActiveBranches(plan.Id, 1, tx)
if err != nil {
return nil, fmt.Errorf("error incrementing active branches: %v", err)
}
return branch, nil
}
func GetDbBranch(planId, name string) (*Branch, error) {
var branch Branch
err := Conn.Get(&branch, "SELECT * FROM branches WHERE plan_id = $1 AND name = $2", planId, name)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting branch: %v", err)
}
return &branch, nil
}
func ListPlanBranches(repo *GitRepo, planId string) ([]*Branch, error) {
var branches []*Branch
err := Conn.Select(&branches, "SELECT * FROM branches WHERE plan_id = $1 ORDER BY created_at", planId)
if err != nil {
return nil, fmt.Errorf("error listing branches: %v", err)
}
// log.Println("branches: ", spew.Sdump(branches))
gitBranches, err := repo.GitListBranches()
if err != nil {
return nil, fmt.Errorf("error listing git branches: %v", err)
}
// log.Println("gitBranches: ", spew.Sdump(gitBranches))
var nameSet = make(map[string]bool)
for _, name := range gitBranches {
nameSet[name] = true
}
var res []*Branch
for _, branch := range branches {
if nameSet[branch.Name] {
res = append(res, branch)
}
}
return res, nil
}
func ListBranchesForPlans(orgId string, planIds []string) ([]*Branch, error) {
var branches []*Branch
err := Conn.Select(&branches, "SELECT * FROM branches WHERE plan_id = ANY($1) ORDER BY created_at", pq.Array(planIds))
if err != nil {
return nil, fmt.Errorf("error listing branches: %v", err)
}
return branches, nil
}
func DeleteBranch(ctx context.Context, repo *GitRepo, planId, branch string) error {
return WithTx(ctx, "delete branch", func(tx *sqlx.Tx) error {
_, err := tx.Exec("DELETE FROM branches WHERE plan_id = $1 AND name = $2", planId, branch)
if err != nil {
return fmt.Errorf("error deleting branch: %v", err)
}
err = IncActiveBranches(planId, -1, tx)
if err != nil {
return fmt.Errorf("error decrementing active branches: %v", err)
}
err = repo.GitDeleteBranch(branch)
if err != nil {
return fmt.Errorf("error deleting branch dir: %v", err)
}
return err
})
}
================================================
FILE: app/server/db/build_helpers.go
================================================
package db
import (
"fmt"
"time"
)
func StorePlanBuild(build *PlanBuild) error {
query := `INSERT INTO plan_builds (org_id, plan_id, convo_message_id, file_path) VALUES (:org_id, :plan_id, :convo_message_id, :file_path) RETURNING id, created_at, updated_at`
args := map[string]interface{}{
"org_id": build.OrgId,
"plan_id": build.PlanId,
"convo_message_id": build.ConvoMessageId,
"file_path": build.FilePath,
}
row, err := Conn.NamedQuery(query, args)
if err != nil {
return fmt.Errorf("error storing plan build: %v", err)
}
defer row.Close()
if row.Next() {
var createdAt, updatedAt time.Time
var id string
if err := row.Scan(&id, &createdAt, &updatedAt); err != nil {
return fmt.Errorf("error storing plan build: %v", err)
}
build.Id = id
build.CreatedAt = createdAt
build.UpdatedAt = updatedAt
}
return nil
}
func SetBuildError(build *PlanBuild) error {
_, err := Conn.Exec("UPDATE plan_builds SET error = $1 WHERE id = $2", build.Error, build.Id)
if err != nil {
return fmt.Errorf("error setting build error: %v", err)
}
return nil
}
================================================
FILE: app/server/db/context_helpers_conflicts.go
================================================
package db
import (
"fmt"
"log"
shared "plandex-shared"
"runtime"
"runtime/debug"
)
type invalidateConflictedResultsParams struct {
orgId string
planId string
filesToUpdate map[string]string
descriptions []*ConvoMessageDescription
currentPlan *shared.CurrentPlanState
}
func invalidateConflictedResults(params invalidateConflictedResultsParams) error {
orgId := params.orgId
planId := params.planId
filesToUpdate := params.filesToUpdate
var descriptions []*ConvoMessageDescription
if params.descriptions == nil {
var err error
descriptions, err = GetConvoMessageDescriptions(orgId, planId)
if err != nil {
return fmt.Errorf("error getting pending build descriptions: %v", err)
}
} else {
descriptions = params.descriptions
}
var currentPlan *shared.CurrentPlanState
if params.currentPlan == nil {
var err error
currentPlan, err = GetCurrentPlanState(CurrentPlanStateParams{
OrgId: orgId,
PlanId: planId,
ConvoMessageDescriptions: descriptions,
})
if err != nil {
return fmt.Errorf("error getting current plan state: %v", err)
}
} else {
currentPlan = params.currentPlan
}
conflictPaths := currentPlan.PlanResult.FileResultsByPath.ConflictedPaths(filesToUpdate)
// log.Println("invalidateConflictedResults - Conflicted paths:", conflictPaths)
if len(conflictPaths) > 0 {
toUpdateDescs := []*ConvoMessageDescription{}
for _, desc := range descriptions {
if !desc.DidBuild || desc.AppliedAt != nil {
continue
}
for _, op := range desc.Operations {
if _, found := conflictPaths[op.Path]; found {
if desc.BuildPathsInvalidated == nil {
desc.BuildPathsInvalidated = make(map[string]bool)
}
desc.BuildPathsInvalidated[op.Path] = true
toUpdateDescs = append(toUpdateDescs, desc)
}
}
}
numRoutines := len(toUpdateDescs) + 1
errCh := make(chan error, numRoutines)
for _, desc := range toUpdateDescs {
go func(desc *ConvoMessageDescription) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in StoreDescription: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in StoreDescription: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
err := StoreDescription(desc)
if err != nil {
errCh <- fmt.Errorf("error storing description: %v", err)
return
}
errCh <- nil
}(desc)
}
go func() {
err := DeletePendingResultsForPaths(orgId, planId, conflictPaths)
if err != nil {
errCh <- fmt.Errorf("error deleting pending results: %v", err)
return
}
errCh <- nil
}()
for i := 0; i < numRoutines; i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error storing description: %v", err)
}
}
}
return nil
}
================================================
FILE: app/server/db/context_helpers_get.go
================================================
package db
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"sort"
"strings"
"sync"
)
func GetPlanContexts(orgId, planId string, includeBody, includeMapParts bool) ([]*Context, error) {
var contexts []*Context
contextDir := getPlanContextDir(orgId, planId)
// get all context files
files, err := os.ReadDir(contextDir)
if err != nil {
if os.IsNotExist(err) {
return contexts, nil
}
return nil, fmt.Errorf("error reading context dir: %v", err)
}
errCh := make(chan error, len(files))
var mu sync.Mutex
// read each context file
for _, file := range files {
if strings.HasSuffix(file.Name(), ".meta") {
go func(file os.DirEntry) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetPlanContexts: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetPlanContexts: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
context, err := GetContext(orgId, planId, strings.TrimSuffix(file.Name(), ".meta"), includeBody, includeMapParts)
mu.Lock()
defer mu.Unlock()
contexts = append(contexts, context)
if err != nil {
errCh <- fmt.Errorf("error reading context file: %v", err)
return
}
errCh <- nil
}(file)
} else {
// only processing meta files here, so just send nil for accurate count
errCh <- nil
}
}
for i := 0; i < len(files); i++ {
err := <-errCh
if err != nil {
return nil, fmt.Errorf("error reading context files: %v", err)
}
}
// sort contexts by CreatedAt
sort.Slice(contexts, func(i, j int) bool {
return contexts[i].CreatedAt.Before(contexts[j].CreatedAt)
})
return contexts, nil
}
func GetContext(orgId, planId, contextId string, includeBody, includeMapParts bool) (*Context, error) {
contextDir := getPlanContextDir(orgId, planId)
// read the meta file
metaPath := filepath.Join(contextDir, contextId+".meta")
metaBytes, err := os.ReadFile(metaPath)
if err != nil {
return nil, fmt.Errorf("error reading context meta file: %v", err)
}
var context Context
err = json.Unmarshal(metaBytes, &context)
if err != nil {
return nil, fmt.Errorf("error unmarshalling context meta file: %v", err)
}
if includeBody {
// read the body file
bodyPath := filepath.Join(contextDir, strings.TrimSuffix(contextId, ".meta")+".body")
bodyBytes, err := os.ReadFile(bodyPath)
if err != nil {
return nil, fmt.Errorf("error reading context body file: %v", err)
}
context.Body = string(bodyBytes)
}
if includeMapParts {
// read the map parts file
mapPartsPath := filepath.Join(contextDir, strings.TrimSuffix(contextId, ".meta")+".map-parts")
mapPartsBytes, err := os.ReadFile(mapPartsPath)
if !os.IsNotExist(err) {
if err != nil {
return nil, fmt.Errorf("error reading context map parts file: %v", err)
}
err = json.Unmarshal(mapPartsBytes, &context.MapParts)
if err != nil {
return nil, fmt.Errorf("error unmarshalling context map parts file: %v", err)
}
}
}
return &context, nil
}
================================================
FILE: app/server/db/context_helpers_load.go
================================================
package db
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
shared "plandex-shared"
"runtime"
"runtime/debug"
"strings"
"sync"
"github.com/google/uuid"
)
// Ctx is a context.Context - to avoid confusion with Plandex contexts
type Ctx context.Context
type LoadContextsParams struct {
Req *shared.LoadContextRequest
OrgId string
Plan *Plan
BranchName string
UserId string
SkipConflictInvalidation bool
CachedMapsByPath map[string]*CachedMap
AutoLoaded bool
}
func LoadContexts(ctx Ctx, params LoadContextsParams) (*shared.LoadContextResponse, []*Context, error) {
// startTime := time.Now()
// showElapsed := func(msg string) {
// elapsed := time.Since(startTime)
// log.Println("LoadContexts", msg, "elapsed: %s\n", elapsed)
// }
// log.Println("LoadContexts - params", spew.Sdump(params))
req := params.Req
orgId := params.OrgId
plan := params.Plan
planId := plan.Id
branchName := params.BranchName
userId := params.UserId
autoLoaded := params.AutoLoaded
filesToLoad := map[string]string{}
for _, context := range *req {
if context.ContextType == shared.ContextFileType {
filesToLoad[context.FilePath] = context.Body
}
}
if !params.SkipConflictInvalidation {
err := invalidateConflictedResults(invalidateConflictedResultsParams{
orgId: orgId,
planId: planId,
filesToUpdate: filesToLoad,
})
if err != nil {
return nil, nil, fmt.Errorf("error invalidating conflicted results: %v", err)
}
}
tokensAdded := 0
basicTokensAdded := 0
paramsByTempId := make(map[string]*shared.LoadContextParams)
numTokensByTempId := make(map[string]int)
branch, err := GetDbBranch(planId, branchName)
if err != nil {
return nil, nil, fmt.Errorf("error getting branch: %v", err)
}
totalTokens := branch.ContextTokens
totalPlannerTokens := totalTokens
totalBasicPlannerTokens := 0
totalMapTokens := 0
settings, err := GetPlanSettings(plan)
if err != nil {
return nil, nil, fmt.Errorf("error getting settings: %v", err)
}
planConfig, err := GetPlanConfig(planId)
if err != nil {
return nil, nil, fmt.Errorf("error getting plan config: %v", err)
}
plannerMaxTokens := settings.GetPlannerEffectiveMaxTokens()
contextLoaderMaxTokens := settings.GetArchitectEffectiveMaxTokens()
mapContextsByFilePath := make(map[string]Context)
existingContexts, err := GetPlanContexts(orgId, planId, false, false)
if err != nil {
return nil, nil, fmt.Errorf("error getting existing contexts: %v", err)
}
// check overall context limits - these should be getting enforced by the client, so just error out if exceeded
numExistingContexts := len(existingContexts)
if numExistingContexts+len(*req) > shared.MaxContextCount {
return nil, nil, fmt.Errorf("too many contexts: %d", numExistingContexts+len(*req))
}
var totalContextSize int64
for _, context := range existingContexts {
totalContextSize += context.BodySize
}
for _, context := range *req {
size := int64(len(context.Body))
totalContextSize += size
if size > shared.MaxContextBodySize {
return nil, nil, fmt.Errorf("context body is too large: %d", size)
}
}
if totalContextSize > shared.MaxTotalContextSize {
return nil, nil, fmt.Errorf("total context size is too large: %d", totalContextSize)
}
existingContextsByName := make(map[string]bool)
for _, context := range existingContexts {
composite := strings.Join([]string{context.Name, string(context.ContextType)}, "|")
existingContextsByName[composite] = true
if planConfig.AutoLoadContext && context.ContextType == shared.ContextMapType {
totalMapTokens += context.NumTokens
totalPlannerTokens -= context.NumTokens
}
if !context.AutoLoaded && context.ContextType != shared.ContextMapType {
totalBasicPlannerTokens += context.NumTokens
}
}
var filteredReq []*shared.LoadContextParams
for _, context := range *req {
composite := strings.Join([]string{context.Name, string(context.ContextType)}, "|")
if !existingContextsByName[composite] {
filteredReq = append(filteredReq, context)
}
}
*req = filteredReq
for _, contextParams := range *req {
tempId := uuid.New().String()
var numTokens int
var err error
var isMap bool
if contextParams.ContextType == shared.ContextMapType && (len(contextParams.MapBodies) > 0 || params.CachedMapsByPath != nil) {
isMap = true
var mappedFiles shared.FileMapBodies
if params.CachedMapsByPath != nil && params.CachedMapsByPath[contextParams.FilePath] != nil {
log.Println("Using cached map for", contextParams.FilePath)
mappedFiles = params.CachedMapsByPath[contextParams.FilePath].MapParts
} else {
log.Println("Using map bodies for", contextParams.FilePath)
mappedFiles = contextParams.MapBodies
// check size and num path limits - these should be getting enforced by the client, so just error out if exceeded
if len(mappedFiles) > shared.MaxContextMapPaths {
return nil, nil, fmt.Errorf("map has too many paths: %d", len(mappedFiles))
}
totalMapSize := 0
for _, body := range mappedFiles {
numBytes := len(body)
totalMapSize += numBytes
if numBytes > shared.MaxContextMapSingleInputSize {
return nil, nil, fmt.Errorf("map input %s is too large: %d", contextParams.FilePath, numBytes)
}
}
if totalMapSize > shared.MaxContextBodySize {
return nil, nil, fmt.Errorf("map is too large: %d", totalMapSize)
}
}
var mapShas map[string]string
var mapTokens map[string]int
var mapSizes map[string]int64
if params.CachedMapsByPath != nil && params.CachedMapsByPath[contextParams.FilePath] != nil {
mapShas = params.CachedMapsByPath[contextParams.FilePath].MapShas
mapTokens = params.CachedMapsByPath[contextParams.FilePath].MapTokens
mapSizes = params.CachedMapsByPath[contextParams.FilePath].MapSizes
} else {
mapShas = contextParams.InputShas
mapTokens = contextParams.InputTokens
mapSizes = contextParams.InputSizes
}
combinedBody := mappedFiles.CombinedMap(mapTokens)
numTokens = shared.GetNumTokensEstimate(combinedBody)
autoLoaded = autoLoaded || contextParams.AutoLoaded
log.Println("LoadContexts - map - autoLoaded", autoLoaded)
newContext := Context{
// Id generated by db layer
OrgId: orgId,
OwnerId: userId,
PlanId: planId,
ProjectId: plan.ProjectId,
ContextType: shared.ContextMapType,
Name: contextParams.Name,
Url: contextParams.Url,
FilePath: contextParams.FilePath,
NumTokens: numTokens,
Body: combinedBody,
MapParts: mappedFiles,
MapShas: mapShas,
MapTokens: mapTokens,
MapSizes: mapSizes,
AutoLoaded: autoLoaded || contextParams.AutoLoaded,
}
mapContextsByFilePath[contextParams.FilePath] = newContext
} else if contextParams.ContextType == shared.ContextImageType {
numTokens, err = shared.GetImageTokens(contextParams.Body, contextParams.ImageDetail)
if err != nil {
return nil, nil, fmt.Errorf("error getting image num tokens: %v", err)
}
} else {
numTokens = shared.GetNumTokensEstimate(contextParams.Body)
}
paramsByTempId[tempId] = contextParams
numTokensByTempId[tempId] = numTokens
totalTokens += numTokens
// maps don't count toward the token limit if auto-loading
if planConfig.AutoLoadContext && isMap {
tokensAdded += numTokens
totalMapTokens += numTokens
} else if autoLoaded {
tokensAdded += numTokens
totalPlannerTokens += numTokens
} else {
tokensAdded += numTokens
totalPlannerTokens += numTokens
totalBasicPlannerTokens += numTokens
basicTokensAdded += numTokens
}
}
// showElapsed("Loaded reqs")
if planConfig.AutoLoadContext {
if totalMapTokens > contextLoaderMaxTokens {
return &shared.LoadContextResponse{
TokensAdded: tokensAdded,
TotalTokens: totalMapTokens,
MaxTokens: contextLoaderMaxTokens,
MaxTokensExceeded: true,
}, nil, nil
}
if totalBasicPlannerTokens > plannerMaxTokens {
return &shared.LoadContextResponse{
TokensAdded: basicTokensAdded,
TotalTokens: totalBasicPlannerTokens,
MaxTokens: plannerMaxTokens,
MaxTokensExceeded: true,
}, nil, nil
}
} else {
if totalTokens > plannerMaxTokens {
return &shared.LoadContextResponse{
TokensAdded: tokensAdded,
TotalTokens: totalTokens,
MaxTokens: plannerMaxTokens,
MaxTokensExceeded: true,
}, nil, nil
}
}
var dbContexts []*Context
var apiContexts []*shared.Context
var mu sync.Mutex
errCh := make(chan error, len(paramsByTempId))
for tempId, loadParams := range paramsByTempId {
go func(tempId string, loadParams *shared.LoadContextParams) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in LoadContexts: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in LoadContexts: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
hash := sha256.Sum256([]byte(loadParams.Body))
sha := hex.EncodeToString(hash[:])
var context Context
if mapContext, ok := mapContextsByFilePath[loadParams.FilePath]; ok {
context = mapContext
} else {
// log.Println("tempId", tempId, "params.FilePath", params.FilePath, "sha", sha)
// log.Println("params.Body", params.Body)
context = Context{
// Id generated by db layer
OrgId: orgId,
OwnerId: userId,
PlanId: planId,
ProjectId: plan.ProjectId,
ContextType: loadParams.ContextType,
Name: loadParams.Name,
Url: loadParams.Url,
FilePath: loadParams.FilePath,
NumTokens: numTokensByTempId[tempId],
Sha: sha,
Body: loadParams.Body,
ForceSkipIgnore: loadParams.ForceSkipIgnore,
ImageDetail: loadParams.ImageDetail,
AutoLoaded: autoLoaded || loadParams.AutoLoaded,
}
}
err := StoreContext(&context, params.CachedMapsByPath != nil)
if err != nil {
errCh <- fmt.Errorf("error storing context: %v", err)
return
}
mu.Lock()
dbContexts = append(dbContexts, &context)
apiContext := context.ToApi()
apiContext.Body = ""
apiContexts = append(apiContexts, apiContext)
mu.Unlock()
errCh <- nil
}(tempId, loadParams)
}
for i := 0; i < len(paramsByTempId); i++ {
err := <-errCh
if err != nil {
return nil, nil, fmt.Errorf("error storing context: %v", err)
}
}
err = AddPlanContextTokens(planId, branchName, tokensAdded)
if err != nil {
return nil, nil, fmt.Errorf("error adding plan context tokens: %v", err)
}
commitMsg := shared.SummaryForLoadContext(apiContexts, tokensAdded, totalTokens)
if len(apiContexts) > 0 {
commitMsg += "\n\n" + shared.TableForLoadContext(apiContexts, false)
}
return &shared.LoadContextResponse{
TokensAdded: tokensAdded,
TotalTokens: totalTokens,
Msg: commitMsg,
}, dbContexts, nil
}
================================================
FILE: app/server/db/context_helpers_map.go
================================================
package db
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
shared "plandex-shared"
)
func GetCachedMap(orgId, projectId, filePath string) (*Context, error) {
mapCacheDir := getProjectMapCacheDir(orgId, projectId)
filePathHash := md5.Sum([]byte(filePath))
filePathHashStr := hex.EncodeToString(filePathHash[:])
mapCachePath := filepath.Join(mapCacheDir, filePathHashStr+".json")
log.Println("GetCachedMap - mapCachePath", mapCachePath)
mapCacheBytes, err := os.ReadFile(mapCachePath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("error reading cached map: %v", err)
}
var context Context
err = json.Unmarshal(mapCacheBytes, &context)
if err != nil {
return nil, fmt.Errorf("error unmarshalling cached map: %v", err)
}
return &context, nil
}
type CachedMap struct {
MapParts shared.FileMapBodies
MapShas map[string]string
MapTokens map[string]int
MapSizes map[string]int64
}
================================================
FILE: app/server/db/context_helpers_remove.go
================================================
package db
import (
"fmt"
"log"
"os"
"path/filepath"
shared "plandex-shared"
"runtime"
"runtime/debug"
)
func ContextRemove(orgId, planId string, contexts []*Context) error {
return contextRemove(contextRemoveParams{
orgId: orgId,
planId: planId,
contexts: contexts,
})
}
type contextRemoveParams struct {
orgId string
planId string
contexts []*Context
descriptions []*ConvoMessageDescription
currentPlan *shared.CurrentPlanState
}
func contextRemove(params contextRemoveParams) error {
orgId := params.orgId
planId := params.planId
contexts := params.contexts
// remove files
numFiles := 0
filesToUpdate := make(map[string]string)
errCh := make(chan error, numFiles)
for _, context := range contexts {
filesToUpdate[context.FilePath] = ""
contextDir := getPlanContextDir(orgId, planId)
for _, ext := range []string{".meta", ".body", ".map-parts"} {
numFiles++
go func(context *Context, dir, ext string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in contextRemove: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in contextRemove: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
errCh <- os.Remove(filepath.Join(dir, context.Id+ext))
}(context, contextDir, ext)
}
}
for i := 0; i < numFiles; i++ {
err := <-errCh
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error removing context file: %v", err)
}
}
err := invalidateConflictedResults(invalidateConflictedResultsParams{
orgId: orgId,
planId: planId,
filesToUpdate: filesToUpdate,
descriptions: params.descriptions,
currentPlan: params.currentPlan,
})
if err != nil {
return fmt.Errorf("error invalidating conflicted results: %v", err)
}
return nil
}
type ClearContextParams struct {
OrgId string
PlanId string
SkipMaps bool
SkipPending bool
}
func ClearContext(params ClearContextParams) error {
orgId := params.OrgId
planId := params.PlanId
skipMaps := params.SkipMaps
skipPending := params.SkipPending
contexts, err := GetPlanContexts(orgId, planId, false, false)
if err != nil {
return fmt.Errorf("error getting plan contexts: %v", err)
}
var descriptions []*ConvoMessageDescription
var currentPlan *shared.CurrentPlanState
if skipPending {
var err error
descriptions, err = GetConvoMessageDescriptions(orgId, planId)
if err != nil {
return fmt.Errorf("error getting pending build descriptions: %v", err)
}
currentPlan, err = GetCurrentPlanState(CurrentPlanStateParams{
OrgId: orgId,
PlanId: planId,
ConvoMessageDescriptions: descriptions,
})
if err != nil {
return fmt.Errorf("error getting current plan state: %v", err)
}
}
toRemove := []*Context{}
for _, context := range contexts {
shouldSkip := false
if !(skipMaps && context.ContextType == shared.ContextMapType) {
shouldSkip = true
}
if skipPending && currentPlan.CurrentPlanFiles.Files[context.FilePath] != "" {
shouldSkip = true
}
if !shouldSkip {
toRemove = append(toRemove, context)
}
}
if len(toRemove) > 0 {
err := contextRemove(contextRemoveParams{
orgId: orgId,
planId: planId,
contexts: toRemove,
descriptions: descriptions,
currentPlan: currentPlan,
})
if err != nil {
return fmt.Errorf("error removing non-map contexts: %v", err)
}
}
return nil
}
================================================
FILE: app/server/db/context_helpers_store.go
================================================
package db
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
shared "plandex-shared"
"github.com/google/uuid"
)
func StoreContext(context *Context, skipMapCache bool) error {
// log.Println("StoreContext - Storing context", context.Id, context.Name, context.ContextType)
// log.Println("StoreContext - Num tokens", context.NumTokens)
contextDir := getPlanContextDir(context.OrgId, context.PlanId)
err := os.MkdirAll(contextDir, os.ModePerm)
if err != nil {
return fmt.Errorf("error creating context dir: %v", err)
}
ts := time.Now().UTC()
if context.Id == "" {
context.Id = uuid.New().String()
context.CreatedAt = ts
}
context.UpdatedAt = ts
context.BodySize = int64(len(context.Body))
metaFilename := context.Id + ".meta"
metaPath := filepath.Join(contextDir, metaFilename)
originalBody := context.Body
originalBody = strings.ReplaceAll(originalBody, "\\`\\`\\`", "\\\\`\\\\`\\\\`")
originalBody = strings.ReplaceAll(originalBody, "```", "\\`\\`\\`")
bodyFilename := context.Id + ".body"
bodyPath := filepath.Join(contextDir, bodyFilename)
body := []byte(originalBody)
context.Body = ""
originalMapParts := context.MapParts
var mapPath string
var mapBytes []byte
if len(context.MapParts) > 0 {
mapFilename := context.Id + ".map-parts"
mapPath = filepath.Join(contextDir, mapFilename)
mapBytes, err = json.MarshalIndent(context.MapParts, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal map parts: %v", err)
}
context.MapParts = nil
}
// Convert the ModelContextPart to JSON
data, err := json.MarshalIndent(context, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal context context: %v", err)
}
// Write the body to the file
if err = os.WriteFile(bodyPath, body, 0644); err != nil {
return fmt.Errorf("failed to write context body to file %s: %v", bodyPath, err)
}
// Write the meta data to the file
if err = os.WriteFile(metaPath, data, 0644); err != nil {
return fmt.Errorf("failed to write context meta to file %s: %v", metaPath, err)
}
if mapPath != "" {
if err = os.WriteFile(mapPath, mapBytes, 0644); err != nil {
return fmt.Errorf("failed to write context map to file %s: %v", mapPath, err)
}
}
context.Body = originalBody
context.MapParts = originalMapParts
if mapPath != "" && !skipMapCache {
log.Println("StoreContext - context.MapParts length", len(context.MapParts))
mapCacheDir := getProjectMapCacheDir(context.OrgId, context.ProjectId)
// ensure map cache dir exists
err = os.MkdirAll(mapCacheDir, os.ModePerm)
if err != nil {
return fmt.Errorf("error creating map cache dir: %v", err)
}
filePathHash := md5.Sum([]byte(context.FilePath))
filePathHashStr := hex.EncodeToString(filePathHash[:])
mapCachePath := filepath.Join(mapCacheDir, filePathHashStr+".json")
log.Println("StoreContext - mapCachePath", mapCachePath)
cachedContext := Context{
ContextType: shared.ContextMapType,
FilePath: context.FilePath,
Name: context.Name,
Body: context.Body,
NumTokens: context.NumTokens,
MapParts: context.MapParts,
MapShas: context.MapShas,
MapTokens: context.MapTokens,
MapSizes: context.MapSizes,
UpdatedAt: context.UpdatedAt,
}
cachedContextBytes, err := json.MarshalIndent(cachedContext, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal cached context: %v", err)
}
err = os.WriteFile(mapCachePath, cachedContextBytes, 0644)
if err != nil {
return fmt.Errorf("failed to write context map to file %s: %v", mapCachePath, err)
}
}
return nil
}
================================================
FILE: app/server/db/context_helpers_update.go
================================================
package db
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
shared "plandex-shared"
"runtime"
"runtime/debug"
"sync"
)
type UpdateContextsParams struct {
Req *shared.UpdateContextRequest
OrgId string
Plan *Plan
BranchName string
ContextsById map[string]*Context
SkipConflictInvalidation bool
}
func UpdateContexts(params UpdateContextsParams) (*shared.UpdateContextResponse, error) {
req := params.Req
orgId := params.OrgId
plan := params.Plan
planId := plan.Id
branchName := params.BranchName
branch, err := GetDbBranch(planId, branchName)
if err != nil {
return nil, fmt.Errorf("error getting branch: %v", err)
}
if branch == nil {
return nil, fmt.Errorf("branch not found")
}
totalTokens := branch.ContextTokens
totalPlannerTokens := totalTokens
totalMapTokens := 0
totalBasicPlannerTokens := 0
for _, context := range params.ContextsById {
if context.ContextType != shared.ContextMapType && !context.AutoLoaded {
totalBasicPlannerTokens += context.NumTokens
}
}
settings, err := GetPlanSettings(plan)
if err != nil {
return nil, fmt.Errorf("error getting settings: %v", err)
}
planConfig, err := GetPlanConfig(planId)
if err != nil {
return nil, fmt.Errorf("error getting plan config: %v", err)
}
modelPacks, err := ListModelPacks(orgId)
if err != nil {
return nil, fmt.Errorf("error getting model packs: %v", err)
}
apiModelPacks := make([]*shared.ModelPack, len(modelPacks))
for i, modelPack := range modelPacks {
apiModelPacks[i] = modelPack.ToApi()
}
plannerMaxTokens := settings.GetPlannerEffectiveMaxTokens()
contextLoaderMaxTokens := settings.GetArchitectEffectiveMaxTokens()
if planConfig.AutoLoadContext {
existingContexts, err := GetPlanContexts(orgId, planId, false, false)
if err != nil {
return nil, fmt.Errorf("error getting existing contexts: %v", err)
}
for _, context := range existingContexts {
if context.ContextType == shared.ContextMapType {
totalMapTokens += context.NumTokens
totalPlannerTokens -= context.NumTokens
}
}
}
aggregateTokensDiff := 0
aggregateBasicTokensDiff := 0
tokenDiffsById := make(map[string]int)
var contextsById map[string]*Context
if params.ContextsById == nil {
contextsById = make(map[string]*Context)
} else {
contextsById = params.ContextsById
}
var totalContextCount int
var totalBodySize int64
for _, context := range contextsById {
totalContextCount++
totalBodySize += context.BodySize
}
for id, params := range *req {
size := int64(len(params.Body))
if size > shared.MaxContextBodySize {
return nil, fmt.Errorf("context body is too large: %d", size)
}
if context, ok := contextsById[id]; ok {
totalBodySize += size - context.BodySize
} else {
totalContextCount++
totalBodySize += size
}
}
if totalContextCount > shared.MaxContextCount {
return nil, fmt.Errorf("too many contexts to update (found %d, limit is %d)", totalContextCount, shared.MaxContextCount)
}
if totalBodySize > shared.MaxContextBodySize {
return nil, fmt.Errorf("total context body size exceeds limit (size %.2f MB, limit %d MB)", float64(totalBodySize)/1024/1024, int(shared.MaxContextBodySize)/1024/1024)
}
var updatedContexts []*shared.Context
numFiles := 0
numUrls := 0
numTrees := 0
numMaps := 0
var mu sync.Mutex
errCh := make(chan error, len(*req))
for id, params := range *req {
go func(id string, params *shared.UpdateContextParams) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in UpdateContexts: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in UpdateContexts: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var context *Context
if _, ok := contextsById[id]; ok {
context = contextsById[id]
} else {
var err error
context, err = GetContext(orgId, planId, id, true, true)
if err != nil {
errCh <- fmt.Errorf("error getting context: %v", err)
return
}
// log.Println("Got context", context.Id, "numTokens", context.NumTokens)
}
mu.Lock()
defer mu.Unlock()
contextsById[id] = context
updatedContexts = append(updatedContexts, context.ToApi())
if context.ContextType != shared.ContextMapType {
var updateNumTokens int
var err error
if context.ContextType == shared.ContextImageType {
updateNumTokens, err = shared.GetImageTokens(params.Body, context.ImageDetail)
if err != nil {
errCh <- fmt.Errorf("error getting num tokens: %v", err)
return
}
} else {
updateNumTokens = shared.GetNumTokensEstimate(params.Body)
// log.Println("len(params.Body)", len(params.Body))
}
// log.Println("Updating context", id, "updateNumTokens", updateNumTokens)
tokenDiff := updateNumTokens - context.NumTokens
tokenDiffsById[id] = tokenDiff
aggregateTokensDiff += tokenDiff
totalTokens += tokenDiff
totalPlannerTokens += tokenDiff
if !context.AutoLoaded {
totalBasicPlannerTokens += tokenDiff
aggregateBasicTokensDiff += tokenDiff
}
context.NumTokens = updateNumTokens
}
switch context.ContextType {
case shared.ContextFileType:
numFiles++
case shared.ContextURLType:
numUrls++
case shared.ContextDirectoryTreeType:
numTrees++
case shared.ContextMapType:
numMaps++
}
errCh <- nil
}(id, params)
}
for i := 0; i < len(*req); i++ {
err := <-errCh
if err != nil {
return nil, fmt.Errorf("error getting context: %v", err)
}
}
if planConfig.AutoLoadContext {
if totalBasicPlannerTokens > plannerMaxTokens {
return &shared.UpdateContextResponse{
TokensAdded: aggregateTokensDiff,
TotalTokens: totalBasicPlannerTokens,
MaxTokens: plannerMaxTokens,
MaxTokensExceeded: true,
}, nil
}
}
filesToLoad := map[string]string{}
for _, context := range updatedContexts {
if context.ContextType == shared.ContextFileType {
filesToLoad[context.FilePath] = (*req)[context.Id].Body
}
}
if !params.SkipConflictInvalidation {
err = invalidateConflictedResults(invalidateConflictedResultsParams{
orgId: orgId,
planId: planId,
filesToUpdate: filesToLoad,
})
if err != nil {
return nil, fmt.Errorf("error invalidating conflicted results: %v", err)
}
}
errCh = make(chan error, len(*req))
for id, params := range *req {
go func(id string, params *shared.UpdateContextParams) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in UpdateContexts: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in UpdateContexts: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
context := contextsById[id]
if context.ContextType == shared.ContextMapType {
oldNumTokens := context.NumTokens
for path, part := range params.MapBodies {
if len(part) > shared.MaxContextMapSingleInputSize {
errCh <- fmt.Errorf("map input %s is too large: %d", path, len(part))
return
}
if context.MapParts == nil {
context.MapParts = make(shared.FileMapBodies)
}
if context.MapShas == nil {
context.MapShas = make(map[string]string)
}
if context.MapTokens == nil {
context.MapTokens = make(map[string]int)
}
if context.MapSizes == nil {
context.MapSizes = make(map[string]int64)
}
// prevNumTokens := context.MapTokens[path]
context.MapParts[path] = part
context.MapShas[path] = params.InputShas[path]
context.MapTokens[path] = params.InputTokens[path]
context.MapSizes[path] = params.InputSizes[path]
}
for _, path := range params.RemovedMapPaths {
delete(context.MapParts, path)
delete(context.MapShas, path)
delete(context.MapTokens, path)
delete(context.MapSizes, path)
}
if len(context.MapParts) > shared.MaxContextMapPaths {
errCh <- fmt.Errorf("map has too many paths: %d", len(context.MapParts))
return
}
totalMapSize := 0
for _, part := range context.MapParts {
totalMapSize += len(part)
}
if totalMapSize > shared.MaxContextBodySize {
errCh <- fmt.Errorf("map total size is too large: %d", totalMapSize)
return
}
context.Body = context.MapParts.CombinedMap(context.MapTokens)
newNumTokens := shared.GetNumTokensEstimate(context.Body)
tokenDiff := newNumTokens - oldNumTokens
mu.Lock()
tokenDiffsById[id] = tokenDiff
aggregateTokensDiff += tokenDiff
totalTokens += tokenDiff
if planConfig.AutoLoadContext {
totalMapTokens += tokenDiff
} else {
totalPlannerTokens += tokenDiff
}
mu.Unlock()
context.NumTokens = newNumTokens
} else {
context.Body = params.Body
hash := sha256.Sum256([]byte(context.Body))
context.Sha = hex.EncodeToString(hash[:])
}
// log.Println("storing context", id)
// log.Printf("context name: %s, sha: %s\n", context.Name, context.Sha)
err := StoreContext(context, false)
if err != nil {
errCh <- fmt.Errorf("error storing context: %v", err)
return
}
// log.Println("stored context", id)
// log.Println()
errCh <- nil
}(id, params)
}
for i := 0; i < len(*req); i++ {
err := <-errCh
if err != nil {
return nil, fmt.Errorf("error storing context: %v", err)
}
}
if planConfig.AutoLoadContext {
if totalMapTokens > contextLoaderMaxTokens {
return &shared.UpdateContextResponse{
TokensAdded: aggregateTokensDiff,
TotalTokens: totalTokens,
MaxTokens: contextLoaderMaxTokens,
MaxTokensExceeded: true,
}, nil
}
}
updateRes := &shared.ContextUpdateResult{
UpdatedContexts: updatedContexts,
TokenDiffsById: tokenDiffsById,
TokensDiff: aggregateTokensDiff,
TotalTokens: totalTokens,
NumFiles: numFiles,
NumUrls: numUrls,
NumTrees: numTrees,
NumMaps: numMaps,
MaxTokens: plannerMaxTokens,
}
err = AddPlanContextTokens(planId, branchName, aggregateTokensDiff)
if err != nil {
return nil, fmt.Errorf("error adding plan context tokens: %v", err)
}
commitMsg := shared.SummaryForUpdateContext(shared.SummaryForUpdateContextParams{
NumFiles: numFiles,
NumTrees: numTrees,
NumUrls: numUrls,
NumMaps: numMaps,
TokensDiff: aggregateTokensDiff,
TotalTokens: totalTokens,
}) + "\n\n" + shared.TableForContextUpdate(updateRes)
return &shared.LoadContextResponse{
TokensAdded: aggregateTokensDiff,
TotalTokens: totalTokens,
Msg: commitMsg,
}, nil
}
================================================
FILE: app/server/db/convo_helpers.go
================================================
package db
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"sort"
"strings"
"time"
shared "plandex-shared"
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/sashabaranov/go-openai"
)
func GetPlanConvo(orgId, planId string) ([]*ConvoMessage, error) {
var convo []*ConvoMessage
convoDir := getPlanConversationDir(orgId, planId)
files, err := os.ReadDir(convoDir)
if err != nil {
if os.IsNotExist(err) {
return convo, nil
}
return nil, fmt.Errorf("error reading convo dir: %v", err)
}
errCh := make(chan error, len(files))
convoCh := make(chan *ConvoMessage, len(files))
for _, file := range files {
go func(file os.DirEntry) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetPlanConvo: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetPlanConvo: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
bytes, err := os.ReadFile(filepath.Join(convoDir, file.Name()))
if err != nil {
errCh <- fmt.Errorf("error reading convo file: %v", err)
return
}
var convoMessage ConvoMessage
err = json.Unmarshal(bytes, &convoMessage)
if err != nil {
errCh <- fmt.Errorf("error unmarshalling convo file: %v", err)
return
}
convoCh <- &convoMessage
}(file)
}
for i := 0; i < len(files); i++ {
select {
case err := <-errCh:
return nil, fmt.Errorf("error reading convo files: %v", err)
case convoMessage := <-convoCh:
convo = append(convo, convoMessage)
}
}
sort.Slice(convo, func(i, j int) bool {
return convo[i].CreatedAt.Before(convo[j].CreatedAt)
})
return convo, nil
}
func GetConvoMessage(orgId, planId, messageId string) (*ConvoMessage, error) {
convoDir := getPlanConversationDir(orgId, planId)
filePath := filepath.Join(convoDir, messageId+".json")
bytes, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("error reading convo message: %v", err)
}
var convoMessage ConvoMessage
err = json.Unmarshal(bytes, &convoMessage)
if err != nil {
return nil, fmt.Errorf("error unmarshalling convo message: %v", err)
}
return &convoMessage, nil
}
func StoreConvoMessage(repo *GitRepo, message *ConvoMessage, currentUserId, branch string, commit bool) (string, error) {
convoDir := getPlanConversationDir(message.OrgId, message.PlanId)
ts := time.Now().UTC()
if message.Id == "" {
message.Id = uuid.New().String()
}
message.CreatedAt = ts
bytes, err := json.Marshal(message)
if err != nil {
return "", fmt.Errorf("error marshalling convo message: %v", err)
}
err = os.MkdirAll(convoDir, os.ModePerm)
if err != nil {
return "", fmt.Errorf("error creating convo dir: %v", err)
}
err = os.WriteFile(filepath.Join(convoDir, message.Id+".json"), bytes, os.ModePerm)
if err != nil {
return "", fmt.Errorf("error writing convo message: %v", err)
}
err = AddPlanConvoMessage(message, branch)
if err != nil {
return "", fmt.Errorf("error adding convo tokens: %v", err)
}
var desc string
if message.Role == openai.ChatMessageRoleUser {
desc = "💬 User prompt"
// TODO: add user name
} else {
desc = "🤖 Plandex reply"
if message.Stopped {
desc += " | 🛑 " + color.New(color.FgHiRed).Sprint("stopped")
}
}
replyTags := message.Flags.GetReplyTags()
var msg string
if len(replyTags) > 0 {
msg = fmt.Sprintf("Message #%d | %s | %s | %d 🪙", message.Num, desc, strings.Join(replyTags, " | "), message.Tokens)
} else {
msg = fmt.Sprintf("Message #%d | %s | %d 🪙", message.Num, desc, message.Tokens)
}
if len(message.AddedSubtasks) > 0 {
msg += "\n\n"
for _, subtask := range message.AddedSubtasks {
msg += "\n• " + subtask.Title
}
}
if len(message.RemovedSubtasks) > 0 {
msg += "\n\n"
msg += "Removed Tasks"
for _, subtask := range message.RemovedSubtasks {
msg += "\n• " + subtask
}
}
log.Println("StoreConvoMessage - message.Flags.CurrentStage.TellStage:", message.Flags.CurrentStage.TellStage)
log.Println("StoreConvoMessage - message.Subtask:", message.Subtask)
if message.Flags.CurrentStage.TellStage == shared.TellStageImplementation && message.Subtask != nil {
msg += "\n\n" + "📋 " + message.Subtask.Title
if len(message.Subtask.UsesFiles) > 0 {
for _, file := range message.Subtask.UsesFiles {
msg += "\n • 📄 " + file
}
}
}
if message.Flags.DidCompletePlan {
msg += "\n\n" + "🏁 Completed Plan"
}
// Cleaner without the cut off message - maybe need a separate command to show both the log and full messages?
// cutoff := 140
// if len(message.Message) > cutoff {
// msg += "\n\n" + message.Message[:cutoff] + "..."
// } else {
// msg += "\n\n" + message.Message
// }
if commit {
log.Printf("[Git] StoreConvoMessage - committing convo message: %s, branch: %s", msg, branch)
err = repo.GitAddAndCommit(branch, msg)
if err != nil {
return "", fmt.Errorf("error committing convo message: %v", err)
}
}
return msg, nil
}
================================================
FILE: app/server/db/data_models.go
================================================
package db
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
// The models below should only be used server-side.
// Many of them have corresponding models in shared/api for client-side use.
// This adds some duplication, but helps ensure that server-only data doesn't leak to the client.
// Models used client-side have a ToApi() method to convert it to the corresponding client-side model.
type AuthToken struct {
Id string `db:"id"`
UserId string `db:"user_id"`
TokenHash string `db:"token_hash"`
CreatedAt time.Time `db:"created_at"`
DeletedAt *time.Time `db:"deleted_at"`
}
type Org struct {
Id string `db:"id"`
Name string `db:"name"`
Domain *string `db:"domain"`
AutoAddDomainUsers bool `db:"auto_add_domain_users"`
OwnerId string `db:"owner_id"`
IsTrial bool `db:"is_trial"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (org *Org) ToApi() *shared.Org {
return &shared.Org{
Id: org.Id,
Name: org.Name,
AutoAddDomainUsers: org.AutoAddDomainUsers,
IsTrial: org.IsTrial,
}
}
type User struct {
Id string `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
Domain string `db:"domain"`
NumNonDraftPlans int `db:"num_non_draft_plans"`
DefaultPlanConfig *shared.PlanConfig `db:"default_plan_config"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (user *User) ToApi() *shared.User {
return &shared.User{
Id: user.Id,
Name: user.Name,
Email: user.Email,
NumNonDraftPlans: user.NumNonDraftPlans,
IsTrial: false, // legacy field
DefaultPlanConfig: user.DefaultPlanConfig,
}
}
type Invite struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
Email string `db:"email"`
Name string `db:"name"`
InviterId string `db:"inviter_id"`
InviteeId *string `db:"invitee_id"`
OrgRoleId string `db:"org_role_id"`
AcceptedAt *time.Time `db:"accepted_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (invite *Invite) ToApi() *shared.Invite {
return &shared.Invite{
Id: invite.Id,
OrgId: invite.OrgId,
Email: invite.Email,
Name: invite.Name,
InviterId: invite.InviterId,
InviteeId: invite.InviteeId,
OrgRoleId: invite.OrgRoleId,
AcceptedAt: invite.AcceptedAt,
CreatedAt: invite.CreatedAt,
}
}
type OrgUser struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
OrgRoleId string `db:"org_role_id"`
UserId string `db:"user_id"`
Config *shared.OrgUserConfig `db:"config"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (orgUser *OrgUser) ToApi() *shared.OrgUser {
return &shared.OrgUser{
OrgId: orgUser.OrgId,
OrgRoleId: orgUser.OrgRoleId,
UserId: orgUser.UserId,
Config: orgUser.Config,
}
}
type Project struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
Name string `db:"name"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (project *Project) ToApi() *shared.Project {
return &shared.Project{
Id: project.Id,
Name: project.Name,
}
}
type Plan struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
OwnerId string `db:"owner_id"`
ProjectId string `db:"project_id"`
Name string `db:"name"`
SharedWithOrgAt *time.Time `db:"shared_with_org_at,omitempty"`
TotalReplies int `db:"total_replies"`
ActiveBranches int `db:"active_branches"`
PlanConfig *shared.PlanConfig `db:"plan_config"`
ArchivedAt *time.Time `db:"archived_at,omitempty"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (plan *Plan) ToApi() *shared.Plan {
return &shared.Plan{
Id: plan.Id,
OwnerId: plan.OwnerId,
ProjectId: plan.ProjectId,
Name: plan.Name,
SharedWithOrgAt: plan.SharedWithOrgAt,
TotalReplies: plan.TotalReplies,
ActiveBranches: plan.ActiveBranches,
PlanConfig: plan.PlanConfig,
ArchivedAt: plan.ArchivedAt,
CreatedAt: plan.CreatedAt,
UpdatedAt: plan.UpdatedAt,
}
}
type Branch struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
OwnerId string `db:"owner_id"`
PlanId string `db:"plan_id"`
ParentBranchId *string `db:"parent_branch_id"`
Name string `db:"name"`
Status shared.PlanStatus `db:"status"`
Error *string `db:"error"`
ContextTokens int `db:"context_tokens"`
ConvoTokens int `db:"convo_tokens"`
SharedWithOrgAt *time.Time `db:"shared_with_org_at,omitempty"`
ArchivedAt *time.Time `db:"archived_at,omitempty"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt *time.Time `db:"deleted_at"`
}
func (branch *Branch) ToApi() *shared.Branch {
return &shared.Branch{
Id: branch.Id,
PlanId: branch.PlanId,
OwnerId: branch.OwnerId,
ParentBranchId: branch.ParentBranchId,
Name: branch.Name,
Status: branch.Status,
ContextTokens: branch.ContextTokens,
ConvoTokens: branch.ConvoTokens,
SharedWithOrgAt: branch.SharedWithOrgAt,
ArchivedAt: branch.ArchivedAt,
CreatedAt: branch.CreatedAt,
UpdatedAt: branch.UpdatedAt,
}
}
type ConvoSummary struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
PlanId string `db:"plan_id"`
LatestConvoMessageId string `db:"latest_convo_message_id"`
LatestConvoMessageCreatedAt time.Time `db:"latest_convo_message_created_at"`
Summary string `db:"summary"`
Tokens int `db:"tokens"`
NumMessages int `db:"num_messages"`
CreatedAt time.Time `db:"created_at"`
}
func (summary *ConvoSummary) ToApi() *shared.ConvoSummary {
return &shared.ConvoSummary{
Id: summary.Id,
LatestConvoMessageId: summary.LatestConvoMessageId,
LatestConvoMessageCreatedAt: summary.LatestConvoMessageCreatedAt,
Summary: summary.Summary,
Tokens: summary.Tokens,
NumMessages: summary.NumMessages,
CreatedAt: summary.CreatedAt,
}
}
type PlanBuild struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
PlanId string `db:"plan_id"`
ConvoMessageId string `db:"convo_message_id"`
FilePath string `db:"file_path"`
Error string `db:"error"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (build *PlanBuild) ToApi() *shared.PlanBuild {
return &shared.PlanBuild{
Id: build.Id,
ConvoMessageId: build.ConvoMessageId,
Error: build.Error,
FilePath: build.FilePath,
CreatedAt: build.CreatedAt,
UpdatedAt: build.UpdatedAt,
}
}
type OrgRole struct {
Id string `db:"id"`
OrgId *string `db:"org_id"`
Name string `db:"name"`
Label string `db:"label"`
Description string `db:"description"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (role *OrgRole) ToApi() *shared.OrgRole {
return &shared.OrgRole{
Id: role.Id,
IsDefault: role.OrgId == nil,
Label: role.Label,
Description: role.Description,
}
}
type ModelStream struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
PlanId string `db:"plan_id"`
InternalIp string `db:"internal_ip"`
Branch string `db:"branch"`
LastHeartbeatAt time.Time `db:"last_heartbeat_at"`
CreatedAt time.Time `db:"created_at"`
FinishedAt *time.Time `db:"finished_at"`
}
// type ModelStreamSubscription struct {
// Id string `db:"id"`
// OrgId string `db:"org_id"`
// PlanId string `db:"plan_id"`
// UserId string `db:"user_id"`
// ModelStreamId string `db:"model_stream_id"`
// UserIp string `db:"user_ip"`
// CreatedAt time.Time `db:"created_at"`
// FinishedAt *time.Time `db:"finished_at"`
// }
type LockScope string
const (
LockScopeRead LockScope = "r"
LockScopeWrite LockScope = "w"
)
type repoLock struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
UserId *string `db:"user_id"`
PlanId string `db:"plan_id"`
Scope LockScope `db:"scope"`
Branch *string `db:"branch"`
PlanBuildId *string `db:"plan_build_id"`
LastHeartbeatAt time.Time `db:"last_heartbeat_at"`
CreatedAt time.Time `db:"created_at"`
}
type ModelPack struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
Name string `db:"name"`
Description string `db:"description"`
Planner shared.PlannerRoleConfig `db:"planner"`
Coder *shared.ModelRoleConfig `db:"coder"`
PlanSummary shared.ModelRoleConfig `db:"plan_summary"`
Builder shared.ModelRoleConfig `db:"builder"`
WholeFileBuilder *shared.ModelRoleConfig `db:"whole_file_builder"`
Namer shared.ModelRoleConfig `db:"namer"`
CommitMsg shared.ModelRoleConfig `db:"commit_msg"`
ExecStatus shared.ModelRoleConfig `db:"exec_status"`
Architect *shared.ModelRoleConfig `db:"context_loader"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func ModelPackFromApi(apiModelPack *shared.ModelPack) *ModelPack {
return &ModelPack{
Name: apiModelPack.Name,
Description: apiModelPack.Description,
Planner: apiModelPack.Planner,
Architect: apiModelPack.Architect,
Coder: apiModelPack.Coder,
PlanSummary: apiModelPack.PlanSummary,
Builder: apiModelPack.Builder,
WholeFileBuilder: apiModelPack.WholeFileBuilder,
Namer: apiModelPack.Namer,
CommitMsg: apiModelPack.CommitMsg,
ExecStatus: apiModelPack.ExecStatus,
}
}
func (modelPack *ModelPack) ToApi() *shared.ModelPack {
return &shared.ModelPack{
Id: modelPack.Id,
Name: modelPack.Name,
Description: modelPack.Description,
Planner: modelPack.Planner,
Architect: modelPack.Architect,
Coder: modelPack.Coder,
PlanSummary: modelPack.PlanSummary,
Builder: modelPack.Builder,
WholeFileBuilder: modelPack.WholeFileBuilder,
Namer: modelPack.Namer,
CommitMsg: modelPack.CommitMsg,
ExecStatus: modelPack.ExecStatus,
}
}
type CustomModel struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
ModelId shared.ModelId `db:"model_id"`
Publisher shared.ModelPublisher `db:"publisher"`
Description string `db:"description"`
MaxTokens int `db:"max_tokens"`
DefaultMaxConvoTokens int `db:"default_max_convo_tokens"`
MaxOutputTokens int `db:"max_output_tokens"`
ReservedOutputTokens int `db:"reserved_output_tokens"`
HasImageSupport bool `db:"has_image_support"`
PreferredOutputFormat shared.ModelOutputFormat `db:"preferred_output_format"`
SystemPromptDisabled bool `db:"system_prompt_disabled"`
RoleParamsDisabled bool `db:"role_params_disabled"`
StopDisabled bool `db:"stop_disabled"`
PredictedOutputEnabled bool `db:"predicted_output_enabled"`
ReasoningEffortEnabled bool `db:"reasoning_effort_enabled"`
ReasoningEffort shared.ReasoningEffort `db:"reasoning_effort"`
IncludeReasoning bool `db:"include_reasoning"`
ReasoningBudget int `db:"reasoning_budget"`
SupportsCacheControl bool `db:"supports_cache_control"`
// for anthropic, single message system prompt needs to be flipped to 'user'
SingleMessageNoSystemPrompt bool `db:"single_message_no_system_prompt"`
// for anthropic, token estimate padding percentage
TokenEstimatePaddingPct float64 `db:"token_estimate_padding_pct"`
Providers CustomModelProviders `db:"providers"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func CustomModelFromApi(apiModel *shared.CustomModel) *CustomModel {
providers := make(CustomModelProviders, len(apiModel.Providers))
for i, provider := range apiModel.Providers {
providers[i] = CustomModelUsesProvider{
Provider: provider.Provider,
CustomProvider: provider.CustomProvider,
ModelName: provider.ModelName,
}
}
dbModel := CustomModel{
Id: apiModel.Id,
ModelId: apiModel.ModelId,
Publisher: apiModel.Publisher,
Description: apiModel.Description,
MaxTokens: apiModel.MaxTokens,
HasImageSupport: apiModel.ModelCompatibility.HasImageSupport,
DefaultMaxConvoTokens: apiModel.DefaultMaxConvoTokens,
MaxOutputTokens: apiModel.MaxOutputTokens,
ReservedOutputTokens: apiModel.ReservedOutputTokens,
PreferredOutputFormat: apiModel.PreferredOutputFormat,
SystemPromptDisabled: apiModel.SystemPromptDisabled,
RoleParamsDisabled: apiModel.RoleParamsDisabled,
StopDisabled: apiModel.StopDisabled,
PredictedOutputEnabled: apiModel.PredictedOutputEnabled,
IncludeReasoning: apiModel.IncludeReasoning,
ReasoningEffortEnabled: apiModel.ReasoningEffortEnabled,
ReasoningEffort: apiModel.ReasoningEffort,
ReasoningBudget: apiModel.ReasoningBudget,
SupportsCacheControl: apiModel.SupportsCacheControl,
SingleMessageNoSystemPrompt: apiModel.SingleMessageNoSystemPrompt,
TokenEstimatePaddingPct: apiModel.TokenEstimatePaddingPct,
Providers: providers,
}
return &dbModel
}
func (model *CustomModel) ToApi() *shared.CustomModel {
providers := make([]shared.BaseModelUsesProvider, len(model.Providers))
for i, provider := range model.Providers {
providers[i] = *provider.ToApi()
}
return &shared.CustomModel{
Id: model.Id,
ModelId: model.ModelId,
Publisher: model.Publisher,
Description: model.Description,
BaseModelShared: shared.BaseModelShared{
DefaultMaxConvoTokens: model.DefaultMaxConvoTokens,
MaxTokens: model.MaxTokens,
MaxOutputTokens: model.MaxOutputTokens,
ReservedOutputTokens: model.ReservedOutputTokens,
PreferredOutputFormat: model.PreferredOutputFormat,
SystemPromptDisabled: model.SystemPromptDisabled,
RoleParamsDisabled: model.RoleParamsDisabled,
StopDisabled: model.StopDisabled,
PredictedOutputEnabled: model.PredictedOutputEnabled,
IncludeReasoning: model.IncludeReasoning,
ReasoningEffortEnabled: model.ReasoningEffortEnabled,
ReasoningEffort: model.ReasoningEffort,
ReasoningBudget: model.ReasoningBudget,
SupportsCacheControl: model.SupportsCacheControl,
SingleMessageNoSystemPrompt: model.SingleMessageNoSystemPrompt,
TokenEstimatePaddingPct: model.TokenEstimatePaddingPct,
ModelCompatibility: shared.ModelCompatibility{
HasImageSupport: model.HasImageSupport,
},
},
Providers: providers,
CreatedAt: &model.CreatedAt,
UpdatedAt: &model.UpdatedAt,
}
}
type ExtraAuthVars []shared.ModelProviderExtraAuthVars
func (e *ExtraAuthVars) Scan(src interface{}) error {
if src == nil {
return nil
}
switch s := src.(type) {
case []byte:
return json.Unmarshal(s, e)
case string:
return json.Unmarshal([]byte(s), e)
default:
return fmt.Errorf("unsupported data type: %T", src)
}
}
func (e ExtraAuthVars) Value() (driver.Value, error) {
return json.Marshal(e)
}
type CustomProvider struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
Name string `db:"name"`
BaseUrl string `db:"base_url"`
SkipAuth bool `db:"skip_auth"`
ApiKeyEnvVar string `db:"api_key_env_var"`
ExtraAuthVars ExtraAuthVars `db:"extra_auth_vars"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func CustomProviderFromApi(apiProvider *shared.CustomProvider) *CustomProvider {
return &CustomProvider{
Id: apiProvider.Id,
Name: apiProvider.Name,
BaseUrl: apiProvider.BaseUrl,
SkipAuth: apiProvider.SkipAuth,
ApiKeyEnvVar: apiProvider.ApiKeyEnvVar,
ExtraAuthVars: apiProvider.ExtraAuthVars,
}
}
func (provider *CustomProvider) ToApi() *shared.CustomProvider {
return &shared.CustomProvider{
Id: provider.Id,
Name: provider.Name,
BaseUrl: provider.BaseUrl,
SkipAuth: provider.SkipAuth,
ApiKeyEnvVar: provider.ApiKeyEnvVar,
ExtraAuthVars: provider.ExtraAuthVars,
}
}
type CustomModelUsesProvider struct {
Provider shared.ModelProvider `db:"provider"`
CustomProvider *string `db:"custom_provider"`
ModelName shared.ModelName `db:"model_name"`
}
func (usesProvider *CustomModelUsesProvider) ToApi() *shared.BaseModelUsesProvider {
return &shared.BaseModelUsesProvider{
Provider: usesProvider.Provider,
ModelName: usesProvider.ModelName,
CustomProvider: usesProvider.CustomProvider,
}
}
type CustomModelProviders []CustomModelUsesProvider
func (providers *CustomModelProviders) Scan(src interface{}) error {
if src == nil {
return nil
}
switch s := src.(type) {
case []byte:
return json.Unmarshal(s, providers)
case string:
return json.Unmarshal([]byte(s), providers)
}
return fmt.Errorf("unsupported data type: %T", src)
}
func (providers CustomModelProviders) Value() (driver.Value, error) {
return json.Marshal(providers)
}
type DefaultPlanSettings struct {
Id string `db:"id"`
OrgId string `db:"org_id"`
PlanSettings shared.PlanSettings `db:"plan_settings"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// Models below are stored in files, not in the database.
// This allows us to store them in a git repo and use git to manage history.
type Context struct {
Id string `json:"id"`
OrgId string `json:"orgId"`
OwnerId string `json:"ownerId"`
ProjectId string `json:"projectId"`
PlanId string `json:"planId"`
ContextType shared.ContextType `json:"contextType"`
Name string `json:"name"`
Url string `json:"url"`
FilePath string `json:"filePath"`
Sha string `json:"sha"`
NumTokens int `json:"numTokens"`
Body string `json:"body,omitempty"`
BodySize int64 `json:"bodySize,omitempty"`
ForceSkipIgnore bool `json:"forceSkipIgnore"`
ImageDetail openai.ImageURLDetail `json:"imageDetail,omitempty"`
MapParts shared.FileMapBodies `json:"mapParts,omitempty"`
MapShas map[string]string `json:"mapShas,omitempty"`
MapTokens map[string]int `json:"mapTokens,omitempty"`
MapSizes map[string]int64 `json:"mapSizes,omitempty"`
AutoLoaded bool `json:"autoLoaded"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (context *Context) ToMeta() *Context {
// everything except body and mapParts
return &Context{
Id: context.Id,
OrgId: context.OrgId,
OwnerId: context.OwnerId,
ProjectId: context.ProjectId,
PlanId: context.PlanId,
ContextType: context.ContextType,
Name: context.Name,
Url: context.Url,
FilePath: context.FilePath,
Sha: context.Sha,
NumTokens: context.NumTokens,
BodySize: context.BodySize,
ForceSkipIgnore: context.ForceSkipIgnore,
AutoLoaded: context.AutoLoaded,
ImageDetail: context.ImageDetail,
MapShas: context.MapShas,
MapTokens: context.MapTokens,
MapSizes: context.MapSizes,
CreatedAt: context.CreatedAt,
UpdatedAt: context.UpdatedAt,
}
}
func (context *Context) ToApi() *shared.Context {
return &shared.Context{
Id: context.Id,
OwnerId: context.OwnerId,
ContextType: context.ContextType,
Name: context.Name,
Url: context.Url,
FilePath: context.FilePath,
Sha: context.Sha,
NumTokens: context.NumTokens,
Body: context.Body,
BodySize: context.BodySize,
ForceSkipIgnore: context.ForceSkipIgnore,
AutoLoaded: context.AutoLoaded,
ImageDetail: context.ImageDetail,
MapParts: context.MapParts,
MapShas: context.MapShas,
MapTokens: context.MapTokens,
MapSizes: context.MapSizes,
CreatedAt: context.CreatedAt,
UpdatedAt: context.UpdatedAt,
}
}
type ConvoMessage struct {
Id string `json:"id"`
OrgId string `json:"orgId"`
PlanId string `json:"planId"`
UserId string `json:"userId"`
Role string `json:"role"`
Tokens int `json:"tokens"`
Num int `json:"num"`
Message string `json:"message"`
Stopped bool `json:"stopped"`
Subtask *Subtask `json:"subtask,omitempty"`
AddedSubtasks []*Subtask `json:"addedSubtasks,omitempty"`
RemovedSubtasks []string `json:"removedSubtasks,omitempty"`
Flags shared.ConvoMessageFlags `json:"flags"`
ActivatedPaths map[string]bool `json:"activatePaths,omitempty"`
ActivatedPathsOrdered []string `json:"activatePathsOrdered,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
func (msg *ConvoMessage) ToApi() *shared.ConvoMessage {
addedSubtasks := make([]*shared.Subtask, len(msg.AddedSubtasks))
for i, subtask := range msg.AddedSubtasks {
addedSubtasks[i] = subtask.ToApi()
}
return &shared.ConvoMessage{
Id: msg.Id,
UserId: msg.UserId,
Role: msg.Role,
Tokens: msg.Tokens,
Num: msg.Num,
Message: msg.Message,
Stopped: msg.Stopped,
Flags: msg.Flags,
Subtask: msg.Subtask.ToApi(),
AddedSubtasks: addedSubtasks,
RemovedSubtasks: msg.RemovedSubtasks,
CreatedAt: msg.CreatedAt,
}
}
type ConvoMessageDescription struct {
Id string `json:"id"`
OrgId string `json:"orgId"`
PlanId string `json:"planId"`
ConvoMessageId string `json:"convoMessageId"`
SummarizedToMessageId string `json:"summarizedToMessageId"`
WroteFiles bool `json:"wroteFiles"`
CommitMsg string `json:"commitMsg"`
// Files []string `json:"files"`
Operations []*shared.Operation `json:"operations"`
Error string `json:"error"`
DidBuild bool `json:"didBuild"`
BuildPathsInvalidated map[string]bool `json:"buildPathsInvalidated"`
AppliedAt *time.Time `json:"appliedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (desc *ConvoMessageDescription) ToApi() *shared.ConvoMessageDescription {
return &shared.ConvoMessageDescription{
Id: desc.Id,
ConvoMessageId: desc.ConvoMessageId,
SummarizedToMessageId: desc.SummarizedToMessageId,
WroteFiles: desc.WroteFiles,
CommitMsg: desc.CommitMsg,
// Files: desc.Files,
Operations: desc.Operations,
DidBuild: desc.DidBuild,
BuildPathsInvalidated: desc.BuildPathsInvalidated,
AppliedAt: desc.AppliedAt,
Error: desc.Error,
CreatedAt: desc.CreatedAt,
UpdatedAt: desc.UpdatedAt,
}
}
type PlanFileResult struct {
Id string `json:"id"`
TypeVersion int `json:"typeVersion"`
ReplaceWithLineNums bool `json:"replaceWithLineNums"`
OrgId string `json:"orgId"`
PlanId string `json:"planId"`
ConvoMessageId string `json:"convoMessageId"`
PlanBuildId string `json:"planBuildId"`
Path string `json:"path"`
Content string `json:"content,omitempty"`
Replacements []*shared.Replacement `json:"replacements"`
RemovedFile bool `json:"removedFile"`
AnyFailed bool `json:"anyFailed"`
Error string `json:"error"`
SyntaxErrors []string `json:"syntaxErrors"`
AppliedAt *time.Time `json:"appliedAt,omitempty"`
RejectedAt *time.Time `json:"rejectedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (res *PlanFileResult) ToApi() *shared.PlanFileResult {
return &shared.PlanFileResult{
Id: res.Id,
TypeVersion: res.TypeVersion,
ReplaceWithLineNums: res.ReplaceWithLineNums,
PlanBuildId: res.PlanBuildId,
ConvoMessageId: res.ConvoMessageId,
Path: res.Path,
Content: res.Content,
AnyFailed: res.AnyFailed,
AppliedAt: res.AppliedAt,
RejectedAt: res.RejectedAt,
Replacements: res.Replacements,
RemovedFile: res.RemovedFile,
CreatedAt: res.CreatedAt,
UpdatedAt: res.UpdatedAt,
}
}
type PlanApply struct {
Id string `json:"id"`
OrgId string `json:"orgId"`
PlanId string `json:"planId"`
UserId string `json:"userId"`
ConvoMessageIds []string `json:"convoMessageIds"`
ConvoMessageDescriptionIds []string `json:"convoMessageDescriptionIds"`
PlanFileResultIds []string `json:"planFileResultIds"`
CommitMsg string `json:"commitMsg"`
CreatedAt time.Time `json:"createdAt"`
}
func (apply *PlanApply) ToApi() *shared.PlanApply {
return &shared.PlanApply{
Id: apply.Id,
UserId: apply.UserId,
ConvoMessageIds: apply.ConvoMessageIds,
ConvoMessageDescriptionIds: apply.ConvoMessageDescriptionIds,
PlanFileResultIds: apply.PlanFileResultIds,
CommitMsg: apply.CommitMsg,
CreatedAt: apply.CreatedAt,
}
}
type Subtask struct {
Title string `json:"title"`
Description string `json:"description"`
UsesFiles []string `json:"usesFiles"`
IsFinished bool `json:"isFinished"`
NumTries int `json:"numTries"`
}
func (subtask *Subtask) ToApi() *shared.Subtask {
if subtask == nil {
return nil
}
return &shared.Subtask{
Title: subtask.Title,
Description: subtask.Description,
UsesFiles: subtask.UsesFiles,
IsFinished: subtask.IsFinished,
}
}
================================================
FILE: app/server/db/db.go
================================================
package db
import (
"errors"
"fmt"
"log"
"net/url"
"os"
"strings"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
var Conn *sqlx.DB
const LockTimeout = 4000
const IdleInTransactionSessionTimeout = 90000
const StatementTimeout = 30000
func Connect() error {
var err error
dbUrl := os.Getenv("DATABASE_URL")
if dbUrl == "" {
if os.Getenv("DB_HOST") != "" &&
os.Getenv("DB_PORT") != "" &&
os.Getenv("DB_USER") != "" &&
os.Getenv("DB_PASSWORD") != "" &&
os.Getenv("DB_NAME") != "" {
encodedPassword := url.QueryEscape(os.Getenv("DB_PASSWORD"))
dbUrl = "postgres://" + os.Getenv("DB_USER") + ":" + encodedPassword + "@" + os.Getenv("DB_HOST") + ":" + os.Getenv("DB_PORT") + "/" + os.Getenv("DB_NAME")
}
if dbUrl == "" {
return errors.New("DATABASE_URL or DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, and DB_NAME environment variables must be set")
}
}
if strings.Contains(dbUrl, "?") {
dbUrl += fmt.Sprintf("&statement_timeout=%d&lock_timeout=%d&timezone=UTC&idle_in_transaction_session_timeout=%d", StatementTimeout, LockTimeout, IdleInTransactionSessionTimeout)
} else {
dbUrl += fmt.Sprintf("?statement_timeout=%d&lock_timeout=%d&timezone=UTC&idle_in_transaction_session_timeout=%d", StatementTimeout, LockTimeout, IdleInTransactionSessionTimeout)
}
Conn, err = sqlx.Connect("postgres", dbUrl)
if err != nil {
return err
}
log.Println("connected to database")
if os.Getenv("GOENV") == "production" {
Conn.SetMaxOpenConns(50)
Conn.SetMaxIdleConns(20)
} else {
Conn.SetMaxOpenConns(10)
Conn.SetMaxIdleConns(5)
}
// Verify settings
type setting struct {
Name string `db:"name"`
Setting string `db:"setting"`
Unit *string `db:"unit"`
Context string `db:"context"`
}
var settings []setting
err = Conn.Select(&settings, `
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN ('statement_timeout', 'lock_timeout', 'TimeZone', 'idle_in_transaction_session_timeout')
`)
if err != nil {
return fmt.Errorf("error checking settings: %v", err)
}
s := ""
for _, setting := range settings {
unitStr := ""
if setting.Unit != nil {
unitStr = " " + *setting.Unit // Add a leading space only if there's a unit
}
s += fmt.Sprintf("- %s = %s%s (context: %s)\n", setting.Name, setting.Setting, unitStr, setting.Context)
}
log.Printf("\n\nDatabase settings:\n%s\n", s)
return nil
}
func MigrationsUp() error {
migrationsDir := "migrations"
if os.Getenv("MIGRATIONS_DIR") != "" {
migrationsDir = os.Getenv("MIGRATIONS_DIR")
}
return migrationsUp(migrationsDir)
}
func MigrationsUpWithDir(dir string) error {
return migrationsUp(dir)
}
func migrationsUp(dir string) error {
if Conn == nil {
return errors.New("db not initialized")
}
driver, err := postgres.WithInstance(Conn.DB, &postgres.Config{})
if err != nil {
return fmt.Errorf("error creating postgres driver: %v", err)
}
m, err := migrate.NewWithDatabaseInstance(
"file://"+dir,
"postgres", driver)
if err != nil {
return fmt.Errorf("error creating migration instance: %v", err)
}
// Uncomment below (and update migration version) to reset migration state to a specific version after a failure
// if os.Getenv("GOENV") == "development" {
// migrateVersion := 2025052900
// if err := m.Force(migrateVersion); err != nil {
// return fmt.Errorf("error forcing migration version: %v", err)
// }
// }
// Uncomment below to run down migrations (RESETS DATABASE!!)
// if os.Getenv("GOENV") == "development" {
// err = m.Down()
// if err != nil {
// if err == migrate.ErrNoChange {
// log.Println("no migrations to run down")
// } else {
// return fmt.Errorf("error running down migrations: %v", err)
// }
// }
// log.Println("ran down migrations - database was reset")
// }
// Uncomment below and edit 'stepsBack' to go back a specific number of migrations
// if os.Getenv("GOENV") == "development" {
// stepsBack := 1
// err = m.Steps(-stepsBack)
// if err != nil {
// return fmt.Errorf("error running down migrations: %v", err)
// }
// log.Printf("went down %d migration\n", stepsBack)
// }
err = m.Up()
if err != nil {
if err == migrate.ErrNoChange {
log.Println("migration state is up to date")
} else {
return fmt.Errorf("error running migrations: %v", err)
}
}
if err == nil {
log.Println("ran migrations successfully")
}
return nil
}
================================================
FILE: app/server/db/diff_helpers.go
================================================
package db
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/debug"
shared "plandex-shared"
)
func GetPlanDiffs(orgId, planId string, plain bool) (string, error) {
planState, err := GetCurrentPlanState(CurrentPlanStateParams{
OrgId: orgId,
PlanId: planId,
})
if err != nil {
return "", fmt.Errorf("error getting current plan state: %v", err)
}
// create temp directory
tempDirPath, err := os.MkdirTemp(getOrgDir(orgId), "tmp-diffs-*")
if err != nil {
return "", fmt.Errorf("error creating temp dir: %v", err)
}
defer func() {
go os.RemoveAll(tempDirPath)
}()
// init a git repo in the temp dir
err = initGitRepo(tempDirPath)
if err != nil {
return "", fmt.Errorf("error initializing git repo: %v", err)
}
files := planState.CurrentPlanFiles.Files
removed := planState.CurrentPlanFiles.Removed
// write the original files to the temp dir
errCh := make(chan error, len(planState.ContextsByPath))
hasAnyOriginal := false
for path, context := range planState.ContextsByPath {
go func(path string, context *shared.Context) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
_, hasPath := files[path]
_, hasRemoved := removed[path]
if hasPath || hasRemoved {
hasAnyOriginal = true
// ensure file directory exists
err = os.MkdirAll(filepath.Dir(filepath.Join(tempDirPath, path)), 0755)
if err != nil {
errCh <- fmt.Errorf("error creating directory: %v", err)
return
}
err = os.WriteFile(filepath.Join(tempDirPath, path), []byte(context.Body), 0644)
if err != nil {
errCh <- fmt.Errorf("error writing file: %v", err)
return
}
}
errCh <- nil
}(path, context)
}
for range planState.ContextsByPath {
err = <-errCh
if err != nil {
return "", fmt.Errorf("error writing original files to temp dir: %v", err)
}
}
if hasAnyOriginal {
// add and commit the files in the temp dir
err := gitAdd(tempDirPath, ".")
if err != nil {
return "", fmt.Errorf("error adding files to git repository for dir: %s, err: %v", tempDirPath, err)
}
err = gitCommit(tempDirPath, "original files")
if err != nil {
return "", fmt.Errorf("error committing files to git repository for dir: %s, err: %v", tempDirPath, err)
}
}
// write the current files to the temp dir
errCh = make(chan error, len(files))
for path, file := range files {
go func(path, file string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
// ensure file directory exists
err = os.MkdirAll(filepath.Dir(filepath.Join(tempDirPath, path)), 0755)
if err != nil {
errCh <- fmt.Errorf("error creating directory: %v", err)
return
}
err = os.WriteFile(filepath.Join(tempDirPath, path), []byte(file), 0644)
if err != nil {
errCh <- fmt.Errorf("error writing file: %v", err)
return
}
errCh <- nil
}(path, file)
}
for path, shouldRemove := range removed {
go func(path string, shouldRemove bool) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetPlanDiffs: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if shouldRemove {
err = os.RemoveAll(filepath.Join(tempDirPath, path))
if err != nil {
errCh <- fmt.Errorf("error removing file: %v", err)
return
}
}
errCh <- nil
}(path, shouldRemove)
}
for i := 0; i < len(files)+len(removed); i++ {
err = <-errCh
if err != nil {
return "", fmt.Errorf("error applying changes to temp dir: %v", err)
}
}
err = gitAdd(tempDirPath, ".")
if err != nil {
return "", fmt.Errorf("error adding files to git repository for dir: %s, err: %v", tempDirPath, err)
}
colorArg := "--color=always"
if plain {
colorArg = "--no-color"
}
res, err := exec.Command("git", "-C", tempDirPath, "diff", "--cached", colorArg).CombinedOutput()
if err != nil {
return "", fmt.Errorf("error getting diffs: %v", err)
}
return string(res), nil
}
================================================
FILE: app/server/db/fs.go
================================================
package db
import (
"fmt"
"log"
"os"
"path/filepath"
)
var BaseDir string
func init() {
home, err := os.UserHomeDir()
if err != nil {
panic(fmt.Errorf("error getting user home dir: %v", err))
}
log.Println("Plandex server home dir:", home)
log.Println("os.Getenv(PLANDEX_BASE_DIR):", os.Getenv("PLANDEX_BASE_DIR"))
log.Println("GOENV:", os.Getenv("GOENV"))
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
log.Println("Local mode enabled")
}
BaseDir = os.Getenv("PLANDEX_BASE_DIR")
if BaseDir == "" {
if os.Getenv("GOENV") == "development" {
BaseDir = filepath.Join(home, "plandex-server")
} else {
BaseDir = "/plandex-server"
}
}
log.Printf("File system dir: %v\n", BaseDir)
}
func InitPlan(orgId, planId string) error {
dir := getPlanDir(orgId, planId)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return fmt.Errorf("error creating plan dir: %v", err)
}
for _, subdirFn := range [](func(orgId, planId string) string){
getPlanContextDir,
getPlanConversationDir,
getPlanResultsDir,
getPlanDescriptionsDir} {
err = os.MkdirAll(subdirFn(orgId, planId), os.ModePerm)
if err != nil {
return fmt.Errorf("error creating plan subdir: %v", err)
}
}
err = InitGitRepo(orgId, planId)
if err != nil {
return fmt.Errorf("error initializing git repo: %v", err)
}
return nil
}
func DeletePlanDir(orgId, planId string) error {
dir := getPlanDir(orgId, planId)
err := os.RemoveAll(dir)
if err != nil {
return fmt.Errorf("error deleting plan dir: %v", err)
}
return nil
}
func getOrgDir(orgId string) string {
return filepath.Join(BaseDir, "orgs", orgId)
}
func getProjectDir(orgId, projectId string) string {
return filepath.Join(getOrgDir(orgId), "projects", projectId)
}
func getProjectMapCacheDir(orgId, projectId string) string {
return filepath.Join(getProjectDir(orgId, projectId), "map_cache")
}
func getPlanDir(orgId, planId string) string {
return filepath.Join(getOrgDir(orgId), "plans", planId)
}
func getPlanContextDir(orgId, planId string) string {
return filepath.Join(getPlanDir(orgId, planId), "context")
}
func getPlanConversationDir(orgId, planId string) string {
return filepath.Join(getPlanDir(orgId, planId), "conversation")
}
func getPlanResultsDir(orgId, planId string) string {
return filepath.Join(getPlanDir(orgId, planId), "results")
}
func getPlanAppliesDir(orgId, planId string) string {
return filepath.Join(getPlanDir(orgId, planId), "applies")
}
func getPlanDescriptionsDir(orgId, planId string) string {
return filepath.Join(getPlanDir(orgId, planId), "descriptions")
}
================================================
FILE: app/server/db/git.go
================================================
package db
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/fatih/color"
)
const (
maxGitRetries = 5
baseGitRetryDelay = 100 * time.Millisecond
)
func init() {
// ensure git is available
cmd := exec.Command("git", "--version")
if err := cmd.Run(); err != nil {
panic(fmt.Errorf("error running git --version: %v", err))
}
}
type GitRepo struct {
orgId string
planId string
}
func InitGitRepo(orgId, planId string) error {
dir := getPlanDir(orgId, planId)
return initGitRepo(dir)
}
func initGitRepo(dir string) error {
// Set the default branch name to 'main' for the new repository
res, err := exec.Command("git", "-C", dir, "init", "-b", "main").CombinedOutput()
if err != nil {
return fmt.Errorf("error initializing git repository with 'main' as default branch for dir: %s, err: %v, output: %s", dir, err, string(res))
}
// Configure user name and email for the repository
if err := setGitConfig(dir, "user.email", "server@plandex.ai"); err != nil {
return err
}
if err := setGitConfig(dir, "user.name", "Plandex"); err != nil {
return err
}
return nil
}
func getGitRepo(orgId, planId string) *GitRepo {
return &GitRepo{
orgId: orgId,
planId: planId,
}
}
func (repo *GitRepo) GitAddAndCommit(branch, message string) error {
log.Printf("[Git] GitAddAndCommit - orgId: %s, planId: %s, branch: %s, message: %s", repo.orgId, repo.planId, branch, message)
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
err := gitWriteOperation(func() error {
return gitAdd(dir, ".")
}, dir, fmt.Sprintf("GitAddAndCommit > gitAdd: plan=%s branch=%s", planId, branch))
if err != nil {
return fmt.Errorf("error adding files to git repository for dir: %s, err: %v", dir, err)
}
err = gitWriteOperation(func() error {
return gitCommit(dir, message)
}, dir, fmt.Sprintf("GitAddAndCommit > gitCommit: plan=%s branch=%s", planId, branch))
if err != nil {
return fmt.Errorf("error committing files to git repository for dir: %s, err: %v", dir, err)
}
// log.Println("[Git] GitAddAndCommit - finished, logging repo state")
// repo.LogGitRepoState()
return nil
}
func (repo *GitRepo) GitRewindToSha(branch, sha string) error {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
err := gitWriteOperation(func() error {
return gitRewindToSha(dir, sha)
}, dir, fmt.Sprintf("GitRewindToSha > gitRewindToSha: plan=%s branch=%s", planId, branch))
if err != nil {
return fmt.Errorf("error rewinding git repository for dir: %s, err: %v", dir, err)
}
return nil
}
func (repo *GitRepo) GetCurrentCommitSha() (sha string, err error) {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
cmd := exec.Command("git", "-C", dir, "rev-parse", "HEAD")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("error getting current commit SHA for dir: %s, err: %v", dir, err)
}
sha = strings.TrimSpace(string(output))
return sha, nil
}
func (repo *GitRepo) GetCommitTime(branch, ref string) (time.Time, error) {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
// Use git show to get the commit timestamp
cmd := exec.Command("git", "-C", dir, "show", "-s", "--format=%ct", ref)
output, err := cmd.Output()
if err != nil {
return time.Time{}, fmt.Errorf("error getting commit time for ref %s: %v", ref, err)
}
// Parse the Unix timestamp
timestamp, err := strconv.ParseInt(strings.TrimSpace(string(output)), 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("error parsing commit timestamp for ref %s: %v", ref, err)
}
// Convert Unix timestamp to time.Time
commitTime := time.Unix(timestamp, 0)
return commitTime, nil
}
func (repo *GitRepo) GitResetToSha(sha string) error {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
err := gitWriteOperation(func() error {
cmd := exec.Command("git", "-C", dir, "reset", "--hard", sha)
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("error resetting git repository to SHA for dir: %s, sha: %s, err: %v", dir, sha, err)
}
return nil
}, dir, fmt.Sprintf("GitResetToSha > gitReset: plan=%s sha=%s", planId, sha))
if err != nil {
return fmt.Errorf("error resetting git repository to SHA for dir: %s, sha: %s, err: %v", dir, sha, err)
}
return nil
}
func (repo *GitRepo) GitCheckoutSha(sha string) error {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
err := gitWriteOperation(func() error {
cmd := exec.Command("git", "-C", dir, "checkout", sha)
_, err := cmd.Output()
if err != nil {
return fmt.Errorf("error checking out git repository at SHA for dir: %s, sha: %s, err: %v", dir, sha, err)
}
return nil
}, dir, fmt.Sprintf("GitCheckoutSha > gitCheckout: plan=%s sha=%s", planId, sha))
if err != nil {
return fmt.Errorf("error checking out git repository at SHA for dir: %s, sha: %s, err: %v", dir, sha, err)
}
return nil
}
func (repo *GitRepo) GetGitCommitHistory(branch string) (body string, shas []string, err error) {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
body, shas, err = getGitCommitHistory(dir)
if err != nil {
return "", nil, fmt.Errorf("error getting git history for dir: %s, err: %v", dir, err)
}
return body, shas, nil
}
func (repo *GitRepo) GetLatestCommit(branch string) (sha, body string, err error) {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
sha, body, err = getLatestCommit(dir)
if err != nil {
return "", "", fmt.Errorf("error getting latest commit for dir: %s, err: %v", dir, err)
}
return sha, body, nil
}
func (repo *GitRepo) GetLatestCommitShaBeforeTime(branch string, before time.Time) (sha string, err error) {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
log.Printf("ADMIN - GetLatestCommitShaBeforeTime - dir: %s, before: %s", dir, before.Format("2006-01-02T15:04:05Z"))
// Round up to the next second
// roundedTime := before.Add(time.Second).Truncate(time.Second)
gitFormattedTime := before.Format("2006-01-02 15:04:05+0000")
// log.Printf("ADMIN - Git formatted time: %s", gitFormattedTime)
cmd := exec.Command("git", "-C", dir, "log", "-n", "1",
"--before="+gitFormattedTime,
"--pretty=%h@@|@@%B@>>>@")
log.Printf("ADMIN - Executing command: %s", cmd.String())
res, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("error getting latest commit before time for dir: %s, err: %v, output: %s", dir, err, string(res))
}
// log.Printf("ADMIN - git log res: %s", string(res))
output := strings.TrimSpace(string(res))
// history := processGitHistoryOutput(strings.TrimSpace(string(res)))
// log.Printf("ADMIN - History: %v", history)
if output == "" {
return "", fmt.Errorf("no commits found before time: %s", before.Format("2006-01-02T15:04:05Z"))
}
sha = strings.Split(output, "@@|@@")[0]
return sha, nil
}
func (repo *GitRepo) GitListBranches() ([]string, error) {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
var out bytes.Buffer
cmd := exec.Command("git", "branch", "--format=%(refname:short)")
cmd.Dir = dir
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("error getting git branches for dir: %s, err: %v", dir, err)
}
branches := strings.Split(strings.TrimSpace(out.String()), "\n")
if len(branches) == 0 || (len(branches) == 1 && branches[0] == "") {
return []string{"main"}, nil
}
return branches, nil
}
func (repo *GitRepo) GitCreateBranch(newBranch string) error {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
err := gitWriteOperation(func() error {
res, err := exec.Command("git", "-C", dir, "checkout", "-b", newBranch).CombinedOutput()
if err != nil {
return fmt.Errorf("error creating git branch for dir: %s, err: %v, output: %s", dir, err, string(res))
}
return nil
}, dir, fmt.Sprintf("GitCreateBranch > gitCheckout: plan=%s branch=%s", planId, newBranch))
if err != nil {
return err
}
return nil
}
func (repo *GitRepo) GitDeleteBranch(branchName string) error {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
err := gitWriteOperation(func() error {
res, err := exec.Command("git", "-C", dir, "branch", "-D", branchName).CombinedOutput()
if err != nil {
return fmt.Errorf("error deleting git branch for dir: %s, err: %v, output: %s", dir, err, string(res))
}
return nil
}, dir, fmt.Sprintf("GitDeleteBranch > gitBranch: plan=%s branch=%s", planId, branchName))
if err != nil {
return err
}
return nil
}
func (repo *GitRepo) GitClearUncommittedChanges(branch string) error {
orgId := repo.orgId
planId := repo.planId
log.Printf("[Git] GitClearUncommittedChanges - orgId: %s, planId: %s, branch: %s", orgId, planId, branch)
dir := getPlanDir(orgId, planId)
// first do a lightweight git status to check if there are any uncommitted changes
// prevents heavier operations below if there are no changes (the usual case)
res, err := exec.Command("git", "status", "--porcelain").CombinedOutput()
if err != nil {
return fmt.Errorf("error checking for uncommitted changes: %v, output: %s", err, string(res))
}
// If there's output, there are uncommitted changes
hasChanges := strings.TrimSpace(string(res)) != ""
if !hasChanges {
log.Printf("[Git] GitClearUncommittedChanges - no changes to clear for plan %s", planId)
return nil
}
err = gitWriteOperation(func() error {
// Reset staged changes
log.Printf("[Git] GitClearUncommittedChanges - resetting staged changes for plan %s", planId)
res, err := exec.Command("git", "-C", dir, "reset", "--hard").CombinedOutput()
if err != nil {
return fmt.Errorf("error resetting staged changes | err: %v, output: %s", err, string(res))
}
log.Printf("[Git] GitClearUncommittedChanges - reset staged changes finished for plan %s", planId)
return nil
}, dir, fmt.Sprintf("GitClearUncommittedChanges > gitReset: plan=%s", planId))
if err != nil {
return err
}
err = gitWriteOperation(func() error {
// Clean untracked files
log.Printf("[Git] GitClearUncommittedChanges - cleaning untracked files for plan %s", planId)
res, err := exec.Command("git", "-C", dir, "clean", "-d", "-f").CombinedOutput()
if err != nil {
return fmt.Errorf("error cleaning untracked files | err: %v, output: %s", err, string(res))
}
log.Printf("[Git] GitClearUncommittedChanges - clean untracked files finished for plan %s", planId)
return nil
}, dir, fmt.Sprintf("GitClearUncommittedChanges > gitClean: plan=%s", planId))
return err
}
func (repo *GitRepo) GitCheckoutBranch(branch string) error {
orgId := repo.orgId
planId := repo.planId
dir := getPlanDir(orgId, planId)
err := gitWriteOperation(func() error {
return gitCheckoutBranch(dir, branch)
}, dir, fmt.Sprintf("GitCheckoutBranch > gitCheckout: plan=%s branch=%s", planId, branch))
if err != nil {
return err
}
return nil
}
func gitAdd(repoDir, path string) error {
if err := gitRemoveIndexLockFileIfExists(repoDir); err != nil {
return fmt.Errorf("error removing lock file before add: %v", err)
}
res, err := exec.Command("git", "-C", repoDir, "add", path).CombinedOutput()
if err != nil {
return fmt.Errorf("error adding files to git repository for dir: %s, err: %v, output: %s", repoDir, err, string(res))
}
return nil
}
func gitCommit(repoDir, commitMsg string) error {
if err := gitRemoveIndexLockFileIfExists(repoDir); err != nil {
return fmt.Errorf("error removing lock file before commit: %v", err)
}
res, err := exec.Command("git", "-C", repoDir, "commit", "-m", commitMsg).CombinedOutput()
if err != nil {
return fmt.Errorf("error committing files to git repository for dir: %s, err: %v, output: %s", repoDir, err, string(res))
}
return nil
}
func gitCheckoutBranch(repoDir, branch string) error {
log.Printf("[Git] gitCheckoutBranch - repoDir: %s, branch: %s", repoDir, branch)
if err := gitRemoveIndexLockFileIfExists(repoDir); err != nil {
return fmt.Errorf("error removing lock file before checkout: %v", err)
}
// get current branch and only checkout if it's not the same
// trying to check out the same branch will result in an error
var out bytes.Buffer
cmd := exec.Command("git", "-C", repoDir, "branch", "--show-current")
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return fmt.Errorf("error getting current git branch for dir: %s, err: %v", repoDir, err)
}
currentBranch := strings.TrimSpace(out.String())
log.Printf("[Git] gitCheckoutBranch - currentBranch: %s", currentBranch)
if currentBranch == branch {
log.Printf("[Git] gitCheckoutBranch - already on branch %s, skipping", branch)
return nil
}
log.Println("[Git] gitCheckoutBranch - checking out branch:", branch)
res, err := exec.Command("git", "-C", repoDir, "checkout", branch).CombinedOutput()
if err != nil {
return fmt.Errorf("error checking out git branch for dir: %s, err: %v, output: %s", repoDir, err, string(res))
}
return nil
}
func gitRewindToSha(repoDir, sha string) error {
res, err := exec.Command("git", "-C", repoDir, "reset", "--hard", sha).CombinedOutput()
if err != nil {
return fmt.Errorf("error executing git reset for dir: %s, sha: %s, err: %v, output: %s", repoDir, sha, err, string(res))
}
return nil
}
func getLatestCommit(dir string) (sha, body string, err error) {
var out bytes.Buffer
cmd := exec.Command("git", "log", "--pretty=%h@@|@@%at@@|@@%B@>>>@")
cmd.Dir = dir
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return "", "", fmt.Errorf("error getting git history for dir: %s, err: %v", dir, err)
}
// Process the log output to get it in the desired format.
history := processGitHistoryOutput(strings.TrimSpace(out.String()))
first := history[0]
sha = first[0]
body = first[1]
return sha, body, nil
}
func getGitCommitHistory(dir string) (body string, shas []string, err error) {
var out bytes.Buffer
cmd := exec.Command("git", "log", "--pretty=%h@@|@@%at@@|@@%B@>>>@")
cmd.Dir = dir
cmd.Stdout = &out
err = cmd.Run()
if err != nil {
return "", nil, fmt.Errorf("error getting git history for dir: %s, err: %v", dir, err)
}
// Process the log output to get it in the desired format.
history := processGitHistoryOutput(strings.TrimSpace(out.String()))
var output []string
for _, el := range history {
shas = append(shas, el[0])
output = append(output, el[1])
}
return strings.Join(output, "\n\n"), shas, nil
}
// processGitHistoryOutput processes the raw output from the git log command and returns a formatted string.
func processGitHistoryOutput(raw string) [][2]string {
var history [][2]string
entries := strings.Split(raw, "@>>>@") // Split entries using the custom separator.
for _, entry := range entries {
// First clean up any leading/trailing whitespace or newlines from each entry.
entry = strings.TrimSpace(entry)
// Now split the cleaned entry into its parts.
parts := strings.Split(entry, "@@|@@")
if len(parts) == 3 {
sha := parts[0]
timestampStr := parts[1]
message := strings.TrimSpace(parts[2]) // Trim whitespace from message as well.
// Extract and format timestamp.
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
continue // Skip entries with invalid timestamps.
}
dt := time.Unix(timestamp, 0).UTC()
formattedTs := dt.Format("Mon Jan 2, 2006 | 3:04:05pm MST")
// Prepare the header with colors.
headerColor := color.New(color.FgCyan, color.Bold)
dateColor := color.New(color.FgCyan)
// Combine sha, formatted timestamp, and message header into one string.
header := fmt.Sprintf("%s | %s", headerColor.Sprintf("📝 Update %s", sha), dateColor.Sprintf("%s", formattedTs))
// Combine header and message with a newline only if the message is not empty.
fullEntry := header
if message != "" {
fullEntry += "\n" + message
}
history = append(history, [2]string{sha, fullEntry})
}
}
return history
}
func removeLockFile(lockFilePath string) error {
_, err := os.Stat(lockFilePath)
exists := err == nil
// log.Println("index.lock file exists:", exists)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error checking lock file: %v", err)
}
attempts := 0
for exists {
if attempts > 10 {
return fmt.Errorf("error removing index.lock file: %v after %d attempts", err, attempts)
}
log.Printf("[Git] removeLockFile - removing index.lock file: %s, attempt: %d", lockFilePath, attempts)
if err := os.Remove(lockFilePath); err != nil {
if os.IsNotExist(err) {
log.Printf("[Git] removeLockFile - %s file not found, skipping removal", lockFilePath)
return nil
}
return fmt.Errorf("error removing lock file: %v", err)
}
_, err = os.Stat(lockFilePath)
exists = err == nil
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error checking lock file: %v", err)
}
log.Printf("[Git] removeLockFile - after removal, %s file exists: %t", lockFilePath, exists)
if exists {
log.Printf("[Git] removeLockFile - %s file still exists, retrying after delay", lockFilePath)
} else {
log.Printf("[Git] removeLockFile - %s file removed successfully", lockFilePath)
return nil
}
attempts++
time.Sleep(20 * time.Millisecond)
}
return nil
}
func gitRemoveIndexLockFileIfExists(repoDir string) error {
log.Printf("[Git] gitRemoveIndexLockFileIfExists - repoDir: %s", repoDir)
paths := []string{
filepath.Join(repoDir, ".git", "index.lock"),
filepath.Join(repoDir, ".git", "refs", "heads", "HEAD.lock"),
filepath.Join(repoDir, ".git", "HEAD.lock"),
}
errCh := make(chan error, len(paths))
for _, path := range paths {
go func(path string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in gitRemoveIndexLockFileIfExists: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in gitRemoveIndexLockFileIfExists: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if err := removeLockFile(path); err != nil {
errCh <- err
return
}
errCh <- nil
}(path)
}
errs := []error{}
for i := 0; i < len(paths); i++ {
err := <-errCh
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("error removing lock files: %v", errs)
}
return nil
}
func setGitConfig(repoDir, key, value string) error {
res, err := exec.Command("git", "-C", repoDir, "config", key, value).CombinedOutput()
if err != nil {
return fmt.Errorf("error setting git config %s to %s for dir: %s, err: %v, output: %s", key, value, repoDir, err, string(res))
}
return nil
}
func gitWriteOperation(operation func() error, repoDir, label string) error {
log.Printf("[Git] gitWriteOperation - label: %s", label)
var err error
for attempt := 0; attempt < maxGitRetries; attempt++ {
if attempt > 0 {
delay := time.Duration(1< 0 {
log.Printf("[Lock][%d] %d expired locks found, deleting | reason: %s", goroutineID, len(expiredLockIds), params.Reason)
if locksVerboseLogging {
log.Printf("deleting expired locks: %v", expiredLockIds)
}
query := "DELETE FROM repo_locks WHERE id = ANY($1)"
_, err := tx.Exec(query, pq.Array(expiredLockIds))
if err != nil {
if isDeadlockError(err) {
log.Println("deadlock clearing expired locks, won't do anything")
} else {
log.Printf("[Lock][%d] error removing expired locks: %v | reason: %s", goroutineID, err, params.Reason)
return "", fmt.Errorf("error removing expired locks: %v", err)
}
}
}
canAcquire := true
for _, lock := range locks {
lockBranch := ""
if lock.Branch != nil {
lockBranch = *lock.Branch
}
if scope == LockScopeRead {
// if we're trying to acquire a read lock, we can do so unless there's a conflicting lock
// a write lock always conflicts with a read lock (regardless of branch)
// a read lock conflicts if it's for a different branch (since it would need to checkout a different branch in the middle of an already-running read)
if lock.Scope == LockScopeWrite {
canAcquire = false
break
} else if lock.Scope == LockScopeRead {
if lockBranch != branch {
canAcquire = false
break
}
}
} else if scope == LockScopeWrite {
// if we're trying to acquire a write lock, we can only do so if there's no other lock (read or write)
canAcquire = false
break
} else {
err = fmt.Errorf("invalid lock scope: %v", scope)
return "", err
}
}
if !canAcquire {
if locksVerboseLogging {
log.Println("can't acquire lock.", "numRetry:", numRetry)
}
conflictErr := errors.New("lock conflict: cannot acquire read/write lock")
log.Printf("[Lock][%d] can't acquire lock, retrying: %v | reason: %s | now: %s | locks:\n%s\n", goroutineID, conflictErr, params.Reason, now, spew.Sdump(locks))
return retryWithExponentialBackoff(params.Ctx, conflictErr, numRetry, func(nextAttempt int) (string, error) {
return lockRepoDB(params, nextAttempt)
})
}
if locksVerboseLogging {
log.Println("can acquire lock - inserting new lock")
}
insertStart := time.Now()
if locksVerboseLogging {
log.Printf("[Lock][%d] Starting INSERT at %v | reason: %s", goroutineID, insertStart, params.Reason)
}
// Insert the new lock
var lockPlanBuildId *string
if planBuildId != "" {
lockPlanBuildId = &planBuildId
}
var lockBranch *string
if branch != "" {
lockBranch = &branch
}
newLock := &repoLock{
PlanId: planId,
OrgId: orgId,
PlanBuildId: lockPlanBuildId,
Scope: scope,
Branch: lockBranch,
}
if userId != "" {
newLock.UserId = &userId
}
var insertedId sql.NullString
insertQuery := "INSERT INTO repo_locks (org_id, user_id, plan_id, plan_build_id, scope, branch) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (plan_id) WHERE scope = 'w' DO NOTHING RETURNING id"
if locksVerboseLogging {
log.Printf("Insert query: %s", insertQuery)
}
err = tx.QueryRow(
insertQuery,
newLock.OrgId,
newLock.UserId,
newLock.PlanId,
newLock.PlanBuildId,
newLock.Scope,
newLock.Branch,
).Scan(&insertedId)
if err != nil {
if err == sql.ErrNoRows {
// Means ON CONFLICT DO NOTHING prevented insertion
// => concurrency conflict => backoff & retry
return retryWithExponentialBackoff(params.Ctx,
errors.New("lock conflict: row not inserted"),
numRetry,
func(nextAttempt int) (string, error) {
return lockRepoDB(params, nextAttempt)
},
)
}
log.Printf("[Lock][%d] error inserting new lock: %v | reason: %s", goroutineID, err, params.Reason)
return "", fmt.Errorf("error inserting new lock: %v", err)
}
if insertedId.Valid {
newLock.Id = insertedId.String
} else {
if locksVerboseLogging {
log.Printf("no rows returned from insert query, means there was a conflict")
}
return retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {
return lockRepoDB(params, nextAttempt)
})
}
if locksVerboseLogging {
log.Printf("[Lock][%d] INSERT took %v | reason: %s",
goroutineID, time.Since(insertStart), params.Reason)
}
// Commit the transaction
if err = tx.Commit(); err != nil {
return "", fmt.Errorf("error committing transaction: %v", err)
}
committed = true
activeLockIdsMu.Lock()
activeLockIds[newLock.Id] = true
activeLockIdsMu.Unlock()
log.Printf("Lock acquired: %s for plan %s with scope %s | reason: %s", newLock.Id, planId, scope, params.Reason)
// Start a goroutine to keep the lock alive
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in heartbeat goroutine: %v\n%s", r, debug.Stack())
cancelFn()
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in lock heartbeat goroutine: %v\n%s", r, debug.Stack()))
}
}()
onCancel := func() {
log.Printf("[Lock][Heartbeat] Timeout or context canceled during heartbeat loop for lock %s for plan %s | reason: %s", newLock.Id, planId, params.Reason)
}
numErrors := 0
for {
select {
case <-ctx.Done():
onCancel()
return
default:
jitter := time.Duration(rand.Int63n(int64(float64(lockHeartbeatInterval)*0.1)) * int64(numErrors+1))
log.Printf("[Lock][Heartbeat] Will update heartbeat for %s | %s | Heartbeat interval: %s, jitter: %s", planId, params.Reason, lockHeartbeatInterval, jitter)
select {
case <-ctx.Done():
onCancel()
return
case <-time.After(lockHeartbeatInterval + jitter):
}
log.Printf("[Lock][Heartbeat] %s | %s | Updating repo lock last heartbeat\n", planId, params.Reason)
res, err := Conn.Exec("UPDATE repo_locks SET last_heartbeat_at = NOW() WHERE id = $1", newLock.Id)
if err != nil {
log.Printf("[Lock][Heartbeat] %s | %s | Error updating repo lock last heartbeat: %v\n", planId, params.Reason, err)
if isDeadlockError(err) {
log.Printf("[Lock][Heartbeat] %s | %s | Heartbeat deadlock error, keep retrying\n", planId, params.Reason)
}
numErrors++
if numErrors > 5 {
log.Printf("[Lock][Heartbeat] %s | %s | Too many errors updating repo lock last heartbeat: %v\n", planId, params.Reason, err)
cancelFn()
return
}
} else {
// check if 0 rows were updated
rowsAffected, err := res.RowsAffected()
if err != nil {
log.Printf("Error getting rows affected: %v\n", err)
cancelFn()
return
}
if rowsAffected == 0 {
log.Printf("[Lock][Heartbeat] %s | %s | Lock not found: %s | stopping heartbeat loop\n", planId, params.Reason, newLock.Id)
return
}
log.Printf("[Lock][Heartbeat] %s | %s | Lock found: %s | continuing heartbeat loop\n", planId, params.Reason, newLock.Id)
}
}
}
}()
// check if git lock file exists
// remove it if so
err = gitRemoveIndexLockFileIfExists(getPlanDir(orgId, planId))
if err != nil {
log.Printf("[Lock] %s | %s | Error removing lock file: %v", planId, params.Reason, err)
return newLock.Id, fmt.Errorf("error removing lock file: %v", err)
}
if branch != "" {
// checkout the branch
err = gitCheckoutBranch(getPlanDir(orgId, planId), branch)
if err != nil {
log.Printf("[Lock] %s | %s | Error checking out branch: %v", planId, params.Reason, err)
return newLock.Id, fmt.Errorf("error checking out branch: %v", err)
}
log.Printf("[Lock] %s | %s | Checked out branch", planId, params.Reason)
}
return newLock.Id, nil
}
func deleteRepoLockDB(id, planId, reason string, numRetry int) error {
start := time.Now()
goroutineID := getGoroutineID()
if locksVerboseLogging {
log.Printf("[Lock][Delete][%d] START delete lock %s at %v | reason: %s", goroutineID, id, start, reason)
defer func() {
log.Printf("[Lock][Delete][%d] END delete lock took %v | reason: %s", goroutineID, time.Since(start), reason)
}()
}
result, err := Conn.Exec("DELETE FROM repo_locks WHERE id = $1", id)
if err != nil {
log.Printf("[Lock][Delete][%d] Error deleting lock: %v | reason: %s", goroutineID, err, reason)
err := retryDeleteLock(shutdown.ShutdownCtx, err, numRetry, func(nextAttempt int) error {
return deleteRepoLockDB(id, planId, reason, nextAttempt)
})
if err != nil {
log.Printf("[Lock][Delete][%d] Error deleting lock after retries: %v | %s | %s | %s", goroutineID, err, id, planId, reason)
return err
}
// retries succeeded, stop
return nil
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected > 0 {
log.Printf("[Lock][Delete][%d] Lock released: %s for plan %s | %s", goroutineID, id, planId, reason)
} else {
log.Printf("[Lock][Delete][%d] Lock not found: %s | %s | %s", goroutineID, id, planId, reason)
}
activeLockIdsMu.Lock()
delete(activeLockIds, id)
activeLockIdsMu.Unlock()
return nil
}
func formatStackTrace(stack []byte) string {
numLines := 10
if !locksVerboseLogging {
numLines = 5
}
return formatStackTraceWithNumLines(stack, numLines)
}
func formatStackTraceLong(stack []byte) string {
return formatStackTraceWithNumLines(stack, 20)
}
func formatStackTraceWithNumLines(stack []byte, numLines int) string {
lines := strings.Split(string(stack), "\n")
// Take first 10 meaningful lines of stack trace
// Skip runtime frames (first 7 lines) and limit to next 10 lines
relevantLines := lines[7:min(len(lines), 7+numLines)]
return strings.Join(relevantLines, "\n")
}
func getGoroutineID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
func isDeadlockError(err error) bool {
if err == nil {
return false
}
if pqErr, ok := err.(*pq.Error); ok && (pqErr.Code == "40001" || pqErr.Code == "40P01") {
return true
}
return false
}
func retryWithExponentialBackoff(
ctx context.Context,
cause error,
attempt int,
nextCall func(int) (string, error),
) (string, error) {
// If we have retried enough times, bail out.
if attempt >= maxLockRetries {
log.Printf("[Lock][Retry][%d] Failed to acquire lock after %d attempts: %v", getGoroutineID(), attempt, cause)
return "", fmt.Errorf("failed to acquire lock after %d attempts: %w", attempt, cause)
}
// Exponential delay: initialRetryDelay * 2^(attempt)
backoff := time.Duration(float64(initialLockRetryDelay) * math.Pow(backoffFactor, float64(attempt)))
// Add jitter: ± jitterFraction
jitterRange := time.Duration(float64(backoff) * jitterFraction)
jitter := time.Duration(rand.Int63n(int64(jitterRange)*2)) - jitterRange
wait := backoff + jitter
if wait < 0 {
wait = 0
}
log.Printf("[Lock][Retry][%d] Lock/transaction conflict (attempt #%d). Retrying in %s... (cause: %v)", getGoroutineID(), attempt, wait, cause)
select {
case <-ctx.Done():
log.Printf("[Lock][Retry][%d] Context canceled while waiting to retry: %v", getGoroutineID(), ctx.Err())
return "", fmt.Errorf("context canceled while waiting to retry: %w", ctx.Err())
case <-time.After(wait):
// Proceed with the next attempt.
}
return nextCall(attempt + 1)
}
func retryDeleteLock(ctx context.Context, cause error, attempt int, nextCall func(int) error) error {
if attempt >= maxDeleteRetries {
return fmt.Errorf("delete lock failed after 10 attempts: %w", cause)
}
// retry 10 times, no backoff or maybe a tiny 50ms
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(deleteRetryDelay):
}
return nextCall(attempt + 1)
}
func CleanupActiveLocks(ctx context.Context) error {
log.Println("Cleaning up any active repo locks...")
// Start a transaction with repeatable read isolation level
tx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
// Ensure rollback is attempted in case of failure
defer func() {
panicErr := recover()
if panicErr != nil {
log.Printf("panic in cleanup all locks: %v", panicErr)
}
if rbErr := tx.Rollback(); rbErr != nil {
if rbErr == sql.ErrTxDone {
// log.Println("attempted to roll back transaction, but it was already committed")
} else {
log.Printf("transaction rollback error: %v\n", rbErr)
}
} else {
if locksVerboseLogging {
log.Println("transaction rolled back")
}
}
}()
// Delete all active locks
query := "DELETE FROM repo_locks WHERE id = ANY($1)"
ids := make([]string, 0, len(activeLockIds))
for id := range activeLockIds {
ids = append(ids, id)
}
_, err = tx.Exec(query, pq.Array(ids))
if err != nil {
if err == sql.ErrNoRows {
log.Println("No active locks to cleanup")
} else {
return fmt.Errorf("error removing all locks: %v", err)
}
}
// Commit the transaction
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
activeLockIdsMu.Lock()
activeLockIds = make(map[string]bool)
activeLockIdsMu.Unlock()
log.Println("Successfully cleaned up all repo locks")
return nil
}
================================================
FILE: app/server/db/models.go
================================================
package db
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
func UpsertCustomModel(tx *sqlx.Tx, model *CustomModel) error {
if tx == nil {
return fmt.Errorf("tx is nil")
}
query := `
INSERT INTO custom_models (
org_id, model_id,
publisher, description,
max_tokens, default_max_convo_tokens, max_output_tokens, reserved_output_tokens,
has_image_support, preferred_output_format,
system_prompt_disabled, role_params_disabled, stop_disabled,
predicted_output_enabled, reasoning_effort_enabled, reasoning_effort,
include_reasoning, reasoning_budget, supports_cache_control,
single_message_no_system_prompt, token_estimate_padding_pct,
providers
)
VALUES (
$1,$2,
$3,$4,
$5,$6,$7,$8,
$9,$10,
$11,$12,$13,
$14,$15,$16,
$17,$18,$19,
$20,$21,
$22
)
ON CONFLICT (org_id, model_id)
DO UPDATE SET
publisher = EXCLUDED.publisher,
description = EXCLUDED.description,
max_tokens = EXCLUDED.max_tokens,
default_max_convo_tokens = EXCLUDED.default_max_convo_tokens,
max_output_tokens = EXCLUDED.max_output_tokens,
reserved_output_tokens = EXCLUDED.reserved_output_tokens,
has_image_support = EXCLUDED.has_image_support,
preferred_output_format = EXCLUDED.preferred_output_format,
system_prompt_disabled = EXCLUDED.system_prompt_disabled,
role_params_disabled = EXCLUDED.role_params_disabled,
stop_disabled = EXCLUDED.stop_disabled,
predicted_output_enabled = EXCLUDED.predicted_output_enabled,
reasoning_effort_enabled = EXCLUDED.reasoning_effort_enabled,
reasoning_effort = EXCLUDED.reasoning_effort,
include_reasoning = EXCLUDED.include_reasoning,
reasoning_budget = EXCLUDED.reasoning_budget,
supports_cache_control = EXCLUDED.supports_cache_control,
single_message_no_system_prompt = EXCLUDED.single_message_no_system_prompt,
token_estimate_padding_pct = EXCLUDED.token_estimate_padding_pct,
providers = EXCLUDED.providers
RETURNING id, created_at, updated_at;
`
return tx.QueryRow(
query,
model.OrgId,
model.ModelId,
model.Publisher,
model.Description,
model.MaxTokens,
model.DefaultMaxConvoTokens,
model.MaxOutputTokens,
model.ReservedOutputTokens,
model.HasImageSupport,
model.PreferredOutputFormat,
model.SystemPromptDisabled,
model.RoleParamsDisabled,
model.StopDisabled,
model.PredictedOutputEnabled,
model.ReasoningEffortEnabled,
model.ReasoningEffort,
model.IncludeReasoning,
model.ReasoningBudget,
model.SupportsCacheControl,
model.SingleMessageNoSystemPrompt,
model.TokenEstimatePaddingPct,
model.Providers,
).Scan(&model.Id, &model.CreatedAt, &model.UpdatedAt)
}
func ListCustomModels(orgId string) ([]*CustomModel, error) {
var models []*CustomModel
err := Conn.Select(&models, `SELECT * FROM custom_models WHERE org_id = $1 ORDER BY created_at`, orgId)
return models, err
}
func ListCustomModelsForModelIds(orgId string, modelIds []string) ([]*CustomModel, error) {
var models []*CustomModel
query := `SELECT * FROM custom_models WHERE org_id = $1 AND model_id = ANY($2) ORDER BY created_at`
err := Conn.Select(&models, query, orgId, pq.Array(modelIds))
return models, err
}
func GetCustomModel(orgId, id string) (*CustomModel, error) {
var model CustomModel
err := Conn.Get(&model, `SELECT * FROM custom_models WHERE org_id = $1 AND id = $2`, orgId, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &model, nil
}
func DeleteCustomModels(tx *sqlx.Tx, orgId string, ids []string) error {
if tx == nil {
return fmt.Errorf("tx is nil")
}
_, err := tx.Exec(`DELETE FROM custom_models WHERE org_id = $1 AND id = ANY($2)`, orgId, pq.Array(ids))
if err != nil {
return fmt.Errorf("error deleting custom models: %v", err)
}
return nil
}
func UpsertCustomProvider(tx *sqlx.Tx, p *CustomProvider) error {
if tx == nil {
return fmt.Errorf("tx is nil")
}
const q = `
INSERT INTO custom_providers (
org_id, name, base_url,
skip_auth, api_key_env_var, extra_auth_vars
)
VALUES (
$1,$2,$3,
$4,$5,$6
)
ON CONFLICT (org_id, name)
DO UPDATE SET
base_url = EXCLUDED.base_url,
skip_auth = EXCLUDED.skip_auth,
api_key_env_var = EXCLUDED.api_key_env_var,
extra_auth_vars = EXCLUDED.extra_auth_vars
RETURNING id, created_at, updated_at;
`
return tx.QueryRow(
q,
p.OrgId,
p.Name,
p.BaseUrl,
p.SkipAuth,
p.ApiKeyEnvVar,
p.ExtraAuthVars,
).Scan(&p.Id, &p.CreatedAt, &p.UpdatedAt)
}
func GetCustomProvider(orgId, id string) (*CustomProvider, error) {
var provider CustomProvider
err := Conn.Get(&provider, `SELECT * FROM custom_providers WHERE org_id = $1 AND id = $2`, orgId, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &provider, nil
}
func ListCustomProviders(orgId string) ([]*CustomProvider, error) {
var providers []*CustomProvider
err := Conn.Select(&providers, `SELECT * FROM custom_providers WHERE org_id = $1 ORDER BY name`, orgId)
return providers, err
}
func ListCustomProvidersForNames(orgId string, names []string) ([]*CustomProvider, error) {
var providers []*CustomProvider
query := `SELECT * FROM custom_providers WHERE org_id = $1 AND name = ANY($2) ORDER BY name`
err := Conn.Select(&providers, query, orgId, pq.Array(names))
return providers, err
}
func DeleteCustomProviders(tx *sqlx.Tx, orgId string, ids []string) error {
if tx == nil {
return fmt.Errorf("tx is nil")
}
_, err := tx.Exec(`DELETE FROM custom_providers WHERE org_id = $1 AND id = ANY($2)`, orgId, pq.Array(ids))
if err != nil {
return fmt.Errorf("error deleting custom providers: %v", err)
}
return nil
}
func UpsertModelPack(tx *sqlx.Tx, mp *ModelPack) error {
if tx == nil {
return fmt.Errorf("tx is nil")
}
const q = `
INSERT INTO model_sets (
org_id, name, description,
planner, coder, plan_summary,
builder, whole_file_builder, namer,
commit_msg, exec_status, context_loader
)
VALUES (
$1,$2,$3,
$4,$5,$6,
$7,$8,$9,
$10,$11,$12
)
ON CONFLICT (org_id, name)
DO UPDATE SET
description = EXCLUDED.description,
planner = EXCLUDED.planner,
coder = EXCLUDED.coder,
plan_summary = EXCLUDED.plan_summary,
builder = EXCLUDED.builder,
whole_file_builder = EXCLUDED.whole_file_builder,
namer = EXCLUDED.namer,
commit_msg = EXCLUDED.commit_msg,
exec_status = EXCLUDED.exec_status,
context_loader = EXCLUDED.context_loader
RETURNING id, created_at;
`
return tx.QueryRow(
q,
mp.OrgId,
mp.Name,
mp.Description,
mp.Planner,
mp.Coder,
mp.PlanSummary,
mp.Builder,
mp.WholeFileBuilder,
mp.Namer,
mp.CommitMsg,
mp.ExecStatus,
mp.Architect,
).Scan(&mp.Id, &mp.CreatedAt)
}
func ListModelPacks(orgId string) ([]*ModelPack, error) {
var modelPacks []*ModelPack
query := `SELECT * FROM model_sets WHERE org_id = $1`
err := Conn.Select(&modelPacks, query, orgId)
if err != nil {
return nil, fmt.Errorf("error fetching model packs: %v", err)
}
return modelPacks, nil
}
func ListModelPacksForNames(orgId string, names []string) ([]*ModelPack, error) {
var modelPacks []*ModelPack
query := `SELECT * FROM model_sets WHERE org_id = $1 AND name = ANY($2)`
err := Conn.Select(&modelPacks, query, orgId, names)
return modelPacks, err
}
func DeleteModelPacks(tx *sqlx.Tx, orgId string, ids []string) error {
if tx == nil {
return fmt.Errorf("tx is nil")
}
_, err := tx.Exec(`DELETE FROM model_sets WHERE org_id = $1 AND id = ANY($2)`, orgId, pq.Array(ids))
if err != nil {
return fmt.Errorf("error deleting model pack: %v", err)
}
return nil
}
================================================
FILE: app/server/db/org_helpers.go
================================================
package db
import (
"database/sql"
"fmt"
"log"
"strings"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
const orgFields = "id, name, domain, auto_add_domain_users, owner_id, is_trial, created_at, updated_at"
func GetAccessibleOrgsForUser(user *User) ([]*Org, error) {
// direct access
var orgUsers []*OrgUser
var orgs []*Org
err := Conn.Select(&orgUsers, "SELECT * FROM orgs_users WHERE user_id = $1", user.Id)
if err != nil {
return nil, fmt.Errorf("error getting orgs for user: %v", err)
}
orgRoleIdByOrgId := map[string]string{}
orgIds := []string{}
for _, ou := range orgUsers {
orgIds = append(orgIds, ou.OrgId)
orgRoleIdByOrgId[ou.OrgId] = ou.OrgRoleId
}
if len(orgIds) > 0 {
query := fmt.Sprintf("SELECT %s FROM orgs WHERE id = ANY($1)", orgFields)
err = Conn.Select(&orgs, query, pq.Array(orgIds))
if err != nil {
return nil, fmt.Errorf("error getting orgs for user: %v", err)
}
} else {
log.Println("No orgs found for user")
return orgs, nil
}
// access via invitation
invites, err := GetPendingInvitesForEmail(user.Email)
if err != nil {
return nil, fmt.Errorf("error getting invites for user: %v", err)
}
orgIds = []string{}
for _, invite := range invites {
orgIds = append(orgIds, invite.OrgId)
orgRoleIdByOrgId[invite.OrgId] = invite.OrgRoleId
}
if len(orgIds) > 0 {
var orgsFromInvites []*Org
query := fmt.Sprintf("SELECT %s FROM orgs WHERE id = ANY($1)", orgFields)
err = Conn.Select(&orgsFromInvites, query, pq.Array(orgIds))
if err != nil {
return nil, fmt.Errorf("error getting orgs from invites: %v", err)
}
orgs = append(orgs, orgsFromInvites...)
}
return orgs, nil
}
func GetOrg(orgId string) (*Org, error) {
var org Org
query := fmt.Sprintf("SELECT %s FROM orgs WHERE id = $1", orgFields)
err := Conn.Get(&org, query, orgId)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("org not found")
}
return nil, fmt.Errorf("error getting org: %v", err)
}
return &org, nil
}
func ValidateOrgMembership(userId string, orgId string) (bool, error) {
var count int
err := Conn.QueryRow("SELECT COUNT(*) FROM orgs_users WHERE user_id = $1 AND org_id = $2", userId, orgId).Scan(&count)
if err != nil {
return false, fmt.Errorf("error validating org membership: %v", err)
}
return count > 0, nil
}
func CreateOrg(req *shared.CreateOrgRequest, userId string, domain *string, tx *sqlx.Tx) (*Org, error) {
org := &Org{
Name: req.Name,
Domain: domain,
AutoAddDomainUsers: req.AutoAddDomainUsers,
OwnerId: userId,
}
err := tx.QueryRow("INSERT INTO orgs (name, domain, auto_add_domain_users, owner_id, is_trial) VALUES ($1, $2, $3, $4, false) RETURNING id", req.Name, domain, req.AutoAddDomainUsers, userId).Scan(&org.Id)
if err != nil {
if IsNonUniqueErr(err) {
// Handle the uniqueness constraint violation
return nil, fmt.Errorf("an org with domain %s already exists", *domain)
}
return nil, fmt.Errorf("error creating org: %v", err)
}
orgOwnerRoleId, err := GetOrgOwnerRoleId()
if err != nil {
return nil, fmt.Errorf("error getting org owner role id: %v", err)
}
_, err = tx.Exec("INSERT INTO orgs_users (org_id, user_id, org_role_id) VALUES ($1, $2, $3)", org.Id, userId, orgOwnerRoleId)
if err != nil {
return nil, fmt.Errorf("error adding org membership: %v", err)
}
return org, nil
}
func GetOrgForDomain(domain string) (*Org, error) {
var org Org
query := fmt.Sprintf("SELECT %s FROM orgs WHERE domain = $1", orgFields)
err := Conn.Get(&org, query, domain)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting org for domain: %v", err)
}
return &org, nil
}
func AddOrgDomainUsers(orgId, domain string, tx *sqlx.Tx) error {
usersForDomain, err := GetUsersForDomain(domain)
if err != nil {
return fmt.Errorf("error getting users for domain: %v", err)
}
orgMemberRoleId, err := GetOrgMemberRoleId()
if err != nil {
return fmt.Errorf("error getting org member role id: %v", err)
}
if len(usersForDomain) > 0 {
// create org users for each user
var valueStrings []string
var valueArgs []interface{}
for i, user := range usersForDomain {
num := i * 3
valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d, $%d)", num+1, num+2, num+3))
valueArgs = append(valueArgs, orgId, user.Id, orgMemberRoleId)
}
// Join all value strings and execute a single query
stmt := fmt.Sprintf("INSERT INTO orgs_users (org_id, user_id, org_role_id) VALUES %s ON CONFLICT ON CONSTRAINT org_user_unique DO NOTHING", strings.Join(valueStrings, ","))
_, err = tx.Exec(stmt, valueArgs...)
if err != nil {
return fmt.Errorf("error adding org users: %v", err)
}
}
return nil
}
func DeleteOrgUser(orgId, userId string, tx *sqlx.Tx) error {
log.Printf("Deleting org user, org: %s | user: %s\n", orgId, userId)
_, err := tx.Exec("DELETE FROM orgs_users WHERE org_id = $1 AND user_id = $2", orgId, userId)
if err != nil {
return fmt.Errorf("error deleting org member: %v", err)
}
return nil
}
func CreateOrgUser(orgId, userId, orgRoleId string, tx *sqlx.Tx) error {
query := "INSERT INTO orgs_users (org_id, user_id, org_role_id) VALUES ($1, $2, $3)"
var err error
if tx == nil {
_, err = Conn.Exec(query, orgId, userId, orgRoleId)
} else {
_, err = tx.Exec(query, orgId, userId, orgRoleId)
}
if err != nil {
return fmt.Errorf("error adding org member: %v", err)
}
return nil
}
func ListOrgRoles(orgId string) ([]*OrgRole, error) {
var orgRoles []*OrgRole
err := Conn.Select(&orgRoles, "SELECT * FROM org_roles WHERE org_id IS NULL OR org_id = $1", orgId)
if err != nil {
return nil, fmt.Errorf("error listing org roles: %v", err)
}
return orgRoles, nil
}
func AddToOrgForDomain(userId, domain string, tx *sqlx.Tx) (string, error) {
org, err := GetOrgForDomain(domain)
if err != nil {
return "", fmt.Errorf("error getting org for domain: %v", err)
}
orgOwnerRoleId, err := GetOrgOwnerRoleId()
if err != nil {
return "", fmt.Errorf("error getting org owner role id: %v", err)
}
if org != nil && org.AutoAddDomainUsers {
err = CreateOrgUser(org.Id, userId, orgOwnerRoleId, tx)
if err != nil {
return "", fmt.Errorf("error adding org user: %v", err)
}
}
var orgId string
if org != nil {
orgId = org.Id
}
return orgId, nil
}
================================================
FILE: app/server/db/plan_config_helpers.go
================================================
package db
import (
"fmt"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
)
func GetPlanConfig(planId string) (*shared.PlanConfig, error) {
query := "SELECT plan_config FROM plans WHERE id = $1"
var config shared.PlanConfig
err := Conn.Get(&config, query, planId)
if err != nil {
return nil, fmt.Errorf("error getting plan config: %v", err)
}
return &config, nil
}
func StorePlanConfig(planId string, config *shared.PlanConfig) error {
query := `
UPDATE plans
SET plan_config = $1
WHERE id = $2
`
_, err := Conn.Exec(query, config, planId)
if err != nil {
return fmt.Errorf("error storing plan config: %v", err)
}
return nil
}
func GetDefaultPlanConfig(userId string) (*shared.PlanConfig, error) {
query := "SELECT default_plan_config FROM users WHERE id = $1"
var config shared.PlanConfig
err := Conn.Get(&config, query, userId)
if err != nil {
return nil, fmt.Errorf("error getting default plan config: %v", err)
}
return &config, nil
}
func StoreDefaultPlanConfig(userId string, config *shared.PlanConfig, tx *sqlx.Tx) error {
query := `
UPDATE users SET default_plan_config = $1 WHERE id = $2
`
_, err := tx.Exec(query, config, userId)
if err != nil {
return fmt.Errorf("error storing default plan config: %v", err)
}
return nil
}
================================================
FILE: app/server/db/plan_helpers.go
================================================
package db
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"time"
shared "plandex-shared"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/sashabaranov/go-openai"
)
func CreatePlan(ctx context.Context, orgId, projectId, userId, name string) (*Plan, error) {
var plan *Plan
err := WithTx(ctx, "create plan", func(tx *sqlx.Tx) error {
planConfig, err := GetDefaultPlanConfig(userId)
if err != nil {
return fmt.Errorf("error getting default plan config: %v", err)
}
query := `INSERT INTO plans (org_id, owner_id, project_id, name, plan_config)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, created_at, updated_at`
plan = &Plan{
OrgId: orgId,
OwnerId: userId,
ProjectId: projectId,
Name: name,
PlanConfig: planConfig,
}
err = tx.QueryRow(
query,
orgId,
userId,
projectId,
name,
planConfig,
).Scan(
&plan.Id,
&plan.CreatedAt,
&plan.UpdatedAt,
)
if err != nil {
return fmt.Errorf("error creating plan: %v", err)
}
_, err = tx.Exec("INSERT INTO lockable_plan_ids (plan_id) VALUES ($1)", plan.Id)
if err != nil {
return fmt.Errorf("error inserting lockable plan id: %v", err)
}
// the one place where we do this to skip the locking queue
// ok to cheat this once since we're creating a new plan
repo := getGitRepo(orgId, plan.Id)
_, err = CreateBranch(repo, plan, nil, "main", tx)
if err != nil {
return fmt.Errorf("error creating main branch: %v", err)
}
log.Println("Created branch main")
err = InitPlan(orgId, plan.Id)
if err != nil {
return fmt.Errorf("error initializing plan dir: %v", err)
}
log.Println("Initialized plan dir")
return nil
})
if err != nil {
return nil, err
}
return plan, nil
}
func ListOwnedPlans(projectIds []string, userId string, archived bool) ([]*Plan, error) {
qs := "SELECT * FROM plans WHERE project_id = ANY($1) AND owner_id = $2"
qargs := []interface{}{pq.Array(projectIds), userId}
if archived {
qs += " AND archived_at IS NOT NULL"
} else {
qs += " AND archived_at IS NULL"
}
qs += " ORDER BY updated_at DESC"
var plans []*Plan
err := Conn.Select(&plans, qs, qargs...)
if err != nil {
return nil, fmt.Errorf("error listing plans: %v", err)
}
return plans, nil
}
func GetPlanNamesById(planIds []string) (map[string]string, error) {
var plans []*Plan
err := Conn.Select(&plans, "SELECT id, name FROM plans WHERE id = ANY($1)", pq.Array(planIds))
if err != nil {
return nil, fmt.Errorf("error getting plan names: %v", err)
}
namesMap := make(map[string]string)
for _, plan := range plans {
namesMap[plan.Id] = plan.Name
}
return namesMap, nil
}
func AddPlanContextTokens(planId, branch string, addTokens int) error {
_, err := Conn.Exec("UPDATE branches SET context_tokens = context_tokens + $1 WHERE plan_id = $2 AND name = $3", addTokens, planId, branch)
if err != nil {
return fmt.Errorf("error updating plan tokens: %v", err)
}
return nil
}
func AddPlanConvoMessage(msg *ConvoMessage, branch string) error {
errCh := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in AddPlanConvoMessage: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in AddPlanConvoMessage: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
_, err := Conn.Exec("UPDATE branches SET convo_tokens = convo_tokens + $1 WHERE plan_id = $2 AND name = $3", msg.Tokens, msg.PlanId, branch)
if err != nil {
errCh <- fmt.Errorf("error updating plan tokens: %v", err)
return
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in AddPlanConvoMessage: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in AddPlanConvoMessage: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if msg.Role != openai.ChatMessageRoleAssistant {
errCh <- nil
return
}
_, err := Conn.Exec("UPDATE plans SET total_replies = total_replies + 1 WHERE id = $1", msg.PlanId)
if err != nil {
errCh <- fmt.Errorf("error updating plan total replies: %v", err)
}
errCh <- nil
}()
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error updating plan tokens: %v", err)
}
}
return nil
}
func SyncPlanTokens(orgId, planId, branch string) error {
var contexts []*Context
var convos []*ConvoMessage
errCh := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var err error
contexts, err = GetPlanContexts(orgId, planId, false, false)
errCh <- err
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in SyncPlanTokens: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var err error
convos, err = GetPlanConvo(orgId, planId)
errCh <- err
}()
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error getting contexts or convo: %v", err)
}
}
contextTokens := 0
for _, context := range contexts {
contextTokens += context.NumTokens
}
convoTokens := 0
for _, msg := range convos {
convoTokens += msg.Tokens
}
_, err := Conn.Exec("UPDATE branches SET context_tokens = $1, convo_tokens = $2 WHERE plan_id = $3 AND name = $4", contextTokens, convoTokens, planId, branch)
if err != nil {
return fmt.Errorf("error updating plan tokens: %v", err)
}
return nil
}
func GetPlan(planId string) (*Plan, error) {
var plan Plan
err := Conn.Get(&plan, "SELECT * FROM plans WHERE id = $1", planId)
if err != nil {
return nil, fmt.Errorf("error getting plan: %v", err)
}
return &plan, nil
}
func SetPlanStatus(planId, branch string, status shared.PlanStatus, errStr string) error {
_, err := Conn.Exec("UPDATE branches SET status = $1, error = $2 WHERE plan_id = $3 AND name = $4", status, errStr, planId, branch)
if err != nil {
return fmt.Errorf("error setting plan status: %v", err)
}
return nil
}
func RenamePlan(planId string, name string, tx *sqlx.Tx) error {
var err error
if tx == nil {
_, err = Conn.Exec("UPDATE plans SET name = $1 WHERE id = $2", name, planId)
} else {
_, err = tx.Exec("UPDATE plans SET name = $1 WHERE id = $2", name, planId)
}
if err != nil {
return fmt.Errorf("error renaming plan: %v", err)
}
return nil
}
func IncActiveBranches(planId string, inc int, tx *sqlx.Tx) error {
_, err := tx.Exec("UPDATE plans SET active_branches = active_branches + $1 WHERE id = $2", inc, planId)
if err != nil {
return fmt.Errorf("error updating plan active branches: %v", err)
}
return nil
}
func IncNumNonDraftPlans(userId string, tx *sqlx.Tx) error {
_, err := tx.Exec("UPDATE users SET num_non_draft_plans = num_non_draft_plans + 1 WHERE id = $1", userId)
if err != nil {
return fmt.Errorf("error updating user num_non_draft_plans: %v", err)
}
return nil
}
func StoreDescription(description *ConvoMessageDescription) error {
descriptionsDir := getPlanDescriptionsDir(description.OrgId, description.PlanId)
err := os.MkdirAll(descriptionsDir, os.ModePerm)
if err != nil {
return fmt.Errorf("error creating convo message descriptions dir: %v", err)
}
for _, op := range description.Operations {
if op.Content != "" {
quoted := strconv.Quote(op.Content)
op.Content = quoted[1 : len(quoted)-1]
}
if op.Description != "" {
quoted := strconv.Quote(op.Description)
op.Description = quoted[1 : len(quoted)-1]
}
}
now := time.Now()
if description.Id == "" {
description.Id = uuid.New().String()
description.CreatedAt = now
}
description.UpdatedAt = now
bytes, err := json.Marshal(description)
if err != nil {
return fmt.Errorf("error marshalling convo message description: %v", err)
}
err = os.WriteFile(filepath.Join(descriptionsDir, description.Id+".json"), bytes, os.ModePerm)
if err != nil {
return fmt.Errorf("error writing convo message description: %v", err)
}
return nil
}
func DeleteDraftPlans(orgId, projectId, userId string) error {
res, err := Conn.Query("DELETE FROM plans WHERE project_id = $1 AND owner_id = $2 AND name = 'draft' RETURNING id;", projectId, userId)
if err != nil {
return fmt.Errorf("error deleting draft plans: %v", err)
}
defer res.Close()
// get ids
var ids []string
for res.Next() {
var id string
err := res.Scan(&id)
if err != nil {
return fmt.Errorf("error scanning deleted draft plan id: %v", err)
}
ids = append(ids, id)
}
errCh := make(chan error, len(ids))
for _, planId := range ids {
go func(planId string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in DeleteDraftPlans: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in DeleteDraftPlans: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
errCh <- DeletePlanDir(orgId, planId)
}(planId)
}
for i := 0; i < len(ids); i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error deleting draft plan dir: %v", err)
}
}
if len(ids) > 0 {
log.Println("Deleted", len(ids), "draft plans")
}
return nil
}
func DeleteOwnerPlans(orgId, projectId, userId string) error {
res, err := Conn.Query("DELETE FROM plans WHERE project_id = $1 AND owner_id = $2 RETURNING id;", projectId, userId)
if err != nil {
return fmt.Errorf("error deleting plans: %v", err)
}
defer res.Close()
// get ids
var ids []string
for res.Next() {
var id string
err := res.Scan(&id)
if err != nil {
return fmt.Errorf("error scanning deleted draft plan id: %v", err)
}
ids = append(ids, id)
}
errCh := make(chan error, len(ids))
for _, planId := range ids {
go func(planId string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in DeleteOwnerPlans: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in DeleteOwnerPlans: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
errCh <- DeletePlanDir(orgId, planId)
}(planId)
}
for i := 0; i < len(ids); i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error deleting plan dir: %v", err)
}
}
if len(ids) > 0 {
log.Println("Deleted", len(ids), "plans")
}
return nil
}
func ValidatePlanAccess(planId, userId, orgId string) (*Plan, error) {
// get plan
plan, err := GetPlan(planId)
if err != nil {
return nil, fmt.Errorf("error getting plan: %v", err)
}
if plan == nil {
return nil, nil
}
if plan.OrgId != orgId {
return nil, nil
}
hasProjectAccess, err := ProjectExists(orgId, plan.ProjectId)
if err != nil {
return nil, fmt.Errorf("error validating project membership: %v", err)
}
if !hasProjectAccess {
return nil, nil
}
// owner has access
if plan.OwnerId == userId {
return plan, nil
}
// plan is shared with org
if plan.SharedWithOrgAt != nil {
return plan, nil
}
return nil, nil
}
func BumpPlanUpdatedAt(planId string, t time.Time) error {
_, err := Conn.Exec("UPDATE plans SET updated_at = $1 WHERE id = $2", t, planId)
if err != nil {
return fmt.Errorf("error updating plan updated at: %v", err)
}
return nil
}
func GetPlanIdsForProject(projectId string) ([]string, error) {
var ids []string
err := Conn.Select(&ids, "SELECT id FROM plans WHERE project_id = $1", projectId)
if err != nil {
return nil, fmt.Errorf("error getting plan ids for project: %v", err)
}
return ids, nil
}
================================================
FILE: app/server/db/project_helpers.go
================================================
package db
import (
"fmt"
"github.com/jmoiron/sqlx"
)
func ProjectExists(orgId, projectId string) (bool, error) {
var count int
err := Conn.QueryRow("SELECT COUNT(*) FROM projects WHERE org_id = $1 AND id = $2", orgId, projectId).Scan(&count)
if err != nil {
return false, fmt.Errorf("error checking if project exists: %v", err)
}
return count > 0, nil
}
func CreateProject(orgId, name string, tx *sqlx.Tx) (string, error) {
var projectId string
err := tx.QueryRow("INSERT INTO projects (org_id, name) VALUES ($1, $2) RETURNING id", orgId, name).Scan(&projectId)
if err != nil {
return "", fmt.Errorf("error creating project: %v", err)
}
return projectId, nil
}
================================================
FILE: app/server/db/queue.go
================================================
package db
import (
"context"
"fmt"
"log"
"runtime/debug"
"sync"
"github.com/google/uuid"
)
type repoOpFn func(repo *GitRepo) error
type repoOperation struct {
orgId string
userId string
planId string
branch string
scope LockScope
planBuildId string
id string
reason string
op repoOpFn
ctx context.Context
cancelFn context.CancelFunc
done chan error
clearRepoOnErr bool
}
type repoQueue struct {
ops []*repoOperation
mu sync.Mutex
isProcessing bool
}
type repoQueueMap map[string]*repoQueue
var queuesMu sync.Mutex
var repoQueues = make(repoQueueMap)
func (m repoQueueMap) getQueue(planId string) *repoQueue {
queuesMu.Lock()
defer queuesMu.Unlock()
if locksVerboseLogging {
log.Printf("[Queue] Getting queue for plan %s", planId)
}
q, ok := m[planId]
if !ok {
if locksVerboseLogging {
log.Printf("[Queue] Creating new queue for plan %s", planId)
}
q = &repoQueue{}
m[planId] = q
}
return q
}
func (m repoQueueMap) add(op *repoOperation) int {
if locksVerboseLogging {
log.Printf("[Queue] Adding operation %s (%s) to queue for plan %s", op.id, op.reason, op.planId)
}
q := m.getQueue(op.planId)
return q.add(op)
}
// Add enqueues an operation, and then kicks off processing if needed.
func (q *repoQueue) add(op *repoOperation) int {
var numOps int
q.mu.Lock()
q.ops = append(q.ops, op)
numOps = len(q.ops)
if locksVerboseLogging {
log.Printf("[Queue] Operation %s (%s) enqueued, queue length now %d", op.id, op.reason, numOps)
}
// If nobody else is processing, we'll start
if !q.isProcessing {
if locksVerboseLogging {
log.Printf("[Queue] Starting queue processing for operation %s (%s)", op.id, op.reason)
}
q.isProcessing = true
go q.runQueue() // run in the background
} else if locksVerboseLogging {
log.Printf("[Queue] Queue already processing, operation %s (%s) will wait", op.id, op.reason)
}
q.mu.Unlock()
return numOps
}
func (q *repoQueue) nextBatch() []*repoOperation {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.ops) == 0 {
if locksVerboseLogging {
log.Printf("[Queue] No operations in queue")
}
return nil
}
firstOp := q.ops[0]
res := []*repoOperation{firstOp}
if locksVerboseLogging {
log.Printf("[Queue] Processing first operation %s (%s) with scope %s, branch %s",
firstOp.id, firstOp.reason, firstOp.scope, firstOp.branch)
}
q.ops = q.ops[1:]
// writes always go one at a time, blocking everything else, as do read locks on the root plan (no branch)
if firstOp.scope == LockScopeWrite || firstOp.branch == "" {
if locksVerboseLogging {
log.Printf("[Queue] Operation %s is write or root branch read, processing alone", firstOp.id)
}
return res
}
// reads go in parallel as long as they are on the same branch
for len(q.ops) > 0 {
op := q.ops[0]
if op.scope == LockScopeRead && op.branch == firstOp.branch {
if locksVerboseLogging {
log.Printf("[Queue] Batching compatible read operation %s (%s) with same branch %s",
op.id, op.reason, op.branch)
}
res = append(res, op)
q.ops = q.ops[1:]
} else {
if locksVerboseLogging {
log.Printf("[Queue] Operation %s (%s) with scope %s, branch %s not compatible with batch, stopping",
op.id, op.reason, op.scope, op.branch)
}
break
}
}
if locksVerboseLogging {
log.Printf("[Queue] Created batch of %d operations", len(res))
}
return res
}
func (q *repoQueue) runQueue() {
if locksVerboseLogging {
log.Printf("[Queue] Starting queue processing")
}
for {
// get the next batch
ops := q.nextBatch()
if len(ops) == 0 {
// Nothing left in the queue, so mark not processing and return
if locksVerboseLogging {
log.Printf("[Queue] Queue empty, stopping processing")
}
q.mu.Lock()
q.isProcessing = false
q.mu.Unlock()
return
}
firstOp := ops[0]
func() {
if locksVerboseLogging {
log.Printf("[Queue] Attempting to acquire DB lock for plan %s, branch %s, scope %s",
firstOp.planId, firstOp.branch, firstOp.scope)
}
lockId, err := lockRepoDB(LockRepoParams{
OrgId: firstOp.orgId,
UserId: firstOp.userId,
PlanId: firstOp.planId,
Branch: firstOp.branch,
Scope: firstOp.scope,
PlanBuildId: firstOp.planBuildId,
Reason: firstOp.reason,
Ctx: firstOp.ctx,
CancelFn: firstOp.cancelFn,
}, 0)
if lockId != "" {
log.Printf("[Queue] Acquired DB lock %s", lockId)
defer func() {
log.Printf("[Queue] Releasing DB lock %s for plan %s", lockId, firstOp.planId)
releaseErr := deleteRepoLockDB(lockId, firstOp.planId, firstOp.reason, 0)
if releaseErr != nil {
log.Printf("[Queue] Failed to release DB lock: %v", releaseErr)
} else {
log.Printf("[Queue] DB lock %s released successfully", lockId)
}
}()
}
if err != nil {
log.Printf("[Queue] Failed to get DB lock: %v", err)
for _, op := range ops {
if locksVerboseLogging {
log.Printf("[Queue] Notifying operation %s (%s) of lock failure", op.id, op.reason)
}
op.done <- fmt.Errorf("failed to get DB lock: %w", err)
}
// we still need to process the rest of the queue
// if the error is critical, caller will handle it
return
}
if locksVerboseLogging {
log.Printf("[Queue] Acquired DB lock %s, processing batch of %d operations", lockId, len(ops))
}
repo := getGitRepo(firstOp.orgId, firstOp.planId)
var needsRollback bool
// Process the batch
// If it's a writer => single op
// If multiple same‐branch readers => do them in parallel
var wg sync.WaitGroup
for _, op := range ops {
wg.Add(1)
go func(op *repoOperation) {
defer wg.Done()
select {
case <-op.ctx.Done():
if locksVerboseLogging {
log.Printf("[Queue] Operation %s (%s) context canceled", op.id, op.reason)
}
op.done <- op.ctx.Err()
default:
if locksVerboseLogging {
log.Printf("[Queue] Starting operation %s (%s)", op.id, op.reason)
}
// actually do the operation
var opErr error
func() {
defer func() {
panicErr := recover()
if panicErr != nil {
log.Printf("[Queue] Panic in operation %s (%s): %v", op.id, op.reason, panicErr)
log.Printf("[Queue] Stack trace: %s", string(debug.Stack()))
opErr = fmt.Errorf("panic in operation: %v\n%s", panicErr, string(debug.Stack()))
}
if opErr != nil && op.scope == LockScopeWrite && op.clearRepoOnErr {
if locksVerboseLogging {
log.Printf("[Queue] Operation %s (%s) failed with error, marking for rollback: %v",
op.id, op.reason, opErr)
}
needsRollback = true
}
}()
if locksVerboseLogging {
log.Printf("[Queue] Executing operation %s (%s)", op.id, op.reason)
}
opErr = op.op(repo)
if locksVerboseLogging {
if opErr != nil {
log.Printf("[Queue] Operation %s (%s) failed with error: %v", op.id, op.reason, opErr)
} else {
log.Printf("[Queue] Operation %s (%s) completed successfully", op.id, op.reason)
}
}
}()
// signal to the caller via op.done
if locksVerboseLogging {
log.Printf("[Queue] Notifying caller of operation %s (%s) completion", op.id, op.reason)
}
op.done <- opErr
}
}(op)
}
wg.Wait()
if needsRollback {
log.Printf("[Queue] Performing rollback for plan %s branch %s", firstOp.planId, firstOp.branch)
rollbackErr := repo.GitClearUncommittedChanges(firstOp.branch)
if rollbackErr != nil {
log.Printf("[Queue] Failed to rollback: %v", rollbackErr)
} else if locksVerboseLogging {
log.Printf("[Queue] Rollback completed successfully")
}
}
}()
}
}
type ExecRepoOperationParams struct {
OrgId string
UserId string
PlanId string
Branch string
Scope LockScope
PlanBuildId string
Reason string
Ctx context.Context
CancelFn context.CancelFunc
ClearRepoOnErr bool
}
func ExecRepoOperation(
params ExecRepoOperationParams,
op repoOpFn,
) error {
id := uuid.New().String()
log.Printf("[Queue] ExecRepoOperation called for plan %s, branch %s, scope %s, reason %s",
params.PlanId, params.Branch, params.Scope, params.Reason)
done := make(chan error, 1)
numOps := repoQueues.add(&repoOperation{
id: id,
orgId: params.OrgId,
planId: params.PlanId,
branch: params.Branch,
scope: params.Scope,
reason: params.Reason,
planBuildId: params.PlanBuildId,
op: op,
done: done,
ctx: params.Ctx,
cancelFn: params.CancelFn,
clearRepoOnErr: params.ClearRepoOnErr,
})
if numOps > 1 {
if locksVerboseLogging {
log.Printf("[Queue] Operation %s (%s) queued behind %d operations", id, params.Reason, numOps-1)
for i, op := range repoQueues.getQueue(params.PlanId).ops {
log.Printf("[Queue] Operation %d: %s - %s\n", i, op.id, op.reason)
}
}
}
select {
case err := <-done:
if locksVerboseLogging {
if err != nil {
log.Printf("[Queue] Operation %s (%s) completed with error: %v", id, params.Reason, err)
} else {
log.Printf("[Queue] Operation %s (%s) completed successfully", id, params.Reason)
}
}
return err
case <-params.Ctx.Done():
if locksVerboseLogging {
log.Printf("[Queue] Operation %s (%s) context canceled while waiting", id, params.Reason)
}
return params.Ctx.Err()
}
}
================================================
FILE: app/server/db/rbac_helpers.go
================================================
package db
import (
"fmt"
"log"
)
var orgOwnerRoleId string
var orgMemberRoleId string
func GetOrgOwnerRoleId() (string, error) {
if orgOwnerRoleId == "" {
err := cacheOrgOwnerRoleId()
if err != nil {
return "", fmt.Errorf("error getting org owner role id: %v", err)
}
}
if orgOwnerRoleId == "" {
return "", fmt.Errorf("org owner role id is empty")
}
return orgOwnerRoleId, nil
}
func GetOrgMemberRoleId() (string, error) {
if orgMemberRoleId == "" {
err := cacheOrgMemberRoleId()
if err != nil {
return "", fmt.Errorf("error getting org member role id: %v", err)
}
}
if orgMemberRoleId == "" {
return "", fmt.Errorf("org member role id is empty")
}
return orgMemberRoleId, nil
}
func GetOrgOwners(orgId string) ([]*User, error) {
var users []*User
err := Conn.Select(&users, "SELECT * FROM users WHERE id IN (SELECT user_id FROM orgs_users WHERE org_id = $1 AND org_role_id = $2)", orgId, orgOwnerRoleId)
if err != nil {
return nil, fmt.Errorf("error getting org owners: %v", err)
}
return users, nil
}
func CacheOrgRoleIds() error {
err := cacheOrgOwnerRoleId()
if err != nil {
return fmt.Errorf("error getting org owner role id: %v", err)
}
if orgOwnerRoleId == "" {
log.Println("org owner role id is empty at startup")
}
err = cacheOrgMemberRoleId()
if err != nil {
return fmt.Errorf("error getting org member role id: %v", err)
}
if orgMemberRoleId == "" {
log.Println("org member role id is empty at startup")
}
return nil
}
func cacheOrgOwnerRoleId() error {
var roleId string
err := Conn.Get(&roleId, "SELECT id FROM org_roles WHERE name = 'owner'")
if err != nil {
return fmt.Errorf("error getting owner role id: %v", err)
}
orgOwnerRoleId = roleId
return nil
}
func cacheOrgMemberRoleId() error {
var roleId string
err := Conn.Get(&roleId, "SELECT id FROM org_roles WHERE name = 'member'")
if err != nil {
return fmt.Errorf("error getting member role id: %v", err)
}
orgMemberRoleId = roleId
return nil
}
================================================
FILE: app/server/db/result_helpers.go
================================================
package db
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"sort"
"strings"
"sync"
"time"
shared "plandex-shared"
"github.com/google/uuid"
)
func StorePlanResult(result *PlanFileResult) error {
now := time.Now()
if result.Id == "" {
result.Id = uuid.New().String()
result.CreatedAt = now
}
result.UpdatedAt = now
bytes, err := json.MarshalIndent(result, "", " ")
if err != nil {
return fmt.Errorf("error marshalling result: %v", err)
}
resultsDir := getPlanResultsDir(result.OrgId, result.PlanId)
err = os.MkdirAll(resultsDir, 0755)
if err != nil {
return fmt.Errorf("error creating results dir: %v", err)
}
log.Printf("Storing plan result: %s - %s", result.Path, result.Id)
err = os.WriteFile(filepath.Join(resultsDir, result.Id+".json"), bytes, 0644)
if err != nil {
return fmt.Errorf("error writing result file: %v", err)
}
return nil
}
type CurrentPlanStateParams struct {
OrgId string
PlanId string
PlanFileResults []*PlanFileResult
ConvoMessageDescriptions []*ConvoMessageDescription
Contexts []*Context
}
func GetFullCurrentPlanStateParams(orgId, planId string) (CurrentPlanStateParams, error) {
errCh := make(chan error, 3)
var results []*PlanFileResult
var convoMessageDescriptions []*ConvoMessageDescription
var contexts []*Context
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetFullCurrentPlanStateParams: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetFullCurrentPlanStateParams: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := GetPlanFileResults(orgId, planId)
if err != nil {
errCh <- fmt.Errorf("error getting plan file results: %v", err)
return
}
results = res
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetFullCurrentPlanStateParams: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetFullCurrentPlanStateParams: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := GetConvoMessageDescriptions(orgId, planId)
if err != nil {
errCh <- fmt.Errorf("error getting latest plan build description: %v", err)
return
}
convoMessageDescriptions = res
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetFullCurrentPlanStateParams: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetFullCurrentPlanStateParams: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := GetPlanContexts(orgId, planId, true, false)
if err != nil {
errCh <- fmt.Errorf("error getting contexts: %v", err)
return
}
contexts = res
errCh <- nil
}()
for i := 0; i < 3; i++ {
err := <-errCh
if err != nil {
return CurrentPlanStateParams{}, err
}
}
return CurrentPlanStateParams{
OrgId: orgId,
PlanId: planId,
PlanFileResults: results,
ConvoMessageDescriptions: convoMessageDescriptions,
Contexts: contexts,
}, nil
}
func GetCurrentPlanState(params CurrentPlanStateParams) (*shared.CurrentPlanState, error) {
orgId := params.OrgId
planId := params.PlanId
var dbPlanFileResults []*PlanFileResult
var convoMessageDescriptions []*shared.ConvoMessageDescription
contextsByPath := map[string]*Context{}
planApplies := []*shared.PlanApply{}
errCh := make(chan error, 4)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if params.PlanFileResults == nil {
res, err := GetPlanFileResults(orgId, planId)
dbPlanFileResults = res
if err != nil {
errCh <- fmt.Errorf("error getting plan file results: %v", err)
return
}
} else {
dbPlanFileResults = params.PlanFileResults
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if params.ConvoMessageDescriptions == nil {
res, err := GetConvoMessageDescriptions(orgId, planId)
if err != nil {
errCh <- fmt.Errorf("error getting latest plan build description: %v", err)
return
}
for _, desc := range res {
convoMessageDescriptions = append(convoMessageDescriptions, desc.ToApi())
}
} else {
for _, desc := range params.ConvoMessageDescriptions {
convoMessageDescriptions = append(convoMessageDescriptions, desc.ToApi())
}
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var contexts []*Context
if params.Contexts == nil {
res, err := GetPlanContexts(orgId, planId, true, false)
if err != nil {
errCh <- fmt.Errorf("error getting contexts: %v", err)
return
}
contexts = res
log.Println("Got contexts:", len(contexts))
} else {
contexts = params.Contexts
}
for _, context := range contexts {
if context.FilePath != "" {
contextsByPath[context.FilePath] = context
}
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetCurrentPlanState: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := GetPlanApplies(orgId, planId)
if err != nil {
errCh <- fmt.Errorf("error getting plan applies: %v", err)
return
}
for _, apply := range res {
planApplies = append(planApplies, apply.ToApi())
}
errCh <- nil
}()
for i := 0; i < 4; i++ {
err := <-errCh
if err != nil {
return nil, err
}
}
var apiPlanFileResults []*shared.PlanFileResult
pendingResultPaths := map[string]bool{}
for _, dbPlanFileResult := range dbPlanFileResults {
// log.Printf("Plan file result: %s", dbPlanFileResult.Id)
apiResult := dbPlanFileResult.ToApi()
apiPlanFileResults = append(apiPlanFileResults, apiResult)
if apiResult.IsPending() {
// log.Printf("Pending result: %s", dbPlanFileResult.Id)
pendingResultPaths[apiResult.Path] = true
} else {
// log.Printf("Not pending result: %s", apiResult.Id)
// log.Printf("Applied at: %v", apiResult.AppliedAt)
// log.Printf("Rejected at: %v", apiResult.RejectedAt)
// log.Printf("Content: %v", apiResult.Content != "")
// log.Printf("Num Replacement: %d", len(apiResult.Replacements))
// log.Printf("Num Pending Replacements: %v", apiResult.NumPendingReplacements())
}
}
planResult := GetPlanResult(apiPlanFileResults)
pendingContextsByPath := map[string]*shared.Context{}
for path, context := range contextsByPath {
pendingContextsByPath[path] = context.ToApi()
}
// log.Println("Pending contexts by path:", len(pendingContextsByPath))
planState := &shared.CurrentPlanState{
PlanResult: planResult,
ConvoMessageDescriptions: convoMessageDescriptions,
ContextsByPath: pendingContextsByPath,
PlanApplies: planApplies,
}
currentPlanFiles, err := planState.GetFiles()
if err != nil {
return nil, fmt.Errorf("error getting current plan files: %v", err)
}
planState.CurrentPlanFiles = currentPlanFiles
return planState, nil
}
func GetConvoMessageDescriptions(orgId, planId string) ([]*ConvoMessageDescription, error) {
var descriptions []*ConvoMessageDescription
descriptionsDir := getPlanDescriptionsDir(orgId, planId)
files, err := os.ReadDir(descriptionsDir)
if err != nil {
if os.IsNotExist(err) {
return descriptions, nil
}
return nil, fmt.Errorf("error reading descriptions dir: %v", err)
}
errCh := make(chan error, len(files))
descCh := make(chan *ConvoMessageDescription, len(files))
for _, file := range files {
go func(file os.DirEntry) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetConvoMessageDescriptions: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetConvoMessageDescriptions: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
path := filepath.Join(descriptionsDir, file.Name())
bytes, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("error reading description file %s: %v", file.Name(), err)
return
}
var description ConvoMessageDescription
err = json.Unmarshal(bytes, &description)
if err != nil {
log.Println("Error unmarshalling description file:", path)
log.Println("bytes:")
log.Println(string(bytes))
errCh <- fmt.Errorf("error unmarshalling description file %s: %v", path, err)
return
}
descCh <- &description
}(file)
}
for i := 0; i < len(files); i++ {
select {
case err := <-errCh:
return nil, fmt.Errorf("error reading description files: %v", err)
case description := <-descCh:
if description.WroteFiles && description.AppliedAt == nil {
descriptions = append(descriptions, description)
}
}
}
sort.Slice(descriptions, func(i, j int) bool {
return descriptions[i].CreatedAt.Before(descriptions[j].CreatedAt)
})
return descriptions, nil
}
func GetPlanFileResults(orgId, planId string) ([]*PlanFileResult, error) {
var results []*PlanFileResult
resultsDir := getPlanResultsDir(orgId, planId)
files, err := os.ReadDir(resultsDir)
if err != nil {
if os.IsNotExist(err) {
return results, nil
}
return nil, fmt.Errorf("error reading results dir: %v", err)
}
errCh := make(chan error, len(files))
resultCh := make(chan *PlanFileResult, len(files))
for _, file := range files {
// log.Printf("Result file: %s", file.Name())
go func(file os.DirEntry) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetPlanFileResults: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetPlanFileResults: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
bytes, err := os.ReadFile(filepath.Join(resultsDir, file.Name()))
if err != nil {
errCh <- fmt.Errorf("error reading result file: %v", err)
return
}
var result PlanFileResult
err = json.Unmarshal(bytes, &result)
if err != nil {
errCh <- fmt.Errorf("error unmarshalling result file: %v", err)
return
}
resultCh <- &result
}(file)
}
for i := 0; i < len(files); i++ {
select {
case err := <-errCh:
return nil, fmt.Errorf("error reading result files: %v", err)
case result := <-resultCh:
results = append(results, result)
}
}
sort.Slice(results, func(i, j int) bool {
return results[i].CreatedAt.Before(results[j].CreatedAt)
})
return results, nil
}
func GetPlanFileResultById(orgId, planId, resultId string) (*PlanFileResult, error) {
resultsDir := getPlanResultsDir(orgId, planId)
bytes, err := os.ReadFile(filepath.Join(resultsDir, resultId+".json"))
if err != nil {
return nil, fmt.Errorf("error reading result file: %v", err)
}
var result PlanFileResult
err = json.Unmarshal(bytes, &result)
if err != nil {
return nil, fmt.Errorf("error unmarshalling result file: %v", err)
}
return &result, nil
}
func GetPlanResult(planFileResults []*shared.PlanFileResult) *shared.PlanResult {
resByPath := make(shared.PlanFileResultsByPath)
replacementsByPath := make(map[string][]*shared.Replacement)
var paths []string
for _, planFileRes := range planFileResults {
if planFileRes.IsPending() {
_, hasPath := resByPath[planFileRes.Path]
resByPath[planFileRes.Path] = append(resByPath[planFileRes.Path], planFileRes)
if !hasPath {
// log.Printf("Adding res path: %s", planFileRes.Path)
paths = append(paths, planFileRes.Path)
}
}
}
for _, results := range resByPath {
for _, planRes := range results {
replacementsByPath[planRes.Path] = append(replacementsByPath[planRes.Path], planRes.Replacements...)
}
}
// sort paths ascending
sort.Slice(paths, func(i, j int) bool {
return paths[i] < paths[j]
})
return &shared.PlanResult{
FileResultsByPath: resByPath,
SortedPaths: paths,
ReplacementsByPath: replacementsByPath,
Results: planFileResults,
}
}
type ApplyPlanParams struct {
OrgId string
UserId string
BranchName string
Plan *Plan
CurrentPlanState *shared.CurrentPlanState
CurrentPlanStateParams *CurrentPlanStateParams
CommitMsg string
}
func ApplyPlan(repo *GitRepo, ctx context.Context, params ApplyPlanParams) error {
orgId := params.OrgId
userId := params.UserId
branchName := params.BranchName
plan := params.Plan
currentPlanState := params.CurrentPlanState
currentPlanParams := params.CurrentPlanStateParams
planId := plan.Id
resultsDir := getPlanResultsDir(orgId, planId)
var pendingDbResults []*PlanFileResult
planFileResults := currentPlanParams.PlanFileResults
convoMessageDescriptions := currentPlanParams.ConvoMessageDescriptions
contexts := currentPlanParams.Contexts
contextsByPath := make(map[string]*Context)
for _, context := range contexts {
if context.FilePath != "" {
contextsByPath[context.FilePath] = context
}
}
for _, result := range planFileResults {
apiResult := result.ToApi()
if apiResult.IsPending() {
pendingDbResults = append(pendingDbResults, result)
}
}
log.Printf("Pending db results: %d", len(pendingDbResults))
pendingNewFilesSet := make(map[string]bool)
pendingUpdatedFilesSet := make(map[string]bool)
for _, result := range pendingDbResults {
if result.Path == "_apply.sh" {
continue
}
if len(result.Replacements) == 0 && result.Content != "" {
pendingNewFilesSet[result.Path] = true
} else if !pendingNewFilesSet[result.Path] {
pendingUpdatedFilesSet[result.Path] = true
}
}
var loadContextRes *shared.LoadContextResponse
var updateContextRes *shared.UpdateContextResponse
numRoutines := len(pendingDbResults) +
len(convoMessageDescriptions)
if len(pendingNewFilesSet) > 0 {
numRoutines++
}
if len(pendingUpdatedFilesSet) > 0 {
numRoutines++
}
errCh := make(chan error, numRoutines)
now := time.Now()
for _, result := range pendingDbResults {
go func(result *PlanFileResult) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
result.AppliedAt = &now
bytes, err := json.MarshalIndent(result, "", " ")
if err != nil {
errCh <- fmt.Errorf("error marshalling result: %v", err)
return
}
err = os.WriteFile(filepath.Join(resultsDir, result.Id+".json"), bytes, 0644)
if err != nil {
errCh <- fmt.Errorf("error writing result file: %v", err)
return
}
errCh <- nil
}(result)
}
for _, description := range convoMessageDescriptions {
go func(description *ConvoMessageDescription) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
description.AppliedAt = &now
err := StoreDescription(description)
if err != nil {
errCh <- fmt.Errorf("error storing convo message description: %v", err)
return
}
errCh <- nil
}(description)
}
if len(pendingNewFilesSet) > 0 {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
loadReq := shared.LoadContextRequest{}
for path := range pendingNewFilesSet {
loadReq = append(loadReq, &shared.LoadContextParams{
ContextType: shared.ContextFileType,
Name: path,
FilePath: path,
Body: currentPlanState.CurrentPlanFiles.Files[path],
})
}
if len(loadReq) > 0 {
res, _, err := LoadContexts(
ctx,
LoadContextsParams{
OrgId: orgId,
UserId: userId,
Plan: plan,
BranchName: branchName,
Req: &loadReq,
SkipConflictInvalidation: true, // no need to invalidate conflicts when applying plan--and fixes race condition since invalidation check loads description
AutoLoaded: true,
},
)
if err != nil {
errCh <- fmt.Errorf("error loading context: %v", err)
return
}
loadContextRes = res
}
errCh <- nil
}()
}
if len(pendingUpdatedFilesSet) > 0 {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in ApplyPlan: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
updateReq := shared.UpdateContextRequest{}
for path := range pendingUpdatedFilesSet {
context := contextsByPath[path]
updateReq[context.Id] = &shared.UpdateContextParams{
Body: currentPlanState.CurrentPlanFiles.Files[path],
}
}
if len(updateReq) > 0 {
res, err := UpdateContexts(
UpdateContextsParams{
OrgId: orgId,
Plan: plan,
BranchName: branchName,
Req: &updateReq,
SkipConflictInvalidation: true, // no need to invalidate conflicts when applying plan--and fixes race condition since invalidation check loads description
},
)
if err != nil {
errCh <- fmt.Errorf("error updating context: %v", err)
return
}
updateContextRes = res
}
errCh <- nil
}()
}
for i := 0; i < numRoutines; i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error applying plan: %v", err)
}
}
// Store the PlanApply record
planApply := &PlanApply{
Id: uuid.New().String(),
OrgId: orgId,
PlanId: planId,
UserId: userId,
CommitMsg: params.CommitMsg,
CreatedAt: now,
}
// Collect the IDs from the pending results and descriptions
var resultIds []string
var descriptionIds []string
var messageIds []string
for _, result := range pendingDbResults {
resultIds = append(resultIds, result.Id)
}
for _, desc := range convoMessageDescriptions {
descriptionIds = append(descriptionIds, desc.Id)
messageIds = append(messageIds, desc.ConvoMessageId)
}
planApply.PlanFileResultIds = resultIds
planApply.ConvoMessageDescriptionIds = descriptionIds
planApply.ConvoMessageIds = messageIds
// Store the PlanApply object
bytes, err := json.MarshalIndent(planApply, "", " ")
if err != nil {
return fmt.Errorf("error marshalling plan apply: %v", err)
}
appliesDir := getPlanAppliesDir(orgId, planId)
err = os.MkdirAll(appliesDir, 0755)
if err != nil {
return fmt.Errorf("error creating applies dir: %v", err)
}
err = os.WriteFile(filepath.Join(appliesDir, planApply.Id+".json"), bytes, 0644)
if err != nil {
return fmt.Errorf("error writing plan apply file: %v", err)
}
msg := "✅ Marked pending results as applied"
currentFiles := currentPlanState.CurrentPlanFiles.Files
var sortedFiles []string
for path := range currentFiles {
sortedFiles = append(sortedFiles, path)
}
sort.Strings(sortedFiles)
for _, path := range sortedFiles {
msg += fmt.Sprintf("\n • 📄 %s", path)
}
msg += "\n" + "✏️ " + params.CommitMsg
if loadContextRes != nil && !loadContextRes.MaxTokensExceeded {
msg += "\n\n" + loadContextRes.Msg
}
if updateContextRes != nil && !updateContextRes.MaxTokensExceeded {
msg += "\n\n" + updateContextRes.Msg
}
err = repo.GitAddAndCommit(branchName, msg)
if err != nil {
return fmt.Errorf("error committing plan: %v", err)
}
return nil
}
func RejectAllResults(orgId, planId string) error {
resultsDir := getPlanResultsDir(orgId, planId)
files, err := os.ReadDir(resultsDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("error reading results dir: %v", err)
}
errCh := make(chan error, len(files))
now := time.Now()
for _, file := range files {
resultId := strings.TrimSuffix(file.Name(), ".json")
go func(resultId string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in RejectAllResults: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in RejectAllResults: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
err := RejectPlanFile(orgId, planId, resultId, now)
if err != nil {
errCh <- fmt.Errorf("error rejecting result: %v", err)
return
}
errCh <- nil
}(resultId)
}
for i := 0; i < len(files); i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error rejecting plan: %v", err)
}
}
return nil
}
func DeletePendingResultsForPaths(orgId, planId string, paths map[string]bool) error {
// log.Println("Deleting pending results for paths")
resultsDir := getPlanResultsDir(orgId, planId)
files, err := os.ReadDir(resultsDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("error reading results dir: %v", err)
}
errCh := make(chan error, len(files))
for _, file := range files {
resultId := strings.TrimSuffix(file.Name(), ".json")
go func(resultId string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in DeletePendingResultsForPaths: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in DeletePendingResultsForPaths: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
bytes, err := os.ReadFile(filepath.Join(resultsDir, resultId+".json"))
if err != nil {
errCh <- fmt.Errorf("error reading result file: %v", err)
return
}
var result PlanFileResult
err = json.Unmarshal(bytes, &result)
if err != nil {
errCh <- fmt.Errorf("error unmarshalling result file: %v", err)
return
}
// log.Printf("Checking pending result: %s", resultId)
if result.ToApi().IsPending() && paths[result.Path] {
log.Printf("Deleting pending result: %s", resultId)
err = os.Remove(filepath.Join(resultsDir, resultId+".json"))
if err != nil {
errCh <- fmt.Errorf("error deleting result file: %v", err)
return
}
}
errCh <- nil
}(resultId)
}
for i := 0; i < len(files); i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error deleting pending results: %v", err)
}
}
return nil
}
func RejectPlanFiles(orgId, planId string, files []string, now time.Time) error {
errCh := make(chan error, len(files))
for _, file := range files {
go func(file string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in RejectPlanFiles: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in RejectPlanFiles: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
err := RejectPlanFile(orgId, planId, file, now)
if err != nil {
errCh <- err
return
}
errCh <- nil
}(file)
}
for i := 0; i < len(files); i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error rejecting plan files: %v", err)
}
}
return nil
}
func RejectPlanFile(orgId, planId, filePathOrResultId string, now time.Time) error {
resultsDir := getPlanResultsDir(orgId, planId)
results, err := GetPlanFileResults(orgId, planId)
if err != nil {
return fmt.Errorf("error getting plan file results: %v", err)
}
errCh := make(chan error, len(results))
for _, result := range results {
go func(result *PlanFileResult) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in RejectPlanFile: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in RejectPlanFile: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil {
result.RejectedAt = &now
} else {
errCh <- nil
return
}
bytes, err := json.MarshalIndent(result, "", " ")
if err != nil {
errCh <- fmt.Errorf("error marshalling result: %v", err)
}
err = os.WriteFile(filepath.Join(resultsDir, result.Id+".json"), bytes, 0644)
if err != nil {
errCh <- fmt.Errorf("error writing result file: %v", err)
}
errCh <- nil
}(result)
}
for i := 0; i < len(results); i++ {
err := <-errCh
if err != nil {
return fmt.Errorf("error rejecting plan: %v", err)
}
}
return nil
}
func RejectReplacement(orgId, planId, resultId, replacementId string) error {
resultsDir := getPlanResultsDir(orgId, planId)
bytes, err := os.ReadFile(filepath.Join(resultsDir, resultId+".json"))
if err != nil {
return fmt.Errorf("error reading result file: %v", err)
}
var result PlanFileResult
err = json.Unmarshal(bytes, &result)
if err != nil {
return fmt.Errorf("error unmarshalling result file: %v", err)
}
if result.RejectedAt != nil {
return nil
}
now := time.Now()
foundReplacement := false
for _, replacement := range result.Replacements {
if replacement.Id == replacementId {
replacement.RejectedAt = &now
foundReplacement = true
break
}
}
if !foundReplacement {
return fmt.Errorf("replacement not found: %s", replacementId)
}
return nil
}
func GetPlanApplies(orgId, planId string) ([]*PlanApply, error) {
appliesDir := getPlanAppliesDir(orgId, planId)
files, err := os.ReadDir(appliesDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("error reading applies dir: %v", err)
}
planApplies := []*PlanApply{}
var mu sync.Mutex
errCh := make(chan error, len(files))
for _, file := range files {
go func(file os.DirEntry) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GetPlanApplies: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GetPlanApplies: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
bytes, err := os.ReadFile(filepath.Join(appliesDir, file.Name()))
if err != nil {
errCh <- fmt.Errorf("error reading apply file: %v", err)
return
}
var apply PlanApply
err = json.Unmarshal(bytes, &apply)
if err != nil {
errCh <- fmt.Errorf("error unmarshalling apply file: %v", err)
return
}
mu.Lock()
planApplies = append(planApplies, &apply)
mu.Unlock()
errCh <- nil
}(file)
}
for i := 0; i < len(files); i++ {
err := <-errCh
if err != nil {
return nil, fmt.Errorf("error getting plan applies: %v", err)
}
}
return planApplies, nil
}
================================================
FILE: app/server/db/settings_helpers.go
================================================
package db
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"time"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
)
func GetPlanSettings(plan *Plan) (settings *shared.PlanSettings, err error) {
planDir := getPlanDir(plan.OrgId, plan.Id)
settingsPath := filepath.Join(planDir, "settings.json")
result, err := GetApiCustomModels(plan.OrgId)
if err != nil {
return nil, fmt.Errorf("error getting custom models: %v", err)
}
defer func() {
if settings != nil {
settings.Configure(result.CustomModelPacks, result.CustomModels, result.CustomProviders, os.Getenv("PLANDEX_CLOUD") != "")
}
}()
bytes, err := os.ReadFile(settingsPath)
if os.IsNotExist(err) || len(bytes) == 0 {
log.Printf("GetPlanSettings - no settings file found for plan %s - checking org defaults", plan.Id)
// see if org has default settings
defaultSettings, err := GetOrgDefaultSettings(plan.OrgId)
if err != nil {
return nil, fmt.Errorf("error getting org default settings: %v", err)
}
if defaultSettings != nil {
log.Printf("GetPlanSettings - found org default settings for plan %s", plan.Id)
return defaultSettings, nil
} else {
log.Printf("GetPlanSettings - no org default settings found for plan %s", plan.Id)
}
log.Println("GetPlanSettings - no default settings found, returning default settings object")
// if it doesn't exist, return default settings object
settings = &shared.PlanSettings{
UpdatedAt: plan.CreatedAt,
ModelPackName: shared.DefaultModelPack.Name,
}
return settings, nil
} else if err != nil {
return nil, fmt.Errorf("error reading settings file: %v", err)
}
log.Printf("GetPlanSettings - settings found in file")
err = json.Unmarshal(bytes, &settings)
if err != nil {
return nil, fmt.Errorf("error unmarshalling settings: %v", err)
}
return settings, nil
}
func StorePlanSettings(plan *Plan, settings shared.PlanSettings) error {
planDir := getPlanDir(plan.OrgId, plan.Id)
settingsPath := filepath.Join(planDir, "settings.json")
settings.UpdatedAt = time.Now()
settings.CustomModelPacks = nil
settings.CustomModels = nil
settings.CustomModelsById = nil
settings.CustomProviders = nil
settings.UsesCustomProviderByModelId = nil
settings.IsCloud = false
settings.Configured = false
bytes, err := json.Marshal(settings)
if err != nil {
return fmt.Errorf("error marshalling settings: %v", err)
}
err = os.WriteFile(settingsPath, bytes, 0644)
if err != nil {
return fmt.Errorf("error writing settings file: %v", err)
}
err = BumpPlanUpdatedAt(plan.Id, settings.UpdatedAt)
if err != nil {
return fmt.Errorf("error bumping plan updated at: %v", err)
}
return nil
}
func GetOrgDefaultSettings(orgId string) (settings *shared.PlanSettings, err error) {
result, err := GetApiCustomModels(orgId)
if err != nil {
return nil, fmt.Errorf("error getting custom models: %v", err)
}
defer func() {
if settings != nil {
settings.Configure(result.CustomModelPacks, result.CustomModels, result.CustomProviders, os.Getenv("PLANDEX_CLOUD") != "")
}
}()
query := "SELECT * FROM default_plan_settings WHERE org_id = $1"
var defaults DefaultPlanSettings
err = Conn.Get(&defaults, query, orgId)
if err != nil {
if err == sql.ErrNoRows {
log.Println("GetOrgDefaultSettings - no rows - returning default settings")
// if it doesn't exist, return default settings object
settings := &shared.PlanSettings{
UpdatedAt: time.Time{},
ModelPackName: shared.DefaultModelPack.Name,
}
return settings, nil
}
return nil, fmt.Errorf("error getting default plan settings: %v", err)
}
return &defaults.PlanSettings, nil
}
func GetOrgDefaultSettingsForUpdate(orgId string, tx *sqlx.Tx) (settings *shared.PlanSettings, err error) {
result, err := GetApiCustomModels(orgId)
if err != nil {
return nil, fmt.Errorf("error getting custom models: %v", err)
}
defer func() {
if settings != nil {
settings.Configure(result.CustomModelPacks, result.CustomModels, result.CustomProviders, os.Getenv("PLANDEX_CLOUD") != "")
}
}()
query := "SELECT * FROM default_plan_settings WHERE org_id = $1 FOR UPDATE"
var defaults DefaultPlanSettings
err = tx.Get(&defaults, query, orgId)
if err != nil {
if err == sql.ErrNoRows {
// if it doesn't exist, return default settings object
settings := &shared.PlanSettings{
UpdatedAt: time.Time{},
ModelPackName: shared.DefaultModelPack.Name,
}
return settings, nil
}
return nil, fmt.Errorf("error getting default plan settings: %v", err)
}
return &defaults.PlanSettings, nil
}
func StoreOrgDefaultSettings(orgId string, settings *shared.PlanSettings, tx *sqlx.Tx) error {
settings.UpdatedAt = time.Now()
query := `INSERT INTO default_plan_settings (org_id, plan_settings)
VALUES ($1, $2)
ON CONFLICT (org_id) DO UPDATE SET plan_settings = excluded.plan_settings
`
_, err := tx.Exec(query, orgId, settings)
if err != nil {
return fmt.Errorf("error storing default plan settings: %v", err)
}
return nil
}
type GetCustomModelsResult struct {
CustomModels []*shared.CustomModel
CustomProviders []*shared.CustomProvider
CustomModelPacks []*shared.ModelPack
}
func GetApiCustomModels(orgId string) (result *GetCustomModelsResult, err error) {
var customModels []*CustomModel
var customProviders []*CustomProvider
var customModelPacks []*ModelPack
errCh := make(chan error, 3)
go func() {
res, err := ListModelPacks(orgId)
if err != nil {
errCh <- fmt.Errorf("error getting custom model packs: %v", err)
}
customModelPacks = res
errCh <- nil
}()
go func() {
res, err := ListCustomModels(orgId)
if err != nil {
errCh <- fmt.Errorf("error getting custom models: %v", err)
}
customModels = res
errCh <- nil
}()
go func() {
res, err := ListCustomProviders(orgId)
if err != nil {
errCh <- fmt.Errorf("error getting custom providers: %v", err)
}
customProviders = res
errCh <- nil
}()
for i := 0; i < 3; i++ {
err := <-errCh
if err != nil {
return nil, err
}
}
apiModelPacks := make([]*shared.ModelPack, len(customModelPacks))
for i, modelPack := range customModelPacks {
apiModelPacks[i] = modelPack.ToApi()
}
apiCustomModels := make([]*shared.CustomModel, len(customModels))
for i, model := range customModels {
apiCustomModels[i] = model.ToApi()
}
apiCustomProviders := make([]*shared.CustomProvider, len(customProviders))
for i, provider := range customProviders {
apiCustomProviders[i] = provider.ToApi()
}
result = &GetCustomModelsResult{
CustomModels: apiCustomModels,
CustomProviders: apiCustomProviders,
CustomModelPacks: apiModelPacks,
}
return result, nil
}
================================================
FILE: app/server/db/stream_helpers.go
================================================
package db
import (
"context"
"database/sql"
"fmt"
"log"
"plandex-server/notify"
"runtime/debug"
"time"
shared "plandex-shared"
"github.com/lib/pq"
)
const modelStreamHeartbeatInterval = 1 * time.Second
const modelStreamHeartbeatTimeout = 5 * time.Second
func StoreModelStream(stream *ModelStream, ctx context.Context, cancelFn context.CancelFunc) error {
query := `INSERT INTO model_streams (org_id, plan_id, internal_ip, branch) VALUES (:org_id, :plan_id, :internal_ip, :branch) RETURNING id, created_at`
row, err := Conn.NamedQuery(query, stream)
if err != nil {
return fmt.Errorf("error storing model stream: %v", err)
}
defer row.Close()
if row.Next() {
var createdAt time.Time
var id string
if err := row.Scan(&id, &createdAt); err != nil {
return fmt.Errorf("error storing model stream: %v", err)
}
stream.Id = id
stream.CreatedAt = createdAt
}
// Start a goroutine to keep the lock alive
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in StoreModelStream: %v\n%s", r, debug.Stack())
cancelFn()
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in StoreModelStream: %v\n%s", r, debug.Stack()))
}
}()
numErrors := 0
for {
select {
case <-ctx.Done():
err := SetModelStreamFinished(stream.Id)
if err != nil {
log.Printf("Error setting model stream %s finished: %v\n", stream.Id, err)
}
return
default:
_, err := Conn.Exec("UPDATE model_streams SET last_heartbeat_at = NOW() WHERE id = $1", stream.Id)
if err != nil {
log.Printf("Error updating model stream last heartbeat: %v\n", err)
numErrors++
if numErrors > 5 {
log.Printf("Too many errors updating model stream last heartbeat: %v\n", err)
cancelFn()
return
}
}
time.Sleep(modelStreamHeartbeatInterval)
}
}
}()
return nil
}
func SetModelStreamFinished(id string) error {
log.Println("Setting model stream finished:", id)
_, err := Conn.Exec("UPDATE model_streams SET finished_at = NOW() WHERE id = $1", id)
if err != nil {
return fmt.Errorf("error setting model stream finished: %v", err)
}
log.Println("Set model stream finished successfully:", id)
return nil
}
func GetActiveModelStream(planId, branch string) (*ModelStream, error) {
var stream ModelStream
err := Conn.Get(&stream, "SELECT * FROM model_streams WHERE plan_id = $1 AND branch = $2 AND finished_at IS NULL", planId, branch)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting active model stream: %v", err)
}
if time.Now().Add(-modelStreamHeartbeatTimeout).After(stream.LastHeartbeatAt) {
log.Printf("Model stream %s has not sent a heartbeat in %s\n", stream.Id, modelStreamHeartbeatTimeout)
err := SetModelStreamFinished(stream.Id)
if err != nil {
return nil, fmt.Errorf("error setting model stream finished: %v", err)
}
err = SetPlanStatus(planId, branch, shared.PlanStatusError, "Model stream has not sent a heartbeat in 5 seconds")
if err != nil {
return nil, fmt.Errorf("error setting plan status to error: %v", err)
}
return nil, nil
} else {
log.Printf("Model stream %s sent heartbeat %d seconds ago\n", stream.Id, int(time.Since(stream.LastHeartbeatAt).Seconds()))
}
return &stream, nil
}
func GetActiveOrRecentModelStreams(planIds []string) ([]*ModelStream, error) {
var streams []*ModelStream
err := Conn.Select(&streams, "SELECT * FROM model_streams WHERE plan_id = ANY($1) AND (finished_at IS NULL OR finished_at > NOW() - INTERVAL '1 hour') ORDER BY created_at", pq.Array(planIds))
if err != nil {
return nil, fmt.Errorf("error getting active or recent model streams: %v", err)
}
return streams, nil
}
func GetActiveModelStreams(planIds []string) ([]*ModelStream, error) {
var streams []*ModelStream
err := Conn.Select(&streams, "SELECT * FROM model_streams WHERE plan_id = ANY($1) AND finished_at IS NULL ORDER BY created_at", pq.Array(planIds))
if err != nil {
return nil, fmt.Errorf("error getting active model streams: %v", err)
}
return streams, nil
}
// func StoreModelStreamSubscription(subscription *ModelStreamSubscription) error {
// query := `INSERT INTO model_stream_subscriptions (model_stream_id, org_id, plan_id, user_id, user_ip) VALUES (:model_stream_id, :org_id, :plan_id, :user_id, :user_ip) RETURNING id, created_at`
// row, err := Conn.NamedQuery(query, subscription)
// if err != nil {
// return fmt.Errorf("error storing model stream subscription: %v", err)
// }
// defer row.Close()
// if row.Next() {
// var createdAt time.Time
// var id string
// if err := row.Scan(&id, &createdAt); err != nil {
// return fmt.Errorf("error storing model stream subscription: %v", err)
// }
// subscription.Id = id
// subscription.CreatedAt = createdAt
// }
// return nil
// }
// func SetModelStreamSubscriptionFinished(id string) error {
// _, err := Conn.Exec("UPDATE model_stream_subscriptions SET finished_at = NOW() WHERE id = $1", id)
// if err != nil {
// return fmt.Errorf("error setting model stream subscription finished: %v", err)
// }
// return nil
// }
================================================
FILE: app/server/db/subtask_helpers.go
================================================
package db
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
func GetPlanSubtasks(orgId, planId string) ([]*Subtask, error) {
planDir := getPlanDir(orgId, planId)
subtasksPath := filepath.Join(planDir, "subtasks.json")
bytes, err := os.ReadFile(subtasksPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("error reading subtasks: %v", err)
}
var subtasks []*Subtask
err = json.Unmarshal(bytes, &subtasks)
if err != nil {
return nil, fmt.Errorf("error unmarshalling subtasks: %v", err)
}
return subtasks, nil
}
func StorePlanSubtasks(orgId, planId string, subtasks []*Subtask) error {
planDir := getPlanDir(orgId, planId)
bytes, err := json.Marshal(subtasks)
if err != nil {
return fmt.Errorf("error marshalling subtasks: %v", err)
}
err = os.WriteFile(filepath.Join(planDir, "subtasks.json"), bytes, os.ModePerm)
if err != nil {
return fmt.Errorf("error writing subtasks: %v", err)
}
return nil
}
================================================
FILE: app/server/db/summary_helpers.go
================================================
package db
import (
"fmt"
"time"
"github.com/lib/pq"
)
func GetPlanSummaries(planId string, convoMessageIds []string) ([]*ConvoSummary, error) {
var summaries []*ConvoSummary
err := Conn.Select(&summaries, "SELECT * FROM convo_summaries WHERE plan_id = $1 AND latest_convo_message_id = ANY($2) ORDER BY created_at", planId, pq.Array(convoMessageIds))
if err != nil {
return nil, fmt.Errorf("error getting plan summaries: %v", err)
}
return summaries, nil
}
func StoreSummary(summary *ConvoSummary) error {
query := "INSERT INTO convo_summaries (org_id, plan_id, latest_convo_message_id, latest_convo_message_created_at, summary, tokens, num_messages) VALUES (:org_id, :plan_id, :latest_convo_message_id, :latest_convo_message_created_at, :summary, :tokens, :num_messages) RETURNING id, created_at"
row, err := Conn.NamedQuery(query, summary)
if err != nil {
return fmt.Errorf("error storing summary: %v", err)
}
defer row.Close()
if row.Next() {
var createdAt time.Time
var id string
if err := row.Scan(&id, &createdAt); err != nil {
return fmt.Errorf("error storing summary: %v", err)
}
summary.Id = id
summary.CreatedAt = createdAt
}
return nil
}
================================================
FILE: app/server/db/transactions.go
================================================
package db
import (
"context"
"database/sql"
"fmt"
"log"
"runtime/debug"
"github.com/jmoiron/sqlx"
)
func WithTx(ctx context.Context, reason string, fn func(tx *sqlx.Tx) error) error {
return withTx(ctx, nil, reason, fn)
}
func WithTxOpts(ctx context.Context, opts *sql.TxOptions, reason string, fn func(tx *sqlx.Tx) error) error {
return withTx(ctx, opts, reason, fn)
}
func withTx(ctx context.Context, opts *sql.TxOptions, reason string, fn func(tx *sqlx.Tx) error) error {
log.Printf("starting transaction: (%s)", reason)
tx, err := Conn.BeginTxx(ctx, opts)
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
var committed bool
// Ensure that rollback is attempted in case of failure
defer func() {
panicErr := recover()
if panicErr != nil {
log.Printf("panic in WithTx (%s): %v\n%s", reason, panicErr, debug.Stack())
log.Printf("stack trace (panic - %s):\n%s", reason, debug.Stack())
}
if committed {
return
}
if rbErr := tx.Rollback(); rbErr != nil {
if rbErr == sql.ErrTxDone {
log.Printf("attempted to roll back transaction, but it was already committed: (%s)", reason)
} else {
log.Printf("transaction rollback error: (%s) %v\n", reason, rbErr)
}
} else {
log.Printf("transaction rolled back: (%s)", reason)
}
}()
err = fn(tx)
if err != nil {
log.Printf("error in WithTx (%s): %v", reason, err)
return err
}
err = tx.Commit()
if err != nil {
log.Printf("error committing transaction: (%s) %v", reason, err)
return fmt.Errorf("error committing transaction: %v", err)
}
committed = true
log.Printf("committed transaction: (%s)", reason)
return nil
}
================================================
FILE: app/server/db/user_helpers.go
================================================
package db
import (
"database/sql"
"fmt"
shared "plandex-shared"
"strings"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
func GetUser(userId string) (*User, error) {
var user User
err := Conn.Get(&user, "SELECT * FROM users WHERE id = $1", userId)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting user: %v", err)
}
return &user, nil
}
func GetUserByEmail(email string) (*User, error) {
var user User
err := Conn.Get(&user, "SELECT * FROM users WHERE email = $1", email)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting user: %v", err)
}
return &user, nil
}
func GetUsersForDomain(domain string) ([]*User, error) {
var users []*User
err := Conn.Select(&users, "SELECT * FROM users WHERE domain = $1", domain)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting users for domain: %v", err)
}
return users, nil
}
func GetOrgUser(userId, orgId string) (*OrgUser, error) {
var orgUser OrgUser
err := Conn.Get(&orgUser, "SELECT * FROM orgs_users WHERE user_id = $1 AND org_id = $2", userId, orgId)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting org user: %v", err)
}
return &orgUser, nil
}
func GetOrgUserConfig(userId, orgId string) (*shared.OrgUserConfig, error) {
var orgUserConfig shared.OrgUserConfig
err := Conn.Get(&orgUserConfig, "SELECT config FROM orgs_users WHERE user_id = $1 AND org_id = $2", userId, orgId)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("error getting org user config: %v", err)
}
return &orgUserConfig, nil
}
func UpdateOrgUserConfig(userId, orgId string, config *shared.OrgUserConfig) error {
_, err := Conn.Exec("UPDATE orgs_users SET config = $1 WHERE user_id = $2 AND org_id = $3", config, userId, orgId)
if err != nil {
return fmt.Errorf("error updating org user config: %v", err)
}
return nil
}
func ListOrgUsers(orgId string) ([]*OrgUser, error) {
var orgUsers []*OrgUser
err := Conn.Select(&orgUsers, "SELECT * FROM orgs_users WHERE org_id = $1", orgId)
if err != nil {
return nil, fmt.Errorf("error listing org users: %v", err)
}
return orgUsers, nil
}
func ListUsers(orgId string) ([]*User, error) {
var users []*User
orgUsers, err := ListOrgUsers(orgId)
if err != nil {
return nil, fmt.Errorf("error listing users: %v", err)
}
userIds := make([]string, len(orgUsers))
for i, ou := range orgUsers {
userIds[i] = ou.UserId
}
err = Conn.Select(&users, "SELECT * FROM users WHERE id = ANY($1)", pq.Array(userIds))
if err != nil {
return nil, fmt.Errorf("error listing users: %v", err)
}
return users, nil
}
func CreateUser(name, email string, tx *sqlx.Tx) (*User, error) {
emailSplit := strings.Split(email, "@")
if len(emailSplit) != 2 {
return nil, fmt.Errorf("invalid email: %v", email)
}
domain := emailSplit[1]
user := User{
Name: name,
Email: email,
Domain: domain,
}
err := tx.QueryRow("INSERT INTO users (name, email, domain) VALUES ($1, $2, $3) RETURNING id", user.Name, user.Email, user.Domain).Scan(&user.Id)
if err != nil {
if IsNonUniqueErr(err) {
return nil, fmt.Errorf("user already exists for email: %v", email)
}
return nil, fmt.Errorf("error creating user: %v", err)
}
return &user, nil
}
func NumUsersWithRole(orgId, roleId string) (int, error) {
var count int
err := Conn.Get(&count, "SELECT COUNT(*) FROM orgs_users WHERE org_id = $1 AND org_role_id = $2", orgId, roleId)
if err != nil {
return 0, fmt.Errorf("error counting users with role: %v", err)
}
return count, nil
}
================================================
FILE: app/server/db/utils.go
================================================
package db
import (
"github.com/lib/pq"
)
func IsNonUniqueErr(err error) bool {
if err, ok := err.(*pq.Error); ok {
if err.Code == "23505" {
return true
}
}
return false
}
================================================
FILE: app/server/diff/diff.go
================================================
package diff
import (
"bufio"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
shared "plandex-shared"
"github.com/google/uuid"
)
func GetDiffs(original, updated string) (string, error) {
// create temp directory
tempDirPath, err := os.MkdirTemp("", "tmp-diffs-*")
if err != nil {
return "", fmt.Errorf("error creating temp dir: %v", err)
}
defer func() {
go os.RemoveAll(tempDirPath)
}()
// write the original file to the temp dir
err = os.WriteFile(filepath.Join(tempDirPath, "original"), []byte(original), 0644)
if err != nil {
return "", fmt.Errorf("error writing original file: %v", err)
}
// write the updated file to the temp dir
err = os.WriteFile(filepath.Join(tempDirPath, "updated"), []byte(updated), 0644)
if err != nil {
return "", fmt.Errorf("error writing updated file: %v", err)
}
cmd := exec.Command("git", "-C", tempDirPath, "diff", "--no-color", "--no-index", "original", "updated")
res, err := cmd.CombinedOutput()
if err != nil {
exitError, ok := err.(*exec.ExitError)
if ok && exitError.ExitCode() == 1 {
// Exit status 1 means diffs were found, which is expected
} else {
log.Printf("Error getting diffs: %v\n", err)
log.Printf("Diff output: %s\n", res)
return "", fmt.Errorf("error getting diffs: %v", err)
}
}
return string(res), nil
}
type change struct {
Old string
New string
Line int
Length int
}
func GetDiffReplacements(original, updated string) ([]*shared.Replacement, error) {
diff, err := GetDiffs(original, updated)
if err != nil {
return nil, fmt.Errorf("error getting git diffs: %v", err)
}
var changes []*change
scanner := bufio.NewScanner(strings.NewReader(diff))
var currentHunk *change
var oldLines, newLines []string
for scanner.Scan() {
line := scanner.Text()
// Parse hunk header
if strings.HasPrefix(line, "@@") {
// If we have a previous hunk, process it
if currentHunk != nil {
change := processHunk(oldLines, newLines, currentHunk.Line)
if change != nil {
changes = append(changes, change)
}
}
// Parse the new hunk header
lineInfo := strings.Split(line, " ")[1:] // Skip @@ part
oldInfo := strings.Split(lineInfo[0], ",")
startLine, _ := strconv.Atoi(strings.TrimPrefix(oldInfo[0], "-"))
currentHunk = &change{
Line: startLine,
}
oldLines = []string{}
newLines = []string{}
continue
}
if currentHunk == nil {
continue // Skip until we find a hunk
}
// Process the lines within a hunk
switch {
case strings.HasPrefix(line, "-"):
oldLines = append(oldLines, strings.TrimPrefix(line, "-"))
case strings.HasPrefix(line, "+"):
newLines = append(newLines, strings.TrimPrefix(line, "+"))
case strings.HasPrefix(line, " "):
// Context lines - add to both
line = strings.TrimPrefix(line, " ")
oldLines = append(oldLines, line)
newLines = append(newLines, line)
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error scanning diff: %v", err)
}
// Process the last hunk if exists
if currentHunk != nil {
change := processHunk(oldLines, newLines, currentHunk.Line)
if change != nil {
changes = append(changes, change)
}
}
replacements := make([]*shared.Replacement, len(changes))
for i, change := range changes {
replacements[i] = &shared.Replacement{
Id: uuid.New().String(),
Old: change.Old,
New: change.New,
}
}
return replacements, nil
}
func processHunk(oldLines, newLines []string, startLine int) *change {
if len(oldLines) == 0 && len(newLines) == 0 {
return nil
}
return &change{
Old: strings.Join(oldLines, "\n"),
New: strings.Join(newLines, "\n"),
Line: startLine,
Length: len(oldLines),
}
}
================================================
FILE: app/server/email/email.go
================================================
package email
import (
"fmt"
"net/smtp"
"os"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
)
// SendEmailViaSES sends an email using AWS SES
func SendEmailViaSES(recipient, subject, htmlBody, textBody string) error {
sess, err := session.NewSession()
if err != nil {
return fmt.Errorf("error creating AWS session: %v", err)
}
// Create an SES session.
svc := ses.New(sess)
// Assemble the email.
input := &ses.SendEmailInput{
Destination: &ses.Destination{
ToAddresses: []*string{
aws.String(recipient),
},
},
Message: &ses.Message{
Body: &ses.Body{
Html: &ses.Content{
Charset: aws.String("UTF-8"),
Data: aws.String(htmlBody),
},
Text: &ses.Content{
Charset: aws.String("UTF-8"),
Data: aws.String(textBody),
},
},
Subject: &ses.Content{
Charset: aws.String("UTF-8"),
Data: aws.String(subject),
},
},
Source: aws.String("Plandex "),
}
// Attempt to send the email.
_, err = svc.SendEmail(input)
return err
}
func sendEmailViaSMTP(recipient, subject, htmlBody, textBody string) error {
smtpHost := os.Getenv("SMTP_HOST")
smtpPort := os.Getenv("SMTP_PORT")
smtpUser := os.Getenv("SMTP_USER")
smtpPassword := os.Getenv("SMTP_PASSWORD")
smtpFrom := os.Getenv("SMTP_FROM")
if smtpHost == "" || smtpPort == "" || smtpUser == "" || smtpPassword == "" {
return fmt.Errorf("SMTP settings not found in environment variables")
}
if smtpFrom == "" {
smtpFrom = smtpUser
}
smtpAddress := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
auth := smtp.PlainAuth("", smtpUser, smtpPassword, smtpHost)
// Generate a MIME boundary
boundary := "BOUNDARY1234567890"
header := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary=\"%s\"\r\n\r\n", smtpFrom, recipient, subject, boundary)
// Prepare the text body part
textPart := fmt.Sprintf("--%s\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\n%s\r\n", boundary, textBody)
// Prepare the HTML body part
htmlPart := fmt.Sprintf("--%s\r\nContent-Type: text/html; charset=\"UTF-8\"\r\n\r\n%s\r\n", boundary, htmlBody)
// End marker for the boundary
endBoundary := fmt.Sprintf("--%s--", boundary)
// Combine the parts to form the full email message
message := []byte(header + textPart + htmlPart + endBoundary)
err := smtp.SendMail(smtpAddress, auth, smtpFrom, []string{recipient}, message)
if err != nil {
return fmt.Errorf("error sending email via SMTP: %v", err)
}
return nil
}
================================================
FILE: app/server/email/invite.go
================================================
package email
import (
"fmt"
"os"
"github.com/gen2brain/beeep"
)
func SendInviteEmail(email, inviteeFirstName, inviterName, orgName string) error {
// Check if the environment is production
if os.Getenv("GOENV") == "production" {
// Production environment - send email using AWS SES
subject := fmt.Sprintf("%s, you've been invited to join %s on Plandex", inviteeFirstName, orgName)
htmlBody := fmt.Sprintf(`
Plandex is a terminal-based AI programming engine for complex tasks.
To accept the invite, first install Plandex, then open a terminal and run 'plandex sign-in'. Enter '%s' when asked for your email and follow the prompts from there.
If you have questions, feedback, or run into a problem, you can reply directly to this email, start a discussion, or open an issue.
`, inviteeFirstName, inviterName, orgName, email)
textBody := fmt.Sprintf(`Hi %s,\n\n%s has invited you to join the org %s on Plandex.\n\nPlandex is a terminal-based AI programming engine for complex tasks.\n\nTo accept the invite, first install Plandex (https://docs.plandex.ai/install/), then open a terminal and run 'plandex sign-in'. Enter '%s' when asked for your email and follow the prompts from there.\n\nIf you have questions, feedback, or run into a problem, you can reply directly to this email, start a discussion (https://github.com/plandex-ai/plandex/discussions), or open an issue (https://github.com/plandex-ai/plandex/issues).`, inviteeFirstName, inviterName, orgName, email)
if os.Getenv("IS_CLOUD") == "" {
return sendEmailViaSMTP(email, subject, htmlBody, textBody)
} else {
return SendEmailViaSES(email, subject, htmlBody, textBody)
}
} else {
// Send notification
err := beeep.Notify("Invite Sent", fmt.Sprintf("Invite sent to %s (email not sent in development)", email), "")
if err != nil {
return fmt.Errorf("error sending notification in dev: %v", err)
}
}
return nil
}
================================================
FILE: app/server/email/verification.go
================================================
package email
import (
"fmt"
"log"
"os"
"github.com/atotto/clipboard"
"github.com/gen2brain/beeep"
)
func SendVerificationEmail(email string, pin string) error {
// Check if the environment is production
if os.Getenv("GOENV") == "production" {
// Production environment - send email using AWS SES
subject := "Your Plandex Pin"
htmlBody := fmt.Sprintf(`
Hi there,
Welcome to Plandex! Your pin is:
%s
It will be valid for the next 5 minutes.
If you didn't request this, you can safely ignore the email.
`, pin)
textBody := fmt.Sprintf("Hi there,\n\nWelcome to Plandex! Your pin is:\n\n%s\n\nIt will be valid for the next 5 minutes.\n\nIf you didn't request this, you can safely ignore the email.", pin)
if os.Getenv("IS_CLOUD") == "" {
return sendEmailViaSMTP(email, subject, htmlBody, textBody)
} else {
return SendEmailViaSES(email, subject, htmlBody, textBody)
}
}
if os.Getenv("GOENV") == "development" {
// Development environment
log.Printf("Development mode: Verification pin is %s for email %s", pin, email)
// Copy pin to clipboard
clipboard.WriteAll(pin) // ignore error
// Send notification
beeep.Notify("Verification Pin", fmt.Sprintf("Verification pin %s copied to clipboard %s", pin, email), "") // ignore error
}
return nil
}
================================================
FILE: app/server/go.mod
================================================
module plandex-server
go 1.23.3
require (
github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/pkg/errors v0.9.1
github.com/sashabaranov/go-openai v1.40.0
plandex-shared v0.0.0-00010101000000-000000000000
)
require (
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/image v0.27.0 // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/atotto/clipboard v0.1.4
github.com/aws/aws-sdk-go v1.55.7
github.com/fatih/color v1.18.0
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82
github.com/stretchr/testify v1.10.0
golang.org/x/mod v0.21.0
golang.org/x/net v0.40.0
)
replace plandex-shared => ../shared
================================================
FILE: app/server/go.sum
================================================
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=
github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=
github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 h1:ygs9POGDQpQGLJPlq4+0LBUmMBNox1N4JSpw+OETcvI=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sashabaranov/go-openai v1.40.0 h1:Peg9Iag5mUJtPW00aYatlsn97YML0iNULiLNe74iPrU=
github.com/sashabaranov/go-openai v1.40.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4=
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: app/server/handlers/accounts.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/types"
"strings"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
)
func CreateAccountHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateAccountHandler")
if os.Getenv("IS_CLOUD") != "" {
log.Println("Creating accounts is not supported in cloud mode")
http.Error(w, "Creating accounts is not supported in cloud mode", http.StatusNotImplemented)
return
}
isLocalMode := (os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1")
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body: "+err.Error(), http.StatusInternalServerError)
return
}
var req shared.CreateAccountRequest
err = json.Unmarshal(body, &req)
if err != nil {
log.Printf("Error unmarshalling request: %v\n", err)
http.Error(w, "Error unmarshalling request: "+err.Error(), http.StatusInternalServerError)
return
}
req.Email = strings.ToLower(req.Email)
var emailVerificationId string
// skipping email verification in dev/local mode
if !isLocalMode {
emailVerificationId, err = db.ValidateEmailVerification(req.Email, req.Pin)
if err != nil {
log.Printf("Error validating email verification: %v\n", err)
http.Error(w, "Error validating email verification: "+err.Error(), http.StatusInternalServerError)
return
}
}
var apiErr *shared.ApiError
var user *db.User
var userId string
var token string
var orgId string
err = db.WithTx(r.Context(), "create account", func(tx *sqlx.Tx) error {
res, err := db.CreateAccount(req.UserName, req.Email, emailVerificationId, tx)
if err != nil {
return fmt.Errorf("error creating account: %v", err)
}
user = res.User
userId = user.Id
token = res.Token
orgId = res.OrgId
_, apiErr = hooks.ExecHook(hooks.CreateAccount, hooks.HookParams{
Auth: &types.ServerAuth{
User: user,
OrgId: orgId,
},
})
return nil
})
if apiErr != nil {
writeApiError(w, *apiErr)
return
}
if err != nil {
log.Printf("Error creating account: %v\n", err)
http.Error(w, "Error creating account: "+err.Error(), http.StatusInternalServerError)
return
}
// get orgs
orgs, err := db.GetAccessibleOrgsForUser(user)
if err != nil {
log.Printf("Error getting orgs for user: %v\n", err)
http.Error(w, "Error getting orgs for user: "+err.Error(), http.StatusInternalServerError)
return
}
apiOrgs, apiErr := toApiOrgs(orgs)
if apiErr != nil {
log.Printf("Error converting orgs to API orgs: %v\n", apiErr)
writeApiError(w, *apiErr)
return
}
resp := shared.SessionResponse{
UserId: userId,
Token: token,
Email: req.Email,
UserName: req.UserName,
Orgs: apiOrgs,
IsLocalMode: os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1",
}
bytes, err := json.Marshal(resp)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully created account")
w.Write(bytes)
}
================================================
FILE: app/server/handlers/auth_helpers.go
================================================
package handlers
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/types"
"strings"
"time"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
"golang.org/x/mod/semver"
)
func Authenticate(w http.ResponseWriter, r *http.Request, requireOrg bool) *types.ServerAuth {
return execAuthenticate(w, r, requireOrg, true)
}
func AuthenticateOptional(w http.ResponseWriter, r *http.Request, requireOrg bool) *types.ServerAuth {
return execAuthenticate(w, r, requireOrg, false)
}
func GetAuthHeader(r *http.Request) (*shared.AuthHeader, error) {
authHeader := r.Header.Get("Authorization")
// check for a cookie as well for ui requests
if authHeader == "" {
log.Println("no auth header - checking for cookie")
// Try to get auth token from a cookie as a fallback
cookie, err := r.Cookie("authToken")
if err != nil {
if err == http.ErrNoCookie {
log.Println("no auth cookie")
return nil, nil
}
return nil, fmt.Errorf("error retrieving auth cookie: %v", err)
}
// Use the token from the cookie as the fallback authorization header
authHeader = cookie.Value
log.Println("got auth header from cookie")
}
if authHeader == "" {
return nil, nil
}
if !strings.HasPrefix(authHeader, "Bearer ") {
return nil, fmt.Errorf("invalid auth header")
}
// strip off the "Bearer " prefix
encoded := strings.TrimPrefix(authHeader, "Bearer ")
// decode the base64-encoded credentials
bytes, err := base64.URLEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("error decoding auth token: %v", err)
}
// parse the credentials
var parsed shared.AuthHeader
err = json.Unmarshal(bytes, &parsed)
if err != nil {
return nil, fmt.Errorf("error parsing auth token: %v", err)
}
return &parsed, nil
}
func ClearAuthCookieIfBrowser(w http.ResponseWriter, r *http.Request) error {
acceptHeader := r.Header.Get("Accept")
if acceptHeader == "" {
// no accept header, not a browser request
return nil
}
// Check for existing auth cookie
_, err := r.Cookie("authToken")
if err == http.ErrNoCookie {
// No auth cookie, nothing to clear
return nil
}
if err != nil {
return fmt.Errorf("error retrieving auth cookie: %v", err)
}
var domain string
if os.Getenv("GOENV") == "production" {
domain = os.Getenv("APP_SUBDOMAIN") + ".plandex.ai"
}
// Clear the authToken cookie
http.SetCookie(w, &http.Cookie{
Name: "authToken",
Path: "/",
Value: "",
MaxAge: -1,
Secure: os.Getenv("GOENV") != "development",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Domain: domain,
})
log.Println("cleared auth cookie")
return nil
}
func ClearAccountFromCookies(w http.ResponseWriter, r *http.Request, userId string) error {
// Get stored accounts
storedAccounts, err := GetAccountsFromCookie(r)
if err != nil {
return fmt.Errorf("error getting accounts from cookie: %v", err)
}
// Remove the account with the given userId
for i, account := range storedAccounts {
if account.UserId == userId {
storedAccounts = append(storedAccounts[:i], storedAccounts[i+1:]...)
break
}
}
// Marshal the updated accounts
updatedAccountsBytes, err := json.Marshal(storedAccounts)
if err != nil {
return fmt.Errorf("error marshalling updated accounts: %v", err)
}
// Encode to base64
encodedAccounts := base64.URLEncoding.EncodeToString(updatedAccountsBytes)
// Set the updated accounts cookie
var domain string
if os.Getenv("GOENV") == "production" {
domain = os.Getenv("APP_SUBDOMAIN") + ".plandex.ai"
}
http.SetCookie(w, &http.Cookie{
Name: "accounts",
Path: "/",
Value: encodedAccounts,
Secure: os.Getenv("GOENV") != "development",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Domain: domain,
})
return nil
}
func SetAuthCookieIfBrowser(w http.ResponseWriter, r *http.Request, user *db.User, token, orgId string) error {
log.Println("setting auth cookie if browser")
acceptHeader := r.Header.Get("Accept")
if acceptHeader == "" {
// no accept header, not a browser request
log.Println("not a browser request")
return nil
}
log.Println("is browser - setting auth cookie")
if token == "" {
authHeader, err := GetAuthHeader(r)
if err != nil {
return fmt.Errorf("error getting auth header: %v", err)
}
token = authHeader.Token
}
if token == "" {
return fmt.Errorf("no token")
}
// set authToken cookie
authHeader := shared.AuthHeader{
Token: token,
OrgId: orgId,
}
bytes, err := json.Marshal(authHeader)
if err != nil {
return fmt.Errorf("error marshalling auth header: %v", err)
}
// base64 encode
token = base64.URLEncoding.EncodeToString(bytes)
var domain string
if os.Getenv("GOENV") == "production" {
domain = os.Getenv("APP_SUBDOMAIN") + ".plandex.ai"
}
cookie := &http.Cookie{
Name: "authToken",
Path: "/",
Value: "Bearer " + token,
Secure: os.Getenv("GOENV") != "development",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Domain: domain,
Expires: time.Now().Add(time.Hour * 24 * 90),
}
log.Println("setting auth cookie", cookie)
http.SetCookie(w, cookie)
storedAccounts, err := GetAccountsFromCookie(r)
if err != nil {
return fmt.Errorf("error getting accounts from cookie: %v", err)
}
found := false
for _, account := range storedAccounts {
if account.UserId == user.Id {
found = true
account.Token = token
account.Email = user.Email
account.UserName = user.Name
break
}
}
if !found {
storedAccounts = append(storedAccounts, &shared.ClientAccount{
Email: user.Email,
UserName: user.Name,
UserId: user.Id,
Token: token,
})
}
bytes, err = json.Marshal(storedAccounts)
if err != nil {
return fmt.Errorf("error marshalling accounts: %v", err)
}
// base64 encode
accounts := base64.URLEncoding.EncodeToString(bytes)
http.SetCookie(w, &http.Cookie{
Name: "accounts",
Path: "/",
Value: accounts,
Secure: os.Getenv("GOENV") != "development",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Domain: domain,
Expires: time.Now().Add(time.Hour * 24 * 90),
})
return nil
}
func GetAccountsFromCookie(r *http.Request) ([]*shared.ClientAccount, error) {
accountsCookie, err := r.Cookie("accounts")
if err == http.ErrNoCookie {
return []*shared.ClientAccount{}, nil
} else if err != nil {
return nil, fmt.Errorf("error getting accounts cookie: %v", err)
}
bytes, err := base64.URLEncoding.DecodeString(accountsCookie.Value)
if err != nil {
return nil, fmt.Errorf("error decoding accounts cookie: %v", err)
}
var accounts []*shared.ClientAccount
err = json.Unmarshal(bytes, &accounts)
if err != nil {
return nil, fmt.Errorf("error unmarshalling accounts cookie: %v", err)
}
return accounts, nil
}
func ValidateAndSignIn(w http.ResponseWriter, r *http.Request, req shared.SignInRequest) (*shared.SessionResponse, error) {
var user *db.User
var emailVerificationId string
var signInCodeId string
var signInCodeOrgId string
var err error
isLocalMode := (os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1")
if req.IsSignInCode {
res, err := db.ValidateSignInCode(req.Pin)
if err != nil {
log.Printf("Error validating sign in code: %v\n", err)
return nil, fmt.Errorf("error validating sign in code: %v", err)
}
user, err = db.GetUser(res.UserId)
if err != nil {
log.Printf("Error getting user: %v\n", err)
return nil, fmt.Errorf("error getting user: %v", err)
}
if user == nil {
log.Printf("User not found for id: %v\n", res.UserId)
return nil, fmt.Errorf("user not found")
}
signInCodeId = res.Id
signInCodeOrgId = res.OrgId
} else {
req.Email = strings.ToLower(req.Email)
user, err = db.GetUserByEmail(req.Email)
if err != nil {
log.Printf("Error getting user: %v\n", err)
return nil, fmt.Errorf("error getting user: %v", err)
}
if user == nil {
log.Printf("User not found for email: %v\n", req.Email)
return nil, fmt.Errorf("not found")
}
// only validate email in non-local mode
if !isLocalMode {
emailVerificationId, err = db.ValidateEmailVerification(req.Email, req.Pin)
if err != nil {
log.Printf("Error validating email verification: %v\n", err)
return nil, fmt.Errorf("error validating email verification: %v", err)
}
log.Println("Email verification successful")
}
}
var token string
var authTokenId string
err = db.WithTx(r.Context(), "validate and sign in", func(tx *sqlx.Tx) error {
var err error
// create auth token
token, authTokenId, err = db.CreateAuthToken(user.Id, tx)
if err != nil {
log.Printf("Error creating auth token: %v\n", err)
return fmt.Errorf("error creating auth token: %v", err)
}
if req.IsSignInCode {
// update sign in code with auth token id
_, err = tx.Exec("UPDATE sign_in_codes SET auth_token_id = $1 WHERE id = $2", authTokenId, signInCodeId)
if err != nil {
log.Printf("Error updating sign in code: %v\n", err)
return fmt.Errorf("error updating sign in code: %v", err)
}
} else if !isLocalMode { // only update email verification in non-local mode
// update email verification with user and auth token ids
_, err = tx.Exec("UPDATE email_verifications SET user_id = $1, auth_token_id = $2 WHERE id = $3", user.Id, authTokenId, emailVerificationId)
if err != nil {
log.Printf("Error updating email verification: %v\n", err)
return fmt.Errorf("error updating email verification: %v", err)
}
log.Println("Email verification updated")
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error validating and signing in: %v", err)
}
// get orgs
orgs, err := db.GetAccessibleOrgsForUser(user)
if err != nil {
log.Printf("Error getting orgs for user: %v\n", err)
return nil, fmt.Errorf("error getting orgs for user: %v", err)
}
if req.IsSignInCode {
filteredOrgs := []*db.Org{}
for _, org := range orgs {
if org.Id == signInCodeOrgId {
filteredOrgs = append(filteredOrgs, org)
}
}
orgs = filteredOrgs
}
// with a single org, set the orgId in the cookie
// otherwise, the user will be prompted to select an org
var orgId string
if len(orgs) == 1 {
orgId = orgs[0].Id
}
log.Println("Setting auth cookie if browser")
err = SetAuthCookieIfBrowser(w, r, user, token, orgId)
if err != nil {
log.Printf("Error setting auth cookie: %v\n", err)
return nil, fmt.Errorf("error setting auth cookie: %v", err)
}
apiOrgs, apiErr := toApiOrgs(orgs)
if apiErr != nil {
log.Printf("Error converting orgs to api orgs: %v\n", apiErr)
return nil, fmt.Errorf("error converting orgs to api orgs: %v", apiErr)
}
resp := shared.SessionResponse{
UserId: user.Id,
Token: token,
Email: user.Email,
UserName: user.Name,
Orgs: apiOrgs,
IsLocalMode: os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1",
}
return &resp, nil
}
func requireMinClientVersion(w http.ResponseWriter, r *http.Request, minVersion string) bool {
msg := fmt.Sprintf("Client version is too old for this endpoint. Please upgrade to version %s or later.", minVersion)
version := r.Header.Get("X-Client-Version")
if version == "" {
http.Error(w, msg, http.StatusBadRequest)
return false
}
if version == "development" {
return true
}
if semver.Compare(version, minVersion) < 0 {
http.Error(w, msg, http.StatusBadRequest)
return false
}
return true
}
func execAuthenticate(w http.ResponseWriter, r *http.Request, requireOrg bool, raiseErr bool) *types.ServerAuth {
log.Println("authenticating request")
parsed, err := GetAuthHeader(r)
if err != nil {
log.Printf("error getting auth header: %v\n", err)
if raiseErr {
http.Error(w, "error getting auth header", http.StatusInternalServerError)
}
return nil
}
if parsed == nil {
log.Println("no auth header")
if raiseErr {
http.Error(w, "no auth header", http.StatusUnauthorized)
}
return nil
}
// validate the token
authToken, err := db.ValidateAuthToken(parsed.Token)
if err != nil {
log.Printf("error validating auth token: %v\n", err)
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeInvalidToken,
Status: http.StatusUnauthorized,
Msg: "Invalid auth token",
})
return nil
}
user, err := db.GetUser(authToken.UserId)
if err != nil {
log.Printf("error getting user: %v\n", err)
if raiseErr {
http.Error(w, "error getting user", http.StatusInternalServerError)
}
return nil
}
if !requireOrg {
return &types.ServerAuth{
AuthToken: authToken,
User: user,
}
}
if parsed.OrgId == "" {
log.Println("no org id")
if raiseErr {
http.Error(w, "no org id", http.StatusUnauthorized)
}
return nil
}
// validate the org membership
isMember, err := db.ValidateOrgMembership(authToken.UserId, parsed.OrgId)
if err != nil {
log.Printf("error validating org membership: %v\n", err)
if raiseErr {
http.Error(w, "error validating org membership", http.StatusInternalServerError)
}
return nil
}
if !isMember {
// check if there's an invite for this user and accept it if so (adds the user to the org)
invite, err := db.GetActiveInviteByEmail(parsed.OrgId, user.Email)
if err != nil {
log.Printf("error getting invite for org user: %v\n", err)
if raiseErr {
http.Error(w, "error getting invite for org user", http.StatusInternalServerError)
}
return nil
}
if invite != nil {
log.Println("accepting invite")
err := db.AcceptInvite(r.Context(), invite, authToken.UserId)
if err != nil {
log.Printf("error accepting invite: %v\n", err)
if raiseErr {
http.Error(w, "error accepting invite", http.StatusInternalServerError)
}
return nil
}
} else {
log.Println("user is not a member of the org")
if raiseErr {
http.Error(w, "not a member of org", http.StatusUnauthorized)
}
return nil
}
}
// get user permissions
permissions, err := db.GetUserPermissions(authToken.UserId, parsed.OrgId)
if err != nil {
log.Printf("error getting user permissions: %v\n", err)
if raiseErr {
http.Error(w, "error getting user permissions", http.StatusInternalServerError)
}
return nil
}
// build the permissions map
permissionsMap := make(shared.Permissions)
for _, permission := range permissions {
permissionsMap[permission] = true
}
auth := &types.ServerAuth{
AuthToken: authToken,
User: user,
OrgId: parsed.OrgId,
Permissions: permissionsMap,
}
// don't send hash for org-session requests
var hash string
if r.URL.Path != "/orgs/session" {
hash = parsed.Hash
}
_, apiErr := hooks.ExecHook(hooks.Authenticate, hooks.HookParams{
Auth: auth,
AuthenticateHookRequestParams: &hooks.AuthenticateHookRequestParams{
Path: r.URL.Path,
Hash: hash,
},
})
if apiErr != nil {
writeApiError(w, *apiErr)
return nil
}
log.Printf("UserId: %s, Email: %s, OrgId: %s\n", authToken.UserId, user.Email, parsed.OrgId)
return auth
}
func authorizeProject(w http.ResponseWriter, projectId string, auth *types.ServerAuth) bool {
return authorizeProjectOptional(w, projectId, auth, true)
}
func authorizeProjectOptional(w http.ResponseWriter, projectId string, auth *types.ServerAuth, shouldErr bool) bool {
log.Println("authorizing project")
projectExists, err := db.ProjectExists(auth.OrgId, projectId)
if err != nil {
log.Printf("error validating project: %v\n", err)
http.Error(w, "error validating project", http.StatusInternalServerError)
return false
}
if !projectExists && shouldErr {
log.Println("project does not exist in org")
http.Error(w, "project does not exist in org", http.StatusNotFound)
return false
}
return projectExists
}
func authorizeProjectRename(w http.ResponseWriter, projectId string, auth *types.ServerAuth) bool {
if !authorizeProject(w, projectId, auth) {
return false
}
if !auth.HasPermission(shared.PermissionRenameAnyProject) {
log.Println("User does not have permission to rename project")
http.Error(w, "User does not have permission to rename project", http.StatusForbidden)
return false
}
return true
}
func authorizeProjectDelete(w http.ResponseWriter, projectId string, auth *types.ServerAuth) bool {
if !authorizeProject(w, projectId, auth) {
return false
}
if !auth.HasPermission(shared.PermissionDeleteAnyProject) {
log.Println("User does not have permission to delete project")
http.Error(w, "User does not have permission to delete project", http.StatusForbidden)
return false
}
return true
}
func authorizePlan(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
log.Println("authorizing plan")
plan, err := db.ValidatePlanAccess(planId, auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("error validating plan membership: %v\n", err)
http.Error(w, "error validating plan membership", http.StatusInternalServerError)
return nil
}
if plan == nil {
log.Println("user doesn't have access the plan")
http.Error(w, "no access to plan", http.StatusUnauthorized)
return nil
}
return plan
}
func authorizePlanUpdate(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
plan := authorizePlan(w, planId, auth)
if plan == nil {
return nil
}
if plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionUpdateAnyPlan) {
log.Println("User does not have permission to update plan")
http.Error(w, "User does not have permission to update plan", http.StatusForbidden)
return nil
}
return plan
}
func authorizePlanDelete(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
plan := authorizePlan(w, planId, auth)
if plan == nil {
return nil
}
if plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionDeleteAnyPlan) {
log.Println("User does not have permission to delete plan")
http.Error(w, "User does not have permission to delete plan", http.StatusForbidden)
return nil
}
return plan
}
func authorizePlanRename(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
plan := authorizePlan(w, planId, auth)
if plan == nil {
return nil
}
if plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionRenameAnyPlan) {
log.Println("User does not have permission to rename plan")
http.Error(w, "User does not have permission to rename plan", http.StatusForbidden)
return nil
}
return plan
}
func authorizePlanArchive(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
plan := authorizePlan(w, planId, auth)
if plan == nil {
return nil
}
if plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionArchiveAnyPlan) {
log.Println("User does not have permission to archive plan")
http.Error(w, "User does not have permission to archive plan", http.StatusForbidden)
return nil
}
return plan
}
================================================
FILE: app/server/handlers/branches.go
================================================
package handlers
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"plandex-server/db"
shared "plandex-shared"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
)
func ListBranchesHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListBranchesHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
if authorizePlan(w, planId, auth) == nil {
return
}
var err error
ctx, cancel := context.WithCancel(r.Context())
var branches []*db.Branch
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: "main",
Reason: "list branches",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
res, err := db.ListPlanBranches(repo, planId)
if err != nil {
return err
}
branches = res
return nil
})
if err != nil {
log.Printf("Error getting branches: %v\n", err)
http.Error(w, "Error getting branches: "+err.Error(), http.StatusInternalServerError)
return
}
jsonBytes, err := json.Marshal(branches)
if err != nil {
log.Printf("Error marshalling branches: %v\n", err)
http.Error(w, "Error marshalling branches: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully retrieved branches")
w.Write(jsonBytes)
}
func CreateBranchHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateBranchHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer func() {
log.Println("Closing request body")
r.Body.Close()
}()
var req shared.CreateBranchRequest
if err := json.Unmarshal(body, &req); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body ", http.StatusBadRequest)
return
}
parentBranch, err := db.GetDbBranch(planId, branch)
if err != nil {
log.Printf("Error getting parent branch: %v\n", err)
http.Error(w, "Error getting parent branch: "+err.Error(), http.StatusInternalServerError)
return
}
ctx, cancel := context.WithCancel(r.Context())
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: "main",
Reason: "create branch",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
err := db.WithTx(ctx, "create branch", func(tx *sqlx.Tx) error {
_, err = db.CreateBranch(repo, plan, parentBranch, req.Name, tx)
if err != nil {
return fmt.Errorf("error creating branch: %v", err)
}
return nil
})
return err
})
if err != nil {
log.Printf("Error creating branch: %v\n", err)
http.Error(w, "Error creating branch: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully created branch")
}
func DeleteBranchHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for DeleteBranchHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
if authorizePlan(w, planId, auth) == nil {
return
}
if branch == "main" {
log.Println("Cannot delete main branch")
http.Error(w, "Cannot delete main branch", http.StatusBadRequest)
return
}
ctx, cancel := context.WithCancel(r.Context())
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: "main",
Reason: "delete branch",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
err := repo.GitDeleteBranch(branch)
return err
})
if err != nil {
log.Printf("Error deleting branch: %v\n", err)
http.Error(w, "Error deleting branch: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully deleted branch")
}
================================================
FILE: app/server/handlers/client_helper.go
================================================
package handlers
import (
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/model"
"plandex-server/types"
shared "plandex-shared"
)
type initClientsParams struct {
w http.ResponseWriter
auth *types.ServerAuth
apiKeys map[string]string // deprecated
openAIOrgId string // deprecated
authVars map[string]string
plan *db.Plan
settings *shared.PlanSettings
orgUserConfig *shared.OrgUserConfig
}
type initClientsResult struct {
clients map[string]model.ClientInfo
authVars map[string]string
}
func initClients(params initClientsParams) initClientsResult {
w := params.w
settings := params.settings
orgUserConfig := params.orgUserConfig
authVars := map[string]string{}
if params.authVars != nil {
authVars = params.authVars
} else if params.apiKeys != nil {
authVars = map[string]string{}
for envVar, apiKey := range params.apiKeys {
authVars[envVar] = apiKey
}
if params.openAIOrgId != "" {
authVars["OPENAI_ORG_ID"] = params.openAIOrgId
}
}
hookResult, apiErr := hooks.ExecHook(hooks.GetIntegratedModels, hooks.HookParams{
Auth: params.auth,
Plan: params.plan,
})
if apiErr != nil {
log.Printf("Error getting integrated models: %v\n", apiErr)
http.Error(w, "Error getting integrated models", http.StatusInternalServerError)
return initClientsResult{}
}
if hookResult.GetIntegratedModelsResult != nil && hookResult.GetIntegratedModelsResult.IntegratedModelsMode {
merged := map[string]string{}
for k, v := range hookResult.GetIntegratedModelsResult.AuthVars {
merged[k] = v
}
if authVars[shared.AnthropicClaudeMaxTokenEnvVar] != "" {
merged[shared.AnthropicClaudeMaxTokenEnvVar] = authVars[shared.AnthropicClaudeMaxTokenEnvVar]
}
authVars = merged
}
if len(authVars) == 0 && os.Getenv("IS_CLOUD") != "" {
log.Println("No api keys/credentials provided for models")
http.Error(w, "No api keys/credentials provided for models", http.StatusBadRequest)
return initClientsResult{}
}
clients := model.InitClients(authVars, settings, orgUserConfig)
return initClientsResult{
clients: clients,
authVars: authVars,
}
}
================================================
FILE: app/server/handlers/context_helper.go
================================================
package handlers
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/model"
"plandex-server/types"
"runtime"
"runtime/debug"
shared "plandex-shared"
)
type loadContextsParams struct {
w http.ResponseWriter
r *http.Request
auth *types.ServerAuth
loadReq *shared.LoadContextRequest
plan *db.Plan
branchName string
cachedMapsByPath map[string]*db.CachedMap
autoLoaded bool
}
func loadContexts(
params loadContextsParams,
) (*shared.LoadContextResponse, []*db.Context) {
w := params.w
r := params.r
auth := params.auth
loadReq := params.loadReq
plan := params.plan
branchName := params.branchName
cachedMapsByPath := params.cachedMapsByPath
autoLoaded := params.autoLoaded
log.Printf("[ContextHelper] Starting loadContexts with %d contexts, cachedMapsByPath: %v, autoLoaded: %v", len(*loadReq), cachedMapsByPath != nil, autoLoaded)
// check file count and size limits
// this is all a sanity check - we should have already checked these limits in the client
totalFiles := 0
mapFilesCount := 0
for _, context := range *loadReq {
totalFiles++
if context.ContextType == shared.ContextMapType {
mapFilesCount++
log.Printf("[ContextHelper] Found map file: %s with %d map bodies", context.FilePath, len(context.MapBodies))
if len(context.MapBodies) > shared.MaxContextMapPaths {
log.Printf("Error: Too many map files to load (found %d, limit is %d)\n", len(context.MapBodies), shared.MaxContextMapPaths)
http.Error(w, fmt.Sprintf("Too many map files to load (found %d, limit is %d)", len(context.MapBodies), shared.MaxContextMapPaths), http.StatusBadRequest)
return nil, nil
}
// these are already mapped, so they shouldn't be anywhere close to the input limit, but we'll use it for the sanity check
for _, body := range context.MapBodies {
if len(body) > shared.MaxContextMapSingleInputSize {
log.Printf("Error: Map file %s exceeds size limit (size %d, limit %d)\n", context.FilePath, len(body), shared.MaxContextMapSingleInputSize)
http.Error(w, fmt.Sprintf("Map file %s exceeds size limit (size %d, limit %d)", context.FilePath, len(body), shared.MaxContextMapSingleInputSize), http.StatusBadRequest)
return nil, nil
}
}
}
if totalFiles > shared.MaxContextCount {
log.Printf("Error: Too many contexts to load (found %d, limit is %d)\n", totalFiles, shared.MaxContextCount)
http.Error(w, fmt.Sprintf("Too many contexts to load (found %d, limit is %d)", totalFiles, shared.MaxContextCount), http.StatusBadRequest)
return nil, nil
}
fileSize := int64(len(context.Body))
if fileSize > shared.MaxContextBodySize {
log.Printf("Error: Context %s exceeds size limit (size %.2f MB, limit %d MB)\n", context.Name, float64(fileSize)/1024/1024, int(shared.MaxContextBodySize)/1024/1024)
http.Error(w, fmt.Sprintf("Context %s exceeds size limit (size %.2f MB, limit %d MB)", context.Name, float64(fileSize)/1024/1024, int(shared.MaxContextBodySize)/1024/1024), http.StatusBadRequest)
return nil, nil
}
}
if mapFilesCount > 0 {
log.Printf("[ContextHelper] Processing %d map files out of %d total contexts", mapFilesCount, totalFiles)
}
var err error
var settings *shared.PlanSettings
var clients map[string]model.ClientInfo
var authVars map[string]string
var orgUserConfig *shared.OrgUserConfig
for _, context := range *loadReq {
if context.ContextType == shared.ContextPipedDataType || context.ContextType == shared.ContextNoteType || context.ContextType == shared.ContextImageType {
settings, err = db.GetPlanSettings(plan)
if err != nil {
log.Printf("Error getting plan settings: %v\n", err)
http.Error(w, "Error getting plan settings: "+err.Error(), http.StatusInternalServerError)
return nil, nil
}
orgUserConfig, err = db.GetOrgUserConfig(auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("Error getting org user config: %v\n", err)
http.Error(w, "Error getting org user config: "+err.Error(), http.StatusInternalServerError)
return nil, nil
}
res := initClients(
initClientsParams{
w: w,
auth: auth,
apiKeys: context.ApiKeys,
openAIOrgId: context.OpenAIOrgId,
authVars: context.AuthVars,
plan: plan,
settings: settings,
},
)
clients = res.clients
authVars = res.authVars
break
}
}
// ensure image compatibility if we're loading an image
for _, context := range *loadReq {
if context.ContextType == shared.ContextImageType {
if !settings.GetModelPack().Planner.GetSharedBaseConfig(settings).HasImageSupport {
log.Printf("Error loading context: %s does not support images in context\n", settings.GetModelPack().Planner.ModelId)
http.Error(w, fmt.Sprintf("Error loading context: %s does not support images in context", settings.GetModelPack().Planner.ModelId), http.StatusBadRequest)
return nil, nil
}
}
}
// get name for piped data or notes if present
num := 0
errCh := make(chan error, len(*loadReq))
for _, context := range *loadReq {
if context.ContextType == shared.ContextPipedDataType {
num++
go func(context *shared.LoadContextParams) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GenPipedDataName: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GenPipedDataName: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
name, err := model.GenPipedDataName(model.GenPipedDataNameParams{
Ctx: r.Context(),
Auth: auth,
Plan: plan,
Settings: settings,
AuthVars: authVars,
SessionId: context.SessionId,
Clients: clients,
PipedContent: context.Body,
OrgUserConfig: orgUserConfig,
})
if err != nil {
errCh <- fmt.Errorf("error generating name for piped data: %v", err)
return
}
context.Name = name
errCh <- nil
}(context)
} else if context.ContextType == shared.ContextNoteType {
num++
go func(context *shared.LoadContextParams) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in GenNoteName: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in GenNoteName: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
name, err := model.GenNoteName(r.Context(), auth, plan, settings, orgUserConfig, clients, authVars, context.Body, context.SessionId)
if err != nil {
errCh <- fmt.Errorf("error generating name for note: %v", err)
return
}
context.Name = name
errCh <- nil
}(context)
}
}
if num > 0 {
for i := 0; i < num; i++ {
err := <-errCh
if err != nil {
log.Printf("Error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, nil
}
}
}
ctx, cancel := context.WithCancel(r.Context())
var loadRes *shared.LoadContextResponse
var dbContexts []*db.Context
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: plan.Id,
Branch: branchName,
Reason: "load contexts",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
ClearRepoOnErr: true,
}, func(repo *db.GitRepo) error {
log.Printf("[ContextHelper] Calling db.LoadContexts with %d contexts, %d cached maps", len(*loadReq), len(cachedMapsByPath))
for path := range cachedMapsByPath {
log.Printf("[ContextHelper] Using cached map for path: %s", path)
}
res, dbContextsRes, err := db.LoadContexts(ctx, db.LoadContextsParams{
OrgId: auth.OrgId,
Plan: plan,
BranchName: branchName,
Req: loadReq,
UserId: auth.User.Id,
CachedMapsByPath: cachedMapsByPath,
AutoLoaded: autoLoaded,
})
if err != nil {
return err
}
loadRes = res
dbContexts = dbContextsRes
log.Printf("[ContextHelper] db.LoadContexts completed successfully, loaded %d contexts", len(dbContexts))
// Log information about loaded map contexts
mapContextsCount := 0
for _, context := range dbContexts {
if context.ContextType == shared.ContextMapType {
mapContextsCount++
log.Printf("[ContextHelper] Loaded map context: %s, path: %s, tokens: %d", context.Name, context.FilePath, context.NumTokens)
}
}
if mapContextsCount > 0 {
log.Printf("[ContextHelper] Successfully loaded %d map contexts out of %d total contexts", mapContextsCount, len(dbContexts))
}
if loadRes.MaxTokensExceeded {
return nil
}
log.Printf("[ContextHelper] Committing changes to branch %s", branchName)
err = repo.GitAddAndCommit(branchName, res.Msg)
if err != nil {
return fmt.Errorf("error committing changes: %v", err)
}
log.Printf("[ContextHelper] Committing changes to branch %s completed successfully", branchName)
return nil
})
if err != nil {
log.Printf("Error loading contexts: %v\n", err)
http.Error(w, "Error loading contexts: "+err.Error(), http.StatusInternalServerError)
return nil, nil
}
if loadRes.MaxTokensExceeded {
log.Printf("The total number of tokens (%d) exceeds the maximum allowed (%d)", loadRes.TotalTokens, loadRes.MaxTokens)
bytes, err := json.Marshal(loadRes)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return nil, nil
}
w.Write(bytes)
return nil, nil
}
return loadRes, dbContexts
}
================================================
FILE: app/server/handlers/err_helper.go
================================================
package handlers
import (
"encoding/json"
"log"
"net/http"
shared "plandex-shared"
)
func writeApiError(w http.ResponseWriter, apiErr shared.ApiError) {
bytes, err := json.Marshal(apiErr)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
// If marshalling fails, fall back to a simpler error message
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
log.Printf("API Error: %v\n", apiErr.Msg)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.Status)
_, writeErr := w.Write(bytes)
if writeErr != nil {
log.Printf("Error writing response: %v\n", writeErr)
}
}
================================================
FILE: app/server/handlers/file_maps.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"plandex-server/db"
"runtime"
"runtime/debug"
"sync"
shared "plandex-shared"
"github.com/gorilla/mux"
)
func GetFileMapHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetFileMapHandler")
auth := Authenticate(w, r, true)
if auth == nil {
log.Println("GetFileMapHandler: auth failed")
return
}
var req shared.GetFileMapRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("Error decoding request: %v", err), http.StatusBadRequest)
return
}
log.Println("GetFileMapHandler: checking limits")
if len(req.MapInputs) > shared.MaxContextMapPaths {
http.Error(w, fmt.Sprintf("Too many files to map: %d (max %d)", len(req.MapInputs), shared.MaxContextMapPaths), http.StatusBadRequest)
return
}
totalSize := 0
for path, input := range req.MapInputs {
// the client should be truncating inputs to the max size, but we'll check here too
if len(input) > shared.MaxContextMapSingleInputSize {
http.Error(w, fmt.Sprintf("File %s is too large: %d (max %d)", path, len(input), shared.MaxContextMapSingleInputSize), http.StatusBadRequest)
return
}
totalSize += len(input)
}
// On the client, once the total size limit is exceeded, we send empty file maps for remaining files
if totalSize > shared.MaxContextMapTotalInputSize+10000 {
http.Error(w, fmt.Sprintf("Max map size exceeded: %d (max %d)", totalSize, shared.MaxContextMapTotalInputSize), http.StatusBadRequest)
return
}
// Check batch size limits
if len(req.MapInputs) > shared.ContextMapMaxBatchSize {
http.Error(w, fmt.Sprintf("Batch contains too many files: %d (max %d)", len(req.MapInputs), shared.ContextMapMaxBatchSize), http.StatusBadRequest)
return
}
if int64(totalSize) > shared.ContextMapMaxBatchBytes {
http.Error(w, fmt.Sprintf("Batch size too large: %d bytes (max %d bytes)", totalSize, shared.ContextMapMaxBatchBytes), http.StatusBadRequest)
return
}
results := make(chan shared.FileMapBodies, 1)
err := queueProjectMapJob(projectMapJob{
inputs: req.MapInputs,
ctx: r.Context(),
results: results,
})
if err != nil {
log.Println("GetFileMapHandler: map queue is full")
http.Error(w, "Too many project map jobs, please try again later", http.StatusTooManyRequests)
return
}
select {
case <-r.Context().Done():
http.Error(w, "Request was cancelled", http.StatusRequestTimeout)
return
case maps := <-results:
if maps == nil {
http.Error(w, "Mapping timed out", http.StatusRequestTimeout)
return
}
resp := shared.GetFileMapResponse{
MapBodies: maps,
}
respBytes, err := json.Marshal(resp)
if err != nil {
http.Error(w, fmt.Sprintf("Error marshalling response: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(respBytes)
log.Printf("GetFileMapHandler success - writing response bytes: %d", len(respBytes))
}
}
func LoadCachedFileMapHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for LoadCachedFileMapHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branchName := vars["branch"]
log.Println("planId: ", planId, "branchName: ", branchName)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
var req shared.LoadCachedFileMapRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("Error decoding request: %v", err), http.StatusBadRequest)
return
}
cachedMetaByPath := map[string]*shared.Context{}
cachedMapsByPath := map[string]*db.CachedMap{}
var mu sync.Mutex
errCh := make(chan error, len(req.FilePaths))
for _, path := range req.FilePaths {
go func(path string) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in LoadCachedFileMapHandler: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in LoadCachedFileMapHandler: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
cachedContext, err := db.GetCachedMap(plan.OrgId, plan.ProjectId, path)
if err != nil {
errCh <- fmt.Errorf("error getting cached map: %v", err)
return
}
if cachedContext != nil {
mu.Lock()
cachedMetaByPath[path] = cachedContext.ToMeta().ToApi()
cachedMapsByPath[path] = &db.CachedMap{
MapParts: cachedContext.MapParts,
MapShas: cachedContext.MapShas,
MapTokens: cachedContext.MapTokens,
MapSizes: cachedContext.MapSizes,
}
mu.Unlock()
}
errCh <- nil
}(path)
}
for range req.FilePaths {
err := <-errCh
if err != nil {
log.Printf("Error getting cached map: %v", err)
http.Error(w, fmt.Sprintf("Error getting cached map: %v", err), http.StatusInternalServerError)
return
}
}
resp := shared.LoadCachedFileMapResponse{}
var loadRes *shared.LoadContextResponse
if len(cachedMetaByPath) == 0 {
log.Println("no cached maps found")
} else {
log.Println("cached map found")
cachedByPath := map[string]bool{}
for _, cachedContext := range cachedMetaByPath {
cachedByPath[cachedContext.FilePath] = true
}
resp.CachedByPath = cachedByPath
var loadReq shared.LoadContextRequest
for _, cachedContext := range cachedMetaByPath {
loadReq = append(loadReq, &shared.LoadContextParams{
ContextType: shared.ContextMapType,
Name: cachedContext.Name,
FilePath: cachedContext.FilePath,
Body: cachedContext.Body,
})
}
loadRes, _ = loadContexts(loadContextsParams{
w: w,
r: r,
auth: auth,
loadReq: &loadReq,
plan: plan,
branchName: branchName,
cachedMapsByPath: cachedMapsByPath,
})
if loadRes == nil {
log.Println("LoadCachedFileMapHandler - loadRes is nil")
return
}
resp.LoadRes = loadRes
}
bytes, err := json.Marshal(resp)
if err != nil {
log.Printf("Error marshalling response: %v", err)
http.Error(w, fmt.Sprintf("Error marshalling response: %v", err), http.StatusInternalServerError)
return
}
w.Write(bytes)
}
================================================
FILE: app/server/handlers/file_maps_queue.go
================================================
package handlers
import (
"context"
"errors"
"log"
"math"
"plandex-server/syntax/file_map"
shared "plandex-shared"
"runtime"
"sync"
"time"
)
// simple in-memory per-instance queue for file map jobs
// ensures mapping doesn't take over all available CPUs
const fileMapMaxQueueSize = 20 // caller errors out if this is exceeded
var fileMapMaxConcurrency = 3 // set to 3/4 of available CPUs below
const mapJobTimeout = 60 * time.Second
type projectMapJob struct {
inputs shared.FileMapInputs
ctx context.Context
results chan shared.FileMapBodies
}
var projectMapQueue = make(chan projectMapJob, fileMapMaxQueueSize)
var mapCPUSem chan struct{}
func init() {
// Use 3/4 of available CPUs for mapping workers
cpus := runtime.NumCPU()
fileMapMaxConcurrency = int(math.Ceil(float64(cpus) * 0.75))
if fileMapMaxConcurrency < 1 {
fileMapMaxConcurrency = 1
}
log.Printf("fileMapMaxConcurrency: %d", fileMapMaxConcurrency)
mapCPUSem = make(chan struct{}, fileMapMaxConcurrency)
// start workers, one per CPU
for i := 0; i < fileMapMaxConcurrency; i++ {
go processProjectMapQueue()
}
}
func processProjectMapQueue() {
for job := range projectMapQueue {
if job.ctx.Err() != nil {
if job.ctx.Err() == context.DeadlineExceeded {
log.Printf("processProjectMapQueue: job context deadline exceeded: %v", job.ctx.Err())
safeSend(job.results, nil)
continue
}
log.Printf("processProjectMapQueue: job context cancelled: %v", job.ctx.Err())
safeSend(job.results, nil)
continue
}
ctxWithTimeout, cancel := context.WithTimeout(job.ctx, mapJobTimeout)
mapWorker(projectMapJob{
inputs: job.inputs,
ctx: ctxWithTimeout,
results: job.results,
})
cancel()
}
}
func queueProjectMapJob(job projectMapJob) error {
log.Printf("queueProjectMapJob: len(projectMapQueue): %d", len(projectMapQueue))
select {
case projectMapQueue <- job:
return nil
default:
return errors.New("queue is full")
}
}
func mapWorker(job projectMapJob) {
maps := make(shared.FileMapBodies)
wg := sync.WaitGroup{}
var mu sync.Mutex
log.Printf("mapWorker: len(job.inputs): %d", len(job.inputs))
for path, input := range job.inputs {
if !shared.HasFileMapSupport(path) {
mu.Lock()
maps[path] = "[NO MAP]"
mu.Unlock()
continue
}
wg.Add(1)
go func(path string, input string) {
if job.ctx.Err() != nil {
wg.Done()
return
}
mapCPUSem <- struct{}{}
defer func() { <-mapCPUSem }()
defer wg.Done()
fileMap, err := file_map.MapFile(job.ctx, path, []byte(input))
if err != nil {
// Skip files that can't be parsed, just log the error
log.Printf("Error mapping file %s: %v", path, err)
mu.Lock()
maps[path] = "[NO MAP]"
mu.Unlock()
return
}
mu.Lock()
maps[path] = fileMap.String()
mu.Unlock()
}(path, input)
}
wg.Wait()
if job.ctx.Err() != nil {
safeSend(job.results, nil)
return
}
safeSend(job.results, maps)
}
func safeSend(ch chan shared.FileMapBodies, v shared.FileMapBodies) {
// never block, never panic
select {
case ch <- v:
default: // buffer already full – receiver must have gone away
}
}
================================================
FILE: app/server/handlers/invites.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/email"
"strings"
shared "plandex-shared"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
)
func InviteUserHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for InviteUserHandler")
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Local mode is not supported for invites",
})
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't invite other users",
})
return
}
currentUserId := auth.User.Id
var req shared.InviteRequest
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Printf("Error unmarshalling request: %v\n", err)
http.Error(w, "Error unmarshalling request: "+err.Error(), http.StatusInternalServerError)
return
}
req.Email = strings.ToLower(req.Email)
// ensure current user can invite target user
permission := shared.Permission(strings.Join([]string{string(shared.PermissionInviteUser), req.OrgRoleId}, "|"))
if !auth.HasPermission(permission) {
log.Printf("User does not have permission to invite user with role: %v\n", req.OrgRoleId)
http.Error(w, "User does not have permission to invite user with role: "+req.OrgRoleId, http.StatusForbidden)
return
}
// ensure user doesn't already have access to org via domain
split := strings.Split(req.Email, "@")
if len(split) != 2 {
log.Printf("Invalid email: %v\n", req.Email)
http.Error(w, "Invalid email: "+req.Email, http.StatusBadRequest)
return
}
domain := &split[1]
if org.AutoAddDomainUsers && org.Domain == domain {
log.Printf("User already has access to org via domain: %v\n", domain)
http.Error(w, "User already has access to org via domain: "+*domain, http.StatusBadRequest)
}
// ensure user with this email isn't already in the org
user, err := db.GetUserByEmail(req.Email)
if err != nil {
log.Printf("Error getting user: %v\n", err)
http.Error(w, "Error getting user: "+err.Error(), http.StatusInternalServerError)
return
}
if user != nil {
isMember, err := db.ValidateOrgMembership(user.Id, auth.OrgId)
if err != nil {
log.Printf("Error validating org membership: %v\n", err)
http.Error(w, "Error validating org membership: "+err.Error(), http.StatusInternalServerError)
return
}
if isMember {
log.Println("User is already a member of org")
http.Error(w, "User is already a member of org", http.StatusBadRequest)
return
}
}
// ensure invite isn't already active
invite, err := db.GetActiveInviteByEmail(auth.OrgId, req.Email)
if err != nil {
log.Printf("Error getting invite: %v\n", err)
http.Error(w, "Error getting invite: "+err.Error(), http.StatusInternalServerError)
return
}
if invite != nil {
log.Println("Invite already exists")
http.Error(w, "Invite already exists", http.StatusBadRequest)
return
}
err = db.WithTx(r.Context(), "invite user", func(tx *sqlx.Tx) error {
err = db.CreateInvite(&db.Invite{
OrgId: auth.OrgId,
OrgRoleId: req.OrgRoleId,
Email: req.Email,
Name: req.Name,
InviterId: currentUserId,
}, tx)
if err != nil {
log.Printf("Error creating invite: %v\n", err)
return fmt.Errorf("error creating invite: %v", err)
}
err = email.SendInviteEmail(req.Email, req.Name, auth.User.Name, org.Name)
if err != nil {
log.Printf("Error sending invite email: %v\n", err)
return fmt.Errorf("error sending invite email: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error inviting user: %v\n", err)
http.Error(w, "Error inviting user: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully created invite")
}
func ListPendingInvitesHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for ListInvitesHandler")
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Local mode is not supported for invites",
})
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't list invites",
})
return
}
invites, err := db.ListPendingInvites(auth.OrgId)
if err != nil {
log.Printf("Error listing invites: %v\n", err)
http.Error(w, "Error listing invites: "+err.Error(), http.StatusInternalServerError)
return
}
var apiInvites []*shared.Invite
for _, invite := range invites {
apiInvites = append(apiInvites, invite.ToApi())
}
bytes, err := json.Marshal(apiInvites)
if err != nil {
log.Printf("Error marshalling invites: %v\n", err)
http.Error(w, "Error marshalling invites: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully processed request for ListPendingInvitesHandler")
}
func ListAcceptedInvitesHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for ListAcceptedInvitesHandler")
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Local mode is not supported for invites",
})
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't list invites",
})
return
}
invites, err := db.ListAcceptedInvites(auth.OrgId)
if err != nil {
log.Printf("Error listing invites: %v\n", err)
http.Error(w, "Error listing invites: "+err.Error(), http.StatusInternalServerError)
return
}
var apiInvites []*shared.Invite
for _, invite := range invites {
apiInvites = append(apiInvites, invite.ToApi())
}
bytes, err := json.Marshal(apiInvites)
if err != nil {
log.Printf("Error marshalling invites: %v\n", err)
http.Error(w, "Error marshalling invites: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully processed request for ListAcceptedInvitesHandler")
}
func ListAllInvitesHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for ListAllInvitesHandler")
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Local mode is not supported for invites",
})
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't list invites",
})
return
}
invites, err := db.ListAllInvites(auth.OrgId)
if err != nil {
log.Printf("Error listing invites: %v\n", err)
http.Error(w, "Error listing invites: "+err.Error(), http.StatusInternalServerError)
return
}
var apiInvites []*shared.Invite
for _, invite := range invites {
apiInvites = append(apiInvites, invite.ToApi())
}
bytes, err := json.Marshal(apiInvites)
if err != nil {
log.Printf("Error marshalling invites: %v\n", err)
http.Error(w, "Error marshalling invites: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully processed request for ListAllInvitesHandler")
}
func DeleteInviteHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for DeleteInviteHandler")
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Local mode is not supported for invites",
})
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't delete invites",
})
return
}
vars := mux.Vars(r)
inviteId := vars["inviteId"]
invite, err := db.GetInvite(inviteId)
if err != nil {
log.Printf("Error getting invite: %v\n", err)
http.Error(w, "Error getting invite: "+err.Error(), http.StatusInternalServerError)
return
}
if invite == nil || invite.OrgId != auth.OrgId {
log.Printf("Invite not found: %v\n", inviteId)
http.Error(w, "Invite not found: "+inviteId, http.StatusNotFound)
return
}
// ensure current user can remove target invite
removePermission := shared.Permission(strings.Join([]string{string(shared.PermissionRemoveUser), invite.OrgRoleId}, "|"))
invitePermission := shared.Permission(strings.Join([]string{string(shared.PermissionInviteUser), invite.OrgRoleId}, "|"))
if !(auth.HasPermission(removePermission) ||
(auth.User.Id == invite.InviterId && auth.HasPermission(invitePermission))) {
log.Printf("User does not have permission to remove invite with role: %v\n", invite.OrgRoleId)
http.Error(w, "User does not have permission to remove invite with role: "+invite.OrgRoleId, http.StatusForbidden)
return
}
err = db.DeleteInvite(inviteId, nil)
if err != nil {
log.Printf("Error deleting invite: %v\n", err)
http.Error(w, "Error deleting invite: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully deleted invite")
}
================================================
FILE: app/server/handlers/models.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"plandex-server/db"
shared "plandex-shared"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
)
const CustomModelsMinClientVersion = "2.2.0"
func UpsertCustomModelsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateCustomModelHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {
return
}
var modelsInput shared.ModelsInput
if err := json.NewDecoder(r.Body).Decode(&modelsInput); err != nil {
log.Printf("Error decoding request body: %v\n", err)
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
if len(modelsInput.CustomProviders) > 0 {
if os.Getenv("IS_CLOUD") != "" {
http.Error(w, "Custom model providers are not supported on Plandex Cloud", http.StatusBadRequest)
return
}
}
if len(modelsInput.CustomModels) > 0 {
if os.Getenv("IS_CLOUD") != "" {
apiOrg, err := getApiOrg(auth.OrgId)
if err != nil {
log.Printf("Error fetching org: %v\n", err)
http.Error(w, "Failed to create custom model: "+err.Error(), http.StatusInternalServerError)
return
}
if apiOrg.IntegratedModelsMode {
http.Error(w, "Custom models are not supported on Plandex Cloud in Integrated Models mode", http.StatusBadRequest)
return
}
}
}
hasDuplicates, errMsg := modelsInput.CheckNoDuplicates()
if !hasDuplicates {
http.Error(w, "Has duplicates: "+errMsg, http.StatusBadRequest)
return
}
for _, provider := range modelsInput.CustomProviders {
if provider.Name == "" {
msg := "Provider name is required"
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
}
for _, model := range modelsInput.CustomModels {
if model.ModelId == "" {
msg := "Model id is required"
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
if shared.BuiltInBaseModelsById[model.ModelId] != nil {
msg := fmt.Sprintf("%s is a built-in base model id, so it can't be used for a custom model", model.ModelId)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
}
for _, modelPack := range modelsInput.CustomModelPacks {
if modelPack.Name == "" {
msg := "Model pack name is required"
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
if shared.BuiltInModelPacksByName[modelPack.Name] != nil {
msg := fmt.Sprintf("%s is a built-in model pack name, so it can't be used for a custom model pack", modelPack.Name)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
}
var existingCustomModelIds = make(map[shared.ModelId]bool)
var existingCustomProviderNames = make(map[string]bool)
customModels, err := db.ListCustomModels(auth.OrgId)
if err != nil {
log.Printf("Error fetching custom models: %v\n", err)
http.Error(w, "Failed to create custom model: "+err.Error(), http.StatusInternalServerError)
return
}
customModelPacks, err := db.ListModelPacks(auth.OrgId)
if err != nil {
log.Printf("Error fetching custom model packs: %v\n", err)
http.Error(w, "Failed to create custom model: "+err.Error(), http.StatusInternalServerError)
return
}
var customProviders []*db.CustomProvider
if os.Getenv("IS_CLOUD") == "" {
customProviders, err = db.ListCustomProviders(auth.OrgId)
if err != nil {
log.Printf("Error fetching custom providers: %v\n", err)
http.Error(w, "Failed to create custom model: "+err.Error(), http.StatusInternalServerError)
return
}
}
apiCustomModels := make([]*shared.CustomModel, len(customModels))
for i, model := range customModels {
apiCustomModels[i] = model.ToApi()
}
apiCustomProviders := make([]*shared.CustomProvider, len(customProviders))
for i, provider := range customProviders {
apiCustomProviders[i] = provider.ToApi()
}
apiCustomModelPacks := make([]*shared.ModelPackSchema, len(customModelPacks))
for i, modelPack := range customModelPacks {
apiCustomModelPacks[i] = modelPack.ToApi().ToModelPackSchema()
}
updatedModelsInput := modelsInput.FilterUnchanged(&shared.ModelsInput{
CustomModels: apiCustomModels,
CustomProviders: apiCustomProviders,
CustomModelPacks: apiCustomModelPacks,
})
for _, model := range customModels {
existingCustomModelIds[model.ModelId] = true
}
for _, provider := range customProviders {
existingCustomProviderNames[provider.Name] = true
}
inputModelIds := make(map[string]bool)
inputProviderNames := make(map[string]bool)
inputModelPackNames := make(map[string]bool)
for _, model := range modelsInput.CustomModels {
inputModelIds[string(model.ModelId)] = true
}
for _, provider := range modelsInput.CustomProviders {
inputProviderNames[provider.Name] = true
}
for _, modelPack := range modelsInput.CustomModelPacks {
inputModelPackNames[modelPack.Name] = true
}
var toUpsertCustomModels []*db.CustomModel
var toUpsertCustomProviders []*db.CustomProvider
var toUpsertModelPacks []*db.ModelPack
for _, provider := range updatedModelsInput.CustomProviders {
dbProvider := db.CustomProviderFromApi(provider)
dbProvider.Id = provider.Id
dbProvider.OrgId = auth.OrgId
toUpsertCustomProviders = append(toUpsertCustomProviders, dbProvider)
}
for _, model := range updatedModelsInput.CustomModels {
// ensure that providers to upsert are either built-in, being imported, or already exist
for _, provider := range model.Providers {
if provider.Provider == shared.ModelProviderCustom {
_, exists := existingCustomProviderNames[*provider.CustomProvider]
_, creating := inputProviderNames[*provider.CustomProvider]
if !exists && !creating {
msg := fmt.Sprintf("'%s' is not a custom model provider that exists or is being imported", *provider.CustomProvider)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
} else {
pc, builtIn := shared.BuiltInModelProviderConfigs[provider.Provider]
if !builtIn {
msg := fmt.Sprintf("'%s' is not a built-in model provider", provider.Provider)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
if os.Getenv("IS_CLOUD") != "" && pc.LocalOnly {
msg := fmt.Sprintf("'%s' is a local-only model provider, so it can't be used on Plandex Cloud", provider.Provider)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
}
}
dbModel := db.CustomModelFromApi(model)
dbModel.Id = model.Id
dbModel.OrgId = auth.OrgId
toUpsertCustomModels = append(toUpsertCustomModels, dbModel)
}
for _, modelPack := range updatedModelsInput.CustomModelPacks {
// ensure that all models are either built-in, being imported, or already exist
allModelIds := modelPack.AllModelIds()
for _, modelId := range allModelIds {
_, exists := existingCustomModelIds[modelId]
_, creating := inputModelIds[string(modelId)]
bm, builtIn := shared.BuiltInBaseModelsById[modelId]
if !exists && !creating && !builtIn {
msg := fmt.Sprintf("'%s' is not built-in, not being imported, and not an existing custom model", modelId)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
if builtIn && os.Getenv("IS_CLOUD") != "" && bm.IsLocalOnly() {
msg := fmt.Sprintf("'%s' is a local-only built-in model, so it can't be used on Plandex Cloud", modelId)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
}
mp := modelPack.ToModelPack()
dbMp := db.ModelPackFromApi(&mp)
dbMp.OrgId = auth.OrgId
dbMp.Id = mp.Id
toUpsertModelPacks = append(toUpsertModelPacks, dbMp)
}
toDeleteCustomModelIds := []string{}
toDeleteCustomProviderIds := []string{}
toDeleteModelPackIds := []string{}
for _, model := range customModels {
if _, exists := inputModelIds[string(model.ModelId)]; !exists {
toDeleteCustomModelIds = append(toDeleteCustomModelIds, model.Id)
}
}
for _, provider := range customProviders {
if _, exists := inputProviderNames[provider.Name]; !exists {
toDeleteCustomProviderIds = append(toDeleteCustomProviderIds, provider.Id)
}
}
for _, modelPack := range customModelPacks {
if _, exists := inputModelPackNames[modelPack.Name]; !exists {
toDeleteModelPackIds = append(toDeleteModelPackIds, modelPack.Id)
}
}
numChanges := len(toUpsertCustomModels) + len(toUpsertCustomProviders) + len(toUpsertModelPacks) + len(toDeleteCustomModelIds) + len(toDeleteCustomProviderIds) + len(toDeleteModelPackIds)
if numChanges == 0 {
w.WriteHeader(http.StatusOK)
log.Println("No changes to custom models/providers/model packs")
return
}
err = db.WithTx(r.Context(), "create custom models/providers/model packs", func(tx *sqlx.Tx) error {
for _, model := range toUpsertCustomModels {
if err := db.UpsertCustomModel(tx, model); err != nil {
return fmt.Errorf("error creating custom model: %w", err)
}
}
for _, provider := range toUpsertCustomProviders {
if err := db.UpsertCustomProvider(tx, provider); err != nil {
return fmt.Errorf("error creating custom provider: %w", err)
}
}
for _, modelPack := range toUpsertModelPacks {
if err := db.UpsertModelPack(tx, modelPack); err != nil {
return fmt.Errorf("error creating model pack: %w", err)
}
}
if len(toDeleteCustomModelIds) > 0 {
if err := db.DeleteCustomModels(tx, auth.OrgId, toDeleteCustomModelIds); err != nil {
return fmt.Errorf("error deleting custom models: %w", err)
}
}
if len(toDeleteCustomProviderIds) > 0 {
if err := db.DeleteCustomProviders(tx, auth.OrgId, toDeleteCustomProviderIds); err != nil {
return fmt.Errorf("error deleting custom providers: %w", err)
}
}
if len(toDeleteModelPackIds) > 0 {
if err := db.DeleteModelPacks(tx, auth.OrgId, toDeleteModelPackIds); err != nil {
return fmt.Errorf("error deleting model packs: %w", err)
}
}
return nil
})
if err != nil {
log.Printf("Error: %v\n", err)
http.Error(w, "Failed to import custom models/providers/model packs: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
log.Println("Successfully imported custom models/providers/model packs")
}
func GetCustomModelHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetCustomModelHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
id := mux.Vars(r)["modelId"]
res, err := db.GetCustomModel(auth.OrgId, id)
if err != nil {
log.Printf("Error fetching custom model: %v\n", err)
http.Error(w, "Failed to fetch custom model: "+err.Error(), http.StatusInternalServerError)
return
}
if res == nil {
http.Error(w, "Custom model not found", http.StatusNotFound)
return
}
err = json.NewEncoder(w).Encode(res.ToApi())
if err != nil {
log.Printf("Error encoding custom model: %v\n", err)
http.Error(w, fmt.Sprintf("Error encoding custom model: %v", err), http.StatusInternalServerError)
return
}
log.Println("Successfully fetched custom model")
}
func ListCustomModelsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListCustomModelsHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {
return
}
models, err := db.ListCustomModels(auth.OrgId)
if err != nil {
log.Printf("Error fetching custom models: %v\n", err)
http.Error(w, "Failed to fetch custom models: "+err.Error(), http.StatusInternalServerError)
return
}
var apiList []*shared.CustomModel
for _, m := range models {
apiList = append(apiList, m.ToApi())
}
err = json.NewEncoder(w).Encode(apiList)
if err != nil {
log.Printf("Error encoding custom models: %v\n", err)
http.Error(w, fmt.Sprintf("Error encoding custom models: %v", err), http.StatusInternalServerError)
return
}
log.Println("Successfully fetched custom models")
}
func GetCustomProviderHandler(w http.ResponseWriter, r *http.Request) {
auth := Authenticate(w, r, true)
if auth == nil {
return
}
id := mux.Vars(r)["providerId"]
res, err := db.GetCustomProvider(auth.OrgId, id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(res.ToApi())
if err != nil {
log.Printf("Error encoding custom provider: %v\n", err)
http.Error(w, fmt.Sprintf("Error encoding custom provider: %v", err), http.StatusInternalServerError)
return
}
log.Println("Successfully fetched custom provider")
}
func ListCustomProvidersHandler(w http.ResponseWriter, r *http.Request) {
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if os.Getenv("IS_CLOUD") != "" {
http.Error(w, "Custom model providers are not supported on Plandex Cloud", http.StatusBadRequest)
return
}
list, err := db.ListCustomProviders(auth.OrgId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var apiList []*shared.CustomProvider
for _, p := range list {
apiList = append(apiList, p.ToApi())
}
err = json.NewEncoder(w).Encode(apiList)
if err != nil {
log.Printf("Error encoding custom providers: %v\n", err)
http.Error(w, fmt.Sprintf("Error encoding custom providers: %v", err), http.StatusInternalServerError)
return
}
log.Println("Successfully fetched custom providers")
}
func CreateModelPackHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateModelPackHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {
return
}
http.Error(w, "Use POST /custom_models instead to create model packs", http.StatusBadRequest)
}
func UpdateModelPackHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdateModelPackHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {
return
}
http.Error(w, "Use POST /custom_models instead to update model packs", http.StatusBadRequest)
}
func ListModelPacksHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListModelPacksHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {
return
}
sets, err := db.ListModelPacks(auth.OrgId)
if err != nil {
log.Printf("Error fetching model packs: %v\n", err)
http.Error(w, "Failed to fetch model packs: "+err.Error(), http.StatusInternalServerError)
return
}
var apiPacks []*shared.ModelPack
for _, mp := range sets {
apiPacks = append(apiPacks, mp.ToApi())
}
json.NewEncoder(w).Encode(apiPacks)
log.Println("Successfully fetched model packs")
}
================================================
FILE: app/server/handlers/org_helpers.go
================================================
package handlers
import (
"log"
"plandex-server/db"
"plandex-server/hooks"
shared "plandex-shared"
)
func toApiOrgs(orgs []*db.Org) ([]*shared.Org, *shared.ApiError) {
var orgIds []string
for _, org := range orgs {
orgIds = append(orgIds, org.Id)
}
hookRes, apiErr := hooks.ExecHook(hooks.GetApiOrgs, hooks.HookParams{
GetApiOrgIds: orgIds,
})
if apiErr != nil {
log.Printf("Error getting integrated models mode by org id: %v\n", apiErr)
return nil, apiErr
}
var apiOrgs []*shared.Org
for _, org := range orgs {
if hookRes.ApiOrgsById != nil {
hookApiOrg := hookRes.ApiOrgsById[org.Id]
apiOrgs = append(apiOrgs, hookApiOrg)
} else {
apiOrgs = append(apiOrgs, org.ToApi())
}
}
return apiOrgs, nil
}
func getApiOrg(orgId string) (*shared.Org, *shared.ApiError) {
org, err := db.GetOrg(orgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
return nil, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Msg: "Error getting org",
}
}
hookRes, apiErr := hooks.ExecHook(hooks.GetApiOrgs, hooks.HookParams{
GetApiOrgIds: []string{org.Id},
})
if apiErr != nil {
log.Printf("Error getting integrated models mode by org id: %v\n", apiErr)
return nil, apiErr
}
if hookRes.ApiOrgsById != nil {
return hookRes.ApiOrgsById[org.Id], nil
}
return org.ToApi(), nil
}
================================================
FILE: app/server/handlers/orgs.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/hooks"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
)
func ListOrgsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListOrgsHandler")
auth := Authenticate(w, r, false)
if auth == nil {
return
}
orgs, err := db.GetAccessibleOrgsForUser(auth.User)
if err != nil {
log.Printf("Error listing orgs: %v\n", err)
http.Error(w, "Error listing orgs: "+err.Error(), http.StatusInternalServerError)
return
}
apiOrgs, apiErr := toApiOrgs(orgs)
if apiErr != nil {
log.Printf("Error converting orgs to api: %v\n", apiErr)
writeApiError(w, *apiErr)
return
}
bytes, err := json.Marshal(apiOrgs)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully listed orgs")
w.Write(bytes)
}
func CreateOrgHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateOrgHandler")
if os.Getenv("IS_CLOUD") != "" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Plandex Cloud orgs can only be created by starting a trial",
})
return
}
auth := Authenticate(w, r, false)
if auth == nil {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body: "+err.Error(), http.StatusInternalServerError)
return
}
var req shared.CreateOrgRequest
err = json.Unmarshal(body, &req)
if err != nil {
log.Printf("Error unmarshalling request: %v\n", err)
http.Error(w, "Error unmarshalling request: "+err.Error(), http.StatusInternalServerError)
return
}
var apiErr *shared.ApiError
var org *db.Org
err = db.WithTx(r.Context(), "create org", func(tx *sqlx.Tx) error {
var err error
var domain *string
if req.AutoAddDomainUsers {
if shared.IsEmailServiceDomain(auth.User.Domain) {
log.Printf("Invalid domain: %v\n", auth.User.Domain)
return fmt.Errorf("invalid domain: %v", auth.User.Domain)
}
domain = &auth.User.Domain
}
// create a new org
org, err = db.CreateOrg(&req, auth.AuthToken.UserId, domain, tx)
if err != nil {
log.Printf("Error creating org: %v\n", err)
return fmt.Errorf("error creating org: %v", err)
}
if org.AutoAddDomainUsers && org.Domain != nil {
err = db.AddOrgDomainUsers(org.Id, *org.Domain, tx)
if err != nil {
log.Printf("Error adding org domain users: %v\n", err)
return fmt.Errorf("error adding org domain users: %v", err)
}
}
_, apiErr = hooks.ExecHook(hooks.CreateOrg, hooks.HookParams{
Auth: auth,
Tx: tx,
CreateOrgHookRequestParams: &hooks.CreateOrgHookRequestParams{
Org: org,
},
})
return nil
})
if apiErr != nil {
writeApiError(w, *apiErr)
return
}
if err != nil {
log.Printf("Error creating org: %v\n", err)
http.Error(w, "Error creating org: "+err.Error(), http.StatusInternalServerError)
return
}
resp := shared.CreateOrgResponse{
Id: org.Id,
}
bytes, err := json.Marshal(resp)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
err = SetAuthCookieIfBrowser(w, r, auth.User, "", org.Id)
if err != nil {
log.Printf("Error setting auth cookie: %v\n", err)
http.Error(w, "Error setting auth cookie: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully created org")
w.Write(bytes)
}
func GetOrgSessionHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetOrgSessionHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, apiErr := getApiOrg(auth.OrgId)
if apiErr != nil {
log.Printf("Error converting org to api: %v\n", apiErr)
writeApiError(w, *apiErr)
return
}
bytes, err := json.Marshal(org)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
err = SetAuthCookieIfBrowser(w, r, auth.User, "", org.Id)
if err != nil {
log.Printf("Error setting auth cookie: %v\n", err)
http.Error(w, "Error setting auth cookie: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully got org session")
}
func ListOrgRolesHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListOrgRolesHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't list org roles",
})
return
}
if !auth.HasPermission(shared.PermissionListOrgRoles) {
log.Println("User cannot list org roles")
http.Error(w, "User cannot list org roles", http.StatusForbidden)
return
}
roles, err := db.ListOrgRoles(auth.OrgId)
if err != nil {
log.Printf("Error listing org roles: %v\n", err)
http.Error(w, "Error listing org roles: "+err.Error(), http.StatusInternalServerError)
return
}
var apiRoles []*shared.OrgRole
for _, role := range roles {
apiRoles = append(apiRoles, role.ToApi())
}
bytes, err := json.Marshal(apiRoles)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully listed org roles")
w.Write(bytes)
}
================================================
FILE: app/server/handlers/plan_config.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"plandex-server/db"
shared "plandex-shared"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
)
func GetPlanConfigHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetPlanConfigHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
config, err := db.GetPlanConfig(planId)
if err != nil {
log.Println("Error getting plan config: ", err)
http.Error(w, "Error getting plan config", http.StatusInternalServerError)
return
}
res := shared.GetPlanConfigResponse{
Config: config,
}
bytes, err := json.Marshal(res)
if err != nil {
log.Println("Error marshalling response: ", err)
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("GetPlanConfigHandler processed successfully")
}
func UpdatePlanConfigHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdatePlanConfigHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
var req shared.UpdatePlanConfigRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Println("Error decoding request body: ", err)
http.Error(w, "Error decoding request body", http.StatusBadRequest)
return
}
err = db.StorePlanConfig(planId, req.Config)
if err != nil {
log.Println("Error storing plan config: ", err)
http.Error(w, "Error storing plan config", http.StatusInternalServerError)
return
}
log.Println("UpdatePlanConfigHandler processed successfully")
}
func GetDefaultPlanConfigHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetDefaultPlanConfigHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
config, err := db.GetDefaultPlanConfig(auth.User.Id)
if err != nil {
log.Println("Error getting default plan config: ", err)
http.Error(w, "Error getting default plan config", http.StatusInternalServerError)
return
}
res := shared.GetDefaultPlanConfigResponse{
Config: config,
}
bytes, err := json.Marshal(res)
if err != nil {
log.Println("Error marshalling response: ", err)
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("GetDefaultPlanConfigHandler processed successfully")
}
func UpdateDefaultPlanConfigHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdateDefaultPlanConfigHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
var req shared.UpdateDefaultPlanConfigRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Println("Error decoding request body: ", err)
http.Error(w, "Error decoding request body", http.StatusBadRequest)
return
}
err = db.WithTx(r.Context(), "update default plan config", func(tx *sqlx.Tx) error {
err := db.StoreDefaultPlanConfig(auth.User.Id, req.Config, tx)
if err != nil {
log.Println("Error storing default plan config: ", err)
return fmt.Errorf("error storing default plan config: %v", err)
}
return nil
})
if err != nil {
log.Println("Error updating default plan config: ", err)
http.Error(w, "Error updating default plan config", http.StatusInternalServerError)
return
}
log.Println("UpdateDefaultPlanConfigHandler processed successfully")
}
================================================
FILE: app/server/handlers/plans_changes.go
================================================
package handlers
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"plandex-server/db"
modelPlan "plandex-server/model/plan"
"time"
shared "plandex-shared"
"github.com/gorilla/mux"
)
func CurrentPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CurrentPlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
sha := vars["sha"]
log.Println("planId: ", planId, "branch: ", branch, "sha: ", sha)
if authorizePlan(w, planId, auth) == nil {
return
}
// Just in case this was sent immediately after a stream finished, wait a little before locking to allow for cleanup
time.Sleep(100 * time.Millisecond)
ctx, cancel := context.WithCancel(r.Context())
scope := db.LockScopeRead
if sha != "" {
scope = db.LockScopeWrite
}
log.Printf("locking with scope: %s", scope)
var planState *shared.CurrentPlanState
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: scope,
Ctx: ctx,
CancelFn: cancel,
Reason: "get current plan state",
}, func(repo *db.GitRepo) error {
var err error
if sha != "" {
err = repo.GitCheckoutSha(sha)
if err != nil {
return fmt.Errorf("error checking out sha: %v", err)
}
defer func() {
checkoutErr := repo.GitCheckoutBranch(branch)
if checkoutErr != nil {
log.Printf("Error checking out branch: %v\n", checkoutErr)
}
}()
}
planState, err = db.GetCurrentPlanState(db.CurrentPlanStateParams{
OrgId: auth.OrgId,
PlanId: planId,
})
if err != nil {
return fmt.Errorf("error getting current plan state: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error getting current plan state: %v\n", err)
http.Error(w, "Error getting current plan state: "+err.Error(), http.StatusInternalServerError)
return
}
jsonBytes, err := json.Marshal(planState)
if err != nil {
log.Printf("Error marshalling plan state: %v\n", err)
http.Error(w, "Error marshalling plan state: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully retrieved current plan state")
w.Write(jsonBytes)
}
func ApplyPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ApplyPlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
var err error
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.ApplyPlanRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
// Just in case this was sent immediately after a stream finished, wait a little before locking to allow for cleanup
time.Sleep(100 * time.Millisecond)
ctx, cancel := context.WithCancel(r.Context())
var settings *shared.PlanSettings
var currentPlanParams db.CurrentPlanStateParams
var currentPlan *shared.CurrentPlanState
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
var err error
settings, err = db.GetPlanSettings(plan)
if err != nil {
return fmt.Errorf("error getting plan settings: %v", err)
}
currentPlanParams, err = db.GetFullCurrentPlanStateParams(auth.OrgId, planId)
if err != nil {
return fmt.Errorf("error getting current plan state params: %v", err)
}
currentPlan, err = db.GetCurrentPlanState(currentPlanParams)
if err != nil {
return fmt.Errorf("error getting current plan state: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error getting current plan state: %v\n", err)
http.Error(w, "Error getting current plan state: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("ApplyPlanHandler: Got current plan state:", currentPlan != nil)
res := initClients(
initClientsParams{
w: w,
auth: auth,
apiKeys: requestBody.ApiKeys,
openAIOrgId: requestBody.OpenAIOrgId,
authVars: requestBody.AuthVars,
plan: plan,
settings: settings,
},
)
clients := res.clients
authVars := res.authVars
commitMsg, err := modelPlan.GenCommitMsgForPendingResults(modelPlan.GenCommitMsgForPendingResultsParams{
Auth: auth,
Plan: plan,
Clients: clients,
Settings: settings,
Current: currentPlan,
AuthVars: authVars,
SessionId: requestBody.SessionId,
Ctx: r.Context(),
})
if err != nil {
log.Printf("Error generating commit message: %v\n", err)
http.Error(w, "Error generating commit message: "+err.Error(), http.StatusInternalServerError)
return
}
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
ClearRepoOnErr: true,
}, func(repo *db.GitRepo) error {
return db.ApplyPlan(repo, ctx, db.ApplyPlanParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
BranchName: branch,
Plan: plan,
CurrentPlanState: currentPlan,
CurrentPlanStateParams: ¤tPlanParams,
CommitMsg: commitMsg,
})
})
if err != nil {
log.Printf("Error applying plan: %v\n", err)
http.Error(w, "Error applying plan: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte(commitMsg))
log.Println("Successfully applied plan", planId)
}
func RejectAllChangesHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for RejectAllChangesHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
if authorizePlan(w, planId, auth) == nil {
return
}
ctx, cancel := context.WithCancel(r.Context())
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
ClearRepoOnErr: true,
}, func(repo *db.GitRepo) error {
err := db.RejectAllResults(auth.OrgId, planId)
if err != nil {
return err
}
err = repo.GitAddAndCommit(branch, "🚫 Rejected all pending changes")
if err != nil {
return fmt.Errorf("error committing rejected changes: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error rejecting all changes: %v\n", err)
http.Error(w, "Error rejecting all changes: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully rejected all changes for plan", planId)
}
func RejectFileHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for RejectFileHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
if authorizePlan(w, planId, auth) == nil {
return
}
var req shared.RejectFileRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Printf("Error decoding request: %v\n", err)
http.Error(w, "Error decoding request: "+err.Error(), http.StatusBadRequest)
return
}
ctx, cancel := context.WithCancel(r.Context())
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
ClearRepoOnErr: true,
}, func(repo *db.GitRepo) error {
err = db.RejectPlanFile(auth.OrgId, planId, req.FilePath, time.Now())
if err != nil {
return err
}
err = repo.GitAddAndCommit(branch, fmt.Sprintf("🚫 Rejected pending changes to file: %s", req.FilePath))
if err != nil {
return fmt.Errorf("error committing rejected changes: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error rejecting result: %v\n", err)
http.Error(w, "Error rejecting result: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully rejected plan file", req.FilePath)
}
func RejectFilesHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for RejectFilesHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
if authorizePlan(w, planId, auth) == nil {
return
}
var req shared.RejectFilesRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Printf("Error decoding request: %v\n", err)
http.Error(w, "Error decoding request: "+err.Error(), http.StatusBadRequest)
return
}
ctx, cancel := context.WithCancel(r.Context())
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
ClearRepoOnErr: true,
}, func(repo *db.GitRepo) error {
err = db.RejectPlanFiles(auth.OrgId, planId, req.Paths, time.Now())
if err != nil {
return err
}
msg := "🚫 Rejected pending changes to file"
if len(req.Paths) > 1 {
msg += "s"
}
msg += ":"
for _, path := range req.Paths {
msg += fmt.Sprintf("\n • %s", path)
}
err = repo.GitAddAndCommit(branch, msg)
if err != nil {
return fmt.Errorf("error committing rejected changes: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error rejecting result: %v\n", err)
http.Error(w, "Error rejecting result: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully rejected plan files", req.Paths)
}
func ArchivePlanHandler(w http.ResponseWriter, r *http.Request) {
auth := Authenticate(w, r, true)
if auth == nil {
return
}
log.Println("Received request for ArchivePlanHandler")
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
plan := authorizePlanArchive(w, planId, auth)
if plan == nil {
return
}
if plan.ArchivedAt != nil {
log.Println("Plan already archived")
http.Error(w, "Plan already archived", http.StatusBadRequest)
return
}
res, err := db.Conn.Exec("UPDATE plans SET archived_at = NOW() WHERE id = $1", planId)
if err != nil {
log.Printf("Error archiving plan: %v\n", err)
http.Error(w, "Error archiving plan: "+err.Error(), http.StatusInternalServerError)
return
}
rowsAffected, err := res.RowsAffected()
if err != nil {
log.Printf("Error getting rows affected: %v\n", err)
http.Error(w, "Error getting rows affected: "+err.Error(), http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
log.Println("Plan not found")
http.Error(w, "Not found", http.StatusNotFound)
return
}
log.Println("Successfully archived plan", planId)
}
func UnarchivePlanHandler(w http.ResponseWriter, r *http.Request) {
auth := Authenticate(w, r, true)
if auth == nil {
return
}
log.Println("Received request for UnarchivePlanHandler")
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
plan := authorizePlanArchive(w, planId, auth)
if plan == nil {
return
}
if plan.ArchivedAt == nil {
log.Println("Plan isn't archived")
http.Error(w, "Plan isn't archived", http.StatusBadRequest)
return
}
res, err := db.Conn.Exec("UPDATE plans SET archived_at = NULL WHERE id = $1", planId)
if err != nil {
log.Printf("Error archiving plan: %v\n", err)
http.Error(w, "Error archiving plan: "+err.Error(), http.StatusInternalServerError)
return
}
rowsAffected, err := res.RowsAffected()
if err != nil {
log.Printf("Error getting rows affected: %v\n", err)
http.Error(w, "Error getting rows affected: "+err.Error(), http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
log.Println("Plan not found")
http.Error(w, "Not found", http.StatusNotFound)
return
}
log.Println("Successfully unarchived plan", planId)
}
func GetPlanDiffsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetPlanDiffs")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
plain := r.URL.Query().Get("plain") == "true"
log.Println("planId: ", planId, "branch: ", branch)
if authorizePlan(w, planId, auth) == nil {
return
}
ctx, cancel := context.WithCancel(r.Context())
var diffs string
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
var err error
diffs, err = db.GetPlanDiffs(auth.OrgId, planId, plain)
if err != nil {
return err
}
return nil
})
if err != nil {
log.Printf("Error getting plan diffs: %v\n", err)
http.Error(w, "Error getting plan diffs: "+err.Error(), http.StatusInternalServerError)
return
}
log.Printf("diffs: %s", diffs)
w.Write([]byte(diffs))
log.Println("Successfully retrieved plan diffs")
}
================================================
FILE: app/server/handlers/plans_context.go
================================================
package handlers
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"plandex-server/db"
shared "plandex-shared"
"github.com/gorilla/mux"
)
func ListContextHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListContextHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
if authorizePlan(w, planId, auth) == nil {
return
}
ctx, cancel := context.WithCancel(r.Context())
var dbContexts []*db.Context
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "list contexts",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
res, err := db.GetPlanContexts(auth.OrgId, planId, false, false)
if err != nil {
return err
}
dbContexts = res
return nil
})
if err != nil {
log.Printf("Error getting contexts: %v\n", err)
http.Error(w, "Error getting contexts: "+err.Error(), http.StatusInternalServerError)
return
}
var apiContexts []*shared.Context
for _, dbContext := range dbContexts {
apiContexts = append(apiContexts, dbContext.ToApi())
}
bytes, err := json.Marshal(apiContexts)
if err != nil {
log.Printf("Error marshalling contexts: %v\n", err)
http.Error(w, "Error marshalling contexts: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
}
func GetContextBodyHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetContextBodyHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
contextId := vars["contextId"]
log.Println("planId:", planId, "branch:", branch, "contextId:", contextId)
if authorizePlan(w, planId, auth) == nil {
return
}
ctx, cancel := context.WithCancel(r.Context())
var dbContexts []*db.Context
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "get context body",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
res, err := db.GetPlanContexts(auth.OrgId, planId, true, false)
if err != nil {
return err
}
dbContexts = res
return nil
})
if err != nil {
log.Printf("Error getting contexts: %v\n", err)
http.Error(w, "Error getting contexts: "+err.Error(), http.StatusInternalServerError)
return
}
var targetContext *db.Context
for _, dbContext := range dbContexts {
if dbContext.Id == contextId {
targetContext = dbContext
break
}
}
if targetContext == nil {
http.Error(w, "Context not found", http.StatusNotFound)
return
}
response := shared.GetContextBodyResponse{
Body: targetContext.Body,
}
bytes, err := json.Marshal(response)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
}
func LoadContextHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for LoadContextHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branchName := vars["branch"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.LoadContextRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
res, _ := loadContexts(loadContextsParams{
w: w,
r: r,
auth: auth,
loadReq: &requestBody,
plan: plan,
branchName: branchName,
})
if res == nil {
return
}
bytes, err := json.Marshal(res)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed LoadContextHandler request")
w.Write(bytes)
}
func UpdateContextHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdateContextHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branchName := vars["branch"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.UpdateContextRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
ctx, cancel := context.WithCancel(r.Context())
var updateRes *shared.UpdateContextResponse
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branchName,
Reason: "update contexts",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
ClearRepoOnErr: true,
}, func(repo *db.GitRepo) error {
var err error
updateRes, err = db.UpdateContexts(db.UpdateContextsParams{
Req: &requestBody,
OrgId: auth.OrgId,
Plan: plan,
BranchName: branchName,
})
if err != nil {
return err
}
if updateRes.MaxTokensExceeded {
return nil
}
err = repo.GitAddAndCommit(branchName, updateRes.Msg)
if err != nil {
return fmt.Errorf("error committing changes: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error error updating contexts: %v\n", err)
http.Error(w, "Error error updating contexts: "+err.Error(), http.StatusInternalServerError)
return
}
if updateRes.MaxTokensExceeded {
log.Printf("The total number of tokens (%d) exceeds the maximum allowed (%d)", updateRes.TotalTokens, updateRes.MaxTokens)
bytes, err := json.Marshal(updateRes)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
return
}
bytes, err := json.Marshal(updateRes)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed UpdateContextHandler request")
w.Write(bytes)
}
func DeleteContextHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for DeleteContextHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branchName := vars["branch"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
branch, err := db.GetDbBranch(planId, branchName)
if err != nil {
log.Printf("Error getting branch: %v\n", err)
http.Error(w, "Error getting branch: "+err.Error(), http.StatusInternalServerError)
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.DeleteContextRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
ctx, cancel := context.WithCancel(r.Context())
var dbContexts []*db.Context
var toRemove []*db.Context
var commitMsg string
removeTokens := 0
var toRemoveApiContexts []*shared.Context
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branchName,
Reason: "delete contexts",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
ClearRepoOnErr: true,
}, func(repo *db.GitRepo) error {
var err error
dbContexts, err = db.GetPlanContexts(auth.OrgId, planId, false, false)
if err != nil {
return fmt.Errorf("error getting contexts: %v", err)
}
for _, dbContext := range dbContexts {
if _, ok := requestBody.Ids[dbContext.Id]; ok {
toRemove = append(toRemove, dbContext)
}
}
err = db.ContextRemove(auth.OrgId, planId, toRemove)
if err != nil {
return fmt.Errorf("error removing contexts: %v", err)
}
for _, dbContext := range toRemove {
toRemoveApiContexts = append(toRemoveApiContexts, dbContext.ToApi())
removeTokens += dbContext.NumTokens
}
commitMsg = shared.SummaryForRemoveContext(toRemoveApiContexts, branch.ContextTokens) + "\n\n" + shared.TableForRemoveContext(toRemoveApiContexts)
err = repo.GitAddAndCommit(branchName, commitMsg)
if err != nil {
return fmt.Errorf("error committing changes: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error deleting contexts: %v\n", err)
http.Error(w, "Error deleting contexts: "+err.Error(), http.StatusInternalServerError)
return
}
err = db.AddPlanContextTokens(planId, branchName, -removeTokens)
if err != nil {
log.Printf("Error updating plan tokens: %v\n", err)
http.Error(w, "Error updating plan tokens: "+err.Error(), http.StatusInternalServerError)
return
}
res := shared.DeleteContextResponse{
TokensRemoved: removeTokens,
TotalTokens: branch.ContextTokens - removeTokens,
Msg: commitMsg,
}
bytes, err := json.Marshal(res)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully deleted contexts")
w.Write(bytes)
}
================================================
FILE: app/server/handlers/plans_convo.go
================================================
package handlers
import (
"context"
"encoding/json"
"log"
"net/http"
"plandex-server/db"
shared "plandex-shared"
"github.com/gorilla/mux"
)
func ListConvoHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for ListConvoHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
if authorizePlan(w, planId, auth) == nil {
return
}
var err error
var convoMessages []*db.ConvoMessage
ctx, cancel := context.WithCancel(r.Context())
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "list convo",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
res, err := db.GetPlanConvo(auth.OrgId, planId)
if err != nil {
return err
}
convoMessages = res
return nil
})
if err != nil {
log.Println("Error getting plan convo: ", err)
http.Error(w, "Error getting plan convo: "+err.Error(), http.StatusInternalServerError)
return
}
apiConvoMessages := make([]*shared.ConvoMessage, len(convoMessages))
for i, convoMessage := range convoMessages {
apiConvoMessages[i] = convoMessage.ToApi()
}
bytes, err := json.Marshal(apiConvoMessages)
if err != nil {
log.Println("Error marshalling plan convo: ", err)
http.Error(w, "Error marshalling plan convo: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed request for ListConvoHandler")
w.Write(bytes)
}
func GetPlanStatusHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for GetPlanStatusHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
ctx, cancel := context.WithCancel(r.Context())
var convoMessages []*db.ConvoMessage
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "get plan status",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
res, err := db.GetPlanConvo(auth.OrgId, planId)
if err != nil {
return err
}
convoMessages = res
return nil
})
if err != nil {
log.Println("Error getting plan convo: ", err)
http.Error(w, "Error getting plan convo: "+err.Error(), http.StatusInternalServerError)
return
}
if len(convoMessages) == 0 {
log.Println("No messages found for plan")
return
}
convoMessageIds := make([]string, len(convoMessages))
for i, convoMessage := range convoMessages {
convoMessageIds[i] = convoMessage.Id
}
summmaries, err := db.GetPlanSummaries(planId, convoMessageIds)
if err != nil {
log.Println("Error getting plan summaries: ", err)
http.Error(w, "Error getting plan summaries: "+err.Error(), http.StatusInternalServerError)
return
}
if len(summmaries) == 0 {
log.Println("No summaries found for plan")
return
}
latestSummary := summmaries[len(summmaries)-1]
bytes := []byte(latestSummary.Summary)
w.Write(bytes)
log.Println("Successfully processed request for GetPlanStatusHandler")
}
================================================
FILE: app/server/handlers/plans_crud.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"plandex-server/db"
"plandex-server/hooks"
"runtime"
"runtime/debug"
"sort"
"strings"
"time"
shared "plandex-shared"
"github.com/gorilla/mux"
)
func CreatePlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreatePlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if !auth.HasPermission(shared.PermissionCreatePlan) {
log.Println("User does not have permission to create a plan")
http.Error(w, "User does not have permission to create a plan", http.StatusForbidden)
return
}
vars := mux.Vars(r)
projectId := vars["projectId"]
log.Println("projectId: ", projectId)
if !authorizeProject(w, projectId, auth) {
return
}
_, apiErr := hooks.ExecHook(hooks.WillCreatePlan, hooks.HookParams{Auth: auth})
if apiErr != nil {
writeApiError(w, *apiErr)
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.CreatePlanRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
name := requestBody.Name
if name == "" {
name = "draft"
}
if name == "draft" {
// delete any existing draft plans
err = db.DeleteDraftPlans(auth.OrgId, projectId, auth.User.Id)
if err != nil {
log.Printf("Error deleting draft plans: %v\n", err)
http.Error(w, "Error deleting draft plans: "+err.Error(), http.StatusInternalServerError)
return
}
} else {
i := 2
originalName := name
for {
var count int
err := db.Conn.Get(&count, "SELECT COUNT(*) FROM plans WHERE project_id = $1 AND owner_id = $2 AND name = $3", projectId, auth.User.Id, name)
if err != nil {
log.Printf("Error checking if plan exists: %v\n", err)
http.Error(w, "Error checking if plan exists: "+err.Error(), http.StatusInternalServerError)
return
}
if count == 0 {
break
}
name = originalName + "." + fmt.Sprint(i)
i++
}
}
plan, err := db.CreatePlan(r.Context(), auth.OrgId, projectId, auth.User.Id, name)
if err != nil {
log.Printf("Error creating plan: %v\n", err)
http.Error(w, "Error creating plan: "+err.Error(), http.StatusInternalServerError)
return
}
resp := shared.CreatePlanResponse{
Id: plan.Id,
Name: plan.Name,
}
bytes, err := json.Marshal(resp)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Printf("Successfully created plan: %v\n", plan)
}
func GetPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetPlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
bytes, err := json.Marshal(plan)
if err != nil {
log.Printf("Error marshalling plan: %v\n", err)
http.Error(w, "Error marshalling plan: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
}
func RenamePlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for RenamePlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
var requestBody shared.RenamePlanRequest
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
if plan.OwnerId != auth.User.Id {
log.Println("Only the plan owner can rename a plan")
http.Error(w, "Only the plan owner can rename a plan", http.StatusForbidden)
return
}
if requestBody.Name == "" {
log.Println("Name cannot be empty")
http.Error(w, "Name cannot be empty", http.StatusBadRequest)
return
}
err := db.RenamePlan(planId, requestBody.Name, nil)
if err != nil {
log.Printf("Error renaming plan: %v\n", err)
http.Error(w, "Error renaming plan: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully renamed plan")
}
func DeletePlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for DeletePlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
log.Println("planId: ", planId)
plan := authorizePlanDelete(w, planId, auth)
if plan == nil {
return
}
if plan.OwnerId != auth.User.Id {
log.Println("Only the plan owner can delete a plan")
http.Error(w, "Only the plan owner can delete a plan", http.StatusForbidden)
return
}
res, err := db.Conn.Exec("DELETE FROM plans WHERE id = $1", planId)
if err != nil {
log.Printf("Error deleting plan: %v\n", err)
http.Error(w, "Error deleting plan: "+err.Error(), http.StatusInternalServerError)
return
}
rowsAffected, err := res.RowsAffected()
if err != nil {
log.Printf("Error getting rows affected: %v\n", err)
http.Error(w, "Error getting rows affected: "+err.Error(), http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
log.Println("Plan not found")
http.Error(w, "Not found", http.StatusNotFound)
return
}
err = db.DeletePlanDir(auth.OrgId, planId)
if err != nil {
log.Printf("Error deleting plan dir: %v\n", err)
http.Error(w, "Error deleting plan dir: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully deleted plan", planId)
}
func DeleteAllPlansHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for DeleteAllPlansHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
projectId := vars["projectId"]
log.Println("projectId: ", projectId)
if !authorizeProject(w, projectId, auth) {
return
}
err := db.DeleteOwnerPlans(auth.OrgId, projectId, auth.User.Id)
if err != nil {
log.Printf("Error deleting plans: %v\n", err)
http.Error(w, "Error deleting plans: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully deleted all plans")
}
func ListPlansHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListPlans")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
projectIds := r.URL.Query()["projectId"]
log.Println("projectIds: ", projectIds)
var apiPlans []*shared.Plan
writePlans := func() {
jsonBytes, err := json.Marshal(apiPlans)
if err != nil {
log.Printf("Error marshalling plans: %v\n", err)
http.Error(w, "Error marshalling plans: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(jsonBytes)
}
if len(projectIds) == 0 {
writePlans()
return
}
authorizedProjectIds := []string{}
for _, projectId := range projectIds {
if authorizeProjectOptional(w, projectId, auth, false) {
authorizedProjectIds = append(authorizedProjectIds, projectId)
}
}
if len(authorizedProjectIds) == 0 {
writePlans()
return
}
plans, err := db.ListOwnedPlans(authorizedProjectIds, auth.User.Id, false)
if err != nil {
log.Printf("Error listing plans: %v\n", err)
http.Error(w, "Error listing plans: "+err.Error(), http.StatusInternalServerError)
return
}
for _, plan := range plans {
apiPlans = append(apiPlans, plan.ToApi())
}
writePlans()
}
func ListArchivedPlansHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListArchivedPlansHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
projectIds := r.URL.Query()["projectId"]
log.Println("projectIds: ", projectIds)
var apiPlans []*shared.Plan
writePlans := func() {
jsonBytes, err := json.Marshal(apiPlans)
if err != nil {
log.Printf("Error marshalling plans: %v\n", err)
http.Error(w, "Error marshalling plans: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed ListArchivedPlansHandler request")
w.Write(jsonBytes)
}
if len(projectIds) == 0 {
writePlans()
return
}
authorizedProjectIds := []string{}
for _, projectId := range projectIds {
if authorizeProjectOptional(w, projectId, auth, false) {
authorizedProjectIds = append(authorizedProjectIds, projectId)
}
}
plans, err := db.ListOwnedPlans(authorizedProjectIds, auth.User.Id, true)
if err != nil {
log.Printf("Error listing plans: %v\n", err)
http.Error(w, "Error listing plans: "+err.Error(), http.StatusInternalServerError)
return
}
for _, plan := range plans {
apiPlans = append(apiPlans, plan.ToApi())
}
writePlans()
}
func ListPlansRunningHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListPlansRunningHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
projectIds := r.URL.Query()["projectId"]
includeRecent := r.URL.Query().Get("recent") == "true"
log.Println("projectIds: ", projectIds)
if len(projectIds) == 0 {
log.Println("No project ids provided")
http.Error(w, "No project ids provided", http.StatusBadRequest)
return
}
for _, projectId := range projectIds {
if !authorizeProject(w, projectId, auth) {
return
}
}
plans, err := db.ListOwnedPlans(projectIds, auth.User.Id, false)
if err != nil {
log.Printf("Error listing plans: %v\n", err)
http.Error(w, "Error listing plans: "+err.Error(), http.StatusInternalServerError)
return
}
var planIds []string
for _, plan := range plans {
planIds = append(planIds, plan.Id)
}
errCh := make(chan error, 2)
var streams []*db.ModelStream
var branches []*db.Branch
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in ListPlansRunningHandler: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in ListPlansRunningHandler: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var err error
if includeRecent {
streams, err = db.GetActiveOrRecentModelStreams(planIds)
} else {
streams, err = db.GetActiveModelStreams(planIds)
}
if err != nil {
errCh <- fmt.Errorf("error getting recent model streams: %v", err)
return
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in ListPlansRunningHandler: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic in ListPlansRunningHandler: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var err error
branches, err = db.ListBranchesForPlans(auth.OrgId, planIds)
if err != nil {
errCh <- fmt.Errorf("error getting branches: %v", err)
return
}
errCh <- nil
}()
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
res := shared.ListPlansRunningResponse{
Branches: []*shared.Branch{},
StreamStartedAtByBranchId: map[string]time.Time{},
StreamFinishedAtByBranchId: map[string]time.Time{},
PlansById: map[string]*shared.Plan{},
StreamIdByBranchId: map[string]string{},
}
var apiPlansById = make(map[string]*shared.Plan)
for _, plan := range plans {
apiPlan := plan.ToApi()
apiPlansById[plan.Id] = apiPlan
}
var apiBranchesByComposite = make(map[string]*shared.Branch)
for _, branch := range branches {
apiBranch := branch.ToApi()
apiBranchesByComposite[branch.PlanId+"|"+branch.Name] = apiBranch
}
addedBranches := make(map[string]bool)
for _, stream := range streams {
branchComposite := stream.PlanId + "|" + stream.Branch
apiBranch, ok := apiBranchesByComposite[branchComposite]
if !ok {
log.Printf("Stream %s has no branch\n", stream.Id)
http.Error(w, "Stream has no branch", http.StatusInternalServerError)
return
}
apiPlan, ok := apiPlansById[stream.PlanId]
if !ok {
log.Printf("Stream %s has no plan\n", stream.Id)
http.Error(w, "Stream has no plan", http.StatusInternalServerError)
return
}
if !addedBranches[branchComposite] {
res.Branches = append(res.Branches, apiBranch)
addedBranches[branchComposite] = true
}
res.StreamStartedAtByBranchId[apiBranch.Id] = stream.CreatedAt
if stream.FinishedAt != nil {
res.StreamFinishedAtByBranchId[apiBranch.Id] = *stream.FinishedAt
}
res.StreamIdByBranchId[apiBranch.Id] = stream.Id
res.PlansById[stream.PlanId] = apiPlan
}
sort.Slice(res.Branches, func(i, j int) bool {
iComposite := res.Branches[i].PlanId + "|" + res.Branches[i].Name
jComposite := res.Branches[j].PlanId + "|" + res.Branches[j].Name
iFinishedAt, iOk := res.StreamFinishedAtByBranchId[iComposite]
jFinishedAt, jOk := res.StreamFinishedAtByBranchId[jComposite]
iCreatedAt := res.StreamStartedAtByBranchId[iComposite]
jCreatedAt := res.StreamStartedAtByBranchId[jComposite]
if iOk && jOk {
return iFinishedAt.Before(jFinishedAt) // Sort finished streams by finishedAt in ascending order.
}
if iOk {
return false // Place i after j if i is finished and j is not.
}
if jOk {
return true // Place i before j if i is not finished and j is.
}
return iCreatedAt.Before(jCreatedAt) // Sort by createdAt in ascending order if both are unfinished.
})
bytes, err := json.Marshal(res)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed ListPlansRunningHandler request")
w.Write(bytes)
}
func GetCurrentBranchByPlanIdHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CurrentBranchByPlanIdHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
projectId := vars["projectId"]
log.Println("projectId: ", projectId)
if !authorizeProject(w, projectId, auth) {
return
}
var req shared.GetCurrentBranchByPlanIdRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
plans, err := db.ListOwnedPlans([]string{projectId}, auth.User.Id, false)
if err != nil {
log.Printf("Error listing plans: %v\n", err)
http.Error(w, "Error listing plans: "+err.Error(), http.StatusInternalServerError)
return
}
if len(plans) == 0 {
log.Println("No plans found")
http.Error(w, "No plans found", http.StatusNotFound)
return
}
query := "SELECT * FROM branches WHERE "
var orConditions []string
var queryArgs []interface{}
currentArg := 1
for _, plan := range plans {
branchName, ok := req.CurrentBranchByPlanId[plan.Id]
if !ok {
continue
}
orConditions = append(orConditions, fmt.Sprintf("(plan_id = $%d AND name = $%d)", currentArg, currentArg+1))
queryArgs = append(queryArgs, plan.Id, branchName)
currentArg += 2
}
query += "(" + strings.Join(orConditions, " OR ") + ") AND archived_at IS NULL AND deleted_at IS NULL"
var branches []db.Branch
err = db.Conn.Select(&branches, query, queryArgs...)
if err != nil {
log.Printf("Error getting branches: %v\n", err)
http.Error(w, "Error getting branches: "+err.Error(), http.StatusInternalServerError)
return
}
res := map[string]*shared.Branch{}
for _, branch := range branches {
res[branch.PlanId] = branch.ToApi()
}
bytes, err := json.Marshal(res)
if err != nil {
log.Printf("Error marshalling branches: %v\n", err)
http.Error(w, "Error marshalling branches: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed GetCurrentBranchByPlanIdHandler request")
w.Write(bytes)
}
================================================
FILE: app/server/handlers/plans_exec.go
================================================
package handlers
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/host"
modelPlan "plandex-server/model/plan"
"plandex-server/notify"
"plandex-server/types"
"time"
shared "plandex-shared"
"github.com/gorilla/mux"
)
func TellPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for TellPlanHandler", "ip:", host.Ip)
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
plan := authorizePlanExecUpdate(w, planId, auth)
if plan == nil {
return
}
settings, err := db.GetPlanSettings(plan)
if err != nil {
log.Printf("Error getting plan settings: %v\n", err)
http.Error(w, "Error getting plan settings", http.StatusInternalServerError)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error reading request body: %v", err))
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer func() {
log.Println("Closing request body")
r.Body.Close()
}()
var requestBody shared.TellPlanRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error parsing request body: %v", err))
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
_, apiErr := hooks.ExecHook(hooks.WillTellPlan, hooks.HookParams{
Auth: auth,
Plan: plan,
})
if apiErr != nil {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error executing will tell plan hook: %v", apiErr))
writeApiError(w, *apiErr)
return
}
orgUserConfig, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("Error getting org user config: %v\n", err)
http.Error(w, "Error getting org user config", http.StatusInternalServerError)
return
}
res := initClients(
initClientsParams{
w: w,
auth: auth,
apiKeys: requestBody.ApiKeys,
openAIOrgId: requestBody.OpenAIOrgId,
authVars: requestBody.AuthVars,
plan: plan,
settings: settings,
orgUserConfig: orgUserConfig,
},
)
err = modelPlan.Tell(modelPlan.TellParams{
Clients: res.clients,
Plan: plan,
Branch: branch,
Auth: auth,
Req: &requestBody,
AuthVars: res.authVars,
})
if err != nil {
log.Printf("Error telling plan: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error telling plan: %v", err))
http.Error(w, "Error telling plan: "+err.Error(), http.StatusInternalServerError)
return
}
if requestBody.ConnectStream {
startResponseStream(r.Context(), w, auth, planId, branch, false)
}
log.Println("Successfully processed request for TellPlanHandler")
}
func BuildPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for BuildPlanHandler", "ip:", host.Ip)
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
plan := authorizePlanExecUpdate(w, planId, auth)
if plan == nil {
return
}
settings, err := db.GetPlanSettings(plan)
if err != nil {
log.Printf("Error getting plan settings: %v\n", err)
http.Error(w, "Error getting plan settings", http.StatusInternalServerError)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error reading request body: %v", err))
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer func() {
log.Println("Closing request body")
r.Body.Close()
}()
var requestBody shared.BuildPlanRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error parsing request body: %v", err))
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
orgUserConfig, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("Error getting org user config: %v\n", err)
http.Error(w, "Error getting org user config", http.StatusInternalServerError)
return
}
res := initClients(
initClientsParams{
w: w,
auth: auth,
apiKeys: requestBody.ApiKeys,
openAIOrgId: requestBody.OpenAIOrgId,
authVars: requestBody.AuthVars,
plan: plan,
settings: settings,
orgUserConfig: orgUserConfig,
},
)
numBuilds, err := modelPlan.Build(modelPlan.BuildParams{
Clients: res.clients,
AuthVars: res.authVars,
Plan: plan,
Branch: branch,
Auth: auth,
SessionId: requestBody.SessionId,
OrgUserConfig: orgUserConfig,
Settings: settings,
})
if err != nil {
log.Printf("Error building plan: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error building plan: %v", err))
http.Error(w, "Error building plan", http.StatusInternalServerError)
return
}
if numBuilds == 0 {
log.Println("No builds were executed")
go notify.NotifyErr(notify.SeverityInfo, fmt.Errorf("no builds were executed"))
http.Error(w, shared.NoBuildsErr, http.StatusNotFound)
return
}
if requestBody.ConnectStream {
startResponseStream(r.Context(), w, auth, planId, branch, false)
}
log.Println("Successfully processed request for BuildPlanHandler")
}
func ConnectPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ConnectPlanHandler", "ip:", host.Ip)
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
log.Println("branch: ", branch)
active := modelPlan.GetActivePlan(planId, branch)
isProxy := r.URL.Query().Get("proxy") == "true"
if active == nil {
if isProxy {
log.Println("No active plan on proxied request")
go notify.NotifyErr(notify.SeverityInfo, fmt.Errorf("no active plan on proxied request"))
http.Error(w, "No active plan", http.StatusNotFound)
return
}
log.Println("No active plan -- proxying request")
proxyActivePlanMethod(w, r, planId, branch, "connect")
return
}
auth := Authenticate(w, r, true)
if auth == nil {
log.Println("No auth")
return
}
plan := authorizePlan(w, planId, auth)
if plan == nil {
log.Println("No plan")
return
}
startResponseStream(r.Context(), w, auth, planId, branch, true)
log.Println("Successfully processed request for ConnectPlanHandler")
}
func StopPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for StopPlanHandler", "ip:", host.Ip)
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
log.Println("branch: ", branch)
active := modelPlan.GetActivePlan(planId, branch)
isProxy := r.URL.Query().Get("proxy") == "true"
if active == nil {
if isProxy {
log.Println("No active plan on proxied request")
http.Error(w, "No active plan", http.StatusNotFound)
return
}
proxyActivePlanMethod(w, r, planId, branch, "stop")
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
if authorizePlan(w, planId, auth) == nil {
return
}
log.Println("Sending stream aborted message to client")
active.Stream(shared.StreamMessage{
Type: shared.StreamMessageAborted,
})
// give some time for stream message to be processed before canceling
log.Println("Sleeping for 100ms before canceling")
time.Sleep(100 * time.Millisecond)
var err error
ctx, cancel := context.WithCancel(r.Context())
// this is here to ensure that the plan is stopped even if the db operation fails
defer func() {
err = modelPlan.Stop(planId, branch, auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("Error stopping plan: %v\n", err)
}
log.Println("Successfully processed request for StopPlanHandler")
}()
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "stop plan",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
log.Println("Stopping plan - storing partial reply")
err = modelPlan.StorePartialReply(repo, planId, branch, auth.User.Id, auth.OrgId)
return err
})
if err != nil {
log.Printf("Error storing partial reply: %v\n", err)
http.Error(w, "Error storing partial reply", http.StatusInternalServerError)
return
}
}
func RespondMissingFileHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for RespondMissingFileHandler", "ip:", host.Ip)
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
log.Println("branch: ", branch)
isProxy := r.URL.Query().Get("proxy") == "true"
active := modelPlan.GetActivePlan(planId, branch)
if active == nil {
if isProxy {
log.Println("No active plan on proxied request")
http.Error(w, "No active plan", http.StatusNotFound)
return
}
proxyActivePlanMethod(w, r, planId, branch, "respond_missing_file")
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.RespondMissingFileRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
log.Println("missing file choice:", requestBody.Choice)
if requestBody.Choice == shared.RespondMissingFileChoiceLoad {
log.Println("loading missing file")
res, dbContexts := loadContexts(loadContextsParams{
w: w,
r: r,
auth: auth,
loadReq: &shared.LoadContextRequest{
&shared.LoadContextParams{
ContextType: shared.ContextFileType,
Name: requestBody.FilePath,
FilePath: requestBody.FilePath,
Body: requestBody.Body,
},
},
plan: plan,
branchName: branch,
autoLoaded: true,
})
if res == nil {
return
}
dbContext := dbContexts[0]
log.Println("loaded missing file:", dbContext.FilePath)
modelPlan.UpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {
if activePlan == nil {
log.Println("Active plan is nil")
http.Error(w, "Active plan is nil", http.StatusInternalServerError)
return
}
activePlan.Contexts = append(activePlan.Contexts, dbContext)
activePlan.ContextsByPath[dbContext.FilePath] = dbContext
})
}
// This will resume model stream
log.Println("Resuming model stream")
active.MissingFileResponseCh <- requestBody.Choice
log.Println("Successfully processed request for RespondMissingFileHandler")
}
func AutoLoadContextHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for AutoLoadContextHandler", "ip:", host.Ip)
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
log.Println("branch: ", branch)
isProxy := r.URL.Query().Get("proxy") == "true"
active := modelPlan.GetActivePlan(planId, branch)
if active == nil {
if isProxy {
log.Println("No active plan on proxied request")
http.Error(w, "No active plan", http.StatusNotFound)
return
}
proxyActivePlanMethod(w, r, planId, branch, "auto_load_context")
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
var err error
defer func() {
if err == nil {
active.AutoLoadContextCh <- struct{}{}
} else {
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error in AutoLoadContextHandler: " + err.Error(),
}
}
}()
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.LoadContextRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
log.Println("AutoLoadContextHandler - loading contexts")
var res *shared.LoadContextResponse
var dbContexts []*db.Context
if len(requestBody) > 0 {
res, dbContexts = loadContexts(loadContextsParams{
w: w,
r: r,
auth: auth,
loadReq: &requestBody,
plan: plan,
branchName: branch,
autoLoaded: true,
})
}
if res == nil {
// the client will treat this as a no-op
markdownRes := shared.LoadContextResponse{
TokensAdded: 0,
TotalTokens: 0,
MaxTokensExceeded: false,
MaxTokens: 0,
Msg: "",
}
bytes, err := json.Marshal(markdownRes)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
w.Write(bytes)
return
}
log.Println("AutoLoadContextHandler - updating active plan")
modelPlan.UpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {
if activePlan == nil {
log.Println("Active plan is nil")
http.Error(w, "Active plan is nil", http.StatusInternalServerError)
return
}
activePlan.Contexts = append(activePlan.Contexts, dbContexts...)
for _, dbContext := range dbContexts {
activePlan.ContextsByPath[dbContext.FilePath] = dbContext
}
})
log.Println("AutoLoadContextHandler - updated active plan")
var apiContexts []*shared.Context
for _, dbContext := range dbContexts {
apiContexts = append(apiContexts, dbContext.ToApi())
}
msg := shared.SummaryForLoadContext(apiContexts, res.TokensAdded, res.TotalTokens)
msg += "\n\n" + shared.TableForLoadContext(apiContexts, true)
markdownRes := shared.LoadContextResponse{
TokensAdded: res.TokensAdded,
TotalTokens: res.TotalTokens,
MaxTokensExceeded: res.MaxTokensExceeded,
MaxTokens: res.MaxTokens,
Msg: msg,
}
bytes, err := json.Marshal(markdownRes)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully processed request for AutoLoadContextHandler")
}
func GetBuildStatusHandler(w http.ResponseWriter, r *http.Request) {
// logs are too chatty on this function, uncomment if you need to debug
// log.Println("Received request for GetBuildStatusHandler", "ip:", host.Ip)
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
isProxy := r.URL.Query().Get("proxy") == "true"
active := modelPlan.GetActivePlan(planId, branch)
if active == nil {
if isProxy {
log.Println("No active plan on proxied request")
http.Error(w, "No active plan", http.StatusNotFound)
return
}
proxyActivePlanMethod(w, r, planId, branch, "auto_load_context")
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
response := shared.GetBuildStatusResponse{
BuiltFiles: active.BuiltFiles,
IsBuildingByPath: active.IsBuildingByPath,
}
bytes, err := json.Marshal(response)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
w.Write(bytes)
// log.Println("Successfully processed request for GetBuildStatusHandler")
}
func authorizePlanExecUpdate(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
plan := authorizePlan(w, planId, auth)
if plan == nil {
return nil
}
if plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionUpdateAnyPlan) {
log.Println("User does not have permission to update plan")
http.Error(w, "User does not have permission to update plan", http.StatusForbidden)
return nil
}
return plan
}
================================================
FILE: app/server/handlers/plans_versions.go
================================================
package handlers
import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"plandex-server/db"
shared "plandex-shared"
"github.com/gorilla/mux"
)
func ListLogsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListLogsHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
if authorizePlan(w, planId, auth) == nil {
return
}
ctx, cancel := context.WithCancel(r.Context())
var body string
var shas []string
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "list logs",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
var err error
body, shas, err = repo.GetGitCommitHistory(branch)
if err != nil {
return err
}
return nil
})
if err != nil {
log.Println("Error getting logs: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
res := shared.LogResponse{
Body: body,
Shas: shas,
}
bytes, err := json.Marshal(res)
if err != nil {
log.Println("Error marshalling logs: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully processed request for ListLogsHandler")
}
func RewindPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for RewindPlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId)
if authorizePlan(w, planId, auth) == nil {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.RewindPlanRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
ctx, cancel := context.WithCancel(r.Context())
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "rewind plan",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
return repo.GitRewindToSha(branch, requestBody.Sha)
})
if err != nil {
log.Println("Error rewinding plan: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = db.SyncPlanTokens(auth.OrgId, planId, branch)
if err != nil {
log.Println("Error syncing plan tokens: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var sha string
var latest string
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "get latest commit",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
sha, latest, err = repo.GetLatestCommit(branch)
return err
})
if err != nil {
log.Println("Error getting latest commit: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
res := shared.RewindPlanResponse{
LatestSha: sha,
LatestCommit: latest,
}
bytes, err := json.Marshal(res)
if err != nil {
log.Println("Error marshalling response: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully processed request for RewindPlanHandler")
}
================================================
FILE: app/server/handlers/projects.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"plandex-server/db"
shared "plandex-shared"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
)
func CreateProjectHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateProjectHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.CreateProjectRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
if requestBody.Name == "" {
log.Println("Received empty name field")
http.Error(w, "name field is required", http.StatusBadRequest)
return
}
var projectId string
err = db.WithTx(r.Context(), "create project", func(tx *sqlx.Tx) error {
var err error
projectId, err = db.CreateProject(auth.OrgId, requestBody.Name, tx)
if err != nil {
log.Printf("Error creating project: %v\n", err)
return fmt.Errorf("error creating project: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error creating project: %v\n", err)
http.Error(w, "Error creating project: "+err.Error(), http.StatusInternalServerError)
return
}
resp := shared.CreateProjectResponse{
Id: projectId,
}
bytes, err := json.Marshal(resp)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("Successfully created project", projectId)
}
func ListProjectsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for ListProjectsHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
rows, err := db.Conn.Query("SELECT id, name FROM projects WHERE org_id = $1", auth.OrgId)
if err != nil {
log.Printf("Error listing projects: %v\n", err)
http.Error(w, "Error listing projects: "+err.Error(), http.StatusInternalServerError)
return
}
var projects []shared.Project
for rows.Next() {
var project shared.Project
err := rows.Scan(&project.Id, &project.Name)
if err != nil {
log.Printf("Error scanning project: %v\n", err)
http.Error(w, "Error scanning project: "+err.Error(), http.StatusInternalServerError)
return
}
projects = append(projects, project)
}
bytes, err := json.Marshal(projects)
if err != nil {
log.Printf("Error marshalling projects: %v\n", err)
http.Error(w, "Error marshalling projects: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
}
func ProjectSetPlanHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdateProjectSetPlanHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
projectId := vars["projectId"]
log.Println("projectId: ", projectId)
if !authorizeProject(w, projectId, auth) {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.SetProjectPlanRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
if requestBody.PlanId == "" {
log.Println("Received empty planId field")
http.Error(w, "planId field is required", http.StatusBadRequest)
return
}
// update statement here -- need auth / current user id
log.Println("Successfully set project plan", projectId)
}
func RenameProjectHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for RenameProjectHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
projectId := vars["projectId"]
log.Println("projectId: ", projectId)
if !authorizeProjectRename(w, projectId, auth) {
return
}
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
var requestBody shared.RenameProjectRequest
if err := json.Unmarshal(body, &requestBody); err != nil {
log.Printf("Error parsing request body: %v\n", err)
http.Error(w, "Error parsing request body", http.StatusBadRequest)
return
}
if requestBody.Name == "" {
log.Println("Received empty name field")
http.Error(w, "name field is required", http.StatusBadRequest)
return
}
res, err := db.Conn.Exec("UPDATE projects SET name = $1 WHERE id = $2", requestBody.Name, projectId)
if err != nil {
log.Printf("Error updating project: %v\n", err)
http.Error(w, "Error updating project: "+err.Error(), http.StatusInternalServerError)
return
}
rowsAffected, err := res.RowsAffected()
if err != nil {
log.Printf("Error getting rows affected: %v\n", err)
http.Error(w, "Error getting rows affected: "+err.Error(), http.StatusInternalServerError)
return
}
if rowsAffected == 0 {
log.Printf("Project not found: %v\n", projectId)
http.Error(w, "Project not found: "+projectId, http.StatusNotFound)
return
}
log.Println("Successfully renamed project", projectId)
}
================================================
FILE: app/server/handlers/proxy_helper.go
================================================
package handlers
import (
"fmt"
"io"
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/host"
"time"
shared "plandex-shared"
)
func proxyActivePlanMethod(w http.ResponseWriter, r *http.Request, planId, branch, method string) {
modelStream, err := db.GetActiveModelStream(planId, branch)
if err != nil {
log.Printf("Error getting active model stream: %v\n", err)
http.Error(w, "Error getting active model stream", http.StatusInternalServerError)
return
}
if modelStream == nil {
log.Printf("No active model stream for plan %s\n", planId)
http.Error(w, "No active model stream for plan", http.StatusNotFound)
return
}
if modelStream.InternalIp == host.Ip {
// No active plan for this plan or else we wouldn't be calling proxyActivePlanMethod -- set the model stream to finished because something went wrong
err := db.SetModelStreamFinished(modelStream.Id)
if err != nil {
log.Printf("Error setting model stream %s to finished: %v\n", modelStream.Id, err)
}
err = db.SetPlanStatus(planId, branch, shared.PlanStatusError, "No active stream for plan")
if err != nil {
log.Printf("Error setting plan %s status to error: %v\n", planId, err)
}
log.Printf("No active plan for plan %s\n", planId)
http.Error(w, "No active plan for plan", http.StatusNotFound)
return
} else {
log.Printf("Forwarding request to %s\n", modelStream.InternalIp)
proxyUrl := fmt.Sprintf("http://%s:%s/plans/%s/%s/%s", modelStream.InternalIp, os.Getenv("PORT"), planId, branch, method)
proxyUrl += "?proxy=true"
log.Printf("Proxy url: %s\n", proxyUrl)
proxyRequest(w, r, proxyUrl)
return
}
}
func proxyRequest(w http.ResponseWriter, originalRequest *http.Request, url string) {
client := &http.Client{
Timeout: time.Second * 10,
}
// Create a new request based on the original request
req, err := http.NewRequestWithContext(originalRequest.Context(), originalRequest.Method, url, originalRequest.Body)
if err != nil {
log.Printf("Error creating request for proxy: %v\n", err)
http.Error(w, "Error creating request for proxy", http.StatusInternalServerError)
return
}
// Copy the headers from the original request to the new request
for name, headers := range originalRequest.Header {
for _, h := range headers {
req.Header.Add(name, h)
}
}
// Copy the body from the original request to the new request if it's a POST or PUT
if originalRequest.Method == http.MethodPost || originalRequest.Method == http.MethodPut {
req.Body = originalRequest.Body
}
// Make the request
resp, err := client.Do(req)
if err != nil {
log.Printf("Error forwarding request: %v\n", err)
http.Error(w, "Error forwarding request", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// Copy the response headers and status code
for name, headers := range resp.Header {
for _, h := range headers {
w.Header().Add(name, h)
}
}
w.WriteHeader(resp.StatusCode)
log.Printf("Proxy forwarded successfully with status code: %d\n", resp.StatusCode)
// Copy the response body
if _, err := io.Copy(w, resp.Body); err != nil {
log.Printf("Error copying response body: %v\n", err)
http.Error(w, "Error copying response body", http.StatusInternalServerError)
}
}
================================================
FILE: app/server/handlers/sessions.go
================================================
package handlers
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/email"
"strings"
shared "plandex-shared"
)
func CreateEmailVerificationHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateEmailVerificationHandler")
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body: "+err.Error(), http.StatusInternalServerError)
return
}
var req shared.CreateEmailVerificationRequest
err = json.Unmarshal(body, &req)
if err != nil {
log.Printf("Error unmarshalling request: %v\n", err)
http.Error(w, "Error unmarshalling request: "+err.Error(), http.StatusInternalServerError)
return
}
req.Email = strings.ToLower(req.Email)
var hasAccount bool
if req.UserId == "" {
user, err := db.GetUserByEmail(req.Email)
if err != nil {
log.Printf("Error getting user: %v\n", err)
http.Error(w, "Error getting user: "+err.Error(), http.StatusInternalServerError)
return
}
hasAccount = user != nil
} else {
hasAccount = true
user, err := db.GetUser(req.UserId)
if err != nil {
log.Printf("Error getting user: %v\n", err)
http.Error(w, "Error getting user: "+err.Error(), http.StatusInternalServerError)
return
}
if user == nil {
log.Printf("User not found for id: %v\n", req.UserId)
http.Error(w, "User not found", http.StatusNotFound)
return
}
if user.Email != req.Email {
log.Printf("User email does not match for id: %v\n", req.UserId)
http.Error(w, "User email does not match", http.StatusBadRequest)
return
}
}
if req.RequireUser && !hasAccount {
log.Printf("User not found for email: %v\n", req.Email)
http.Error(w, "User not found", http.StatusNotFound)
return
} else if req.RequireNoUser && hasAccount {
log.Printf("User already exists for email: %v\n", req.Email)
http.Error(w, "User already exists", http.StatusConflict)
return
}
var res shared.CreateEmailVerificationResponse
if !(os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1") {
// create pin - 6 alphanumeric characters
pinBytes, err := shared.GetRandomAlphanumeric(6)
if err != nil {
log.Printf("Error generating random pin: %v\n", err)
http.Error(w, "Error generating random pin: "+err.Error(), http.StatusInternalServerError)
return
}
// get sha256 hash of pin
hashBytes := sha256.Sum256(pinBytes)
pinHash := hex.EncodeToString(hashBytes[:])
// create verification
err = db.CreateEmailVerification(req.Email, req.UserId, pinHash)
if err != nil {
log.Printf("Error creating email verification: %v\n", err)
http.Error(w, "Error creating email verification: "+err.Error(), http.StatusInternalServerError)
return
}
err = email.SendVerificationEmail(req.Email, string(pinBytes))
if err != nil {
log.Printf("Error sending verification email: %v\n", err)
http.Error(w, "Error sending verification email: "+err.Error(), http.StatusInternalServerError)
return
}
res = shared.CreateEmailVerificationResponse{
HasAccount: hasAccount,
}
} else {
res = shared.CreateEmailVerificationResponse{
HasAccount: hasAccount,
IsLocalMode: true,
}
}
bytes, err := json.Marshal(res)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully created email verification")
w.Write(bytes)
}
func CheckEmailPinHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for VerifyEmailPinHandler")
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body: "+err.Error(), http.StatusInternalServerError)
return
}
var req shared.VerifyEmailPinRequest
err = json.Unmarshal(body, &req)
if err != nil {
log.Printf("Error unmarshalling request: %v\n", err)
http.Error(w, "Error unmarshalling request: "+err.Error(), http.StatusInternalServerError)
return
}
req.Email = strings.ToLower(req.Email)
_, err = db.ValidateEmailVerification(req.Email, req.Pin)
if err != nil {
if err.Error() == db.InvalidOrExpiredPinError {
http.Error(w, "Invalid or expired pin", http.StatusNotFound)
return
}
log.Printf("Error validating email verification: %v\n", err)
http.Error(w, "Error validating email verification: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully verified email pin")
}
// sign in codes allow users to authenticate between different clients
// like UI to CLI or vice versa
func CreateSignInCodeHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for CreateSignInCodeHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
// create pin - 6 alphanumeric characters
pinBytes, err := shared.GetRandomAlphanumeric(6)
if err != nil {
log.Printf("Error generating random pin: %v\n", err)
http.Error(w, "Error generating random pin: "+err.Error(), http.StatusInternalServerError)
return
}
// get sha256 hash of pin
hashBytes := sha256.Sum256(pinBytes)
pinHash := hex.EncodeToString(hashBytes[:])
err = db.CreateSignInCode(auth.User.Id, auth.OrgId, pinHash)
if err != nil {
log.Printf("Error creating sign in code: %v\n", err)
http.Error(w, "Error creating sign in code: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully created sign in code")
// return the pin as a response
w.Write(pinBytes)
}
func SignInHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for SignInHandler")
// read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body: "+err.Error(), http.StatusInternalServerError)
return
}
var req shared.SignInRequest
err = json.Unmarshal(body, &req)
if err != nil {
log.Printf("Error unmarshalling request: %v\n", err)
http.Error(w, "Error unmarshalling request: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Validating and signing in")
resp, err := ValidateAndSignIn(w, r, req)
if err != nil {
log.Printf("Error signing in: %v\n", err)
http.Error(w, "Error signing in: "+err.Error(), http.StatusInternalServerError)
return
}
bytes, err := json.Marshal(resp)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully signed in")
w.Write(bytes)
}
func SignOutHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for SignOutHandler")
auth := Authenticate(w, r, false)
if auth == nil {
return
}
_, err := db.Conn.Exec("UPDATE auth_tokens SET deleted_at = NOW() WHERE token_hash = $1", auth.AuthToken.TokenHash)
if err != nil {
log.Printf("Error deleting auth token: %v\n", err)
http.Error(w, "Error deleting auth token: "+err.Error(), http.StatusInternalServerError)
return
}
err = ClearAuthCookieIfBrowser(w, r)
if err != nil {
log.Printf("Error clearing auth cookie: %v\n", err)
http.Error(w, "Error clearing auth cookie: "+err.Error(), http.StatusInternalServerError)
return
}
err = ClearAccountFromCookies(w, r, auth.User.Id)
if err != nil {
log.Printf("Error clearing account from cookies: %v\n", err)
http.Error(w, "Error clearing account from cookies: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully signed out")
}
func GetOrgUserConfigHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetOrgUserConfigHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
orgUserConfig, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("Error getting org user config: %v\n", err)
http.Error(w, "Error getting org user config: "+err.Error(), http.StatusInternalServerError)
return
}
bytes, err := json.Marshal(orgUserConfig)
if err != nil {
log.Printf("Error marshalling response: %v\n", err)
http.Error(w, "Error marshalling response: "+err.Error(), http.StatusInternalServerError)
return
}
w.Write(bytes)
}
func UpdateOrgUserConfigHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdateOrgUserConfigHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading request body: %v\n", err)
http.Error(w, "Error reading request body: "+err.Error(), http.StatusInternalServerError)
return
}
var req shared.OrgUserConfig
err = json.Unmarshal(body, &req)
if err != nil {
log.Printf("Error unmarshalling request: %v\n", err)
http.Error(w, "Error unmarshalling request: "+err.Error(), http.StatusInternalServerError)
return
}
err = db.UpdateOrgUserConfig(auth.User.Id, auth.OrgId, &req)
if err != nil {
log.Printf("Error updating org user config: %v\n", err)
http.Error(w, "Error updating org user config: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully updated org user config")
}
================================================
FILE: app/server/handlers/settings.go
================================================
package handlers
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"plandex-server/db"
"reflect"
shared "plandex-shared"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
)
func GetSettingsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetSettingsHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
var settings *shared.PlanSettings
ctx, cancel := context.WithCancel(r.Context())
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "get settings",
Scope: db.LockScopeRead,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
res, err := db.GetPlanSettings(plan)
if err != nil {
return err
}
settings = res
return nil
})
if err != nil {
log.Println("Error getting settings: ", err)
http.Error(w, "Error getting settings", http.StatusInternalServerError)
return
}
bytes, err := json.Marshal(settings)
if err != nil {
log.Println("Error marshalling settings: ", err)
http.Error(w, "Error marshalling settings", http.StatusInternalServerError)
return
}
log.Println("GetSettingsHandler processed successfully")
w.Write(bytes)
}
func UpdateSettingsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdateSettingsHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
vars := mux.Vars(r)
planId := vars["planId"]
branch := vars["branch"]
log.Println("planId: ", planId, "branch: ", branch)
plan := authorizePlan(w, planId, auth)
if plan == nil {
return
}
var req shared.UpdateSettingsRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Println("Error decoding request body: ", err)
http.Error(w, "Error decoding request body", http.StatusInternalServerError)
return
}
if req.ModelPackName == "" && req.ModelPack == nil {
log.Println("No model pack name or model pack provided")
http.Error(w, "No model pack name or model pack provided", http.StatusBadRequest)
return
}
if req.ModelPackName != "" {
if mp, builtIn := shared.BuiltInModelPacksByName[req.ModelPackName]; builtIn {
if os.Getenv("IS_CLOUD") != "" && mp.LocalProvider != "" {
msg := fmt.Sprintf("Built-in local model pack %s can't be used on Plandex Cloud", req.ModelPackName)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
}
}
if req.ModelPack != nil {
if req.ModelPack.LocalProvider != "" {
msg := fmt.Sprintf("Local model pack %s can't be used on Plandex Cloud", req.ModelPack.Name)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
ids := req.ModelPack.ToModelPackSchema().AllModelIds()
for _, id := range ids {
bm, builtIn := shared.BuiltInBaseModelsById[id]
if builtIn && os.Getenv("IS_CLOUD") != "" && bm.IsLocalOnly() {
msg := fmt.Sprintf("Built-in local model %s can't be used on Plandex Cloud", id)
log.Println(msg)
http.Error(w, msg, http.StatusUnprocessableEntity)
return
}
}
}
ctx, cancel := context.WithCancel(r.Context())
var commitMsg string
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Reason: "update settings",
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancel,
}, func(repo *db.GitRepo) error {
originalSettings, err := db.GetPlanSettings(plan)
if err != nil {
return fmt.Errorf("error getting settings: %v", err)
}
settings, err := originalSettings.DeepCopy()
if err != nil {
return fmt.Errorf("error copying settings: %v", err)
}
if req.ModelPackName != "" {
settings.SetModelPackByName(req.ModelPackName)
} else if req.ModelPack != nil {
settings.SetCustomModelPack(req.ModelPack)
} else {
return fmt.Errorf("no model pack name or model pack provided")
}
// log.Println("Original settings:")
// spew.Dump(originalSettings)
// log.Println("req.Settings:")
// spew.Dump(req.Settings)
err = db.StorePlanSettings(plan, *settings)
if err != nil {
return fmt.Errorf("error storing settings: %v", err)
}
commitMsg = getUpdateCommitMsg(settings, originalSettings, false)
err = repo.GitAddAndCommit(branch, commitMsg)
if err != nil {
return fmt.Errorf("error committing settings: %v", err)
}
return nil
})
if err != nil {
log.Println("Error updating settings: ", err)
http.Error(w, "Error updating settings", http.StatusInternalServerError)
return
}
res := shared.UpdateSettingsResponse{
Msg: commitMsg,
}
bytes, err := json.Marshal(res)
if err != nil {
log.Println("Error marshalling response: ", err)
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("UpdateSettingsHandler processed successfully")
}
func GetDefaultSettingsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for GetDefaultSettingsHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
settings, err := db.GetOrgDefaultSettings(auth.OrgId)
if err != nil {
log.Println("Error getting default settings: ", err)
http.Error(w, "Error getting default settings", http.StatusInternalServerError)
return
}
bytes, err := json.Marshal(settings)
if err != nil {
log.Println("Error marshalling default settings: ", err)
http.Error(w, "Error marshalling default settings", http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("GetDefaultSettingsHandler processed successfully")
}
func UpdateDefaultSettingsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request for UpdateDefaultSettingsHandler")
auth := Authenticate(w, r, true)
if auth == nil {
return
}
var req shared.UpdateSettingsRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
log.Println("Error decoding request body: ", err)
http.Error(w, "Error decoding request body", http.StatusInternalServerError)
return
}
if req.ModelPackName == "" && req.ModelPack == nil {
log.Println("No model pack name or model pack provided")
http.Error(w, "No model pack name or model pack provided", http.StatusBadRequest)
return
}
var originalSettings *shared.PlanSettings
var settings *shared.PlanSettings
err = db.WithTx(r.Context(), "update default settings", func(tx *sqlx.Tx) error {
var err error
originalSettings, err = db.GetOrgDefaultSettingsForUpdate(auth.OrgId, tx)
if err != nil {
log.Println("Error getting default settings: ", err)
return fmt.Errorf("error getting default settings: %v", err)
}
settings, err = originalSettings.DeepCopy()
if err != nil {
return fmt.Errorf("error copying settings: %v", err)
}
if req.ModelPackName != "" {
settings.SetModelPackByName(req.ModelPackName)
} else if req.ModelPack != nil {
settings.SetCustomModelPack(req.ModelPack)
} else {
return fmt.Errorf("no model pack name or model pack provided")
}
// log.Println("Original settings:")
// spew.Dump(originalSettings)
// log.Println("req.Settings:")
// spew.Dump(req.Settings)
err = db.StoreOrgDefaultSettings(auth.OrgId, settings, tx)
if err != nil {
log.Println("Error storing default settings: ", err)
return fmt.Errorf("error storing default settings: %v", err)
}
return nil
})
if err != nil {
log.Println("Error updating default settings: ", err)
http.Error(w, "Error updating default settings", http.StatusInternalServerError)
return
}
commitMsg := getUpdateCommitMsg(settings, originalSettings, true)
res := shared.UpdateSettingsResponse{
Msg: commitMsg,
}
bytes, err := json.Marshal(res)
if err != nil {
log.Println("Error marshalling response: ", err)
http.Error(w, "Error marshalling response", http.StatusInternalServerError)
return
}
w.Write(bytes)
log.Println("UpdateDefaultSettingsHandler processed successfully")
}
func getUpdateCommitMsg(settings *shared.PlanSettings, originalSettings *shared.PlanSettings, isOrgDefault bool) string {
// log.Println("Comparing settings")
// log.Println("Original:")
// spew.Dump(originalSettings)
// log.Println("New:")
// spew.Dump(settings)
// log.Println("Changes to settings:", strings.Join(changes, "\n"))
var s string
if isOrgDefault {
s = "⚙️ Updated org-wide default settings:"
} else {
s = "⚙️ Updated model settings:"
}
var changes []string
changes = compareSettings(originalSettings, settings, changes)
if len(changes) == 0 {
return "No changes to settings"
}
for _, change := range changes {
s += "\n" + " • " + change
}
return s
}
func compareSettings(original, updated *shared.PlanSettings, changes []string) []string {
if updated.ModelPackName != "" {
originalName := "custom"
if original.ModelPackName != "" {
originalName = original.ModelPackName
}
changes = append(changes, fmt.Sprintf("model-pack | %v → %v", originalName, updated.ModelPackName))
} else if updated.ModelPack != nil {
if original.ModelPack == nil {
changes = append(changes, fmt.Sprintf("model-pack | %v → %v", original.ModelPackName, "custom"))
}
changes = compareAny(original.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles, updated.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles, "", changes)
}
return changes
}
func compareAny(a, b interface{}, path string, changes []string) []string {
aVal, bVal := reflect.ValueOf(a), reflect.ValueOf(b)
if !aVal.IsValid() && !bVal.IsValid() {
return changes
}
// Pointer / nil handling – BEFORE dereferencing
if aVal.Kind() == reflect.Ptr || bVal.Kind() == reflect.Ptr {
// both nil → nothing
if (aVal.Kind() == reflect.Ptr && aVal.IsNil()) &&
(bVal.Kind() == reflect.Ptr && bVal.IsNil()) {
return changes
}
// one nil, one non-nil → record diff
if (aVal.Kind() == reflect.Ptr && aVal.IsNil()) ||
(bVal.Kind() == reflect.Ptr && bVal.IsNil()) {
aStr := "none"
bStr := "none"
if aVal.Kind() != reflect.Ptr || !aVal.IsNil() {
aStr = short(aVal)
}
if bVal.Kind() != reflect.Ptr || !bVal.IsNil() {
bStr = short(bVal)
}
changes = append(changes, fmt.Sprintf("%s | %s → %s", path, aStr, bStr))
return changes
}
// both non-nil pointers → safe to dereference
if aVal.Kind() == reflect.Ptr {
aVal = aVal.Elem()
}
if bVal.Kind() == reflect.Ptr {
bVal = bVal.Elem()
}
}
// Check again after dereferencing
if !aVal.IsValid() && !bVal.IsValid() {
return changes
}
// One side nil → record diff and stop
if !aVal.IsValid() || !bVal.IsValid() {
var aStr, bStr string
if aVal.IsValid() {
aStr = short(aVal)
} else {
aStr = "none"
}
if bVal.IsValid() {
bStr = short(bVal)
} else {
bStr = "none"
}
changes = append(changes, fmt.Sprintf("%s | %s → %s", path, aStr, bStr))
return changes
}
// Ensure we can safely call Interface()
if !aVal.CanInterface() || !bVal.CanInterface() {
return changes
}
if reflect.DeepEqual(aVal.Interface(), bVal.Interface()) {
return changes // No difference found
}
switch aVal.Kind() {
case reflect.Struct:
for i := 0; i < aVal.NumField(); i++ {
field := aVal.Type().Field(i)
if !field.IsExported() {
continue // Skip unexported fields
}
fieldName := field.Name
dasherizedName := shared.Dasherize(fieldName)
updatedPath := path
if !(dasherizedName == "model-set" ||
dasherizedName == "model-role-config" ||
dasherizedName == "base-model-config" ||
dasherizedName == "planner-model-config" ||
dasherizedName == "task-model-config") {
if updatedPath != "" {
updatedPath = updatedPath + "." + dasherizedName
} else {
if dasherizedName == "model-overrides" {
dasherizedName = "overrides"
}
updatedPath = dasherizedName
}
}
changes = compareAny(aVal.Field(i).Interface(), bVal.Field(i).Interface(), updatedPath, changes)
}
default:
var aStr, bStr string
if aVal.IsValid() {
aStr = short(aVal)
} else {
aStr = "no override"
}
if bVal.IsValid() {
bStr = short(bVal)
} else {
bStr = "no override"
}
change := fmt.Sprintf("%s | %v → %v", path, aStr, bStr)
changes = append(changes, change)
}
return changes
}
func short(v reflect.Value) string {
if !v.IsValid() {
return "none"
}
// If it’s a pointer, follow it once
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return "none"
}
v = v.Elem()
}
switch v.Kind() {
case reflect.String:
return v.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return fmt.Sprintf("%d", v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return fmt.Sprintf("%d", v.Uint())
case reflect.Float32, reflect.Float64:
return fmt.Sprintf("%g", v.Float())
case reflect.Struct:
// Special-case ModelRoleConfigSchema: show the ModelId only
if f := v.FieldByName("ModelId"); f.IsValid() && f.Kind() == reflect.String {
return f.String()
}
return fmt.Sprintf("%T", v.Interface()) // fall-back: just the type name
default:
return fmt.Sprintf("%v", v.Interface())
}
}
================================================
FILE: app/server/handlers/stream_helper.go
================================================
package handlers
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"plandex-server/db"
modelPlan "plandex-server/model/plan"
"plandex-server/types"
"time"
shared "plandex-shared"
)
const HeartbeatInterval = 5 * time.Second
func startResponseStream(reqCtx context.Context, w http.ResponseWriter, auth *types.ServerAuth, planId, branch string, isConnect bool) {
log.Println("Response stream manager: starting plan stream")
active := modelPlan.GetActivePlan(planId, branch)
if active == nil {
log.Printf("Response stream manager: active plan not found for plan ID %s on branch %s\n", planId, branch)
http.Error(w, "Active plan not found", http.StatusNotFound)
return
}
w.Header().Set("Transfer-Encoding", "chunked")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// send initial message to client
msg := shared.StreamMessage{
Type: shared.StreamMessageStart,
}
bytes, err := json.Marshal(msg)
if err != nil {
log.Printf("Response stream manager: error marshalling message: %v\n", err)
return
}
log.Println("Response stream manager: sending initial message")
err = sendStreamMessage(w, string(bytes))
if err != nil {
log.Println("Response stream manager: error sending initial message:", err)
return
}
if isConnect {
time.Sleep(100 * time.Millisecond)
err = initConnectActive(auth, planId, branch, w)
if err != nil {
log.Println("Response stream manager: error initializing connection to active plan:", err)
return
}
}
subscriptionId, ch := modelPlan.SubscribePlan(reqCtx, planId, branch)
defer func() {
log.Println("Response stream manager: client stream closed")
modelPlan.UnsubscribePlan(planId, branch, subscriptionId)
}()
if isConnect {
time.Sleep(50 * time.Millisecond)
} else {
time.Sleep(100 * time.Millisecond)
}
chHeartbeat := make(chan string)
// send heartbeats while the stream is active
go func() {
ticker := time.NewTicker(HeartbeatInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
chHeartbeat <- string(shared.StreamMessageHeartbeat)
case <-reqCtx.Done():
return
}
}
}()
for {
select {
case <-reqCtx.Done():
log.Println("Response stream manager: request context done")
return
case msg := <-chHeartbeat:
err = sendStreamMessage(w, msg)
if err != nil {
return
}
case msg := <-ch:
// log.Println("Response stream manager: sending message:", msg)
err = sendStreamMessage(w, msg)
if err != nil {
return
}
}
}
}
func sendStreamMessage(w http.ResponseWriter, msg string) error {
bytes := []byte(msg + shared.STREAM_MESSAGE_SEPARATOR)
// log.Printf("Response stream manager: writing message to client: %s\n", msg)
_, err := w.Write(bytes)
if err != nil {
log.Printf("Response stream manager: error writing to client: %v\n", err)
return err
} else if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
return nil
}
func initConnectActive(auth *types.ServerAuth, planId, branch string, w http.ResponseWriter) error {
log.Println("Response stream manager: initializing connection to active plan")
active := modelPlan.GetActivePlan(planId, branch)
if active == nil {
return fmt.Errorf("active plan not found for plan ID %s on branch %s", planId, branch)
}
msg := shared.StreamMessage{
Type: shared.StreamMessageConnectActive,
}
if active.Prompt != "" && !active.BuildOnly {
msg.InitPrompt = active.Prompt
}
if active.BuildOnly {
msg.InitBuildOnly = true
}
if len(active.StoredReplyIds) > 0 {
convo, err := db.GetPlanConvo(auth.OrgId, active.Id)
if err != nil {
return fmt.Errorf("error getting plan convo: %v", err)
}
convoMsgById := map[string]*db.ConvoMessage{}
for _, convoMsg := range convo {
convoMsgById[convoMsg.Id] = convoMsg
}
for _, replyId := range active.StoredReplyIds {
if convoMsg, ok := convoMsgById[replyId]; ok {
msg.InitReplies = append(msg.InitReplies, convoMsg.Message)
}
}
}
if active.CurrentReplyContent != "" {
msg.InitReplies = append(msg.InitReplies, active.CurrentReplyContent)
}
if active.MissingFilePath != "" {
msg.MissingFilePath = active.MissingFilePath
}
bytes, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("error marshalling message: %v", err)
}
log.Println("Response stream manager: sending connect message")
err = sendStreamMessage(w, string(bytes))
if err != nil {
return fmt.Errorf("error sending connect message: %v", err)
}
buildQueuesByPath := modelPlan.GetActivePlan(planId, branch).BuildQueuesByPath
// if we're connecting to an active stream and there are active builds, send initial build info
if len(buildQueuesByPath) > 0 {
for path, queue := range buildQueuesByPath {
buildInfo := shared.BuildInfo{Path: path}
for _, build := range queue {
if build.BuildFinished() {
buildInfo.NumTokens = 0
buildInfo.Finished = true
} else {
// no longer showing token counts in build info - leaving commented out for now for reference
// tokens := build.WithLineNumsBufferTokens
buildInfo.Finished = false
// buildInfo.NumTokens += tokens
}
}
msg := shared.StreamMessage{
Type: shared.StreamMessageBuildInfo,
BuildInfo: &buildInfo,
}
bytes, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("error marshalling message: %v", err)
}
err = sendStreamMessage(w, string(bytes))
if err != nil {
return fmt.Errorf("error sending message: %v", err)
}
}
}
return nil
}
================================================
FILE: app/server/handlers/users.go
================================================
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"plandex-server/db"
"strings"
shared "plandex-shared"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
)
func ListUsersHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for ListUsersHandler")
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Local mode is not supported for user management",
})
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't list users",
})
return
}
users, err := db.ListUsers(auth.OrgId)
if err != nil {
log.Println("Error listing users: ", err)
http.Error(w, "Error listing users: "+err.Error(), http.StatusInternalServerError)
return
}
apiUsers := make([]*shared.User, 0, len(users))
for _, user := range users {
apiUsers = append(apiUsers, user.ToApi())
}
orgUsers, err := db.ListOrgUsers(auth.OrgId)
if err != nil {
log.Println("Error listing org users: ", err)
http.Error(w, "Error listing org users: "+err.Error(), http.StatusInternalServerError)
return
}
orgUsersByUserId := make(map[string]*shared.OrgUser)
for _, orgUser := range orgUsers {
orgUsersByUserId[orgUser.UserId] = orgUser.ToApi()
}
resp := shared.ListUsersResponse{
Users: apiUsers,
OrgUsersByUserId: orgUsersByUserId,
}
bytes, err := json.Marshal(resp)
if err != nil {
log.Println("Error marshalling users: ", err)
http.Error(w, "Error marshalling users: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed request for ListUsersHandler")
w.Write(bytes)
}
func DeleteOrgUserHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received a request for DeleteOrgUserHandler")
if os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1" {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusForbidden,
Msg: "Local mode is not supported for user management",
})
return
}
auth := Authenticate(w, r, true)
if auth == nil {
return
}
org, err := db.GetOrg(auth.OrgId)
if err != nil {
log.Printf("Error getting org: %v\n", err)
http.Error(w, "Error getting org: "+err.Error(), http.StatusInternalServerError)
return
}
if org.IsTrial {
writeApiError(w, shared.ApiError{
Type: shared.ApiErrorTypeTrialActionNotAllowed,
Status: http.StatusForbidden,
Msg: "Trial user can't delete users",
})
return
}
vars := mux.Vars(r)
userId := vars["userId"]
log.Println("userId: ", userId)
orgUser, err := db.GetOrgUser(userId, auth.OrgId)
if err != nil {
log.Printf("Error getting org user: %v\n", err)
http.Error(w, "Error getting org user: "+err.Error(), http.StatusInternalServerError)
return
}
// ensure current user can remove target user
removePermission := shared.Permission(strings.Join([]string{string(shared.PermissionRemoveUser), orgUser.OrgRoleId}, "|"))
if !auth.HasPermission(removePermission) {
log.Printf("User does not have permission to remove user with role: %v\n", orgUser.OrgRoleId)
http.Error(w, "User does not have permission to remove user with role: "+orgUser.OrgRoleId, http.StatusForbidden)
return
}
// verify user is org member
isMember, err := db.ValidateOrgMembership(userId, auth.OrgId)
if err != nil {
log.Printf("Error validating org membership: %v\n", err)
http.Error(w, "Error validating org membership: "+err.Error(), http.StatusInternalServerError)
return
}
if !isMember {
log.Printf("User %s is not a member of org %s\n", userId, auth.OrgId)
http.Error(w, "User "+userId+" is not a member of org "+auth.OrgId, http.StatusForbidden)
return
}
orgOwnerRoleId, err := db.GetOrgOwnerRoleId()
if err != nil {
log.Printf("Error getting org owner role id: %v\n", err)
http.Error(w, "Error getting org owner role id: "+err.Error(), http.StatusInternalServerError)
return
}
// verify user isn't the only org owner
if orgUser.OrgRoleId == orgOwnerRoleId {
numOwners, err := db.NumUsersWithRole(auth.OrgId, orgOwnerRoleId)
if err != nil {
log.Printf("Error getting number of org owners: %v\n", err)
http.Error(w, "Error getting number of org owners: "+err.Error(), http.StatusInternalServerError)
return
}
if numOwners == 1 {
log.Println("Cannot delete the only org owner")
http.Error(w, "Cannot delete the only org owner", http.StatusForbidden)
return
}
}
err = db.WithTx(r.Context(), "delete org user", func(tx *sqlx.Tx) error {
err = db.DeleteOrgUser(auth.OrgId, userId, tx)
if err != nil {
log.Println("Error deleting org user: ", err)
return fmt.Errorf("error deleting org user: %v", err)
}
invite, err := db.GetActiveInviteByEmail(auth.OrgId, auth.User.Email)
if err != nil {
log.Println("Error getting invite for org user: ", err)
return fmt.Errorf("error getting invite for org user: %v", err)
}
if invite != nil {
err = db.DeleteInvite(invite.Id, tx)
if err != nil {
log.Println("Error deleting invite: ", err)
return fmt.Errorf("error deleting invite: %v", err)
}
}
return nil
})
if err != nil {
log.Println("Error deleting org user: ", err)
http.Error(w, "Error deleting org user: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successfully processed request for DeleteOrgUserHandler")
}
================================================
FILE: app/server/hooks/hooks.go
================================================
package hooks
import (
"context"
"plandex-server/db"
"plandex-server/types"
"time"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
"github.com/sashabaranov/go-openai"
)
const (
HealthCheck = "health_check"
CreateAccount = "create_account"
WillCreatePlan = "will_create_plan"
WillTellPlan = "will_tell_plan"
WillExecPlan = "will_exec_plan"
WillSendModelRequest = "will_send_model_request"
DidSendModelRequest = "did_send_model_request"
DidFinishBuilderRun = "did_finish_builder_run"
CreateOrg = "create_org"
Authenticate = "authenticate"
GetIntegratedModels = "get_integrated_models"
GetApiOrgs = "get_api_orgs"
CallFastApply = "call_fast_apply"
)
type WillSendModelRequestParams struct {
InputTokens int
OutputTokens int
ModelName shared.ModelName
IsUserPrompt bool
ModelTag shared.ModelTag
ModelId shared.ModelId
}
type DidSendModelRequestParams struct {
InputTokens int
OutputTokens int
CachedTokens int
ModelId shared.ModelId
ModelTag shared.ModelTag
ModelName shared.ModelName
ModelProvider shared.ModelProvider
ModelRole shared.ModelRole
ModelPackName string
Purpose string
GenerationId string
PlanId string
ModelStreamId string
ConvoMessageId string
BuildId string
StoppedEarly bool
UserCancelled bool
HadError bool
NoReportedUsage bool
SessionId string
RequestStartedAt time.Time
Streaming bool
StreamResult string
FirstTokenAt time.Time
Req *types.ExtendedChatCompletionRequest
Res *openai.ChatCompletionResponse
ModelConfig *shared.ModelRoleConfig
}
type DidFinishBuilderRunParams struct {
PlanId string
FilePath string
FileExt string
Lang string
GenerationIds []string
ValidateModelConfig *shared.ModelRoleConfig
FastApplyModelConfig *shared.ModelRoleConfig
WholeFileModelConfig *shared.ModelRoleConfig
AutoApplySuccess bool
AutoApplyValidationReasons []string
AutoApplyValidationSyntaxErrors []string
AutoApplyValidationPassed bool
AutoApplyValidationFailureResponse string
AutoApplyValidationStartedAt time.Time
AutoApplyValidationFinishedAt time.Time
DidReplacement bool
ReplacementSuccess bool
ReplacementSyntaxErrors []string
ReplacementFailureResponse string
ReplacementStartedAt time.Time
ReplacementFinishedAt time.Time
DidRewriteProposed bool
RewriteProposedSuccess bool
RewriteProposedSyntaxErrors []string
RewriteProposedFailureResponse string
RewriteProposedStartedAt time.Time
RewriteProposedFinishedAt time.Time
DidFastApply bool
FastApplySuccess bool
FastApplySyntaxErrors []string
FastApplyFailureResponse string
FastApplyStartedAt time.Time
FastApplyFinishedAt time.Time
BuiltWholeFile bool
BuildWholeFileStartedAt time.Time
BuildWholeFileFinishedAt time.Time
StartedAt time.Time
FinishedAt time.Time
}
type CreateOrgHookRequestParams struct {
Org *db.Org
}
type AuthenticateHookRequestParams struct {
Path string
Hash string
}
type FastApplyParams struct {
InitialCode string `json:"initialCode"`
EditSnippet string `json:"editSnippet"`
InitialCodeTokens int
EditSnippetTokens int
Language shared.Language
Ctx context.Context
}
type HookParams struct {
Auth *types.ServerAuth
Plan *db.Plan
Tx *sqlx.Tx
WillSendModelRequestParams *WillSendModelRequestParams
DidSendModelRequestParams *DidSendModelRequestParams
CreateOrgHookRequestParams *CreateOrgHookRequestParams
GetApiOrgIds []string
AuthenticateHookRequestParams *AuthenticateHookRequestParams
DidFinishBuilderRunParams *DidFinishBuilderRunParams
FastApplyParams *FastApplyParams
}
type GetIntegratedModelsResult struct {
IntegratedModelsMode bool
AuthVars map[string]string
}
type FastApplyResult struct {
MergedCode string
}
type HookResult struct {
GetIntegratedModelsResult *GetIntegratedModelsResult
ApiOrgsById map[string]*shared.Org
FastApplyResult *FastApplyResult
}
type Hook func(params HookParams) (HookResult, *shared.ApiError)
var hooks = make(map[string]Hook)
func RegisterHook(name string, hook Hook) {
hooks[name] = hook
}
func ExecHook(name string, params HookParams) (HookResult, *shared.ApiError) {
hook, ok := hooks[name]
if !ok {
return HookResult{}, nil
}
return hook(params)
}
func TestUpdate() {
}
================================================
FILE: app/server/host/ip.go
================================================
package host
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
)
var Ip string
func LoadIp() error {
if os.Getenv("GOENV") == "development" {
Ip = "localhost"
return nil
}
if os.Getenv("IS_CLOUD") != "" {
var err error
Ip, err = getAwsIp()
if err != nil {
return fmt.Errorf("error getting AWS ECS IP: %v", err)
}
log.Println("Got AWS ECS IP: ", Ip)
} else if os.Getenv("IP") != "" {
Ip = os.Getenv("IP")
return nil
}
return nil
}
type ecsMetadata struct {
Networks []struct {
IPv4Addresses []string `json:"IPv4Addresses"`
} `json:"Networks"`
}
var awsIp string
func getAwsIp() (string, error) {
ecsMetadataURL := os.Getenv("ECS_CONTAINER_METADATA_URI")
log.Printf("Getting ECS metadata from %s\n", ecsMetadataURL)
resp, err := http.Get(ecsMetadataURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var metadata ecsMetadata
err = json.Unmarshal(body, &metadata)
if err != nil {
return "", err
}
if len(metadata.Networks) == 0 || len(metadata.Networks[0].IPv4Addresses) == 0 {
return "", errors.New("no IP address found in ECS metadata")
}
awsIp = metadata.Networks[0].IPv4Addresses[0]
return awsIp, nil
}
================================================
FILE: app/server/litellm_proxy.py
================================================
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from typing import List, Optional
_orig_get_hdrs = AnthropicModelInfo.get_anthropic_headers
def _oauth_get_hdrs(
self,
api_key: str,
anthropic_version: Optional[str] = None,
computer_tool_used: bool = False,
prompt_caching_set: bool = False,
pdf_used: bool = False,
file_id_used: bool = False,
mcp_server_used: bool = False,
is_vertex_request: bool = False,
user_anthropic_beta_headers: Optional[List[str]] = None,
):
# call the original builder first
hdrs = _orig_get_hdrs(
self,
api_key=api_key,
anthropic_version=anthropic_version,
computer_tool_used=computer_tool_used,
prompt_caching_set=prompt_caching_set,
pdf_used=pdf_used,
file_id_used=file_id_used,
mcp_server_used=mcp_server_used,
is_vertex_request=is_vertex_request,
user_anthropic_beta_headers=user_anthropic_beta_headers,
)
# remove x-api-key when we detect an OAuth access-token
print(f"api_key: {api_key}")
if api_key and api_key.startswith(("sk-ant-oat", "sk-ant-oau")):
hdrs["anthropic-beta"] = "oauth-2025-04-20"
hdrs["anthropic-product"] = "claude-code"
hdrs.pop("x-api-key", None)
print(f"Anthropic headers: {hdrs}")
return hdrs
# monkey-patch AnthropicModelInfo.get_anthropic_headers to handle OAuth headers
AnthropicModelInfo.get_anthropic_headers = _oauth_get_hdrs
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse, JSONResponse
from litellm import completion, _turn_on_debug
import json
import re
# _turn_on_debug()
LOGGING_ENABLED = False
print("Litellm proxy: starting proxy server on port 4000...")
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok"}
@app.post("/v1/chat/completions")
async def passthrough(request: Request):
payload = await request.json()
if LOGGING_ENABLED:
# Log the request data for debugging
try:
# Get headers (excluding authorization to avoid logging credentials)
headers = dict(request.headers)
if "Authorization" in headers:
headers["Authorization"] = "Bearer [REDACTED]"
if "api-key" in headers:
headers["api-key"] = "[REDACTED]"
# Create a log-friendly representation
request_data = {
"method": request.method,
"url": str(request.url),
"headers": headers,
"body": payload
}
# Log the request data
print("Incoming request to /v1/chat/completions:")
print(json.dumps(request_data, indent=2))
except Exception as e:
print(f"Error logging request: {str(e)}")
model = payload.get("model", None)
print(f"Litellm proxy: calling model: {model}")
api_key = payload.pop("api_key", None)
if not api_key:
api_key = request.headers.get("Authorization")
if not api_key:
api_key = request.headers.get("api-key")
if api_key and api_key.startswith("Bearer "):
api_key = api_key.replace("Bearer ", "")
# api key optional for local/ollama models, so no need to error if not provided
# clean up for ollama if needed
payload = normalise_for_ollama(payload)
try:
if payload.get("stream"):
try:
response_stream = completion(api_key=api_key, **payload)
except Exception as e:
return error_response(e)
def stream_generator():
try:
for chunk in response_stream:
yield f"data: {json.dumps(chunk.to_dict())}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
# surface the problem to the client _inside_ the SSE stream
yield f"data: {json.dumps({'error': str(e)})}\n\n"
return
finally:
try:
response_stream.close()
except AttributeError:
pass
print(f"Litellm proxy: Initiating streaming response for model: {payload.get('model', 'unknown')}")
return StreamingResponse(stream_generator(), media_type="text/event-stream")
else:
print(f"Litellm proxy: Non-streaming response requested for model: {payload.get('model', 'unknown')}")
try:
result = completion(api_key=api_key, **payload)
except Exception as e:
return error_response(e)
return JSONResponse(content=result)
except Exception as e:
err_msg = str(e)
print(f"Litellm proxy: Error: {err_msg}")
status_match = re.search(r"status code: (\d+)", err_msg)
if status_match:
status_code = int(status_match.group(1))
else:
status_code = 500
return JSONResponse(
status_code=status_code,
content={"error": err_msg}
)
def error_response(exc: Exception) -> JSONResponse:
status = getattr(exc, "status_code", 500)
retry_after = (
getattr(getattr(exc, "response", None), "headers", {})
.get("Retry-After")
)
hdrs = {"Retry-After": retry_after} if retry_after else {}
return JSONResponse(status_code=status, content={"error": str(exc)}, headers=hdrs)
def normalise_for_ollama(p):
if not p.get("model", "").startswith("ollama"):
return p
# flatten content parts
for m in p.get("messages", []):
if isinstance(m["content"], list): # [{type:"text", text:"…"}]
m["content"] = "".join(part.get("text", "")
for part in m["content"]
if part.get("type") == "text")
# drop params Ollama ignores
for k in ("top_p", "temperature", "presence_penalty",
"tool_choice", "tools", "seed"):
p.pop(k, None)
return p
================================================
FILE: app/server/main.go
================================================
package main
import (
"fmt"
"log"
"os"
"plandex-server/model"
"plandex-server/routes"
"plandex-server/setup"
"github.com/gorilla/mux"
)
func main() {
// Configure the default logger to include milliseconds in timestamps
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
routes.RegisterHandlePlandex(func(router *mux.Router, path string, isStreaming bool, handler routes.PlandexHandler) *mux.Route {
return router.HandleFunc(path, handler)
})
err := model.EnsureLiteLLM(2)
if err != nil {
panic(fmt.Sprintf("Failed to start LiteLLM proxy: %v", err))
}
setup.RegisterShutdownHook(func() {
model.ShutdownLiteLLMServer()
})
r := mux.NewRouter()
routes.AddHealthRoutes(r)
routes.AddApiRoutes(r)
routes.AddProxyableApiRoutes(r)
setup.MustLoadIp()
setup.MustInitDb()
setup.StartServer(r, nil, nil)
os.Exit(0)
}
================================================
FILE: app/server/migrations/2023120500_init.down.sql
================================================
DROP TABLE IF EXISTS branches;
DROP TABLE IF EXISTS convo_summaries;
DROP TABLE IF EXISTS plan_builds;
DROP TABLE IF EXISTS users_projects;
DROP TABLE IF EXISTS plans;
DROP TABLE IF EXISTS projects;
DROP TABLE IF EXISTS orgs_users;
DROP TABLE IF EXISTS email_verifications;
DROP TABLE IF EXISTS auth_tokens;
DROP TABLE IF EXISTS invites;
DROP TABLE IF EXISTS orgs;
DROP TABLE IF EXISTS users;
================================================
FILE: app/server/migrations/2023120500_init.up.sql
================================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
domain VARCHAR(255) NOT NULL,
is_trial BOOLEAN NOT NULL,
num_non_draft_plans INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_users_modtime BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
ALTER TABLE users ADD UNIQUE (email);
CREATE INDEX users_domain_idx ON users(domain);
CREATE TABLE IF NOT EXISTS orgs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
domain VARCHAR(255),
auto_add_domain_users BOOLEAN NOT NULL DEFAULT FALSE,
owner_id UUID NOT NULL REFERENCES users(id),
is_trial BOOLEAN NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_orgs_modtime BEFORE UPDATE ON orgs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
ALTER TABLE orgs ADD UNIQUE (domain);
CREATE TABLE IF NOT EXISTS orgs_users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_orgs_users_modtime BEFORE UPDATE ON orgs_users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE INDEX orgs_users_user_idx ON orgs_users(user_id);
CREATE INDEX orgs_users_org_idx ON orgs_users(org_id);
CREATE TABLE IF NOT EXISTS invites (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
inviter_id UUID NOT NULL REFERENCES users(id),
invitee_id UUID REFERENCES users(id),
accepted_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_invites_modtime BEFORE UPDATE ON invites FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE INDEX invites_pending_idx ON invites(org_id, (accepted_at IS NULL));
CREATE INDEX invites_email_idx ON invites(email, (accepted_at IS NULL));
CREATE INDEX invites_org_user_idx ON invites(org_id, invitee_id);
CREATE TABLE IF NOT EXISTS auth_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(64) NOT NULL,
is_trial BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP
);
CREATE UNIQUE INDEX auth_tokens_idx ON auth_tokens(token_hash);
CREATE TABLE IF NOT EXISTS email_verifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) NOT NULL,
pin_hash VARCHAR(64) NOT NULL,
user_id UUID REFERENCES users(id),
auth_token_id UUID REFERENCES auth_tokens(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_email_verifications_modtime BEFORE UPDATE ON email_verifications FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE UNIQUE INDEX email_verifications_idx ON email_verifications(pin_hash, email, created_at DESC);
CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_projects_modtime BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TABLE IF NOT EXISTS plans (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
shared_with_org_at TIMESTAMP,
total_replies INTEGER NOT NULL DEFAULT 0,
active_branches INTEGER NOT NULL DEFAULT 0,
archived_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_plans_modtime BEFORE UPDATE ON plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE INDEX plans_name_idx ON plans(project_id, owner_id, name);
CREATE INDEX plans_archived_idx ON plans(project_id, owner_id, archived_at);
CREATE TABLE IF NOT EXISTS branches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
parent_branch_id UUID REFERENCES branches(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
status VARCHAR(32) NOT NULL,
error TEXT,
context_tokens INTEGER NOT NULL DEFAULT 0,
convo_tokens INTEGER NOT NULL DEFAULT 0,
shared_with_org_at TIMESTAMP,
archived_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP
);
CREATE TRIGGER update_branches_modtime BEFORE UPDATE ON branches FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE UNIQUE INDEX branches_name_idx ON branches(plan_id, name, archived_at, deleted_at);
CREATE TABLE IF NOT EXISTS users_projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
last_active_plan_id UUID REFERENCES plans(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_users_projects_modtime BEFORE UPDATE ON users_projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE INDEX users_projects_idx ON users_projects(user_id, org_id, project_id);
CREATE TABLE IF NOT EXISTS convo_summaries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
latest_convo_message_id UUID NOT NULL,
latest_convo_message_created_at TIMESTAMP NOT NULL,
summary TEXT NOT NULL,
tokens INTEGER NOT NULL,
num_messages INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS plan_builds (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
convo_message_id UUID NOT NULL,
file_path VARCHAR(255) NOT NULL,
error TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_plan_builds_modtime BEFORE UPDATE ON plan_builds FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
================================================
FILE: app/server/migrations/2024011700_rbac.down.sql
================================================
ALTER TABLE orgs_users DROP COLUMN org_role_id;
ALTER TABLE invites DROP COLUMN org_role_id;
DROP TABLE IF EXISTS org_roles_permissions;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS org_roles;
================================================
FILE: app/server/migrations/2024011700_rbac.up.sql
================================================
CREATE TABLE IF NOT EXISTS org_roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
label VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_org_roles_modtime BEFORE UPDATE ON org_roles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE UNIQUE INDEX org_roles_org_idx ON org_roles(org_id, name);
ALTER TABLE orgs_users ADD COLUMN org_role_id UUID NOT NULL REFERENCES org_roles(id) ON DELETE RESTRICT;
CREATE INDEX orgs_users_org_role_idx ON orgs_users(org_id, org_role_id);
ALTER TABLE invites ADD COLUMN org_role_id UUID NOT NULL REFERENCES org_roles(id) ON DELETE RESTRICT;
CREATE INDEX invites_org_role_idx ON invites(org_id, org_role_id);
CREATE TABLE IF NOT EXISTS permissions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
resource_id UUID,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS org_roles_permissions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_role_id UUID NOT NULL REFERENCES org_roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
INSERT INTO org_roles (name, label, description) VALUES
('owner', 'Owner', 'Can read and update any plan, invite other owners/admins/members, manage email domain auth, manage billing, read audit logs, delete the org'),
('billing_admin', 'Billing Admin', 'Can manage billing'),
('admin', 'Admin', 'Can read and update any plan, invite other admins/members'),
('member', 'Member', 'Can read and update their own plans or plans shared with them');
DO $$
DECLARE
owner_org_role_id UUID;
billing_admin_org_role_id UUID;
admin_org_role_id UUID;
member_org_role_id UUID;
BEGIN
SELECT id INTO owner_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'owner';
SELECT id INTO billing_admin_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'billing_admin';
SELECT id INTO admin_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'admin';
SELECT id INTO member_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'member';
INSERT INTO permissions (name, description, resource_id) VALUES
('delete_org', 'Delete an org', NULL),
('manage_email_domain_auth', 'Configure whether orgs_users from the org''s email domain are auto-admitted to org', NULL),
('manage_billing', 'Manage an org''s billing', NULL),
('invite_user', 'Invite owners to an org', owner_org_role_id),
('invite_user', 'Invite admins to an org', admin_org_role_id),
('invite_user', 'Invite billing admins to an org', billing_admin_org_role_id),
('invite_user', 'Invite members to an org', member_org_role_id),
('remove_user', 'Remove owners from an org', owner_org_role_id),
('remove_user', 'Remove admins from an org', admin_org_role_id),
('remove_user', 'Remove billing admins from an org', billing_admin_org_role_id),
('remove_user', 'Remove members from an org', member_org_role_id),
('set_user_role', 'Update an owner''s role in an org', owner_org_role_id),
('set_user_role', 'Update an admin''s role in an org', admin_org_role_id),
('set_user_role', 'Update a billing admin''s role in an org', billing_admin_org_role_id),
('set_user_role', 'Update a member''s role in an org', member_org_role_id),
('list_org_roles', 'List org roles', NULL),
('create_project', 'Create a project', NULL),
('rename_any_project', 'Rename a project', NULL),
('delete_any_project', 'Delete a project', NULL),
('create_plan', 'Create a plan', NULL),
('manage_any_plan_shares', 'Unshare a plan any user shared', NULL),
('rename_any_plan', 'Rename a plan', NULL),
('delete_any_plan', 'Delete a plan', NULL),
('update_any_plan', 'Update a plan', NULL),
('archive_any_plan', 'Archive a plan', NULL);
END $$;
-- Insert all permissions for the 'org owner' role
INSERT INTO org_roles_permissions (org_role_id, permission_id)
SELECT
(SELECT id FROM org_roles WHERE name = 'owner') AS org_role_id,
p.id AS permission_id
FROM
permissions p;
-- Insert all permissions except specific ones and those exclusive to 'owner' or 'billing admin' for the 'org admin' role
INSERT INTO org_roles_permissions (org_role_id, permission_id)
SELECT
(SELECT id FROM org_roles WHERE name = 'admin') AS org_role_id,
p.id AS permission_id
FROM
permissions p
WHERE
p.name NOT IN ('delete_org', 'manage_email_domain_auth', 'manage_billing')
AND NOT EXISTS (
SELECT 1 FROM permissions p2
WHERE p2.resource_id IN (SELECT id FROM org_roles WHERE name IN ('owner', 'billing_admin'))
AND p2.id = p.id
);
INSERT INTO org_roles_permissions (org_role_id, permission_id)
SELECT
(SELECT id FROM org_roles WHERE name = 'billing_admin') AS org_role_id,
p.id AS permission_id
FROM
permissions p
WHERE
p.name IN (
'manage_billing'
);
INSERT INTO org_roles_permissions (org_role_id, permission_id)
SELECT
(SELECT id FROM org_roles WHERE name = 'member') AS org_role_id,
p.id AS permission_id
FROM
permissions p
WHERE
p.name IN (
'create_project',
'create_plan'
);
================================================
FILE: app/server/migrations/2024012400_streams.down.sql
================================================
DROP TABLE IF EXISTS model_streams;
-- DROP TABLE IF EXISTS model_stream_subscriptions;
================================================
FILE: app/server/migrations/2024012400_streams.up.sql
================================================
CREATE TABLE IF NOT EXISTS model_streams (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
branch VARCHAR(255) NOT NULL,
internal_ip VARCHAR(45) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
finished_at TIMESTAMP
);
CREATE UNIQUE INDEX model_streams_plan_idx ON model_streams(plan_id, branch, finished_at);
-- CREATE TABLE IF NOT EXISTS model_stream_subscriptions (
-- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
-- plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
-- model_stream_id UUID NOT NULL REFERENCES model_streams(id) ON DELETE CASCADE,
-- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- user_ip VARCHAR(45) NOT NULL,
-- created_at TIMESTAMP NOT NULL DEFAULT NOW()
-- finished_at TIMESTAMP
-- );
================================================
FILE: app/server/migrations/2024012500_locks.down.sql
================================================
DROP TABLE IF EXISTS repo_locks;
================================================
FILE: app/server/migrations/2024012500_locks.up.sql
================================================
CREATE UNLOGGED TABLE IF NOT EXISTS repo_locks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
plan_build_id UUID REFERENCES plan_builds(id) ON DELETE CASCADE,
scope VARCHAR(1) NOT NULL,
branch VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX repo_locks_plan_idx ON repo_locks(plan_id);
================================================
FILE: app/server/migrations/2024013000_plan_build_convo_ids.down.sql
================================================
ALTER TABLE plan_builds
RENAME COLUMN convo_message_ids TO convo_message_id;
ALTER TABLE plan_builds
ALTER COLUMN convo_message_id TYPE UUID USING (convo_message_id[1]);
================================================
FILE: app/server/migrations/2024013000_plan_build_convo_ids.up.sql
================================================
ALTER TABLE plan_builds
RENAME COLUMN convo_message_id TO convo_message_ids;
ALTER TABLE plan_builds
ALTER COLUMN convo_message_ids TYPE UUID[] USING ARRAY[convo_message_ids];
================================================
FILE: app/server/migrations/2024020800_heartbeats.down.sql
================================================
ALTER TABLE repo_locks DROP COLUMN last_heartbeat_at;
ALTER TABLE model_streams DROP COLUMN last_heartbeat_at;
================================================
FILE: app/server/migrations/2024020800_heartbeats.up.sql
================================================
ALTER TABLE repo_locks ADD COLUMN last_heartbeat_at TIMESTAMP NOT NULL DEFAULT NOW();
ALTER TABLE model_streams ADD COLUMN last_heartbeat_at TIMESTAMP NOT NULL DEFAULT NOW();
================================================
FILE: app/server/migrations/2024022000_revert_plan_build_convo_ids.down.sql
================================================
ALTER TABLE plan_builds
RENAME COLUMN convo_message_id TO convo_message_ids;
ALTER TABLE plan_builds
ALTER COLUMN convo_message_ids TYPE UUID[] USING ARRAY[convo_message_ids];
================================================
FILE: app/server/migrations/2024022000_revert_plan_build_convo_ids.up.sql
================================================
ALTER TABLE plan_builds
RENAME COLUMN convo_message_ids TO convo_message_id;
ALTER TABLE plan_builds
ALTER COLUMN convo_message_id TYPE UUID USING (convo_message_id[1]);
================================================
FILE: app/server/migrations/2024032700_remove_billing_admin.down.sql
================================================
================================================
FILE: app/server/migrations/2024032700_remove_billing_admin.up.sql
================================================
DELETE FROM org_roles WHERE name = 'billing_admin';
================================================
FILE: app/server/migrations/2024032701_drop_users_projects.down.sql
================================================
CREATE TABLE IF NOT EXISTS users_projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
last_active_plan_id UUID REFERENCES plans(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_users_projects_modtime BEFORE UPDATE ON users_projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE INDEX users_projects_idx ON users_projects(user_id, org_id, project_id);
================================================
FILE: app/server/migrations/2024032701_drop_users_projects.up.sql
================================================
DROP TABLE IF EXISTS users_projects;
================================================
FILE: app/server/migrations/2024040400_add_orgs_users_unique.down.sql
================================================
ALTER TABLE orgs_users DROP CONSTRAINT org_user_unique;
================================================
FILE: app/server/migrations/2024040400_add_orgs_users_unique.up.sql
================================================
-- clean up any duplicates added mistakenly earlier
WITH ranked_duplicates AS (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY org_id, user_id ORDER BY created_at) AS rn
FROM orgs_users
)
DELETE FROM orgs_users
WHERE id IN (
SELECT id FROM ranked_duplicates WHERE rn > 1
);
ALTER TABLE orgs_users ADD CONSTRAINT org_user_unique UNIQUE (org_id, user_id);
================================================
FILE: app/server/migrations/2024041500_model_sets_models.down.sql
================================================
DROP TABLE IF EXISTS model_sets;
DROP TABLE IF EXISTS custom_models;
================================================
FILE: app/server/migrations/2024041500_model_sets_models.up.sql
================================================
CREATE TABLE IF NOT EXISTS model_sets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
planner JSON,
plan_summary JSON,
builder JSON,
namer JSON,
commit_msg JSON,
exec_status JSON,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX model_sets_org_idx ON model_sets(org_id);
CREATE TABLE IF NOT EXISTS custom_models (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
provider VARCHAR(255) NOT NULL,
custom_provider VARCHAR(255),
base_url VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
description TEXT,
max_tokens INTEGER NOT NULL,
api_key_env_var VARCHAR(255),
is_openai_compatible BOOLEAN NOT NULL,
has_json_mode BOOLEAN NOT NULL,
has_streaming BOOLEAN NOT NULL,
has_function_calling BOOLEAN NOT NULL,
has_streaming_function_calls BOOLEAN NOT NULL,
default_max_convo_tokens INTEGER NOT NULL,
default_reserved_output_tokens INTEGER NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_custom_models_modtime BEFORE UPDATE ON custom_models FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE INDEX custom_models_org_idx ON custom_models(org_id);
================================================
FILE: app/server/migrations/2024042600_default_plan_settings.down.sql
================================================
DROP TABLE IF EXISTS default_plan_settings;
================================================
FILE: app/server/migrations/2024042600_default_plan_settings.up.sql
================================================
CREATE TABLE IF NOT EXISTS default_plan_settings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
plan_settings JSON,
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_default_plan_settings_modtime BEFORE UPDATE ON default_plan_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE UNIQUE INDEX default_plan_settings_org_idx ON default_plan_settings(org_id);
================================================
FILE: app/server/migrations/2024091800_sign_in_codes.down.sql
================================================
DROP TABLE IF EXISTS sign_in_codes;
================================================
FILE: app/server/migrations/2024091800_sign_in_codes.up.sql
================================================
CREATE TABLE IF NOT EXISTS sign_in_codes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
pin_hash VARCHAR(64) NOT NULL,
user_id UUID REFERENCES users(id),
org_id UUID REFERENCES orgs(id),
auth_token_id UUID REFERENCES auth_tokens(id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TRIGGER update_sign_in_codes_modtime BEFORE UPDATE ON sign_in_codes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE UNIQUE INDEX sign_in_codes_idx ON sign_in_codes(pin_hash, created_at DESC);
================================================
FILE: app/server/migrations/2024092100_remove_trial_fields.down.sql
================================================
ALTER TABLE auth_tokens ADD COLUMN is_trial BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN is_trial BOOLEAN NOT NULL DEFAULT FALSE;
================================================
FILE: app/server/migrations/2024092100_remove_trial_fields.up.sql
================================================
ALTER TABLE auth_tokens DROP COLUMN is_trial;
ALTER TABLE users DROP COLUMN is_trial;
================================================
FILE: app/server/migrations/2024100900_update_locks.down.sql
================================================
-- Revert user_id to NOT NULL if no NULL values exist
DO $$
BEGIN
-- Check for NULL values in user_id
IF EXISTS (SELECT 1 FROM repo_locks WHERE user_id IS NULL) THEN
RAISE EXCEPTION 'Cannot revert to NOT NULL, as there are rows with NULL values in user_id.';
ELSE
-- Proceed with setting the columns to NOT NULL
ALTER TABLE repo_locks
ALTER COLUMN user_id SET NOT NULL;
END IF;
END $$;
================================================
FILE: app/server/migrations/2024100900_update_locks.up.sql
================================================
ALTER TABLE repo_locks
ALTER COLUMN user_id DROP NOT NULL;
================================================
FILE: app/server/migrations/2024121400_plan_config.down.sql
================================================
ALTER TABLE plans DROP COLUMN IF EXISTS plan_config;
ALTER TABLE users DROP COLUMN IF EXISTS default_plan_config;
================================================
FILE: app/server/migrations/2024121400_plan_config.up.sql
================================================
ALTER TABLE plans ADD COLUMN IF NOT EXISTS plan_config JSON;
ALTER TABLE users ADD COLUMN IF NOT EXISTS default_plan_config JSON;
================================================
FILE: app/server/migrations/2025012600_update_custom_models.down.sql
================================================
ALTER TABLE custom_models DROP COLUMN preferred_output_format;
ALTER TABLE custom_models ADD COLUMN has_streaming BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE custom_models ADD COLUMN has_function_calling BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE custom_models ADD COLUMN has_streaming_function_calls BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE custom_models ADD COLUMN has_json_mode BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE custom_models ADD COLUMN is_openai_compatible BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE custom_models DROP COLUMN has_image_support;
================================================
FILE: app/server/migrations/2025012600_update_custom_models.up.sql
================================================
ALTER TABLE custom_models ADD COLUMN preferred_output_format VARCHAR(32) NOT NULL DEFAULT 'xml';
ALTER TABLE custom_models DROP COLUMN has_streaming_function_calls;
ALTER TABLE custom_models DROP COLUMN has_json_mode;
ALTER TABLE custom_models DROP COLUMN has_streaming;
ALTER TABLE custom_models DROP COLUMN has_function_calling;
ALTER TABLE custom_models DROP COLUMN is_openai_compatible;
ALTER TABLE custom_models ADD COLUMN has_image_support BOOLEAN NOT NULL DEFAULT FALSE;
================================================
FILE: app/server/migrations/2025021101_locks_unique.down.sql
================================================
DROP TABLE IF EXISTS lockable_plan_ids;
DROP INDEX IF EXISTS repo_locks_single_write_lock;
================================================
FILE: app/server/migrations/2025021101_locks_unique.up.sql
================================================
CREATE UNIQUE INDEX repo_locks_single_write_lock
ON repo_locks(plan_id)
WHERE (scope = 'w');
CREATE TABLE IF NOT EXISTS lockable_plan_ids (
plan_id UUID NOT NULL PRIMARY KEY REFERENCES plans(id) ON DELETE CASCADE
);
================================================
FILE: app/server/migrations/2025022700_remove_models_col.down.sql
================================================
ALTER TABLE custom_models ADD COLUMN default_reserved_output_tokens INTEGER NOT NULL;
================================================
FILE: app/server/migrations/2025022700_remove_models_col.up.sql
================================================
ALTER TABLE custom_models DROP COLUMN default_reserved_output_tokens;
================================================
FILE: app/server/migrations/2025031300_add_model_roles.down.sql
================================================
ALTER TABLE model_sets DROP COLUMN context_loader;
ALTER TABLE model_sets DROP COLUMN whole_file_builder;
ALTER TABLE model_sets DROP COLUMN coder;
================================================
FILE: app/server/migrations/2025031300_add_model_roles.up.sql
================================================
ALTER TABLE model_sets ADD COLUMN context_loader JSON;
ALTER TABLE model_sets ADD COLUMN whole_file_builder JSON;
ALTER TABLE model_sets ADD COLUMN coder JSON;
================================================
FILE: app/server/migrations/2025031900_add_custom_model_cols.down.sql
================================================
ALTER TABLE custom_models DROP COLUMN max_output_tokens;
ALTER TABLE custom_models DROP COLUMN reserved_output_tokens;
ALTER TABLE custom_models DROP COLUMN model_id;
================================================
FILE: app/server/migrations/2025031900_add_custom_model_cols.up.sql
================================================
ALTER TABLE custom_models
ADD COLUMN max_output_tokens INTEGER NOT NULL,
ADD COLUMN reserved_output_tokens INTEGER NOT NULL,
ADD COLUMN model_id VARCHAR(255) NOT NULL;
================================================
FILE: app/server/migrations/2025032400_sign_in_codes_on_delete.down.sql
================================================
ALTER TABLE sign_in_codes
DROP CONSTRAINT sign_in_codes_org_id_fkey,
ADD CONSTRAINT sign_in_codes_org_id_fkey
FOREIGN KEY (org_id)
REFERENCES orgs(id);
================================================
FILE: app/server/migrations/2025032400_sign_in_codes_on_delete.up.sql
================================================
ALTER TABLE sign_in_codes
DROP CONSTRAINT sign_in_codes_org_id_fkey,
ADD CONSTRAINT sign_in_codes_org_id_fkey
FOREIGN KEY (org_id)
REFERENCES orgs(id)
ON DELETE SET NULL;
================================================
FILE: app/server/migrations/2025051600_custom_models_refactor.down.sql
================================================
DROP TABLE IF EXISTS custom_models;
DROP TABLE IF EXISTS custom_providers;
ALTER TABLE custom_models_legacy RENAME TO custom_models;
================================================
FILE: app/server/migrations/2025051600_custom_models_refactor.up.sql
================================================
BEGIN;
ALTER TABLE custom_models RENAME TO custom_models_legacy;
CREATE TABLE IF NOT EXISTS custom_models (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
model_id VARCHAR(255) NOT NULL,
description TEXT,
publisher VARCHAR(64) NOT NULL DEFAULT '',
max_tokens INTEGER NOT NULL,
default_max_convo_tokens INTEGER NOT NULL,
max_output_tokens INTEGER NOT NULL,
reserved_output_tokens INTEGER NOT NULL,
has_image_support BOOLEAN NOT NULL DEFAULT FALSE,
preferred_output_format VARCHAR(32) NOT NULL DEFAULT 'xml',
system_prompt_disabled BOOLEAN NOT NULL DEFAULT FALSE,
role_params_disabled BOOLEAN NOT NULL DEFAULT FALSE,
stop_disabled BOOLEAN NOT NULL DEFAULT FALSE,
predicted_output_enabled BOOLEAN NOT NULL DEFAULT FALSE,
reasoning_effort_enabled BOOLEAN NOT NULL DEFAULT FALSE,
reasoning_effort VARCHAR(32) NOT NULL DEFAULT '',
include_reasoning BOOLEAN NOT NULL DEFAULT FALSE,
reasoning_budget INTEGER NOT NULL DEFAULT 0,
supports_cache_control BOOLEAN NOT NULL DEFAULT FALSE,
single_message_no_system_prompt BOOLEAN NOT NULL DEFAULT FALSE,
token_estimate_padding_pct FLOAT NOT NULL DEFAULT 0.0,
providers JSON NOT NULL DEFAULT '[]',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS cmv_org_idx ON custom_models(org_id);
CREATE UNIQUE INDEX IF NOT EXISTS cmv_unique_idx ON custom_models(org_id, model_id);
CREATE TRIGGER cmv_modtime BEFORE UPDATE ON custom_models
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TABLE IF NOT EXISTS custom_providers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
base_url VARCHAR(255) NOT NULL,
skip_auth BOOLEAN NOT NULL DEFAULT FALSE,
api_key_env_var VARCHAR(255),
extra_auth_vars JSON NOT NULL DEFAULT '[]',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS cp_org_idx ON custom_providers(org_id);
CREATE UNIQUE INDEX IF NOT EXISTS cp_unique_idx ON custom_providers(org_id, name);
CREATE TRIGGER cp_modtime BEFORE UPDATE ON custom_providers
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
/* ---- migrate base rows into the new custom_models ------------------ */
INSERT INTO custom_models (
id, org_id, model_id, description,
max_tokens, default_max_convo_tokens,
max_output_tokens, reserved_output_tokens,
has_image_support, preferred_output_format,
providers, -- <-- aggregated JSON array
created_at, updated_at
)
SELECT
id, org_id, model_id, description,
max_tokens, default_max_convo_tokens,
max_output_tokens, reserved_output_tokens,
has_image_support, preferred_output_format,
/* -------- build a one-element providers array -------- */
json_build_array(
json_build_object(
'provider', provider,
'custom_provider', custom_provider,
'model_name', model_name
)
)::json,
created_at, updated_at
FROM custom_models_legacy
ON CONFLICT (org_id, model_id) DO NOTHING;
/* ---- migrate unique custom providers ------------------------------- */
WITH src AS (
SELECT DISTINCT
org_id,
custom_provider AS name,
base_url,
api_key_env_var
FROM custom_models_legacy
WHERE custom_provider IS NOT NULL
)
INSERT INTO custom_providers (org_id, name, base_url, api_key_env_var)
SELECT org_id, name, base_url, api_key_env_var
FROM src
ON CONFLICT (org_id, name) DO NOTHING;
COMMIT;
================================================
FILE: app/server/migrations/2025052200_model_pack_cols.down.sql
================================================
ALTER TABLE model_sets
DROP COLUMN IF EXISTS updated_at;
DROP TRIGGER IF EXISTS model_set_modtime ON model_sets;
DROP INDEX IF EXISTS model_set_unique_idx;
================================================
FILE: app/server/migrations/2025052200_model_pack_cols.up.sql
================================================
ALTER TABLE model_sets
ADD COLUMN updated_at TIMESTAMP NOT NULL DEFAULT NOW();
CREATE TRIGGER model_set_modtime BEFORE UPDATE ON model_sets
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE UNIQUE INDEX IF NOT EXISTS model_set_unique_idx ON model_sets(org_id, name);
================================================
FILE: app/server/migrations/2025070200_add_org_user_config.down.sql
================================================
ALTER TABLE orgs_users DROP COLUMN config;
================================================
FILE: app/server/migrations/2025070200_add_org_user_config.up.sql
================================================
ALTER TABLE orgs_users ADD COLUMN config JSON;
================================================
FILE: app/server/model/client.go
================================================
package model
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"plandex-server/db"
"plandex-server/types"
"strings"
"sync"
"time"
shared "plandex-shared"
"github.com/davecgh/go-spew/spew"
"github.com/sashabaranov/go-openai"
)
// note that we are *only* using streaming requests now
// non-streaming request handling has been removed completely
// streams offer more predictable cancellation partial results
const (
ACTIVE_STREAM_CHUNK_TIMEOUT = time.Duration(60) * time.Second
USAGE_CHUNK_TIMEOUT = time.Duration(10) * time.Second
MAX_ADDITIONAL_RETRIES_WITH_FALLBACK = 1
MAX_RETRIES_WITHOUT_FALLBACK = 3
MAX_RETRY_DELAY_SECONDS = 10
)
var httpClient = &http.Client{}
type ClientInfo struct {
Client *openai.Client
ProviderConfig shared.ModelProviderConfigSchema
ApiKey string
OpenAIOrgId string
}
func InitClients(authVars map[string]string, settings *shared.PlanSettings, orgUserConfig *shared.OrgUserConfig) map[string]ClientInfo {
clients := make(map[string]ClientInfo)
providers := shared.GetProvidersForAuthVars(authVars, settings, orgUserConfig)
for _, provider := range providers {
clients[provider.ToComposite()] = newClient(provider, authVars)
}
return clients
}
func newClient(providerConfig shared.ModelProviderConfigSchema, authVars map[string]string) ClientInfo {
var apiKey string
if providerConfig.ApiKeyEnvVar != "" {
apiKey = authVars[providerConfig.ApiKeyEnvVar]
} else if providerConfig.HasClaudeMaxAuth {
apiKey = authVars[shared.AnthropicClaudeMaxTokenEnvVar]
}
config := openai.DefaultConfig(apiKey)
config.BaseURL = providerConfig.BaseUrl
var openAIOrgId string
if providerConfig.Provider == shared.ModelProviderOpenAI && authVars["OPENAI_ORG_ID"] != "" {
openAIOrgId = authVars["OPENAI_ORG_ID"]
config.OrgID = openAIOrgId
}
return ClientInfo{
Client: openai.NewClientWithConfig(config),
ApiKey: apiKey,
ProviderConfig: providerConfig,
OpenAIOrgId: openAIOrgId,
}
}
// ExtendedChatCompletionStream can wrap either a native OpenAI stream or our custom implementation
type ExtendedChatCompletionStream struct {
openaiStream *openai.ChatCompletionStream
customReader *StreamReader[types.ExtendedChatCompletionStreamResponse]
ctx context.Context
}
// StreamReader handles the SSE stream reading
type StreamReader[T any] struct {
reader *bufio.Reader
response *http.Response
emptyMessagesLimit int
errAccumulator *ErrorAccumulator
unmarshaler *JSONUnmarshaler
}
// ErrorAccumulator keeps track of errors during streaming
type ErrorAccumulator struct {
errors []error
mu sync.Mutex
}
// JSONUnmarshaler handles JSON unmarshaling for stream responses
type JSONUnmarshaler struct{}
func CreateChatCompletionStream(
clients map[string]ClientInfo,
authVars map[string]string,
modelConfig *shared.ModelRoleConfig,
settings *shared.PlanSettings,
orgUserConfig *shared.OrgUserConfig,
currentOrgId string,
currentUserId string,
ctx context.Context,
req types.ExtendedChatCompletionRequest,
) (*ExtendedChatCompletionStream, error) {
providerComposite := modelConfig.GetProviderComposite(authVars, settings, orgUserConfig)
_, ok := clients[providerComposite]
if !ok {
return nil, fmt.Errorf("client not found for provider composite: %s", providerComposite)
}
baseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)
// ensure the model name is set correctly on fallbacks
req.Model = baseModelConfig.ModelName
resolveReq(&req, modelConfig, baseModelConfig, settings)
// choose the fastest provider by latency/throughput on openrouter
if baseModelConfig.Provider == shared.ModelProviderOpenRouter {
if !strings.HasSuffix(string(req.Model), ":nitro") && !strings.HasSuffix(string(req.Model), ":free") && !strings.HasSuffix(string(req.Model), ":floor") {
req.Model += ":nitro"
}
}
if baseModelConfig.ReasoningBudget > 0 {
req.ReasoningConfig = &types.ReasoningConfig{
MaxTokens: baseModelConfig.ReasoningBudget,
Exclude: !baseModelConfig.IncludeReasoning || baseModelConfig.HideReasoning,
}
} else if baseModelConfig.ReasoningEffortEnabled {
req.ReasoningConfig = &types.ReasoningConfig{
Effort: shared.ReasoningEffort(baseModelConfig.ReasoningEffort),
Exclude: !baseModelConfig.IncludeReasoning || baseModelConfig.HideReasoning,
}
} else if baseModelConfig.IncludeReasoning {
req.ReasoningConfig = &types.ReasoningConfig{
Exclude: baseModelConfig.HideReasoning,
}
}
return withStreamingRetries(ctx, func(numTotalRetry int, didProviderFallback bool, modelErr *shared.ModelError) (*ExtendedChatCompletionStream, shared.FallbackResult, error) {
handleClaudeMaxRateLimitedIfNeeded(
modelErr,
modelConfig,
authVars,
settings,
orgUserConfig,
currentOrgId,
currentUserId,
)
fallbackRes := modelConfig.GetFallbackForModelError(
numTotalRetry,
didProviderFallback,
modelErr,
authVars,
settings,
orgUserConfig,
)
resolvedModelConfig := fallbackRes.ModelRoleConfig
if resolvedModelConfig == nil {
return nil, fallbackRes, fmt.Errorf("model config is nil")
}
providerComposite := resolvedModelConfig.GetProviderComposite(authVars, settings, orgUserConfig)
baseModelConfig := resolvedModelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)
opClient, ok := clients[providerComposite]
if !ok {
return nil, fallbackRes, fmt.Errorf("client not found for provider composite: %s", providerComposite)
}
if modelErr != nil && modelErr.Kind == shared.ErrCacheSupport {
for i := range req.Messages {
for j := range req.Messages[i].Content {
if req.Messages[i].Content[j].CacheControl != nil {
req.Messages[i].Content[j].CacheControl = nil
}
}
}
}
modelConfig = resolvedModelConfig
log.Println("createChatCompletionStreamExtended - modelConfig")
spew.Dump(map[string]interface{}{
"modelConfig.ModelId": baseModelConfig.ModelId,
"modelConfig.ModelTag": baseModelConfig.ModelTag,
"modelConfig.ModelName": baseModelConfig.ModelName,
"modelConfig.Provider": baseModelConfig.Provider,
"modelConfig.BaseUrl": baseModelConfig.BaseUrl,
"modelConfig.ApiKeyEnvVar": baseModelConfig.ApiKeyEnvVar,
})
resp, err := createChatCompletionStreamExtended(resolvedModelConfig, opClient, authVars, settings, orgUserConfig, ctx, req)
return resp, fallbackRes, err
}, func(resp *ExtendedChatCompletionStream, err error) {})
}
func createChatCompletionStreamExtended(
modelConfig *shared.ModelRoleConfig,
client ClientInfo,
authVars map[string]string,
settings *shared.PlanSettings,
orgUserConfig *shared.OrgUserConfig,
ctx context.Context,
extendedReq types.ExtendedChatCompletionRequest,
) (*ExtendedChatCompletionStream, error) {
baseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)
// ensure the model name is set correctly on fallbacks
extendedReq.Model = baseModelConfig.ModelName
var openaiReq *types.ExtendedOpenAIChatCompletionRequest
if baseModelConfig.Provider == shared.ModelProviderOpenAI {
openaiReq = extendedReq.ToOpenAI()
log.Println("Creating chat completion stream with direct OpenAI provider request")
}
switch baseModelConfig.Provider {
case shared.ModelProviderGoogleVertex:
if authVars["VERTEXAI_PROJECT"] != "" {
extendedReq.VertexProject = authVars["VERTEXAI_PROJECT"]
}
if authVars["VERTEXAI_LOCATION"] != "" {
extendedReq.VertexLocation = authVars["VERTEXAI_LOCATION"]
}
if authVars["GOOGLE_APPLICATION_CREDENTIALS"] != "" {
extendedReq.VertexCredentials = authVars["GOOGLE_APPLICATION_CREDENTIALS"]
}
case shared.ModelProviderAzureOpenAI:
if authVars["AZURE_API_BASE"] != "" {
extendedReq.LiteLLMApiBase = authVars["AZURE_API_BASE"]
}
if authVars["AZURE_API_VERSION"] != "" {
extendedReq.AzureApiVersion = authVars["AZURE_API_VERSION"]
}
if authVars["AZURE_DEPLOYMENTS_MAP"] != "" {
var azureDeploymentsMap map[string]string
err := json.Unmarshal([]byte(authVars["AZURE_DEPLOYMENTS_MAP"]), &azureDeploymentsMap)
if err != nil {
return nil, fmt.Errorf("error unmarshalling AZURE_DEPLOYMENTS_MAP: %w", err)
}
modelName := string(extendedReq.Model)
modelName = strings.ReplaceAll(modelName, "azure/", "")
deploymentName, ok := azureDeploymentsMap[modelName]
if ok {
log.Println("azure - deploymentName", deploymentName)
modelName = "azure/" + deploymentName
extendedReq.Model = shared.ModelName(modelName)
}
}
// azure uses 'reasoning_config' instead of 'reasoning' like direct openai api
if extendedReq.ReasoningConfig != nil {
extendedReq.AzureReasoningEffort = extendedReq.ReasoningConfig.Effort
extendedReq.ReasoningConfig = nil
}
case shared.ModelProviderAmazonBedrock:
if authVars["AWS_ACCESS_KEY_ID"] != "" {
extendedReq.BedrockAccessKeyId = authVars["AWS_ACCESS_KEY_ID"]
}
if authVars["AWS_SECRET_ACCESS_KEY"] != "" {
extendedReq.BedrockSecretAccessKey = authVars["AWS_SECRET_ACCESS_KEY"]
}
if authVars["AWS_SESSION_TOKEN"] != "" {
extendedReq.BedrockSessionToken = authVars["AWS_SESSION_TOKEN"]
}
if authVars["AWS_REGION"] != "" {
extendedReq.BedrockRegion = authVars["AWS_REGION"]
}
if authVars["AWS_INFERENCE_PROFILE_ARN"] != "" {
extendedReq.BedrockInferenceProfileArn = authVars["AWS_INFERENCE_PROFILE_ARN"]
}
case shared.ModelProviderOllama:
if os.Getenv("OLLAMA_BASE_URL") != "" {
extendedReq.LiteLLMApiBase = os.Getenv("OLLAMA_BASE_URL")
}
}
if client.ProviderConfig.HasClaudeMaxAuth {
extendedReq.Messages = append([]types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{Type: openai.ChatMessagePartTypeText,
Text: "You are Claude Code, Anthropic's official CLI for Claude."},
},
},
}, extendedReq.Messages...)
if extendedReq.ExtraHeaders == nil {
extendedReq.ExtraHeaders = make(map[string]string)
}
extendedReq.ExtraHeaders["anthropic-beta"] = shared.AnthropicClaudeMaxBetaHeader
extendedReq.ExtraHeaders["Authorization"] = "Bearer " + authVars[shared.AnthropicClaudeMaxTokenEnvVar]
extendedReq.ExtraHeaders["anthropic-product"] = "claude-code"
}
// Marshal the request body to JSON
var jsonBody []byte
var err error
if openaiReq != nil {
jsonBody, err = json.Marshal(openaiReq)
} else {
jsonBody, err = json.Marshal(extendedReq)
}
if err != nil {
return nil, fmt.Errorf("error marshaling request: %w", err)
}
// log.Println("request jsonBody", string(jsonBody))
// Create new request
baseUrl := baseModelConfig.BaseUrl
url := baseUrl + "/chat/completions"
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody))
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
// Set required headers for streaming
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Connection", "keep-alive")
// some providers send api key in the body, some in the header
// some use other auth methods and so don't have a simple api key
if client.ApiKey != "" {
req.Header.Set("Authorization", "Bearer "+client.ApiKey)
}
if client.OpenAIOrgId != "" {
req.Header.Set("OpenAI-Organization", client.OpenAIOrgId)
}
addOpenRouterHeaders(req)
// Send the request
resp, err := httpClient.Do(req) //nolint:bodyclose // body is closed in stream.Close()
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading error response: %w", err)
}
return nil, &HTTPError{
StatusCode: resp.StatusCode,
Body: string(body),
Header: resp.Header.Clone(), // retain Retry-After etc.
}
}
// Log response headers
// log.Println("Response headers:")
// for key, values := range resp.Header {
// log.Printf("%s: %v\n", key, values)
// }
reader := &StreamReader[types.ExtendedChatCompletionStreamResponse]{
reader: bufio.NewReader(resp.Body),
response: resp,
emptyMessagesLimit: 30,
errAccumulator: NewErrorAccumulator(),
unmarshaler: &JSONUnmarshaler{},
}
return &ExtendedChatCompletionStream{
customReader: reader,
ctx: ctx,
}, nil
}
func NewErrorAccumulator() *ErrorAccumulator {
return &ErrorAccumulator{
errors: make([]error, 0),
}
}
func (ea *ErrorAccumulator) Add(err error) {
ea.mu.Lock()
defer ea.mu.Unlock()
ea.errors = append(ea.errors, err)
}
func (ea *ErrorAccumulator) GetErrors() []error {
ea.mu.Lock()
defer ea.mu.Unlock()
return ea.errors
}
func (ju *JSONUnmarshaler) Unmarshal(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
// Recv reads from the stream
func (stream *StreamReader[T]) Recv() (*T, error) {
for {
line, err := stream.reader.ReadString('\n')
if err != nil {
return nil, err
}
// Trim any whitespace
line = strings.TrimSpace(line)
// Skip empty lines
if line == "" {
continue
}
// Check for data prefix
if !strings.HasPrefix(line, "data: ") {
continue
}
// Extract the data
data := strings.TrimPrefix(line, "data: ")
// log.Println("\n\n--- stream data:\n", data, "\n\n")
// Check for stream completion
if data == "[DONE]" {
return nil, io.EOF
}
// Parse the response
var response T
err = stream.unmarshaler.Unmarshal([]byte(data), &response)
if err != nil {
stream.errAccumulator.Add(err)
continue
}
return &response, nil
}
}
func (stream *StreamReader[T]) Close() error {
if stream.response != nil {
return stream.response.Body.Close()
}
return nil
}
// Recv returns the next message in the stream
func (stream *ExtendedChatCompletionStream) Recv() (*types.ExtendedChatCompletionStreamResponse, error) {
select {
case <-stream.ctx.Done():
return nil, stream.ctx.Err()
default:
if stream.openaiStream != nil {
bytes, err := stream.openaiStream.RecvRaw()
if err != nil {
return nil, err
}
var response types.ExtendedChatCompletionStreamResponse
err = json.Unmarshal(bytes, &response)
if err != nil {
return nil, err
}
return &response, nil
}
return stream.customReader.Recv()
}
}
// Close the response body
func (stream *ExtendedChatCompletionStream) Close() error {
if stream.openaiStream != nil {
return stream.openaiStream.Close()
}
return stream.customReader.Close()
}
func resolveReq(req *types.ExtendedChatCompletionRequest, modelConfig *shared.ModelRoleConfig, baseModelConfig *shared.BaseModelConfig, settings *shared.PlanSettings) {
// if system prompt is disabled, change the role of the system message to user
if modelConfig.GetSharedBaseConfig(settings).SystemPromptDisabled {
log.Println("System prompt disabled - changing role of system message to user")
for i, msg := range req.Messages {
log.Println("Message role:", msg.Role)
if msg.Role == openai.ChatMessageRoleSystem {
log.Println("Changing role of system message to user")
req.Messages[i].Role = openai.ChatMessageRoleUser
}
}
for _, msg := range req.Messages {
log.Println("Final message role:", msg.Role)
}
}
if modelConfig.GetSharedBaseConfig(settings).RoleParamsDisabled {
log.Println("Role params disabled - setting temperature and top p to 0")
req.Temperature = 0
req.TopP = 0
}
if baseModelConfig.Provider == shared.ModelProviderOllama {
// ollama doesn't support temperature or top p params
log.Println("Ollama - clearing temperature and top p")
req.Temperature = 0
req.TopP = 0
}
}
func addOpenRouterHeaders(req *http.Request) {
req.Header.Set("HTTP-Referer", "https://plandex.ai")
req.Header.Set("X-Title", "Plandex")
req.Header.Set("X-OR-Prefer", "ttft,throughput")
if os.Getenv("GOENV") == "production" {
req.Header.Set("X-OR-Region", "us-east-1")
}
}
func handleClaudeMaxRateLimitedIfNeeded(modelErr *shared.ModelError, modelConfig *shared.ModelRoleConfig, authVars map[string]string, settings *shared.PlanSettings, orgUserConfig *shared.OrgUserConfig, currentOrgId string, currentUserId string) {
// if we used a claude max provider and got rate limited, set the cooldown on org user config and update the db in the background
if modelErr != nil && modelErr.Kind == shared.ErrRateLimited && modelErr.RetryAfterSeconds == 0 {
baseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)
if baseModelConfig.BaseModelProviderConfig.HasClaudeMaxAuth {
orgUserConfig.ClaudeSubscriptionCooldownStartedAt = time.Now()
go func() {
err := db.UpdateOrgUserConfig(currentUserId, currentOrgId, orgUserConfig)
if err != nil {
log.Printf("Error updating org user config: %v\n", err)
}
}()
}
}
}
================================================
FILE: app/server/model/client_stream.go
================================================
package model
import (
"context"
"fmt"
"io"
"log"
"math/rand"
"plandex-server/types"
shared "plandex-shared"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/sashabaranov/go-openai"
)
type OnStreamFn func(chunk string, buffer string) (shouldStop bool)
func CreateChatCompletionWithInternalStream(
clients map[string]ClientInfo,
authVars map[string]string,
modelConfig *shared.ModelRoleConfig,
settings *shared.PlanSettings,
orgUserConfig *shared.OrgUserConfig,
currentOrgId string,
currentUserId string,
ctx context.Context,
req types.ExtendedChatCompletionRequest,
onStream OnStreamFn,
reqStarted time.Time,
) (*types.ModelResponse, error) {
providerComposite := modelConfig.GetProviderComposite(authVars, settings, orgUserConfig)
_, ok := clients[providerComposite]
if !ok {
return nil, fmt.Errorf("client not found for provider composite: %s", providerComposite)
}
baseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)
resolveReq(&req, modelConfig, baseModelConfig, settings)
// choose the fastest provider by latency/throughput on openrouter
if baseModelConfig.Provider == shared.ModelProviderOpenRouter {
req.Model += ":nitro"
}
// Force streaming mode since we're using the streaming API
req.Stream = true
// Include usage in stream response
req.StreamOptions = &openai.StreamOptions{
IncludeUsage: true,
}
return withStreamingRetries(ctx, func(numTotalRetry int, didProviderFallback bool, modelErr *shared.ModelError) (resp *types.ModelResponse, fallbackRes shared.FallbackResult, err error) {
handleClaudeMaxRateLimitedIfNeeded(modelErr, modelConfig, authVars, settings, orgUserConfig, currentOrgId, currentUserId)
fallbackRes = modelConfig.GetFallbackForModelError(numTotalRetry, didProviderFallback, modelErr, authVars, settings, orgUserConfig)
resolvedModelConfig := fallbackRes.ModelRoleConfig
if resolvedModelConfig == nil {
return nil, fallbackRes, fmt.Errorf("model config is nil")
}
providerComposite := resolvedModelConfig.GetProviderComposite(authVars, settings, orgUserConfig)
opClient, ok := clients[providerComposite]
if !ok {
return nil, fallbackRes, fmt.Errorf("client not found for provider composite: %s", providerComposite)
}
modelConfig = resolvedModelConfig
resp, err = processChatCompletionStream(resolvedModelConfig, opClient, authVars, settings, orgUserConfig, ctx, req, onStream, reqStarted)
if err != nil {
return nil, fallbackRes, err
}
return resp, fallbackRes, nil
}, func(resp *types.ModelResponse, err error) {
if resp != nil {
resp.Stopped = true
resp.Error = err.Error()
}
})
}
func processChatCompletionStream(
modelConfig *shared.ModelRoleConfig,
client ClientInfo,
authVars map[string]string,
settings *shared.PlanSettings,
orgUserConfig *shared.OrgUserConfig,
ctx context.Context,
req types.ExtendedChatCompletionRequest,
onStream OnStreamFn,
reqStarted time.Time,
) (*types.ModelResponse, error) {
streamCtx, cancel := context.WithCancel(ctx)
log.Println("processChatCompletionStream - modelConfig", spew.Sdump(map[string]interface{}{
"model": modelConfig.ModelId,
}))
stream, err := createChatCompletionStreamExtended(modelConfig, client, authVars, settings, orgUserConfig, streamCtx, req)
if err != nil {
cancel()
return nil, fmt.Errorf("error creating chat completion stream: %w", err)
}
defer stream.Close()
defer cancel()
accumulator := types.NewStreamCompletionAccumulator()
// Create a timer that will trigger if no chunk is received within the specified duration
timer := time.NewTimer(ACTIVE_STREAM_CHUNK_TIMEOUT)
defer timer.Stop()
streamFinished := false
receivedFirstChunk := false
// Process stream until EOF or error
for {
select {
case <-streamCtx.Done():
log.Println("Stream canceled")
return accumulator.Result(true, streamCtx.Err()), streamCtx.Err()
case <-timer.C:
log.Println("Stream timed out due to inactivity")
if streamFinished {
log.Println("Stream finished—timed out waiting for usage chunk")
return accumulator.Result(false, nil), nil
} else {
log.Println("Stream timed out due to inactivity")
return accumulator.Result(true, fmt.Errorf("stream timed out due to inactivity. The model is not responding.")), nil
}
default:
response, err := stream.Recv()
if err == io.EOF {
if streamFinished {
return accumulator.Result(false, nil), nil
}
err = fmt.Errorf("model stream ended unexpectedly: %w", err)
return accumulator.Result(true, err), err
}
if err != nil {
err = fmt.Errorf("error receiving stream chunk: %w", err)
return accumulator.Result(true, err), err
}
if response.ID != "" {
accumulator.SetGenerationId(response.ID)
}
if !receivedFirstChunk {
receivedFirstChunk = true
accumulator.SetFirstTokenAt(time.Now())
}
if !timer.Stop() {
<-timer.C
}
timer.Reset(ACTIVE_STREAM_CHUNK_TIMEOUT)
// Process the response
if response.Usage != nil {
accumulator.SetUsage(response.Usage)
return accumulator.Result(false, nil), nil
}
emptyChoices := false
var content string
if len(response.Choices) == 0 {
// Previously we'd return an error if there were no choices, but some models do this and then keep streaming, so we'll just log it and continue
log.Println("processChatCompletionStream - no choices in response")
// err := fmt.Errorf("no choices in response")
// return accumulator.Result(false, err), err
emptyChoices = true
}
// We'll be more accepting of multiple choices and just take the first one
// if len(response.Choices) > 1 {
// err = fmt.Errorf("stream finished with more than one choice | The model failed to generate a valid response.")
// return accumulator.Result(true, err), err
// }
if !emptyChoices {
choice := response.Choices[0]
if choice.FinishReason != "" {
if choice.FinishReason == "error" {
err = fmt.Errorf("model stopped with error status | The model is not responding.")
return accumulator.Result(true, err), err
} else {
// Reset the timer for the usage chunk
if !timer.Stop() {
<-timer.C
}
timer.Reset(USAGE_CHUNK_TIMEOUT)
streamFinished = true
continue
}
}
if req.Tools != nil {
if choice.Delta.ToolCalls != nil {
toolCall := choice.Delta.ToolCalls[0]
content = toolCall.Function.Arguments
}
} else {
if choice.Delta.Content != "" {
content = choice.Delta.Content
}
}
}
accumulator.AddContent(content)
// pass the chunk and the accumulated content to the callback
if onStream != nil {
shouldReturn := onStream(content, accumulator.Content())
if shouldReturn {
return accumulator.Result(false, nil), nil
}
}
}
}
}
func withStreamingRetries[T any](
ctx context.Context,
operation func(numRetry int, didProviderFallback bool, modelErr *shared.ModelError) (resp *T, fallbackRes shared.FallbackResult, err error),
onContextDone func(resp *T, err error),
) (*T, error) {
var resp *T
var numTotalRetry int
var numFallbackRetry int
var fallbackRes shared.FallbackResult
var modelErr *shared.ModelError
var didProviderFallback bool
for {
if ctx.Err() != nil {
if resp != nil {
// Return partial result with context error
onContextDone(resp, ctx.Err())
return resp, ctx.Err()
}
return nil, ctx.Err()
}
var err error
var numRetry int
if numFallbackRetry > 0 {
numRetry = numFallbackRetry
} else {
numRetry = numTotalRetry
}
log.Printf("withStreamingRetries - will run operation")
log.Println(spew.Sdump(map[string]interface{}{
"numTotalRetry": numTotalRetry,
"didProviderFallback": didProviderFallback,
"modelErr": modelErr,
}))
resp, fallbackRes, err = operation(numTotalRetry, didProviderFallback, modelErr)
if err == nil {
return resp, nil
}
log.Printf("withStreamingRetries - operation returned error: %v", err)
isFallback := fallbackRes.IsFallback
maxRetries := MAX_RETRIES_WITHOUT_FALLBACK
if isFallback {
maxRetries = MAX_ADDITIONAL_RETRIES_WITH_FALLBACK
}
if fallbackRes.FallbackType == shared.FallbackTypeProvider {
didProviderFallback = true
}
compareRetries := numTotalRetry
if isFallback {
compareRetries = numFallbackRetry
}
log.Printf("Error in streaming operation: %v, isFallback: %t, numTotalRetry: %d, numFallbackRetry: %d, numRetry: %d, compareRetries: %d, maxRetries: %d\n", err, isFallback, numTotalRetry, numFallbackRetry, numRetry, compareRetries, maxRetries)
classifyRes := classifyBasicError(err, fallbackRes.BaseModelConfig.HasClaudeMaxAuth)
modelErr = &classifyRes
newFallback := false
if !modelErr.Retriable {
log.Printf("withStreamingRetries - operation returned non-retriable error: %v", err)
spew.Dump(modelErr)
if modelErr.Kind == shared.ErrContextTooLong && fallbackRes.ModelRoleConfig.LargeContextFallback == nil {
log.Printf("withStreamingRetries - non-retriable context too long error and no large context fallback is defined, returning error")
// if it's a context too long error and no large context fallback is defined, return the error
return resp, err
} else if modelErr.Kind != shared.ErrContextTooLong && fallbackRes.ModelRoleConfig.ErrorFallback == nil {
log.Printf("withStreamingRetries - non-retriable error and no error fallback is defined, returning error")
// if it's any other error and no error fallback is defined, return the error
return resp, err
}
log.Printf("withStreamingRetries - operation returned non-retriable error, but has fallback - resetting numFallbackRetry to 0 and continuing to retry")
numFallbackRetry = 0
newFallback = true
compareRetries = 0
// otherwise, continue to retry logic
}
if compareRetries >= maxRetries {
log.Printf("withStreamingRetries - compareRetries >= maxRetries - returning error")
return resp, err
}
var retryDelay time.Duration
if modelErr != nil && modelErr.RetryAfterSeconds > 0 {
// if the model err has a retry after, then use that with a bit of padding
retryDelay = time.Duration(int(float64(modelErr.RetryAfterSeconds)*1.1)) * time.Second
} else {
// otherwise, use some jitter
retryDelay = time.Duration(1000+rand.Intn(200)) * time.Millisecond
}
log.Printf("withStreamingRetries - retrying stream in %v seconds", retryDelay)
time.Sleep(retryDelay)
if modelErr != nil && modelErr.ShouldIncrementRetry() {
numTotalRetry++
if isFallback && !newFallback {
numFallbackRetry++
}
}
}
}
================================================
FILE: app/server/model/litellm.go
================================================
package model
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strconv"
"sync"
"time"
)
var (
liteLLMOnce sync.Once
liteLLMCmd *exec.Cmd
)
func EnsureLiteLLM(numWorkers int) error {
var finalErr error
liteLLMOnce.Do(func() {
if isLiteLLMHealthy() {
log.Println("LiteLLM proxy is already healthy")
return
}
log.Println("LiteLLM proxy is not running. Starting...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := startLiteLLMServer(numWorkers)
if err != nil {
log.Println("LiteLLM proxy launch failed:", err)
finalErr = fmt.Errorf("LiteLLM proxy launch failed: %w", err)
return
}
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
log.Println("LiteLLM proxy launch timed out")
finalErr = fmt.Errorf("LiteLLM proxy launch timed out")
return
case <-ticker.C:
if isLiteLLMHealthy() {
log.Println("LiteLLM proxy is healthy")
return
} else {
log.Println("LiteLLM proxy is not healthy yet, retrying after 500ms...")
}
}
}
})
return finalErr
}
func ShutdownLiteLLMServer() error {
if liteLLMCmd != nil && liteLLMCmd.Process != nil {
log.Println("Shutting down LiteLLM proxy gracefully...")
if err := liteLLMCmd.Process.Signal(os.Interrupt); err != nil {
return fmt.Errorf("failed to signal LiteLLM for shutdown: %w", err)
}
done := make(chan error, 1)
go func() {
done <- liteLLMCmd.Wait()
}()
select {
case <-time.After(5 * time.Second):
log.Println("LiteLLM proxy shutdown timed out, forcing kill")
return liteLLMCmd.Process.Kill()
case err := <-done:
return err
}
}
return nil
}
func isLiteLLMHealthy() bool {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "http://localhost:4000/health", nil)
if err != nil {
log.Println("LiteLLM health check request failed:", err)
return false
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Println("LiteLLM health check failed:", err)
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}
func startLiteLLMServer(numWorkers int) error {
liteLLMCmd = exec.Command("python3",
"-m", "uvicorn",
"litellm_proxy:app",
"--host", "0.0.0.0",
"--port", "4000",
"--workers", strconv.Itoa(numWorkers),
)
if os.Getenv("LITELLM_PROXY_DIR") != "" {
liteLLMCmd.Dir = os.Getenv("LITELLM_PROXY_DIR")
}
// clean env
liteLLMCmd.Env = []string{
"PATH=" + os.Getenv("PATH"),
"HOME=" + os.Getenv("HOME"),
}
if os.Getenv("OLLAMA_BASE_URL") != "" {
log.Println("OLLAMA_BASE_URL is set, so we can reach ollama from inside docker container in local mode")
// so we can reach ollama from inside docker container in local mode
liteLLMCmd.Env = append(liteLLMCmd.Env, "OLLAMA_BASE_URL="+os.Getenv("OLLAMA_BASE_URL"))
}
liteLLMCmd.Stdout = os.Stdout
liteLLMCmd.Stderr = os.Stderr
err := liteLLMCmd.Start()
if err != nil {
return err
}
log.Println("LiteLLM proxy launched")
return nil
}
================================================
FILE: app/server/model/model_error.go
================================================
package model
import (
"fmt"
"log"
"net/http"
shared "plandex-shared"
"regexp"
"strconv"
"strings"
"time"
)
type HTTPError struct {
StatusCode int
Body string
Header http.Header
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("status code: %d, body: %s", e.StatusCode, e.Body)
}
// JSON-style `"retry_after_ms":1234`
var reJSON = regexp.MustCompile(`"retry_after_ms"\s*:\s*(\d+)`)
// Header- or text-style "Retry-After: 12" / "retry_after: 12s"
var reRetryAfter = regexp.MustCompile(
`retry[_\-\s]?after[_\-\s]?(?:[:\s]+)?(\d+)(ms|seconds?|secs?|s)?`,
)
// Free-form Azure style "Try again in 59 seconds."
// Also matches "Retry in 10 seconds."
var reTryAgain = regexp.MustCompile(
`(?:re)?try[_\-\s]+(?:again[_\-\s]+)?in[_\-\s]+(\d+)(ms|seconds?|secs?|s)?`,
)
func ClassifyErrMsg(msg string) *shared.ModelError {
log.Printf("Classifying error message: %s", msg)
msg = strings.ToLower(msg)
if strings.Contains(msg, "maximum context length") ||
strings.Contains(msg, "context length exceeded") ||
strings.Contains(msg, "exceed context limit") ||
strings.Contains(msg, "decrease input length") ||
strings.Contains(msg, "too many tokens") ||
strings.Contains(msg, "payload too large") ||
strings.Contains(msg, "payload is too large") ||
strings.Contains(msg, "input is too large") ||
strings.Contains(msg, "input too large") ||
strings.Contains(msg, "input is too long") ||
strings.Contains(msg, "input too long") {
log.Printf("Context too long error: %s", msg)
return &shared.ModelError{
Kind: shared.ErrContextTooLong,
Retriable: false,
RetryAfterSeconds: 0,
}
}
if strings.Contains(msg, "model_overloaded") ||
strings.Contains(msg, "model overloaded") ||
strings.Contains(msg, "server is overloaded") ||
strings.Contains(msg, "model is currently overloaded") ||
strings.Contains(msg, "overloaded_error") ||
strings.Contains(msg, "resource has been exhausted") {
log.Printf("Overloaded error: %s", msg)
return &shared.ModelError{
Kind: shared.ErrOverloaded,
Retriable: true,
RetryAfterSeconds: 0,
}
}
if strings.Contains(msg, "cache control") {
log.Printf("Cache control error: %s", msg)
return &shared.ModelError{
Kind: shared.ErrCacheSupport,
Retriable: true,
RetryAfterSeconds: 0,
}
}
log.Println("No error classification based on message")
return nil
}
func ClassifyModelError(code int, message string, headers http.Header, isClaudeMax bool) shared.ModelError {
msg := strings.ToLower(message)
// first of all, if it's claude max and a 429, it means the subscription limit was reached, so handle it accordingly
if isClaudeMax && code == 429 {
retryAfter := extractRetryAfter(headers, msg)
if retryAfter > 0 {
return shared.ModelError{
Kind: shared.ErrSubscriptionQuotaExhausted,
Retriable: true,
RetryAfterSeconds: retryAfter,
}
}
return shared.ModelError{
Kind: shared.ErrSubscriptionQuotaExhausted,
Retriable: false,
RetryAfterSeconds: 0,
}
}
// next try to classify the error based on the message only
msgRes := ClassifyErrMsg(msg)
if msgRes != nil {
log.Printf("Classified error message: %+v", msgRes)
return *msgRes
}
var res shared.ModelError
switch code {
case 429, 529:
res = shared.ModelError{
Kind: shared.ErrRateLimited,
Retriable: true,
RetryAfterSeconds: 0,
}
case 413:
res = shared.ModelError{
Kind: shared.ErrContextTooLong,
Retriable: false,
RetryAfterSeconds: 0,
}
// rare codes but they never succeed on retry if they do show up
case 501, 505:
res = shared.ModelError{
Kind: shared.ErrOther,
Retriable: false,
RetryAfterSeconds: 0,
}
default:
res = shared.ModelError{
Kind: shared.ErrOther,
Retriable: code >= 500 || strings.Contains(msg, "provider returned error"), // 'provider returned error' is from OpenRouter, and unless it's a non-retriable status code, it should still be retried since OpenRouter may switch to a different provider
RetryAfterSeconds: 0,
}
}
log.Printf("Model error: %+v", res)
// best‑effort parse of "Retry‑After" style hints in the message
if res.Retriable {
retryAfter := extractRetryAfter(headers, msg)
// if the retry after is greater than the max delay, then the error is not retriable
if retryAfter > MAX_RETRY_DELAY_SECONDS {
log.Printf("Retry after %d seconds is greater than the max delay of %d seconds - not retriable", retryAfter, MAX_RETRY_DELAY_SECONDS)
res.Retriable = false
} else {
res.RetryAfterSeconds = retryAfter
}
}
return res
}
func extractRetryAfter(h http.Header, body string) (sec int) {
now := time.Now()
// Retry-After header: seconds or HTTP-date
if v := h.Get("Retry-After"); v != "" {
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {
return n
}
if t, err := time.Parse(http.TimeFormat, v); err == nil {
d := int(t.Sub(now).Seconds())
if d > 0 {
return d
}
}
}
// X-RateLimit-Reset epoch
if v := h.Get("X-RateLimit-Reset"); v != "" {
if reset, _ := strconv.ParseInt(v, 10, 64); reset > now.Unix() {
return int(reset - now.Unix())
}
}
lower := strings.ToLower(strings.TrimSpace(body))
// "retry_after_ms": 1234
if m := reJSON.FindStringSubmatch(lower); len(m) == 2 {
n, _ := strconv.Atoi(m[1])
return n / 1000
}
// "retry after 12"
if m := reRetryAfter.FindStringSubmatch(lower); len(m) >= 2 {
unit := ""
if len(m) == 3 {
unit = m[2]
}
return normalizeUnit(m[1], unit)
}
// "try again in 8"
if m := reTryAgain.FindStringSubmatch(lower); len(m) >= 2 {
unit := ""
if len(m) == 3 {
unit = m[2]
}
return normalizeUnit(m[1], unit)
}
return 0
}
func normalizeUnit(numStr, unit string) int {
n, _ := strconv.Atoi(numStr) // safe because the regex matched \d+
switch unit {
case "ms": // milliseconds
return n / 1000
case "sec", "secs", "second", "seconds", "s":
return n // already in seconds
default: // unit omitted ⇒ assume seconds
return n
}
}
func classifyBasicError(err error, isClaudeMax bool) shared.ModelError {
// if it's an http error, classify it based on the status code and body
if httpErr, ok := err.(*HTTPError); ok {
me := ClassifyModelError(
httpErr.StatusCode,
httpErr.Body,
httpErr.Header,
isClaudeMax,
)
return me
}
// try to classify the error based on the message only
msgRes := ClassifyErrMsg(err.Error())
if msgRes != nil {
return *msgRes
}
// Fall back to old heuristic – still keeps the signature identical
if isNonRetriableBasicErr(err) {
return shared.ModelError{Kind: shared.ErrOther, Retriable: false}
}
return shared.ModelError{Kind: shared.ErrOther, Retriable: true}
}
func isNonRetriableBasicErr(err error) bool {
errStr := err.Error()
// we don't want to retry on the errors below
if strings.Contains(errStr, "context deadline exceeded") || strings.Contains(errStr, "context canceled") {
log.Println("Context deadline exceeded or canceled - no retry")
return true
}
if strings.Contains(errStr, "status code: 400") &&
strings.Contains(errStr, "reduce the length of the messages") {
log.Println("Token limit exceeded - no retry")
return true
}
if strings.Contains(errStr, "status code: 401") {
log.Println("Invalid auth or api key - no retry")
return true
}
if strings.Contains(errStr, "status code: 429") && strings.Contains(errStr, "exceeded your current quota") {
log.Println("Current quota exceeded - no retry")
return true
}
return false
}
================================================
FILE: app/server/model/model_request.go
================================================
package model
import (
"context"
"fmt"
"log"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/notify"
"plandex-server/types"
shared "plandex-shared"
"runtime/debug"
"strings"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/sashabaranov/go-openai"
)
type ModelRequestParams struct {
Clients map[string]ClientInfo
AuthVars map[string]string
Auth *types.ServerAuth
Plan *db.Plan
ModelConfig *shared.ModelRoleConfig
Settings *shared.PlanSettings
OrgUserConfig *shared.OrgUserConfig
Purpose string
Messages []types.ExtendedChatMessage
Prediction string
Stop []string
Tools []openai.Tool
ToolChoice *openai.ToolChoice
EstimatedOutputTokens int // optional
ModelStreamId string
ConvoMessageId string
BuildId string
ModelPackName string
SessionId string
BeforeReq func()
AfterReq func()
OnStream func(string, string) bool
WillCacheNumTokens int
}
func ModelRequest(
ctx context.Context,
params ModelRequestParams,
) (*types.ModelResponse, error) {
clients := params.Clients
authVars := params.AuthVars
auth := params.Auth
plan := params.Plan
messages := params.Messages
prediction := params.Prediction
stop := params.Stop
tools := params.Tools
toolChoice := params.ToolChoice
modelConfig := params.ModelConfig
modelStreamId := params.ModelStreamId
convoMessageId := params.ConvoMessageId
buildId := params.BuildId
modelPackName := params.ModelPackName
purpose := params.Purpose
sessionId := params.SessionId
settings := params.Settings
orgUserConfig := params.OrgUserConfig
currentOrgId := auth.OrgId
currentUserId := auth.User.Id
if purpose == "" {
return nil, fmt.Errorf("purpose is required")
}
baseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)
messages = FilterEmptyMessages(messages)
messages = CheckSingleSystemMessage(modelConfig, baseModelConfig, messages)
inputTokensEstimate := GetMessagesTokenEstimate(messages...) + TokensPerRequest
config := modelConfig.GetRoleForInputTokens(inputTokensEstimate, settings)
modelConfig = &config
if params.EstimatedOutputTokens != 0 {
config = modelConfig.GetRoleForOutputTokens(params.EstimatedOutputTokens, settings)
modelConfig = &config
}
log.Println("ModelRequest - modelConfig:")
spew.Dump(modelConfig)
log.Println("ModelRequest - baseModelConfig:")
spew.Dump(baseModelConfig)
log.Printf("Model config - role: %s, model: %s, max output tokens: %d\n", modelConfig.Role, baseModelConfig.ModelName, baseModelConfig.MaxOutputTokens)
expectedOutputTokens := baseModelConfig.MaxOutputTokens - inputTokensEstimate
if params.EstimatedOutputTokens != 0 {
expectedOutputTokens = params.EstimatedOutputTokens
}
_, apiErr := hooks.ExecHook(hooks.WillSendModelRequest, hooks.HookParams{
Auth: auth,
Plan: plan,
WillSendModelRequestParams: &hooks.WillSendModelRequestParams{
InputTokens: inputTokensEstimate,
OutputTokens: expectedOutputTokens,
ModelName: baseModelConfig.ModelName,
ModelId: baseModelConfig.ModelId,
ModelTag: baseModelConfig.ModelTag,
},
})
if apiErr != nil {
return nil, apiErr
}
if params.BeforeReq != nil {
params.BeforeReq()
}
reqStarted := time.Now()
req := types.ExtendedChatCompletionRequest{
Model: baseModelConfig.ModelName,
Messages: messages,
}
if !baseModelConfig.RoleParamsDisabled {
req.Temperature = modelConfig.Temperature
req.TopP = modelConfig.TopP
}
if len(tools) > 0 {
req.Tools = tools
}
if toolChoice != nil {
req.ToolChoice = toolChoice
}
onStream := params.OnStream
if baseModelConfig.StopDisabled {
if len(stop) > 0 {
onStream = func(chunk string, buffer string) (shouldStop bool) {
for _, stopSequence := range stop {
if strings.Contains(buffer, stopSequence) {
return true
}
}
if params.OnStream != nil {
return params.OnStream(chunk, buffer)
}
return false
}
}
} else {
req.Stop = stop
}
if prediction != "" {
req.Prediction = &types.OpenAIPrediction{
Type: "content",
Content: prediction,
}
}
res, err := CreateChatCompletionWithInternalStream(clients, authVars, modelConfig, settings, orgUserConfig, currentOrgId, currentUserId, ctx, req, onStream, reqStarted)
if err != nil {
return nil, err
}
if baseModelConfig.StopDisabled && len(stop) > 0 {
earliest := len(res.Content)
found := false
for _, s := range stop {
if i := strings.Index(res.Content, s); i != -1 && i < earliest {
earliest = i
found = true
}
}
if found {
res.Content = res.Content[:earliest]
}
}
if params.AfterReq != nil {
params.AfterReq()
}
// log.Printf("\n\n**\n\nModel response: %s\n\n**\n\n", res.Content)
var inputTokens int
var outputTokens int
var cachedTokens int
if res.Usage != nil {
if res.Usage.PromptTokensDetails != nil {
cachedTokens = res.Usage.PromptTokensDetails.CachedTokens
}
inputTokens = res.Usage.PromptTokens
outputTokens = res.Usage.CompletionTokens
} else {
inputTokens = inputTokensEstimate
outputTokens = shared.GetNumTokensEstimate(res.Content)
if params.WillCacheNumTokens > 0 {
cachedTokens = params.WillCacheNumTokens
}
}
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in DidSendModelRequest hook: %v\n%s", r, debug.Stack())
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in DidSendModelRequest hook: %v\n%s", r, debug.Stack()))
}
}()
_, apiErr := hooks.ExecHook(hooks.DidSendModelRequest, hooks.HookParams{
Auth: auth,
Plan: plan,
DidSendModelRequestParams: &hooks.DidSendModelRequestParams{
InputTokens: inputTokens,
OutputTokens: outputTokens,
CachedTokens: cachedTokens,
ModelId: baseModelConfig.ModelId,
ModelTag: baseModelConfig.ModelTag,
ModelName: baseModelConfig.ModelName,
ModelProvider: baseModelConfig.Provider,
ModelPackName: modelPackName,
ModelRole: modelConfig.Role,
Purpose: purpose,
GenerationId: res.GenerationId,
PlanId: plan.Id,
ModelStreamId: modelStreamId,
ConvoMessageId: convoMessageId,
BuildId: buildId,
RequestStartedAt: reqStarted,
Streaming: true,
Req: &req,
StreamResult: res.Content,
ModelConfig: modelConfig,
FirstTokenAt: res.FirstTokenAt,
SessionId: sessionId,
},
})
if apiErr != nil {
log.Printf("buildWholeFile - error executing DidSendModelRequest hook: %v", apiErr)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error executing DidSendModelRequest hook: %v", apiErr))
}
}()
return res, nil
}
func FilterEmptyMessages(messages []types.ExtendedChatMessage) []types.ExtendedChatMessage {
filteredMessages := []types.ExtendedChatMessage{}
for _, message := range messages {
var content []types.ExtendedChatMessagePart
for _, part := range message.Content {
if part.Type != openai.ChatMessagePartTypeText || part.Text != "" {
content = append(content, part)
}
}
if len(content) > 0 {
filteredMessages = append(filteredMessages, types.ExtendedChatMessage{
Role: message.Role,
Content: content,
})
}
}
return filteredMessages
}
func CheckSingleSystemMessage(modelConfig *shared.ModelRoleConfig, baseModelConfig *shared.BaseModelConfig, messages []types.ExtendedChatMessage) []types.ExtendedChatMessage {
if len(messages) == 1 && baseModelConfig.SingleMessageNoSystemPrompt {
if messages[0].Role == openai.ChatMessageRoleSystem {
msg := messages[0]
msg.Role = openai.ChatMessageRoleUser
return []types.ExtendedChatMessage{msg}
}
}
return messages
}
================================================
FILE: app/server/model/name.go
================================================
package model
import (
"context"
"encoding/json"
"fmt"
"plandex-server/db"
"plandex-server/model/prompts"
"plandex-server/types"
"plandex-server/utils"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
func GenPlanName(
auth *types.ServerAuth,
plan *db.Plan,
settings *shared.PlanSettings,
orgUserConfig *shared.OrgUserConfig,
clients map[string]ClientInfo,
authVars map[string]string,
planContent string,
sessionId string,
ctx context.Context,
) (string, error) {
config := settings.GetModelPack().Namer
var tools []openai.Tool
var toolChoice *openai.ToolChoice
baseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)
var sysPrompt string
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
sysPrompt = prompts.SysPlanNameXml
} else {
sysPrompt = prompts.SysPlanName
tools = []openai.Tool{
{
Type: "function",
Function: &prompts.PlanNameFn,
},
}
choice := openai.ToolChoice{
Type: "function",
Function: openai.ToolFunction{
Name: prompts.PlanNameFn.Name,
},
}
toolChoice = &choice
}
prompt := prompts.GetPlanNamePrompt(sysPrompt, planContent)
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompt,
},
},
},
}
modelRes, err := ModelRequest(ctx, ModelRequestParams{
Clients: clients,
AuthVars: authVars,
Auth: auth,
Plan: plan,
ModelConfig: &config,
OrgUserConfig: orgUserConfig,
Purpose: "Plan name",
Messages: messages,
Tools: tools,
ToolChoice: toolChoice,
SessionId: sessionId,
Settings: settings,
})
if err != nil {
fmt.Printf("Error during plan name model call: %v\n", err)
return "", err
}
var planName string
content := modelRes.Content
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
planName = utils.GetXMLContent(content, "planName")
if planName == "" {
return "", fmt.Errorf("No planName tag found in XML response")
}
} else {
if content == "" {
fmt.Println("no namePlan function call found in response")
return "", fmt.Errorf("No namePlan function call found in response. The model failed to generate a valid response.")
}
var nameRes prompts.PlanNameRes
err = json.Unmarshal([]byte(content), &nameRes)
if err != nil {
fmt.Printf("Error unmarshalling plan description response: %v\n", err)
return "", err
}
planName = nameRes.PlanName
}
return planName, nil
}
type GenPipedDataNameParams struct {
Ctx context.Context
Auth *types.ServerAuth
Plan *db.Plan
Settings *shared.PlanSettings
OrgUserConfig *shared.OrgUserConfig
AuthVars map[string]string
SessionId string
Clients map[string]ClientInfo
PipedContent string
}
func GenPipedDataName(
params GenPipedDataNameParams,
) (string, error) {
ctx := params.Ctx
auth := params.Auth
plan := params.Plan
settings := params.Settings
clients := params.Clients
authVars := params.AuthVars
pipedContent := params.PipedContent
sessionId := params.SessionId
orgUserConfig := params.OrgUserConfig
config := settings.GetModelPack().Namer
var sysPrompt string
var tools []openai.Tool
var toolChoice *openai.ToolChoice
baseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
sysPrompt = prompts.SysPipedDataNameXml
} else {
sysPrompt = prompts.SysPipedDataName
tools = []openai.Tool{
{
Type: "function",
Function: &prompts.PipedDataNameFn,
},
}
choice := openai.ToolChoice{
Type: "function",
Function: openai.ToolFunction{
Name: prompts.PipedDataNameFn.Name,
},
}
toolChoice = &choice
}
prompt := prompts.GetPipedDataNamePrompt(sysPrompt, pipedContent)
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompt,
},
},
},
}
modelRes, err := ModelRequest(ctx, ModelRequestParams{
Clients: clients,
Auth: auth,
AuthVars: authVars,
Plan: plan,
ModelConfig: &config,
Purpose: "Piped data name",
Messages: messages,
Tools: tools,
ToolChoice: toolChoice,
SessionId: sessionId,
Settings: settings,
OrgUserConfig: orgUserConfig,
})
if err != nil {
fmt.Printf("Error during piped data name model call: %v\n", err)
return "", err
}
var name string
content := modelRes.Content
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
name = utils.GetXMLContent(content, "name")
if name == "" {
return "", fmt.Errorf("No name tag found in XML response")
}
} else {
if content == "" {
fmt.Println("no namePipedData function call found in response")
return "", fmt.Errorf("No namePipedData function call found in response. The model failed to generate a valid response.")
}
var nameRes prompts.PipedDataNameRes
err = json.Unmarshal([]byte(content), &nameRes)
if err != nil {
fmt.Printf("Error unmarshalling piped data name response: %v\n", err)
return "", err
}
name = nameRes.Name
}
return name, nil
}
func GenNoteName(
ctx context.Context,
auth *types.ServerAuth,
plan *db.Plan,
settings *shared.PlanSettings,
orgUserConfig *shared.OrgUserConfig,
clients map[string]ClientInfo,
authVars map[string]string,
note string,
sessionId string,
) (string, error) {
config := settings.GetModelPack().Namer
var sysPrompt string
var tools []openai.Tool
var toolChoice *openai.ToolChoice
baseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
sysPrompt = prompts.SysNoteNameXml
} else {
sysPrompt = prompts.SysNoteName
tools = []openai.Tool{
{
Type: "function",
Function: &prompts.NoteNameFn,
},
}
choice := openai.ToolChoice{
Type: "function",
Function: openai.ToolFunction{
Name: prompts.NoteNameFn.Name,
},
}
toolChoice = &choice
}
prompt := prompts.GetNoteNamePrompt(sysPrompt, note)
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompt,
},
},
},
}
modelRes, err := ModelRequest(ctx, ModelRequestParams{
Clients: clients,
Auth: auth,
AuthVars: authVars,
Plan: plan,
ModelConfig: &config,
Purpose: "Note name",
Messages: messages,
Tools: tools,
ToolChoice: toolChoice,
SessionId: sessionId,
Settings: settings,
OrgUserConfig: orgUserConfig,
})
if err != nil {
fmt.Printf("Error during note name model call: %v\n", err)
return "", err
}
var name string
content := modelRes.Content
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
name = utils.GetXMLContent(content, "name")
if name == "" {
return "", fmt.Errorf("No name tag found in XML response")
}
} else {
if content == "" {
fmt.Println("no nameNote function call found in response")
return "", fmt.Errorf("No nameNote function call found in response. The model failed to generate a valid response.")
}
var nameRes prompts.NoteNameRes
err = json.Unmarshal([]byte(content), &nameRes)
if err != nil {
fmt.Printf("Error unmarshalling note name response: %v\n", err)
return "", err
}
name = nameRes.Name
}
return name, nil
}
================================================
FILE: app/server/model/parse/subtasks.go
================================================
package parse
import (
"log"
"plandex-server/db"
"regexp"
"strings"
)
func ParseSubtasks(replyContent string) []*db.Subtask {
split := strings.Split(replyContent, "### Tasks")
if len(split) < 2 {
split = strings.Split(replyContent, "### Task")
if len(split) < 2 {
log.Println("[Subtasks] No tasks section found in reply")
return nil
}
}
lines := strings.Split(split[1], "\n")
var subtasks []*db.Subtask
var currentTask *db.Subtask
var descLines []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Check for any number followed by a period and space
if matched, _ := regexp.MatchString(`^\d+\.\s`, line); matched {
// Save previous task if exists
if currentTask != nil {
currentTask.Description = strings.Join(descLines, "\n")
log.Printf("[Subtasks] Adding subtask: %q with %d uses files", currentTask.Title, len(currentTask.UsesFiles))
subtasks = append(subtasks, currentTask)
}
// Start new task
parts := strings.SplitN(line, ". ", 2)
if len(parts) == 2 {
title := parts[1]
currentTask = &db.Subtask{
Title: title,
}
descLines = nil
}
continue
}
// Handle Uses: section
if strings.HasPrefix(line, "Uses:") {
if currentTask != nil {
usesStr := strings.TrimPrefix(line, "Uses:")
for _, use := range strings.Split(usesStr, ",") {
use = strings.TrimSpace(use)
use = strings.Trim(use, "`")
if use != "" {
currentTask.UsesFiles = append(currentTask.UsesFiles, use)
}
}
log.Printf("[Subtasks] Added uses files for %q: %v", currentTask.Title, currentTask.UsesFiles)
}
continue
}
// Add to description if we have a current task
if currentTask != nil {
// Remove bullet point if present, but don't require it
line = strings.TrimPrefix(line, "-")
line = strings.TrimSpace(line)
if line != "" {
descLines = append(descLines, line)
}
}
}
// Save final task if exists
if currentTask != nil {
currentTask.Description = strings.Join(descLines, "\n")
log.Printf("[Subtasks] Adding final subtask: %q with %d uses files", currentTask.Title, len(currentTask.UsesFiles))
subtasks = append(subtasks, currentTask)
}
log.Printf("[Subtasks] Parsed %d total subtasks", len(subtasks))
return subtasks
}
func ParseRemoveSubtasks(replyContent string) []string {
split := strings.Split(replyContent, "### Remove Tasks")
if len(split) < 2 {
return nil
}
section := split[1]
lines := strings.Split(section, "\n")
var tasksToRemove []string
sawEmptyLine := false
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
sawEmptyLine = true
continue
}
if sawEmptyLine && !strings.HasPrefix(line, "-") {
break
}
if strings.HasPrefix(line, "- ") {
title := strings.TrimPrefix(line, "- ")
title = strings.TrimSpace(title)
if title != "" {
tasksToRemove = append(tasksToRemove, title)
}
}
}
return tasksToRemove
}
================================================
FILE: app/server/model/parse/subtasks_test.go
================================================
package parse
import (
"plandex-server/db"
"testing"
)
func TestParseSubtasks(t *testing.T) {
tests := []struct {
name string
input string
expected []*db.Subtask
}{
{
name: "empty input",
input: "",
expected: nil,
},
{
name: "single task without description",
input: `### Tasks
1. Create a new file`,
expected: []*db.Subtask{
{
Title: "Create a new file",
Description: "",
UsesFiles: nil,
},
},
},
{
name: "multiple tasks with descriptions and uses",
input: `### Tasks
1. Create config file
- Will store application settings
- Contains environment variables
Uses: ` + "`config/settings.yml`" + `, ` + "`config/defaults.yml`" + `
2. Update main function
- Add configuration loading
Uses: ` + "`main.go`",
expected: []*db.Subtask{
{
Title: "Create config file",
Description: "Will store application settings\nContains environment variables",
UsesFiles: []string{"config/settings.yml", "config/defaults.yml"},
},
{
Title: "Update main function",
Description: "Add configuration loading",
UsesFiles: []string{"main.go"},
},
},
},
{
name: "alternative task header",
input: `### Task
1. Simple task`,
expected: []*db.Subtask{
{
Title: "Simple task",
Description: "",
UsesFiles: nil,
},
},
},
{
name: "tasks with empty lines between",
input: `### Tasks
1. First task
- Description one
2. Second task
- Description two`,
expected: []*db.Subtask{
{
Title: "First task",
Description: "Description one",
UsesFiles: nil,
},
{
Title: "Second task",
Description: "Description two",
UsesFiles: nil,
},
},
},
{
name: "single task from pong",
input: "### Tasks" + `
9. Update Makefile to include Homebrew-specific include and library search paths
- Modify CFLAGS in Makefile to add -I/opt/homebrew/include
- Modify LDFLAGS in Makefile to add -L/opt/homebrew/lib
Uses: ` + "`Makefile`" + `, ` + "`_apply.sh`",
expected: []*db.Subtask{
{
Title: "Update Makefile to include Homebrew-specific include and library search paths",
Description: "Modify CFLAGS in Makefile to add -I/opt/homebrew/include\nModify LDFLAGS in Makefile to add -L/opt/homebrew/lib",
UsesFiles: []string{"Makefile", "_apply.sh"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseSubtasks(tt.input)
if len(got) != len(tt.expected) {
t.Errorf("ParseSubtasks() returned %d subtasks, want %d", len(got), len(tt.expected))
return
}
for i := range got {
if got[i].Title != tt.expected[i].Title {
t.Errorf("Subtask[%d].Title = %q, want %q", i, got[i].Title, tt.expected[i].Title)
}
if got[i].Description != tt.expected[i].Description {
t.Errorf("Subtask[%d].Description = %q, want %q", i, got[i].Description, tt.expected[i].Description)
}
if !sliceEqual(got[i].UsesFiles, tt.expected[i].UsesFiles) {
t.Errorf("Subtask[%d].UsesFiles = %v, want %v", i, got[i].UsesFiles, tt.expected[i].UsesFiles)
}
}
})
}
}
// sliceEqual compares two string slices for equality
func sliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
================================================
FILE: app/server/model/plan/activate.go
================================================
package plan
import (
"fmt"
"log"
"plandex-server/db"
"plandex-server/host"
"plandex-server/model"
"plandex-server/types"
"time"
shared "plandex-shared"
)
func activatePlan(
clients map[string]model.ClientInfo,
plan *db.Plan,
branch string,
auth *types.ServerAuth,
prompt string,
buildOnly,
autoContext bool,
sessionId string,
) (*types.ActivePlan, error) {
log.Printf("Activate plan: plan ID %s on branch %s\n", plan.Id, branch)
// Just in case this request was made immediately after another stream finished, wait a little to allow for cleanup
log.Println("Waiting 100ms before checking for active plan")
time.Sleep(100 * time.Millisecond)
log.Println("Done waiting, checking for active plan")
active := GetActivePlan(plan.Id, branch)
if active != nil {
log.Printf("Tell: Active plan found for plan ID %s on branch %s\n", plan.Id, branch) // Log if an active plan is found
return nil, fmt.Errorf("plan %s branch %s already has an active stream on this host", plan.Id, branch)
}
modelStream, err := db.GetActiveModelStream(plan.Id, branch)
if err != nil {
log.Printf("Error getting active model stream: %v\n", err)
return nil, fmt.Errorf("error getting active model stream: %v", err)
}
if modelStream != nil {
log.Printf("Tell: Active model stream found for plan ID %s on branch %s on host %s\n", plan.Id, branch, modelStream.InternalIp) // Log if an active model stream is found
return nil, fmt.Errorf("plan %s branch %s already has an active stream on host %s", plan.Id, branch, modelStream.InternalIp)
}
active = CreateActivePlan(
auth.OrgId,
auth.User.Id,
plan.Id,
branch,
prompt,
buildOnly,
autoContext,
sessionId,
)
modelStream = &db.ModelStream{
OrgId: auth.OrgId,
PlanId: plan.Id,
InternalIp: host.Ip,
Branch: branch,
}
err = db.StoreModelStream(modelStream, active.Ctx, active.CancelFn)
if err != nil {
log.Printf("Tell: Error storing model stream for plan ID %s on branch %s: %v\n", plan.Id, branch, err) // Log error storing model stream
log.Printf("Error storing model stream: %v\n", err)
log.Printf("Tell: Error storing model stream: %v\n", err) // Log error storing model stream
active.StreamDoneCh <- &shared.ApiError{Msg: fmt.Sprintf("Error storing model stream: %v", err)}
return nil, fmt.Errorf("error storing model stream: %v", err)
}
active.ModelStreamId = modelStream.Id
log.Printf("Tell: Model stream stored with ID %s for plan ID %s on branch %s\n", modelStream.Id, plan.Id, branch) // Log successful storage of model stream
log.Println("Model stream id:", modelStream.Id)
return active, nil
}
================================================
FILE: app/server/model/plan/build_exec.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"path/filepath"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/model"
"plandex-server/notify"
"plandex-server/types"
"runtime/debug"
"time"
shared "plandex-shared"
)
type BuildParams struct {
Clients map[string]model.ClientInfo
AuthVars map[string]string
Plan *db.Plan
Branch string
Auth *types.ServerAuth
SessionId string
OrgUserConfig *shared.OrgUserConfig
Settings *shared.PlanSettings
}
func Build(params BuildParams) (int, error) {
clients := params.Clients
authVars := params.AuthVars
plan := params.Plan
branch := params.Branch
auth := params.Auth
sessionId := params.SessionId
orgUserConfig := params.OrgUserConfig
settings := params.Settings
log.Printf("Build: Called with plan ID %s on branch %s\n", plan.Id, branch)
log.Println("Build: Starting Build operation")
state := activeBuildStreamState{
clients: clients,
authVars: authVars,
auth: auth,
currentOrgId: auth.OrgId,
currentUserId: auth.User.Id,
orgUserConfig: orgUserConfig,
plan: plan,
branch: branch,
settings: settings,
}
streamDone := func() {
active := GetActivePlan(plan.Id, branch)
if active != nil {
active.StreamDoneCh <- nil
}
}
onErr := func(err error) (int, error) {
log.Printf("Build error: %v\n", err)
streamDone()
return 0, err
}
pendingBuildsByPath, err := state.loadPendingBuilds(sessionId)
if err != nil {
return onErr(err)
}
if len(pendingBuildsByPath) == 0 {
log.Println("No pending builds")
streamDone()
return 0, nil
}
err = db.SetPlanStatus(plan.Id, branch, shared.PlanStatusBuilding, "")
if err != nil {
log.Printf("Error setting plan status to building: %v\n", err)
return onErr(fmt.Errorf("error setting plan status to building: %v", err))
}
log.Printf("Starting %d builds\n", len(pendingBuildsByPath))
for _, pendingBuilds := range pendingBuildsByPath {
go state.queueBuilds(pendingBuilds)
}
return len(pendingBuildsByPath), nil
}
func (state *activeBuildStreamState) queueBuild(activeBuild *types.ActiveBuild) {
planId := state.plan.Id
branch := state.branch
filePath := activeBuild.Path
// log.Printf("Queue:")
// spew.Dump(activePlan.BuildQueuesByPath[filePath])
var isBuilding bool
UpdateActivePlan(planId, branch, func(active *types.ActivePlan) {
active.BuildQueuesByPath[filePath] = append(active.BuildQueuesByPath[filePath], activeBuild)
isBuilding = active.IsBuildingByPath[filePath]
})
log.Printf("Queued build for file %s\n", filePath)
if isBuilding {
log.Printf("Already building file %s\n", filePath)
return
} else {
log.Printf("Not building file %s\n", filePath)
active := GetActivePlan(planId, branch)
if active == nil {
log.Printf("Active plan not found for plan ID %s and branch %s\n", planId, branch)
return
}
UpdateActivePlan(planId, branch, func(active *types.ActivePlan) {
active.IsBuildingByPath[filePath] = true
})
go state.execPlanBuild(activeBuild)
}
}
func (state *activeBuildStreamState) queueBuilds(activeBuilds []*types.ActiveBuild) {
log.Printf("Queueing %d builds\n", len(activeBuilds))
for _, activeBuild := range activeBuilds {
state.queueBuild(activeBuild)
}
}
func (buildState *activeBuildStreamState) execPlanBuild(activeBuild *types.ActiveBuild) {
if activeBuild == nil {
log.Println("No active build")
return
}
log.Printf("execPlanBuild - %s\n", activeBuild.Path)
// log.Println(spew.Sdump(activeBuild))
planId := buildState.plan.Id
branch := buildState.branch
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Printf("Active plan not found for plan ID %s and branch %s\n", planId, branch)
return
}
defer func() {
if r := recover(); r != nil {
log.Printf("execPlanBuild: Panic: %v\n%s\n", r, string(debug.Stack()))
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("execPlanBuild: Panic: %v\n%s", r, string(debug.Stack())))
activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Panic in execPlanBuild",
}
}
}()
filePath := activeBuild.Path
if !activePlan.IsBuildingByPath[filePath] {
UpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {
ap.IsBuildingByPath[filePath] = true
})
}
fileState := &activeBuildStreamFileState{
activeBuildStreamState: buildState,
filePath: filePath,
activeBuild: activeBuild,
builderRun: hooks.DidFinishBuilderRunParams{
StartedAt: time.Now(),
PlanId: activePlan.Id,
FilePath: filePath,
FileExt: filepath.Ext(filePath),
},
}
log.Printf("execPlanBuild - %s - calling fileState.loadBuildFile()\n", filePath)
err := fileState.loadBuildFile(activeBuild)
if err != nil {
log.Printf("Error loading build file: %v\n", err)
fileState.onBuildFileError(fmt.Errorf("error loading build file: %v", err))
return
}
fileState.resolvePreBuildState()
// unless it's a file operation, stream initial status to client
if !activeBuild.IsFileOperation() && !fileState.isNewFile {
log.Printf("execPlanBuild - %s - streaming initial build info\n", filePath)
// spew.Dump(activeBuild)
buildInfo := &shared.BuildInfo{
Path: filePath,
NumTokens: 0,
Finished: false,
}
activePlan.Stream(shared.StreamMessage{
Type: shared.StreamMessageBuildInfo,
BuildInfo: buildInfo,
})
} else if activeBuild.IsFileOperation() {
log.Printf("execPlanBuild - %s - file operation - won't stream initial build info\n", filePath)
} else if fileState.isNewFile {
log.Printf("execPlanBuild - %s - new file - won't stream initial build info\n", filePath)
}
log.Printf("execPlanBuild - %s - calling fileState.buildFile()\n", filePath)
fileState.buildFile()
}
func (fileState *activeBuildStreamFileState) buildFile() {
filePath := fileState.filePath
activeBuild := fileState.activeBuild
planId := fileState.plan.Id
branch := fileState.branch
currentOrgId := fileState.currentOrgId
build := fileState.build
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Printf("Active plan not found for plan ID %s and branch %s\n", planId, branch)
return
}
log.Printf("Building file %s\n", filePath)
log.Printf("%d files in context\n", len(activePlan.ContextsByPath))
// log.Println("activePlan.ContextsByPath files:")
// for k := range activePlan.ContextsByPath {
// log.Println(k)
// }
if activeBuild.IsMoveOp {
log.Printf("File %s is a move operation. Moving to %s\n", filePath, activeBuild.MoveDestination)
// For move operations, we split it into two separate builds:
// 1. A removal build for the source file
// 2. A creation build for the destination file with the current content
// This is simpler than handling moves in a single build since our build system
// is designed around operating on one path at a time
fileState.activeBuildStreamState.queueBuilds([]*types.ActiveBuild{
{
ReplyId: activeBuild.ReplyId,
Path: activeBuild.Path,
IsRemoveOp: true,
},
{
ReplyId: activeBuild.ReplyId,
Path: activeBuild.MoveDestination,
FileContent: fileState.preBuildState,
FileContentTokens: 0,
},
})
// Mark this move operation as successful since we've queued the actual work
activeBuild.Success = true
UpdateActivePlan(planId, branch, func(active *types.ActivePlan) {
active.IsBuildingByPath[filePath] = false
active.BuiltFiles[filePath] = true
})
// Process the next build in queue (which will be our removal build)
// We need to explicitly advance the queue for the source path since this
// current build is holding the 'building' state open
// The create build for the destination will be handled automatically by the queue logic
fileState.buildNextInQueue()
return
}
if activeBuild.IsRemoveOp {
log.Printf("File %s is a remove operation. Removing file.\n", filePath)
log.Printf("streaming remove build info for file %s\n", filePath)
buildInfo := &shared.BuildInfo{
Path: filePath,
NumTokens: 0,
Removed: true,
Finished: true,
}
activePlan.Stream(shared.StreamMessage{
Type: shared.StreamMessageBuildInfo,
BuildInfo: buildInfo,
})
planRes := &db.PlanFileResult{
OrgId: currentOrgId,
PlanId: planId,
PlanBuildId: build.Id,
ConvoMessageId: build.ConvoMessageId,
Path: filePath,
Content: "",
RemovedFile: true,
}
fileState.onFinishBuildFile(planRes)
return
}
if activeBuild.IsResetOp {
log.Printf("File %s is a reset operation. Resetting file.\n", filePath)
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: currentOrgId,
UserId: fileState.currentUserId,
PlanId: planId,
Branch: branch,
PlanBuildId: build.Id,
Scope: db.LockScopeWrite,
Reason: "reset file op",
Ctx: activePlan.Ctx,
CancelFn: activePlan.CancelFn,
}, func(repo *db.GitRepo) error {
now := time.Now()
return db.RejectPlanFile(currentOrgId, planId, filePath, now)
})
if err != nil {
log.Printf("Error rejecting plan file: %v\n", err)
fileState.onBuildFileError(fmt.Errorf("error rejecting plan file: %v", err))
return
}
buildInfo := &shared.BuildInfo{
Path: filePath,
NumTokens: 0,
Finished: true,
Removed: fileState.contextPart == nil,
}
activePlan.Stream(shared.StreamMessage{
Type: shared.StreamMessageBuildInfo,
BuildInfo: buildInfo,
})
time.Sleep(200 * time.Millisecond)
fileState.onBuildProcessed(activeBuild)
return
}
if fileState.preBuildState == "" {
log.Printf("File %s not found in model context or current plan. Creating new file.\n", filePath)
buildInfo := &shared.BuildInfo{
Path: filePath,
NumTokens: 0,
Finished: true,
}
log.Printf("streaming new file build info for file %s\n", filePath)
activePlan.Stream(shared.StreamMessage{
Type: shared.StreamMessageBuildInfo,
BuildInfo: buildInfo,
})
// new file
planRes := &db.PlanFileResult{
OrgId: currentOrgId,
PlanId: planId,
PlanBuildId: build.Id,
ConvoMessageId: build.ConvoMessageId,
Path: filePath,
Content: activeBuild.FileContent,
}
// log.Println("build exec - new file result")
// spew.Dump(planRes)
fileState.onFinishBuildFile(planRes)
return
} else {
currentNumTokens := shared.GetNumTokensEstimate(fileState.preBuildState)
log.Printf("Current state num tokens: %d\n", currentNumTokens)
activeBuild.CurrentFileTokens = currentNumTokens
activePlan.DidEditFiles = true
}
// build structured edits strategy now works regardless of language/tree-sitter support
log.Println("buildFile - building structured edits")
fileState.buildStructuredEdits()
}
func (fileState *activeBuildStreamFileState) resolvePreBuildState() {
filePath := fileState.filePath
currentPlan := fileState.currentPlanState
planId := fileState.plan.Id
branch := fileState.branch
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Printf("Active plan not found for plan ID %s and branch %s\n", planId, branch)
return
}
contextPart := activePlan.ContextsByPath[filePath]
var currentState string
currentPlanFile, fileInCurrentPlan := currentPlan.CurrentPlanFiles.Files[filePath]
// log.Println("plan files:")
// spew.Dump(currentPlan.CurrentPlanFiles.Files)
if fileInCurrentPlan {
log.Printf("File %s found in current plan.\n", filePath)
fileState.isNewFile = false
currentState = currentPlanFile
// log.Println("\n\nCurrent state:\n", currentState, "\n\n")
} else if contextPart != nil {
log.Printf("File %s found in model context. Using context state.\n", filePath)
fileState.isNewFile = false
currentState = contextPart.Body
// log.Println("\n\nCurrent state:\n", currentState, "\n\n")
} else {
fileState.isNewFile = true
}
fileState.preBuildState = currentState
fileState.contextPart = contextPart
}
================================================
FILE: app/server/model/plan/build_finish.go
================================================
package plan
import (
"context"
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/notify"
"plandex-server/types"
"strings"
"time"
shared "plandex-shared"
)
func (state *activeBuildStreamFileState) onFinishBuild() {
log.Println("Build finished")
planId := state.plan.Id
branch := state.branch
currentOrgId := state.currentOrgId
currentUserId := state.currentUserId
convoMessageId := state.convoMessageId
build := state.build
// first check if any of the messages we're building hasen't finished streaming yet
stillStreaming := false
var doneCh chan bool
ap := GetActivePlan(planId, branch)
if ap == nil {
log.Println("onFinishBuild - Active plan not found")
return
}
if ap.CurrentStreamingReplyId == convoMessageId {
stillStreaming = true
doneCh = ap.CurrentReplyDoneCh
}
if stillStreaming {
log.Println("Reply is still streaming, waiting for it to finish before finishing build")
<-doneCh
}
// Check again if build is finished
// (more builds could have been queued while we were waiting for the reply to finish streaming)
ap = GetActivePlan(planId, branch)
if ap == nil {
log.Println("onFinishBuild - Active plan not found")
return
}
if !ap.BuildFinished() {
log.Println("Build not finished after waiting for reply to finish streaming")
return
}
log.Println("Locking repo for finished build")
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: currentOrgId,
UserId: currentUserId,
PlanId: planId,
Branch: branch,
PlanBuildId: build.Id,
Scope: db.LockScopeWrite,
Ctx: ap.Ctx,
CancelFn: ap.CancelFn,
Reason: "finish build",
}, func(repo *db.GitRepo) error {
// get plan descriptions
var planDescs []*db.ConvoMessageDescription
planDescs, err := db.GetConvoMessageDescriptions(currentOrgId, planId)
if err != nil {
log.Printf("Error getting pending build descriptions: %v\n", err)
return fmt.Errorf("error getting pending build descriptions: %v", err)
}
var unbuiltDescs []*db.ConvoMessageDescription
for _, desc := range planDescs {
if !desc.DidBuild || len(desc.BuildPathsInvalidated) > 0 {
unbuiltDescs = append(unbuiltDescs, desc)
}
}
// get fresh current plan state
var currentPlan *shared.CurrentPlanState
currentPlan, err = db.GetCurrentPlanState(db.CurrentPlanStateParams{
OrgId: currentOrgId,
PlanId: planId,
ConvoMessageDescriptions: planDescs,
})
if err != nil {
log.Printf("Error getting current plan state: %v\n", err)
return fmt.Errorf("error getting current plan state: %v", err)
}
descErrCh := make(chan error, len(unbuiltDescs))
for _, desc := range unbuiltDescs {
if len(desc.Operations) > 0 {
desc.DidBuild = true
desc.BuildPathsInvalidated = map[string]bool{}
}
go func(desc *db.ConvoMessageDescription) {
err := db.StoreDescription(desc)
if err != nil {
descErrCh <- fmt.Errorf("error storing description: %v", err)
return
}
descErrCh <- nil
}(desc)
}
for range unbuiltDescs {
err = <-descErrCh
if err != nil {
log.Printf("Error storing description: %v\n", err)
return err
}
}
err = repo.GitAddAndCommit(branch, currentPlan.PendingChangesSummaryForBuild())
if err != nil {
if strings.Contains(err.Error(), "nothing to commit") {
log.Println("Nothing to commit")
return nil
}
return fmt.Errorf("error committing plan build: %v", err)
}
log.Println("Plan build committed")
return nil
})
if err != nil {
log.Printf("Error finishing build: %v\n", err)
if err.Error() != context.Canceled.Error() {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error finishing build: %v", err))
ap.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error finishing build: " + err.Error(),
}
}
return
}
active := GetActivePlan(planId, branch)
if active != nil && (active.RepliesFinished || active.BuildOnly) {
active.Finish()
}
}
func (fileState *activeBuildStreamFileState) onFinishBuildFile(planRes *db.PlanFileResult) {
planId := fileState.plan.Id
branch := fileState.branch
currentOrgId := fileState.currentOrgId
build := fileState.build
activeBuild := fileState.activeBuild
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Println("onFinishBuildFile - Active plan not found")
return
}
filePath := fileState.filePath
log.Printf("onFinishBuildFile: %s\n", filePath)
if planRes == nil {
log.Println("onFinishBuildFile - planRes is nil")
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("onFinishBuildFile: planRes is nil"))
activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error storing plan result: planRes is nil",
}
return
}
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: currentOrgId,
UserId: fileState.currentUserId,
PlanId: planId,
Branch: branch,
PlanBuildId: build.Id,
Scope: db.LockScopeWrite,
Ctx: activePlan.Ctx,
CancelFn: activePlan.CancelFn,
Reason: "store plan result",
}, func(repo *db.GitRepo) error {
log.Println("Storing plan result", planRes.Path)
err := db.StorePlanResult(planRes)
if err != nil {
log.Printf("Error storing plan result: %v\n", err)
return err
}
return nil
})
if err != nil {
log.Printf("Error storing plan build result: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error storing plan build result: %v", err))
activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error storing plan build result: " + err.Error(),
}
return
}
fileState.builderRun.FinishedAt = time.Now()
hooks.ExecHook(hooks.DidFinishBuilderRun, hooks.HookParams{
Auth: fileState.auth,
Plan: fileState.plan,
DidFinishBuilderRunParams: &fileState.builderRun,
})
log.Printf("Finished building file %s - setting activeBuild.Success to true\n", filePath)
// log.Println(spew.Sdump(activeBuild))
fileState.onBuildProcessed(activeBuild)
}
func (fileState *activeBuildStreamFileState) onBuildProcessed(activeBuild *types.ActiveBuild) {
filePath := fileState.filePath
planId := fileState.plan.Id
branch := fileState.branch
activeBuild.Success = true
stillBuildingPath := fileState.buildNextInQueue()
if stillBuildingPath {
return
}
log.Printf("No more builds for path %s, checking if entire build is finished\n", filePath)
buildFinished := false
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.BuiltFiles[filePath] = true
ap.IsBuildingByPath[filePath] = false
if ap.BuildFinished() {
buildFinished = true
}
})
log.Printf("Finished building file %s\n", filePath)
if buildFinished {
log.Println("Finished building plan, calling onFinishBuild")
fileState.onFinishBuild()
} else {
log.Println("Finished building file, but plan is not finished")
}
}
func (fileState *activeBuildStreamFileState) onBuildFileError(err error) {
planId := fileState.plan.Id
branch := fileState.branch
filePath := fileState.filePath
build := fileState.build
activeBuild := fileState.activeBuild
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Println("onBuildFileError - Active plan not found")
return
}
log.Printf("Error for file %s: %v\n", filePath, err)
activeBuild.Success = false
activeBuild.Error = err
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error for file %s: %v", filePath, err))
activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: err.Error(),
}
if err != nil {
log.Printf("Error storing plan error result: %v\n", err)
}
build.Error = err.Error()
err = db.SetBuildError(build)
if err != nil {
log.Printf("Error setting build error: %v\n", err)
}
}
func (fileState *activeBuildStreamFileState) buildNextInQueue() bool {
filePath := fileState.filePath
activePlan := GetActivePlan(fileState.plan.Id, fileState.branch)
if activePlan == nil {
log.Println("onFinishBuildFile - Active plan not found")
return false
}
// if more builds are queued, start the next one
if !activePlan.PathQueueEmpty(filePath) {
log.Printf("Processing next build for file %s\n", filePath)
queue := activePlan.BuildQueuesByPath[filePath]
var nextBuild *types.ActiveBuild
for _, build := range queue {
if !build.BuildFinished() {
nextBuild = build
break
}
}
if nextBuild != nil {
log.Println("Calling execPlanBuild for next build in queue")
go fileState.execPlanBuild(nextBuild)
}
return true
}
return false
}
================================================
FILE: app/server/model/plan/build_load.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/notify"
"plandex-server/syntax"
"plandex-server/types"
"runtime"
"runtime/debug"
shared "plandex-shared"
)
func (state *activeBuildStreamState) loadPendingBuilds(sessionId string) (map[string][]*types.ActiveBuild, error) {
clients := state.clients
plan := state.plan
branch := state.branch
auth := state.auth
active, err := activatePlan(clients, plan, branch, auth, "", true, false, sessionId)
if err != nil {
log.Printf("Error activating plan: %v\n", err)
}
modelStreamId := active.ModelStreamId
state.modelStreamId = modelStreamId
var modelContext []*db.Context
var pendingBuildsByPath map[string][]*types.ActiveBuild
var settings *shared.PlanSettings
var orgUserConfig *shared.OrgUserConfig
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: plan.Id,
Branch: branch,
Scope: db.LockScopeRead,
Ctx: active.Ctx,
CancelFn: active.CancelFn,
Reason: "load pending builds",
}, func(repo *db.GitRepo) error {
errCh := make(chan error, 4)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanContexts: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting plan modelContext: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := db.GetPlanContexts(auth.OrgId, plan.Id, true, false)
if err != nil {
log.Printf("Error getting plan modelContext: %v\n", err)
errCh <- fmt.Errorf("error getting plan modelContext: %v", err)
return
}
modelContext = res
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanSettings: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting plan settings: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := active.PendingBuildsByPath(auth.OrgId, auth.User.Id, nil)
if err != nil {
log.Printf("Error getting pending builds by path: %v\n", err)
errCh <- fmt.Errorf("error getting pending builds by path: %v", err)
return
}
pendingBuildsByPath = res
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanSettings: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting plan settings: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := db.GetPlanSettings(plan)
if err != nil {
log.Printf("Error getting plan settings: %v\n", err)
errCh <- fmt.Errorf("error getting plan settings: %v", err)
return
}
settings = res
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getOrgUserConfig: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting org user config: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("Error getting org user config: %v\n", err)
errCh <- fmt.Errorf("error getting org user config: %v", err)
return
}
orgUserConfig = res
errCh <- nil
}()
for i := 0; i < 4; i++ {
err = <-errCh
if err != nil {
log.Printf("Error getting plan data: %v\n", err)
return err
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error getting plan data: %v", err)
}
UpdateActivePlan(plan.Id, branch, func(ap *types.ActivePlan) {
ap.Contexts = modelContext
for _, context := range modelContext {
if context.FilePath != "" {
ap.ContextsByPath[context.FilePath] = context
}
}
})
state.modelContext = modelContext
state.settings = settings
state.orgUserConfig = orgUserConfig
return pendingBuildsByPath, nil
}
func (state *activeBuildStreamFileState) loadBuildFile(activeBuild *types.ActiveBuild) error {
currentOrgId := state.currentOrgId
planId := state.plan.Id
branch := state.branch
filePath := state.filePath
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
return fmt.Errorf("active plan not found")
}
convoMessageId := activeBuild.ReplyId
parser, lang, fallbackParser, fallbackLang := syntax.GetParserForPath(filePath)
if parser != nil {
validationRes, err := syntax.ValidateWithParsers(activePlan.Ctx, lang, parser, fallbackLang, fallbackParser, state.preBuildState)
if err != nil {
log.Printf(" error validating original file syntax: %v\n", err)
return fmt.Errorf("error validating original file syntax: %v", err)
}
state.language = validationRes.Lang
state.parser = validationRes.Parser
state.builderRun.Lang = string(validationRes.Lang)
if validationRes.TimedOut {
state.syntaxCheckTimedOut = true
} else if !validationRes.Valid {
state.preBuildStateSyntaxInvalid = true
}
}
build := &db.PlanBuild{
OrgId: currentOrgId,
PlanId: planId,
ConvoMessageId: convoMessageId,
FilePath: filePath,
}
err := db.StorePlanBuild(build)
if err != nil {
log.Printf("Error storing plan build: %v\n", err)
UpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {
ap.IsBuildingByPath[filePath] = false
})
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error storing plan build: %v", err))
activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error storing plan build: " + err.Error(),
}
return err
}
var currentPlan *shared.CurrentPlanState
var convo []*db.ConvoMessage
log.Println("Locking repo for load build file")
err = db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: currentOrgId,
UserId: state.activeBuildStreamState.currentUserId,
PlanId: planId,
Branch: branch,
PlanBuildId: build.Id,
Scope: db.LockScopeRead,
Ctx: activePlan.Ctx,
CancelFn: activePlan.CancelFn,
Reason: "load build file",
}, func(repo *db.GitRepo) error {
errCh := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getCurrentPlanState: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting current plan state: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
log.Println("loadBuildFile - Getting current plan state")
res, err := db.GetCurrentPlanState(db.CurrentPlanStateParams{
OrgId: currentOrgId,
PlanId: planId,
})
if err != nil {
log.Printf("Error getting current plan state: %v\n", err)
UpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {
ap.IsBuildingByPath[filePath] = false
})
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error getting current plan state: %v", err))
activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error getting current plan state: " + err.Error(),
}
errCh <- fmt.Errorf("error getting current plan state: %v", err)
return
}
currentPlan = res
log.Println("Got current plan state")
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanConvo: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting plan convo: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := db.GetPlanConvo(currentOrgId, planId)
if err != nil {
log.Printf("Error getting plan convo: %v\n", err)
errCh <- fmt.Errorf("error getting plan convo: %v", err)
return
}
convo = res
errCh <- nil
}()
for i := 0; i < 2; i++ {
err = <-errCh
if err != nil {
log.Printf("Error getting plan data: %v\n", err)
return err
}
}
return nil
})
if err != nil {
log.Printf("Error loading build file: %v\n", err)
UpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {
ap.IsBuildingByPath[filePath] = false
})
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error loading build file: %v", err))
activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error loading build file: " + err.Error(),
}
return err
}
state.filePath = filePath
state.convoMessageId = convoMessageId
state.build = build
state.currentPlanState = currentPlan
state.convo = convo
return nil
}
================================================
FILE: app/server/model/plan/build_race.go
================================================
package plan
import (
"context"
"errors"
"fmt"
"log"
"plandex-server/syntax"
"plandex-server/utils"
"runtime"
"runtime/debug"
"strings"
"time"
)
type raceResult struct {
content string
valid bool
}
type buildRaceParams struct {
updated string
proposedContent string
desc string
reasons []syntax.NeedsVerifyReason
syntaxErrors []string
didCallFastApply bool
fastApplyCh chan string
sessionId string
}
func (fileState *activeBuildStreamFileState) buildRace(
buildCtx context.Context,
cancelBuild context.CancelFunc,
params buildRaceParams,
) (raceResult, error) {
log.Printf("buildRace - starting race for file")
defer func() {
log.Printf("buildRace - canceling build context")
cancelBuild()
}()
originalFile := fileState.preBuildState
updated := params.updated
proposedContent := params.proposedContent
desc := params.desc
reasons := params.reasons
syntaxErrors := params.syntaxErrors
fastApplyCh := params.fastApplyCh
sessionId := params.sessionId
log.Printf("buildRace - original file length: %d, updated length: %d", len(originalFile), len(updated))
log.Printf("buildRace - has %d syntax errors and %d verify reasons", len(syntaxErrors), len(reasons))
maxErrs := 3
resCh := make(chan raceResult, 1)
errCh := make(chan error, maxErrs)
sendRes := func(res raceResult) {
select {
case resCh <- res:
case <-buildCtx.Done():
log.Printf("buildRace - context canceled, skipping sendRes")
}
}
sendErr := func(err error) {
select {
case errCh <- err:
case <-buildCtx.Done():
log.Printf("buildRace - context canceled, skipping sendErr")
}
}
startedFallbacks := false
startWholeFileBuild := func(comments string) {
log.Printf("buildRace - starting whole file fallback build")
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in startWholeFileBuild: %v\n%s", r, debug.Stack())
sendErr(fmt.Errorf("error starting whole file build: %v", r))
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
select {
case <-buildCtx.Done():
log.Printf("buildRace - context already canceled, skipping whole file build")
return
default:
}
content, err := fileState.buildWholeFileFallback(buildCtx, proposedContent, desc, comments, sessionId)
if err != nil {
if errors.Is(err, context.Canceled) {
log.Printf("Context canceled during whole file build")
return
}
log.Printf("buildRace - whole file build failed: %v", err)
sendErr(fmt.Errorf("error building whole file: %w", err))
} else {
log.Printf("buildRace - whole file build succeeded")
sendRes(raceResult{content: content, valid: true})
}
}()
}
maybeStartFastApply := func(onFail func()) {
log.Printf("buildRace - starting fast apply")
if !params.didCallFastApply {
log.Printf("buildRace - fast apply isn't defined, skipping")
sendErr(nil) // no error, just no fast apply
onFail()
return
}
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in maybeStartFastApply: %v\n%s", r, debug.Stack())
sendErr(fmt.Errorf("error starting fast apply: %v", r))
onFail()
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var fastApplyRes string
select {
case fastApplyRes = <-fastApplyCh:
case <-buildCtx.Done():
log.Printf("buildRace - context canceled, skipping fast apply")
sendErr(nil) // no error, just no fast apply
onFail()
return
}
if fastApplyRes == "" {
log.Printf("buildRace - fast apply isn't defined or failed to run")
sendErr(nil) // no error, just no fast apply
onFail()
return
}
// log.Printf("buildRace - fast apply result:\n\n%s", fastApplyRes)
fastApplySyntaxErrors := fileState.validateSyntax(buildCtx, fastApplyRes)
fileState.builderRun.FastApplySyntaxErrors = fastApplySyntaxErrors
if len(fastApplySyntaxErrors) > 0 {
log.Printf("buildRace - fast apply succeeded, but has %d syntax errors", len(fastApplySyntaxErrors))
sendErr(fmt.Errorf("fast apply succeeded, but has %d syntax errors", len(fastApplySyntaxErrors)))
onFail()
return
}
log.Printf("buildRace - fast apply returned, validating... ")
validateResult, err := fileState.buildValidateLoop(buildCtx, buildValidateLoopParams{
originalFile: originalFile,
updated: fastApplyRes,
proposedContent: proposedContent,
desc: desc,
reasons: reasons,
// just validate since we're already building replacements in parallel
maxAttempts: 1,
validateOnlyOnFinalAttempt: true,
isInitial: false,
sessionId: sessionId,
})
if err != nil {
if errors.Is(err, context.Canceled) {
log.Printf("Context canceled during fast apply validation")
return
}
log.Printf("buildRace - fast apply validation failed with error: %v", err)
sendErr(fmt.Errorf("fast apply validation failed: %w", err))
onFail()
return
}
if validateResult.valid {
log.Printf("buildRace - fast apply validation succeeded")
fileState.builderRun.FastApplySuccess = true
sendRes(raceResult{content: validateResult.updated, valid: validateResult.valid})
} else {
log.Printf("buildRace - fast apply validation failed with problem: %s", validateResult.problem)
fileState.builderRun.FastApplyFailureResponse = validateResult.problem
sendErr(fmt.Errorf("fast apply validation failed: %s", validateResult.problem))
onFail()
return
}
}()
}
startFallbacks := func(comments string) {
startedFallbacks = true
// try fast apply + validation first if it's defined
// if it's undefined or fails, start the whole file build fallback
maybeStartFastApply(func() {
startWholeFileBuild(comments)
})
}
// If we get an incorrect marker, start the whole file build in the background while the validation/replacement loop continues
onInitialStream := func(chunk string, buffer string) bool {
if !startedFallbacks && strings.Contains(buffer, "") && strings.Contains(buffer, "") {
log.Printf("buildRace - detected incorrect marker, triggering whole file build")
comments := utils.GetXMLContent(buffer, "PlandexComments")
startFallbacks(comments)
}
// keep streaming
return false
}
fileState.builderRun.AutoApplyValidationStartedAt = time.Now()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in buildRace validation loop: %v\n%s", r, debug.Stack())
sendErr(fmt.Errorf("error building validate loop: %v", r))
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
log.Printf("buildRace - starting validation loop")
validateResult, err := fileState.buildValidateLoop(buildCtx, buildValidateLoopParams{
originalFile: originalFile,
updated: updated,
proposedContent: proposedContent,
desc: desc,
reasons: reasons,
syntaxErrors: syntaxErrors,
initialPhaseOnStream: onInitialStream,
isInitial: true,
sessionId: sessionId,
})
fileState.builderRun.AutoApplyValidationFinishedAt = time.Now()
if err != nil {
if errors.Is(err, context.Canceled) {
log.Printf("Context canceled during buildValidate")
return
}
log.Printf("buildRace - validation loop failed: %v", err)
sendErr(fmt.Errorf("error building validate loop: %w", err))
} else {
log.Printf("buildRace - validation loop finished, valid: %v", validateResult.valid)
if validateResult.valid {
log.Printf("buildRace - validation loop succeeded, valid: %v", validateResult.valid)
sendRes(raceResult{content: validateResult.updated, valid: validateResult.valid})
} else {
log.Printf("buildRace - validation loop failed, valid: %v", validateResult.valid)
sendErr(fmt.Errorf("validation loop failed: %s", validateResult.problem))
}
}
}()
errs := []error{}
errChNumReceived := 0
for {
select {
case <-buildCtx.Done():
log.Printf("buildRace - context canceled")
return raceResult{}, buildCtx.Err()
case err := <-errCh:
errChNumReceived++
log.Printf("buildRace - error channel received %d: %v\n", errChNumReceived, err)
if err != nil {
errs = append(errs, err)
}
if errChNumReceived >= maxErrs {
log.Printf("buildRace - all attempts failed with %d errors", len(errs))
return raceResult{}, fmt.Errorf("all build attempts failed: %v", errs)
}
if !startedFallbacks {
log.Printf("buildRace - starting build fallbacks")
startFallbacks("") // since replacements failed, pass an empty string for comments -- this causes whole file build to classify comments first
}
case res := <-resCh:
log.Printf("buildRace - got successful result")
return res, nil
}
}
}
================================================
FILE: app/server/model/plan/build_state.go
================================================
package plan
import (
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/model"
"plandex-server/types"
shared "plandex-shared"
sitter "github.com/smacker/go-tree-sitter"
)
const MaxBuildErrorRetries = 3 // uses semi-exponential backoff so be careful with this
type activeBuildStreamState struct {
modelStreamId string
clients map[string]model.ClientInfo
authVars map[string]string
auth *types.ServerAuth
currentOrgId string
currentUserId string
orgUserConfig *shared.OrgUserConfig
plan *db.Plan
branch string
settings *shared.PlanSettings
modelContext []*db.Context
convo []*db.ConvoMessage
}
type activeBuildStreamFileState struct {
*activeBuildStreamState
filePath string
convoMessageId string
build *db.PlanBuild
currentPlanState *shared.CurrentPlanState
activeBuild *types.ActiveBuild
preBuildState string
parser *sitter.Parser
language shared.Language
syntaxCheckTimedOut bool
preBuildStateSyntaxInvalid bool
validationNumRetry int
wholeFileNumRetry int
isNewFile bool
contextPart *db.Context
builderRun hooks.DidFinishBuilderRunParams
}
================================================
FILE: app/server/model/plan/build_structured_edits.go
================================================
package plan
import (
"context"
"fmt"
"log"
"plandex-server/db"
diff_pkg "plandex-server/diff"
"plandex-server/hooks"
"plandex-server/syntax"
"plandex-server/utils"
"runtime"
"runtime/debug"
"strings"
"time"
shared "plandex-shared"
)
func (fileState *activeBuildStreamFileState) buildStructuredEdits() {
filePath := fileState.filePath
activeBuild := fileState.activeBuild
planId := fileState.plan.Id
branch := fileState.branch
originalFile := fileState.preBuildState
parser := fileState.parser
if parser == nil {
log.Printf("buildStructuredEdits - tree-sitter parser is nil for file %s\n", filePath)
}
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Printf("Active plan not found for plan ID %s and branch %s\n", planId, branch)
fileState.onBuildFileError(fmt.Errorf("active plan not found for plan ID %s and branch %s", planId, branch))
return
}
buildCtx, cancelBuild := context.WithCancel(activePlan.Ctx)
proposedContent := activeBuild.FileContent
desc := activeBuild.FileDescription
descLower := strings.ToLower(desc)
isReplaceOrRemove := strings.Contains(descLower, "type: replace") || strings.Contains(descLower, "type: remove") || strings.Contains(descLower, "type: overwrite")
var autoApplyRes *syntax.ApplyChangesResult
var autoApplySyntaxErrors []string
calledFastApply := false
var fastApplyRes string
fastApplyCh := make(chan string, 1)
callFastApply := func() {
log.Printf("buildStructuredEdits - %s - calling fast apply hook\n", filePath)
fileState.builderRun.DidFastApply = true
fileState.builderRun.FastApplyStartedAt = time.Now()
calledFastApply = true
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in callFastApply: %v\n%s", r, debug.Stack())
fastApplyCh <- ""
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := hooks.ExecHook(hooks.CallFastApply, hooks.HookParams{
FastApplyParams: &hooks.FastApplyParams{
InitialCode: originalFile,
EditSnippet: proposedContent,
Language: fileState.language,
Ctx: buildCtx,
},
})
if err != nil {
log.Printf("buildStructuredEdits - error executing fast apply hook: %v\n", err)
// empty string acts as a no-op
fastApplyCh <- ""
return
} else if res.FastApplyResult == nil {
log.Printf("buildStructuredEdits - fast apply hook returned nil result\n")
// empty string acts as a no-op
fastApplyCh <- ""
return
}
fastApplyRes = res.FastApplyResult.MergedCode
log.Printf("buildStructuredEdits - %s - got fast apply hook result\n", filePath)
// fmt.Printf("buildStructuredEdits - fastApplyRes:\n%s", fastApplyRes)
fileState.builderRun.FastApplyFinishedAt = time.Now()
fastApplyCh <- fastApplyRes
}()
}
if isReplaceOrRemove {
callFastApply()
}
log.Printf("buildStructuredEdits - %s - applying changes\n", filePath)
// Apply plan logic
log.Printf("buildStructuredEdits - %s - calling ApplyChanges\n", filePath)
autoApplyRes = syntax.ApplyChanges(
buildCtx,
syntax.ApplyChangesParams{
Original: originalFile,
Proposed: proposedContent,
Desc: desc,
AddMissingStartEndRefs: true,
Parser: fileState.parser,
Language: fileState.language,
},
)
log.Printf("buildStructuredEdits - %s - got ApplyChanges result\n", filePath)
// log.Printf("buildStructuredEdits - autoApplyRes.NewFile:\n\n%s", autoApplyRes.NewFile)
log.Println("buildStructuredEdits - autoApplyRes.NeedsVerifyReasons:", autoApplyRes.NeedsVerifyReasons)
autoApplySyntaxErrors = fileState.validateSyntax(buildCtx, autoApplyRes.NewFile)
hasNeedsVerifyReasons := len(autoApplyRes.NeedsVerifyReasons) > 0
autoApplyHasSyntaxErrors := len(autoApplySyntaxErrors) > 0
autoApplyIsValid := !autoApplyHasSyntaxErrors && !hasNeedsVerifyReasons
if !autoApplyIsValid && !calledFastApply {
callFastApply()
}
log.Printf("buildStructuredEdits - %s - autoApplyHasSyntaxErrors: %t, hasNeedsVerifyReasons: %t, autoApplyIsValid: %t\n",
filePath, autoApplyHasSyntaxErrors, hasNeedsVerifyReasons, autoApplyIsValid)
updated := autoApplyRes.NewFile
// If no problems, we trust the direct ApplyChanges result
if autoApplyIsValid {
log.Printf("buildStructuredEdits - %s - changes are valid, using ApplyChanges result\n", filePath)
fileState.builderRun.AutoApplySuccess = true
} else {
log.Printf("buildStructuredEdits - %s - auto apply has syntax errors or NeedsVerifyReasons", filePath)
fileState.builderRun.AutoApplyValidationReasons = make([]string, len(autoApplyRes.NeedsVerifyReasons))
for i, reason := range autoApplyRes.NeedsVerifyReasons {
fileState.builderRun.AutoApplyValidationReasons[i] = string(reason)
}
fileState.builderRun.AutoApplyValidationSyntaxErrors = autoApplySyntaxErrors
buildRaceParams := buildRaceParams{
updated: updated,
proposedContent: proposedContent,
desc: desc,
reasons: autoApplyRes.NeedsVerifyReasons,
syntaxErrors: autoApplySyntaxErrors,
didCallFastApply: calledFastApply,
fastApplyCh: fastApplyCh,
sessionId: activePlan.SessionId,
}
buildRaceResult, err := fileState.buildRace(buildCtx, cancelBuild, buildRaceParams)
if err != nil {
if apiErr, ok := err.(*shared.ApiError); ok {
activePlan.StreamDoneCh <- apiErr
return
} else {
log.Printf("buildStructuredEdits - %s - error building race: %v\n", filePath, err)
fileState.onBuildFileError(fmt.Errorf("error building race: %v", err))
}
return
}
updated = buildRaceResult.content
}
// output diff and store build results
buildInfo := &shared.BuildInfo{
Path: filePath,
NumTokens: 0,
Finished: true,
}
log.Printf("streaming build info for finished file %s\n", filePath)
activePlan.Stream(shared.StreamMessage{
Type: shared.StreamMessageBuildInfo,
BuildInfo: buildInfo,
})
time.Sleep(50 * time.Millisecond)
// strip any blank lines from beginning/end of updated file
updated = utils.StripAddedBlankLines(originalFile, updated)
log.Printf("buildStructuredEdits - %s - getting diff replacements\n", filePath)
replacements, err := diff_pkg.GetDiffReplacements(originalFile, updated)
if err != nil {
log.Printf("buildStructuredEdits - error getting diff replacements: %v\n", err)
fileState.onBuildFileError(fmt.Errorf("error getting diff replacements: %v", err))
return
}
log.Printf("buildStructuredEdits - %s - got %d replacements\n", filePath, len(replacements))
for _, replacement := range replacements {
replacement.Summary = strings.TrimSpace(desc)
}
res := db.PlanFileResult{
TypeVersion: 1,
OrgId: fileState.plan.OrgId,
PlanId: fileState.plan.Id,
PlanBuildId: fileState.build.Id,
ConvoMessageId: fileState.convoMessageId,
Content: "",
Path: filePath,
Replacements: replacements,
}
log.Printf("buildStructuredEdits - %s - finishing build file\n", filePath)
fileState.onFinishBuildFile(&res)
}
func (fileState *activeBuildStreamFileState) validateSyntax(buildCtx context.Context, updated string) []string {
if fileState.parser != nil && !fileState.preBuildStateSyntaxInvalid && !fileState.syntaxCheckTimedOut {
validationRes, err := syntax.ValidateWithParsers(buildCtx, fileState.language, fileState.parser, "", nil, updated) // fallback parser was already set as fileState.parser if needed during initial preBuildState syntax check
if err != nil {
log.Printf("buildStructuredEdits - error validating updated file: %v\n", err)
} else if validationRes.TimedOut {
log.Printf("buildStructuredEdits - syntax check timed out for updated file\n")
fileState.syntaxCheckTimedOut = true
return nil
} else {
return validationRes.Errors
}
}
return nil
}
================================================
FILE: app/server/model/plan/build_validate_and_fix.go
================================================
package plan
import (
"context"
"errors"
"fmt"
"log"
"math/rand"
diff_pkg "plandex-server/diff"
"plandex-server/model"
"plandex-server/model/prompts"
"plandex-server/syntax"
"plandex-server/types"
"plandex-server/utils"
shared "plandex-shared"
"strings"
"time"
"github.com/sashabaranov/go-openai"
)
const MaxValidationFixAttempts = 3
type buildValidateLoopParams struct {
originalFile string
updated string
proposedContent string
desc string
syntaxErrors []string
reasons []syntax.NeedsVerifyReason
initialPhaseOnStream func(chunk string, buffer string) bool
validateOnlyOnFinalAttempt bool
maxAttempts int
isInitial bool
sessionId string
}
type buildValidateLoopResult struct {
valid bool
updated string
problem string
}
func (fileState *activeBuildStreamFileState) buildValidateLoop(
ctx context.Context,
params buildValidateLoopParams,
) (buildValidateLoopResult, error) {
log.Printf("Starting buildValidateLoop for file: %s", fileState.filePath)
originalFile := params.originalFile
updated := params.updated
proposedContent := params.proposedContent
desc := params.desc
syntaxErrors := params.syntaxErrors
numAttempts := 0
problems := []string{}
maxAttempts := MaxValidationFixAttempts
if params.maxAttempts > 0 {
maxAttempts = params.maxAttempts
}
for numAttempts < maxAttempts {
currentAttempt := numAttempts + 1
log.Printf("Starting validation attempt %d/%d", currentAttempt, MaxValidationFixAttempts)
// check for context cancellation
if ctx.Err() != nil {
log.Printf("Context cancelled during attempt %d", currentAttempt)
return buildValidateLoopResult{}, ctx.Err()
}
// reset retry count for each phase
fileState.validationNumRetry = 0
log.Printf("Reset validation retry count for attempt %d", currentAttempt)
var onStream func(chunk string, buffer string) bool
if numAttempts == 0 {
onStream = params.initialPhaseOnStream
log.Printf("Using initial phase onStream handler")
} else {
onStream = nil
log.Printf("No onStream handler for attempt %d", currentAttempt)
}
var reasons []syntax.NeedsVerifyReason
if numAttempts == 0 {
reasons = params.reasons
log.Printf("Using initial reasons for validation")
} else {
reasons = []syntax.NeedsVerifyReason{}
log.Printf("Using empty reasons list for attempt %d", currentAttempt)
}
modelConfig := fileState.settings.GetModelPack().Builder
// if available, switch to stronger model after the first attempt failed
if currentAttempt > 2 && modelConfig.StrongModel != nil {
log.Printf("Switching to strong model for attempt %d", currentAttempt)
modelConfig = *modelConfig.StrongModel
}
isLastAttempt := numAttempts == maxAttempts-1
// build validate params
validateParams := buildValidateParams{
originalFile: originalFile,
updated: updated,
proposedContent: proposedContent,
desc: desc,
onStream: onStream,
syntaxErrors: syntaxErrors,
reasons: reasons,
modelConfig: &modelConfig,
validateOnly: isLastAttempt && params.validateOnlyOnFinalAttempt,
phase: currentAttempt,
isInitial: params.isInitial,
sessionId: params.sessionId,
}
log.Printf("Calling buildValidate for attempt %d", currentAttempt)
res, err := fileState.buildValidate(ctx, validateParams)
if err != nil {
if errors.Is(err, context.Canceled) {
log.Printf("Context canceled during buildValidate")
return buildValidateLoopResult{}, err
}
log.Printf("Error in buildValidate during attempt %d: %v", currentAttempt, err)
return buildValidateLoopResult{}, fmt.Errorf("error building validate: %v", err)
}
updated = res.updated
syntaxErrors = fileState.validateSyntax(ctx, updated)
log.Printf("Found %d syntax errors after attempt %d", len(syntaxErrors), currentAttempt)
if res.valid && len(syntaxErrors) == 0 {
log.Printf("Validation succeeded in attempt %d", currentAttempt)
return buildValidateLoopResult{
valid: res.valid,
updated: res.updated,
}, nil
}
problems = append(problems, res.problem)
log.Printf("Validation failed in attempt %d, preparing for next attempt", currentAttempt)
numAttempts++
}
log.Printf("Validation failed after %d attempts", MaxValidationFixAttempts)
return buildValidateLoopResult{
valid: false,
updated: updated,
problem: strings.Join(problems, "\n\n"),
}, nil
}
type buildValidateParams struct {
originalFile string
updated string
proposedContent string
desc string
syntaxErrors []string
reasons []syntax.NeedsVerifyReason
onStream func(chunk string, buffer string) bool
phase int
modelConfig *shared.ModelRoleConfig
validateOnly bool
isInitial bool
sessionId string
}
type buildValidateResult struct {
valid bool
updated string
problem string
}
func (fileState *activeBuildStreamFileState) buildValidate(
ctx context.Context,
params buildValidateParams,
) (buildValidateResult, error) {
log.Printf("Starting buildValidate for phase %d", params.phase)
auth := fileState.auth
filePath := fileState.filePath
clients := fileState.clients
authVars := fileState.authVars
modelConfig := params.modelConfig
originalFile := params.originalFile
updated := params.updated
proposedContent := params.proposedContent
desc := params.desc
onStream := params.onStream
syntaxErrors := params.syntaxErrors
reasons := params.reasons
baseModelConfig := modelConfig.GetBaseModelConfig(authVars, fileState.settings, fileState.orgUserConfig)
// Get diff for validation
log.Printf("Getting diffs between original and updated content")
diff, err := diff_pkg.GetDiffs(originalFile, updated)
if err != nil {
log.Printf("Error getting diffs: %v", err)
return buildValidateResult{}, fmt.Errorf("error getting diffs: %v", err)
}
originalWithLineNums := shared.AddLineNums(originalFile)
proposedWithLineNums := shared.AddLineNums(proposedContent)
maxExpectedOutputTokens := shared.GetNumTokensEstimate(originalFile)/2 + shared.GetNumTokensEstimate(proposedContent)
// Choose prompt and tools based on preferred format
log.Printf("Building XML validation replacements prompt")
promptText, headNumTokens := prompts.GetValidationReplacementsXmlPrompt(prompts.ValidationPromptParams{
Path: filePath,
OriginalWithLineNums: originalWithLineNums,
Desc: desc,
ProposedWithLineNums: proposedWithLineNums,
Diff: diff,
SyntaxErrors: syntaxErrors,
Reasons: reasons,
})
// log.Printf("Prompt to LLM: %s", promptText)
log.Printf("Creating initial messages for phase 1")
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: promptText,
},
},
},
}
reqStarted := time.Now()
fileState.builderRun.ReplacementStartedAt = reqStarted
if params.validateOnly {
log.Printf("Making validation-only model request")
} else {
log.Printf("Making validation-replacements model request")
}
// log.Printf("Messages: %v", messages)
stop := []string{""}
if params.validateOnly {
stop = []string{"", ""}
}
var willCacheNumTokens int
isFirstPass := params.isInitial && params.phase == 1
if !isFirstPass && baseModelConfig.Provider == shared.ModelProviderOpenAI {
willCacheNumTokens = headNumTokens
}
log.Printf("buildValidate - calling model.ModelRequest")
// spew.Dump(messages)
// Use ModelRequest for both formats
res, err := model.ModelRequest(ctx, model.ModelRequestParams{
Clients: clients,
Auth: auth,
AuthVars: authVars,
Plan: fileState.plan,
ModelConfig: modelConfig,
Purpose: "File edit",
Messages: messages,
ModelStreamId: fileState.modelStreamId,
ConvoMessageId: fileState.convoMessageId,
BuildId: fileState.build.Id,
ModelPackName: fileState.settings.GetModelPack().Name,
Stop: stop,
BeforeReq: func() {
log.Printf("Starting model request")
fileState.builderRun.ReplacementStartedAt = time.Now()
},
AfterReq: func() {
log.Printf("Finished model request")
fileState.builderRun.ReplacementFinishedAt = time.Now()
},
OnStream: onStream,
WillCacheNumTokens: willCacheNumTokens,
SessionId: params.sessionId,
EstimatedOutputTokens: maxExpectedOutputTokens,
Settings: fileState.settings,
OrgUserConfig: fileState.orgUserConfig,
})
if err != nil {
if errors.Is(err, context.Canceled) {
log.Printf("Context canceled during model request")
return buildValidateResult{}, err
}
log.Printf("Error calling model: %v", err)
return fileState.validationRetryOrError(ctx, params, err)
}
// log.Printf("Model response:\n\n%s", res.Content)
fileState.builderRun.GenerationIds = append(fileState.builderRun.GenerationIds, res.GenerationId)
log.Printf("Added generation ID: %s", res.GenerationId)
// Handle response based on format
parseRes, err := handleXMLResponse(fileState, res.Content, originalWithLineNums, updated, params.validateOnly)
if err != nil {
log.Printf("Error handling response: %v", err)
return fileState.validationRetryOrError(ctx, params, err)
}
log.Printf("Validation result: valid=%v", parseRes.valid)
return parseRes, nil
}
func handleXMLResponse(
fileState *activeBuildStreamFileState,
content string,
originalWithLineNums shared.LineNumberedTextType,
updated string,
validateOnly bool,
) (buildValidateResult, error) {
log.Printf("Handling XML response for file: %s", fileState.filePath)
if strings.Contains(content, "") {
log.Printf("XML response indicates changes are correct")
fileState.builderRun.ReplacementSuccess = true
return buildValidateResult{
valid: true,
updated: updated,
}, nil
}
if validateOnly {
log.Printf("Validation-only mode, skipping replacements")
return buildValidateResult{
valid: false,
updated: updated,
}, nil
}
originalFileLines := strings.Split(string(originalWithLineNums), "\n")
incremental := originalWithLineNums
log.Printf("Processing XML replacement blocks")
replacementsOuter := utils.GetXMLContent(content, "PlandexReplacements")
if replacementsOuter == "" {
log.Printf("No replacements found in XML response")
return buildValidateResult{
valid: false,
updated: shared.RemoveLineNums(incremental),
problem: "No replacements found in XML response",
}, nil
}
replacements := utils.GetAllXMLContent(replacementsOuter, "Replacement")
for i, replacement := range replacements {
log.Printf("Processing replacement: %d/%d", i+1, len(replacements))
old := utils.GetXMLContent(replacement, "Old")
new := utils.GetXMLContent(replacement, "New")
if old == "" {
log.Printf("No old content found for replacement")
return buildValidateResult{valid: false, updated: updated}, fmt.Errorf("no old content found for replacement")
}
old = strings.TrimSpace(old)
// log.Printf("Old content trimmed:\n\n%s", strconv.Quote(old))
// log.Printf("New content:\n\n%s", strconv.Quote(new))
if !strings.HasPrefix(old, "pdx-") {
log.Printf("Old content does not have a line number prefix for first line")
return buildValidateResult{valid: false, updated: updated}, fmt.Errorf("old content does not have a line number prefix for first line")
}
oldLines := strings.Split(old, "\n")
var lastLine string
var lastLineNum int
firstLine := oldLines[0]
if len(oldLines) > 1 {
lastLine = oldLines[len(oldLines)-1]
}
firstLineNum, err := shared.ExtractLineNumberWithPrefix(firstLine, "pdx-")
if err != nil {
log.Printf("Error extracting line number from first line: %v", err)
return buildValidateResult{valid: false, updated: updated}, fmt.Errorf("error extracting line number from first line: %v", err)
}
if lastLine != "" {
lastLineNum, err = shared.ExtractLineNumberWithPrefix(lastLine, "pdx-")
if err != nil {
log.Printf("Error extracting line number from last line: %v", err)
return buildValidateResult{valid: false, updated: updated}, fmt.Errorf("error extracting line number from last line: %v", err)
}
}
if lastLineNum == 0 {
if !(firstLineNum > 0 && firstLineNum <= len(originalFileLines)) {
log.Printf("Invalid line number for first line: %d", firstLineNum)
return buildValidateResult{valid: false, updated: updated}, fmt.Errorf("invalid line number for first line: %d", firstLineNum)
}
old = originalFileLines[firstLineNum-1]
} else {
if !(firstLineNum > 0 && firstLineNum <= len(originalFileLines) && lastLineNum > firstLineNum && lastLineNum <= len(originalFileLines)) {
log.Printf("Invalid line numbers for first and last lines: %d-%d", firstLineNum, lastLineNum)
return buildValidateResult{valid: false, updated: updated}, fmt.Errorf("invalid line numbers: %d-%d", firstLineNum, lastLineNum)
}
old = strings.Join(originalFileLines[firstLineNum-1:lastLineNum], "\n")
}
// log.Printf("Applying replacement.\n\nOld:\n\n%s\n\nNew:\n\n%s", old, new)
incremental = shared.LineNumberedTextType(strings.Replace(string(incremental), old, new, 1))
// log.Printf("Updated content:\n\n%s", string(incremental))
}
var problem string
if strings.Contains(content, "") {
split := strings.Split(content, "")
problem = split[0]
} else if strings.Contains(content, "") {
split := strings.Split(content, "")
problem = split[0]
}
final := shared.RemoveLineNums(incremental)
// log.Printf("Final content:\n\n%s", final)
return buildValidateResult{valid: false, updated: final, problem: problem}, nil
}
func (fileState *activeBuildStreamFileState) validationRetryOrError(buildCtx context.Context, validateParams buildValidateParams, err error) (buildValidateResult, error) {
log.Printf("Handling validation error for file: %s", fileState.filePath)
if fileState.validationNumRetry < MaxBuildErrorRetries {
fileState.validationNumRetry++
log.Printf("Retrying validation (attempt %d/%d) due to error: %v",
fileState.validationNumRetry, MaxBuildErrorRetries, err)
activePlan := GetActivePlan(fileState.plan.Id, fileState.branch)
if activePlan == nil {
log.Printf("Active plan not found for plan ID %s and branch %s",
fileState.plan.Id, fileState.branch)
return buildValidateResult{}, fmt.Errorf("active plan not found for plan ID %s and branch %s",
fileState.plan.Id, fileState.branch)
}
select {
case <-buildCtx.Done():
log.Printf("Context canceled during retry wait")
return buildValidateResult{}, context.Canceled
case <-time.After(time.Duration(fileState.validationNumRetry*fileState.validationNumRetry)*200*time.Millisecond + time.Duration(rand.Intn(500))*time.Millisecond):
log.Printf("Retry wait completed, attempting validation again")
break
}
return fileState.buildValidate(buildCtx, validateParams)
} else {
log.Printf("Max retries (%d) exceeded, returning error", MaxBuildErrorRetries)
return buildValidateResult{}, err
}
}
================================================
FILE: app/server/model/plan/build_whole_file.go
================================================
package plan
import (
"context"
"errors"
"fmt"
"log"
"math/rand"
"plandex-server/model"
"plandex-server/model/prompts"
"plandex-server/types"
"plandex-server/utils"
"time"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
func (fileState *activeBuildStreamFileState) buildWholeFileFallback(buildCtx context.Context, proposedContent string, desc string, comments string, sessionId string) (string, error) {
auth := fileState.auth
filePath := fileState.filePath
clients := fileState.clients
authVars := fileState.authVars
planId := fileState.plan.Id
branch := fileState.branch
originalFile := fileState.preBuildState
config := fileState.settings.GetModelPack().GetWholeFileBuilder()
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Printf("Active plan not found for plan ID %s and branch %s\n", planId, branch)
fileState.onBuildFileError(fmt.Errorf("active plan not found for plan ID %s and branch %s", planId, branch))
return "", fmt.Errorf("active plan not found for plan ID %s and branch %s", planId, branch)
}
baseModelConfig := config.GetBaseModelConfig(authVars, fileState.settings, fileState.orgUserConfig)
originalFileWithLineNums := shared.AddLineNums(originalFile)
proposedContentWithLineNums := shared.AddLineNums(proposedContent)
sysPrompt, headNumTokens := prompts.GetWholeFilePrompt(filePath, originalFileWithLineNums, proposedContentWithLineNums, desc, comments)
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: sysPrompt,
},
},
},
}
inputTokens := model.GetMessagesTokenEstimate(messages...) + model.TokensPerRequest
maxExpectedOutputTokens := shared.GetNumTokensEstimate(originalFile + proposedContent)
modelConfig := config.GetRoleForInputTokens(inputTokens, fileState.settings)
modelConfig = modelConfig.GetRoleForOutputTokens(maxExpectedOutputTokens, fileState.settings)
log.Println("buildWholeFile - calling model for whole file write")
var prediction string
if baseModelConfig.PredictedOutputEnabled && comments != "" {
prediction = `
` + originalFile + `
`
}
// This allows proper accounting for cached input tokens even when the stream is cancelled -- OpenAI only for now
var willCacheNumTokens int
if baseModelConfig.Provider == shared.ModelProviderOpenAI {
willCacheNumTokens = headNumTokens
}
log.Println("buildWholeFile - calling model.ModelRequest")
// spew.Dump(messages)
modelRes, err := model.ModelRequest(buildCtx, model.ModelRequestParams{
Clients: clients,
Auth: auth,
AuthVars: authVars,
Plan: fileState.plan,
ModelConfig: &config,
Purpose: "File edit",
Messages: messages,
Prediction: prediction,
ModelStreamId: fileState.modelStreamId,
ConvoMessageId: fileState.convoMessageId,
BuildId: fileState.build.Id,
BeforeReq: func() {
fileState.builderRun.BuiltWholeFile = true
fileState.builderRun.BuildWholeFileStartedAt = time.Now()
},
AfterReq: func() {
fileState.builderRun.BuildWholeFileFinishedAt = time.Now()
},
WillCacheNumTokens: willCacheNumTokens,
EstimatedOutputTokens: maxExpectedOutputTokens,
SessionId: sessionId,
Settings: fileState.settings,
OrgUserConfig: fileState.orgUserConfig,
})
if err != nil {
if errors.Is(err, context.Canceled) {
log.Printf("buildWholeFileFallback - context canceled during model request for file %s", filePath)
return "", err
}
return "", fmt.Errorf("error calling model: %v", err)
}
fileState.builderRun.GenerationIds = append(fileState.builderRun.GenerationIds, modelRes.GenerationId)
fileState.builderRun.BuildWholeFileFinishedAt = time.Now()
content := modelRes.Content
// log.Printf("buildWholeFile - %s - content:\n%s\n", filePath, content)
wholeFile := utils.GetXMLContent(content, "PlandexWholeFile")
if wholeFile == "" {
log.Printf("buildWholeFile - no whole file found in response\n")
return fileState.wholeFileRetryOrError(buildCtx, proposedContent, desc, comments, sessionId, fmt.Errorf("no whole file found in response"))
}
return wholeFile, nil
}
func (fileState *activeBuildStreamFileState) wholeFileRetryOrError(buildCtx context.Context, proposedContent string, desc string, comments string, sessionId string, err error) (string, error) {
if fileState.wholeFileNumRetry < MaxBuildErrorRetries {
fileState.wholeFileNumRetry++
log.Printf("buildWholeFile - retrying whole file file '%s' due to error: %v\n", fileState.filePath, err)
activePlan := GetActivePlan(fileState.plan.Id, fileState.branch)
if activePlan == nil {
log.Printf("buildWholeFile - active plan not found for plan ID %s and branch %s\n", fileState.plan.Id, fileState.branch)
// fileState.onBuildFileError(fmt.Errorf("active plan not found for plan ID %s and branch %s", fileState.plan.Id, fileState.branch))
return "", fmt.Errorf("active plan not found for plan ID %s and branch %s", fileState.plan.Id, fileState.branch)
}
select {
case <-buildCtx.Done():
log.Printf("buildWholeFile - context canceled\n")
return "", context.Canceled
case <-time.After(time.Duration(fileState.wholeFileNumRetry*fileState.wholeFileNumRetry)*200*time.Millisecond + time.Duration(rand.Intn(500))*time.Millisecond):
break
}
return fileState.buildWholeFileFallback(buildCtx, proposedContent, desc, comments, sessionId)
} else {
// fileState.onBuildFileError(err)
return "", err
}
}
================================================
FILE: app/server/model/plan/commit_msg.go
================================================
package plan
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/model"
"plandex-server/model/prompts"
"plandex-server/notify"
"plandex-server/types"
"plandex-server/utils"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
func (state *activeTellStreamState) genPlanDescription() (*db.ConvoMessageDescription, *shared.ApiError) {
auth := state.auth
plan := state.plan
planId := plan.Id
branch := state.branch
settings := state.settings
clients := state.clients
authVars := state.authVars
orgUserConfig := state.orgUserConfig
config := settings.GetModelPack().CommitMsg
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("active plan not found for plan %s and branch %s", planId, branch))
return nil, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("active plan not found for plan %s and branch %s", planId, branch),
}
}
baseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)
var sysPrompt string
var tools []openai.Tool
var toolChoice *openai.ToolChoice
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
sysPrompt = prompts.SysDescribeXml
} else {
sysPrompt = prompts.SysDescribe
tools = []openai.Tool{
{
Type: "function",
Function: &prompts.DescribePlanFn,
},
}
choice := openai.ToolChoice{
Type: "function",
Function: openai.ToolFunction{
Name: prompts.DescribePlanFn.Name,
},
}
toolChoice = &choice
}
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: sysPrompt,
},
},
},
{
Role: openai.ChatMessageRoleAssistant,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: activePlan.CurrentReplyContent,
},
},
},
}
reqParams := model.ModelRequestParams{
Clients: clients,
Auth: auth,
AuthVars: authVars,
Plan: plan,
ModelConfig: &config,
Purpose: "Response summary",
Messages: messages,
ModelStreamId: state.modelStreamId,
ConvoMessageId: state.replyId,
SessionId: activePlan.SessionId,
Settings: settings,
OrgUserConfig: orgUserConfig,
}
if tools != nil {
reqParams.Tools = tools
}
if toolChoice != nil {
reqParams.ToolChoice = toolChoice
}
modelRes, err := model.ModelRequest(activePlan.Ctx, reqParams)
if err != nil {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error during plan description model call: %v", err))
return nil, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("error during plan description model call: %v", err),
}
}
log.Println("Plan description model call complete")
content := modelRes.Content
var commitMsg string
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
commitMsg = utils.GetXMLContent(content, "commitMsg")
if commitMsg == "" {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("no commitMsg tag found in XML response"))
return nil, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "No commitMsg tag found in XML response",
}
}
} else {
if content == "" {
fmt.Println("no describePlan function call found in response")
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("no describePlan function call found in response"))
return nil, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "No describePlan function call found in response. The model failed to generate a valid response.",
}
}
var desc shared.ConvoMessageDescription
err = json.Unmarshal([]byte(content), &desc)
if err != nil {
fmt.Printf("Error unmarshalling plan description response: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error unmarshalling plan description response: %v", err))
return nil, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("error unmarshalling plan description response: %v", err),
}
}
commitMsg = desc.CommitMsg
}
return &db.ConvoMessageDescription{
PlanId: planId,
CommitMsg: commitMsg,
}, nil
}
type GenCommitMsgForPendingResultsParams struct {
Auth *types.ServerAuth
Plan *db.Plan
Settings *shared.PlanSettings
Current *shared.CurrentPlanState
SessionId string
Ctx context.Context
Clients map[string]model.ClientInfo
AuthVars map[string]string
}
func GenCommitMsgForPendingResults(params GenCommitMsgForPendingResultsParams) (string, error) {
auth := params.Auth
plan := params.Plan
settings := params.Settings
current := params.Current
sessionId := params.SessionId
ctx := params.Ctx
clients := params.Clients
authVars := params.AuthVars
config := settings.GetModelPack().CommitMsg
s := ""
num := 0
for _, desc := range current.ConvoMessageDescriptions {
if desc.WroteFiles && desc.DidBuild && len(desc.BuildPathsInvalidated) == 0 && desc.AppliedAt == nil {
s += desc.CommitMsg + "\n"
num++
}
}
if num <= 1 {
return s, nil
}
prompt := "Pending changes:\n\n" + s
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompts.SysPendingResults,
},
},
},
{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompt,
},
},
},
}
modelRes, err := model.ModelRequest(ctx, model.ModelRequestParams{
Clients: clients,
AuthVars: authVars,
Auth: auth,
Plan: plan,
ModelConfig: &config,
Purpose: "Commit message",
Messages: messages,
SessionId: sessionId,
Settings: settings,
})
if err != nil {
fmt.Println("Generate commit message error:", err)
return "", err
}
content := modelRes.Content
if content == "" {
return "", fmt.Errorf("no response from model")
}
return content, nil
}
================================================
FILE: app/server/model/plan/exec_status.go
================================================
package plan
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"plandex-server/model"
"plandex-server/model/prompts"
"plandex-server/notify"
"plandex-server/types"
"plandex-server/utils"
"strings"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
// controls the number steps to spent trying to finish a single subtask
// if a subtask is not finished in this number of steps, we'll give up and mark it done
// necessary to prevent infinite loops
const MaxPreviousMessages = 4
type execStatusShouldContinueResult struct {
subtaskFinished bool
}
func (state *activeTellStreamState) execStatusShouldContinue(currentMessage string, sessionId string, ctx context.Context) (execStatusShouldContinueResult, *shared.ApiError) {
auth := state.auth
plan := state.plan
settings := state.settings
clients := state.clients
authVars := state.authVars
config := settings.GetModelPack().ExecStatus
orgUserConfig := state.orgUserConfig
baseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)
currentSubtask := state.currentSubtask
if currentSubtask == nil {
log.Printf("[ExecStatus] No current subtask")
return execStatusShouldContinueResult{
subtaskFinished: true,
}, nil
}
// Check subtask completion
completionMarker := fmt.Sprintf("**%s** has been completed", currentSubtask.Title)
log.Printf("[ExecStatus] Checking for subtask completion marker: %q", completionMarker)
log.Printf("[ExecStatus] Current subtask: %q", currentSubtask.Title)
if strings.Contains(currentMessage, completionMarker) {
log.Printf("[ExecStatus] ✓ Subtask completion marker found")
return execStatusShouldContinueResult{
subtaskFinished: true,
}, nil
// NOTE: tried using an LLM to verify "suspicious" subtask completions, but in practice led to too many extra LLM calls and disagreement cycles between agent roles (it's finished. no it's note! etc.)
// now just going back to trusting the completion marker... basically it's better to err on the side of marking tasks done.
// var potentialProblem bool
// if len(state.chunkProcessor.replyOperations) == 0 {
// log.Printf("[ExecStatus] ✗ Subtask completion marker found, but there are no operations to execute")
// potentialProblem = true
// } else {
// wroteToPaths := map[string]bool{}
// for _, op := range state.chunkProcessor.replyOperations {
// if op.Type == shared.OperationTypeFile {
// wroteToPaths[op.Path] = true
// }
// }
// for _, path := range currentSubtask.UsesFiles {
// if !wroteToPaths[path] {
// log.Printf("[ExecStatus] ✗ Subtask completion marker found, but the operations did not write to the file %q from the 'Uses' list", path)
// potentialProblem = true
// break
// }
// }
// }
// if !potentialProblem {
// log.Printf("[ExecStatus] ✓ Subtask completion marker found and no potential problem - will mark as completed")
// return execStatusShouldContinueResult{
// subtaskFinished: true,
// }, nil
// } else if currentSubtask.NumTries >= 1 {
// log.Printf("[ExecStatus] ✓ Subtask completion marker found, but the operations are questionable -- marking it done anyway since it's the second try and we can't risk an infinite loop")
// return execStatusShouldContinueResult{
// subtaskFinished: true,
// }, nil
// } else {
// log.Printf("[ExecStatus] ✗ Subtask completion marker found, but the operations are questionable -- will verify with LLM call")
// }
} else {
log.Printf("[ExecStatus] ✗ No subtask completion marker found in message")
}
log.Println("[ExecStatus] Current subtasks state:")
for i, task := range state.subtasks {
log.Printf("[ExecStatus] Task %d: %q (finished=%v)", i+1, task.Title, task.IsFinished)
}
log.Println("Checking if plan should continue based on exec status")
fullSubtask := currentSubtask.Title
fullSubtask += "\n\n" + currentSubtask.Description
previousMessages := []string{}
for _, msg := range state.convo {
if msg.Subtask != nil && msg.Subtask.Title == currentSubtask.Title {
previousMessages = append(previousMessages, msg.Message)
}
}
if len(previousMessages) >= MaxPreviousMessages {
log.Printf("[ExecStatus] ✗ Max previous messages reached - will mark as completed and move on to next subtask")
return execStatusShouldContinueResult{
subtaskFinished: true,
}, nil
}
prompt := prompts.GetExecStatusFinishedSubtask(prompts.GetExecStatusFinishedSubtaskParams{
UserPrompt: state.userPrompt,
CurrentSubtask: fullSubtask,
CurrentMessage: currentMessage,
PreviousMessages: previousMessages,
PreferredOutputFormat: baseModelConfig.PreferredOutputFormat,
})
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompt,
},
},
},
}
modelRes, err := model.ModelRequest(ctx, model.ModelRequestParams{
Clients: clients,
Auth: auth,
AuthVars: authVars,
Plan: plan,
ModelConfig: &config,
Purpose: "Task completion check",
Messages: messages,
ModelStreamId: state.modelStreamId,
ConvoMessageId: state.replyId,
SessionId: sessionId,
Settings: settings,
OrgUserConfig: orgUserConfig,
})
if err != nil {
log.Printf("[ExecStatus] Error in model call: %v", err)
return execStatusShouldContinueResult{}, nil
}
content := modelRes.Content
var reasoning string
var subtaskFinished bool
if baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {
reasoning = utils.GetXMLContent(content, "reasoning")
subtaskFinishedStr := utils.GetXMLContent(content, "subtaskFinished")
subtaskFinished = subtaskFinishedStr == "true"
if reasoning == "" || subtaskFinishedStr == "" {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("execStatusShouldContinue: missing required XML tags in response"))
return execStatusShouldContinueResult{}, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Missing required XML tags in response",
}
}
} else {
if content == "" {
log.Printf("[ExecStatus] No function response found in model output")
return execStatusShouldContinueResult{}, nil
}
var res types.ExecStatusResponse
if err := json.Unmarshal([]byte(content), &res); err != nil {
log.Printf("[ExecStatus] Failed to parse response: %v", err)
return execStatusShouldContinueResult{}, nil
}
reasoning = res.Reasoning
subtaskFinished = res.SubtaskFinished
}
log.Printf("[ExecStatus] Decision: subtaskFinished=%v, reasoning=%v",
subtaskFinished, reasoning)
return execStatusShouldContinueResult{
subtaskFinished: subtaskFinished,
}, nil
}
================================================
FILE: app/server/model/plan/shutdown.go
================================================
package plan
================================================
FILE: app/server/model/plan/state.go
================================================
package plan
import (
"context"
"fmt"
"log"
"plandex-server/db"
"plandex-server/notify"
"plandex-server/shutdown"
"plandex-server/types"
"strings"
"time"
shared "plandex-shared"
)
var (
activePlans types.SafeMap[*types.ActivePlan] = *types.NewSafeMap[*types.ActivePlan]()
)
func GetActivePlan(planId, branch string) *types.ActivePlan {
return activePlans.Get(strings.Join([]string{planId, branch}, "|"))
}
func CreateActivePlan(orgId, userId, planId, branch, prompt string, buildOnly, autoContext bool, sessionId string) *types.ActivePlan {
activePlan := types.NewActivePlan(orgId, userId, planId, branch, prompt, buildOnly, autoContext, sessionId)
key := strings.Join([]string{planId, branch}, "|")
activePlans.Set(key, activePlan)
go func() {
for {
select {
case <-activePlan.Ctx.Done():
log.Printf("case <-activePlan.Ctx.Done(): %s\n", planId)
err := db.SetPlanStatus(planId, branch, shared.PlanStatusStopped, "")
if err != nil {
log.Printf("Error setting plan %s status to stopped: %v\n", planId, err)
}
DeleteActivePlan(orgId, userId, planId, branch)
return
case apiErr := <-activePlan.StreamDoneCh:
log.Printf("case apiErr := <-activePlan.StreamDoneCh: %s\n", planId)
log.Printf("apiErr: %v\n", apiErr)
if apiErr == nil {
log.Printf("Plan %s stream completed successfully", planId)
err := db.SetPlanStatus(planId, branch, shared.PlanStatusFinished, "")
if err != nil {
log.Printf("Error setting plan %s status to ready: %v\n", planId, err)
}
// cancel *after* the DeleteActivePlan call
// allows queued operations to complete
DeleteActivePlan(orgId, userId, planId, branch)
activePlan.CancelFn()
return
} else {
log.Printf("Error streaming plan %s: %v\n", planId, apiErr)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error streaming plan %s: %v", planId, apiErr))
err := db.SetPlanStatus(planId, branch, shared.PlanStatusError, apiErr.Msg)
if err != nil {
log.Printf("Error setting plan %s status to error: %v\n", planId, err)
}
log.Println("Sending error message to client")
activePlan.Stream(shared.StreamMessage{
Type: shared.StreamMessageError,
Error: apiErr,
})
activePlan.FlushStreamBuffer()
log.Println("Stopping any active summary stream")
activePlan.SummaryCancelFn()
log.Println("Waiting 100ms after streaming error before canceling active plan")
time.Sleep(100 * time.Millisecond)
// cancel *before* the DeleteActivePlan call below
// short circuits any active operations
log.Println("Cancelling active plan")
activePlan.CancelFn()
DeleteActivePlan(orgId, userId, planId, branch)
return
}
}
}
}()
return activePlan
}
func DeleteActivePlan(orgId, userId, planId, branch string) {
log.Printf("Deleting active plan %s - %s - %s\n", planId, branch, orgId)
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Printf("DeleteActivePlan - No active plan found for plan ID %s on branch %s\n", planId, branch)
return
}
ctx, cancelFn := context.WithTimeout(shutdown.ShutdownCtx, 10*time.Second)
defer cancelFn()
log.Printf("Clearing uncommitted changes for plan %s - %s - %s\n", planId, branch, orgId)
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: orgId,
UserId: userId,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancelFn,
Reason: "delete active plan",
}, func(repo *db.GitRepo) error {
log.Printf("Starting clear uncommitted changes for plan %s - %s - %s\n", planId, branch, orgId)
err := repo.GitClearUncommittedChanges(branch)
log.Printf("Finished clear uncommitted changes for plan %s - %s - %s\n", planId, branch, orgId)
log.Printf("Error: %v\n", err)
return err
})
if err != nil {
log.Printf("Error clearing uncommitted changes for plan %s: %v\n", planId, err)
}
activePlans.Delete(strings.Join([]string{planId, branch}, "|"))
log.Printf("Deleted active plan %s - %s - %s\n", planId, branch, orgId)
}
func UpdateActivePlan(planId, branch string, fn func(*types.ActivePlan)) {
activePlans.Update(strings.Join([]string{planId, branch}, "|"), fn)
}
func SubscribePlan(ctx context.Context, planId, branch string) (string, chan string) {
log.Printf("Subscribing to plan %s\n", planId)
var id string
var ch chan string
activePlan := GetActivePlan(planId, branch)
if activePlan == nil {
log.Printf("SubscribePlan - No active plan found for plan ID %s on branch %s\n", planId, branch)
return "", nil
}
UpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {
id, ch = activePlan.Subscribe(ctx)
})
return id, ch
}
func UnsubscribePlan(planId, branch, subscriptionId string) {
log.Printf("UnsubscribePlan %s - %s - %s\n", planId, branch, subscriptionId)
active := GetActivePlan(planId, branch)
if active == nil {
log.Printf("No active plan found for plan ID %s on branch %s\n", planId, branch)
return
}
UpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {
activePlan.Unsubscribe(subscriptionId)
log.Printf("Unsubscribed from plan %s - %s - %s\n", planId, branch, subscriptionId)
})
}
func NumActivePlans() int {
return activePlans.Len()
}
================================================
FILE: app/server/model/plan/stop.go
================================================
package plan
import (
"fmt"
"plandex-server/db"
"github.com/sashabaranov/go-openai"
)
func Stop(planId, branch, currentUserId, currentOrgId string) error {
active := GetActivePlan(planId, branch)
if active == nil {
return fmt.Errorf("no active plan with id %s", planId)
}
active.SummaryCancelFn()
active.CancelFn()
return nil
}
func StorePartialReply(repo *db.GitRepo, planId, branch, currentUserId, currentOrgId string) error {
active := GetActivePlan(planId, branch)
if active == nil {
return fmt.Errorf("no active plan with id %s", planId)
}
if !active.BuildOnly && !active.RepliesFinished {
num := active.MessageNum + 1
userMsg := db.ConvoMessage{
OrgId: currentOrgId,
PlanId: planId,
UserId: currentUserId,
Role: openai.ChatMessageRoleAssistant,
Tokens: active.NumTokens,
Num: num,
Stopped: true,
Message: active.CurrentReplyContent,
}
_, err := db.StoreConvoMessage(repo, &userMsg, currentUserId, branch, true)
if err != nil {
return fmt.Errorf("error storing convo message: %v", err)
}
}
return nil
}
================================================
FILE: app/server/model/plan/tell_build_pending.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"plandex-server/notify"
"runtime/debug"
shared "plandex-shared"
)
func (state *activeTellStreamState) queuePendingBuilds() {
plan := state.plan
planId := plan.Id
branch := state.branch
auth := state.auth
clients := state.clients
authVars := state.authVars
currentOrgId := state.currentOrgId
currentUserId := state.currentUserId
active := GetActivePlan(planId, branch)
if active == nil {
log.Printf("execTellPlan: Active plan not found for plan ID %s on branch %s\n", planId, branch)
return
}
defer func() {
if r := recover(); r != nil {
log.Printf("panic in queuePendingBuilds: %v\n%s", r, debug.Stack())
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error getting pending builds by path: %v", r))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error getting pending builds by path: %v\n%s", r, debug.Stack()),
}
}
}()
pendingBuildsByPath, err := active.PendingBuildsByPath(auth.OrgId, auth.User.Id, state.convo)
if err != nil {
log.Printf("Error getting pending builds by path: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error getting pending builds by path: %v", err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error getting pending builds by path: %v", err),
}
return
}
if len(pendingBuildsByPath) == 0 {
log.Println("Tell plan: no pending builds")
return
}
log.Printf("Tell plan: found %d pending builds\n", len(pendingBuildsByPath))
// spew.Dump(pendingBuildsByPath)
buildState := &activeBuildStreamState{
modelStreamId: active.ModelStreamId,
clients: clients,
authVars: authVars,
auth: auth,
currentOrgId: currentOrgId,
currentUserId: currentUserId,
plan: plan,
branch: branch,
settings: state.settings,
modelContext: state.modelContext,
orgUserConfig: state.orgUserConfig,
}
for _, pendingBuilds := range pendingBuildsByPath {
buildState.queueBuilds(pendingBuilds)
}
}
================================================
FILE: app/server/model/plan/tell_context.go
================================================
package plan
import (
"fmt"
"log"
"plandex-server/types"
"regexp"
"sort"
"strings"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
type formatModelContextParams struct {
includeMaps bool
smartContextEnabled bool
includeApplyScript bool
baseOnly bool
cacheControl bool
activeOnly bool
autoOnly bool
activatePaths map[string]bool
activatePathsOrdered []string
maxTokens int
}
func (state *activeTellStreamState) formatModelContext(params formatModelContextParams) []*types.ExtendedChatMessagePart {
log.Println("Tell plan - formatModelContext")
includeMaps := params.includeMaps
smartContextEnabled := params.smartContextEnabled
includeApplyScript := params.includeApplyScript
currentStage := state.currentStage
basicOnly := params.baseOnly
activeOnly := params.activeOnly
autoOnly := params.autoOnly
activatePaths := params.activatePaths
activatePathsOrdered := params.activatePathsOrdered
if activatePaths == nil {
activatePaths = map[string]bool{}
}
maxTokens := params.maxTokens
// log all the flags
log.Printf("Tell plan - formatModelContext - basicOnly: %t, activeOnly: %t, autoOnly: %t, smartContextEnabled: %t, execEnabled: %t, includeMaps: %t, activatePaths: %v, activatePathsOrdered: %v, maxTokens: %d\n",
basicOnly, activeOnly, autoOnly, smartContextEnabled, includeApplyScript, includeMaps, activatePaths, activatePathsOrdered, params.maxTokens)
var contextBodies []string = []string{
"### LATEST PLAN CONTEXT ###",
}
addedFilesSet := map[string]bool{}
uses := map[string]bool{}
// log.Println("Tell plan - formatModelContext - state.currentSubtask:\n", spew.Sdump(state.currentSubtask))
// if state.currentSubtask != nil {
// log.Println("Tell plan - formatModelContext - state.currentSubtask.UsesFiles:\n", spew.Sdump(state.currentSubtask.UsesFiles))
// }
// log.Println("Tell plan - formatModelContext - currentStage.TellStage:\n", currentStage.TellStage)
// log.Println("Tell plan - formatModelContext - smartContextEnabled:\n", smartContextEnabled)
if currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil {
log.Println("Tell plan - formatModelContext - implementation stage - smart context enabled for current subtask")
for _, path := range state.currentSubtask.UsesFiles {
uses[path] = true
}
if verboseLogging {
log.Printf("Tell plan - formatModelContext - uses: %v\n", uses)
}
}
// log.Println("Tell plan - formatModelContext - state.modelContext:\n", spew.Sdump(state.modelContext))
totalTokens := 0
type toLoad struct {
FilePath string
Name string
Url string
NumTokens int
Body string
ContextType shared.ContextType
ImageDetail openai.ImageURLDetail
IsPending bool
}
var toLoadAll []toLoad
for _, part := range state.modelContext {
if verboseLogging {
log.Printf("Tell plan - formatModelContext - part: %s - %s - %s - %d tokens\n", part.ContextType, part.Name, part.FilePath, part.NumTokens)
}
if !(part.ContextType == shared.ContextMapType && includeMaps) {
if basicOnly && part.AutoLoaded {
if verboseLogging {
log.Println("Tell plan - formatModelContext - skipping auto loaded part -- basicOnly && part.AutoLoaded")
}
continue
}
if autoOnly && !part.AutoLoaded {
if verboseLogging {
log.Println("Tell plan - formatModelContext - skipping auto loaded part -- autoOnly && !part.AutoLoaded")
}
continue
}
}
if currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil && part.ContextType == shared.ContextFileType && !uses[part.FilePath] {
if verboseLogging {
log.Println("Tell plan - formatModelContext - skipping part -- currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil && part.ContextType == shared.ContextFileType && !uses[part.FilePath]")
}
continue
}
if activeOnly && !activatePaths[part.FilePath] {
if verboseLogging {
log.Println("Tell plan - formatModelContext - skipping part -- activeOnly && !activatePaths[part.FilePath]")
}
continue
}
if part.ContextType == shared.ContextMapType && !includeMaps {
if verboseLogging {
log.Println("Tell plan - formatModelContext - skipping part -- part.ContextType == shared.ContextMapType && !includeMaps")
}
continue
}
toLoadAll = append(toLoadAll, toLoad{
FilePath: part.FilePath,
NumTokens: part.NumTokens,
Body: part.Body,
ContextType: part.ContextType,
Name: part.Name,
Url: part.Url,
ImageDetail: part.ImageDetail,
})
if part.ContextType == shared.ContextFileType {
addedFilesSet[part.FilePath] = true
}
}
// Add any current pendingFiles in plan that weren't added to the context
var currentPlanFiles *shared.CurrentPlanFiles
var pendingFiles map[string]string = map[string]string{}
if state.currentPlanState != nil && state.currentPlanState.CurrentPlanFiles != nil && state.currentPlanState.CurrentPlanFiles.Files != nil {
currentPlanFiles = state.currentPlanState.CurrentPlanFiles
pendingFiles = state.currentPlanState.CurrentPlanFiles.Files
}
for filePath, body := range pendingFiles {
if !addedFilesSet[filePath] {
if currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && !uses[filePath] {
continue
}
if filePath == "_apply.sh" {
continue
}
if activeOnly && !activatePaths[filePath] {
continue
}
numTokens := shared.GetNumTokensEstimate(body)
toLoadAll = append(toLoadAll, toLoad{
FilePath: filePath,
NumTokens: numTokens,
Body: body,
ContextType: shared.ContextFileType,
Name: filePath,
IsPending: true,
})
if verboseLogging {
log.Printf("Tell plan - formatModelContext - added current plan file - %s\n", filePath)
}
}
}
if len(activatePathsOrdered) > 0 {
indexByPath := map[string]int{}
for i, path := range activatePathsOrdered {
indexByPath[path] = i
}
sort.Slice(toLoadAll, func(i, j int) bool {
iIndex, ok1 := indexByPath[toLoadAll[i].FilePath]
jIndex, ok2 := indexByPath[toLoadAll[j].FilePath]
// If neither has an index, sort by Name so we are using a stable order for caching
if !ok1 && !ok2 {
return toLoadAll[i].Name < toLoadAll[j].Name
}
// If only i doesn't have an index, it goes after j
if !ok1 {
return false
}
// If only j doesn't have an index, it goes after i
if !ok2 {
return true
}
// Both have indices, compare them
return iIndex < jIndex
})
}
for _, part := range toLoadAll {
totalTokens += part.NumTokens
if maxTokens > 0 && totalTokens > maxTokens {
if verboseLogging {
log.Printf("Tell plan - formatModelContext - total tokens: %d\n", totalTokens)
}
break
}
var message string
var fmtStr string
var args []any
if part.ContextType == shared.ContextDirectoryTreeType {
fmtStr = "\n\n- %s | directory tree:\n\n```\n%s\n```"
args = append(args, part.FilePath, part.Body)
} else if part.ContextType == shared.ContextFileType {
// if we're in the context phase and the file is pending, just include that the file is pending, not the full content
// there is generally enough related context from the conversation and summary to decide on whether to load the file or not
// without this, the context phase can get overloaded with pending file content
if currentStage.TellStage == shared.TellStagePlanning &&
currentStage.PlanningPhase == shared.PlanningPhaseContext &&
part.IsPending {
fmtStr = "\n\n- File `%s` has pending changes (%d 🪙)"
args = append(args, part.FilePath, part.NumTokens)
} else {
fmtStr = "\n\n- %s:\n\n```\n%s\n```"
// use pending file value if available
var body string
var found bool
res, ok := pendingFiles[part.FilePath]
if ok {
body = res
found = true
}
if !found {
body = part.Body
}
args = append(args, part.FilePath, body)
}
} else if part.ContextType == shared.ContextMapType {
fmtStr = "\n\n- %s | map:\n\n```\n%s\n```"
args = append(args, part.FilePath, part.Body)
} else if part.Url != "" {
fmtStr = "\n\n- %s:\n\n```\n%s\n```"
args = append(args, part.Url, part.Body)
} else if part.ContextType != shared.ContextImageType {
fmtStr = "\n\n- content%s:\n\n```\n%s\n```"
args = append(args, part.Name, part.Body)
}
if part.ContextType != shared.ContextImageType {
message = fmt.Sprintf(fmtStr, args...)
contextBodies = append(contextBodies, message)
}
if verboseLogging {
log.Printf("Tell plan - formatModelContext - added context: %s - %s - %s - %d tokens\n", part.ContextType, part.Name, part.FilePath, part.NumTokens)
}
}
if currentPlanFiles != nil && len(currentPlanFiles.Removed) > 0 {
contextBodies = append(contextBodies, "*Removed files:*\n")
for path := range currentPlanFiles.Removed {
contextBodies = append(contextBodies, fmt.Sprintf("- %s", path))
}
contextBodies = append(contextBodies, "These files have been *removed* and are no longer in the plan. If you want to re-add them to the plan, you must explicitly create them again.")
log.Println("Tell plan - formatModelContext - added removed files")
log.Println(contextBodies)
}
var execScriptLines []string
if includeApplyScript &&
// don't show _apply.sh history and content if smart context is enabled and the current subtask doesn't use it
!(currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil && !uses["_apply.sh"]) {
execHistory := state.currentPlanState.ExecHistory()
execScriptLines = append(execScriptLines, execHistory)
scriptContent, ok := pendingFiles["_apply.sh"]
var isEmpty bool
if !ok || scriptContent == "" {
scriptContent = "[empty]"
isEmpty = true
}
execScriptLines = append(execScriptLines, "*Current* state of _apply.sh script:")
execScriptLines = append(execScriptLines, fmt.Sprintf("\n\n- _apply.sh:\n\n```\n%s\n```", scriptContent))
if isEmpty && currentStage.TellStage == shared.TellStagePlanning && currentStage.PlanningPhase != shared.PlanningPhaseContext {
execScriptLines = append(execScriptLines, "The _apply.sh script is *empty*. You ABSOLUTELY MUST include a '### Commands' section in your response prior to the '### Tasks' section that evaluates whether any commands should be written to _apply.sh during the plan. This is MANDATORY. Do NOT UNDER ANY CIRCUMSTANCES omit this section. If you determine that commands should be added or updated in _apply.sh, you MUST also create a subtask referencing _apply.sh in the '### Tasks' section.")
if execHistory != "" {
execScriptLines = append(execScriptLines, "Consider the history of previously executed _apply.sh scripts when determining which commands to include in the new _apply.sh file. Are there any commands that should be run again after code changes? If so, mention them in the '### Commands' section and then include a subtask to include them in the _apply.sh file in the '### Tasks' section.")
}
}
}
log.Println("Tell plan - formatModelContext - contextMessages:", len(contextBodies))
textMsg := &types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: strings.Join(contextBodies, "\n"),
}
res := []*types.ExtendedChatMessagePart{textMsg}
// now add any images that should be included
// we'll check later for model image support once the final model config is set
for _, load := range toLoadAll {
if load.ContextType == shared.ContextImageType {
res = append(res, &types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: fmt.Sprintf("Image: %s", load.Name),
})
res = append(res, &types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeImageURL,
ImageURL: &openai.ChatMessageImageURL{URL: shared.GetImageDataURI(load.Body, load.FilePath), Detail: load.ImageDetail},
})
}
}
if params.cacheControl && len(res) > 0 {
res[len(res)-1].CacheControl = &types.CacheControlSpec{
Type: types.CacheControlTypeEphemeral,
}
}
if len(execScriptLines) > 0 {
res = append(res, &types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: strings.Join(execScriptLines, "\n"),
})
}
res = append(res, &types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: "### END OF CONTEXT ###\n\n",
})
return res
}
var pathRegex = regexp.MustCompile("`(.+?)`")
type checkAutoLoadContextResult struct {
autoLoadPaths []string
activatePaths map[string]bool
hasExplicitPaths bool
activatePathsOrdered []string
}
func (state *activeTellStreamState) checkAutoLoadContext() checkAutoLoadContextResult {
req := state.req
activePlan := state.activePlan
contextsByPath := activePlan.ContextsByPath
currentStage := state.currentStage
// can only auto load context in planning stage
// context phase is primary loading phase
// planning phase can still load additional context files as a backup
if currentStage.TellStage != shared.TellStagePlanning {
return checkAutoLoadContextResult{}
}
// for chat responses, only auto load context if we're in the context phase
if req.IsChatOnly && currentStage.PlanningPhase != shared.PlanningPhaseContext {
return checkAutoLoadContextResult{}
}
log.Printf("%d existing contexts by path\n", len(contextsByPath))
// pick out all potential file paths within backticks
matches := pathRegex.FindAllStringSubmatch(activePlan.CurrentReplyContent, -1)
toAutoLoad := map[string]bool{}
toActivate := map[string]bool{}
toActivateOrdered := []string{}
allSet := map[string]bool{}
allFiles := []string{}
for _, match := range matches {
trimmed := strings.TrimSpace(match[1])
if trimmed == "" {
continue
}
if req.ProjectPaths[trimmed] {
if !allSet[trimmed] {
allFiles = append(allFiles, trimmed)
allSet[trimmed] = true
toActivate[trimmed] = true
toActivateOrdered = append(toActivateOrdered, trimmed)
if contextsByPath[trimmed] == nil {
toAutoLoad[trimmed] = true
}
}
}
}
toAutoLoadPaths := []string{}
for path := range toAutoLoad {
toAutoLoadPaths = append(toAutoLoadPaths, path)
}
hasExplicitPaths := strings.Contains(activePlan.CurrentReplyContent, "### Files")
log.Printf("Tell plan - checkAutoLoadContext - toAutoLoad: %v\n", toAutoLoadPaths)
log.Printf("Tell plan - checkAutoLoadContext - toActivate: %v\n", toActivateOrdered)
return checkAutoLoadContextResult{
autoLoadPaths: toAutoLoadPaths,
activatePaths: toActivate,
activatePathsOrdered: toActivateOrdered,
hasExplicitPaths: hasExplicitPaths,
}
}
================================================
FILE: app/server/model/plan/tell_exec.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"runtime/debug"
"time"
"plandex-server/db"
"plandex-server/hooks"
"plandex-server/model"
"plandex-server/notify"
"plandex-server/types"
shared "plandex-shared"
"github.com/davecgh/go-spew/spew"
"github.com/google/uuid"
"github.com/sashabaranov/go-openai"
)
type TellParams struct {
Clients map[string]model.ClientInfo
AuthVars map[string]string
Plan *db.Plan
Branch string
Auth *types.ServerAuth
Req *shared.TellPlanRequest
}
func Tell(params TellParams) error {
clients := params.Clients
plan := params.Plan
branch := params.Branch
auth := params.Auth
req := params.Req
authVars := params.AuthVars
log.Printf("Tell: Called with plan ID %s on branch %s\n", plan.Id, branch)
_, err := activatePlan(
clients,
plan,
branch,
auth,
req.Prompt,
false,
req.AutoContext,
req.SessionId,
)
if err != nil {
log.Printf("Error activating plan: %v\n", err)
return err
}
go execTellPlan(execTellPlanParams{
clients: clients,
plan: plan,
branch: branch,
auth: auth,
req: req,
iteration: 0,
shouldBuildPending: !req.IsChatOnly && req.BuildMode == shared.BuildModeAuto,
authVars: authVars,
})
log.Printf("Tell: Tell operation completed successfully for plan ID %s on branch %s\n", plan.Id, branch)
return nil
}
type execTellPlanParams struct {
clients map[string]model.ClientInfo
authVars map[string]string
plan *db.Plan
branch string
auth *types.ServerAuth
req *shared.TellPlanRequest
iteration int
missingFileResponse shared.RespondMissingFileChoice
shouldBuildPending bool
unfinishedSubtaskReasoning string
}
func execTellPlan(params execTellPlanParams) {
clients := params.clients
authVars := params.authVars
plan := params.plan
branch := params.branch
auth := params.auth
req := params.req
iteration := params.iteration
missingFileResponse := params.missingFileResponse
shouldBuildPending := params.shouldBuildPending
unfinishedSubtaskReasoning := params.unfinishedSubtaskReasoning
log.Printf("[TellExec] Starting iteration %d for plan %s on branch %s", iteration, plan.Id, branch)
currentUserId := auth.User.Id
currentOrgId := auth.OrgId
active := GetActivePlan(plan.Id, branch)
if active == nil {
log.Printf("execTellPlan: Active plan not found for plan ID %s on branch %s\n", plan.Id, branch)
return
}
defer func() {
if r := recover(); r != nil {
log.Printf("execTellPlan: Panic: %v\n%s\n", r, string(debug.Stack()))
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("execTellPlan: Panic: %v\n%s", r, string(debug.Stack())))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Panic in execTellPlan: %v\n%s", r, string(debug.Stack())),
}
}
}()
if missingFileResponse == "" {
log.Println("Executing WillExecPlanHook")
_, apiErr := hooks.ExecHook(hooks.WillExecPlan, hooks.HookParams{
Auth: auth,
Plan: plan,
})
if apiErr != nil {
time.Sleep(100 * time.Millisecond)
active.StreamDoneCh <- apiErr
return
}
}
planId := plan.Id
log.Println("execTellPlan - Setting plan status to replying")
err := db.SetPlanStatus(planId, branch, shared.PlanStatusReplying, "")
if err != nil {
log.Printf("Error setting plan %s status to replying: %v\n", planId, err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error setting plan %s status to replying: %v", planId, err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error setting plan status to replying: %v", err),
}
log.Printf("execTellPlan: execTellPlan operation completed for plan ID %s on branch %s, iteration %d\n", plan.Id, branch, iteration)
return
}
log.Println("execTellPlan - Plan status set to replying")
state := &activeTellStreamState{
modelStreamId: active.ModelStreamId,
clients: clients,
authVars: authVars,
req: req,
auth: auth,
currentOrgId: currentOrgId,
currentUserId: currentUserId,
plan: plan,
branch: branch,
iteration: iteration,
missingFileResponse: missingFileResponse,
}
log.Println("execTellPlan - Loading tell plan")
err = state.loadTellPlan()
if err != nil {
return
}
log.Println("execTellPlan - Tell plan loaded")
activatePaths, activatePathsOrdered := state.resolveCurrentStage()
var tentativeModelConfig shared.ModelRoleConfig
var tentativeMaxTokens int
if state.currentStage.TellStage == shared.TellStagePlanning {
if state.currentStage.PlanningPhase == shared.PlanningPhaseContext {
log.Println("Tell plan - isContextStage - setting modelConfig to context loader")
tentativeModelConfig = state.settings.GetModelPack().GetArchitect()
tentativeMaxTokens = state.settings.GetArchitectEffectiveMaxTokens()
} else {
plannerConfig := state.settings.GetModelPack().Planner
tentativeModelConfig = plannerConfig.ModelRoleConfig
tentativeMaxTokens = state.settings.GetPlannerEffectiveMaxTokens()
}
} else if state.currentStage.TellStage == shared.TellStageImplementation {
tentativeModelConfig = state.settings.GetModelPack().GetCoder()
tentativeMaxTokens = state.settings.GetCoderEffectiveMaxTokens()
} else {
log.Printf("Tell plan - execTellPlan - unknown tell stage: %s\n", state.currentStage.TellStage)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("execTellPlan: unknown tell stage: %s", state.currentStage.TellStage))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Unknown tell stage",
}
return
}
ok, tokensWithoutContext := state.dryRunCalculateTokensWithoutContext(tentativeMaxTokens, unfinishedSubtaskReasoning)
if !ok {
return
}
var planStageSharedMsgs []*types.ExtendedChatMessagePart
var planningPhaseOnlyMsgs []*types.ExtendedChatMessagePart
var implementationMsgs []*types.ExtendedChatMessagePart
if state.currentStage.TellStage == shared.TellStageImplementation {
implementationMsgs = state.formatModelContext(formatModelContextParams{
includeMaps: false,
smartContextEnabled: req.SmartContext,
includeApplyScript: req.ExecEnabled,
})
} else if state.currentStage.TellStage == shared.TellStagePlanning {
// add the shared context between planning and context phases first so it can be cached
// this is just for the map and any manually loaded contexts - auto contexts will be added later
planStageSharedMsgs = state.formatModelContext(formatModelContextParams{
includeMaps: true,
smartContextEnabled: req.SmartContext,
includeApplyScript: req.ExecEnabled,
baseOnly: true,
cacheControl: true,
})
if state.currentStage.PlanningPhase == shared.PlanningPhaseTasks {
if req.AutoContext {
msg := types.ExtendedChatMessage{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{},
}
for _, part := range planStageSharedMsgs {
msg.Content = append(msg.Content, *part)
}
sharedMsgsTokens := model.GetMessagesTokenEstimate(msg)
tokensRemaining := tentativeMaxTokens - (sharedMsgsTokens + tokensWithoutContext)
if tokensRemaining < 0 {
log.Println("tokensRemaining is negative")
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("tokensRemaining is negative"))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Max tokens exceeded before adding context",
}
return
}
planningPhaseOnlyMsgs = state.formatModelContext(formatModelContextParams{
includeMaps: false,
smartContextEnabled: req.SmartContext,
includeApplyScript: false, // already included in planStageSharedMsgs
activeOnly: true,
activatePaths: activatePaths,
activatePathsOrdered: activatePathsOrdered,
maxTokens: int(float64(tokensRemaining) * 0.95), // leave a little extra room
})
} else {
// if auto context is disabled, just dump in any remaining auto contexts, since all basic contexts have already been added in planStageSharedMsgs
planningPhaseOnlyMsgs = state.formatModelContext(formatModelContextParams{
includeMaps: false,
smartContextEnabled: req.SmartContext,
includeApplyScript: false, // already included in planStageSharedMsgs
autoOnly: true,
})
}
}
}
getTellSysPromptParams := getTellSysPromptParams{
planStageSharedMsgs: planStageSharedMsgs,
planningPhaseOnlyMsgs: planningPhaseOnlyMsgs,
implementationMsgs: implementationMsgs,
contextTokenLimit: tentativeMaxTokens,
}
// log.Println("getTellSysPromptParams:\n", spew.Sdump(getTellSysPromptParams))
sysParts, err := state.getTellSysPrompt(getTellSysPromptParams)
if err != nil {
log.Printf("Error getting tell sys prompt: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error getting tell sys prompt: %v", err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error getting tell sys prompt: %v", err),
}
return
}
// log.Println("**sysPrompt:**\n", spew.Sdump(sysParts))
state.messages = []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: sysParts,
},
}
promptMessage, ok := state.resolvePromptMessage(unfinishedSubtaskReasoning)
if !ok {
return
}
// log.Println("messages:\n\n", spew.Sdump(state.messages))
// log.Println("promptMessage:", spew.Sdump(promptMessage))
state.tokensBeforeConvo =
model.GetMessagesTokenEstimate(state.messages...) +
model.GetMessagesTokenEstimate(*promptMessage) +
state.latestSummaryTokens +
model.TokensPerRequest
// print out breakdown of token usage
log.Printf("Latest summary tokens: %d\n", state.latestSummaryTokens)
log.Printf("Total tokens before convo: %d\n", state.tokensBeforeConvo)
var effectiveMaxTokens int
if state.currentStage.TellStage == shared.TellStagePlanning {
if state.currentStage.PlanningPhase == shared.PlanningPhaseContext {
effectiveMaxTokens = state.settings.GetArchitectEffectiveMaxTokens()
} else {
effectiveMaxTokens = state.settings.GetPlannerEffectiveMaxTokens()
}
} else if state.currentStage.TellStage == shared.TellStageImplementation {
effectiveMaxTokens = state.settings.GetCoderEffectiveMaxTokens()
}
if state.tokensBeforeConvo > effectiveMaxTokens {
// token limit already exceeded before adding conversation
err := fmt.Errorf("token limit exceeded before adding conversation")
log.Printf("Error: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("token limit exceeded before adding conversation"))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Token limit exceeded before adding conversation",
}
return
}
if !state.addConversationMessages() {
return
}
// add the prompt message to the end of the messages slice
if promptMessage != nil {
state.messages = append(state.messages, *promptMessage)
} else {
log.Println("promptMessage is nil")
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("promptMessage is nil"))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Prompt message isn't set",
}
return
}
state.replyId = uuid.New().String()
state.replyParser = types.NewReplyParser()
if missingFileResponse != "" && !state.handleMissingFileResponse(unfinishedSubtaskReasoning) {
return
}
// filter out any messages that are empty
state.messages = model.FilterEmptyMessages(state.messages)
log.Printf("\n\nMessages: %d\n", len(state.messages))
// for _, message := range state.messages {
// log.Printf("%s: %v\n", message.Role, message.Content)
// }
requestTokens := model.GetMessagesTokenEstimate(state.messages...) + model.TokensPerRequest
state.totalRequestTokens = requestTokens
modelConfig := tentativeModelConfig
log.Println("Tell plan - setting modelConfig")
log.Println("Tell plan - requestTokens:", requestTokens)
log.Println("Tell plan - state.currentStage.TellStage:", state.currentStage.TellStage)
log.Println("Tell plan - state.currentStage.PlanningPhase:", state.currentStage.PlanningPhase)
if state.currentStage.TellStage == shared.TellStagePlanning {
if state.currentStage.PlanningPhase == shared.PlanningPhaseContext {
log.Println("Tell plan - isContextStage - setting modelConfig to context loader")
modelConfig = state.settings.GetModelPack().GetArchitect().GetRoleForInputTokens(requestTokens, state.settings)
log.Println("Tell plan - got modelConfig for context phase")
} else if state.currentStage.PlanningPhase == shared.PlanningPhaseTasks {
modelConfig = state.settings.GetModelPack().Planner.GetRoleForInputTokens(requestTokens, state.settings)
log.Println("Tell plan - got modelConfig for tasks phase")
}
} else if state.currentStage.TellStage == shared.TellStageImplementation {
modelConfig = state.settings.GetModelPack().GetCoder().GetRoleForInputTokens(requestTokens, state.settings)
log.Println("Tell plan - got modelConfig for implementation stage")
}
state.modelConfig = &modelConfig
baseModelConfig := modelConfig.GetBaseModelConfig(authVars, state.settings, state.orgUserConfig)
if baseModelConfig == nil {
log.Println("Tell plan - baseModelConfig is nil")
log.Println("Tell plan - modelConfig id:", modelConfig.ModelId)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("No model config found for: %s", state.modelConfig.ModelId))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "No model config found for: " + string(state.modelConfig.ModelId),
}
return
}
state.baseModelConfig = baseModelConfig
// if the model doesn't support cache control, remove the cache control spec from the messages
if !baseModelConfig.SupportsCacheControl {
for i := range state.messages {
for j := range state.messages[i].Content {
if state.messages[i].Content[j].CacheControl != nil {
state.messages[i].Content[j].CacheControl = nil
}
}
}
}
// if the model doesn't support images, remove any image parts from the messages
if !baseModelConfig.HasImageSupport {
log.Println("Tell exec - model doesn't support images. Removing image parts from messages. File name will still be included.")
for i := range state.messages {
filteredContent := []types.ExtendedChatMessagePart{}
for _, part := range state.messages[i].Content {
if part.Type != openai.ChatMessagePartTypeImageURL {
filteredContent = append(filteredContent, part)
}
}
state.messages[i].Content = filteredContent
}
}
log.Println("tell exec - will send model request with:", spew.Sdump(map[string]interface{}{
"provider": baseModelConfig.Provider,
"modelId": baseModelConfig.ModelId,
"modelTag": baseModelConfig.ModelTag,
"modelName": baseModelConfig.ModelName,
"tokens": requestTokens,
}))
_, apiErr := hooks.ExecHook(hooks.WillSendModelRequest, hooks.HookParams{
Auth: auth,
Plan: plan,
WillSendModelRequestParams: &hooks.WillSendModelRequestParams{
InputTokens: requestTokens,
OutputTokens: baseModelConfig.MaxOutputTokens - requestTokens,
ModelName: baseModelConfig.ModelName,
ModelId: baseModelConfig.ModelId,
ModelTag: baseModelConfig.ModelTag,
IsUserPrompt: true,
},
})
if apiErr != nil {
active.StreamDoneCh <- apiErr
return
}
state.doTellRequest()
if shouldBuildPending {
go state.queuePendingBuilds()
}
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.CurrentStreamingReplyId = state.replyId
ap.CurrentReplyDoneCh = make(chan bool, 1)
})
}
func (state *activeTellStreamState) doTellRequest() {
clients := state.clients
authVars := state.authVars
modelConfig := state.modelConfig
active := state.activePlan
fallbackRes := modelConfig.GetFallbackForModelError(state.numErrorRetry, state.didProviderFallback, state.modelErr, authVars, state.settings, state.orgUserConfig)
modelConfig = fallbackRes.ModelRoleConfig
stop := []string{""}
baseModelConfig := modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)
if fallbackRes.FallbackType == shared.FallbackTypeProvider {
state.didProviderFallback = true
}
// log.Println("Stop:", stop)
// spew.Dump(state.messages)
log.Println("modelConfig:", spew.Sdump(map[string]interface{}{
"modelName": baseModelConfig.ModelName,
"modelId": baseModelConfig.ModelId,
"modelTag": baseModelConfig.ModelTag,
}))
if state.noCacheSupportErr {
log.Println("Tell exec - request failed with cache support error. Removing cache control breakpoints from messages.")
for i := range state.messages {
for j := range state.messages[i].Content {
if state.messages[i].Content[j].CacheControl != nil {
state.messages[i].Content[j].CacheControl = nil
}
}
}
}
modelReq := types.ExtendedChatCompletionRequest{
Model: baseModelConfig.ModelName,
Messages: state.messages,
Stream: true,
StreamOptions: &openai.StreamOptions{
IncludeUsage: true,
},
Temperature: modelConfig.Temperature,
TopP: modelConfig.TopP,
}
if baseModelConfig.StopDisabled {
state.manualStop = stop
} else {
modelReq.Stop = stop
}
// update state
state.fallbackRes = fallbackRes
state.requestStartedAt = time.Now()
state.originalReq = &modelReq
state.modelConfig = modelConfig
// output the modelReq to a json file
// if jsonData, err := json.MarshalIndent(modelReq, "", " "); err == nil {
// timestamp := time.Now().Format("2006-01-02-150405")
// filename := fmt.Sprintf("generations/model-request-%s.json", timestamp)
// if err := os.WriteFile(filename, jsonData, 0644); err != nil {
// log.Printf("Error writing model request to file: %v\n", err)
// }
// } else {
// log.Printf("Error marshaling model request to JSON: %v\n", err)
// }
log.Printf("[Tell] doTellRequest retry=%d fallbackRetry=%d using model=%s",
state.numErrorRetry, state.numFallbackRetry, baseModelConfig.ModelName)
// start the stream
stream, err := model.CreateChatCompletionStream(clients, authVars, modelConfig, state.settings, state.orgUserConfig, state.currentOrgId, state.currentUserId, active.ModelStreamCtx, modelReq)
if err != nil {
log.Printf("Error starting reply stream: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error starting reply stream: %v", err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Error starting reply stream: " + err.Error(),
}
return
}
// handle stream chunks
go state.listenStream(stream)
}
func (state *activeTellStreamState) dryRunCalculateTokensWithoutContext(tentativeMaxTokens int, unfinishedSubtaskReasoning string) (bool, int) {
clone := &activeTellStreamState{
modelStreamId: state.modelStreamId,
clients: state.clients,
req: state.req,
auth: state.auth,
currentOrgId: state.currentOrgId,
currentUserId: state.currentUserId,
plan: state.plan,
branch: state.branch,
iteration: state.iteration,
missingFileResponse: state.missingFileResponse,
settings: state.settings,
currentStage: state.currentStage,
subtasks: state.subtasks,
currentSubtask: state.currentSubtask,
convo: state.convo,
summaries: state.summaries,
latestSummaryTokens: state.latestSummaryTokens,
userPrompt: state.userPrompt,
promptMessage: state.promptMessage,
hasContextMap: state.hasContextMap,
contextMapEmpty: state.contextMapEmpty,
hasAssistantReply: state.hasAssistantReply,
modelContext: state.modelContext,
activePlan: state.activePlan,
}
sysParts, err := clone.getTellSysPrompt(getTellSysPromptParams{
contextTokenLimit: tentativeMaxTokens,
dryRunWithoutContext: true,
})
if err != nil {
log.Printf("error getting tell sys prompt for dry run token calculation: %v", err)
msg := "Error getting tell sys prompt for dry run token calculation"
if err.Error() == AllTasksCompletedMsg {
msg = "There's no current task to implement. Try a prompt instead of the 'continue' command."
go notify.NotifyErr(notify.SeverityInfo, msg)
} else {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error getting tell sys prompt for dry run token calculation: %v", err))
}
state.activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: msg,
}
return false, 0
}
clone.messages = []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: sysParts,
},
}
promptMessage, ok := clone.resolvePromptMessage(unfinishedSubtaskReasoning)
if !ok {
return false, 0
}
clone.tokensBeforeConvo =
model.GetMessagesTokenEstimate(clone.messages...) +
model.GetMessagesTokenEstimate(*promptMessage) +
clone.latestSummaryTokens +
model.TokensPerRequest
var effectiveMaxTokens int
if clone.currentStage.TellStage == shared.TellStagePlanning {
if clone.currentStage.PlanningPhase == shared.PlanningPhaseContext {
effectiveMaxTokens = clone.settings.GetArchitectEffectiveMaxTokens()
} else {
effectiveMaxTokens = clone.settings.GetPlannerEffectiveMaxTokens()
}
} else if clone.currentStage.TellStage == shared.TellStageImplementation {
effectiveMaxTokens = clone.settings.GetCoderEffectiveMaxTokens()
}
if clone.tokensBeforeConvo > effectiveMaxTokens {
log.Println("tokensBeforeConvo exceeds max tokens during dry run")
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("tokensBeforeConvo exceeds max tokens during dry run"))
state.activePlan.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Max tokens exceeded before adding conversation",
}
return false, 0
}
if !clone.addConversationMessages() {
return false, 0
}
clone.messages = append(clone.messages, *promptMessage)
return true, model.GetMessagesTokenEstimate(clone.messages...) + model.TokensPerRequest
}
================================================
FILE: app/server/model/plan/tell_load.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/model"
"plandex-server/notify"
"plandex-server/types"
"runtime"
"runtime/debug"
shared "plandex-shared"
"github.com/jmoiron/sqlx"
"github.com/sashabaranov/go-openai"
)
func (state *activeTellStreamState) loadTellPlan() error {
clients := state.clients
authVars := state.authVars
req := state.req
auth := state.auth
plan := state.plan
planId := plan.Id
branch := state.branch
currentUserId := state.currentUserId
currentOrgId := state.currentOrgId
iteration := state.iteration
missingFileResponse := state.missingFileResponse
err := state.setActivePlan()
if err != nil {
return err
}
active := state.activePlan
lockScope := db.LockScopeWrite
if iteration > 0 || missingFileResponse != "" {
lockScope = db.LockScopeRead
}
var modelContext []*db.Context
var convo []*db.ConvoMessage
var promptMsg *db.ConvoMessage
var summaries []*db.ConvoSummary
var subtasks []*db.Subtask
var settings *shared.PlanSettings
var orgUserConfig *shared.OrgUserConfig
var latestSummaryTokens int
var currentPlan *shared.CurrentPlanState
log.Printf("[TellLoad] Tell plan - loadTellPlan - iteration: %d, missingFileResponse: %s, req.IsUserContinue: %t, lockScope: %s\n", iteration, missingFileResponse, req.IsUserContinue, lockScope)
db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: auth.OrgId,
UserId: auth.User.Id,
PlanId: planId,
Branch: branch,
Scope: lockScope,
Ctx: active.Ctx,
CancelFn: active.CancelFn,
Reason: "load tell plan",
}, func(repo *db.GitRepo) error {
errCh := make(chan error, 4)
// get name for plan and rename if it's a draft
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanSettings: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("panic getting plan settings: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := db.GetPlanSettings(plan)
if err != nil {
log.Printf("Error getting plan settings: %v\n", err)
errCh <- fmt.Errorf("error getting plan settings: %v", err)
return
}
settings = res
orgUserConfigRes, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)
if err != nil {
log.Printf("Error getting org user config: %v\n", err)
errCh <- fmt.Errorf("error getting org user config: %v", err)
return
}
orgUserConfig = orgUserConfigRes
if plan.Name == "draft" {
name, err := model.GenPlanName(
auth,
plan,
settings,
orgUserConfig,
clients,
authVars,
req.Prompt,
active.SessionId,
active.Ctx,
)
if err != nil {
log.Printf("Error generating plan name: %v\n", err)
errCh <- fmt.Errorf("error generating plan name: %v", err)
return
}
err = db.WithTx(active.Ctx, "rename plan", func(tx *sqlx.Tx) error {
err := db.RenamePlan(planId, name, tx)
if err != nil {
log.Printf("Error renaming plan: %v\n", err)
return fmt.Errorf("error renaming plan: %v", err)
}
err = db.IncNumNonDraftPlans(currentUserId, tx)
if err != nil {
log.Printf("Error incrementing num non draft plans: %v\n", err)
return fmt.Errorf("error incrementing num non draft plans: %v", err)
}
return nil
})
if err != nil {
log.Printf("Error renaming plan: %v\n", err)
errCh <- fmt.Errorf("error renaming plan: %v", err)
return
}
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanContexts: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting plan modelContext: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if iteration > 0 || missingFileResponse != "" {
modelContext = active.Contexts
} else {
res, err := db.GetPlanContexts(currentOrgId, planId, true, false)
if err != nil {
log.Printf("Error getting plan modelContext: %v\n", err)
errCh <- fmt.Errorf("error getting plan modelContext: %v", err)
return
}
log.Printf("[TellLoad] Tell plan - loadTellPlan - modelContext: %v\n", len(modelContext))
// for _, part := range modelContext {
// log.Printf("[TellLoad] Tell plan - loadTellPlan - part: %s - %s - %s - %d tokens\n", part.ContextType, part.Name, part.FilePath, part.NumTokens)
// }
modelContext = res
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanConvo: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting plan convo: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := db.GetPlanConvo(currentOrgId, planId)
if err != nil {
log.Printf("Error getting plan convo: %v\n", err)
errCh <- fmt.Errorf("error getting plan convo: %v", err)
return
}
convo = res
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.MessageNum = len(convo)
})
promptTokens := shared.GetNumTokensEstimate(req.Prompt)
innerErrCh := make(chan error, 2)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in storeUserMessage: %v\n%s", r, debug.Stack())
innerErrCh <- fmt.Errorf("error storing user message: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
if iteration == 0 && missingFileResponse == "" && !req.IsUserContinue {
num := len(convo) + 1
log.Printf("[TellLoad] storing user message | len(convo): %d | num: %d\n", len(convo), num)
promptMsg = &db.ConvoMessage{
OrgId: currentOrgId,
PlanId: planId,
UserId: currentUserId,
Role: openai.ChatMessageRoleUser,
Tokens: promptTokens,
Num: num,
Message: req.Prompt,
Flags: shared.ConvoMessageFlags{
IsApplyDebug: req.IsApplyDebug,
IsUserDebug: req.IsUserDebug,
IsChat: req.IsChatOnly,
},
}
log.Println("[TellLoad] storing user message")
// repo.LogGitRepoState()
_, err = db.StoreConvoMessage(repo, promptMsg, auth.User.Id, branch, true)
if err != nil {
log.Printf("[TellLoad] Error storing user message: %v\n", err)
innerErrCh <- fmt.Errorf("error storing user message: %v", err)
return
}
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.MessageNum = num
})
}
innerErrCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanSummaries: %v\n%s", r, debug.Stack())
innerErrCh <- fmt.Errorf("error getting plan summaries: %v", r)
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
var convoMessageIds []string
for _, convoMessage := range convo {
convoMessageIds = append(convoMessageIds, convoMessage.Id)
}
log.Println("getting plan summaries")
log.Println("convoMessageIds:", convoMessageIds)
res, err := db.GetPlanSummaries(planId, convoMessageIds)
if err != nil {
log.Printf("Error getting plan summaries: %v\n", err)
innerErrCh <- fmt.Errorf("error getting plan summaries: %v", err)
return
}
summaries = res
log.Printf("got %d plan summaries", len(summaries))
if len(summaries) > 0 {
latestSummaryTokens = shared.GetNumTokensEstimate(summaries[len(summaries)-1].Summary)
}
innerErrCh <- nil
}()
for i := 0; i < 2; i++ {
err := <-innerErrCh
if err != nil {
errCh <- err
return
}
}
if promptMsg != nil {
convo = append(convo, promptMsg)
}
errCh <- nil
}()
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in getPlanSubtasks: %v\n%s", r, debug.Stack())
errCh <- fmt.Errorf("error getting plan subtasks: %v\n%s", r, debug.Stack())
runtime.Goexit() // don't allow outer function to continue and double-send to channel
}
}()
res, err := db.GetPlanSubtasks(auth.OrgId, planId)
if err != nil {
log.Printf("Error getting plan subtasks: %v\n", err)
errCh <- fmt.Errorf("error getting plan subtasks: %v", err)
return
}
subtasks = res
errCh <- nil
}()
for i := 0; i < 4; i++ {
err = <-errCh
if err != nil {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error loading plan: %v", err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error loading plan: %v", err),
}
return err
}
}
res, err := db.GetCurrentPlanState(db.CurrentPlanStateParams{
OrgId: currentOrgId,
PlanId: planId,
Contexts: modelContext,
})
if err != nil {
return fmt.Errorf("error getting current plan state: %v", err)
}
currentPlan = res
return nil
})
if err != nil {
log.Printf("execTellPlan: error loading tell plan: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error loading tell plan: %v", err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error loading tell plan: %v", err),
}
return err
}
state.modelContext = modelContext
state.convo = convo
state.promptConvoMessage = promptMsg
state.summaries = summaries
state.latestSummaryTokens = latestSummaryTokens
state.settings = settings
state.currentPlanState = currentPlan
state.subtasks = subtasks
for _, subtask := range state.subtasks {
if !subtask.IsFinished {
state.currentSubtask = subtask
break
}
}
log.Printf("[TellLoad] Subtasks: %+v", state.subtasks)
log.Printf("[TellLoad] Current subtask: %+v", state.currentSubtask)
state.hasContextMap = false
state.contextMapEmpty = true
for _, context := range state.modelContext {
if context.ContextType == shared.ContextMapType {
state.hasContextMap = true
if context.NumTokens > 0 {
state.contextMapEmpty = false
}
break
}
}
state.hasAssistantReply = false
for _, convoMessage := range state.convo {
if convoMessage.Role == openai.ChatMessageRoleAssistant {
state.hasAssistantReply = true
break
}
}
if iteration == 0 && missingFileResponse == "" {
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.Contexts = state.modelContext
for _, context := range state.modelContext {
if context.FilePath != "" {
ap.ContextsByPath[context.FilePath] = context
}
}
})
} else if missingFileResponse == "" {
// reset current reply content and num tokens
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.CurrentReplyContent = ""
ap.NumTokens = 0
})
}
// if any skipped paths have since been added to context, remove them from skipped paths
if len(active.SkippedPaths) > 0 {
var toUnskipPaths []string
for contextPath := range active.ContextsByPath {
if active.SkippedPaths[contextPath] {
toUnskipPaths = append(toUnskipPaths, contextPath)
}
}
if len(toUnskipPaths) > 0 {
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
for _, path := range toUnskipPaths {
delete(ap.SkippedPaths, path)
}
})
}
}
return nil
}
func (state *activeTellStreamState) setActivePlan() error {
plan := state.plan
branch := state.branch
active := GetActivePlan(plan.Id, branch)
if active == nil {
return fmt.Errorf("no active plan with id %s", plan.Id)
}
state.activePlan = active
return nil
}
================================================
FILE: app/server/model/plan/tell_missing_file.go
================================================
package plan
import (
"log"
"plandex-server/model/prompts"
"plandex-server/types"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
func (state *activeTellStreamState) handleMissingFileResponse(unfinishedSubtaskReasoning string) bool {
missingFileResponse := state.missingFileResponse
planId := state.plan.Id
branch := state.branch
req := state.req
active := GetActivePlan(planId, branch)
if active == nil {
log.Printf("execTellPlan: Active plan not found for plan ID %s on branch %s\n", planId, branch)
return false
}
log.Println("Missing file response:", missingFileResponse, "setting replyParser")
// log.Printf("Current reply content:\n%s\n", active.CurrentReplyContent)
state.replyParser.AddChunk(active.CurrentReplyContent, true)
res := state.replyParser.Read()
currentFile := res.CurrentFilePath
log.Printf("Current file: %s\n", currentFile)
// log.Println("Current reply content:\n", active.CurrentReplyContent)
replyContent := active.CurrentReplyContent
numTokens := active.NumTokens
if missingFileResponse == shared.RespondMissingFileChoiceSkip {
replyBeforeCurrentFile := state.replyParser.GetReplyBeforeCurrentPath()
numTokens = shared.GetNumTokensEstimate(replyBeforeCurrentFile)
replyContent = replyBeforeCurrentFile
state.replyParser = types.NewReplyParser()
state.replyParser.AddChunk(replyContent, true)
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.CurrentReplyContent = replyContent
ap.NumTokens = numTokens
ap.SkippedPaths[currentFile] = true
})
} else {
if missingFileResponse == shared.RespondMissingFileChoiceOverwrite {
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.AllowOverwritePaths[currentFile] = true
})
}
}
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleAssistant,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: active.CurrentReplyContent,
},
},
})
if missingFileResponse == shared.RespondMissingFileChoiceSkip {
res := state.replyParser.FinishAndRead()
skipPrompt := prompts.GetSkipMissingFilePrompt(res.CurrentFilePath)
params := prompts.UserPromptParams{
CreatePromptParams: prompts.CreatePromptParams{
ExecMode: req.ExecEnabled,
AutoContext: req.AutoContext,
IsUserDebug: req.IsUserDebug,
IsApplyDebug: req.IsApplyDebug,
ContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),
},
Prompt: skipPrompt,
OsDetails: req.OsDetails,
CurrentStage: state.currentStage,
UnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,
}
prompt := prompts.GetWrappedPrompt(params) + "\n\n" + skipPrompt // repetition of skip prompt to improve instruction following
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompt,
},
},
})
} else {
missingPrompt := prompts.GetMissingFileContinueGeneratingPrompt(res.CurrentFilePath)
params := prompts.UserPromptParams{
CreatePromptParams: prompts.CreatePromptParams{
ExecMode: req.ExecEnabled,
AutoContext: req.AutoContext,
IsUserDebug: req.IsUserDebug,
IsApplyDebug: req.IsApplyDebug,
ContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),
},
Prompt: missingPrompt,
OsDetails: req.OsDetails,
CurrentStage: state.currentStage,
UnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,
}
prompt := prompts.GetWrappedPrompt(params) + "\n\n" + missingPrompt // repetition of missing prompt to improve instruction following
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompt,
},
},
})
}
return true
}
================================================
FILE: app/server/model/plan/tell_prompt_message.go
================================================
package plan
import (
"log"
"net/http"
"plandex-server/model/prompts"
"plandex-server/types"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
func (state *activeTellStreamState) resolvePromptMessage(
unfinishedSubtaskReasoning string,
) (*types.ExtendedChatMessage, bool) {
req := state.req
active := state.activePlan
iteration := state.iteration
var promptMessage *types.ExtendedChatMessage
state.skipConvoMessages = map[string]bool{}
lastMessage := state.lastSuccessfulConvoMessage()
if req.IsUserContinue {
if lastMessage == nil {
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeContinueNoMessages,
Status: http.StatusBadRequest,
Msg: "No messages yet. Can't continue plan.",
}
return nil, false
}
log.Println("User is continuing plan. Last message role:", lastMessage.Role)
}
if req.IsChatOnly {
var prompt string
if req.IsUserContinue {
if lastMessage.Role == openai.ChatMessageRoleUser {
log.Println("User is continuing plan in chat only mode. Last message was user message. Using last user message as prompt")
content := lastMessage.Message
prompt = content
state.userPrompt = content
state.skipConvoMessages[lastMessage.Id] = true
} else {
log.Println("User is continuing plan in chat only mode. Last message was assistant message. Using user continue prompt")
prompt = prompts.UserContinuePrompt
}
} else {
prompt = req.Prompt
}
wrapped := prompts.GetWrappedChatOnlyPrompt(prompts.ChatUserPromptParams{
CreatePromptParams: prompts.CreatePromptParams{
AutoContext: req.AutoContext,
ExecMode: req.ExecEnabled,
IsGitRepo: req.IsGitRepo,
// no need to pass in IsUserDebug or IsApplyDebug here because it's a chat message
},
Prompt: prompt,
OsDetails: req.OsDetails,
// no current task for chat only mode
})
promptMessage = &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: wrapped,
},
},
}
} else if req.IsUserContinue {
// log.Println("User is continuing plan. Last message:\n\n", lastMessage.Content)
if lastMessage.Role == openai.ChatMessageRoleUser {
// if last message was a user message, we want to remove it from the messages array and then use that last message as the prompt so we can continue from where the user left off
log.Println("User is continuing plan in tell mode. Last message was user message. Using last user message as prompt")
content := lastMessage.Message
params := prompts.UserPromptParams{
CreatePromptParams: prompts.CreatePromptParams{
ExecMode: req.ExecEnabled,
AutoContext: req.AutoContext,
IsGitRepo: req.IsGitRepo,
ContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),
// no need to pass in IsUserDebug or IsApplyDebug here because we're continuing
},
Prompt: content,
OsDetails: req.OsDetails,
CurrentStage: state.currentStage,
UnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,
}
promptMessage = &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompts.GetWrappedPrompt(params),
},
},
}
state.userPrompt = content
} else {
// if the last message was an assistant message, we'll use the user continue prompt
log.Println("User is continuing plan in tell mode. Last message was assistant message. Using user continue prompt")
params := prompts.UserPromptParams{
CreatePromptParams: prompts.CreatePromptParams{
ExecMode: req.ExecEnabled,
AutoContext: req.AutoContext,
IsGitRepo: req.IsGitRepo,
ContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),
// no need to pass in IsUserDebug or IsApplyDebug here because we're continuing
},
Prompt: prompts.UserContinuePrompt,
OsDetails: req.OsDetails,
CurrentStage: state.currentStage,
UnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,
}
promptMessage = &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompts.GetWrappedPrompt(params),
},
},
}
}
} else {
var prompt string
if iteration == 0 {
prompt = req.Prompt
} else if state.currentStage.TellStage == shared.TellStageImplementation {
prompt = prompts.AutoContinueImplementationPrompt
} else {
prompt = prompts.AutoContinuePlanningPrompt
}
state.userPrompt = prompt
params := prompts.UserPromptParams{
CreatePromptParams: prompts.CreatePromptParams{
ExecMode: req.ExecEnabled,
AutoContext: req.AutoContext,
IsUserDebug: req.IsUserDebug,
IsApplyDebug: req.IsApplyDebug,
IsGitRepo: req.IsGitRepo,
ContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),
},
Prompt: prompt,
OsDetails: req.OsDetails,
CurrentStage: state.currentStage,
UnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,
}
finalPrompt := prompts.GetWrappedPrompt(params)
// log.Println("Final prompt:", finalPrompt)
promptMessage = &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: finalPrompt,
},
},
}
}
// log.Println("Prompt message:", promptMessage.Content)
return promptMessage, true
}
================================================
FILE: app/server/model/plan/tell_stage.go
================================================
package plan
import (
"log"
"plandex-server/db"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
func (state *activeTellStreamState) lastSuccessfulConvoMessage() *db.ConvoMessage {
for i := len(state.convo) - 1; i >= 0; i-- {
msg := state.convo[i]
if msg.Stopped || msg.Flags.HasError {
continue
}
return msg
}
return nil
}
func (state *activeTellStreamState) resolveCurrentStage() (activatePaths map[string]bool, activatePathsOrdered []string) {
req := state.req
iteration := state.iteration
hasContextMap := state.hasContextMap
convo := state.convo
contextMapEmpty := state.contextMapEmpty
log.Printf("[resolveCurrentStage] Initial state: hasContextMap: %v, convo len: %d", hasContextMap, len(convo))
lastConvoMsg := state.lastSuccessfulConvoMessage()
activatePaths = map[string]bool{}
activatePathsOrdered = []string{}
isContinueFromAssistantMsg := false
if lastConvoMsg != nil {
isContinueFromAssistantMsg = iteration == 0 && req.IsUserContinue && lastConvoMsg.Role == openai.ChatMessageRoleAssistant
log.Printf("[resolveCurrentStage] isContinueFromAssistantMsg: %v (IsUserContinue: %v, LastMsgRole: %s)",
isContinueFromAssistantMsg, req.IsUserContinue, lastConvoMsg.Role)
} else {
log.Println("[resolveCurrentStage] No previous successful conversation message found")
}
isUserPrompt := false
if !isContinueFromAssistantMsg {
isUserPrompt = lastConvoMsg == nil || lastConvoMsg.Role == openai.ChatMessageRoleUser
log.Printf("[resolveCurrentStage] isUserPrompt: %v", isUserPrompt)
}
var tellStage shared.TellStage
var planningPhase shared.PlanningPhase
if isUserPrompt {
tellStage = shared.TellStagePlanning
log.Println("[resolveCurrentStage] Set tellStage to Planning due to user prompt")
} else {
if lastConvoMsg != nil && lastConvoMsg.Flags.DidMakePlan {
tellStage = shared.TellStageImplementation
log.Println("[resolveCurrentStage] Set tellStage to Implementation - DidMakePlan: true, IsChatOnly: false")
} else if lastConvoMsg != nil && lastConvoMsg.Flags.CurrentStage.TellStage == shared.TellStageImplementation {
tellStage = shared.TellStageImplementation
log.Println("[resolveCurrentStage] Set tellStage to Implementation - CurrentStage: implementation")
} else {
tellStage = shared.TellStagePlanning
log.Printf("[resolveCurrentStage] Set tellStage to Planning - DidMakePlan: %v, IsChatOnly: %v",
lastConvoMsg != nil && lastConvoMsg.Flags.DidMakePlan, req.IsChatOnly)
}
}
wasContextStage := false
if lastConvoMsg != nil {
flags := lastConvoMsg.Flags
log.Printf("[resolveCurrentStage] Last convo message flags: %+v", flags)
if flags.CurrentStage.TellStage == shared.TellStagePlanning && flags.CurrentStage.PlanningPhase == shared.PlanningPhaseContext {
wasContextStage = true
activatePaths = lastConvoMsg.ActivatedPaths
activatePathsOrdered = lastConvoMsg.ActivatedPathsOrdered
log.Printf("[resolveCurrentStage] Was context stage, copied activatePaths: %v", activatePaths)
}
}
if tellStage == shared.TellStagePlanning {
if req.AutoContext && hasContextMap && !contextMapEmpty && !wasContextStage {
planningPhase = shared.PlanningPhaseContext
log.Printf("[resolveCurrentStage] Set planningPhase to Context - AutoContext: %v, hasContextMap: %v, contextMapEmpty: %v, wasContextStage: %v",
req.AutoContext, hasContextMap, contextMapEmpty, wasContextStage)
} else {
planningPhase = shared.PlanningPhaseTasks
log.Printf("[resolveCurrentStage] Set planningPhase to Tasks - AutoContext: %v, hasContextMap: %v, contextMapEmpty: %v, wasContextStage: %v",
req.AutoContext, hasContextMap, contextMapEmpty, wasContextStage)
}
}
state.currentStage = shared.CurrentStage{
TellStage: tellStage,
PlanningPhase: planningPhase,
}
log.Printf("[resolveCurrentStage] Final state - TellStage: %s, PlanningPhase: %s", tellStage, planningPhase)
return activatePaths, activatePathsOrdered
}
================================================
FILE: app/server/model/plan/tell_state.go
================================================
package plan
import (
"plandex-server/db"
"plandex-server/model"
"plandex-server/types"
"time"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
type activeTellStreamState struct {
activePlan *types.ActivePlan
modelStreamId string
clients map[string]model.ClientInfo
authVars map[string]string
req *shared.TellPlanRequest
auth *types.ServerAuth
currentOrgId string
currentUserId string
orgUserConfig *shared.OrgUserConfig
plan *db.Plan
branch string
iteration int
replyId string
modelContext []*db.Context
hasContextMap bool
contextMapEmpty bool
convo []*db.ConvoMessage
promptConvoMessage *db.ConvoMessage
currentPlanState *shared.CurrentPlanState
missingFileResponse shared.RespondMissingFileChoice
summaries []*db.ConvoSummary
summarizedToMessageId string
latestSummaryTokens int
userPrompt string
promptMessage *openai.ChatCompletionMessage
replyParser *types.ReplyParser
replyNumTokens int
messages []types.ExtendedChatMessage
tokensBeforeConvo int
totalRequestTokens int
settings *shared.PlanSettings
subtasks []*db.Subtask
currentSubtask *db.Subtask
hasAssistantReply bool
currentStage shared.CurrentStage
chunkProcessor *chunkProcessor
generationId string
requestStartedAt time.Time
firstTokenAt time.Time
originalReq *types.ExtendedChatCompletionRequest
modelConfig *shared.ModelRoleConfig
baseModelConfig *shared.BaseModelConfig
fallbackRes shared.FallbackResult
skipConvoMessages map[string]bool
manualStop []string
numErrorRetry int
numFallbackRetry int
modelErr *shared.ModelError
noCacheSupportErr bool
didProviderFallback bool
}
type chunkProcessor struct {
replyOperations []*shared.Operation
chunksReceived int
maybeRedundantOpeningTagContent string
fileOpen bool
contentBuffer string
awaitingBlockOpeningTag bool
awaitingBlockClosingTag bool
awaitingOpClosingTag bool
awaitingBackticks bool
}
================================================
FILE: app/server/model/plan/tell_stream_error.go
================================================
package plan
import (
"context"
"fmt"
"log"
"math/rand"
"net/http"
"plandex-server/db"
"plandex-server/model"
"plandex-server/notify"
"plandex-server/shutdown"
"strconv"
"time"
shared "plandex-shared"
)
type onErrorParams struct {
streamErr error
streamApiErr *shared.ApiError
storeDesc bool
convoMessageId string
commitMsg string
canRetry bool
modelErr *shared.ModelError
}
type onErrorResult struct {
shouldContinueMainLoop bool
shouldReturn bool
}
func (state *activeTellStreamState) onError(params onErrorParams) onErrorResult {
log.Printf("\nStream error: %v\n", params.streamErr)
streamErr := params.streamErr
storeDesc := params.storeDesc
convoMessageId := params.convoMessageId
commitMsg := params.commitMsg
modelErr := params.modelErr
planId := state.plan.Id
branch := state.branch
currentOrgId := state.currentOrgId
summarizedToMessageId := state.summarizedToMessageId
active := GetActivePlan(planId, branch)
if active == nil {
log.Printf("tellStream onError - Active plan not found for plan ID %s on branch %s\n", planId, branch)
return onErrorResult{
shouldReturn: true,
}
}
canRetry := params.canRetry
isFallback := state.fallbackRes.IsFallback
maxRetries := model.MAX_RETRIES_WITHOUT_FALLBACK
if isFallback {
maxRetries = model.MAX_ADDITIONAL_RETRIES_WITH_FALLBACK
}
compareRetries := state.numErrorRetry
if isFallback {
compareRetries = state.numFallbackRetry
}
potentialFallback := state.modelConfig.GetFallbackForModelError(
state.numErrorRetry,
state.didProviderFallback,
modelErr,
state.authVars,
state.settings,
state.orgUserConfig,
)
newFallback := false
if modelErr != nil {
if !modelErr.Retriable {
log.Printf("tellStream onError - operation returned non-retriable error: %v", modelErr)
if !potentialFallback.IsFallback {
canRetry = false
} else {
log.Printf("tellStream onError - operation returned non-retriable error, but has fallback - resetting numFallbackRetry to 0 and continuing to retry")
state.numFallbackRetry = 0
// otherwise, continue to retry logic
canRetry = true
newFallback = true
}
}
}
if canRetry {
log.Println("tellStream onError - canRetry", canRetry)
if compareRetries >= maxRetries {
log.Printf("tellStream onError - Max retries reached for plan ID %s on branch %s\n", planId, branch)
canRetry = false
}
}
if canRetry {
log.Println("tellStream onError - retrying stream")
// stop stream via context (ensures we stop child streams too)
active.CancelModelStreamFn()
active.ResetModelCtx()
var retryDelay time.Duration
if modelErr != nil && modelErr.RetryAfterSeconds > 0 {
// if the model err has a retry after, then use that with a bit of padding
retryDelay = time.Duration(int(float64(modelErr.RetryAfterSeconds)*1.1)) * time.Second
} else {
// otherwise, use some jitter
retryDelay = time.Duration(1000+rand.Intn(200)) * time.Millisecond
}
cacheSupportErr := modelErr != nil && modelErr.Kind == shared.ErrCacheSupport
numErrorRetry := state.numErrorRetry
if modelErr != nil && modelErr.ShouldIncrementRetry() {
numErrorRetry = numErrorRetry + 1
}
log.Printf("tellStream onError - Retry %d/%d - Retrying stream in %v", numErrorRetry, maxRetries, retryDelay)
time.Sleep(retryDelay)
state.numErrorRetry = numErrorRetry
if isFallback && !newFallback && modelErr != nil && modelErr.ShouldIncrementRetry() {
state.numFallbackRetry = state.numFallbackRetry + 1
}
// if we got a cache support error, keep everything the same, including the modelErr (if we're already retrying) so we can make the exact same request again without cache control breakpoints
if cacheSupportErr {
state.noCacheSupportErr = true
} else {
state.modelErr = modelErr
if newFallback {
// if we got a new fallback, we need to reset the noCacheSupportErr flag since we're using a different model now
state.noCacheSupportErr = false
}
}
// retry the request
state.doTellRequest()
return onErrorResult{
shouldReturn: true,
}
}
storeDescAndReply := func() error {
log.Println("tellStream onError - storing desc and reply")
ctx, cancelFn := context.WithTimeout(shutdown.ShutdownCtx, 5*time.Second)
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: currentOrgId,
UserId: state.currentUserId,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeWrite,
Ctx: ctx,
CancelFn: cancelFn,
Reason: "store desc and reply",
}, func(repo *db.GitRepo) error {
storedMessage := false
storedDesc := false
if convoMessageId == "" {
hasUnfinishedSubtasks := false
for _, subtask := range state.subtasks {
if !subtask.IsFinished {
hasUnfinishedSubtasks = true
break
}
}
assistantMsg, msg, err := state.storeAssistantReply(repo, storeAssistantReplyParams{
flags: shared.ConvoMessageFlags{
CurrentStage: state.currentStage,
HasUnfinishedSubtasks: hasUnfinishedSubtasks,
HasError: true,
},
subtask: nil,
addedSubtasks: nil,
})
if err == nil {
convoMessageId = assistantMsg.Id
commitMsg = msg
storedMessage = true
} else {
log.Printf("Error storing assistant message after stream error: %v\n", err)
return err
}
}
if storeDesc && convoMessageId != "" {
err := db.StoreDescription(&db.ConvoMessageDescription{
OrgId: currentOrgId,
PlanId: planId,
SummarizedToMessageId: summarizedToMessageId,
WroteFiles: false,
ConvoMessageId: convoMessageId,
BuildPathsInvalidated: map[string]bool{},
Error: streamErr.Error(),
})
if err == nil {
storedDesc = true
} else {
log.Printf("Error storing description after stream error: %v\n", err)
return err
}
}
if storedMessage || storedDesc {
err := repo.GitAddAndCommit(branch, commitMsg)
if err != nil {
log.Printf("Error committing after stream error: %v\n", err)
return err
}
}
return nil
})
if err != nil {
log.Printf("Error storing description and reply after stream error: %v\n", err)
return err
}
return nil
}
if active.CurrentReplyContent != "" {
storeDescAndReply() // best effort to store description and reply, ignore errors
}
if params.streamApiErr != nil {
active.StreamDoneCh <- params.streamApiErr
} else {
msg := "Stream error: " + streamErr.Error()
if params.canRetry && state.numErrorRetry >= maxRetries {
msg += " | Failed after " + strconv.Itoa(state.numErrorRetry) + " retries"
}
go notify.NotifyErr(notify.SeverityInfo, fmt.Sprintf("tellStream stream error after %d retries: %v", state.numErrorRetry, streamErr))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: msg,
}
}
return onErrorResult{
shouldContinueMainLoop: true,
}
}
func (state *activeTellStreamState) onActivePlanMissingError() {
planId := state.plan.Id
branch := state.branch
log.Printf("Active plan not found for plan ID %s on branch %s\n", planId, branch)
state.onError(onErrorParams{
streamErr: fmt.Errorf("active plan not found for plan ID %s on branch %s", planId, branch),
storeDesc: true,
})
}
================================================
FILE: app/server/model/plan/tell_stream_finish.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/notify"
"plandex-server/types"
"runtime/debug"
"time"
shared "plandex-shared"
"github.com/davecgh/go-spew/spew"
)
const MaxAutoContinueIterations = 200
type handleStreamFinishedResult struct {
shouldContinueMainLoop bool
shouldReturn bool
}
func (state *activeTellStreamState) handleStreamFinished() handleStreamFinishedResult {
planId := state.plan.Id
branch := state.branch
auth := state.auth
plan := state.plan
req := state.req
clients := state.clients
authVars := state.authVars
settings := state.settings
orgUserConfig := state.orgUserConfig
currentOrgId := state.currentOrgId
summaries := state.summaries
convo := state.convo
iteration := state.iteration
replyOperations := state.chunkProcessor.replyOperations
err := state.setActivePlan()
if err != nil {
state.onActivePlanMissingError()
return handleStreamFinishedResult{
shouldContinueMainLoop: true,
shouldReturn: false,
}
}
active := state.activePlan
time.Sleep(30 * time.Millisecond)
active.FlushStreamBuffer()
time.Sleep(100 * time.Millisecond)
active.Stream(shared.StreamMessage{
Type: shared.StreamMessageDescribing,
})
active.FlushStreamBuffer()
err = db.SetPlanStatus(planId, branch, shared.PlanStatusDescribing, "")
if err != nil {
res := state.onError(onErrorParams{
streamErr: fmt.Errorf("failed to set plan status to describing: %v", err),
storeDesc: true,
})
return handleStreamFinishedResult{
shouldContinueMainLoop: res.shouldContinueMainLoop,
shouldReturn: res.shouldReturn,
}
}
autoLoadContextResult := state.checkAutoLoadContext()
checkNewSubtasksResult := state.checkNewSubtasks()
hasExplicitTasks := checkNewSubtasksResult.hasExplicitTasks
addedSubtasks := checkNewSubtasksResult.newSubtasks
checkRemoveSubtasksResult := state.checkRemoveSubtasks()
removedSubtasks := checkRemoveSubtasksResult.removedSubtasks
hasExplicitRemoveTasks := checkRemoveSubtasksResult.hasExplicitRemoveTasks
log.Println("removedSubtasks:\n", spew.Sdump(removedSubtasks))
log.Println("addedSubtasks:\n", spew.Sdump(addedSubtasks))
log.Println("hasNewSubtasks:\n", hasExplicitTasks)
handleDescAndExecStatusRes := state.handleDescAndExecStatus()
if handleDescAndExecStatusRes.shouldContinueMainLoop || handleDescAndExecStatusRes.shouldReturn {
return handleDescAndExecStatusRes.handleStreamFinishedResult
}
generatedDescription := handleDescAndExecStatusRes.generatedDescription
subtaskFinished := handleDescAndExecStatusRes.subtaskFinished
log.Printf("subtaskFinished: %v\n", subtaskFinished)
storeOnFinishedResult := state.storeOnFinished(storeOnFinishedParams{
replyOperations: replyOperations,
generatedDescription: generatedDescription,
subtaskFinished: subtaskFinished,
hasNewSubtasks: hasExplicitTasks,
autoLoadContextResult: autoLoadContextResult,
addedSubtasks: addedSubtasks,
removedSubtasks: removedSubtasks,
})
if storeOnFinishedResult.shouldContinueMainLoop || storeOnFinishedResult.shouldReturn {
return storeOnFinishedResult.handleStreamFinishedResult
}
allSubtasksFinished := storeOnFinishedResult.allSubtasksFinished
log.Println("allSubtasksFinished:\n", spew.Sdump(allSubtasksFinished))
// summarize convo needs to come *after* the reply is stored in order to correctly summarize the latest message
log.Println("summarizing convo in background")
// summarize in the background
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in summarizeConvo: %v\n%s", r, debug.Stack())
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error summarizing convo: %v", r),
}
}
}()
err := summarizeConvo(clients, authVars, settings, orgUserConfig, summarizeConvoParams{
auth: auth,
plan: plan,
branch: branch,
convo: convo,
summaries: summaries,
userPrompt: state.userPrompt,
currentOrgId: currentOrgId,
currentReply: active.CurrentReplyContent,
currentReplyNumTokens: active.NumTokens,
modelPackName: settings.GetModelPack().Name,
}, active.SummaryCtx)
if err != nil {
log.Printf("Error summarizing convo: %v\n", err)
active.StreamDoneCh <- err
}
}()
log.Println("Sending active.CurrentReplyDoneCh <- true")
active.CurrentReplyDoneCh <- true
log.Println("Resetting active.CurrentReplyDoneCh")
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.CurrentStreamingReplyId = ""
ap.CurrentReplyDoneCh = nil
})
autoLoadPaths := autoLoadContextResult.autoLoadPaths
log.Printf("len(autoLoadPaths): %d\n", len(autoLoadPaths))
if len(autoLoadPaths) > 0 {
log.Println("Sending stream message to load context files")
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic streaming auto-load context: %v\n%s", r, debug.Stack())
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic streaming auto-load context: %v\n%s", r, debug.Stack()))
}
}()
active.Stream(shared.StreamMessage{
Type: shared.StreamMessageLoadContext,
LoadContextFiles: autoLoadPaths,
})
active.FlushStreamBuffer()
}()
log.Println("Waiting for client to auto load context (30s timeout)")
select {
case <-active.Ctx.Done():
log.Println("Context cancelled while waiting for auto load context")
state.execHookOnStop(false)
return handleStreamFinishedResult{
shouldContinueMainLoop: false,
shouldReturn: true,
}
case <-time.After(30 * time.Second):
log.Println("Timeout waiting for auto load context")
res := state.onError(onErrorParams{
streamErr: fmt.Errorf("timeout waiting for auto load context response"),
storeDesc: true,
})
return handleStreamFinishedResult{
shouldContinueMainLoop: res.shouldContinueMainLoop,
shouldReturn: res.shouldReturn,
}
case <-active.AutoLoadContextCh:
}
}
willContinue := state.willContinuePlan(willContinuePlanParams{
hasNewSubtasks: hasExplicitTasks,
allSubtasksFinished: allSubtasksFinished,
activatePaths: autoLoadContextResult.activatePaths,
removedSubtasks: hasExplicitRemoveTasks,
hasExplicitPaths: autoLoadContextResult.hasExplicitPaths,
})
if willContinue {
log.Println("Auto continue plan")
// continue plan
execTellPlan(execTellPlanParams{
clients: clients,
plan: plan,
branch: branch,
auth: auth,
req: req,
iteration: iteration + 1,
authVars: authVars,
})
} else {
var buildFinished bool
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
buildFinished = ap.BuildFinished()
ap.RepliesFinished = true
})
log.Printf("Won't continue plan. Build finished: %v\n", buildFinished)
time.Sleep(50 * time.Millisecond)
if buildFinished {
log.Println("Reply is finished and build is finished, calling active.Finish()")
active := GetActivePlan(planId, branch)
if active == nil {
state.onActivePlanMissingError()
return handleStreamFinishedResult{
shouldContinueMainLoop: true,
shouldReturn: false,
}
}
active.Finish()
} else {
log.Println("Plan is still building")
log.Println("Updating status to building")
err := db.SetPlanStatus(planId, branch, shared.PlanStatusBuilding, "")
if err != nil {
log.Printf("Error setting plan status to building: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error setting plan status to building: %v", err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error setting plan status to building: %v", err),
}
return handleStreamFinishedResult{
shouldContinueMainLoop: true,
shouldReturn: false,
}
}
log.Println("Sending RepliesFinished stream message")
active.Stream(shared.StreamMessage{
Type: shared.StreamMessageRepliesFinished,
})
}
}
return handleStreamFinishedResult{}
}
================================================
FILE: app/server/model/plan/tell_stream_main.go
================================================
package plan
import (
"errors"
"fmt"
"log"
"net/http"
"plandex-server/model"
"plandex-server/notify"
"plandex-server/types"
"runtime/debug"
"strings"
"time"
shared "plandex-shared"
"github.com/davecgh/go-spew/spew"
)
func (state *activeTellStreamState) listenStream(stream *model.ExtendedChatCompletionStream) {
defer stream.Close()
plan := state.plan
planId := plan.Id
branch := state.branch
active := GetActivePlan(planId, branch)
if active == nil {
log.Printf("listenStream - Active plan not found for plan ID %s on branch %s\n", planId, branch)
return
}
defer func() {
if r := recover(); r != nil {
log.Printf("listenStream: Panic: %v\n%s\n", r, string(debug.Stack()))
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("listenStream: Panic: %v\n%s", r, string(debug.Stack())))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Panic in listenStream",
}
}
}()
state.chunkProcessor = &chunkProcessor{
replyOperations: []*shared.Operation{},
chunksReceived: 0,
maybeRedundantOpeningTagContent: "",
fileOpen: false,
contentBuffer: "",
awaitingBlockOpeningTag: false,
awaitingBlockClosingTag: false,
awaitingBackticks: false,
}
// Create a timer that will trigger if no chunk is received within the specified duration
firstTokenTimeout := firstTokenTimeout(state.totalRequestTokens, state.baseModelConfig.LocalOnly)
log.Printf("listenStream - firstTokenTimeout: %s\n", firstTokenTimeout)
timer := time.NewTimer(firstTokenTimeout)
defer timer.Stop()
streamFinished := false
baseModelConfig := state.modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)
modelProvider := baseModelConfig.Provider
modelName := baseModelConfig.ModelName
respCh := make(chan *types.ExtendedChatCompletionStreamResponse)
streamErrCh := make(chan error)
// receive chunks from the stream in a separate goroutine so that we can handle errors and timeouts — needed because stream.Recv() blocks forever
go func() {
for {
resp, err := stream.Recv()
if err != nil {
streamErrCh <- err
return
}
respCh <- resp
}
}()
mainLoop:
for {
select {
case <-active.Ctx.Done():
// The main modelContext was canceled (not the timer)
log.Println("\nTell: stream canceled")
state.execHookOnStop(false)
return
case <-timer.C:
// Timer triggered because no new chunk was received in time
log.Println("\nTell: stream timeout due to inactivity")
if streamFinished {
log.Println("Tell stream finished—timed out waiting for usage chunk")
state.execHookOnStop(false)
return
} else {
res := state.onError(onErrorParams{
streamErr: fmt.Errorf("stream timeout due to inactivity: The AI model (%s/%s) is not responding", modelProvider, modelName),
storeDesc: true,
canRetry: active.CurrentReplyContent == "", // if there was no output yet, we can retry
})
if res.shouldReturn {
return
}
if res.shouldContinueMainLoop {
continue mainLoop
}
}
case err := <-streamErrCh:
log.Printf("listenStream - received from streamErrCh: %v\n", err)
if err.Error() == "context canceled" {
log.Println("Tell: stream context canceled")
state.execHookOnStop(false)
return
}
log.Printf("Tell: error receiving stream chunk: %v\n", err)
state.execHookOnStop(true)
var msg string
name := modelName
if !strings.Contains(string(modelName), string(modelProvider)) {
name = shared.ModelName(fmt.Sprintf("%s/%s", modelProvider, modelName))
}
if active.CurrentReplyContent == "" {
msg = fmt.Sprintf("The AI model (%s) didn't respond: %v", name, err)
} else {
msg = fmt.Sprintf("The AI model (%s) stopped responding: %v", name, err)
}
state.onError(onErrorParams{
streamErr: errors.New(msg),
storeDesc: true,
canRetry: active.CurrentReplyContent == "", // if there was no output yet, we can retry
})
// here we want to return no matter what -- state.onError will decide whether to retry or not
return
case response := <-respCh:
// Successfully received a chunk, reset the timer
if !timer.Stop() {
<-timer.C
}
timer.Reset(model.ACTIVE_STREAM_CHUNK_TIMEOUT)
// log.Println("tell stream main: received stream response", spew.Sdump(response))
if response.ID != "" && state.generationId == "" {
state.generationId = response.ID
}
if state.firstTokenAt.IsZero() {
state.firstTokenAt = time.Now()
}
if response.Error != nil {
log.Println("listenStream - stream finished with error", spew.Sdump(response.Error))
baseModelConfig := state.fallbackRes.BaseModelConfig
modelErr := model.ClassifyModelError(response.Error.Code, response.Error.Message, nil, baseModelConfig.HasClaudeMaxAuth)
res := state.onError(onErrorParams{
streamErr: fmt.Errorf("The AI model (%s/%s) stopped streaming with error code %d: %s", modelProvider, modelName, response.Error.Code, response.Error.Message),
storeDesc: true,
canRetry: active.CurrentReplyContent == "",
modelErr: &modelErr,
})
if res.shouldReturn {
return
}
if res.shouldContinueMainLoop {
continue mainLoop
}
}
if len(response.Choices) == 0 {
if response.Usage != nil {
state.handleUsageChunk(response.Usage)
return
}
log.Println("listenStream - stream finished with no choices", spew.Sdump(response))
// Previously we'd return an error if there were no choices, but some models do this and then keep streaming, so we'll just log it and continue, waiting for an EOF if there's a problem
// res := state.onError(onErrorParams{
// streamErr: fmt.Errorf("stream finished with no choices | The model failed to generate a valid response."),
// storeDesc: true,
// canRetry: true,
// })
// if res.shouldReturn {
// return
// }
// if res.shouldContinueMainLoop {
// // continue instead of returning so that context cancellation is handled
// continue mainLoop
// }
continue mainLoop
}
choice := response.Choices[0]
processChunkRes := state.processChunk(choice)
if processChunkRes.shouldReturn {
return
}
handleFinished := func() handleStreamFinishedResult {
streamFinishResult := state.handleStreamFinished()
if streamFinishResult.shouldReturn || streamFinishResult.shouldContinueMainLoop {
return streamFinishResult
}
// usage can either be included in the final chunk (openrouter) or in a separate chunk (openai)
// if the usage chunk is included, handle it and then return out of listener
// otherwise keep listening for the usage chunk
if response.Usage != nil {
state.handleUsageChunk(response.Usage)
return handleStreamFinishedResult{
shouldReturn: true,
}
}
// Reset the timer for the usage chunk
if !timer.Stop() {
<-timer.C
}
timer.Reset(model.USAGE_CHUNK_TIMEOUT)
streamFinished = true
return handleStreamFinishedResult{
shouldContinueMainLoop: true,
}
}
if processChunkRes.shouldStop {
log.Println("Model stream reached stop sequence")
res := handleFinished()
if res.shouldReturn {
return
}
continue
}
if choice.FinishReason != "" {
log.Println("Model stream finished")
log.Println("Finish reason: ", choice.FinishReason)
if choice.FinishReason == "error" {
log.Println("Model stream finished with error")
res := state.onError(onErrorParams{
streamErr: fmt.Errorf("The AI model (%s/%s) stopped streaming with an error status", modelProvider, modelName),
storeDesc: true,
canRetry: active.CurrentReplyContent == "",
})
if res.shouldReturn {
return
}
if res.shouldContinueMainLoop {
continue mainLoop
}
}
res := handleFinished()
if res.shouldReturn {
return
}
continue
} else if response.Usage != nil {
state.handleUsageChunk(response.Usage)
return
}
// let main loop continue
}
}
}
func firstTokenTimeout(tok int, isLocalModel bool) time.Duration {
const (
base = 90 * time.Second
slope = 90 * time.Second
step = 150_000
cap = 15 * time.Minute
)
// local models can have a long cold start, and timeouts are less relevant
if isLocalModel {
return cap
}
if tok <= step {
return base
}
extra := time.Duration((tok-step)/step) * slope
if extra > cap-base {
extra = cap - base
}
return base + extra
}
================================================
FILE: app/server/model/plan/tell_stream_processor.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/notify"
"plandex-server/types"
"regexp"
"runtime/debug"
"strings"
"time"
shared "plandex-shared"
"github.com/davecgh/go-spew/spew"
)
const verboseLogging = false
var openingTagRegex = regexp.MustCompile(``)
type processChunkResult struct {
shouldReturn bool
shouldStop bool
}
func (state *activeTellStreamState) processChunk(choice types.ExtendedChatCompletionStreamChoice) processChunkResult {
req := state.req
// missingFileResponse := state.missingFileResponse
processor := state.chunkProcessor
replyParser := state.replyParser
plan := state.plan
planId := plan.Id
branch := state.branch
active := GetActivePlan(planId, branch)
if active == nil {
state.onActivePlanMissingError()
return processChunkResult{}
}
defer func() {
if r := recover(); r != nil {
log.Printf("processChunk: Panic: %v\n%s\n", r, string(debug.Stack()))
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("processChunk: Panic: %v\n%s", r, string(debug.Stack())))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Panic in processChunk: %v\n%s", r, string(debug.Stack())),
}
}
}()
delta := choice.Delta
content := delta.Content
baseModelConfig := state.modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)
if baseModelConfig.IncludeReasoning && !baseModelConfig.HideReasoning && delta.Reasoning != "" {
content = delta.Reasoning
}
if content == "" {
return processChunkResult{}
}
processor.chunksReceived++
if verboseLogging {
log.Printf("Adding chunk to parser: %s\n", content)
log.Printf("fileOpen: %v\n", processor.fileOpen)
}
replyParser.AddChunk(content, true)
parserRes := replyParser.Read()
if !processor.fileOpen && parserRes.CurrentFilePath != "" {
if verboseLogging {
log.Printf("File open: %s\n", parserRes.CurrentFilePath)
}
processor.fileOpen = true
}
if processor.fileOpen && strings.HasSuffix(active.CurrentReplyContent+content, "") {
if verboseLogging {
log.Println("FinishAndRead because of closing tag")
}
parserRes = replyParser.FinishAndRead()
processor.fileOpen = false
}
if processor.fileOpen && parserRes.CurrentFilePath == "" {
if verboseLogging {
log.Println("File open but current file path is empty, closing file")
}
processor.fileOpen = false
}
operations := parserRes.Operations
state.replyNumTokens = parserRes.TotalTokens
currentFile := parserRes.CurrentFilePath
// log.Printf("currentFile: %s\n", currentFile)
// log.Println("files:")
// spew.Dump(files)
// Handle file that is present in project paths but not in context
// Prompt user for what to do on the client side, stop the stream, and wait for user response before proceeding
bufferOrStreamRes := processor.bufferOrStream(content, &parserRes, state.currentStage, state.manualStop)
if currentFile != "" &&
!req.IsChatOnly &&
active.ContextsByPath[currentFile] == nil &&
req.ProjectPaths[currentFile] &&
!active.AllowOverwritePaths[currentFile] {
return state.handleMissingFile(bufferOrStreamRes.content, currentFile, bufferOrStreamRes.blockLang)
}
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.CurrentReplyContent += content
ap.NumTokens++
})
if verboseLogging {
log.Println("processor before bufferOrStream")
spew.Dump(processor)
log.Println("maybeFilePath", parserRes.MaybeFilePath)
log.Println("currentFilePath", parserRes.CurrentFilePath)
log.Println("bufferOrStreamRes")
spew.Dump(bufferOrStreamRes)
}
if bufferOrStreamRes.shouldStream {
active.Stream(shared.StreamMessage{
Type: shared.StreamMessageReply,
ReplyChunk: bufferOrStreamRes.content,
})
}
if verboseLogging {
log.Println("processor after bufferOrStream")
spew.Dump(processor)
}
if !req.IsChatOnly && len(operations) > len(processor.replyOperations) {
state.handleNewOperations(&parserRes)
}
return processChunkResult{
shouldStop: bufferOrStreamRes.shouldStop,
}
}
type bufferOrStreamResult struct {
shouldStream bool
content string
blockLang string
shouldStop bool
}
func (processor *chunkProcessor) bufferOrStream(content string, parserRes *types.ReplyParserRes, currentStage shared.CurrentStage, manualStopSequences []string) bufferOrStreamResult {
if len(manualStopSequences) > 0 {
for _, stopSequence := range manualStopSequences {
// if the chunk contains the entire stop sequence, stream everything before it then caller can stop the stream
if strings.Contains(content, stopSequence) {
split := strings.Split(content, stopSequence)
if len(split) > 1 {
return bufferOrStreamResult{
shouldStream: true,
content: split[0],
shouldStop: true,
}
} else {
// there was nothing before the stop sequence, so nothing to stream
return bufferOrStreamResult{
shouldStream: false,
shouldStop: true,
}
}
}
// otherwise if the buffer plus chunk contains the stop sequence, don't stream anything and stop the stream
if strings.Contains(processor.contentBuffer+content, stopSequence) {
log.Printf("bufferOrStream - stop sequence found in buffer plus chunk\n")
split := strings.Split(content, stopSequence)
if len(split) > 1 {
// we'll stream the part before the stop sequence
return bufferOrStreamResult{
shouldStream: true,
content: split[0],
shouldStop: true,
}
} else {
// there was nothing before the stop sequence, so nothing to stream
return bufferOrStreamResult{
shouldStream: false,
shouldStop: true,
}
}
}
// otherwise if the buffer plus chunk ends with a prefix of the stop sequence, buffer it and continue
toCheck := processor.contentBuffer + content
tailLen := len(stopSequence) - 1
if tailLen > len(toCheck) {
tailLen = len(toCheck)
}
suffix := toCheck[len(toCheck)-tailLen:]
if strings.HasPrefix(stopSequence, suffix) {
log.Printf("bufferOrStream - stop sequence prefix found in buffer plus chunk. buffer and continue\n")
processor.contentBuffer += content
return bufferOrStreamResult{
shouldStream: false,
content: content,
}
}
}
}
// apart from manual stop sequences, no buffering in planning stages
if currentStage.TellStage == shared.TellStagePlanning {
return bufferOrStreamResult{
shouldStream: true,
content: content,
}
}
var shouldStream bool
var blockLang string
awaitingTag := processor.awaitingBlockOpeningTag || processor.awaitingBlockClosingTag || processor.awaitingOpClosingTag
awaitingAny := awaitingTag || processor.awaitingBackticks
if awaitingAny {
if verboseLogging {
log.Println("awaitingAny")
}
processor.contentBuffer += content
content = processor.contentBuffer
if verboseLogging {
log.Printf("awaitingBlockOpeningTag: %v\n", processor.awaitingBlockOpeningTag)
log.Printf("awaitingBlockClosingTag: %v\n", processor.awaitingBlockClosingTag)
log.Printf("awaitingBackticks: %v\n", processor.awaitingBackticks)
log.Printf("awaitingOpClosingTag: %v\n", processor.awaitingOpClosingTag)
log.Printf("content: %q\n", content)
}
}
if processor.awaitingBackticks {
if strings.Contains(content, "```") {
processor.awaitingBackticks = false
content = strings.ReplaceAll(content, "```", "\\`\\`\\`")
if !(processor.awaitingBlockOpeningTag || processor.awaitingBlockClosingTag) {
shouldStream = true
}
} else if !strings.HasSuffix(content, "`") {
// fewer than 3 backticks, no need to escape
processor.awaitingBackticks = false
if !(processor.awaitingBlockOpeningTag || processor.awaitingBlockClosingTag) {
shouldStream = true
}
}
}
if awaitingTag {
if verboseLogging {
log.Println("awaitingTag")
}
if processor.awaitingBlockOpeningTag {
if verboseLogging {
log.Println("processor.awaitingBlockOpeningTag")
}
var matchedPrefix bool
if parserRes.CurrentFilePath != "" {
matched, replaced := replaceCodeBlockOpeningTag(content, func(lang string) string {
blockLang = lang
return "```" + lang
})
if matched {
shouldStream = true
processor.awaitingBlockOpeningTag = false
processor.fileOpen = true
content = replaced
} else {
// tag is missing - something is wrong - we shouldn't be here but let's try to recover anyway
if verboseLogging {
log.Printf("Opening tag is missing even though parserRes.CurrentFile is set - something is wrong: %s\n", content)
}
processor.awaitingBlockOpeningTag = false
processor.fileOpen = false
content += "\n```" // add ``` to the end of the line to close the markdown code block
shouldStream = true
}
} else {
split := strings.Split(content, "<")
if len(split) > 1 {
last := split[len(split)-1]
if verboseLogging {
log.Printf("last: %s\n", last)
}
if strings.HasPrefix(`PlandexBlock lang="`, last) {
if verboseLogging {
log.Println("strings.HasPrefix(`PlandexBlock lang=", last)
}
shouldStream = false
matchedPrefix = true
} else if strings.HasPrefix(last, `PlandexBlock lang="`) {
if verboseLogging {
log.Println("partialOpeningTagRegex.MatchString(last)")
}
shouldStream = false
matchedPrefix = true
} else {
if verboseLogging {
log.Println("partialOpeningTagRegex.MatchString(last) is false")
}
}
}
}
if !matchedPrefix && parserRes.MaybeFilePath == "" && parserRes.CurrentFilePath == "" {
// wasn't really a file path / code block
processor.awaitingBlockOpeningTag = false
shouldStream = true
}
} else if processor.awaitingBlockClosingTag {
if parserRes.CurrentFilePath == "" {
if strings.Contains(content, "") {
shouldStream = true
processor.awaitingBlockClosingTag = false
processor.fileOpen = false
// replace with ``` to close the markdown code block
content = strings.ReplaceAll(content, "", "```")
} else {
log.Printf("Closing tag is missing even though parserRes.CurrentOperation is nil - something is wrong: %s\n", content)
processor.awaitingBlockClosingTag = false
shouldStream = true
}
}
} else if processor.awaitingOpClosingTag {
if verboseLogging {
log.Printf("awaitingOpClosingTag: %v\n", processor.awaitingOpClosingTag)
}
if strings.Contains(content, "") {
if verboseLogging {
log.Printf("Found \n")
}
processor.awaitingOpClosingTag = false
content = strings.Replace(content, "\n", "", 1)
content = strings.Replace(content, "", "", 1)
shouldStream = true
}
}
} else {
if verboseLogging {
log.Println("not awaiting tag")
}
if parserRes.MaybeFilePath != "" && parserRes.CurrentFilePath == "" {
processor.awaitingBlockOpeningTag = true
} else {
// this will set processor.awaitingBlockOpeningTag to true if the content starts with any prefix of 1 {
last := split[len(split)-1]
if strings.HasPrefix(`PlandexBlock lang="`, last) {
processor.awaitingBlockOpeningTag = true
} else if strings.HasPrefix(last, `PlandexBlock lang="`) {
processor.awaitingBlockOpeningTag = true
}
}
}
if parserRes.CurrentFilePath != "" {
if verboseLogging {
log.Println("parserRes.CurrentFilePath != \"\"")
}
if strings.Contains(content, "") {
if verboseLogging {
log.Println("strings.Contains(content, \"\")")
}
processor.awaitingBlockClosingTag = true
} else {
if verboseLogging {
log.Println("not strings.Contains(content, \"\")")
}
split := strings.Split(content, "<")
// log.Printf("split: %v\n", split)
if len(split) > 1 {
if verboseLogging {
log.Println("len(split) > 1")
}
last := split[len(split)-1]
// log.Printf("last: %s\n", last)
if strings.HasPrefix("/PlandexBlock>", last) {
if verboseLogging {
log.Println("strings.HasPrefix(\"/PlandexBlock>\", last)")
}
processor.awaitingBlockClosingTag = true
}
}
}
} else if parserRes.FileOperationBlockOpen() {
if verboseLogging {
log.Println("parserRes.FileOperationBlockOpen()")
}
if strings.Contains(content, "") {
if verboseLogging {
log.Println("strings.Contains(content, \"\")")
}
processor.awaitingOpClosingTag = true
} else {
if verboseLogging {
log.Println("not strings.Contains(content, \"\")")
}
split := strings.Split(content, "<")
if len(split) > 1 {
if verboseLogging {
log.Println("len(split) > 1")
}
last := split[len(split)-1]
if strings.HasPrefix("EndPlandexFileOps/>", last) {
if verboseLogging {
log.Println("strings.HasPrefix(\"EndPlandexFileOps/>\", last)")
}
processor.awaitingOpClosingTag = true
}
}
}
} else if strings.Contains(content, "") {
if verboseLogging {
log.Println("strings.Contains(content, \"\")")
}
content = strings.Replace(content, "", "```", 1)
} else if strings.Contains(content, "") {
if verboseLogging {
log.Println("strings.Contains(content, \"\")")
}
content = strings.Replace(content, "\n", "", 1)
content = strings.Replace(content, "", "", 1)
}
if processor.fileOpen && (strings.Contains(content, "```") || strings.HasSuffix(content, "`")) {
if verboseLogging {
log.Println("processor.fileOpen && (strings.Contains(content, \"```\") || strings.HasSuffix(content, \"`\"))")
}
processor.awaitingBackticks = true
}
var matchedOpeningTag bool
if processor.fileOpen {
if verboseLogging {
log.Println("processor.fileOpen")
}
var replaced string
matchedOpeningTag, replaced = replaceCodeBlockOpeningTag(content, func(lang string) string {
blockLang = lang
return "```" + lang
})
if verboseLogging {
log.Println("matchedOpeningTag", matchedOpeningTag)
log.Println("replaced", replaced)
}
if matchedOpeningTag {
processor.awaitingBlockOpeningTag = false
content = replaced
}
}
shouldStream = !processor.awaitingBlockOpeningTag && !processor.awaitingBlockClosingTag && !processor.awaitingOpClosingTag && !processor.awaitingBackticks
if verboseLogging {
log.Println("processor.awaitingBlockOpeningTag", processor.awaitingBlockOpeningTag)
log.Println("processor.awaitingBlockClosingTag", processor.awaitingBlockClosingTag)
log.Println("processor.awaitingOpClosingTag", processor.awaitingOpClosingTag)
log.Println("processor.awaitingBackticks", processor.awaitingBackticks)
log.Println("shouldStream", shouldStream)
}
}
if verboseLogging {
log.Println("returning bufferOrStreamResult")
log.Println("shouldStream", shouldStream)
log.Println("content", content)
log.Println("blockLang", blockLang)
}
if shouldStream {
processor.contentBuffer = ""
} else {
processor.contentBuffer = content
}
return bufferOrStreamResult{
shouldStream: shouldStream,
content: content,
blockLang: blockLang,
}
}
func (state *activeTellStreamState) handleNewOperations(parserRes *types.ReplyParserRes) {
processor := state.chunkProcessor
plan := state.plan
planId := plan.Id
branch := state.branch
clients := state.clients
auth := state.auth
authVars := state.authVars
req := state.req
replyId := state.replyId
currentOrgId := state.currentOrgId
currentUserId := state.currentUserId
settings := state.settings
operations := parserRes.Operations
log.Printf("%d new operations\n", len(operations)-len(processor.replyOperations))
for i, op := range operations {
if i < len(processor.replyOperations) {
continue
}
log.Printf("Detected operation: %s\n", op.Name())
if req.BuildMode == shared.BuildModeAuto {
log.Printf("Queuing build for %s\n", op.Name())
// log.Println("Content:")
// log.Println(strconv.Quote(op.Content))
buildState := &activeBuildStreamState{
modelStreamId: state.modelStreamId,
clients: clients,
authVars: authVars,
auth: auth,
currentOrgId: currentOrgId,
currentUserId: currentUserId,
plan: plan,
branch: branch,
settings: settings,
modelContext: state.modelContext,
orgUserConfig: state.orgUserConfig,
}
var opContentTokens int
if op.Type == shared.OperationTypeFile {
opContentTokens = shared.GetNumTokensEstimate(op.Content)
} else {
opContentTokens = op.NumTokens
}
// log.Printf("buildState.queueBuilds - op.Description:\n%s\n", op.Description)
buildState.queueBuilds([]*types.ActiveBuild{{
ReplyId: replyId,
FileDescription: op.Description,
FileContent: op.Content,
FileContentTokens: opContentTokens,
Path: op.Path,
MoveDestination: op.Destination,
IsMoveOp: op.Type == shared.OperationTypeMove,
IsRemoveOp: op.Type == shared.OperationTypeRemove,
IsResetOp: op.Type == shared.OperationTypeReset,
}})
}
processor.replyOperations = append(processor.replyOperations, op)
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.Operations = append(ap.Operations, op)
})
}
}
func (state *activeTellStreamState) handleMissingFile(content, currentFile, blockLang string) processChunkResult {
branch := state.branch
plan := state.plan
planId := plan.Id
replyParser := state.replyParser
iteration := state.iteration
clients := state.clients
auth := state.auth
req := state.req
authVars := state.authVars
active := GetActivePlan(planId, branch)
if active == nil {
state.onActivePlanMissingError()
return processChunkResult{}
}
log.Printf("Attempting to overwrite a file that isn't in context: %s\n", currentFile)
// attempting to overwrite a file that isn't in context
// we will stop the stream and ask the user what to do
err := db.SetPlanStatus(planId, branch, shared.PlanStatusMissingFile, "")
if err != nil {
log.Printf("Error setting plan %s status to prompting: %v\n", planId, err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error setting plan %s status to prompting: %v", planId, err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error setting plan status to prompting: %v", err),
}
return processChunkResult{}
}
var trimmedReply string
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.MissingFilePath = currentFile
trimmedReply = replyParser.GetReplyForMissingFile()
ap.CurrentReplyContent = trimmedReply
})
// log.Println("Content:")
// log.Println(content)
// log.Println("Block lang:")
// log.Println(blockLang)
// log.Println("Trimmed content:")
// log.Println(trimmedReply)
// try to replace the code block opening tag in the chunk with an empty string
// this will remove the code block opening tag if it exists
splitBy := "```" + blockLang
split := strings.Split(content, splitBy)
chunkToStream := split[0] + splitBy + "\n"
// log.Printf("chunkToStream: %s\n", chunkToStream)
if chunkToStream != "" {
log.Printf("Streaming remaining chunk before missing file prompt: %s\n", chunkToStream)
active.Stream(shared.StreamMessage{
Type: shared.StreamMessageReply,
ReplyChunk: chunkToStream,
})
active.FlushStreamBuffer()
time.Sleep(20 * time.Millisecond)
}
log.Printf("Prompting user for missing file: %s\n", currentFile)
active.Stream(shared.StreamMessage{
Type: shared.StreamMessagePromptMissingFile,
MissingFilePath: currentFile,
MissingFileAutoContext: active.AutoContext,
})
log.Printf("Stopping stream for missing file: %s\n", currentFile)
// log.Printf("Chunk content: %s\n", content)
// log.Printf("Current reply content: %s\n", active.CurrentReplyContent)
// stop stream for now
active.CancelModelStreamFn()
log.Printf("Stopped stream for missing file: %s\n", currentFile)
// wait for user response to come in
var userChoice shared.RespondMissingFileChoice
select {
case <-active.Ctx.Done():
log.Println("Context cancelled while waiting for missing file response")
state.execHookOnStop(false)
return processChunkResult{shouldReturn: true}
case <-time.After(30 * time.Minute): // long timeout here since we're waiting for user input
log.Println("Timeout waiting for missing file choice")
state.onError(onErrorParams{
streamErr: fmt.Errorf("timeout waiting for missing file choice"),
storeDesc: true,
})
return processChunkResult{}
case userChoice = <-active.MissingFileResponseCh:
}
log.Printf("User choice for missing file: %s\n", userChoice)
active.ResetModelCtx()
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.MissingFilePath = ""
ap.CurrentReplyContent = replyParser.GetReplyForMissingFile()
})
log.Println("Continuing stream")
// continue plan
execTellPlan(execTellPlanParams{
clients: clients,
plan: plan,
branch: branch,
auth: auth,
req: req,
iteration: iteration, // keep the same iteration
missingFileResponse: userChoice,
authVars: authVars,
})
return processChunkResult{shouldReturn: true}
}
func getCroppedChunk(uncropped, cropped, chunk string) string {
uncroppedIdx := strings.Index(uncropped, chunk)
if uncroppedIdx == -1 {
return ""
}
croppedChunk := cropped[uncroppedIdx:]
return croppedChunk
}
func replaceCodeBlockOpeningTag(content string, replaceWithFn func(lang string) string) (bool, string) {
// check for opening tag matching
match := openingTagRegex.FindStringSubmatch(content)
if match != nil {
// Found complete opening tag with lang and path attributes
lang := match[1] // Extract the language from the first capture group
return true, strings.Replace(content, match[0], replaceWithFn(lang), 1)
} else if strings.Contains(content, "") {
// This is a fallback case that should probably be removed since we now require both attributes
return true, strings.Replace(content, "", replaceWithFn(""), 1)
}
return false, ""
}
================================================
FILE: app/server/model/plan/tell_stream_processor_test.go
================================================
package plan
import (
"plandex-server/types"
shared "plandex-shared"
"testing"
)
func TestBufferOrStream(t *testing.T) {
tests := []struct {
only bool
name string
initialState *chunkProcessor
chunk string
maybeFilePath string
currentFilePath string
isInMoveBlock bool
isInRemoveBlock bool
isInResetBlock bool
want bufferOrStreamResult
wantState *chunkProcessor // To verify state transitions
manualStop []string
}{
{
name: "streams regular content",
initialState: &chunkProcessor{
contentBuffer: "",
},
chunk: "some regular text",
want: bufferOrStreamResult{
shouldStream: true,
content: "some regular text",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
awaitingBlockClosingTag: false,
awaitingBackticks: false,
fileOpen: false,
},
},
{
name: "buffers partial opening tag",
initialState: &chunkProcessor{
awaitingBlockOpeningTag: true,
fileOpen: false,
contentBuffer: "",
},
chunk: `` + "\n",
awaitingBlockOpeningTag: true,
},
chunk: `package`,
maybeFilePath: "",
currentFilePath: "main.go",
want: bufferOrStreamResult{
shouldStream: true,
content: "```go\npackage",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
awaitingBlockClosingTag: false,
awaitingBackticks: false,
fileOpen: true,
},
},
{
// occurs when replayParser can't identify a 'maybeFilePath' prior a full opening tag being sent ('maybeFilePath' gets skipped and 'currentFilePath' is set immediately)
name: "converts opening tag without awaitingOpeningTag",
initialState: &chunkProcessor{
fileOpen: true,
contentBuffer: "",
awaitingBlockOpeningTag: false,
},
chunk: `` + "\npackage",
maybeFilePath: "",
currentFilePath: "main.go",
want: bufferOrStreamResult{
shouldStream: true,
content: "```go\npackage",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
awaitingBlockClosingTag: false,
awaitingBackticks: false,
fileOpen: true,
},
},
{
name: "buffers partial backticks",
initialState: &chunkProcessor{
fileOpen: true,
contentBuffer: "here's some co",
},
chunk: "de:`",
currentFilePath: "main.go",
want: bufferOrStreamResult{
shouldStream: false,
},
wantState: &chunkProcessor{
awaitingBackticks: true,
fileOpen: true,
},
},
{
name: "escapes backticks in content",
initialState: &chunkProcessor{
fileOpen: true,
awaitingBackticks: true,
contentBuffer: "here's some code:\n`",
},
chunk: "``\npackage",
currentFilePath: "main.go",
want: bufferOrStreamResult{
shouldStream: true,
content: "here's some code:\n\\`\\`\\`\npackage",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
awaitingBlockClosingTag: false,
awaitingBackticks: false,
fileOpen: true,
},
},
{
name: "buffers partial closing tag",
initialState: &chunkProcessor{
fileOpen: true,
awaitingBlockClosingTag: false,
contentBuffer: "",
},
currentFilePath: "main.go",
chunk: "\n}",
want: bufferOrStreamResult{
shouldStream: false,
},
wantState: &chunkProcessor{
awaitingBlockClosingTag: true,
fileOpen: true,
contentBuffer: "\n}",
},
},
{
name: "replaces full closing tag with file closed",
initialState: &chunkProcessor{
fileOpen: false,
awaitingBlockClosingTag: false,
contentBuffer: "",
},
currentFilePath: "",
chunk: "\n}",
want: bufferOrStreamResult{
shouldStream: true,
content: "\n}```",
},
wantState: &chunkProcessor{
awaitingBlockClosingTag: false,
fileOpen: false,
},
},
{
name: "replaces full closing tag with file closed and awaiting backticks",
initialState: &chunkProcessor{
fileOpen: false,
awaitingBlockClosingTag: false,
awaitingBackticks: true,
contentBuffer: "",
},
currentFilePath: "",
chunk: " ONLY this one-line title and nothing else.`\n\n\nNow let",
want: bufferOrStreamResult{
shouldStream: true,
content: " ONLY this one-line title and nothing else.`\n```\n\nNow let",
},
wantState: &chunkProcessor{
awaitingBlockClosingTag: false,
fileOpen: false,
},
},
{
name: "handles single backticks",
initialState: &chunkProcessor{
fileOpen: true,
awaitingBackticks: true,
contentBuffer: "`file.go`",
},
chunk: "\nsomething",
currentFilePath: "main.go",
want: bufferOrStreamResult{
shouldStream: true,
content: "`file.go`\nsomething",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
awaitingBlockClosingTag: false,
awaitingBackticks: false,
fileOpen: true,
},
},
{
name: "handles close and re-open backticks",
initialState: &chunkProcessor{
fileOpen: true,
awaitingBackticks: true,
contentBuffer: "`file.go`",
},
chunk: "\n`file2.go`",
currentFilePath: "main.go",
want: bufferOrStreamResult{
shouldStream: false,
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
awaitingBlockClosingTag: false,
awaitingBackticks: true,
fileOpen: true,
contentBuffer: "`file.go`\n`file2.go`",
},
},
{
name: "buffers for end of file operations",
initialState: &chunkProcessor{},
isInMoveBlock: true,
chunk: "\n\nmore",
want: bufferOrStreamResult{
shouldStream: false,
},
wantState: &chunkProcessor{
awaitingOpClosingTag: true,
contentBuffer: "\n\nmore",
},
},
{
name: "replaces full end of file operations tag",
initialState: &chunkProcessor{
awaitingOpClosingTag: true,
contentBuffer: "\n\nmore",
},
chunk: " stuff",
want: bufferOrStreamResult{
shouldStream: true,
content: "\nmore stuff",
},
wantState: &chunkProcessor{
awaitingOpClosingTag: false,
},
},
{
name: "buffers for end of file operations with partial tag",
initialState: &chunkProcessor{
awaitingOpClosingTag: true,
},
chunk: "\n\nmore",
want: bufferOrStreamResult{
shouldStream: true,
content: "\nmore",
},
wantState: &chunkProcessor{
awaitingOpClosingTag: false,
},
},
{
name: "buffers for partial opening tag with no file path label",
initialState: &chunkProcessor{},
chunk: "something\npackage",
want: bufferOrStreamResult{
shouldStream: true,
content: "something\n```go\npackage",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
fileOpen: true,
},
},
{
name: "replaces full opening tag without file path label",
initialState: &chunkProcessor{
fileOpen: true,
},
currentFilePath: "main.go",
chunk: "something\n\npackage",
want: bufferOrStreamResult{
shouldStream: true,
content: "something\n```go\npackage",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
fileOpen: true,
},
},
{
name: "stop tag entirely in one chunk",
initialState: &chunkProcessor{}, // empty buffer
chunk: "hello bye",
manualStop: []string{""},
want: bufferOrStreamResult{
shouldStream: true, // stream only the prefix
content: "hello ", // text before the tag
shouldStop: true, // tell caller to stop
},
wantState: &chunkProcessor{
contentBuffer: "", // nothing left buffered
},
},
{
name: "stop tag split across two chunks (prefix + rest)",
only: true, // helper if you want to run just this one
initialState: &chunkProcessor{
contentBuffer: "", // begins empty
},
// FIRST CHUNK —— just a proper prefix
chunk: ""},
want: bufferOrStreamResult{
shouldStream: false, // nothing streams yet
shouldStop: false, // not complete, keep going
},
wantState: &chunkProcessor{
contentBuffer: "\nmore text", // completes tag + trailing text
manualStop: []string{""},
want: bufferOrStreamResult{
shouldStream: false, // do NOT leak "more text"
shouldStop: true, // signal caller to stop
},
wantState: &chunkProcessor{
contentBuffer: "", // may keep full tag inside
},
},
{
name: "stop prefix turns out to be different tag, falls through to other parsing logic",
initialState: &chunkProcessor{
contentBuffer: ""},
want: bufferOrStreamResult{
shouldStream: false,
shouldStop: false,
},
wantState: &chunkProcessor{
contentBuffer: "\npackage",
manualStop: []string{""},
want: bufferOrStreamResult{
shouldStream: true,
content: "something\n```go\npackage",
},
wantState: &chunkProcessor{
awaitingBlockOpeningTag: false,
fileOpen: true,
},
},
}
only := map[int]bool{}
for i, tt := range tests {
if tt.only {
only[i] = true
}
}
for i, tt := range tests {
if len(only) > 0 && !only[i] {
continue
}
t.Run(tt.name, func(t *testing.T) {
processor := tt.initialState
got := processor.bufferOrStream(tt.chunk, &types.ReplyParserRes{
MaybeFilePath: tt.maybeFilePath,
CurrentFilePath: tt.currentFilePath,
IsInMoveBlock: tt.isInMoveBlock,
IsInRemoveBlock: tt.isInRemoveBlock,
IsInResetBlock: tt.isInResetBlock,
}, shared.CurrentStage{
TellStage: shared.TellStageImplementation,
}, tt.manualStop)
if got.shouldStream != tt.want.shouldStream {
t.Errorf("shouldStream = %v, want %v", got.shouldStream, tt.want.shouldStream)
}
if got.shouldStream && got.content != tt.want.content {
t.Errorf("content = %q, want %q", got.content, tt.want.content)
}
// Check all state transitions
if processor.fileOpen != tt.wantState.fileOpen {
t.Errorf("fileOpen = %v, want %v", processor.fileOpen, tt.wantState.fileOpen)
}
if processor.awaitingBlockOpeningTag != tt.wantState.awaitingBlockOpeningTag {
t.Errorf("awaitingOpeningTag = %v, want %v", processor.awaitingBlockOpeningTag, tt.wantState.awaitingBlockOpeningTag)
}
if processor.awaitingBlockClosingTag != tt.wantState.awaitingBlockClosingTag {
t.Errorf("awaitingClosingTag = %v, want %v", processor.awaitingBlockClosingTag, tt.wantState.awaitingBlockClosingTag)
}
if processor.awaitingBackticks != tt.wantState.awaitingBackticks {
t.Errorf("awaitingBackticks = %v, want %v", processor.awaitingBackticks, tt.wantState.awaitingBackticks)
}
if tt.wantState.contentBuffer != "" {
if processor.contentBuffer != tt.wantState.contentBuffer {
t.Errorf("content buffer = %q, want %q", processor.contentBuffer, tt.wantState.contentBuffer)
}
}
// Check buffer is reset when it should be
if tt.want.shouldStream && processor.contentBuffer != "" {
t.Error("content buffer should be reset after streaming")
}
})
}
}
================================================
FILE: app/server/model/plan/tell_stream_status.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"plandex-server/db"
shared "plandex-shared"
"runtime/debug"
)
type handleDescAndExecStatusResult struct {
handleStreamFinishedResult
subtaskFinished bool
generatedDescription *db.ConvoMessageDescription
}
func (state *activeTellStreamState) handleDescAndExecStatus() handleDescAndExecStatusResult {
currentOrgId := state.currentOrgId
summarizedToMessageId := state.summarizedToMessageId
planId := state.plan.Id
branch := state.branch
replyOperations := state.chunkProcessor.replyOperations
active := GetActivePlan(planId, branch)
if active == nil {
state.onActivePlanMissingError()
return handleDescAndExecStatusResult{
handleStreamFinishedResult: handleStreamFinishedResult{
shouldContinueMainLoop: true,
shouldReturn: false,
},
}
}
var generatedDescription *db.ConvoMessageDescription
var subtaskFinished bool
var errCh = make(chan *shared.ApiError, 2)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in genPlanDescription: %v\n%s", r, debug.Stack())
errCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error generating plan description: %v", r),
}
}
}()
if len(replyOperations) > 0 {
log.Println("Generating plan description")
res, err := state.genPlanDescription()
if err != nil {
errCh <- err
return
}
generatedDescription = res
generatedDescription.OrgId = currentOrgId
generatedDescription.SummarizedToMessageId = summarizedToMessageId
generatedDescription.WroteFiles = true
generatedDescription.Operations = replyOperations
log.Println("Generated plan description.")
}
errCh <- nil
}()
if state.currentStage.TellStage == shared.TellStageImplementation {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in execStatusShouldContinue: %v\n%s", r, debug.Stack())
errCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error getting exec status: %v", r),
}
}
}()
log.Println("Getting exec status")
var err *shared.ApiError
res, err := state.execStatusShouldContinue(active.CurrentReplyContent, active.SessionId, active.Ctx)
if err != nil {
errCh <- err
return
}
subtaskFinished = res.subtaskFinished
log.Printf("subtaskFinished: %v\n", subtaskFinished)
errCh <- nil
}()
} else {
errCh <- nil
}
for i := 0; i < 2; i++ {
err := <-errCh
if err != nil {
res := state.onError(onErrorParams{
streamApiErr: err,
storeDesc: true,
})
return handleDescAndExecStatusResult{
handleStreamFinishedResult: handleStreamFinishedResult{
shouldContinueMainLoop: res.shouldContinueMainLoop,
shouldReturn: res.shouldReturn,
},
subtaskFinished: subtaskFinished,
generatedDescription: generatedDescription,
}
}
}
return handleDescAndExecStatusResult{
handleStreamFinishedResult: handleStreamFinishedResult{},
subtaskFinished: subtaskFinished,
generatedDescription: generatedDescription,
}
}
type willContinuePlanParams struct {
hasNewSubtasks bool
removedSubtasks bool
allSubtasksFinished bool
activatePaths map[string]bool
hasExplicitPaths bool
}
func (state *activeTellStreamState) willContinuePlan(params willContinuePlanParams) bool {
hasNewSubtasks := params.hasNewSubtasks
removedSubtasks := params.removedSubtasks
allSubtasksFinished := params.allSubtasksFinished
activatePaths := params.activatePaths
currentSubtask := state.currentSubtask
log.Printf("[willContinuePlan] currentStage: %v", state.currentStage)
log.Printf("[willContinuePlan] Initial state - hasNewSubtasks: %v, allSubtasksFinished: %v, tellStage: %v, planningPhase: %v, iteration: %d, autoContinue: %v",
hasNewSubtasks, allSubtasksFinished, state.currentStage.TellStage, state.currentStage.PlanningPhase, state.iteration, state.req.AutoContinue)
if state.currentStage.TellStage == shared.TellStagePlanning {
log.Println("[willContinuePlan] In planning stage")
// always continue to response or planning phase after context phase
if state.currentStage.PlanningPhase == shared.PlanningPhaseContext {
// if it's the context stage but it's chat mode and no files were loaded, don't continue
if state.req.IsChatOnly && len(activatePaths) == 0 {
log.Println("[willContinuePlan] Chat only - no files loaded - stopping")
return false
}
// if no files were listed explicitly in a ### Files section, don't continue if it's chat mode
if state.req.IsChatOnly && !params.hasExplicitPaths {
log.Println("[willContinuePlan] Chat only - no files loaded - stopping")
return false
}
log.Println("[willContinuePlan] In context phase - continuing to planning phase")
return true
}
if state.req.IsChatOnly {
log.Println("[willContinuePlan] Chat only - stopping")
return false
}
// otherwise, if auto-continue is disabled, never continue
if !state.req.AutoContinue {
log.Println("[willContinuePlan] Auto-continue disabled - stopping")
return false
}
// if there are new subtasks, continue
if hasNewSubtasks && !allSubtasksFinished {
log.Println("[willContinuePlan] Has new subtasks - continuing")
return true
}
if removedSubtasks && !allSubtasksFinished {
log.Println("[willContinuePlan] Removed subtasks - continuing")
return true
}
// if all subtasks are finished, don't continue
log.Printf("[willContinuePlan] Checking subtasks finished - allSubtasksFinished: %v, will continue: %v",
allSubtasksFinished, !allSubtasksFinished)
log.Printf("[willContinuePlan] currentSubtask: %v", currentSubtask)
return !allSubtasksFinished && currentSubtask != nil
} else if state.currentStage.TellStage == shared.TellStageImplementation {
log.Println("[willContinuePlan] In implementation stage")
// if all subtasks are finished, don't continue
if allSubtasksFinished {
log.Println("[willContinuePlan] All subtasks finished - stopping")
return false
}
// if we've automatically continued too many times, don't continue
if state.iteration >= MaxAutoContinueIterations {
log.Printf("[willContinuePlan] Reached max iterations (%d) - stopping", MaxAutoContinueIterations)
return false
}
// otherwise, continue with implementation
log.Println("[willContinuePlan] Continuing implementation")
return true
}
log.Printf("[willContinuePlan] Unknown tell stage: %v - won't continue", state.currentStage.TellStage)
return false
}
================================================
FILE: app/server/model/plan/tell_stream_store.go
================================================
package plan
import (
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/notify"
"plandex-server/types"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
type storeOnFinishedParams struct {
replyOperations []*shared.Operation
generatedDescription *db.ConvoMessageDescription
subtaskFinished bool
hasNewSubtasks bool
autoLoadContextResult checkAutoLoadContextResult
addedSubtasks []*db.Subtask
removedSubtasks []string
}
type storeOnFinishedResult struct {
handleStreamFinishedResult
allSubtasksFinished bool
}
func (state *activeTellStreamState) storeOnFinished(params storeOnFinishedParams) storeOnFinishedResult {
replyOperations := params.replyOperations
generatedDescription := params.generatedDescription
subtaskFinished := params.subtaskFinished
hasNewSubtasks := params.hasNewSubtasks
autoLoadContextResult := params.autoLoadContextResult
currentOrgId := state.currentOrgId
currentUserId := state.currentUserId
planId := state.plan.Id
branch := state.branch
summarizedToMessageId := state.summarizedToMessageId
active := state.activePlan
addedSubtasks := params.addedSubtasks
removedSubtasks := params.removedSubtasks
var allSubtasksFinished bool
log.Println("[storeOnFinished] Locking repo to store assistant reply and description")
err := db.ExecRepoOperation(db.ExecRepoOperationParams{
OrgId: currentOrgId,
UserId: currentUserId,
PlanId: planId,
Branch: branch,
Scope: db.LockScopeWrite,
Ctx: active.Ctx,
CancelFn: active.CancelFn,
Reason: "store on finished",
}, func(repo *db.GitRepo) error {
log.Println("storeOnFinished: hasNewSubtasks", hasNewSubtasks)
log.Println("storeOnFinished: subtaskFinished", subtaskFinished)
log.Println("storeOnFinished: removedSubtasks", removedSubtasks)
messageSubtask := state.currentSubtask
// first resolve subtask state
if hasNewSubtasks || len(removedSubtasks) > 0 || subtaskFinished {
if subtaskFinished && state.currentSubtask != nil {
log.Printf("[storeOnFinished] Marking subtask as finished: %q", state.currentSubtask.Title)
state.currentSubtask.IsFinished = true
log.Printf("[storeOnFinished] Current subtask state after marking as finished: %+v", state.currentSubtask)
}
log.Printf("[storeOnFinished] Storing plan subtasks (hasNewSubtasks=%v, subtaskFinished=%v)", hasNewSubtasks, subtaskFinished)
log.Printf("[storeOnFinished] Current subtasks state before storing:")
for i, task := range state.subtasks {
log.Printf("[storeOnFinished] Task %d: %q (finished=%v)", i+1, task.Title, task.IsFinished)
}
state.currentSubtask = nil
allSubtasksFinished = true
for _, subtask := range state.subtasks {
if !subtask.IsFinished {
state.currentSubtask = subtask
allSubtasksFinished = false
break
}
}
if state.currentSubtask != nil {
log.Printf("[storeOnFinished] Set new current subtask: %q", state.currentSubtask.Title)
} else {
log.Println("[storeOnFinished] No new current subtask set")
}
log.Printf("[storeOnFinished] All subtasks finished: %v", allSubtasksFinished)
} else if state.currentSubtask != nil && !subtaskFinished {
log.Printf("[storeOnFinished] Current subtask is not finished: %q", state.currentSubtask.Title)
state.currentSubtask.NumTries++
}
log.Println("storeOnFinished: state.currentSubtask", state.currentSubtask)
log.Println("storeOnFinished: state.subtasks", state.subtasks)
log.Println("storeOnFinished: state.currentStage", state.currentStage)
var flags shared.ConvoMessageFlags
flags.CurrentStage = state.currentStage
if len(replyOperations) > 0 {
flags.DidWriteCode = true
}
if hasNewSubtasks {
log.Println("storeOnFinished: hasNewSubtasks")
flags.DidMakePlan = true
}
if len(removedSubtasks) > 0 {
log.Println("storeOnFinished: len(removedSubtasks) > 0")
flags.DidMakePlan = true
flags.DidRemoveTasks = true
}
if len(autoLoadContextResult.autoLoadPaths) > 0 {
flags.DidLoadContext = true
}
if subtaskFinished && messageSubtask != nil {
flags.DidCompleteTask = true
}
if allSubtasksFinished {
log.Println("storeOnFinished: allSubtasksFinished")
flags.DidCompletePlan = true
}
if hasNewSubtasks && (state.req.IsApplyDebug || state.req.IsUserDebug) {
log.Println("storeOnFinished: hasNewSubtasks && (state.req.IsApplyDebug || state.req.IsUserDebug)")
flags.DidMakeDebuggingPlan = true
}
log.Println("storeOnFinished: flags", flags)
assistantMsg, convoCommitMsg, err := state.storeAssistantReply(repo, storeAssistantReplyParams{
flags: flags,
subtask: messageSubtask,
addedSubtasks: addedSubtasks,
activatePaths: autoLoadContextResult.activatePaths,
activatePathsOrdered: autoLoadContextResult.activatePathsOrdered,
removedSubtasks: removedSubtasks,
}) // updates state.convo
if err != nil {
state.onError(onErrorParams{
streamErr: fmt.Errorf("failed to store assistant message: %v", err),
storeDesc: true,
})
return err
}
log.Println("getting description for assistant message: ", assistantMsg.Id)
var description *db.ConvoMessageDescription
if len(replyOperations) == 0 {
description = &db.ConvoMessageDescription{
OrgId: currentOrgId,
PlanId: planId,
ConvoMessageId: assistantMsg.Id,
SummarizedToMessageId: summarizedToMessageId,
BuildPathsInvalidated: map[string]bool{},
WroteFiles: false,
}
} else {
description = generatedDescription
description.ConvoMessageId = assistantMsg.Id
}
log.Println("[storeOnFinished] Storing description")
err = db.StoreDescription(description)
if err != nil {
state.onError(onErrorParams{
streamErr: fmt.Errorf("failed to store description: %v", err),
storeDesc: false,
convoMessageId: assistantMsg.Id,
commitMsg: convoCommitMsg,
})
return err
}
log.Println("[storeOnFinished] Description stored")
// store subtasks
err = db.StorePlanSubtasks(currentOrgId, planId, state.subtasks)
if err != nil {
log.Printf("Error storing plan subtasks: %v\n", err)
state.onError(onErrorParams{
streamErr: fmt.Errorf("failed to store plan subtasks: %v", err),
storeDesc: false,
convoMessageId: assistantMsg.Id,
commitMsg: convoCommitMsg,
})
return err
}
log.Println("Comitting after store on finished")
err = repo.GitAddAndCommit(branch, convoCommitMsg)
if err != nil {
state.onError(onErrorParams{
streamErr: fmt.Errorf("failed to commit: %v", err),
storeDesc: false,
convoMessageId: assistantMsg.Id,
commitMsg: convoCommitMsg,
})
return err
}
log.Println("Assistant reply, description, and subtasks committed")
return nil
})
if err != nil {
log.Printf("Error storing on finished: %v\n", err)
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error storing on finished: %v", err))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("Error storing on finished: %v", err),
}
return storeOnFinishedResult{
handleStreamFinishedResult: handleStreamFinishedResult{
shouldContinueMainLoop: true,
shouldReturn: false,
},
allSubtasksFinished: false,
}
}
return storeOnFinishedResult{
handleStreamFinishedResult: handleStreamFinishedResult{},
allSubtasksFinished: allSubtasksFinished,
}
}
type storeAssistantReplyParams struct {
flags shared.ConvoMessageFlags
subtask *db.Subtask
addedSubtasks []*db.Subtask
activatePaths map[string]bool
activatePathsOrdered []string
removedSubtasks []string
}
func (state *activeTellStreamState) storeAssistantReply(repo *db.GitRepo, params storeAssistantReplyParams) (*db.ConvoMessage, string, error) {
flags := params.flags
subtask := params.subtask
addedSubtasks := params.addedSubtasks
activatePaths := params.activatePaths
activatePathsOrdered := params.activatePathsOrdered
removedSubtasks := params.removedSubtasks
currentOrgId := state.currentOrgId
currentUserId := state.currentUserId
planId := state.plan.Id
branch := state.branch
auth := state.auth
replyNumTokens := state.replyNumTokens
replyId := state.replyId
convo := state.convo
num := len(convo) + 1
log.Printf("storing assistant reply | len(convo) %d | num %d\n", len(convo), num)
activePlan := state.activePlan
// fmt.Println("raw message: ", activePlan.CurrentReplyContent)
assistantMsg := db.ConvoMessage{
Id: replyId,
OrgId: currentOrgId,
PlanId: planId,
UserId: currentUserId,
Role: openai.ChatMessageRoleAssistant,
Tokens: replyNumTokens,
Num: num,
Message: activePlan.CurrentReplyContent,
Flags: flags,
Subtask: subtask,
AddedSubtasks: addedSubtasks,
ActivatedPaths: activatePaths,
ActivatedPathsOrdered: activatePathsOrdered,
RemovedSubtasks: removedSubtasks,
}
commitMsg, err := db.StoreConvoMessage(repo, &assistantMsg, auth.User.Id, branch, false)
if err != nil {
log.Printf("Error storing assistant message: %v\n", err)
return nil, "", err
}
UpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {
ap.MessageNum = num
ap.StoredReplyIds = append(ap.StoredReplyIds, replyId)
})
convo = append(convo, &assistantMsg)
state.convo = convo
return &assistantMsg, commitMsg, err
}
================================================
FILE: app/server/model/plan/tell_stream_usage.go
================================================
package plan
import (
"fmt"
"log"
"plandex-server/hooks"
"plandex-server/notify"
"runtime/debug"
"github.com/davecgh/go-spew/spew"
"github.com/sashabaranov/go-openai"
)
func (state *activeTellStreamState) handleUsageChunk(usage *openai.Usage) {
auth := state.auth
plan := state.plan
generationId := state.generationId
log.Println("Tell stream usage:")
log.Println(spew.Sdump(usage))
var cachedTokens int
if usage.PromptTokensDetails != nil {
cachedTokens = usage.PromptTokensDetails.CachedTokens
}
sessionId := state.activePlan.SessionId
modelConfig := state.modelConfig
baseModelConfig := modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in handleUsageChunk: %v\n%s", r, debug.Stack())
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in handleUsageChunk: %v\n%s", r, debug.Stack()))
}
}()
_, apiErr := hooks.ExecHook(hooks.DidSendModelRequest, hooks.HookParams{
Auth: auth,
Plan: plan,
DidSendModelRequestParams: &hooks.DidSendModelRequestParams{
InputTokens: usage.PromptTokens,
OutputTokens: usage.CompletionTokens,
CachedTokens: cachedTokens,
ModelId: baseModelConfig.ModelId,
ModelTag: baseModelConfig.ModelTag,
ModelName: baseModelConfig.ModelName,
ModelProvider: baseModelConfig.Provider,
ModelPackName: state.settings.GetModelPack().Name,
ModelRole: modelConfig.Role,
Purpose: "Response",
GenerationId: generationId,
PlanId: plan.Id,
ModelStreamId: state.modelStreamId,
ConvoMessageId: state.replyId,
RequestStartedAt: state.requestStartedAt,
Streaming: true,
FirstTokenAt: state.firstTokenAt,
Req: state.originalReq,
StreamResult: state.activePlan.CurrentReplyContent,
ModelConfig: state.modelConfig,
SessionId: sessionId,
},
})
if apiErr != nil {
log.Printf("handleUsageChunk - error executing DidSendModelRequest hook: %v", apiErr)
}
}()
}
func (state *activeTellStreamState) execHookOnStop(sendStreamErr bool) {
generationId := state.generationId
log.Printf("execHookOnStop - sendStreamErr: %t\n", sendStreamErr)
planId := state.plan.Id
branch := state.branch
auth := state.auth
plan := state.plan
active := GetActivePlan(planId, branch)
if active == nil {
log.Printf(" Active plan not found for plan ID %s on branch %s\n", planId, branch)
return
}
modelConfig := state.modelConfig
baseModelConfig := modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in execHookOnStop: %v\n%s", r, debug.Stack())
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in execHookOnStop: %v\n%s", r, debug.Stack()))
}
}()
_, apiErr := hooks.ExecHook(hooks.DidSendModelRequest, hooks.HookParams{
Auth: auth,
Plan: plan,
DidSendModelRequestParams: &hooks.DidSendModelRequestParams{
InputTokens: state.totalRequestTokens,
OutputTokens: active.NumTokens,
ModelId: baseModelConfig.ModelId,
ModelTag: baseModelConfig.ModelTag,
ModelName: baseModelConfig.ModelName,
ModelProvider: baseModelConfig.Provider,
ModelPackName: state.settings.GetModelPack().Name,
ModelRole: modelConfig.Role,
Purpose: "Response",
GenerationId: generationId,
PlanId: plan.Id,
ModelStreamId: state.modelStreamId,
ConvoMessageId: state.replyId,
StoppedEarly: true,
UserCancelled: !sendStreamErr,
HadError: sendStreamErr,
NoReportedUsage: true,
RequestStartedAt: state.requestStartedAt,
Streaming: true,
FirstTokenAt: state.firstTokenAt,
Req: state.originalReq,
StreamResult: state.activePlan.CurrentReplyContent,
ModelConfig: state.modelConfig,
SessionId: active.SessionId,
},
})
if apiErr != nil {
log.Printf("execHookOnStop - error executing DidSendModelRequest hook: %v", apiErr)
}
}()
}
================================================
FILE: app/server/model/plan/tell_subtasks.go
================================================
package plan
import (
"fmt"
"log"
"plandex-server/db"
"plandex-server/model/parse"
shared "plandex-shared"
"strings"
"github.com/davecgh/go-spew/spew"
)
func (state *activeTellStreamState) formatSubtasks() string {
subtasksText := "### LATEST PLAN TASKS ###\n\n"
var current *db.Subtask
for idx, subtask := range state.subtasks {
subtasksText += fmt.Sprintf("%d. %s\n", idx+1, subtask.Title)
if subtask.Description != "" {
subtasksText += "\n" + subtask.Description + "\n"
}
if len(subtask.UsesFiles) > 0 {
subtasksText += "Uses: "
usesFiles := []string{}
for _, file := range subtask.UsesFiles {
usesFiles = append(usesFiles, fmt.Sprintf("`%s`", file))
}
subtasksText += strings.Join(usesFiles, ", ") + "\n"
}
subtasksText += "Done: "
if subtask.IsFinished {
subtasksText += "yes"
} else {
subtasksText += "no"
}
subtasksText += "\n"
if state.currentSubtask != nil && subtask.Title == state.currentSubtask.Title && state.currentStage.TellStage == shared.TellStageImplementation {
current = subtask
subtasksText += "Current subtask: yes"
}
subtasksText += "\n"
}
if current != nil && state.currentStage.TellStage == shared.TellStageImplementation {
subtasksText += fmt.Sprintf("\n### Current subtask\n%s\n", current.Title)
if current.Description != "" {
subtasksText += "\n" + current.Description + "\n"
}
if len(current.UsesFiles) > 0 {
subtasksText += "Uses: "
usesFiles := []string{}
for _, file := range current.UsesFiles {
usesFiles = append(usesFiles, fmt.Sprintf("`%s`", file))
}
subtasksText += strings.Join(usesFiles, ", ") + "\n"
}
} else if state.currentStage.TellStage == shared.TellStagePlanning {
if state.currentStage.PlanningPhase == shared.PlanningPhaseTasks {
subtasksText += `
Remember, you are in the *PLANNING* phase and ABSOLUTELY MUST NOT implement any of the subtasks. You MUST NOT write any code or create any files. You can ONLY add or remove subtasks with a '### Tasks' section or a '### Remove Tasks' section. You CANNOT implement any of the subtasks in this response. Follow the PLANNING instructions. The existing subtasks are included for your reference so that you can see what has been planned so far, what has been done, and what is left to do, so that you can add or remove subtasks as needed. DO NOT implement any of the subtasks in this response-follow the instructions for the PLANNING phase.
`
} else if state.currentStage.PlanningPhase == shared.PlanningPhaseContext {
subtasksText += `
Remember, you are in the *CONTEXT* phase. You MUST NOT implement any of the subtasks. You MUST NOT write any code or create any files. You MUST NOT make a plan with a '### Tasks' section or a '### Remove Tasks' section. Follow the instructions for the CONTEXT phase-they are summarized for you in the [SUMMARY OF INSTRUCTIONS] section. The existing subtasks are included for your reference so that you can see what has been planned so far, what has been done, and what is left to do. DO NOT implement any of the subtasks in this response Do NOT add or remove subtasks. Follow the instructions for the CONTEXT phase.
`
}
}
return subtasksText
}
type checkNewSubtasksResult struct {
hasExplicitTasks bool
newSubtasks []*db.Subtask
}
func (state *activeTellStreamState) checkNewSubtasks() checkNewSubtasksResult {
activePlan := GetActivePlan(state.plan.Id, state.branch)
if activePlan == nil {
return checkNewSubtasksResult{
hasExplicitTasks: false,
newSubtasks: nil,
}
}
content := activePlan.CurrentReplyContent
subtasks := parse.ParseSubtasks(content)
if len(subtasks) == 0 {
log.Println("No new subtasks found")
return checkNewSubtasksResult{
hasExplicitTasks: false,
newSubtasks: nil,
}
}
log.Println("Found new subtasks:", len(subtasks))
// log.Println(spew.Sdump(subtasks))
subtasksByName := map[string]*db.Subtask{}
// Only index unfinished subtasks by name
for _, subtask := range state.subtasks {
if !subtask.IsFinished {
subtasksByName[subtask.Title] = subtask
}
}
var newSubtasks []*db.Subtask
var updatedSubtasks []*db.Subtask
// Keep finished subtasks
for _, subtask := range state.subtasks {
if subtask.IsFinished {
updatedSubtasks = append(updatedSubtasks, subtask)
}
}
// Add new subtasks if they don't exist
for _, subtask := range subtasks {
if subtasksByName[subtask.Title] == nil {
newSubtasks = append(newSubtasks, subtask)
updatedSubtasks = append(updatedSubtasks, subtask)
}
}
state.subtasks = updatedSubtasks
var currentSubtaskName string
if state.currentSubtask != nil {
currentSubtaskName = state.currentSubtask.Title
}
found := false
for _, subtask := range state.subtasks {
if subtask.Title == currentSubtaskName {
found = true
state.currentSubtask = subtask
break
}
}
if !found {
state.currentSubtask = nil
}
if state.currentSubtask == nil {
for _, subtask := range state.subtasks {
if !subtask.IsFinished {
state.currentSubtask = subtask
break
}
}
}
// log.Println("state.subtasks:\n", spew.Sdump(state.subtasks))
log.Println("state.currentSubtask:\n", spew.Sdump(state.currentSubtask))
return checkNewSubtasksResult{
hasExplicitTasks: len(subtasks) > 0,
newSubtasks: newSubtasks,
}
}
type checkRemoveSubtasksResult struct {
hasExplicitRemoveTasks bool
removedSubtasks []string
}
func (state *activeTellStreamState) checkRemoveSubtasks() checkRemoveSubtasksResult {
activePlan := GetActivePlan(state.plan.Id, state.branch)
if activePlan == nil {
return checkRemoveSubtasksResult{
hasExplicitRemoveTasks: false,
removedSubtasks: nil,
}
}
content := activePlan.CurrentReplyContent
// Parse tasks to remove
tasksToRemove := parse.ParseRemoveSubtasks(content)
if len(tasksToRemove) == 0 {
log.Println("No tasks to remove found")
return checkRemoveSubtasksResult{
hasExplicitRemoveTasks: false,
removedSubtasks: nil,
}
}
log.Println("Found tasks to remove:", len(tasksToRemove))
// log.Println(spew.Sdump(tasksToRemove))
// Create a map of task titles to remove for efficient lookup
removeMap := make(map[string]bool)
for _, task := range tasksToRemove {
removeMap[task] = true
}
var removedSubtasks []*db.Subtask
var remainingSubtasks []*db.Subtask
// Keep tasks that aren't in the remove list
for _, subtask := range state.subtasks {
if removeMap[subtask.Title] {
// Only track unfinished tasks that are being removed
if !subtask.IsFinished {
removedSubtasks = append(removedSubtasks, subtask)
}
} else {
remainingSubtasks = append(remainingSubtasks, subtask)
}
}
state.subtasks = remainingSubtasks
// Update current subtask if it was removed
if state.currentSubtask != nil && removeMap[state.currentSubtask.Title] {
state.currentSubtask = nil
// Find the first unfinished subtask to set as current
for _, subtask := range state.subtasks {
if !subtask.IsFinished {
state.currentSubtask = subtask
break
}
}
}
removedSubtaskTitles := []string{}
for _, subtask := range removedSubtasks {
removedSubtaskTitles = append(removedSubtaskTitles, subtask.Title)
}
log.Println("removedSubtaskTitles:\n", spew.Sdump(removedSubtaskTitles))
return checkRemoveSubtasksResult{
hasExplicitRemoveTasks: len(tasksToRemove) > 0,
removedSubtasks: removedSubtaskTitles,
}
}
================================================
FILE: app/server/model/plan/tell_summary.go
================================================
package plan
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"plandex-server/db"
"plandex-server/model"
"plandex-server/model/prompts"
"plandex-server/notify"
"plandex-server/types"
"time"
shared "plandex-shared"
"github.com/davecgh/go-spew/spew"
"github.com/sashabaranov/go-openai"
)
func (state *activeTellStreamState) addConversationMessages() bool {
summaries := state.summaries
tokensBeforeConvo := state.tokensBeforeConvo
active := GetActivePlan(state.plan.Id, state.branch)
convo := []*db.ConvoMessage{}
for _, msg := range state.convo {
if state.skipConvoMessages != nil && state.skipConvoMessages[msg.Id] {
continue
}
convo = append(convo, msg)
}
if active == nil {
log.Println("summarizeMessagesIfNeeded - Active plan not found")
return false
}
conversationTokens := 0
tokensUpToTimestamp := make(map[int64]int)
convoMessagesById := make(map[string]*db.ConvoMessage)
for _, convoMessage := range convo {
conversationTokens += convoMessage.Tokens + model.TokensPerMessage + model.TokensPerName
timestamp := convoMessage.CreatedAt.UnixNano() / int64(time.Millisecond)
tokensUpToTimestamp[timestamp] = conversationTokens
convoMessagesById[convoMessage.Id] = convoMessage
// log.Printf("Timestamp: %s | Tokens: %d | Total: %d | conversationTokens\n", convoMessage.Timestamp, convoMessage.Tokens, conversationTokens)
}
log.Printf("Conversation tokens: %d\n", conversationTokens)
log.Printf("Max conversation tokens: %d\n", state.settings.GetPlannerMaxConvoTokens())
// log.Println("Tokens up to timestamp:")
// spew.Dump(tokensUpToTimestamp)
log.Printf("Total tokens: %d\n", tokensBeforeConvo+conversationTokens)
log.Printf("Max tokens: %d\n", state.settings.GetPlannerEffectiveMaxTokens())
var summary *db.ConvoSummary
if (tokensBeforeConvo+conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens() ||
conversationTokens > state.settings.GetPlannerMaxConvoTokens() {
log.Println("Token limit exceeded. Attempting to reduce via conversation summary.")
// log.Printf("(tokensBeforeConvo+conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens(): %v\n", (tokensBeforeConvo+conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens())
// log.Printf("conversationTokens > state.settings.GetPlannerMaxConvoTokens(): %v\n", conversationTokens > state.settings.GetPlannerMaxConvoTokens())
log.Printf("Num summaries: %d\n", len(summaries))
// token limit exceeded after adding conversation
// get summary for as much as the conversation as necessary to stay under the token limit
for _, s := range summaries {
timestamp := s.LatestConvoMessageCreatedAt.UnixNano() / int64(time.Millisecond)
tokens, ok := tokensUpToTimestamp[timestamp]
log.Printf("Last message timestamp: %d | found: %v\n", timestamp, ok)
log.Printf("Tokens up to timestamp: %d\n", tokens)
if !ok {
// try a fallback by id instead of timestamp, in case timestamp rounding caused it to be missing
convoMessage, ok := convoMessagesById[s.LatestConvoMessageId]
if ok {
timestamp = convoMessage.CreatedAt.UnixNano() / int64(time.Millisecond)
tokens, ok = tokensUpToTimestamp[timestamp]
}
if !ok {
// instead of erroring here as we did previously, we'll just log and continue
// if no summary is found, we still handle it as an error below
// but this way we don't error out completely for a single detached summary
log.Println("conversation summary timestamp not found in conversation")
log.Println("timestamp:", timestamp)
// log.Println("Conversation summary:")
// spew.Dump(s)
log.Println("tokensUpToTimestamp:")
log.Println(spew.Sdump(tokensUpToTimestamp))
go notify.NotifyErr(notify.SeverityInfo, fmt.Errorf("conversation summary timestamp not found in conversation"))
continue
}
}
updatedConversationTokens := (conversationTokens - tokens) + s.Tokens
savedTokens := conversationTokens - updatedConversationTokens
log.Printf("Conversation summary tokens: %d\n", tokens)
log.Printf("Updated conversation tokens: %d\n", updatedConversationTokens)
log.Printf("Saved tokens: %d\n", savedTokens)
if updatedConversationTokens <= state.settings.GetPlannerMaxConvoTokens() &&
(tokensBeforeConvo+updatedConversationTokens) <= state.settings.GetPlannerEffectiveMaxTokens() {
log.Printf("Summarizing up to %s | saving %d tokens\n", s.LatestConvoMessageCreatedAt.Format(time.RFC3339), savedTokens)
summary = s
conversationTokens = updatedConversationTokens
break
}
}
if summary == nil && tokensBeforeConvo+conversationTokens > state.settings.GetPlannerEffectiveMaxTokens() {
err := errors.New("couldn't get under token limit with conversation summary")
log.Printf("Error: %v\n", err)
go notify.NotifyErr(notify.SeverityInfo, fmt.Errorf("couldn't get under token limit with conversation summary"))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Couldn't get under token limit with conversation summary",
}
return false
}
}
var latestSummary *db.ConvoSummary
if len(summaries) > 0 {
latestSummary = summaries[len(summaries)-1]
}
if summary == nil {
for _, convoMessage := range convo {
// this gets added later in tell_exec.go
if state.promptConvoMessage != nil && convoMessage.Id == state.promptConvoMessage.Id {
continue
}
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: convoMessage.Message,
},
},
})
// add the latest summary as a conversation message if this is the last message summarized, in order to reinforce the current state of the plan to the model
if latestSummary != nil && convoMessage.Id == latestSummary.LatestConvoMessageId {
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleAssistant,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: latestSummary.Summary,
},
},
})
}
}
} else {
if (tokensBeforeConvo + conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens() {
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("token limit still exceeded after summarizing conversation"))
active.StreamDoneCh <- &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: "Token limit still exceeded after summarizing conversation",
}
return false
}
state.summarizedToMessageId = summary.LatestConvoMessageId
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleAssistant,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: summary.Summary,
},
},
})
// add messages after the last message in the summary
for _, convoMessage := range convo {
// this gets added later in tell_exec.go
if state.promptConvoMessage != nil && convoMessage.Id == state.promptConvoMessage.Id {
continue
}
if convoMessage.CreatedAt.After(summary.LatestConvoMessageCreatedAt) {
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: convoMessage.Message,
},
},
})
// add the latest summary as a conversation message if this is the last message summarized, in order to reinforce the current state of the plan to the model
if latestSummary != nil && convoMessage.Id == latestSummary.LatestConvoMessageId {
state.messages = append(state.messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleAssistant,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: latestSummary.Summary,
},
},
})
}
}
}
}
return true
}
type summarizeConvoParams struct {
auth *types.ServerAuth
plan *db.Plan
branch string
convo []*db.ConvoMessage
summaries []*db.ConvoSummary
userPrompt string
currentReply string
currentReplyNumTokens int
currentOrgId string
modelPackName string
}
func summarizeConvo(clients map[string]model.ClientInfo, authVars map[string]string, settings *shared.PlanSettings, orgUserConfig *shared.OrgUserConfig, params summarizeConvoParams, ctx context.Context) *shared.ApiError {
plan := params.plan
planId := plan.Id
log.Printf("summarizeConvo: Called for plan ID %s on branch %s\n", planId, params.branch)
log.Printf("summarizeConvo: Starting summarizeConvo for planId: %s\n", planId)
branch := params.branch
convo := params.convo
summaries := params.summaries
userPrompt := params.userPrompt
currentReply := params.currentReply
active := GetActivePlan(planId, branch)
config := settings.GetModelPack().PlanSummary
if active == nil {
log.Printf("Active plan not found for plan ID %s and branch %s\n", planId, branch)
return &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("active plan not found for plan ID %s and branch %s", planId, branch),
}
}
log.Println("Generating plan summary for planId:", planId)
// log.Printf("planId: %s\n", planId)
// log.Printf("convo: ")
// spew.Dump(convo)
// log.Printf("summaries: ")
// spew.Dump(summaries)
// log.Printf("promptMessage: ")
// spew.Dump(promptMessage)
// log.Printf("currentOrgId: %s\n", currentOrgId)
var summaryMessages []*types.ExtendedChatMessage
var latestSummary *db.ConvoSummary
var numMessagesSummarized int = 0
var latestMessageSummarizedAt time.Time
var latestMessageId string
if len(summaries) > 0 {
latestSummary = summaries[len(summaries)-1]
numMessagesSummarized = latestSummary.NumMessages
}
// log.Println("Generating plan summary - latest summary:")
// spew.Dump(latestSummary)
// log.Println("Generating plan summary - convo:")
// spew.Dump(convo)
numTokens := 0
if latestSummary == nil {
for _, convoMessage := range convo {
summaryMessages = append(summaryMessages, &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: convoMessage.Message,
},
},
})
latestMessageId = convoMessage.Id
latestMessageSummarizedAt = convoMessage.CreatedAt
numMessagesSummarized++
numTokens += convoMessage.Tokens + model.TokensPerMessage + model.TokensPerName
}
} else {
summaryMessages = append(summaryMessages, &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleAssistant,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: latestSummary.Summary,
},
},
})
numTokens += latestSummary.Tokens + model.TokensPerMessage + model.TokensPerName
var found bool
for _, convoMessage := range convo {
if convoMessage.Id == latestSummary.LatestConvoMessageId {
found = true
continue
}
if found {
summaryMessages = append(summaryMessages, &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: convoMessage.Message,
},
},
})
numMessagesSummarized++
numTokens += convoMessage.Tokens + model.TokensPerMessage + model.TokensPerName
}
}
latestConvoMessage := convo[len(convo)-1]
latestMessageId = latestConvoMessage.Id
latestMessageSummarizedAt = latestConvoMessage.CreatedAt
}
log.Println("generating summary - latestMessageId:", latestMessageId)
log.Println("generating summary - latestMessageSummarizedAt:", latestMessageSummarizedAt)
if userPrompt != "" {
if userPrompt != prompts.UserContinuePrompt && userPrompt != prompts.AutoContinuePlanningPrompt && userPrompt != prompts.AutoContinueImplementationPrompt {
summaryMessages = append(summaryMessages, &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: userPrompt,
},
},
})
tokens := shared.GetNumTokensEstimate(userPrompt)
numTokens += tokens + model.TokensPerMessage + model.TokensPerName
}
}
if currentReply != "" {
summaryMessages = append(summaryMessages, &types.ExtendedChatMessage{
Role: openai.ChatMessageRoleAssistant,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: currentReply,
},
},
})
numTokens += params.currentReplyNumTokens + model.TokensPerMessage + model.TokensPerName
}
log.Printf("Calling model for plan summary. Summarizing %d messages\n", len(summaryMessages))
// log.Println("Generating summary - summary messages:")
// spew.Dump(summaryMessages)
// latestSummaryCh := make(chan *db.ConvoSummary, 1)
// active.LatestSummaryCh = latestSummaryCh
summary, apiErr := model.PlanSummary(clients, authVars, settings, orgUserConfig, config, model.PlanSummaryParams{
Conversation: summaryMessages,
ConversationNumTokens: numTokens,
LatestConvoMessageId: latestMessageId,
LatestConvoMessageCreatedAt: latestMessageSummarizedAt,
NumMessages: numMessagesSummarized,
Auth: params.auth,
Plan: plan,
ModelPackName: params.modelPackName,
ModelStreamId: active.ModelStreamId,
SessionId: active.SessionId,
}, ctx)
if apiErr != nil {
log.Printf("summarizeConvo: Error generating plan summary for plan %s: %v\n", planId, apiErr)
return apiErr
}
log.Printf("summarizeConvo: Summary generated and stored for plan %s\n", planId)
// log.Println("Generated summary:")
// spew.Dump(summary)
err := db.StoreSummary(summary)
if err != nil {
log.Printf("Error storing plan summary for plan %s: %v\n", planId, err)
return &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("error storing plan summary for plan %s: %v", planId, err),
}
}
// latestSummaryCh <- summary
return nil
}
================================================
FILE: app/server/model/plan/tell_sys_prompt.go
================================================
package plan
import (
"errors"
"fmt"
"log"
"plandex-server/model/prompts"
"plandex-server/types"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
const AllTasksCompletedMsg = "All tasks have been completed. There is no current task to implement."
type getTellSysPromptParams struct {
planStageSharedMsgs []*types.ExtendedChatMessagePart
planningPhaseOnlyMsgs []*types.ExtendedChatMessagePart
implementationMsgs []*types.ExtendedChatMessagePart
contextTokenLimit int
dryRunWithoutContext bool
}
func (state *activeTellStreamState) getTellSysPrompt(params getTellSysPromptParams) ([]types.ExtendedChatMessagePart, error) {
planningSharedMsgs := params.planStageSharedMsgs
plannerOnlyMsgs := params.planningPhaseOnlyMsgs
implementationMsgs := params.implementationMsgs
contextTokenLimit := params.contextTokenLimit
req := state.req
active := state.activePlan
currentStage := state.currentStage
sysParts := []types.ExtendedChatMessagePart{}
createPromptParams := prompts.CreatePromptParams{
ExecMode: req.ExecEnabled,
AutoContext: req.AutoContext,
IsUserDebug: req.IsUserDebug,
IsApplyDebug: req.IsApplyDebug,
IsGitRepo: req.IsGitRepo,
ContextTokenLimit: contextTokenLimit,
}
// log.Println("getTellSysPrompt - prompt params:", spew.Sdump(params))
if currentStage.TellStage == shared.TellStagePlanning {
if len(planningSharedMsgs) == 0 && !params.dryRunWithoutContext {
log.Println("planningSharedMsgs is empty - required for planning stage")
return nil, fmt.Errorf("planningSharedMsgs is empty - required for planning stage")
}
for _, msg := range planningSharedMsgs {
sysParts = append(sysParts, *msg)
}
if currentStage.PlanningPhase == shared.PlanningPhaseContext {
log.Println("Planning phase is context -- adding auto context prompt")
var txt string
if req.IsChatOnly {
txt = prompts.GetAutoContextChatPrompt(createPromptParams)
} else {
txt = prompts.GetAutoContextTellPrompt(createPromptParams)
}
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: txt,
CacheControl: &types.CacheControlSpec{
Type: types.CacheControlTypeEphemeral,
},
})
} else if currentStage.PlanningPhase == shared.PlanningPhaseTasks {
var txt string
if req.IsChatOnly {
txt = prompts.GetChatSysPrompt(createPromptParams)
} else {
txt = prompts.GetPlanningPrompt(createPromptParams)
}
if len(state.subtasks) > 0 {
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: txt,
})
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: state.formatSubtasks(),
CacheControl: &types.CacheControlSpec{
Type: types.CacheControlTypeEphemeral,
},
})
} else {
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: txt,
CacheControl: &types.CacheControlSpec{
Type: types.CacheControlTypeEphemeral,
},
})
}
if !req.IsChatOnly {
if len(active.SkippedPaths) > 0 {
skippedPrompt := prompts.SkippedPathsPrompt
for skippedPath := range active.SkippedPaths {
skippedPrompt += fmt.Sprintf("- %s\n", skippedPath)
}
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: skippedPrompt,
})
}
}
}
for _, msg := range plannerOnlyMsgs {
sysParts = append(sysParts, *msg)
}
if len(implementationMsgs) > 0 {
return nil, fmt.Errorf("implementationMsgs not supported during planning phase")
}
} else if currentStage.TellStage == shared.TellStageImplementation {
if state.currentSubtask == nil {
return nil, errors.New(AllTasksCompletedMsg)
}
if len(state.subtasks) > 0 {
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: prompts.GetImplementationPrompt(state.currentSubtask.Title),
})
sysParts = append(sysParts,
types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: state.formatSubtasks(),
CacheControl: &types.CacheControlSpec{
Type: types.CacheControlTypeEphemeral,
},
})
} else {
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: prompts.GetImplementationPrompt(state.currentSubtask.Title),
CacheControl: &types.CacheControlSpec{
Type: types.CacheControlTypeEphemeral,
},
})
}
if !req.IsChatOnly {
if len(active.SkippedPaths) > 0 {
skippedPrompt := prompts.SkippedPathsPrompt
for skippedPath := range active.SkippedPaths {
skippedPrompt += fmt.Sprintf("- %s\n", skippedPath)
}
sysParts = append(sysParts, types.ExtendedChatMessagePart{
Type: openai.ChatMessagePartTypeText,
Text: skippedPrompt,
})
}
}
if implementationMsgs != nil {
for _, msg := range implementationMsgs {
sysParts = append(sysParts, *msg)
}
} else if !params.dryRunWithoutContext {
log.Println("implementationMsgs is nil - required for implementation stage")
return nil, fmt.Errorf("implementationMsgs is nil - required for implementation stage")
}
if planningSharedMsgs != nil {
log.Println("planningSharedMsgs not supported during implementation stage - only basic or smart context is supported")
return nil, fmt.Errorf("planningSharedMsgs not supported during implementation stage - only basic or smart context is supported")
}
}
return sysParts, nil
}
================================================
FILE: app/server/model/plan/utils.go
================================================
package plan
import (
"plandex-server/types"
"strings"
)
func StripBackticksWrapper(s string) string {
check := strings.TrimSpace(s)
split := strings.Split(check, "\n")
if len(split) > 2 {
firstLine := strings.TrimSpace(split[0])
secondLine := strings.TrimSpace(split[1])
lastLine := strings.TrimSpace(split[len(split)-1])
if types.LineMaybeHasFilePath(firstLine) && strings.HasPrefix(secondLine, "```") {
if lastLine == "```" {
return strings.Join(split[1:len(split)-1], "\n")
}
} else if strings.HasPrefix(firstLine, "```") && lastLine == "```" {
return strings.Join(split[1:len(split)-1], "\n")
}
}
return s
}
================================================
FILE: app/server/model/prompts/apply_exec.go
================================================
package prompts
const ApplyScriptSharedPrompt = `
## _apply.sh file and command execution
**Execution mode is enabled.**
In addition to creating and updating files with code blocks, you can also execute commands on the user's machine by writing to a another *special path*: _apply.sh
### Core _apply.sh Concepts
The _apply.sh script is a special file that allows execution of commands on the user's machine. This script will be executed EXACTLY ONCE after ALL files from ALL subtasks have been created or updated. The entire script runs as a single unit in the root directory of the plan.
#### Core Restrictions
You ABSOLUTELY MUST NOT:
- Use _apply.sh to create files or directories (use code blocks instead - necessary directories will be created automatically)
- Use _apply.sh for file operations (move/remove/reset) on files in context
- Include shebang lines or error handling (this is handled externally)
- Give _apply.sh execution privileges (this is handled externally)
- Tell users to run the script (it runs automatically)
- Use separate script files unless specifically requested or absolutely necessary due to complexity
#### Safety and Security
BE CAREFUL AND CONSERVATIVE when making changes to the user's machine:
- Only make changes that are strictly necessary for the plan
- If a command is highly risky, tell the user to run it themselves
- Do not run malicious commands, commands that will harm the user's machine, or commands intended to cause harm to other systems, networks, or people.
- Prefer local changes over global system changes (e.g., npm install --save-dev over --global)
- Only modify files/directories in the root directory of the plan unless specifically directed otherwise
- Unless some commands are risky/dangerous, include ALL commands in _apply.sh rather than telling users to run them later
#### Avoid User Prompts
Avoid user prompts. Make reasonable default choices rather than prompting the user for input. The _apply.sh script MUST be able to run successfully in a non-interactive context.
#### Keep It Lightweight And Simple
The _apply.sh script should be lightweight and shouldn't do too much work. *Offload to separate files* in the plan if a lot of scripting is needed. _apply.sh doesn't get written to the user's project, so anything that might be valuable to save, reuse, and version control should be in a separate file. You can chmod and execute those separate files from _apply.sh. _apply.sh is for 'throwaway' commands that only need to be run once after the plan is applied to the user's project, like installing dependencies, running tests, or runing a start command. It shouldn't be complex.
Do not use fancy bash constructs that can be difficult to debug or cause portability problems. Keep it very straightforward so there's a 0% chance of bugs in the _apply.sh script.
ABSOLUTELY DO NOT use the _apply.sh script to generate config files, project files, instructions, documentation, or any other necessary files. The _apply.sh script MUST NOT create files or directories—this must be done ONLY with code blocks. Create those files like any other files in the plan using code blocks. Do NOT include any large context blocks of any kind in the _apply.sh script. Use separate files for large content. Keep the _apply.sh script lightweight, simple, and focused only on executing necessary commands.
#### Startup Logic
` + ApplyScriptStartupLogic + `
❌ DO NOT include complex startup logic or commands with flags in _apply.sh:
- _apply.sh:
echo "Importing project resources..."
godot --headless --quit
# Check if the main scene file exists
if [ ! -f "scenes/main.tscn" ]; then
echo "Error: Main scene file 'scenes/main.tscn' not found."
exit 1
fi
echo "Validating main scene file..."
if ! godot --headless --check-only --quit scenes/main.tscn; then
echo "Error: The main scene file 'scenes/main.tscn' contains errors."
exit 1
fi
echo "Checking for resource loading issues..."
if ! godot --headless --check-only --quit project.godot; then
echo "Error: The project contains resource loading issues."
exit 1
fi
echo "Starting Godot project..."
godot --position 100,100 --resolution 1280x720 --verbose
✅ DO include complex startup logic or commands with flags in a *separate file* in the project, created with a *code block*, not in _apply.sh:
- run.sh:
#!/bin/bash
set -euo pipefail
echo "Importing project resources..."
godot --headless --quit
# Check if the main scene file exists
if [ ! -f "scenes/main.tscn" ]; then
echo "Error: Main scene file 'scenes/main.tscn' not found."
exit 1
fi
echo "Validating main scene file..."
if ! godot --headless --check-only --quit scenes/main.tscn; then
echo "Error: The main scene file 'scenes/main.tscn' contains errors."
exit 1
fi
echo "Checking for resource loading issues..."
if ! godot --headless --check-only --quit project.godot; then
echo "Error: The project contains resource loading issues."
exit 1
fi
echo "Starting Godot project..."
godot --position 100,100 --resolution 1280x720 --verbose
- _apply.sh:
chmod +x run.sh
./run.sh
#### Command Preservation Rules
The _apply.sh script accumulates commands during the plan:
- ALL commands must be preserved until successful application
- Each update ADDS to or MODIFIES existing commands but NEVER removes them
- When updating an existing command, modify it rather than duplicating it
- After successful application, the script resets to empty
- Current state and history of previously executed scripts will be provided in the prompt
- Use script history to inform what commands might need to be re-run
#### Dependencies and Tools
When handling tools and dependencies:
1. Context-based Assumptions:
- Make reasonable assumptions about installed tools based on:
* The user's operating system
* Files and paths in the context
* Project structure and existing configuration
* Conversation history
- For example, if working with an existing Node.js project (has package.json), do NOT include commands to install Node.js/npm
- Similarly for other languages/frameworks: don't install Go for a Go project, Python for a Python project, etc.
2. Checking for Tools:
- For tools that aren't clearly present in context:
* Always check if the tool is installed before using it
* Either install missing tools or exit with a clear error
* Make the check specific and informative
- If no commands need to be run, do not write anything to _apply.sh
3. Dependency Management:
- DO NOT install dependencies that are already used in the project
- Only install new dependencies that are specifically needed for new features
- When working with an entirely new project, you can include basic tooling installation
- When adding to an existing project, assume core tooling is present
For example, in an existing Node.js project:
❌ DO NOT: Install Node.js or npm
❌ DO NOT: Reinstall dependencies listed in package.json
✅ DO: Install only new packages needed for new features
✅ DO: Check for specific tools needed for new functionality
#### Avoid Heavy Commands Unless Directed
You must be conservative about running 'heavy' commands like tests that could be slow or resource intensive to run.
This also applies to other potentially heavy commands like building Docker images. Use your best judgement.
#### Additional Requirements
Script execution:
- Assumes bash/zsh shell is available (OS/shell details provided in prompt)
- The script runs in the root directory of the plan
- All commands execute as a single unit after all file operations are complete
Special cases:
- If the plan includes other script files aside from _apply.sh, they must be given execution privileges and run from _apply.sh
- Only use separate script files if specifically requested or if the number of commands is too large for a single _apply.sh
- When using separate scripts, they must be run from _apply.sh, not manually by the user
Running programs:
- If appropriate, include commands to run the actual program
- For example: after 'make', include the command to run the program
- After 'npm install', include 'npm start' if appropriate
- Use judgment on the best way to run/execute the implemented plan
- Running web servers and browsers:
* Launch the default browser with the appropriate localhost URL after starting the server
* When writing a web server that connects to a port, use a port environment variable or command line argument to specify the port number. If you include a fallback port, you can use a common port in the context of the project like 3000 or 8080. Include a port override in the _apply.sh script that uses an UNCOMMON port number that is unlikely to be in use.
* Try multiple ports so if a port is in use, the server won't fail to start
* When starting a web server that needs a browser launched:
* CRITICAL: ALWAYS run the server in the background using & or the script will block and never reach the browser launch
* Add a brief sleep to allow the server to start (use your judgment based on the server type and the complexity of the server startup process how long is reasonable)
* ALWAYS use the special command 'plandex browser [urls...]' to launch the browser with one or more URLs. This command is provided by Plandex and is available on all operating systems. Substitute the actual URL or URLs you want to open in place of [urls...]. This special command *blocks* and streams the browser output to the console. So if you need to run other commands *after* the browser is launched, you must background the browser command and correclty handle cleanup like other background processes. If the browser command exits with an error, kill any other background processes and exit the entire script with a non-zero exit code.
Example:
# INCORRECT - will block and never launch browser:
npm start
plandex browser http://localhost:$PORT
# CORRECT - runs in background, waits, then launches browser:
npm start &
SERVER_PID=$!
sleep 3
plandex browser http://localhost:$PORT || {
kill $SERVER_PID
exit 1
}
wait $SERVER_PID
NOTE: when running anything in the background, you must handle the possibility that the process might fail so that no orphaned processes remain.
- ALWAYS use 'plandex browser' to open the browser and load urls. Do NOT use 'open' or 'xdg-open' or any other command to open the browser. USE 'plandex browser' instead.
* When using the 'plandex browser' command, you ABSOLUTE MUST EXPLICITLY kill all other processes and exit the script with a non-zero exit code if the browser command fails. It is CRITICAL that you DO NOT omit this. The 'plandex browser' command will fail if there are any uncaught errors or console.error logs in the browser.
*CRUCIAL NOTE: the _apply.sh script will be run with 'set -e' (it will be set for you, don't add it yourself) so you must DIRECTLY handle errors in foreground commands and cleanup in a '|| { ... }' block immediately when the command fails. *This includes the 'plandex browser' command.* Do NOT omit the '|| { ... }' block for 'plandex browser' or any other foreground command.
Example:
## INCORRECT - will not kill other processes and will not exit on browser failure:
npm start &
SERVER_PID=$!
sleep 3
plandex browser http://localhost:$PORT
wait $SERVER_PID
## INCORRECT - will not cleanup on failure due to 'set -e':
npm start &
SERVER_PID=$!
sleep 3
plandex browser http://localhost:$PORT
if [ $? -ne 0 ]; then
kill $SERVER_PID
exit 1
fi
wait $SERVER_PID
## CORRECT - will kill other processes and exit on browser failure, correctly handles 'set -e' with '|| { ... }' block:
npm start &
SERVER_PID=$!
sleep 3
plandex browser http://localhost:$PORT || {
kill $SERVER_PID
exit 1
}
wait $SERVER_PID
`
const ApplyScriptPlanningPrompt = ApplyScriptSharedPrompt + `
## Planning _apply.sh Updates
When planning tasks that involve command execution, always consider the natural hierarchy of commands:
1. First install any required packages/dependencies
2. Then run any necessary build commands
3. Finally run any test/execution commands
### Good Practices for Task Organization
When organizing subtasks that involve writing to _apply.sh:
- Write dependency installations close to the subtasks that introduce them
- Group related commands together when they're part of the same logical change
- Commands like 'make', 'npm install', or 'npm run build' that affect the whole project should appear only ONCE
- If adding a command that's already in _apply.sh, plan to update the existing command rather than duplicating it
### Bad Practices to Avoid
DO NOT:
- Plan to write the same command multiple times (e.g., 'make' after each file update)
- Create separate subtasks just to write a single command to _apply.sh
- Add new 'npm install' commands when you could update an existing one
- Plan to run the same program multiple times
### Example of Good Task Organization
Good task structure:
1. Add authentication feature
- Update auth-related files
- Write to _apply.sh: npm install auth-package
2. Add other features
- Update feature files
- Write to _apply.sh: npm install other-package
3. Build and run
- Write to _apply.sh:
npm run build
npm start
### Task Planning Guidelines
When breaking down tasks:
- Remember the single execution model - all commands run after all files are updated
- Consider dependencies between tasks and their required commands
- Group related file changes and their associated commands together
- Think about the logical ordering of commands
- Include _apply.sh in the 'Uses:' list for any subtask that will modify it
### Command Strategy
Think strategically about command execution:
- Plan command ordering based on dependencies
- Consider what will be needed after file changes are complete
- Group related commands together
- Plan for proper error handling and dependency checking
- Consider the user's environment and likely installed tools
- For web applications and web servers:
* Use port environment variables or command line arguments to specify the port number. If you include a fallback port, you can use a common port in the context of the project like 3000 or 8080. Include a port override in the _apply.sh script that uses an UNCOMMON port number that is unlikely to be in use.
* Include default browser launch commands after server start
` + ApplyScriptResetUpdatePlanningPrompt
const ApplyScriptImplementationPrompt = ApplyScriptSharedPrompt + `
## Implementing _apply.sh Updates
Remember that the _apply.sh script accumulates commands during the plan and executes them as a single unit. When adding new commands, carefully consider:
- Dependencies between commands (what needs to run before what)
- Whether similar commands already exist that should be updated rather than duplicated
- How your commands fit into the overall hierarchy (install → build → test/run)
### Creating and Updating _apply.sh
The script must be written using a correctly formatted code block:
- _apply.sh:
# Code goes here
CRITICAL rules:
- ALWAYS include the file path label exactly as shown above
- NEVER leave out the file path label when writing to _apply.sh
- There must be NO lines between the file path and opening tag
- Use lang="bash" in the tag
When writing to _apply.sh include an ### Action Explanation Format section, a file path label, and a tag that includes both a 'lang' attribute and a 'path' attribute as described in the instructions above.
If the current state of the _apply.sh script is *empty*, follow ALL instructions for *creating a new file* when writing to _apply.sh. Include the *entire* _apply.sh script in the code block.
If the current state of the _apply.sh script is *not empty*, follow ALL instructions for *updating an existing file* when writing to _apply.sh.
### Command Output and Error Handling
DO NOT hide or filter command output. For example, DO NOT do this:
- _apply.sh:
if ! make clean && make; then
echo "Error: Compilation failed"
exit 1
fi
Instead, show all command output:
- _apply.sh:
make clean
make
### Script Organization and Comments
The script should be:
- Written defensively to fail gracefully
- Organized logically with similar commands grouped
- Commented only when necessary for understanding
- Clear and maintainable
Include logging ONLY for:
- Error conditions
- Long-running operations
- DO NOT log script start/end (handled externally)
### Command Preservation
When updating an existing script:
1. Review current contents carefully
2. Preserve ALL existing commands exactly
3. Add new commands while maintaining existing ones
4. Verify no commands were accidentally removed/modified
Example of proper update:
Starting script:
npm install typescript
npm run build
Adding test command (CORRECT):
npm install typescript
npm run build
npm test
Adding test command (INCORRECT - NEVER DO THIS):
npm test
### Tool and Dependency Checks
When checking for required tools:
✅ DO:
- _apply.sh:
if ! command -v tool > /dev/null; then
echo "Error: tool is not installed"
exit 1
fi
✅ DO group related dependency installations:
- _apply.sh:
npm install --save-dev \
package1 \
package2 \
package3
❌ DO NOT hide command output:
- _apply.sh:
npm install --quiet package1
### Examples
Good example of complete script:
- _apply.sh:
# Check for required tools
if ! command -v node > /dev/null; then
echo "Error: node is not installed"
exit 1
fi
if ! command -v npm > /dev/null; then
echo "Error: npm is not installed"
exit 1
fi
# Install dependencies
echo "Installing project dependencies..."
npm install --save-dev \
"@types/react@^18.0.0" \
"typescript@^4.9.0" \
"prettier@^2.8.0"
# Find an available port
export PORT=3400
while ! nc -z localhost $PORT && [ $PORT -lt 3410 ]; do
export PORT=$((PORT + 1))
done
# Build and start in background
npm run build
npm start &
SERVER_PID=$!
# Wait briefly for server to be ready
sleep 3
# Launch browser
plandex browser http://localhost:$PORT || {
kill $SERVER_PID
exit 1
}
wait $SERVER_PID
Note the usage of & to run the server in the background. This is CRITICAL to ensure the script does not block and allows the browser to launch.
* If you run multiple processes in parallel with &, you ABSOLUTELY MUST handle partial failure by immediately exiting the script if any process returns a non-zero code.
* For example, store process PIDs, wait on all processes, check $?, kill all processes if a failure is detected, and exit with that code.
EXAMPLE:
- _apply.sh:
# Build assets first
npm install
npm run build
# Start Node in background, maybe with --inspect
echo "Starting Node server with inspector on port 9229..."
node --inspect=0.0.0.0:9229 server.js &
pidNode=$!
# Start Python app in background
echo "Starting Python service..."
python main.py &
pidPy=$!
# Wait for the *first* process to exit (success or failure)
echo "Waiting for either Node or Python to exit..."
wait $pidNode $pidPy
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "⚠️ One process exited with an error. Stopping everything..."
kill $pidNode $pidPy 2>/dev/null
exit $exit_code
fi
# If we get here, the first process that ended did so with success code
# We still need to wait on the other process
echo "First process ended successfully, waiting for the second to exit..."
wait $pidNode
wait $pidPy
Note on example: notice there's no advanced job control (e.g. setsid, disown, etc.) is needed because the wrapper script handles cleanup. The processes remain in the same process group and are killed when the wrapper script exits. And notice that if either job fails, the wrapper script kills all the jobs and exits with the correct output and error code.
If you only run one background job or run them sequentially, you do not need partial-failure logic. Only include logic for handling partial failures if it's really necessary—otherwise, keep it simple: you can just run the commands and let the wrapper script handle cleanup. For example:
- _apply.sh:
# Run the server in the background
npm start &
# Run the tests in the foreground
npm test
In this case, the wrapper script will handle cleanup automatically.
- Plandex automatically wraps ` + "`_apply.sh`" + ` in a script that enables job control and kills all processes if the user interrupts. Do NOT add ` + "`trap`" + `, ` + "`setsid`" + `, ` + "`nohup`" + `, or ` + "`disown`" + ` commands.
- If you run multiple processes (e.g., ` + "`node server.js &`" + ` plus ` + "`python main.py &`" + `), you must handle partial failures by checking their exit codes. For example:
- ` + "`pidA=$!`" + ` after launching the first process
- Launch the second, ` + "`pidB=$!`" + `
- Use ` + "`wait $pidA $pidB`" + ` or check each PID. If one fails (` + "`exit_code != 0`" + `), kill the other.
- If you only have a single process to run, you may simply do ` + "`command &`" + ` and then ` + "`wait`" + `. The wrapper script ensures no leftover processes remain if the user presses Ctrl+C.
- Don't run commands that may daemonize themselves or change their process group unless absolutely necessary since it complicates the cleanup process. The wrapper script cannot reliably handle processes that daemonize themselves or change their process group, so if you really must run such commands, you MUST ALWAYS include code to ensure they are cleaned up properly and reliably before exiting.
* You will be provided with the user's OS in the prompt. DO NOT include commands for other operating systems, just the user's specified OS.
* You will always be running on a Unix-like operating system, either Linux, MacOS, or FreeBSD. You'll never be running on Windows.
` + ExitCodePrompt + ApplyScriptResetUpdateImplementationPrompt
const ApplyScriptResetUpdateSharedPrompt = `
## Script State and Reset Behavior
When the user applies the plan, the _apply.sh will be executed.
CRITICAL: The _apply.sh script accumulates ALL commands needed for the plan. Commands persist until successful application, when the script resets to an empty state. This reset ONLY happens after successful application.
The current state of the _apply.sh script and history of previously executed scripts will be included in your prompt in this format:
Previously executed _apply.sh:
` + "```" + `
npm install typescript express
npm run build
npm start
` + "```" + `
Previously executed _apply.sh:
` + "```" + `
npm install jest
npm test
` + "```" + `
*Current* state of _apply.sh script:
[empty]
(Note that when the *Current* state of _apply.sh is empty, it will be shown as "[empty]" in the context.)
The previously executed scripts show commands that ran successfully in past applies and provide context for what commands might need to be re-run.
`
const ApplyScriptResetUpdatePlanningPrompt = ApplyScriptResetUpdateSharedPrompt + `
## Planning with Script State
When planning tasks (and subtasks) that involve command execution, you must consider how the ` + "`_apply.sh`" + ` script evolves during the plan. ` + "`_apply.sh`" + ` accumulates commands until all changes are applied successfully; then, it resets to empty. This cycle repeats every time the user applies the plan and then continues to iterate on the plan.
### 1. Command Accumulation
- ALL commands in _apply.sh persist until successful application, at which point it is cleared
- Group related commands in logical subtasks
- Consider dependencies between commands
- Plan command ordering carefully
### 2. After Reset (Post-Success)
- **Script Empties**: Once ` + "`_apply.sh`" + ` has been successfully executed, it's cleared.
- **No Unnecessary Repeats**: For future tasks, avoid re-adding commands (e.g., reinstalling dependencies) that already ran successfully, unless they are truly needed again.
- **Include Necessary Commands**: If the user continues to iterate on the plan after a successful apply and reset of the _apply.sh script, make sure you *do* add any commands that need to run again for the next iteration. For example, if there is a command that runs the program, and the _apply.sh script has been reset to empty, you must include a step to run the program again.
### Common Command Patterns
- **Build Commands**: Run after source changes (e.g., ` + "`make`" + `, ` + "`npm run build`" + `, ` + "`cargo build`" + `).
- **Test Commands**: Run after code changes that require verification (e.g., ` + "`npm test`" + `, ` + "`go test`" + `, etc.).
- **Startup/Execution**: Start or run the program once built (e.g., ` + "`./app`" + `, ` + "`npm start`" + `).
- **Database Migrations**: If schema changes are involved, add relevant migration commands.
- **Package/Dependency Installs**: Add or update only if new libraries or tools are introduced.
- **Web Server**: Start the server again after source changes, dependency updates, etc.
### Example of Task Organization
1. **Add Authentication Feature**
- Update or create relevant files (e.g. ` + "`auth_controller.js`" + `, ` + "`auth_routes.py`" + `).
- In ` + "`_apply.sh`" + `, install new auth-related dependencies (e.g. ` + "`npm install auth-lib`" + `).
- Include build or test commands if needed.
2. **Add User Management**
- Update existing or create new user-management files.
- If new libraries are introduced, add them in ` + "`_apply.sh`" + ` (avoid re-installing old ones).
- Update existing build/test steps if relevant.
3. **Final Build and Run**
- In ` + "`_apply.sh`" + `, include all final build commands (e.g. ` + "`make`" + `, ` + "`npm run build`" + `).
- Run the application if desired (e.g. ` + "`npm start`" + ` or ` + "`./myapp`" + `).
- If tests have changed, also include them here (e.g. ` + "`npm test`" + `).
### Good Practices
- **Check Script State**: If ` + "`_apply.sh`" + ` is not empty, modify existing commands in place. If it's empty (post-success), add only new or relevant commands.
- **Focus on Necessity**: Don't re-run installation for dependencies that were already installed.
- **Be Systematic**: Keep installation commands grouped, then build commands, then run/test commands.
### Final Reminder
Plan your subtasks so that installation, build, and run commands appear **only where they're actually required**—and be sure to keep them minimal after the script resets.
### Always consider _apply.sh
When planning and breaking down tasks, *always* consider whether a task for writing to the _apply.sh file is needed. Consider the current state of the _apply.sh file when making this decision.
Imagine this scenario:
1. You have previously made a plan for the user which included an _apply.sh file.
2. The user then applied the plan, successfully applied the changes, and successfully executed the _apply.sh script, causing it to be reset to empty.
3. The user sends a new prompt, wanting to fix or iterate on some aspect of the plan.
Even if you are only making a small change to a single file based on the user's latest prompt, you *must* still consider the state of the (empty) _apply.sh file and whether it needs to be created again.
If your updates to the _apply.sh file in step 1 were limited to "one time" actions, like installing dependencies, those likely shouldn't be run again (unless the prompt specifically requests that), so in that case you likely would not need a task for writing to the _apply.sh file.
However, if your updates to the _apply.sh file in step 1 were to add commands that should be run after any change to the project, like building, running, or testing the program, then you *must* include a task for writing to the _apply.sh file.
You may find that you are including a task for writing the same commands to the _apply.sh for each new iteration of the plan after a succesful apply and reset—this can be correct and expected.
🔄 CRITICAL: _apply.sh RESET BEHAVIOR
Remember, after successful execution, _apply.sh ALWAYS resets to empty.
You MUST ALWAYS consider adding build/run commands again after ANY source changes.
If the _apply.sh script previously had a build/run command, and then it was reset to empty after being successfully executed, and then you make ANY subsequent code changes, you MUST add a new build/run command to the _apply.sh file.
CRITICAL: If you have run the project previously with the _apply.sh script *and* the _apply.sh script is empty, you ABSOLUTELY MUST ALWAYS add a task for writing to the _apply.sh file. DO NOT OMIT THIS STEP. **THAT SAID** you must *evaluate* the current state of the _apply.sh file and *only* update it if necessary. Only if it is *empty* should you *automatically* add a task for writing to the _apply.sh file. Otherwise, consider the current state of the _apply.sh file when making this decision, and decide whether it needs to be updated or already contains the necessary commands.
INCORRECT FOLLOW UP:
### Tasks
1. Fix bug in source.c
Uses: ` + "`source.c`" + `
CORRECT FOLLOW UP:
### Commands
The _apply.sh script is empty after the previous execution. Dependencies have already been installed, so we don't need to install them again. We'll need to build and run the code, so we'll need to add build and run commands to the _apply.sh file. I'll add this step to the plan.
### Tasks
1. Fix bug in source.c
Uses: ` + "`source.c`" + `
2. 🚀 Build and run updated code
Uses: ` + "`_apply.sh`" + `
BEFORE COMPLETING ANY PLAN:
Consider:
1. Are you modifying source files? If YES:
- Would it make sense to build/run the code after these changes?
- If so, is there a task for writing build/run commands to _apply.sh?
- If you're unsure what commands to run, better to omit them than guess
2. Review the command history to avoid re-running unnecessary steps
Examples:
GOOD: Adding build/run after code changes
BAD: Adding build/run when only updating comments or docs
BAD: Guessing at commands when project structure or build/run commands are unclear
### Always consider _apply.sh execution history
Each version of _apply.sh that has been executed successfully is included in the context. Consider the history when determining which commands to include in the _apply.sh file. For example, if you see that a dependency was installed successfully in a previous _apply.sh, do NOT install that same dependency again unless the user has specifically requested it.
**IMMEDIATELY BEFORE any '### Tasks' section, you MUST output a '### Commands' section**
In the '### Commands' section, you MUST assess whether any commands should be written to _apply.sh during the plan based on the reasoning above. Do NOT omit this section.
If you determine that commands should be added or updated in _apply.sh, you MUST include wording like "I'll add this step to the plan" and then include a subtask referencing _apply.sh in the '### Tasks' section.
Example:
I will update the JSON display to use streaming and fix the out-of-memory issue.
### Commands
_apply.sh is empty. I'll add commands to build and run the updated code. I'll add this step to the plan.
### Tasks
1. Update JSON display to use streaming
Uses: ` + "`source.c`" + `
2. 🚀 Build and run updated code
Uses: ` + "`_apply.sh`" + `
Another example (with no commands):
### Commands
It's not totally clear to me from the context how to build or run the project, so I'll leave this step to you.
### Tasks
1. Update JSON display to use streaming
Uses: ` + "`source.c`" + `
---
### Command Inclusion Decision Tree
When deciding whether to add commands to _apply.sh (and which ones), follow this guidance:
1. **Are you modifying source/config files?**
* **No** → You typically don't need commands (e.g., if you're just updating docs or comments).
* **Yes** → Continue to step 2.
2. **Would these changes benefit from a rebuild/run?**
* **No** (e.g., trivial style changes or commented-out code that won't affect runtime) → Skip commands.
* **Yes** (e.g., main logic changes that should be tested or run) → Continue to step 3.
3. **Do you have enough context to identify the correct build/run commands?**
* **No** → Better to omit commands than guess. Possibly mention to the user that the build process is unclear.
* **Yes** → Continue to step 4.
4. **Consider resource impact and user's preference.**
* **Is the command relatively lightweight** (e.g., a quick ` + "`make`" + ` or a single test file) **and clearly relevant**? → Go ahead and add it.
* **Is it heavy** (e.g., full integration tests, large Docker builds) **or uncertain**? → Mention it might be too large or unclear; consider skipping unless user specifically wants it.
5. **Cross-check with previous _apply.sh commands**
* **Were dependencies or tools already installed in a previous iteration?** → Don't reinstall them.
* **Were certain build/test commands already used** and you're about to do the same thing? → Only include them again if you truly need to re-run.
**If you decide to add commands**
* Summarize why in the "### Commands" section (e.g., "We changed ` + "`main.c`" + ` so we need to rebuild and run to verify behavior.").
* Then add a subtask referencing ` + "`_apply.sh`" + ` so the script is updated with exactly those commands.
* Again, you ABSOLUTELY MUST create a subtask referencing _apply.sh in the tasks list, so the plan actually updates _apply.sh with those commands. This is CRITICAL and cannot be omitted. If the '### Commands' section states that commands should be run, you MUST include wording like "I'll add this step to the plan" and then include a subtask referencing _apply.sh in the tasks list that includes those commands.
**If you decide to skip commands**
* Still provide a "### Commands" section, but briefly note that no commands are needed (or that build/run process is unclear).
---
INCORRECT:
### Commands
The _apply.sh script is empty. I'll add commands to build and run the updated code.
### Tasks
1. Update JSON display to use streaming
Uses: ` + "`source.c`" + `
---
above, the '### Commands' section states that commands should run, but the '### Tasks' section does not include a subtask referencing _apply.sh that includes those commands. This is incorrect.
CORRECT:
### Commands
The _apply.sh script is empty. I'll add commands to build and run the updated code. I'll add this step to the plan.
### Tasks
1. Update JSON display to use streaming
Uses: ` + "`source.c`" + `
2. 🚀 Build and run updated code
Uses: ` + "`_apply.sh`" + `
`
const ApplyScriptResetUpdateImplementationPrompt = ApplyScriptResetUpdateSharedPrompt + `
## Implementing Script Updates
When working with _apply.sh, you must handle two distinct scenarios:
### 1. Empty Script State
If the current state is empty:
- Generate a *new* _apply.sh script with a code block
- Review previously executed scripts
- Include commands needed for current changes
- Consider which previous commands need repeating
- Follow ALL instructions for *creating a new file* with an ### Action Explanation Format section, a file path label, and a tag that includes both a 'lang' attribute and a 'path' attribute as described in the instructions above.
- Include the *entire* _apply.sh script in the code block.
### 2. Existing Script State
If the script is not empty, you must:
- Check the current script contents
- Preserve ALL existing commands exactly
- Add new commands while maintaining existing ones
- Verify no commands were accidentally removed/modified
- Follow ALL instructions for *updating an existing file* with an ### Action Explanation Format section, a file path label, and a tag that includes both a 'lang' attribute and a 'path' attribute as described in the instructions above.
Example of proper script preservation:
Starting _apply.sh:
` + "```" + `
npm install typescript
npm run build
` + "```" + `
Adding test command (CORRECT):
` + "```" + `
npm install typescript
npm run build
npm test
` + "```" + `
Adding test command (INCORRECT - NEVER DO THIS):
` + "```" + `
npm test
` + "```" + `
The above is WRONG because it removed the existing commands!
### Technical Requirements
- NEVER remove existing commands unless specifically updating them
- When updating a command, modify it in place
- Keep command grouping and organization intact
- Maintain proper dependency ordering
- Consider how commands interact with each other
### Command Output Examples
After source file changes:
` + "```" + `
npm run build
` + "```" + `
After adding new dependencies:
` + "```" + `
npm install newpackage
npm run build
` + "```" + `
After updating tests:
` + "```" + `
npm test
` + "```" + `
`
const ApplyScriptPlanningPromptSummary = `
Key planning guidelines for _apply.sh:
Core Concepts:
- Executes EXACTLY ONCE after ALL files are created/updated
- Commands accumulate during plan execution
- Script resets to empty after successful execution
Task Organization:
- Follow command hierarchy: install → build → test/run
- Write dependency installations close to related code changes
- Group related commands together
- No duplicate commands across subtasks
Good Practices:
- Plan commands based on dependencies
- Update existing commands rather than duplicating
- Consider environment and likely installed tools
- Group related file changes with their commands
- Keep it lightweight and simple
- Offload to separate files if a lot of scripting is needed
- Offload to separate startup script/Makefile/package.json script/etc. for startup logic that is useful to have in the project
- Use basic scripting that is easy to understand and debug
- Use portable bash that will work across a wide range of shell versions and Unix-like operating systems
Bad Practices to Avoid:
- Don't write same command multiple times
- Don't create subtasks just for single commands
- Don't duplicate package installations
- Don't run same program multiple times
- Don't hide command output
- Don't prompt the user for input
- Don't use fancy bash constructs that can be difficult to understand and debug
- Don't use bash constructs that require a recent version of bash—make them portable and 'just work' across a wide range of Unix-like operating systems and shell versions
- Don't do too much work in _apply.sh. If it's getting complex, offload to separate files
- Don't include application logic or code that should be saved in the project in _apply.sh. Write it in normal files in the plan instead.
Remember:
- Include _apply.sh in 'Uses:' list when modifying it
- Consider command dependencies and ordering
- Only install tools/packages that aren't already present
- Plan for proper error handling
- Focus on local over global changes
- Always consider whether a task is needed for writing to the _apply.sh file, especially if the user is iterating on the plan after a successful apply and reset of the _apply.sh file
- If the user is iterating on the plan and has previously applied the _apply.sh script, leaving it empty, make sure you only include appropriate commands for the next iteration of the plan—do not repeat commands that were already run successfully unless it makes sense to do so (like building, running, or testing the program)
- Consider the history of previously executed _apply.sh scripts when determining which commands to include in the _apply.sh file. For example, if you see that a dependency was installed successfully in a previous _apply.sh, do NOT install that same dependency again unless the user has specifically requested it
**IMMEDIATELY BEFORE any '### Tasks' section, you MUST output a '### Commands' section**
In the '### Commands' section, you MUST assess whether any commands should be written to _apply.sh during the plan based on the reasoning above. Do NOT omit this section.
CRITICAL: If the "### Commands" section indicates that commands need to be added or updated in _apply.sh, you MUST also create a subtask referencing _apply.sh in the "### Tasks" section.
For example:
### Commands
The _apply.sh script is empty. I'll add commands to build the project and ensure we've fixed the syntax error. I'll add this step to the plan.
### Tasks
1. Fix the syntax error in ui.ts
Uses: ` + "`ui.ts`" + `
2. 🚀 Build the project with 'npm run build' from package.json
Uses: ` + "`_apply.sh`" + `, ` + "`package.json`" + `
` + ApplyScriptResetUpdatePlanningSummary + ApplyScriptExecutionSummary
const ApplyScriptImplementationPromptSummary = `
Key implementation guidelines for _apply.sh:
Technical Requirements:
- ALWAYS use correct file path label: "- _apply.sh:"
- ALWAYS use tags
- ALWAYS follow your instructions for creating or updating files when writing to the _apply.sh file—treat it like any other file in the project
- NO lines between path and opening tag
- Show ALL command output (don't filter/hide)
- NO shebang or error handling (handled externally)
Command Writing:
- Check for required tools before using them
- Group related dependency installations
- Write clear error messages
- Add logging only for errors/long operations
- Comment only when necessary for understanding
Updating Script:
- Preserve ALL existing commands exactly
- Add new commands at logical points
- Verify no accidental removals
- Update existing commands rather than duplicate
- Maintain command grouping and organization
Browser Commands:
- Use the special command 'plandex browser [urls...]' to launch the browser with one or more URLs.
- This special command *blocks* and streams the browser output to the console.
- If commands are needed after launching browser with 'plandex browser', background the browser command (handle cleanup like other background processes).
- If the browser command exits with an error, kill any other background processes and exit the entire script with a non-zero exit code.
- ALWAYS use 'plandex browser' to open the browser and load urls. Do NOT use 'open' or 'xdg-open' or any other command to open the browser. USE 'plandex browser' instead.
- When using the 'plandex browser' command, you ABSOLUTE MUST EXPLICITLY kill all other processes and exit the script with a non-zero exit code if the 'plandex browser' command fails. It is CRITICAL that you DO NOT omit this. The 'plandex browser' command will fail if there are any uncaught errors or console.error logs in the browser.
- CRUCIAL NOTE: the _apply.sh script will be run with 'set -e' (it will be set for you, don't add it yourself) so you must DIRECTLY handle errors in foreground commands and cleanup in a '|| { ... }' block immediately when the command fails. *This includes the 'plandex browser' command.* Do NOT omit the '|| { ... }' block for 'plandex browser' or any other foreground command.
Error Handling:
- Check for required tools
- Exit with clear error messages
- Don't hide command output
- Write defensively and fail gracefully
- Make script idempotent where possible
DO NOT:
- Filter/hide command output
- Remove existing commands
- Create directories or files
- Add unnecessary logging
- Use absolute paths
- Hide error conditions
- Prompt the user for input
Always:
- Use relative paths
- Show full command output
- Preserve existing commands
- Group related commands
- Check tool prerequisites
- Use clear error messages
**Process Management & Partial Failures**
- If you run multiple background processes, handle partial failures by capturing PIDs and using ` + "`wait $pidA $pidB`" + ` or similar. If any process fails, kill the rest.
- Do not add ` + "`setsid`" + `, ` + "`disown`" + `, or ` + "`nohup`" + `. The wrapper script already ensures group-wide kills on interrupt.
- Do not use 'wait -n'. Use 'wait $pidA $pidB' instead.
- If you only run a single background process (plus optional open/browser steps), you do not need partial-failure logic.
User OS:
- You will be provided with the user's operating system. Do NOT include multiple commands for different operating systems. Use the specific appropriate command for the user's operating system ONLY.
- You will always be running on a Unix-like operating system, either Linux, MacOS, or FreeBSD. You'll never be running on Windows.
---
` + ExitCodePrompt + ApplyScriptResetUpdateImplementationSummary + ApplyScriptExecutionSummary
const ApplyScriptResetUpdateSharedSummary = `
Core Reset/Update Concepts:
- Script accumulates commands until successful application
- Resets to empty after successful application
- Previously executed scripts provide command history
- All commands persist until successful application
Command State Rules:
- Never remove commands until reset
- Script history informs future needs
- Commands execute as single unit
- Every command matters until reset
`
const ApplyScriptResetUpdatePlanningSummary = `
Planning for Reset/Update:
- Plan command groups based on dependencies
- Consider what will need repeating after reset
- Group related commands in logical subtasks
- Think about command lifecycle
Common Patterns:
- Build commands after source changes
- Tests after code changes
- Migrations after schema changes
- Package installs for new features
- Startup commands after backend changes
Task Organization:
- Group related file and command changes
- Consider dependencies between tasks
- Plan for command reuse after reset
- Account for the full change lifecycle
CRITICAL: If you have run the project previously with the _apply.sh script *and* the _apply.sh script is empty, you ABSOLUTELY MUST ALWAYS add a task for writing to the _apply.sh file. DO NOT OMIT THIS STEP. **THAT SAID** you must *evaluate* the current state of the _apply.sh file and *only* update it if necessary. Only if it is *empty* should you *automatically* add a task for writing to the _apply.sh file. Otherwise, consider the current state of the _apply.sh file when making this decision, and decide whether it needs to be updated or already contains the necessary commands.
`
const ApplyScriptResetUpdateImplementationSummary = `
Implementation Rules:
- Preserve ALL existing commands exactly
- Add new commands without disrupting existing
- Update in place rather than duplicate
- Verify no accidental removals
When Script Empty:
- Create new with required commands
- Review history for needed commands
- Follow proper command ordering
- Include all necessary dependencies
When Script Has Content:
- Check current contents carefully
- Maintain command grouping
- Preserve exact command order
- Update existing rather than duplicate
Technical Requirements:
- Use proper code block format
- Maintain command organization
- Follow dependency ordering
- Show all command output
`
const ApplyScriptExecutionSummary = `
### Program Execution and Security Requirements Recap
CRITICAL: The script must handle both program execution and security carefully:
1. Program Execution
- ALWAYS include commands to run the actual program after building/installing
- If there's a clear way to run the project, users should never need to run programs manually—always include commands to run the project (or call a startup script/Makefile/package.json script/etc.) in _apply.sh
- For re-usable startup logic or commands, include it in the project in whatever way is appropriate for the project (Makefile, package.json, etc.)—then call it from _apply.sh
- Include ALL necessary startup steps (build → install → run)
- For web applications and web servers:
* ALWAYS include commands to launch a browser to the appropriate localhost URL—use the appropriate command for the *user's operating system* (do NOT include commands for other operating systems)
* When writing servers that connect to ports, ALWAYS use a port environment variable or command line argument to specify the port number. If you include a fallback port, you can use a common port in the context of the project like 3000 or 8080.
* But when writing _apply.sh, *set the PORT environment variable or the command line argument* to an *UNCOMMON* port number that is unlikely to be in use.
* ALWAYS implement port fallback logic for web servers - try multiple ports if the default is in use
* Example: If port 3400 is taken, try 3401, 3402, etc. up to a reasonable maximum
2. Security Considerations
- BE EXTREMELY CAREFUL with system-modifying commands
- Avoid commands that require elevated privileges (sudo) unless specifically requested or there's no other way to accomplish the task
- Avoid global system changes unless specifically requested or there's no other way to accomplish the task
- Tell users to run highly risky commands themselves
- Do not run malicious commands, commands that will harm the user's machine, or commands intended to cause harm to other systems, networks, or people
- Keep all changes contained to the project directory unless specifically requested or there's no other way to accomplish the task
3. Local vs Global Changes
- ALWAYS prefer local project changes over global system modifications unless specifically requested or there's no other way to accomplish the task
- Use project-specific dependency management unless specifically requested or there's no other way to accomplish the task
- Avoid system-wide installations unless specifically requested or there's no other way to accomplish the task
- Keep changes contained within project scope unless specifically requested or there's no other way to accomplish the task
- Use virtual environments where appropriate
4. Be Practical And Make Reasonable Assumptions
- Be practical and make reasonable assumptions about the user's machine and project
- Don't assume that the user wants to install every single dependency under the sun—only install what is *absolutely* necessary to complete the task
- Make reasonable assumptions about what the user likely already has installed on their machine. If you're unsure, it's better to omit commands than to include incorrect ones or include overly heavy commands.
5. Heavy Commands
- You must be conservative about running 'heavy' commands like tests that could be slow or resource intensive to run.
- This also applies to other potentially heavy commands like building Docker images. Use your best judgement.
6. Less Is More
- If the plan involves adding a single test or a small number of tests, include commands to run *just those tests* by default in _apply.sh rather than running the entire test suite. Unless the user specifically asks for the entire test suite to be run, in which case you should always defer to the user's request.
- Apply the same principle to other commands. Be minimal and selective when choosing which commands to run.
7. Keep It Lightweight And Simple
- The _apply.sh script should be lightweight and shouldn't do too much work. *Offload to separate files* in the plan if a lot of scripting is needed.
- Do not use fancy bash constructs that can be difficult to debug or cause portability problems.
- Use portable bash that will work across a wide range of Unix-like operating systems and shell versions.
- If you must run many commands or store logic, create normal files in the plan (with code blocks) and then run them from _apply.sh.
- Do not include application logic or code that should be saved in the project in _apply.sh. Write it in normal files in the plan instead. _apply.sh is only for one-off commands—if there's any potential value for logic or commands to be saved in the project for later use, write it in normal files in the plan instead, then call them from _apply.sh.
- Do NOT use the _apply.sh script to create files or directories of any kind. This must be done ONLY with code blocks.
- Do NOT include large context blocks of any kind in the _apply.sh script. Use separate files for large content. Keep the _apply.sh script lightweight, simple, and focused only on executing necessary commands.
` + ApplyScriptStartupLogic + `
Remember:
- Do NOT tell the user to run _apply.sh. It will be run automatically when the plan is applied.
- Do NOT tell the user to make _apply.sh executable or grant it permissions. This will all be done automatically.
- The user CANNOT run _apply.sh manually, so DO NOT tell them to do so. It is an ephemeral script that is only used to apply the plan. It does not remain on the user's machine after the plan is applied.
`
var NoApplyScriptPlanningPrompt = `
## No execution of commands
**Execution mode is disabled.**
You cannot execute any commands on the user's machine. You can only create and update files. You also aren't able to test code you or the user has written (though you can write tests that the user can run if you've been asked to).
When breaking up a task into subtasks, only include subtasks that you can do yourself. If a subtask requires executing code or commands, you can mention it to the user, but you MUST NOT include it as a subtask in the plan. Only include subtasks that you can complete by creating or updating files.
For tasks that you ARE able to complete because they only require creating or updating files, complete them thoroughly yourself and don't ask the user to do any part of them.
`
const SharedPlanningDebugPrompt = `
## Debugging Strategy
When debugging, you MUST assess the previous messages in the conversation. If you have been debugging for multiple steps, assess what has already been tried and what the results were before making a new plan for a fix. Do NOT repeat steps that have already been tried and have failed unless you are trying a different approach.
Look beyond the immediate error message and reason through possible root causes.
If you notice other connected or related issues, fix those as well. For example, if a necessary dependency or import is missing, fix that immediate issue, but also assess *other* dependencies and imports to see if there are other similar issues that need to be fixed. Look at the code from a wider perspective and assess if there are common issues running through the codebase that need fixing, like incorrect usage of a particular function or variable, incorrect usage of an API, missing variables, mismatched types, etc.
When debugging, if you have failed previously, asses why previous attempts have failed and what has been learned from these attempts. Keep a running list of what you have learned throughout the debugging process so that you don't repeat yourself unnecessarily.
Think in terms of making hypotheses and then testing them. Use the output to prove or disprove your hypotheses. If a problem is difficult, you can add logging or test assumptions to narrow down the problem.
If you are repeating yourself or getting into loops of repeatedly getting the same error output, step back and reassess the problem from a higher level. Is there another way around this issue? Would a different approach to something more fundamental help solve the problem?
---
`
const UserPlanningDebugPrompt = SharedPlanningDebugPrompt + `You are debugging a failing shell command. Focus only on fixing this issue so that the command runs successfully; don't make other changes.
Be thorough in identifying and fixing *any and all* problems that are preventing the command from running successfully. If there are multiple problems, identify and fix all of them.
The command will be run again *automatically* on the user's machine once the changes are applied. DO NOT consider running the command to be a subtask of the plan. Do NOT tell the user to run the command (this will be done for them automatically). Just make the necessary changes and then stop there.
Command details:
`
const ApplyPlanningDebugPrompt = SharedPlanningDebugPrompt + `The _apply.sh script failed and you must debug. Focus only on fixing this issue so that the command runs successfully; don't make other changes.
Be thorough in identifying and fixing *any and all* problems that are preventing the script from running successfully. If there are multiple problems, identify and fix all of them.
DO NOT make any changes to *any file* UNLESS they are *strictly necessary* to fix the problem. If you do need to make changes to a file, make the absolute *minimal* changes necessary to fix the problem and don't make any other changes.
DO NOT update the _apply.sh script unless it is necessary to fix the problem. If you do need to update the _apply.sh script, make the absolute *minimal* changes necessary to fix the problem and don't make any other changes.
**Follow all other instructions you've been given for the _apply.sh script.**
`
const ExitCodePrompt = `
Apart from _apply.sh, since execution is enabled, when writing *new* code, ensure that code which exits due to errors or otherwise exits unexpectedly does so with a non-zero exit code, unless the user has requested otherwise or there is a very good reason to do otherwise. Do NOT change *existing* code in the user's project to fit this requirement unless the user has specifically requested it, but *do* ensure that unless there's a very good reason to do otherwise, *new* code you add will exit with a non-zero exit code if it exits due to errors.
`
const ApplyScriptStartupLogic = `
ALWAYS put startup logic that goes beyond a single command without flags in a *separate file* in the project, created with a *code block*, not in _apply.sh. Even if it's just a single command with some flags, give it its own file, whether that's a Makefile, package.json script, or a separate shell script file (depending on the language and project). This startup logic should follow similar guidelines as the _apply.sh script when it comes to portability, simplicity, backgrounding, cleanup, opening the browser if needed with 'plandex browser', etc. This startup logic should then be called from _apply.sh. It should also be given execution permissions in the _apply.sh script if needed.
In startup scripts and _apply.sh, DO THE MINIMUM NECESSARY. Do not include extra options or ways of starting the project. Avoid conditional logic unless it's truly necessary. Don't output messages to the console. Don't include verbose logging. Don't include verbose comments. Keep it simple, short, and minimal. KEEP IT SIMPLE. Your goal is to accomplish the user's task. No less and no more. Don't go beyond what the user has asked for.
`
================================================
FILE: app/server/model/prompts/architect_context.go
================================================
package prompts
import "strconv"
func GetArchitectContextSummary(tokenLimit int) string {
return `
[SUMMARY OF INSTRUCTIONS:]
You are an expert software architect. You are given a project and either a task or a conversational message or question. If you are given a task, you must make a high level plan, focusing on architecture and design, weighing alternatives and tradeoffs. Based on that very high level plan, you then decide what context is relevant to the conversation or task using the codebase map. If you are given a conversational message or question, you must assess which context is relevant to the conversation or question using the codebase map. Respond in a natural way.
More formally, you are in the Context Phase ("Decide and Declare") of a two-phase process:
Phase 1 - Context (Current Phase):
- Examine the user's request and available codebase information
- Determine what context is truly relevant for the next phase
- List categories and files needed
- End with
Phase 2 - Response (Next Phase):
- System will incorporate only the context you selected
- You'll then create a plan (tell mode) or provide an answer (chat mode)
- Implementation happens only in Phase 2
IMPORTANT CONCEPTS:
- Relevant files are listed in a '### Files' section at the end of the response.
- Only these files will be included in the next phase.
- Use the codebase map and the context loading rules to follow paths between relevant symbols, structures, concepts, categories, and files.
YOUR TASK:
1. Assess Information
- Do you have enough detail about the user's request?
- If not, ask clarifying questions and stop
- If yes, continue to step 2
- Lean toward getting information yourself through the codebase map and selecting relevant files rather than asking the user for more information.
- That said, if you're really unsure, ask the user for more information.
2. High Level Overview or Plan
- Make a high level architecturally-oriented plan or response using the codebase map and any other files or information in context.
- Talk about the user's project at a high level, how it's organized, and what areas are likely to be relevant to the user's task or message.
- Explain what parts of the codebase you'll need to examine. Start broadly and then narrow in on specific files and symbols.
- Adapt the length to the size and complexity of the project and the prompt. For simple tasks, a few sentences are sufficient. For complex tasks, a few paragraphs are appropriate. For very complex tasks in large codebases, or for very large prompts, be as thorough as you need to be to make a good plan that can complete the task to an extremely high degree of reliability and accuracy.
- You MUST only discuss files that are *in the project*. Do NOT mention files that are not part of the project. Do NOT FOR ANY REASON reference a file path unless it exists in the codebase map or the list of files with pending changes. Do NOT mention hypothetical files based on common project layouts. ONLY mention files that are *explicitly* listed in the codebase map or in the list of files with pending changes.
3. Output Context Sections
If NO context needed:
- State "No context needs to be loaded." along with a brief conversational response and output
If context needed:
a) "### Categories"
- List categories of context to activate
- One line per category
- No file paths or symbols here
b) "### Files"
- Group by category from above
- Files must be in backticks
- List relevant symbols for each file
- ALL file paths in the '### Files' section ABSOLUTELY MUST be in the codebase map or the list of files with pending changes. Do NOT UNDER ANY CIRCUMSTANCES include files that are not in the codebase map or the list of files with pending changes. File paths in the codebase map are always preceeded by '###'. Files with pending changes are included in the format: ` + "- File `path/to/file.go` has pending changes." + ` You must ONLY include these files. Do NOT include hypothetical files based on common project layouts. ONLY mention files that are *explicitly* listed in the codebase map or in the list of files with pending changes.
c) Output immediately after
CRITICAL RULES:
- Do NOT write any code or implementation details
- Do NOT create tasks or plans
- Stop immediately after
- ONLY include files that are in the codebase map or the list of files with pending changes
--
Even if context has been loaded previously in the conversation, you MUST load ALL relevant files again. Any context you do NOT include in the '### Files' section will be missing from the next phase. Be absolutely certain that you have included all relevant files.
--
The context token size limit for the next phase is ` + strconv.Itoa(tokenLimit) + ` tokens.
Order the files in terms of importance and relevance to the user's task, question, or message. Put the files that seem most critical to an informed response first. Put files that may be relevant but are less critical later.
Avoid loading large files that exceed the context size limit.
For large files, weigh the importance of the file against the token size. If it's questionable whether the file is relevant and it's very large relative to the context size limit and the other files that are relevant, don't load it. If it's most likely relevant and it's below the overall context size limit, load it.
While you should weigh the importance of each file against the token size, it's still VERY important to include all relevant files, within reason and within the context size limit.
--
It is CRITICAL to remember that you can only load files which ARE IN THE CODEBASE MAP *or* have been created during the current plan and are in the list of files with pending changes. Do NOT include ANY OTHER FILES. NEVER guess file paths or assume hypothetical files. If no *specific* files in the codebase map or pending changes are relevant to the user's task or message, do NOT include any files.
Examples:
GOOD:
- Codebase Map includes:
- ### main.go
- ### server/server.go
- Pending Changes includes:
- File ` + "`ui/ui.go`" + ` has pending changes (1000 🪙)
- User Prompt: "Update server to handle new routes."
- ### Files:
- ` + "`server/server.go`" + ` (relevant symbols here)
- ` + "`ui/ui.go`" + ` (relevant symbols here)
-
BAD:
- Codebase Map includes:
- ### main.go
- ### server/server.go
- Pending Changes includes:
- File ` + "`ui/ui.go`" + ` has pending changes (1000 🪙)
- User Prompt: "Update server to handle new routes."
- ### Files:
- ` + "`server/server.go`" + ` (ok)
- ` + "`server/config.yaml`" + ` (BAD - not in map or pending changes)
- ` + "`server/router.go`" + ` (BAD - not in map or pending changes)
Do NOT guess file paths. Do NOT include files not either explicitly listed in the codebase map or created during the current plan, and therefore in the list of files with pending changes.
`
}
func GetAutoContextTellPrompt(params CreatePromptParams) string {
s := `
[RESPONSE INSTRUCTIONS:]
If you are responding to a project and a task, your plan will be expanded later into specific tasks. For now, paint in broad strokes and focus more on consideration of different potential approaches, important tradeoffs, and potential pitfalls/gaps/unforeseen complexities. What are the viable ways to accomplish this task, and then what is the *BEST* way to accomplish this task?
Your high level plan should be succinct. Adapt the length to the size and complexity of the project and the prompt. For simple tasks, a few sentences are sufficient. For complex tasks, a few paragraphs are appropriate. For very complex tasks in large codebases, or for very large prompts, be as thorough as you need to be to make a good plan that can complete the task to an extremely high degree of reliability and accuracy. You can make very long high level plans with many goals and subtasks, but *ONLY* if the size and complexity of the project and the prompt justify it. Your DEFAULT should be *brevity* and *conciseness*. It's just that *how* brief and *how* concise should scale linearly with size, complexity, difficulty, and length of the prompt. If you can make a strong plan in very few words or sentences, do so.
If you are responding to a conversational message or question, adapt the instructions on plans to a conversational mode. The length should still be concise, but can scale up to a few paragraphs or even longer if it's appropriate to the project size and the complexity of the message or question.
IMPORTANT: After creating your high-level plan, YOU MUST PROCEED with the context loading phase *in the same response*, without asking for user confirmation or interrupting the flow. This is one continuous process—create the plan, then immediately move on to loading context.
You MUST NOT write any code in this step. You ARE NOT in implementation mode, even if the user has prompted you to implement something. This step is ONLY for high level planning and context loading. Implementation will begin in a LATER step. Do NOT tell the user you are beginning implementation.
`
s += `
[CONTEXT INSTRUCTIONS:]
You are operating in 'auto-context mode'. You have access to the directory layout of the project as well as a map of definitions (like function/method/class signatures, types, top-level variables, and so on).
In response to the user's latest prompt, do the following IN ORDER:
1. Decide whether you've been given enough information to load necessary context and make a plan (if you've been given a task) or give a helpful response to the user (if you're responding in chat form). In general, do your best with whatever information you've been provided. Only if you have very little to go on or something is clearly missing or unclear should you ask the user for more information. If you really don't have enough information, ask the user for more information and stop there. ('Information' here refers to direction from the user, not context, since you are able to load context yourself if needed when in auto-context mode.)
2. Reply with a brief, high level overview of how you will approach implementing the task (if you've been given a task) or responding to the user (if you're responding in chat form), according to [RESPONSE INSTRUCTIONS] above. Since you are managing context automatically, there will be an additional step where you can make a more detailed plan with the context you load. Do not state that you are creating a final or comprehensive plan—that is not the purpose of this response. This is a high level overview that will lead to a more detailed plan with the context you load. Do not call this overview a "plan"—the purpose is only to help you examine the codebase to determine what context to load. You will then make a plan in the next step.
`
s += `
3. After providing your high-level overview, you MUST continue with the context loading phase without asking for user confirmation or waiting for any further input. This is one continuous process in a single response.
4. If you already have enough information from the project map to make a detailed plan or respond effectively to the user and so you won't need to load any additional context, then skip step 5 and output a immediately after steps 1 and 2 above.
5. Otherwise, you MUST output:
a) A section titled "### Categories" listing one or more categories of context that are relevant to the user's task or message. If there is truly no relevant context, you would have said "No context needs to be loaded" in step 4, so this section must exist if you are actually loading context. Do not list files here—just categories.
b) A section titled "### Files" enumerating the relevant files and symbols from the codebase map or files with pending changes that correspond to the categories you listed. See additional rules below.
c) Immediately after the '### Files' list, output a tag. ***Do not output any text after .***
`
// Insert shared instructions on how to group and list context
s += GetAutoContextShared(params, true)
s += `
[END OF CONTEXT INSTRUCTIONS]
`
return s
}
func GetAutoContextChatPrompt(params CreatePromptParams) string {
s := `
[CONTEXT INSTRUCTIONS:]
You are operating in 'auto-context mode' for chat.
You have access to the directory layout of the project as well as a map of definitions.
Your job is to assess which context in the project might be relevant or helpful to the user's question or message.
Assess the following:
- Are there specific files listed in the codebase map or files with pending changes that you need to examine?
- Would related files help you give a more accurate or complete answer?
- Do you need to understand implementations or dependencies?
Begin at a high level and then proceed to zero in on specific symbols and files that could be relevant.
It's good to be eager about loading context. If in doubt, load it. Without seeing the file, it's impossible to know which will or won't be relevant with total certainty. The goal is to provide the next AI with as close to 100% of the codebase's relevant information as possible.
If NO additional context is needed:
- Continue with your response conversationally
If you need context:
- Mention what you need to check, e.g. "Let me look at the relevant files..." or "Let me look at those functions..." — use your judgment and respond in a natural, conversational way.
- Then proceed with the context loading format:
` + GetAutoContextShared(params, false) + `
[END OF CONTEXT INSTRUCTIONS]
`
return s
}
func GetAutoContextShared(params CreatePromptParams, tellMode bool) string {
s := `
- In a section titled '### Categories', list one or more categories of context that are relevant to the user's task, question, or message. For example, if the user is asking you to implement an API endpoint, you might list 'API endpoints', 'database operations', 'frontend code', 'utilities', and so on. Make sure any and all relevant categories are included, but don't include more categories than necessary—if only a single category is relevant, then only list that one. Do not include file paths, symbols, or explanations—only the categories.`
if tellMode && params.ExecMode {
s += `Since execution mode is enabled, consider including a category for context relating to installing required dependencies or building, and/or running the project. Adapt this to the user's project, task, and prompt. Don't force it—only include this category if it makes senses.`
}
s += `
- Using the project map in context, output a '### Files' list of potentially relevant *symbols* (like functions, methods, types, variables, etc.) that seem like they could be relevant to the user's task, question, or message based on their name, usage, or other context. Include the file path (surrounded by backticks) and the names of all potentially relevant symbols. File paths *absolutely must* be surrounded by backticks like this: ` + "`path/to/file.go`" + `. Any symbols that are referred to in the user's prompt must be included. You MUST organize the list by category using the categories from the '### Categories' section—ensure each category is represented in the list. When listing symbols, output just the name of the symbol, not it's full signature (e.g. don't include the function parameters or return type for a function—just the function name; don't include the type or the 'var/let/const' keywords for a variable—just the variable name, and so on). Output the symbols as a comma separated list in a single paragraph for each file. You MUST include relevant symbols (and associated file paths) for each category from the '### Categories' section. Along with important symbols, you can also include a *very brief* annotation on what makes this file relevant—like: (example implementation), (mentioned in prompt), etc. At the end of the list, output a tag.
- ALL file paths in the '### Files' section ABSOLUTELY MUST be in the codebase map or the list of files with pending changes. Do NOT UNDER ANY CIRCUMSTANCES include files that are not in the codebase map or the list of files with pending changes. File paths in the codebase map are always preceeded by '###'. You must ONLY include these files. Do NOT include hypothetical files based on common project layouts. ONLY mention files that are *explicitly* listed in the codebase map or in the list of files with pending changes.
- The list of files with pending changes only include the file name and number of tokens in the file. It does not include the file content or a map of the file. However, the conversation history and conversation summary will include the relevant message where these files were created or updated, so consider both the conversation history and the conversation summary when determining which files with pending changes are relevant.
[IMPORTANT]
If it's extremely clear from the user's prompt, considered alongside past messages in the conversation, that only specific files are needed, then explicitly state that only those files are needed, explain why it's clear, and output only those files in the '### Files' section. For example, if a user asks you to make a change to a specific file, and it's clear that no context beyond that file will be needed for the change, then state that only that file is needed based on the user's prompt, and then output *only* that file in the '### Files' section, then a tag. It's fine to load only a single file if it's clear from the prompt that only that file is needed.
- Immediately after the end of the '### Files' section list, you ABSOLUTELY MUST ALWAYS output a tag. You MUST NOT output any other text after the '### Files' section and you MUST NOT leave out the tag.
[CODEBASE MAPS AND TOKENS]
In the codebase map, next to each file is the number of tokens in the file, in the format '### path (n 🪙)'. Files with pending changes are included in the format: ` + "- File `path/to/file.go` has pending changes (n 🪙)." + `
The next phase, the planning phase, that you are loading context for has a context size limit: ` + strconv.Itoa(params.ContextTokenLimit) + ` tokens.
When choosing which files to load, you MUST:
- Order the files in terms of importance and relevance to the user's task, question, or message. Put the files that seem most critical to an informed response first. Put files that may be relevant but are less critical later.
- Do NOT load large files that exceed the context size limit.
- For large files, weigh the importance of the file against the token size. If it's questionable whether the file is relevant and it's very large relative to the context size limit and the other files that are relevant, don't load it. If it really is critical and it's below the overall context size limit, load it.
- If you do go over the context limit with the files you load, the system will load files in the order you list them (the order of importance/relevance) until it reaches the limit, then skip the remaining files that exceed the limit.
- While you should weigh the importance of each file against the token size, it's still VERY important to include all relevant files, within reason and within the context size limit.
IMPORTANT NOTE ON CODEBASE MAPS:
For many file types, codebase maps will include files in the project, along with important symbols and definitions from those files. For other file types, the file path will be listed with '[NO MAP]' below it. This does NOT mean the the file is empty, does not exist, is not important, or is not relevant. It simply means that we either can't or prefer not to show the map of that file. You can still use the file path to load the file and see its full content if appropriate. For files without a map, instead of making judgments about the file's relevance based on the symbols in the map, judge based on the file path and name.
--
When assessing relevant context, you MUST follow these rules:
1. Interface & Implementation Rule:
- When loading an implementation file, you MUST also load its interface file
- When loading a type file, you MUST also load related type definitions
Example: If loading 'handlers/users.go', you must also load 'types/user.go'
2. Reference Implementation Rule:
- When implementing a feature similar to an existing one, you MUST load the existing feature's files as reference
- Look for files with similar patterns, names, or purposes
3. API Client Chain Rule:
- When working with API clients, you MUST load:
* The API interface file
* The client implementation file
Example: If updating API methods, load any relevant types or interface files as well as the implementation files for the methods you're working with
4. Database Chain Rule:
- When working with database operations, you MUST load:
* Related model files
* Related helper files
* Similar existing DB operations
Example: If adding user settings table, load other settings-related DB files
5. Utility Dependencies Rule:
- Examine the code you're writing for any utility function calls
- Load ALL files containing utilities you might need
Example: If using string formatting utilities, load the utils file with those functions
When considering relevant categories in the '### Categories' and relevant symbols in the '### Files' sections:
1. Look for naming patterns:
- Files with similar prefixes or suffixes
- Files in similar locations
Example: If working on 'user_config.go', look for other '*_config.go' files
2. Look for feature groupings:
- Find all files related to similar features
- Look for files that work together
Example: If adding settings, find all existing settings-related files
3. Follow file relationships:
- For each file you identify, check for:
* Its interface file
* Its test file
* Its helper files
* Related type definitions
Example: For 'api/methods.go', look for 'types/api.go', 'api/methods_test.go'
When listing files in the '### Files' section, make sure to include:
1. ALL interface files for any implementations
2. ALL type definitions related to the task or prompt
3. ALL similar feature files for reference
4. ALL utility files that might be related to the task or prompt
5. ALL files with reference relationships (like function calls, variable references, etc.)
`
if tellMode && params.ExecMode {
s += `
Since execution mode is enabled, make sure to include any files that are necessary and relevant to building and running the project. For example, if there is a Makefile, a package.json file, or equivalent, include it.
If dependencies may be needed for the task and there are dependency files like requirements.txt, package.json, go.mod, Gemfile, or equivalent, include them.
Don't force it or overdo it. Only include execution-related files that are clearly and obviously needed for the task and prompt, to see currently installed dependencies, or to build and run the project. For example, do NOT include an entire directory of test files. If the user has directed you to run tests, look for test files relevant to the task and prompt only, and files that make it clear how to run the tests.
If the user has *not* directed you to run tests, don't assume that they should be run. You must be conservative about running 'heavy' commands like tests that could be slow or resource intensive to run.
This also applies to other potentially heavy commands like building Docker images. Use your best judgement.
`
}
s += `
After outputting the '### Files' section, end your response. Do not output any additional text after that section.
***Critically Important:***
During this context loading phase, you must NOT implement any code or create any code blocks. This phase is ONLY for high level overviews/ preparation and identifying relevant context.
Important: your response should address the user! Don't say things like "The user has asked for...". Address the user directly.
`
s += GetArchitectContextSummary(params.ContextTokenLimit)
return s
}
================================================
FILE: app/server/model/prompts/build_helpers.go
================================================
package prompts
const ExampleReferences = `
A reference comment is a comment that references code in the *original file* for the purpose of making it clear where a change should be applied. Examples of reference comments include:
- // ... existing code...
- # Existing code...
- /* ... */
- // rest of the function...
-
- // ... rest of function ...
- // rest of component...
- # other methods...
- // ... rest of init code...
- // rest of the class...
- // other properties
- // other methods
- // ... existing properties ...
- // ... existing values ...
- // ... existing text ...
Reference comments often won't exactly match one of the above examples, but they will always be referencing a block of code from the *original file* that is left out of the *proposed updates* for the sake of focusing on the specific change that is being made.
Reference comments do NOT need to be valid comments for the given file type. For file types like JSON or plain text that do not use comments, reference comments in the form of '// ... existing properties ...' or '// ... existing values ...' or '// ... existing text ...' can still be present. These MUST be treated as valid reference comments regardless of the file type or the validity of the syntax.
`
const CommentClassifierPrompt = `
You must analyze the *original file* and the *proposed updates* and output a element that lists *EVERY* comment in the *proposed updates*, including the line number of each comment prefixed by 'pdx-new-'. Below each comment, evaluate whether it is a reference comment.
` + ExampleReferences + `
For each comment in the proposed changes, focus on whether the comment is clearly referencing a block of code in the *original file*, whether it is explaining a change being made, or whether it is a comment that was carried over from the *original file* but does *not* reference any code that was left out of the *proposed updates*. After this evaluation, state whether each comment is a reference comment or not. Only list valid *comments* for the given programming language in the comments section. Do not include non-comment lines of code in the comments section.
Example:
pdx-new-1: // ... existing code to start transaction ...
Evaluation: refers the code at the beginning of the 'update' function that starts the database transaction.
Reference: true
pdx-new-5: // verify user permission before performing update
Evaluation: describes the change being made. Does not refer to any code in the *original file*.
Reference: false
pdx-new-10: // ... existing update code ...
Evaluation: refers the code inside the 'update' function that updates the user.
Reference: true
If there are no comments in the *proposed updates*, output an empty element.
ONLY include valid comments for the language in this list. Do NOT include any other lines of code in the comments section. You MUST include ALL comments from the *proposed updates*.
`
================================================
FILE: app/server/model/prompts/build_validation_replacements.go
================================================
package prompts
import (
"fmt"
"strings"
"plandex-server/syntax"
shared "plandex-shared"
)
type ValidationPromptParams struct {
Path string
OriginalWithLineNums shared.LineNumberedTextType
Desc string
ProposedWithLineNums shared.LineNumberedTextType
Diff string
Reasons []syntax.NeedsVerifyReason
SyntaxErrors []string
}
// GetValidationReplacementsXmlPrompt constructs the complete prompt string for XML responses.
func GetValidationReplacementsXmlPrompt(params ValidationPromptParams) (string, int) {
reasons := params.Reasons
syntaxErrs := params.SyntaxErrors
path := params.Path
originalWithLineNums := params.OriginalWithLineNums
desc := params.Desc
proposedWithLineNums := params.ProposedWithLineNums
diff := params.Diff
s := getBuildPromptHead(path, originalWithLineNums, desc, proposedWithLineNums)
headNumTokens := shared.GetNumTokensEstimate(s)
s += fmt.Sprintf(
`
Diff of applied changes:
>>>
%s
<<<
`,
diff,
)
var parts []string
reasonMap := map[syntax.NeedsVerifyReason]string{
syntax.NeedsVerifyReasonAmbiguousLocation: "Changes were applied to an ambiguous location. This may indicate incorrect anchor spacing/indentation, wrong anchor ordering, or missing context.",
syntax.NeedsVerifyReasonCodeRemoved: "Code was removed or replaced. Verify if this was intentional according to the plan.",
syntax.NeedsVerifyReasonCodeDuplicated: "Code may have been duplicated. Verify if this was intentional according to the plan.",
}
for _, reason := range reasons {
if msg, ok := reasonMap[reason]; ok {
parts = append(parts, msg)
}
}
if len(syntaxErrs) > 0 {
parts = append(parts, fmt.Sprintf(
"The applied changes resulted in syntax errors:\n%s\n\nInclude an assessment of what caused these errors.",
strings.Join(syntaxErrs, "\n"),
))
}
s += strings.Join(parts, "\n\n")
s += `
## Validation
Your first task is to examine whether the changes were applied as described in the proposed changes explanation. Do NOT evaluate:
- Code quality
- Missing imports
- Unused variables
- Best practices
- Potential bugs
- Syntax (unless syntax errors have been previously specified and you are determining the cause of the syntax errors)
Your evaluation should ONLY assess:
a. Whether the changes were applied at the correct location, *exactly* as specified in the proposed changes explanation, and at the correct level of nesting/indentation
b. Whether the changes included *all* the specified additions/modifications
c. Whether *any* unintended changes were made to surrounding code
d. Whether *any* specified code was accidentally removed or duplicated
e. Any syntax errors that have been previously specified
--
Line numbers prefixed with 'pdx-' are included in the original file. Line numbers prefixed with 'pdx-new-' are included in the proposed changes. The diff WILL NOT include these line numbers and you must not include them in your evaluation. You must ignore them completely.
--
First, briefly reason through and assess whether the changes were applied *correctly*.
You MUST include reasoning–do not skip this step.
If the changes were applied *correctly*, you MUST output a tag, followed by a tag, then end your response, like this:
--
If the changes were applied *incorrectly*, first assess what went wrong in your reasoning, and briefly strategize on how these issues can be avoided when you generate replacements. You MUST include reasoning–do not skip this step.
Next, you MUST output a tag, and then proceed to output the tag and the tag with at least one element (see below for details). Example:
...
......
--
## Comments
Next, if the changes were applied *incorrectly*:
` + CommentClassifierPrompt + `
--
## Replacements
Next, if the changes were applied *incorrectly*, you must analyze the *original file* and the *proposed updates* and output a element that applies the changes described in the *proposed updates* to the *original file* in order to produce a final, valid resulting file with all changes correctly applied.
CRITICALLY IMPORTANT: When applying changes with replacements, NO REFERENCE COMMENTS CAN BE PRESENT IN THE RESULTING FILE. All reference comments (as listed in the element above) ABSOLUTELY MUST be replaced with the code they refer to in the *original file*.
Now output a element that contains all the replacements needed to correctly apply the changes described in the *proposed updates* to the *original file*. The element MUST contain at least one element.
For each replacement, use a element with the following structure:
......
The element must contain the *exact* original code that will be replaced. *Every* character in the element must be present in the original file. You MUST include line numbers prefixed with 'pdx-' in the element (NOT with 'pdx-new-'). Every line in the element must exactly match a line in the original file, including spacing, indentation, newlines, and the 'pdx-' line number. MUST NOT contain any partial lines, only complete lines.
The element must contain ALL the new code that will replace the code in . It must contain complete lines only (no partial lines). It must be syntactically correct and valid for the given programming language. It MUST NOT contain any line numbers. It MUST NOT contain any reference comments listed in the element. ALL reference comments ABSOLUTELY MUST be replaced with the actual code they refer to in the *original file*.
Apply changes intelligently *in order* to avoid syntax errors, breaking code, or removing code from the original file that should not be removed. Consider the reason behind the update and make sure the result is consistent with the intention of the plan.
Pay *EXTREMELY close attention* to opening and closing brackets, parentheses, and braces. Never leave them unbalanced when the changes are applied. Also pay *EXTREMELY close attention* to newlines and indentation. Make sure that the indentation of the new code is consistent with the indentation of the original code, and syntactically correct.
Replacements must be ordered according to their position in the file. Each block must come after the previous block in the file. Replacements MUST NOT overlap. If a replacement is dependent on another replacement or intersects with it, group those replacements together into a single block.
You ABSOLUTELY MUST NOT overwrite or delete code from the original file unless the plan *clearly intends* for the code to be overwritten or removed. Do NOT replace a full section of code with only new code unless that is the clear intention of the plan. Instead, merge the original code and the proposed updates together intelligently according to the intention of the plan.
--
Example responses:
1. Changes Applied Correctly:
## Evaluate Diff
The new function 'someFunction' was correctly added to the end of the file, with proper indentation and spacing.
2. Changes Applied Incorrectly:
## Evaluate Diff
The new function 'someFunction' was incorrectly added to the end of the file - it was inserted with wrong indentation.
pdx-new-42: // Update the user
Evaluation: Describes the change being made. Not a reference.
Reference: false
pdx-new-44: // ... existing code ...
Evaluation: Refers to code that initializes the database connection in the original file.
Reference: true
pdx-42: func someFunction() {
pdx-43: connectToDatabase()
pdx-44: }
func someFunction() {
err := connectToDatabase()
if err != nil {
log.Printf("error: %v", err)
return
}
processData()
}
IMPORTANT RULES:
1. If your evaluation finds ANY issues, you MUST use followed by a element and a element with at least one element.
2. If your evaluation finds NO issues, you MUST use then a element. Do NOT output comments or replacements if the changes were applied correctly.
3. In replacements, every line in the element MUST exactly match a line in the original file and MUST begin with the line number with a 'pdx-' prefix (NOT with a 'pdx-new-' prefix).
4. In replacements, lines in the element MUST NOT begin with a line number or prefix.
5. Always include reasoning in a '## Evaluate Diff' section prior to outputting the or tags.
--
DO NOT FORGET TO INCLUDE THE ***'pdx-' PREFIXED*** LINE NUMBERS IN THE ELEMENT.
`
return s, headNumTokens
}
// getBuildPromptHead describes the original file and proposed changes
func getBuildPromptHead(filePath string, preBuildStateWithLineNums shared.LineNumberedTextType, desc string, proposedWithLineNums shared.LineNumberedTextType) string {
return fmt.Sprintf(
`Path: %s
Original file (with line nums prefixed with 'pdx-'):
>>>
%s
<<<
Proposed changes explanation:
>>>
%s
<<<
Proposed changes (with line nums prefixed with 'pdx-new-'):
>>>
%s
<<<
`,
filePath,
preBuildStateWithLineNums,
desc,
proposedWithLineNums,
)
}
================================================
FILE: app/server/model/prompts/build_whole_file.go
================================================
package prompts
import shared "plandex-shared"
func GetWholeFilePrompt(filePath string, preBuildStateWithLineNums shared.LineNumberedTextType, changesWithLineNumsType shared.LineNumberedTextType, changesDesc string, comments string) (string, int) {
s := getBuildPromptHead(filePath, preBuildStateWithLineNums, changesDesc, changesWithLineNumsType)
headNumTokens := shared.GetNumTokensEstimate(s)
s += "## Comments\n\n"
if comments != "" {
s += comments + "\n\n"
} else {
s += CommentClassifierPrompt + "\n\n"
}
s += WholeFilePrompt
return s, headNumTokens
}
const WholeFilePrompt = `
## Whole File
Output the *entire merged file* with the *proposed updates* correctly applied. ALL reference comments will be replaced by the appropriate code from the *original file*. You will correctly merge the code from the *original file* with the *proposed updates* and output the entire file.
ALL identified reference comments MUST be replaced by the appropriate code from the *original file*. You MUST correctly merge the code from the *original file* with the *proposed updates* and output the *entire* resulting file. The resulting file MUST NOT include any reference comments.
The resulting file MUST be syntactically and semantically correct. All code structures must be properly balanced.
The full resulting file should be output within a element, like this:
package main
import "logger"
function main() {
logger.info("Hello, world!");
exec()
}
Do NOT include line numbers in the element. Do NOT include reference comments in the element. Output the ENTIRE file, no matter how long it is, with NO EXCEPTIONS. Include the resulting file *only* with no other text. Do NOT wrap the file output in triple backticks or any other formatting, except for the element tags.
Do NOT include any additional text after the element. The output must end after . DO NOT use the string anywhere else in the output. ONLY use it to start the element.
Do NOT UNDER ANY CIRCUMSTANCES *remove or change* any code that is not part of the changes in the *proposed updates*. ALL OTHER code from the *original file* must be reproduced *exactly* as it is in the *original file*. Do NOT remove comments, logging statements, commented out code, or anything else that is not part of the changes in the *proposed updates*. Your job is *only* to *apply* the changes in the *proposed updates* to the *original file*, not to make additional changes of *any kind*.
The ABSOLUTE MOST IMPORTANT THING is to leave all existing code that is not DIRECTLY part of the changes in the *proposed updates* *exactly* as it is in the *original file*. Do NOT remove any code that is not part of the changes in the *proposed updates*. Do NOT include any reference comments in the output; replace them with the appropriate code from the *original file*. Be ABSOLUTELY CERTAIN you have not left anything out which belongs in the final result.
`
================================================
FILE: app/server/model/prompts/chat.go
================================================
package prompts
func GetChatSysPrompt(params CreatePromptParams) string {
base := `
[YOUR INSTRUCTIONS:]
You are a knowledgeable technical assistant helping users with Plandex, a tool for planning and implementing changes to codebases. Plandex allows developers to discuss changes, make plans, and implement updates to their code with AI assistance.`
modeSpecific := ``
if params.ExecMode {
modeSpecific += `
You have execution mode enabled, which means you can discuss both file changes and tasks that require running commands. When discussing potential solutions:
- You can suggest both file changes and command execution steps
- Be clear about which parts require execution vs. file changes
- Consider build processes, testing, and deployment when relevant
- Be specific about what commands would need to be run`
} else {
modeSpecific += `
Note that execution mode is not enabled, so while discussing potential solutions:
- Focus on changes that can be made through file updates
- If a solution would require running commands, mention that execution mode would be needed
- You can still discuss build processes, testing, and deployment conceptually
- Be clear when certain steps would require execution mode to be enabled`
}
contextHandling := ``
if params.AutoContext {
contextHandling = `
Since context was just loaded (if needed) in the previous response:
- Continue the conversation naturally using the context you now have access to`
} else {
contextHandling = `
Context handling:
- You'll work with the context explicitly provided by the user
- If you need additional context, ask the user to provide it
- Be specific about which files would be helpful to see
- You can still reference any files already in context`
}
return base + modeSpecific + `
You are currently in chat mode, which means you're having a natural technical conversation with the user. Many users start in chat mode to:
- Explore and understand their codebase
- Discuss potential changes before implementing them
- Get explanations about code behavior
- Debug issues and discuss solutions
- Think through approaches before making a plan
- Evaluate different implementation strategies
- Understand best practices and potential pitfalls
At any point, the user can transition to 'tell mode' to start making actual changes to files. Users often chat first to:
- Clarify their goals before starting implementation
- Get your input on different approaches
- Better understand their codebase with your help
- Work through technical decisions
- Learn about relevant patterns and practices
Best practices for technical discussion:
- Focus on what the user has specifically asked about - don't suggest extra features or changes unless asked
- Consider existing codebase structure and organization when discussing potential changes
- When discussing libraries, focus on well-maintained, widely-used options with permissive licenses
- Think about code organization - smaller, logically separated files are often better than large monolithic ones
- Consider error handling, logging, and security best practices in your suggestions
- Be thoughtful about where new code should be placed to maintain consistent codebase structure
- Keep in mind that any suggested changes should work with the latest state of the codebase
During chat mode:
You can:
- Engage in natural technical discussion about the code and context
- Provide explanations and answer questions
- Include code snippets when they help explain concepts
- Reference and discuss files from the context
- Help debug issues by examining code and suggesting fixes
- Suggest approaches and discuss trade-offs
- Discuss potential plans informally
- Help evaluate different implementation strategies
- Discuss best practices and potential pitfalls
- Consider and explain implications of different approaches
You cannot:
- Create or modify any files
- Output formal implementation code blocks
- Make formal plans using conventions like "### Tasks"
- Structure responses as if implementing changes` +
contextHandling + `
When implementation is needed:
- If the user wants to move forward with changes, remind them they can use 'tell mode' to start planning and implementing changes. If you use the exact phrase 'switch to tell mode', the user will be automatically given the option to switch, so use that exact phrase if it makes sense to give the user the option to switch based on their prompt and your response.
- In tell mode, you'll help them plan and make actual changes to their codebase
- The transition can happen at any point - users often chat first, then move to implementation when ready
- When discussing potential implementations, consider what files would need to be created or updated
Your responses should feel like a natural technical conversation while still being precise and helpful. Remember that many users are using chat mode as a precursor to making actual changes, so be thorough in your technical discussion while keeping things conversational.
Users can switch between chat mode and tell mode at any point in a plan. A user might switch to chat mode in the middle of a plan's implementation in order to discuss the in-progress plan before proceeding. Even if you are in the middle of a plan, you MUST follow all the instructions above for chat mode and not attempt to write code or implement any tasks. You may receive a list of tasks that are in progress, including a 'current subtask'. You MUST NOT implement any tasks—only discuss them.
`
}
================================================
FILE: app/server/model/prompts/code_block_langs.go
================================================
package prompts
const ValidLangIdentifiers = `
abap
abl
abnf
actionscript3
ada
agda
ahk
al
alloy
antlr
apache
apl
applescript
aql
arduino
armasm
awk
ballerina
bash
basic
bibtex
bicep
blitzbasic
bnf
brainfuck
c
cpp
csharp
caddy
capnp
cassandra
ceylon
chapel
clojure
cmake
cobol
coffeescript
common-lisp
console
coq
crystal
css
cucumber
cue
cython
d
dart
dax
diff
django
dockerfile
dtd
dylan
ebnf
elixir
elm
erlang
factor
fennel
fish
forth
fortran
fsharp
gawk
gdscript
gherkin
gleam
glsl
gnuplot
go
graphql
groff
groovy
handlebars
hare
haskell
haxe
hcl
hlsl
html
http
idris
ini
io
java
javascript
jinja
json
jsx
julia
kotlin
latex
lisp
llvm
lua
make
markdown
mathematica
matlab
meson
mlir
modula2
mysql
nasm
nginx
nim
nix
objc
ocaml
octave
odin
openscad
org
perl
php
plpgsql
postscript
powershell
prolog
promql
protobuf
prql
python
qml
r
racket
raku
reason
rego
restructuredtext
rexx
ruby
rust
sas
sass
scala
scheme
scss
shell
smalltalk
solidity
sparql
sql
swift
systemverilog
tcl
terraform
tex
toml
tsx
turtle
typescript
vala
vbnet
verilog
vhdl
vim
vue
wgsl
xml
yaml
zig
zsh
`
================================================
FILE: app/server/model/prompts/describe.go
================================================
package prompts
import (
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
const SysDescribeXml = `You are an AI parser. You turn an AI's plan for a programming task into a structured description. You MUST output a valid XML response that includes a tag. The tag should contain a good, succinct commit message for the changes proposed. Do not use XML attributes - put all data as tag content.
Example response:
Add user authentication system with JWT support`
const SysDescribe = "You are an AI parser. You turn an AI's plan for a programming task into a structured description. You MUST call the 'describePlan' function with a valid JSON object that includes the 'commitMsg' key. 'commitMsg' should be a good, succinct commit message for the changes proposed. You must ALWAYS call the 'describePlan' function. Never call any other function."
var DescribePlanFn = openai.FunctionDefinition{
Name: "describePlan",
Parameters: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"commitMsg": {
Type: jsonschema.String,
},
},
Required: []string{"commitMsg"},
},
}
const SysPendingResults = "You are an AI commit message summarizer. You take a list of descriptions of pending changes and turn them into a succinct one-line summary of all the pending changes that makes for a good commit message title. Output ONLY this one-line title and nothing else."
================================================
FILE: app/server/model/prompts/exec_status.go
================================================
package prompts
import (
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
shared "plandex-shared"
)
const SysExecStatusFinishedSubtaskXml = `You are tasked with evaluating a response generated by another AI (AI 1) that has been given a coding task to implement.
Your goal is to determine whether the current task was fully implemented in the supplied message(s) from AI 1.
To do this, you need to analyze the latest message from AI 1, and possibly previous messages, and then carefully decide based on the following criteria:
First, examining any previous messages along with the current message, assess whether the current task was fully implemented when these messages are taken together. A task is only considered fully implemented if all necessary code changes for that task have been completed with no remaining todo placeholders or partial implementations.
You MUST output a valid XML response that includes a tag. The tag must contain two child tags:
- : A brief explanation of whether the task was completed and why
- : Either "true" or "false" indicating if the task is done
Do not use XML attributes - put all data as tag content.
Example response:
Task is complete - all required code changes implemented with no placeholderstrue`
const SysExecStatusFinishedSubtask = `You are tasked with evaluating a response generated by another AI (AI 1) that has been given a coding task to implement.
Your goal is to determine whether the current task was fully implemented in the supplied message(s) from AI 1.
To do this, you need to analyze the latest message from AI 1, and possibly previous messages, and then carefully and decide based on the following criteria:
First, examining any previous messages along with the current message, assess whether the current task was fully implemented when these messages are taken together. A task is only considered fully implemented if all necessary code changes for that task have been completed with no remaining todo placeholders or partial implementations.
You *must* call the didFinishSubtask function with a JSON object containing the keys 'reasoning' and 'subtaskFinished'.
Set 'reasoning' to a string briefly and succinctly explaining whether the current task was or was not fully implemented, and why.
If AI 1 has stated that the task has been completed, consider that in your reasoning and response, but also assess the actual implementation and whether it really did complete the task. Do NOT validate the code or assess the quality of the implementation, only whether each item in the task has been implemented (even that implementation is not perfect). Only respond that a task is not finished if a significant step is missing—otherwise, respond that it is finished.
The 'subtaskFinished' key is a boolean that indicates whether the current task has been fully implemented in the latest message from AI 1. If the current task has been fully implemented, 'subtaskFinished' must be true. If the current task has not been fully implemented or there are unexplained todo placeholders, 'subtaskFinished' must be false. If the task has been skipped because it is not necessary or was already implemented in an earlier step, 'subtaskFinished' must be true.
You must always call 'didFinishSubtask'. Don't call any other function.`
type GetExecStatusFinishedSubtaskParams struct {
UserPrompt string
CurrentSubtask string
CurrentMessage string
PreviousMessages []string
PreferredOutputFormat shared.ModelOutputFormat
}
func GetExecStatusFinishedSubtask(params GetExecStatusFinishedSubtaskParams) string {
userPrompt := params.UserPrompt
currentSubtask := params.CurrentSubtask
currentMessage := params.CurrentMessage
previousMessages := params.PreviousMessages
preferredOutputFormat := params.PreferredOutputFormat
var s string
if preferredOutputFormat == shared.ModelOutputFormatXml {
s = SysExecStatusFinishedSubtaskXml
} else {
s = SysExecStatusFinishedSubtask
}
if userPrompt != "" {
s += "\n\n**Here is the user's prompt:**\n" + userPrompt
}
s += "\n\n**Here is the current task:**\n" + currentSubtask
for _, msg := range previousMessages {
s += "\n\n**Here is a previous message from AI 1 that was working on the same task:**\n" + msg
}
s += "\n\n**Here is the latest message from AI 1:**\n" + currentMessage
return s
}
var DidFinishSubtaskFn = openai.FunctionDefinition{
Name: "didFinishSubtask",
Parameters: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"reasoning": {
Type: jsonschema.String,
},
"subtaskFinished": {
Type: jsonschema.Boolean,
},
},
Required: []string{"reasoning", "subtaskFinished"},
},
}
================================================
FILE: app/server/model/prompts/explanation_format.go
================================================
package prompts
const ChangeExplanationPrompt = `
### Action Explanation Format
#### 1. Updating an existing file in context
Prior to any code block that is *updating* an existing file in context, you MUST explain the change in the following format EXACTLY:
---
**Updating ` + "`[file path]`" + `**
Type: [type]
Summary: [brief description, symbols/sections being changed]
Replace: [lines to replace/remove]
Context: [describe surrounding code that helps locate the change unambiguously]
Preserve: [symbols/structures/sections to preserve when overwriting entire file]
---
OR if multiple changes are being made to the same file in a single subtask and a single code block, list each change independently like this:
---
**Updating ` + "`[file path]`" + `**
Change 1.
Type: [type]
Summary: [brief description, symbols/sections being changed]
Replace: [lines to replace/remove]
Context: [describe surrounding code that helps locate the change unambiguously]
Change 2.
Type: [type]
Summary: [brief description, symbols/sections being changed]
Replace: [lines to replace/remove]
Context: [describe surrounding code that helps locate the change unambiguously]
... and so on for each change
---
Include a line break after the initial '**Updating ` + "`[file path]`" + `**' line as well as each of the following fields. Use the exact same spacing and formatting as shown in the above format and in the examples further down.
The Type field MUST be exactly one of these values: 'add', 'prepend', 'append', 'replace', 'remove', or 'overwrite'.
- add
- For inserting new code within the file *only*
- Only use if NO existing code is being changed or removed - otherwise use 'replace' or 'overwrite'
- If inserting code at the start of the file, use 'prepend' instead
- If inserting code at the end of the file, use 'append' instead
- prepend
- For inserting new code at the start of the file *only*
- Only use if NO existing code is being changed or removed - otherwise use 'replace' or 'overwrite'
- append
- For inserting new code at the end of the file *only*
- Only use if NO existing code is being changed or removed - otherwise use 'replace' or 'overwrite'
- replace
- For replacing existing code within the file *only*
- Only use if existing code is being replaced by new code. If new code is being added but none is being replaced, use 'add', 'append', or 'prepend' instead
- If the entire file is being replaced, use 'overwrite' instead
- If existing code is being removed and nothing new is being added, use 'remove' instead
- remove
- For removing existing code within the file *only*
- Only use if existing code is being removed. If new code is being added but none is being removed, use 'add', 'append', or 'prepend' instead
- If code is being removed and replaced with new code, use 'replace' instead
- overwrite
- For replacing the entire file *only*
- Only use if the *entire file* is being replaced. If new code is being added but none is being replaced or removed, use 'add', 'append', or 'prepend' instead.
For each Type, follow these validation rules:
- For 'add':
- Summary MUST briefly describe the new code being added and where it will be inserted
- Context MUST describe the surrounding code structures that help locate where the new code will be inserted. The context MUST be *OUTSIDE* of the lines that are being added so that it 'anchors' the exact location of the change in the original file.
- Preserve field must be omitted
- Replace field must be omitted
- In the code block, include the anchors identified in the 'Context' field, collapsed with a reference comment if they span more than a few lines, that are immediately before and after the new code being added. Do NOT include large sections of code from the original file that are not being modified when using 'add'; include enough surrounding code to unambiguously locate the change in the original file, and no more.
- In the code block, DO NOT UNDER ANY CIRCUMSTANCES reproduce the entire original file with the new code added—that's not what 'add' is for. If you're reproducing the entire original file, use 'overwrite' instead.
- For 'prepend':
- Summary MUST briefly describe the new code being prepended to the start of the file
- Context MUST identify the first *existing* code structure in the original file (which will NOT be modified) that the new code will be added before
- Preserve field must be omitted
- Replace field must be omitted
- Code block MUST include JUST the first existing code structure in the original file (which will NOT be modified), collapsed with a reference comment if it spans more than a few lines, immediately followed by the new code being prepended. Do NOT include large sections of code from the original file that are not being modified when using 'prepend'.
- In the code block, DO NOT UNDER ANY CIRCUMSTANCES reproduce the entire original file with the new code prepended—that's not what 'prepend' is for. If you're reproducing the entire original file, use 'overwrite' instead.
- For 'append':
- Summary MUST briefly describe the new code being appended to the end of the file
- Context MUST identify the last *existing* code structure in the original file (which will NOT be modified) that the new code will be added after
- Preserve field must be omitted
- Replace field must be omitted
- Code block MUST include JUST the last existing code structure in the original file (which will NOT be modified), collapsed with a reference comment if it spans more than a few lines, immediately followed by the new code being appended. Do NOT include large sections of code from the original file that are not being modified when using 'append'.
- In the code block, DO NOT UNDER ANY CIRCUMSTANCES reproduce the entire original file with the new code appended—that's not what 'append' is for. If you're reproducing the entire original file, use 'overwrite' instead.
- For 'replace':
- Summary MUST briefly describe the change
- Replace field MUST list lines in the original file that are being replaced. Use the exact format: 'lines [startLineNumber]-[endLineNumber]' — e.g. 'lines 10-20' or for a single line, 'line [lineNumber]' — e.g. 'line 10', or if multiple sections are being replaced, use 'lines [startLineNumber]-[endLineNumber], [startLineNumber]-[endLineNumber], ...' — e.g. 'lines 10-20, 30-40' (can also include single lines if desired, or a mix of single and multiple lines, e.g. 'line 10, lines 30-40') — DO NOT use any other format, or describe the lines in any other way.
- Context MUST describe the surrounding code structures that help locate what is being replaced. Context MUST be *OUTSIDE* of the lines that are being replaced so that it 'anchors' the exact location of the change in the original file.
- Preserve field must be omitted
- In the code block, include the anchors identified in the 'Context' field, collapsed with a reference comment if they span more than a few lines, that are immediately before and after the lines being replaced. Do NOT include large sections of code from the original file that are not being modified when using 'replace'; include enough surrounding code to unambiguously locate the change in the original file, and no more.
- Do NOT UNDER ANY CIRCUMSTANCES reproduce the entire original file with the new code added—that's not what 'replace' is for. If you're reproducing the entire original file, use 'overwrite' instead.
- For 'remove':
- Summary MUST briefly describe the change
- Replace field MUST list lines in the original file that are being removed. Use the exact format: 'lines [startLineNumber]-[endLineNumber]' — e.g. 'lines 10-20' or for a single line, 'line [lineNumber]' — e.g. 'line 10', or if multiple sections are being removed, use 'lines [startLineNumber]-[endLineNumber], [startLineNumber]-[endLineNumber], ...' — e.g. 'lines 10-20, 30-40' (can also include single lines if desired, or a mix of single and multiple lines, e.g. 'line 10, lines 30-40') — DO NOT use any other format, or describe the lines in any other way.
- Context MUST describe the surrounding code structures that help locate what is being removed. Context MUST be *OUTSIDE* of the lines that are being removed so that it 'anchors' the exact location of the change in the original file.
- Preserve field must be omitted
- In the code block, include the anchors identified in the 'Context' field, collapsed with a reference comment if they span more than a few lines, that are immediately before and after the lines being removed. Do NOT include large sections of code from the original file that are not being modified when using 'remove'; include enough surrounding code to unambiguously locate the change in the original file, and no more.
- Do NOT UNDER ANY CIRCUMSTANCES reproduce the entire original file with the removed code omitted—that's not what 'remove' is for. If you're reproducing the entire original file, use 'overwrite' instead.
- For 'overwrite':
- Summary MUST briefly describe the change and list the specific symbols/sections being changed or replaced
- Context field must be omitted
- Preserve MUST *exhaustively* list all symbols/sections in the original file that should be included in the final result. Do *NOT* say that you are 'preserving nothing' because you are overwriting the entire file—the point what, if anything, will be *kept the same* from the original file, even though you are overwriting the whole file. Only say that you're preserving nothing if *nothing* will be kept the same from the original file and the new file will be completely new. The point of this field is to ensure that the final result is a *complete* and *correct* replacement of the original file, and that no important code is omitted.
- Changes with 'overwrite' MUST NOT be combined with other changes in the same code block. An 'overwrite' change MUST be the ONLY change for the code block.
In the Context, Summary, Remove, and Preserve fields, when listing code symbols, list them in a comma-separated list and surround them with backticks. For example, ` + "`foo`,`someFunc`, `someVar`" + `
IMPORTANT: when listing code symbols or structures in the Context, Summary, and Preserve fields, you MUST include the name of the symbol or structure only, *not* the full signature (e.g. don't include the function parameters or return type for a function—just the function name; don't include the type or the 'var/let/const' keywords for a variable—just the variable name, and so on). DO NOT UNDER ANY CIRCUMSTANCES include full function signatures when listing functions. Include *only* the function name.
For example, instead of ` + "`func (state *activeTellStreamState) genPlanDescription() (*db.ConvoMessageDescription, error)`" + `, you should use ` + "`genPlanDescription`" + `. Instead of ` + "`var foo int`" + `, you should use ` + "`foo`" + `.
CRITICAL: The Context field MUST include symbols/structures that are NOT being modified in any way. They must be completely outside of and untouched by the change. They serve as anchors to locate where the change should occur in the file. The purpose is to clearly demonstrate which context immediately *surrounds* the change so that it can be included in the code block that updates the file.
INCORRECT - symbols in Context are part of the change:
Summary: Replace implementations of ` + "`foo`, `bar`, and `baz`" + `
Replace: lines 105-200
Context: Located between ` + "`foo`" + ` and ` + "`baz`" + ` # Wrong - these are being changed!
CORRECT - symbols in Context are outside the change:
Summary: Replace implementations of ` + "`foo`, `bar`, and `baz`" + `
Replace: lines 105-200
Context: Located between ` + "`setup`" + ` and ` + "`cleanup`" + ` functions # Correct - these aren't being changed
Again, the point of the Context field is to identify *anchors* that exist completely *outside* of the bounds of the change in the original file. The Context field is NOT used to identify code that is being *modified* or *replaced* as part of the change, but rather the code immediately *surrounding* the change.
The symbols/structure you mention in the Context field MUST ALSO be *immediately adjacent* to the change in the original file. Do NOT use symbols or structures that are further away from the change and have other code between them and the change.
ALWAYS surround the symbols/structures you mention in the Context field with backticks. Do NOT leave them out.
Furthermore, every symbol/structure you mention in the Context field ABSOLUTELY MUST be included in the code block that updates the file. Do NOT UNDER ANY CIRCUMSTANCES omit any of these symbols/structures from the code block. Use reference comments to avoid repeating code that is not changing.
Keep the explanation as succinct as possible while still following all of the above rules.
You ABSOLUTELY MUST use this template EXACTLY as described above. DO NOT CHANGE THE FORMATTING OR WORDING IN ANY WAY! DO NOT OMIT ANY FIELDS FROM THE EXPLANATION AS DESCRIBED ABOVE.
Example explanations:
**Updating ` + "`server/api/client.go`" + `**
Type: add
Summary: Add new ` + "`doRequest`" + ` method to ` + "`Client`" + ` struct after the constructor method
Context: Located between ` + "`NewClient`" + ` constructor and ` + "`getUser`" + ` method
**Updating ` + "`server/types/api.go`" + `**
Type: replace
Summary: Replace implementation of ` + "`extractName`" + ` function with new version using ` + "`xml.Decoder`" + `
Replace: lines 8-15
Context: Located between ` + "`validateName`" + ` and ` + "`formatName`" + ` functions
**Updating ` + "`cli/cmd/update.go`" + `**
Type: overwrite
Summary: Replace implementations of ` + "`updateCmd`" + `, ` + "`runUpdate`" + `, and ` + "`validateUpdate`" + ` functions with new versions
Preserve: ` + "`updateFlags`" + ` struct and ` + "`defaultTimeout`" + ` constant
**Updating ` + "`server/config/init.go`" + `**
Type: prepend
Summary: Add new ` + "`validateConfig`" + ` function at start of file
Context: Will be placed before the ` + "`init`" + ` function
**Updating ` + "`server/models/user.go`" + `**
Type: append
Summary: Add new ` + "`cleanupUserData`" + ` function at end of file
Context: Will be placed after the ` + "`validateUser`" + ` function
**Updating ` + "`server/handlers/auth.go`" + `**
Type: remove
Summary: Remove unused ` + "`validateLegacyTokens`" + ` function and its helper ` + "`checkTokenFormat`" + `
Replace: lines 25-85
Context: Located between ` + "`parseAuthHeader`" + ` and ` + "`validateJWT`" + ` functions
*
If multiple changes are being made to the same file in a single subtask, you MUST ALWAYS combine them into a SINGLE code block. Do NOT use multiple code blocks for multiple changes to the same file.
When writing the explanation for multiple changes that will be included in a single code block, list each change independently like this:
**Updating + "server/handlers/auth.go" + **
Change 1.
Type: remove
Summary: Remove unused ` + "`validateLegacyTokens`" + ` function and its helper ` + "`checkTokenFormat`" + `
Replace: lines 25-85
Context: Located between ` + "`parseAuthHeader`" + ` and ` + "`validateJWT`" + ` functions
Change 2.
Type: append
Summary: Append just-removed ` + "`checkTokenFormat`" + ` function to the end of the file
Replace: lines 8-15
Context: The last code structure is ` + "`finalizeAuth`" + ` function
When outputting a compound explanation in the above format, it is CRITICAL that you still only output a SINGLE code block. Do NOT output multiple code blocks.
*
Again, ALL code structures/symbols that are mentioned in the Context field MUST be included as *anchors* in the code block that updates the file. If you are inserting new code between [structure 1] and [structure 2], then you MUST include both [structure 1] and [structure 2] as anchors in the code block that updates the file. Include *anchors* from the Context field so that the change is clearly positioned in the file between sections of code that are *not* being modified.
At the same time, you MUST NOT reproduce large sections of code from the original file that are not changing. You MUST use reference comments "// ... existing code ..." to avoid reproducing large sections of code from the original file that are not changing.
If you are using functions that are not being modified as anchors, then include the function signatures and closing braces, but use a reference comment for the function bodies. Here is an example:
If you are using functions that are not being modified as anchors, then include the function signatures and closing braces, but use a reference comment for the function bodies. Here is an example:
If your change description is:
**Updating ` + "`server/api/users.go`" + `**
Type: replace
Summary: Replace implementation of ` + "`validateUser`" + ` function to add role and permission validation
Replace: lines 10-20
Context: Located between ` + "`parseUser`" + ` and ` + "`updateUser`" + ` functions
Then your code block MUST look like:
---
// ... existing code ...
func (api *API) parseUser(input []byte) (*User, error) {
// ... existing code ...
}
func (api *API) validateUser(user *User) error {
// Validate basic fields
if user.ID == "" {
return errors.New("user ID is required")
}
if user.Email == "" {
return errors.New("email is required")
}
// New validation for roles
if len(user.Roles) == 0 {
return errors.New("user must have at least one role")
}
for _, role := range user.Roles {
if !isValidRole(role) {
return fmt.Errorf("invalid role: %s", role)
}
}
// New validation for permissions
for _, permission := range user.Permissions {
if !isValidPermission(permission) {
return fmt.Errorf("invalid permission: %s", permission)
}
}
return nil
}
func (api *API) updateUser(user *User) error {
// ... existing code ...
}
// ... existing code ...
---
Notice how:
- The anchor functions 'parseUser' and 'updateUser' are included with their full signatures
- Their bodies are replaced with '// ... existing code ...' since they aren't being modified
- The new 'validateUser' implementation is included in full since it's the actual change
- The file starts and ends with '// ... existing code ...' comments since this change is in the middle of the file
- There's a comment indicating we're replacing the existing implementation
*
❌ INCORRECT - Context symbols missing from code block:
**Updating ` + "`sound.py`" + `**
Type: add
Summary: Add ` + "`debug_status`" + ` method to ` + "`Engine`" + ` class
Context: Located in the ` + "`Engine`" + ` class, right after the ` + "`__init__`" + ` method and right before the ` + "`cleanup`" + ` method
- sound.py:
# ... existing code ...
def debug_status(self):
"""Print debug information about the sound engine state."""
print("Sound engine debug info")
# ... existing code ...
✅ CORRECT - Context symbols included in code block:
**Updating ` + "`sound.py`" + `**
Type: add
Summary: Add ` + "`debug_status`" + ` method to ` + "`Engine`" + ` class
Context: Located in the ` + "`Engine`" + ` class, after the ` + "`cleanup`" + ` method
- sound.py:
# ... existing code ...
class Engine:
def __init__(self):
# ... existing code ...
def debug_status(self):
"""Print debug information about the sound engine state."""
print("Sound engine debug info")
def cleanup(self):
# ... existing code ...
# ... existing code ...
*
As you can see, in the correct example, every symbol/structure mentioned in the Context field is included in the code block, unambiguously locating the change.
*
If a file is being *updated* and the above explanation does *not* indicate that the file is being *overwritten* or that the change is being prepended to the *start* of the file, then the code block ABSOLUTELY ALWAYS MUST begin with an "... existing code ..." comment to account for all the code before the change. It is EXTREMELY IMPORTANT that you include this comment when it is needed—it must not be omitted.
If a file is being *updated* and the above explanation does *not* indicate that the file is being *overwritten* or that the change is being appended to the *end* of the file, then the code block ABSOLUTELY ALWAYS MUST end with an "... existing code ..." comment to account for all the code after the change. It is EXTREMELY IMPORTANT that you include this comment when it is needed—it must not be omitted.
Again, unless a file is being fully ovewritten, or the change either starts at the *absolute start* of the file or ends at the *absolute end* of the file, IT IS ABSOLUTELY CRITICAL that the file both BEGINS with an "... existing code ..." comment and ENDS with an "... existing code ..." comment.
If a file must begin with an "... existing code ..." comment according to the above rules, then there MUST NOT be any code before the initial "... existing code ..." comment.
If a file must end with an "... existing code ..." comment according to the above rules, then there MUST NOT be any code after the final "... existing code ..." comment.
Again, if the change *does not* end at the *absolute end* of the file, then the LAST LINE of the code block MUST be an "... existing code ..." comment. Ending the code block like this:
---
// ... existing code ...
func (a *Api) NewMethod() {
callExistingMethod()
}
func (a *Api) LoadContext(planId, branch string, req
shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {
// ... existing code ...
}
---
is NOT CORRECT, because the last line is not an "... existing code ..." comment—it is rather the '}' closing bracket of the function. Instead, it must be:
---
// ... existing code ...
func (a *Api) NewMethod() {
callExistingMethod()
}
func (a *Api) LoadContext(planId, branch string, req
shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {
// ... existing code ...
}
// ... existing code ...
---
Now the final line is an "... existing code ..." comment, which is correct.
*
If the explanation states that it will overwrite the entire file, then the code block that updates the file MUST include the ENTIRE file *with no reference or removal comments* and no necessary code omitted. Include *all* code from both the original file and the intended change merged together correctly. Do NOT omit any code from the original file unless the specific intention of the task is to replace or remove that code. Ensure that all symbols/sections mentioned in the 'Preserve' field are included in the code block that updates the file. *MAKE THE CODE BLOCK AS LONG AS NECESSARY TO INCLUDE THE **ENTIRE** FILE.* If the file is too long to fit within a single code block or a single response, *do not* use the 'overwrite' type. Use another type to make a more specific change.
Do NOT overwrite the entire file for very large files that cannot fit within a single response.
*
If the explanation includes a 'Preserve' field, be absolutely certain that the corresponding code block does *not* remove or replace any of the code listed in the 'Preserve' field.
---
Example of an explanation that includes multiple changes to the same file, with a *single* code block:
**Updating + "server/handlers/auth.go" + **
Change 1.
Type: remove
Summary: Remove + "validateLegacyTokens" + and + "checkTokenFormat" + (original file lines 25-35).
Context: Located between + "parseAuthHeader" + and + "validateJWT" + functions
Change 2.
Type: append
Summary: Append a new + "checkTokenFormatV2" + function at the end of the file
Context: The last code structure is + "finalizeAuth" + function
- server/handlers/auth.go:
// ... existing code ...
func parseAuthHeader() {
// ... existing code ...
}
// Plandex: removed code
func validateJWT() {
// ... existing code ...
}
func finalizeAuth() {
// ... existing code ...
}
func checkTokenFormatV2(header string) bool {
// new code for updated token checking
return header != ""
}
// ... existing code ...
*
Remember, when outputting a compound explanation in the above format, it is CRITICAL that you still only output a SINGLE code block.
❌ INCORRECT - Including too much of the file with append
**Updating ` + "`server/models/user.go`" + `**
Type: append
Summary: Add new ` + "`validateUserEmail`" + ` function at the end of file
Context: Will be placed after the ` + "`isAdmin`" + ` function
- server/models/user.go:
package models
import (
"errors"
"strings"
)
type User struct {
ID string
Name string
Email string
Role string
}
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
}
}
func (u *User) isAdmin() bool {
return u.Role == "admin"
}
func (u *User) validateUserEmail() error {
if u.Email == "" {
return errors.New("email cannot be empty")
}
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email format")
}
return nil
}
✅ CORRECT - Proper append example
**Updating ` + "`server/models/user.go`" + `**
Type: append
Summary: Add new ` + "`validateUserEmail`" + ` function at the end of file
Context: Will be placed after the ` + "`isAdmin`" + ` function
- server/models/user.go:
// ... existing code ...
func (u *User) isAdmin() bool {
// ... existing code ...
}
func (u *User) validateUserEmail() error {
if u.Email == "" {
return errors.New("email cannot be empty")
}
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email format")
}
return nil
}
❌ INCORRECT - Reproducing too much of the file with prepend
**Updating ` + "`server/handlers/users.go`" + `**
Type: prepend
Summary: Add imports and package declaration at the beginning of the file
Context: Will be placed before the ` + "`UserHandler`" + ` struct definition
- server/handlers/users.go:
package handlers
import (
"encoding/json"
"net/http"
"github.com/example/app/models"
"github.com/example/app/utils"
)
type UserHandler struct {
UserService *models.UserService
}
func NewUserHandler(service *models.UserService) *UserHandler {
return &UserHandler{
UserService: service,
}
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// ... existing code ...
}
✅ CORRECT - Proper prepend example
**Updating ` + "`server/handlers/users.go`" + `**
Type: prepend
Summary: Add imports and package declaration at the beginning of the file
Context: Will be placed before the ` + "`UserHandler`" + ` struct definition
- server/handlers/users.go:
package handlers
import (
"encoding/json"
"net/http"
"github.com/example/app/models"
"github.com/example/app/utils"
)
type UserHandler struct {
// ... existing code ...
}
❌ INCORRECT - Using overwrite when replace would be better
**Updating ` + "`server/config/defaults.go`" + `**
Type: overwrite
Summary: Update the ` + "`NewDefaultConfig`" + ` function to change default timeout
Preserve: ` + "`ConfigVersion`" + ` constant, ` + "`DefaultConfig`" + ` struct
- server/config/defaults.go:
package config
const ConfigVersion = "1.0.0"
type DefaultConfig struct {
Port int
Host string
LogLevel string
MaxConn int
Timeout int
EnableCache bool
}
func NewDefaultConfig() *DefaultConfig {
return &DefaultConfig{
Port: 8080,
Host: "localhost",
LogLevel: "info",
MaxConn: 100,
Timeout: 60, // Changed from 30 to 60
EnableCache: true,
}
}
✅ CORRECT - Using replace instead of overwrite for a small change
**Updating ` + "`server/config/defaults.go`" + `**
Type: replace
Summary: Update the ` + "`NewDefaultConfig`" + ` function to change default timeout
Replace: lines 15-24
Context: Located between ` + "`DefaultConfig`" + ` struct definition and end of file
- server/config/defaults.go:
// ... existing code ...
type DefaultConfig struct {
Port int
Host string
LogLevel string
MaxConn int
Timeout int
EnableCache bool
}
func NewDefaultConfig() *DefaultConfig {
return &DefaultConfig{
Port: 8080,
Host: "localhost",
LogLevel: "info",
MaxConn: 100,
Timeout: 60, // Changed from 30 to 60
EnableCache: true,
}
}
// ... existing code ...
✅ CORRECT - Proper use of overwrite
**Updating ` + "`server/config/defaults.go`" + `**
Type: overwrite
Summary: Replace entire file with new implementation of ` + "`DefaultConfig`" + ` and add new ` + "`ValidateConfig`" + ` function
Preserve: ` + "`ConfigVersion`" + ` constant
- server/config/defaults.go:
package config
const ConfigVersion = "1.0.0"
type DefaultConfig struct {
Port int
Host string
LogLevel string
MaxConn int
Timeout int
EnableCache bool
}
func NewDefaultConfig() *DefaultConfig {
return &DefaultConfig{
Port: 8080,
Host: "localhost",
LogLevel: "info",
MaxConn: 100,
Timeout: 30,
EnableCache: true,
}
}
func ValidateConfig(cfg *DefaultConfig) error {
if cfg.Port <= 0 {
return errors.New("port must be positive")
}
if cfg.Host == "" {
return errors.New("host cannot be empty")
}
return nil
}
❌ INCORRECT - Vague Context that doesn't specify exact location
**Updating ` + "`server/api/auth.go`" + `**
Type: add
Summary: Add new ` + "`validateToken`" + ` helper function
Context: Located in the auth package
- server/api/auth.go:
package auth
import (
"errors"
"strings"
"time"
)
func validateToken(token string) (bool, error) {
if token == "" {
return false, errors.New("token cannot be empty")
}
parts := strings.Split(token, ".")
if len(parts) != 3 {
return false, errors.New("invalid token format")
}
return true, nil
}
✅ CORRECT - Proper use of Context field with anchors
**Updating ` + "`server/api/auth.go`" + `**
Type: add
Summary: Add new ` + "`validateToken`" + ` helper function after the imports
Context: Located between the import statements and the ` + "`AuthHandler`" + ` struct definition
- server/api/auth.go:
// ... existing code ...
import (
"errors"
"strings"
"time"
)
func validateToken(token string) (bool, error) {
if token == "" {
return false, errors.New("token cannot be empty")
}
parts := strings.Split(token, ".")
if len(parts) != 3 {
return false, errors.New("invalid token format")
}
return true, nil
}
type AuthHandler struct {
// ... existing code ...
}
// ... existing code ...
❌ INCORRECT - Multiple code blocks for changes to the same file
**Updating ` + "`server/handlers/users.go`" + `**
Type: add
Summary: Add new ` + "`validateUserInput`" + ` helper function
Context: Located between the import statements and the ` + "`UserHandler`" + ` struct definition
- server/handlers/users.go:
// ... existing code ...
import (
"encoding/json"
"errors"
"net/http"
"github.com/example/app/models"
)
func validateUserInput(user *models.User) error {
if user.Name == "" {
return errors.New("name cannot be empty")
}
if user.Email == "" {
return errors.New("email cannot be empty")
}
return nil
}
type UserHandler struct {
// ... existing code ...
}
// ... existing code ...
**Updating ` + "`server/handlers/users.go`" + `**
Type: replace
Summary: Update ` + "`CreateUser`" + ` method to use the new validation function
Replace: lines 25-35
Context: Located between the ` + "`UserHandler`" + ` struct definition and the ` + "`GetUser`" + ` method
- server/handlers/users.go:
// ... existing code ...
type UserHandler struct {
// ... existing code ...
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var user models.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validateUserInput(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := h.UserService.Create(&user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// ... existing code ...
}
// ... existing code ...
✅ CORRECT - Multiple changes to the same file with a single code block
**Updating ` + "`server/handlers/users.go`" + `**
Change 1.
Type: add
Summary: Add new ` + "`validateUserInput`" + ` helper function
Context: Located between the import statements and the ` + "`UserHandler`" + ` struct definition
Change 2.
Type: replace
Summary: Update ` + "`CreateUser`" + ` method to use the new validation function
Replace: lines 25-35
Context: Located between the ` + "`UserHandler`" + ` struct definition and the ` + "`GetUser`" + ` method
- server/handlers/users.go:
// ... existing code ...
import (
"encoding/json"
"errors"
"net/http"
"github.com/example/app/models"
)
func validateUserInput(user *models.User) error {
if user.Name == "" {
return errors.New("name cannot be empty")
}
if user.Email == "" {
return errors.New("email cannot be empty")
}
return nil
}
type UserHandler struct {
UserService *models.UserService
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var user models.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validateUserInput(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := h.UserService.Create(&user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
// ... existing code ...
}
// ... existing code ...
---
#### 2. Creating a new file
Prior to any code block that is *creating a new file*, you MUST explain the change in the following format EXACTLY:
---
**Creating ` + "`[file path]`" + `**
Type: new file
Summary: [brief description of the new file]
---
Include a line break after the initial '**Creating ` + "`[file path]`" + `**' line as well as each of the following fields. Use the exact same spacing and formatting as shown in the above format and in the examples further down.
The Type field MUST be exactly 'new file'.
The Summary field MUST briefly describe the new file and its purpose.
Do NOT include the 'Context' or 'Preserve' fields when creating a new file. Just the 'Type' and 'Summary' fields are required.
You ABSOLUTELY MUST use this template EXACTLY as described above.
Example explanation for a *new file*:
**Creating ` + "`server/handlers/auth.go`" + `**
Type: new file
Summary: Add new ` + "`auth`" + ` handler in the ` + "`server/handlers`" + ` directory
- server/handlers/auth.go:
package handlers
func (api *API) authHandler(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
valid := validateAuthHeader(authHeader)
if !valid {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
session, err := api.sessionStore.Get(r, "session")
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
response := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("OK")),
}
json.NewEncoder(w).Encode(response)
}
*
For new files:
- You MUST ALWAYS include the *entire file* in the code block. Do not omit any code from the file.
- Do NOT use placeholder code or comments like '// implement authentication here' to indicate that the file is incomplete. Implement *all* functionality.
- Do NOT use reference comments like '// ... existing code ...'. Those are only used for updating existing files and *never* when creating new files.
- Include the *entire file* in the code block.
`
================================================
FILE: app/server/model/prompts/file_ops.go
================================================
package prompts
const FileOpsPlanningPrompt = `
## File Operations Planning
You can create special subtasks for file operations that move, remove, or reset changes to files that are in context or have pending changes. These operations *can only* be used on files that are in context or have pending changes. They *cannot* be used on other files or directories in the user's project (or any other files/directories). *ONLY* use these sections for files that are in context or have pending changes.
## Important Notes On Planning File Operations
1. These sections can only operate on files that are:
- Already in context, OR
- Have pending changes from earlier in the plan
- All files that are in context or have pending changes will be listed in your prompt
2. You cannot:
- Move, remove, or reset files that aren't in context or pending
- Create new directories (they will be created as needed by the operations)
- Move a file to a path that is *already* in context or pending (and would therefore overwrite the existing file)
3. Updated State:
- Note that when you *move* a file, any further updates to that file must be applied to the *new* location. The context in your prompt will be updated to reflect the new location. Ensure the new path takes precedence over any updates to the old path in the conversation history.
- Note that when you *remove* a file, applying further updates to that file will require *creating a new file*. The file must be considered to not exist unless you explicitly create it again. The context in your prompt will be updated to reflect the file's removal. Ensure the file's removal takes precedence over any updates to the file in the conversation history.
In most cases, these special file operations are *not* used when initially implementing a plan, since in that case you are only creating files and updating them, and possibly writing to the _apply.sh script if execution mode is enabled and you need to take actions on the user's machine when the plan is applied. The only exception is if the users specifically asks you to move or remove files in context in the initial prompt. Otherwise, do not use these operations when initially implementing a plan.
In most cases, file operations are only useful for revising a plan with pending changes in response to another prompt from the user. For example, if you have created several files and the user asks you to create them in a different directory, you can use a move operation to move them to the new directory. Similarly, if a user tells you that a file you have created is not needed, you can use a remove operation to remove it. Similarly, if a user tells you that your changes to a particular file are incorrect or not needed, you can use a reset operation to clear the pending changes to that file.
You MUST NOT implement any file operations in this section. You MUST only plan the file operations by including them in the ### Tasks section as subtasks. They will be implemented in subsequent responses.
`
const FileOpsImplementationPrompt = `
## File Operations Implementation
You can perform file operations using special sections in your response. These sections allow you to move, remove, or reset changes to files that are in context or have pending changes. These special sections *can only* be used on files that are in context or have pending changes. They *cannot* be used on other files or directories in the user's project (or any other files/directories). *ONLY* use these sections for files that are in context or have pending changes.
You ABSOLUTELY MUST end every file operation section with a tag.
*Move Files Section:*
Use the '### Move Files' section to move or rename files:
### Move Files
- ` + "`source/path.tsx` → `dest/path.tsx`" + `
- ` + "`components/button.tsx` → `pages/button.tsx`" + `
Rules for the Move Files section:
- Each line must start with a dash (-)
- Source and destination paths must be wrapped in backticks (` + "`" + `)
- Paths must be separated by → (Unicode arrow, NOT ->)
- Can only move individual files (not directories)
- All source paths MUST match a path in context or that has pending changes
- Destination path must be in the same base directory as files in context
- Destination path MUST NOT already exist in context or pending files—i.e. you cannot move a file to a path that is *already* in context or pending (and would therefore overwrite the existing file)
- You CAN move a file to a directory that does not exist yet—it will be created as needed automatically
- You MUST end the '### Move Files' section with a tag
*Remove Files Section:*
Use the '### Remove Files' section to remove/delete files:
### Remove Files
- ` + "`components/page.tsx`" + `
- ` + "`layouts/header.tsx`" + `
Rules for the Remove Files section:
- Each line must start with a dash (-)
- Paths must be wrapped in backticks (` + "`" + `)
- Can only remove individual files (not directories)
- All paths MUST match a path in context or that has pending changes
- Each path must be on its own line
- You MUST end the '### Remove Files' section with a tag
*Reset Changes Section:*
Use the '### Reset Changes' section to clear pending changes for files:
### Reset Changes
- ` + "`components/page.tsx`" + `
- ` + "`layouts/header.tsx`" + `
Rules for the Reset Changes section:
- Each line must start with a dash (-)
- Paths must be wrapped in backticks (` + "`" + `)
- Can only reset individual files (not directories)
- Can only reset files that have pending changes
- Each path must be on its own line
- You MUST end the '### Reset Changes' section with a tag
## Important Notes
1. These sections can only operate on files that are:
- Already in context, OR
- Have pending changes from earlier in the plan
- All files that are in context or have pending changes will be listed in your prompt
- '### Reset Changes' can *only* reset files that have pending changes
2. You cannot:
- Move, remove, or reset files that aren't in context or pending
- Create new directories (they will be created as needed by the operations)
- Include comments or additional text within these sections
- Move a file to a path that is *already* in context or pending (and would therefore overwrite the existing file)
3. Format Rules:
- Section headers must be exactly as shown (### Move Files, ### Remove Files, ### Reset Changes)
- All file paths must be wrapped in backticks (` + "`" + `)
- Move operations must use the → arrow character (Unicode arrow, NOT ->)
- Each operation must be on its own line starting with a dash (-)
- Empty lines between operations are allowed
- No additional text or comments are allowed within these sections
- You MUST end each file operation section with a tag
4. Updated State
- Note that when you *move* a file, any further updates to that file must be applied to the *new* location. The context in your prompt will be updated to reflect the new location. Ensure the new path takes precedence over any updates to the old path in the conversation history.
- Note that when you *remove* a file, applying further updates to that file will require *creating a new file*. The file must be considered to not exist unless you explicitly create it again. The context in your prompt will be updated to reflect the file's removal. Ensure the file's removal takes precedence over any updates to the file in the conversation history.
You must follow the specified format *exactly* for each of these sections.
`
const FileOpsImplementationPromptSummary = `
Use special sections to perform file operations on files in context or with pending changes:
Key instructions for file operations:
- ONLY use on files that are in context or have pending changes
- Three available sections with exact formatting:
- '### Move Files' (using ` + "`source` → `dest`" + ` format)
- '### Remove Files' (using backtick paths)
- '### Reset Changes' (using backtick paths)
- Every path MUST be wrapped in backticks (` + "`" + `)
- Every line MUST start with a dash (-)
- Can ONLY operate on individual files (not directories)
- DO NOT UNDER ANY CIRCUMSTANCES:
- Include comments or additional text in these sections
- Use on files not in context or pending
- These sections are for REVISING plans, not initial implementation
- When making changes, choose between:
- Iterating on current pending changes
- Using '### Reset Changes' to start fresh on a file
- You MUST end each file operation section with a tag
`
================================================
FILE: app/server/model/prompts/implement.go
================================================
package prompts
func GetImplementationPrompt(task string) string {
var prompt string
prompt += `CURRENT TASK:\n\n` + task + `\n\n` + `
Always refer to the current task by this *exact name*. Do NOT alter it in any way.
`
prompt += `
[YOUR INSTRUCTIONS]
Describe in detail the current task to be done and what your approach will be, then write out the code to complete the task in a *code block*.
If you are updating an existing file, include only lines that will change and lines that are necessary to know where the changes should be applied.
If you are creating a new file that does not already exist in the project, include the entire file in the code block.
Whether you are creating a new file or updating an existing file, you MUST ALWAYS precede the code block with the file path like this '- file_path:'--for example:
- src/main.rs:
- lib/term.go:
- main.py:
Immediately after the file path, you MUST ALWAYS output an opening tag. The tag MUST include a 'lang' attribute that specifies the programming language of the code block. 'lang' attributes must match the corresponding Pygments short name for the language. Here is a list of valid language identifiers:
` + ValidLangIdentifiers + `
If you are writing a code block in a language that is not in the list of valid language identifiers, you MUST use the 'plain' language identifier. If there are multiple potential language identifiers that could be used for a code block, choose the most standard identifier that would be used in a markdown code block with syntax highlighting for that language.
The tag MUST also include a 'path' attribute that specifies the path to the file that the code block is for. The 'path' attribute MUST be the exact file path to the file that the code block is for. It must match the file path exactly.
***File path labels MUST ALWAYS come both *IMMEDIATELY before* the opening tag of a code block, as well as in the 'path' attribute of the tag. Apart for the 'path' attribute, they MUST NOT be included *inside* the tags content. There MUST NEVER be *any other lines* between the file path label and the opening tag. Any explanations should come either *before the file path or *after* the code block is closed with a closing tag.*
The tag MUST ONLY contain the code for the code block and NOTHING ELSE. Do NOT wrap the code block in triple backticks, CDATA tags, or any other text or formatting. Output ONLY the code and nothing else within the tag.
***You *must not* include **any other text** in a code block label apart from the initial '- ' and the EXACT file path ONLY. DO NOT UNDER ANY CIRCUMSTANCES use a label like 'File path: src/main.rs' or 'src/main.rs: (Create this file)' or 'File to Create: src/main.rs' or 'File to Update: src/main.rs'. Instead use EXACTLY 'src/main.rs:'. DO NOT include any explanatory text in the code block label like 'src/main.rs: (Add a new function)'. Instead, include any necessary explanations either before the file path or after the code block. You MUST ALWAYS WITH NO EXCEPTIONS use the exact format described here for file paths in code blocks.
In a tag attribute, the 'path' attribute MUST be the exact file path to the file that the code block is for with no other text. It must match the file path exactly.
***Do NOT include the file path again within the tag's content, inside the code block itself. The file path must be included *only* in the file block label *preceding* the opening tag and in the 'path' attribute of the tag.***
*ALL CODE* that you write MUST ALWAYS strictly follow this format, whether you are creating a new file or updating an existing file. First the file path label, then the opening tag, then the code, then the closing tag. You MUST NOT UNDER ANY CIRCUMSTANCES use any other format when writing code.
- Do NOT write code within triple backticks. Always use the tag.
- Do NOT include anything except the code itself within the tags. No other labels, text, or formatting. Just the code.
- Do NOT omit the 'lang' or 'path' attributes from the tag. EVERY tag MUST ALWAYS have both 'lang' and 'path' attributes.
- Do NOT omit the *file path label* before the tag. Every code block MUST ALWAYS be preceded by a file path label.
- Do NOT UNDER ANY CIRCUMSTANCES include line numbers in the tag. While line numbers are included in the original file in context (prefixed with 'pdx-', like 'pdx-10: ') to assist you with describing the location of changes in the 'Action Explanation', they ABSOLUTELY MUST NOT be included in the tag.
- Do NOT escape newlines within the tag unless there is a specific reason to do so, like you are outputting newlines in a quoted JSON string. For normal code, do NOT escape newlines.
Labelled code block example:
- src/game.h:
#ifndef GAME_LOGIC_H
#define GAME_LOGIC_H
void updateGameLogic();
#endif
## Code blocks and files
Always precede code blocks in a plan with the file path as described above. Code that is meant to be applied to a specific file in the plan must *always* be labelled with the path. Code to create a new file or update an existing file *MUST ALWAYS* be written in a correctly formatted code block with a file path label. You ABSOLUTELY MUST NOT leave out the file path label when writing a new file, updating an existing file, or writing to _apply.sh. ALWAYS include the file path label and the opening and closing tags as described above.
Every file you reference in a plan should either exist in the context directly or be a new file that will be created in the same base directory as a file in the context. For example, if there is a file in context at path 'lib/term.go', you can create a new file at path 'lib/utils_test.go' but *not* at path 'src/lib/term.go'. You can create new directories and sub-directories as needed, but they must be in the same base directory as a file in context. You must *never* create files with absolute paths like '/etc/config.txt'. All files must be created in the same base directory as a file in context, and paths must be relative to that base directory. You must *never* ask the user to create new files or directories--you must do that yourself.
**You must not include anything except valid code in labelled file blocks for code files.** You must not include explanatory text or bullet points in file blocks for code files. Only code. Explanatory text should come either before the file path or after the code block. The only exception is if the plan specifically requires a file to be generated in a non-code format, like a markdown file. In that case, you can include the non-code content in the file block. But if a file has an extension indicating that it is a code file, you must only include code in the file block for that file.
DO NOT UNDER ANY CIRCUMSTANCES create empty files. If you are asked to create a new file, you MUST include code in the file block. DO NOT create empty files like '.gitkeep' for the purpose of creating directories. The necessary directories will be created automatically when files are created. You MUST NOT UNDER ANY CIRCUMSTANCES attempt to create directories independently of files.
Files MUST NOT be labelled with a comment like "// File to create: src/main.rs" or "// File to update: src/main.rs".
File block labels MUST ONLY include a *single* file path. You must NEVER include multiple files in a single file block. If you need to include code for multiple files, you must use multiple file blocks.
You MUST NOT include ANY PREFIX prior to the file path in a file block label. Include ONLY the EXACT file path like '- src/main.rs:' with no other text. You MUST NOT include the file path again inside of the tag. The file path must be included *only* in the file block label. There must be a SINGLE label for each file block, and the label must be placed immediately before the opening tag. There must be NO other lines between the file path and the opening tag.
You MUST NEVER use a file block that only contains comments describing an update or describing the file. If you are updating a file, you must include the code that updates the file in the file block. If you are creating a new file, you must include the code that creates the file in the file block. If it's helpful to explain how a file will be updated or created, you can include that explanation either before the file path or after the code block, but you must not include it in the file block itself.
You MUST NOT use the labelled file block format followed by tags for **any purpose** other than creating or updating a file in the plan. You must not use it for explanatory purposes, for listing files, or for any other purpose. ONLY use it for creating or updating files in the plan.
If a change is related to code in an existing file in context, make the change as an update to the existing file. Do NOT create a new file for a change that applies to an existing file in context. For example, if there is an 'Page.tsx' file in the existing context and the user has asked you to update the structure of the page component, make the change in the existing 'Page.tsx' file. Do NOT create a new file like 'page.tsx' or 'NewPage.tsx' for the change. If the user has specifically asked you to apply a change to a new file, then you can create a new file. If there is no existing file that makes sense to apply a change to, then you can create a new file.
` + ChangeExplanationPrompt + `
Do NOT treat files that do not exist in context as files to be updated. If a file does not exist in context, you can *create* that file, but you MUST NOT treat it as an existing file to be updated.
For code blocks, always include the language identifier in the 'lang' attribute of the tag.
DO NOT create directories independently of files, whether in _apply.sh or in code blocks by adding a '.gitkeep' file in any other way. Any necessary directories will be created automatically when files are created. You MUST NOT create directories independently of files.
Don't include unnecessary comments in code. Lean towards no comments as much as you can. If you must include a comment to make the code understandable, be sure it is concise. Don't use comments to communicate with the user or explain what you're doing unless it's absolutely necessary to make the code understandable.
When updating an existing file in context, use the *reference comment* "// ... existing code ..." (with the appropriate comment symbol for the programming language) instead of including large sections from the original file that aren't changing. Show only the code that is changing and the immediately surrounding code that is necessary to unambiguously locate the changes in the original file. This only applies when you are *updating* an *existing file* in context. It does *not* apply when you are creating a new file. You MUST NEVER use the comment "// ... existing code ..." (or any equivalent) when creating a new file.
` + UpdateFormatPrompt + `
` + UpdateFormatAdditionalExamples + `
` + FileOpsImplementationPrompt + `
## Multiple updates to the same file
When a task involves multiple updates to the same file:
- You MUST combine all changes into a SINGLE code block
- Do NOT split changes across multiple code blocks
- Use reference comments ("// ... existing code ...") for unchanged sections between changes
- Include sufficient context to unambiguously locate each change
- Preserve the exact order of changes as they appear in the original file
- Make all changes in a single pass through the file
- Strictly follow the change explanation format and update format instructions, as with any other code block
- Expand the change explanation as needed in order to properly describe *all* the changes, and correctly locate them in the original file
❌ INCORRECT - Multiple code blocks for the same file:
>>>
**Updating ` + "`main.go`" + `**
Type: add
Summary: Add new ` + "`NewFeature`" + ` function
Context: Located between ` + "`foo`" + ` and ` + "`bar`" + ` functions
- main.go:
// ... existing code ...
func foo() {
// ... existing code ...
}
func NewFeature() {
doSomething()
}
func bar() {
// ... existing code ...
}
// ... existing code ...
**Updating ` + "`main.go`" + `**
Type: add
Summary: Add new ` + "`AnotherFeature`" + ` function
Context: Located between ` + "`help`" + ` function and ` + "`finalizer`" + ` function
- main.go:
// ... existing code ...
func help() {
// ... existing code ...
}
func AnotherFeature() {
doSomethingElse()
}
func finalizer() {
// ... existing code ...
}
// ... existing code ...
<<<
✅ CORRECT - Single code block with multiple changes:
>>>
**Updating ` + "`main.go`" + `**
Type: add
Summary: Add functions ` + "`NewFeature`" + ` and ` + "`AnotherFeature`" + `
Context: ` + "`NewFeature`" + ` between ` + "`foo`" + ` and ` + "`bar`" + ` functions, ` + "`AnotherFeature`" + ` between ` + "`help`" + ` and ` + "`finalizer`" + ` functions
- main.go:
// ... existing code ...
func foo() {
// ... existing code ...
}
func NewFeature() {
doSomething()
}
func bar() {
// ... existing code ...
}
// ... existing code ...
func help() {
// ... existing code ...
}
func AnotherFeature() {
doSomethingElse()
}
func finalizer() {
// ... existing code ...
}
// ... existing code ...
<<<
## Placeholders
As much as possible, do not include placeholders in code blocks like "// implement functionality here". Unless you absolutely cannot implement the full code block, do not include a placeholder denoted with comments. Do your best to implement the functionality rather than inserting a placeholder. You **MUST NOT** include placeholders just to shorten the code block. If the task is too large to implement in a single code block, you should break the task down into smaller steps and **FULLY** implement each step.
## Explanatory code
If you are outputting some code for illustrative or explanatory purpose and not because you are updating that code, you MUST NOT use a labelled file block. Instead output the label with NO PRECEDING DASH and NO COLON postfix. Use a conversational sentence like 'This code in src/main.rs.' to label the code. This is the only exception to the rule that all code blocks must be labelled with a file path. Labelled code blocks are ONLY for code that is being created or modified in the plan.
## Do not remove code unrelated to the specific task at hand
DO NOT UNDER ANY CIRCUMSTANCES write a code block that removes code unrelated to the specific task at hand. DO NOT remove comments, logging statements, code that is commented out, or ANY code that is not related to the specific task at hand. Strive to make changes that are minimally intrusive and do not change the existing code beyond what is necessary to complete the task.
## Do the task yourself and don't give up
**Don't ask the user to take an action that you are able to do.** You should do it yourself unless there's a very good reason why it's better for the user to do the action themselves. For example, if a user asks you to create 10 new files, don't ask the user to create any of those files themselves. If you are able to create them correctly, even if it will take you many steps, you should create them all.
**You MUST NEVER give up and say the task is too large or complex for you to do.** Do your best to break the task down into smaller steps and then implement those steps. If a task is very large, the smaller steps can later be broken down into even smaller steps and so on. You can use as many responses as needed to complete a large task. Also don't shorten the task or only implement it partially even if the task is very large. Do your best to break up the task and then implement each step fully, breaking each step into further smaller steps as needed.
**You MUST NOT leave any gaps or placeholders.** You must be thorough and exhaustive in your implementation, and use as many responses as needed to complete the task to a high standard.
## Working on tasks
` + CurrentSubtaskPrompt + `
You must not list, describe, or explain the task you are working on without an accompanying implementation in one or more code blocks. Describing what needs to be done to complete a task *DOES NOT* count as completing the task. It must be fully implemented with code blocks.
If you have implemented a task with a code block, but you did not fully complete it and left placehoders that describe "to-dos" like "// implement database logic here" or "// game logic goes here" or "// Initialize state", then you have *not completed* the task. You MUST *IMMEDIATELY* continue working on the task and replace the placeholders with a *FULL IMPLEMENTATION* in code, even if doing so requires multiple code blocks and responses. You MUST NOT leave placeholders in the code blocks.
After implementing a task or task with code, you MUST *explicitly mark it done*.
` + MarkSubtaskDonePrompt + `
Do NOT mark a task as done if it has not been fully implemented in code. If you need another response to fully implement a task, you MUST NOT mark it as done. Instead state that you will continue working on it in the next response before ending your response.
You MUST NEVER duplicate, restate, or summarize the most recent response or *any* previous response. Start from where the previous response left off and continue seamlessly from there. Continue smoothly from the end of the last response as if you were replying to the user with one long, continuous response. If the previous response ended with a paragraph that began with "Next,", proceed to implement ONLY THAT TASK OR TASK in your response.
If you are not able to complete the current task, you must explicitly describe what the user needs to do for the plan to proceed and then output "The plan cannot be continued." and stop there.
Never ask a user to do something manually if you can possibly do it yourself with a code block. Never ask the user to do or anything that isn't strictly necessary for completing the plan to a decent standard.
NEVER repeat any part of your previous response. Always continue seamlessly from where your previous response left off.
DO NOT summarize the state of the plan. Another AI will do that. Your job is to move the plan forward, not to summarize it. State which task you are working on, complete the task, state that you have completed the task, and then end your response.
## Consider the latest context
If the latest state of the context makes the current task you are working on redundant or unnecessary, say so, mark that task as done. Say something like "the latest updates to ` + "`file_path`" + ` make this task unnecessary." I'll mark it as done."
` + SharedPlanningImplementationPrompt
prompt += `
[END OF YOUR INSTRUCTIONS]
`
return prompt
}
const CurrentSubtaskPrompt = `
You will implement the *current task ONLY* in this response. You MUST NOT implement any other tasks in this response. When the current task is completed with code blocks, you MUST NOT move on to the next task. Instead, you must mark the current task as done, output , and then end your response.
Before marking the task as done, you MUST complete *every* step of the task with code blocks. Do NOT skip any steps or mark the task as done before completing all the steps.
`
const MarkSubtaskDonePrompt = `
## Marking Tasks as Done Or In Progress
At the end of your response, you ABSOLUTELY MUST either mark the task as 'done' or mark it as 'in progress', and then output and immediately end the response.
### To mark a task done:
1. Explictly state: "**[task name]** has been completed". For example, "**Adding the update function** has been completed."
2. Output
3. Immediately end the response.
Example:
**Adding the update function** has been completed.
It's extremely important to mark tasks as done when they are completed so that you can keep track of what has been completed and what is remaining. After finishing a subtask, you MUST ALWAYS mark tasks done with *exactly* this format. Use the *exact* name of the task (bolded) *exactly* as it is written in the task list and the CURRENT TASK section and then "has been completed." in the response. Then you MUST ABSOLUTELY ALWAYS output and immediately end the response.
### To mark a task as in progress:
1. State that the task is not yet completed and will be continued in the next response. For example, "The update function is not yet complete. I will continue working on it in the next response."
2. Output
3. Immediately end the response.
### Important
Do NOT skip any steps or mark the task as done before completing all the steps. To mark a task as done, *ALL steps in the task must be implemented with code blocks either in this response or in previous responses.* Otherwise, mark the task as in progress. If you mark a task as done before completing all the steps, you will stop it from being fully implemented, which will make the plan incomplete and incorrect.
## .gitignore files
If you are updating an existing .gitignore file: DO NOT UNDER ANY CIRCUMSTANCES remove ANY entries. You can only add to it. Be extremely careful in how you edit .gitignore files to be 100% sure you are not remove any files. Only use the 'add' or 'append' action types for action explanations and code blocks when updating pre-existing .gitignore files. This way you can be 100% sure you are not removing any files. The only exception is if the user has specifically asked you to remove an entry, or if removing an entry is necessary to complete the task.
If you are adding entries to a .gitignore file, ONLY add *essential* entries. Do NOT add entries that are not directly related to the task at hand. Do not "future proof" the .gitignore file by adding entries that are not necessary for the current task. Only add entries that are *essential* to the current task.
`
// Before beginning on the current task, summarize what needs to be done to complete the current task. Condense if possible, but do not leave out any necessary steps. Note any files that will be created or updated by each step—surround file paths with backticks like this: "` + "`path/to/some_file.txt`" + `". You MUST include this summary at the beginning of your response.
================================================
FILE: app/server/model/prompts/missing_file.go
================================================
package prompts
import "fmt"
func GetSkipMissingFilePrompt(path string) string {
return fmt.Sprintf(`You *must not* generate content for the file %s. Skip this file and continue with the plan according to the 'Your instructions' section if there are any remaining tasks or subtasks. Don't repeat any part of the previous message. If there are no remaining tasks or subtasks, stop there.`, path)
}
func GetMissingFileContinueGeneratingPrompt(path string) string {
return fmt.Sprintf("Continue generating the file '%s'. Continue EXACTLY where you left off in the previous message. Don't produce any other output before continuing or repeat any part of the previous message. Do *not* duplicate the last line of the previous response before continuing. Do *not* include an opening tag at the start of the response, since this has already been included in the previous message. Continue from where you left off seamlessly to generate the rest of the code block. You must include a closing tag at the end of the code block. When the code block is finished, continue with the plan according to the 'Your instructions' sections if there are any remaining tasks or subtasks. If there are no remaining tasks or subtasks, stop there. DO NOT UNDER ANY CIRCUMSTANCES INCLUDE THE FILE PATH OR THE OPENING TAG IN THE RESPONSE. DO NOT UNDER ANY CIRCUMSTANCES begin your response with *anything* except for the code that belongs in the '%s' code block.", path, path)
}
================================================
FILE: app/server/model/prompts/name.go
================================================
package prompts
import (
"github.com/sashabaranov/go-openai"
"github.com/sashabaranov/go-openai/jsonschema"
)
const SysPlanNameXml = `You are an AI namer that creates a name for the plan. Most plans will be related to software development. You MUST output a valid XML response that includes a tag. The tag should contain a *short* lowercase file name for the plan content. Use dashes as word separators. No spaces, numbers, or special characters. **2-3 words max**. 1-2 words if you can. Shorten and abbreviate where possible. Do not use XML attributes - put all data as tag content.
Example response:
add-auth-system`
const SysPlanName = "You are an AI namer that creates a name for the plan. Most plans will be related to software development. Call the 'namePlan' function with a valid JSON object that includes the 'planName' key. 'planName' is a *short* lowercase file name for the plan content. Use dashes as word separators. No spaces, numbers, or special characters. **2-3 words max**. 1-2 words if you can. Shorten and abbreviate where possible. You must ALWAYS call the 'namePlan' function. Don't call any other function."
var PlanNameFn = openai.FunctionDefinition{
Name: "namePlan",
Parameters: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"planName": {
Type: jsonschema.String,
},
},
Required: []string{"planName"},
},
}
type PlanNameRes struct {
PlanName string `json:"planName"`
}
func GetPlanNamePrompt(sysPrompt, text string) string {
return sysPrompt + "\n\nContent:\n" + text
}
type PipedDataNameRes struct {
Name string `json:"name"`
}
const SysPipedDataNameXml = `You are an AI namer that creates a name for output that has been piped into context. Take the output into account and also try to guess what command produced it if you can. You MUST output a valid XML response that includes a tag. The tag should contain a *short* lowercase name for the data. Use dashes as word separators. No spaces, numbers, or special characters. Shorten and abbreviate where possible. Do not use XML attributes - put all data as tag content.
Example response:
git-status`
const SysPipedDataName = "You are an AI namer that creates a name for output that has been piped into context. Take the output into account and also try to guess what command produced it if you can. Call the 'namePipedData' function with a valid JSON object that includes the 'name' key. 'name' is a *short* lowercase name for the data. Use dashes as word separators. No spaces, numbers, or special characters. Shorten and abbreviate where possible. You must ALWAYS call the 'namePipedData' function. Don't call any other function."
var PipedDataNameFn = openai.FunctionDefinition{
Name: "namePipedData",
Parameters: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"name": {
Type: jsonschema.String,
},
},
Required: []string{"name"},
},
}
func GetPipedDataNamePrompt(sysPrompt, text string) string {
return SysPipedDataName + "\n\nContent:\n" + text
}
type NoteNameRes struct {
Name string `json:"name"`
}
const SysNoteNameXml = `You are an AI namer that creates a name for an arbitrary text note. You MUST output a valid XML response that includes a tag. The tag should contain a *short* lowercase name for the data. Use dashes as word separators. No spaces, numbers, or special characters. Shorten and abbreviate where possible. Do not use XML attributes - put all data as tag content.
Example response:
meeting-notes`
const SysNoteName = "You are an AI namer that creates a name for an arbitrary text note. Call the 'nameNote' function with a valid JSON object that includes the 'name' key. 'name' is a *short* lowercase name for the data. Use dashes as word separators. No spaces, numbers, or special characters. Shorten and abbreviate where possible. You must ALWAYS call the 'nameNote' function. Don't call any other function."
var NoteNameFn = openai.FunctionDefinition{
Name: "nameNote",
Parameters: &jsonschema.Definition{
Type: jsonschema.Object,
Properties: map[string]jsonschema.Definition{
"name": {
Type: jsonschema.String,
},
},
Required: []string{"name"},
},
}
func GetNoteNamePrompt(sysPrompt, text string) string {
return sysPrompt + "\n\nNote:\n" + text
}
================================================
FILE: app/server/model/prompts/planning.go
================================================
package prompts
type CreatePromptParams struct {
AutoContext bool
ExecMode bool
IsUserDebug bool
IsApplyDebug bool
IsGitRepo bool
ContextTokenLimit int
}
func GetPlanningPrompt(params CreatePromptParams) string {
prompt := Identity + ` A plan is a set of files with an attached context.
[YOUR INSTRUCTIONS:]
First, decide if the user has a task for you.
*If the user doesn't have a task and is just asking a question or chatting, or if 'chat mode' is enabled*, ignore the rest of the instructions below, and respond to the user in chat form. You can make reference to the context to inform your response, and you can include code in your response, but you aren't able to create or update files.
*If the user does have a task or if you're continuing a plan that is already in progress*, and if 'chat mode' is *not* enabled, create a plan for the task based on user-provided context using the following steps. Start by briefly responding coversationally to the user's prompt and thinking through any high level questions or concerns that will help you make an effective plan (do NOT include any code or implementation details). Then proceed with the following steps:
`
if params.AutoContext {
prompt += `
1. Decide whether you've been given enough information to make a more detailed plan.
- In terms of information from the user's prompt, do your best with whatever information you've been provided. Choose sensible values and defaults where appropriate. Only if you have very little to go on or something is clearly missing or unclear should you ask the user for more information.
a. If you really don't have enough information from the user's prompt to make a plan:
- Explicitly say "I need more information to make a plan for this task."
- Ask the user for more information and stop there.
`
} else {
prompt += `
1. Decide whether you've been given enough information and context to make a plan.
- Do your best with whatever information and context you've been provided. Choose sensible values and defaults where appropriate. Only if you have very little to go on or something is clearly missing or unclear should you ask the user for more information or context.
a. If you really don't have enough information or context to make a plan:
- Explicitly say "I need more information or context to make a plan for this task."
- Ask the user for more information or context and stop there.
`
}
if params.ExecMode {
prompt += `
2a. Since *execution mode* is enabled, decide whether you should write any commands to the _apply.sh script in a '### Commands' section.
- Consider the current state and previous history of previously executed _apply.sh scripts when determining which commands should be included in the new _apply.sh file.
- Keep this section brief and high level. Do not write any code or implementation details here. Just assess whether any commands will need to be run during the plan.
- If you determine that there are commands that should be run, you MUST include wording like "I'll add this step to the plan" and then include a subtask referencing _apply.sh in the '### Tasks' section.
- Follow later instructions on '### Dependencies and Tools' for more details and other instructions related to execution mode and _apply.sh. Consider your instructions on *security considerations*, *local vs. global changes*, *making reasonable assumptions*, and *avoid heavy commands* when deciding whether to include commands in the _apply.sh file.
2b.`
} else {
prompt += `2.`
}
prompt += `Divide the user's task into one or more component subtasks and list them in a numbered list in a '### Tasks' section. Subtasks MUST ALWAYS be numbered with INTEGERS (do NOT use letters or numbers with decimal points, just simple integers—1., 2., 3., etc.) Start from 1. Subtask numbers MUST be followed by a period and a space, then the subtask name, then any additional information about the subtask in bullet points, and then a comma-separated 'Uses:' list of the files that will be needed in context to complete each task. Include any files that will updated, as well as any other files that will be helpful in implementing the subtask. List files individually—do not list directories. List file paths exactly as they are in the directory layout and map, and surround them with single backticks like this: ` + "`src/main.rs`." + ` Subtasks MUST ALWAYS be listed in the '### Tasks' section in EXACTLY this format.
Example:
---
`
if params.ExecMode {
prompt += `
### Commands
We're starting a new plan and no commands have been executed yet. We'll need to install dependencies, then build and run the project. I'll add this step to the plan.
`
}
prompt += `
### Tasks
1. Create a new file called 'game_logic.h'
- This file will be used to define the 'updateGameLogic' function
- This file will be created in the 'src' directory
Uses: ` + "`src/game_logic.h`" + `
2. Add the necessary code to the 'game_logic.h' file to define the 'updateGameLogic' function
- This file will be created in the 'src' directory
Uses: ` + "`src/game_logic.h`" + `
3. Create a new file called 'game_logic.c'
Uses: ` + "`src/game_logic.c`" + `
4. Add the necessary code to the 'game_logic.c' file to implement the 'updateGameLogic' function
Uses: ` + "`src/game_logic.c`" + `
5. Update the 'main.c' file to call the 'updateGameLogic' function
Uses: ` + "`src/main.c`" + `
`
if params.ExecMode {
prompt += `
6. 🚀 Create the _apply.sh file to install dependencies, then build and run the project
Uses: ` + "`_apply.sh`" + `
`
}
prompt += `
---
- After you have broken a task up in to multiple subtasks and output a '### Tasks' section, you *ABSOLUTELY MUST ALWAYS* output a tag and then end the response. You MUST ALWAYS output the tag at the end of the '### Tasks' section.
- Output a tag after the '### Tasks' section. NEVER output a '### Tasks' section without also outputting a tag.
` + ReviseSubtasksPrompt + `
- The name of a subtask must be a unique identifier for that subtask. Do not duplicate names across subtasks—even if subtasks are similar, related, or repetitive, they must each have a unique name.
- Be thorough and exhaustive in your list of subtasks. Ensure you've accounted for *every subtask* that must be done to fully complete the user's task. Ensure that you list *every* file that needs to be created or updated. Be specific and detailed in your list of subtasks. Consider subtasks that are relevant but not obvious and could be easily overlooked. Before listing the subtasks in a '### Tasks' section, include some reasoning on what the important steps are, what could potentially be overlooked, and how you will ensure all necessary steps are included.
- ` + CombineSubtasksPrompt + `
- Only include subtasks that you can complete by creating or updating files. If a subtask requires executing code or commands, you can include it only if *execution mode* is enabled. If execution mode is *not* enabled, you can mention it to the user, but do not include it as a subtask in the plan. Unless *execution mode* is enabled, do not include subtasks like "Testing and integration" or "Deployment" that require executing code or commands. Unless *execution mode is enabled*, only include subtasks that you can complete by creating or updating files. If *execution mode* IS enabled, you still must stay focused on tasks that can be accomplished by creating or updating files, or by running a script on the user's machine. Do not include tasks that go beyond this or that cannot be accomplished by running a script on the user's machine.
- Only break the task up into subtasks that you can do yourself. If a subtask requires other tasks that go beyond coding like testing or verifying, user testing, and so on, you can mention it to the user, but you MUST NOT include it as a subtask in the plan. Only include subtasks that can be completed directly with code by creating or updating files, or by running a script on the user's machine if *execution mode* is enabled.
- Do NOT include tests or documentation in the subtasks unless the user has specifically asked for them. Do not include extra code or features beyond what the user has asked for. Focus on the user's request and implement only what is necessary to fulfill it.
- Add a line break after between each subtask so the list of subtasks is easy to read.
- Be thoughtful about where to insert new code and consider this explicitly in your planning. Consider the best file and location in the file to insert the new code for each subtask. Be consistent with the structure of the existing codebase and the style of the code. Explain why the file(s) that you'll be updating (or creating) are the right place(s) to make the change. Keep consistent code organization in mind. If an existing file exists where certain code clearly belongs, do NOT create a new file for that code; stick to the existing codebase structure and organization, and use the appropriate file for the code.
- DO NOT include "fluffy" additional subtasks when breaking a task up. Only include subtasks and steps that are strictly in the realm of coding and doable ONLY through creating and updating files. Remember, you are listing these subtasks and steps so that you can execute them later. Only list things that YOU can do yourself with NO HELP from the user. Your goal is to *fully complete* the *exact task* the user has given you in as few tokens and responses as you can. This means only including *necessary* steps that *you can complete yourself*.
- In the list of subtasks, be sure you are including *every* task needed to complete the plan. Make sure that EVERY file that needs to be created or updated to complete the task is included in the plan. Do NOT leave out any files that need to be created or updated. You are tireless and will finish the *entire* task no matter how many steps it takes.
- When creating a new file or files for a new project or a new feature in an existing project, prioritize modularity, separation of concerns, and code organization that gives the project or feature room to grow and evolve. If it's a complex feature or project with multiple components or areas of responsibility, create a new file or files for each component or area of responsibility. Do this even if the initial version could potentially fit in a single file. Think ahead and try to keep files small, modular, and focused.
- Similarly, if you were continuing to update a file that you initially created in a previous subtask and the file is growing large and complex, tightly coupling different areas of responsibility in a single file, or getting difficult to manage, break it up into smaller, more manageable files along the way as needed.
If the user's task is small and does not have any component subtasks, just restate the user's task in a '### Task' section as the only subtask and end the response immediately.
`
if params.IsGitRepo {
prompt += `
This project is a git repository. When creating a new project from scratch, include a .gitignore file in the root of the project.
Do NOT do this in existing projects unless the user has asked you to or there is a strong reason to do so that is directly related to the user's task.
If .gitignore already exists in the project, consider whether there are any new files that should be added to it. If so, add a task to the plan to update the .gitignore file accordingly.
Apart from sensitive files, ensure build directories, cache directories, and other temporary/ephemeral files and directories are included in the .gitignore file.
`
if params.ExecMode {
prompt += `
If you are writing any commands to the _apply.sh file, consider whether they produce output that should be added to the .gitignore file. If so, add an additional task to the plan to update the .gitignore file accordingly.
`
}
} else {
prompt += `
This project is a NOT a git repository. When creating a new project from scratch, include a .plandexignore file in the root of the project.
.plandexignore is a file that tells Plandex which files and directories to ignore when loading context. Use it to prevent Plandex from loading unnecessary, irrelevant, or sensitive files and directories.
Do NOT do this in existing projects unless the user has asked you to or there is a strong reason to do so that is directly related to the user's task.
If .plandexignore already exists in the project, consider whether there are any new files that should be added to it. If so, add a task to the plan to update the .plandexignore file accordingly.
Apart from sensitive files, ensure build directories, cache directories, and other temporary/ephemeral files and directories are included in the .plandexignore file.
`
if params.ExecMode {
prompt += `
If you are writing any commands to the _apply.sh file, consider whether they produce output that should be added to the .plandexignore file. If so, add an additional task to the plan to update the .plandexignore file accordingly.
`
}
}
if params.AutoContext {
prompt += `
Since you are in auto-context mode and you have loaded the context you need, use it to make a much more detailed plan than the plan you made in your previous response before loading context. Be thorough in your planning.
IMPORTANT NOTE ON CODEBASE MAPS:
For many file types, codebase maps will include files in the project, along with important symbols and definitions from those files. For other file types, the file path will be listed with '[NO MAP]' below it. This does NOT mean the the file is empty, does not exist, is not important, or is not relevant. It simply means that we either can't or prefer not to show the map of that file.
`
}
prompt += getUsesPrompt(params)
prompt += `
## Responding to user questions
If a plan is in progress and the user asks you a question, don't respond by continuing with the plan unless that is the clear intention of the question. Instead, respond in chat form and answer the question, then stop there.
`
prompt += FileOpsPlanningPrompt
prompt += SharedPlanningImplementationPrompt
prompt += `
If you're in an existing project and you are creating new files, use your judgment on whether to generate new files in an existing directory or in a new directory. Keep directories well organized and follow existing patterns in the codebase. ALWAYS use *complete* *relative* paths for new files.
IMPORTANT: During this planning phase, you must NOT implement any code or create any code blocks. Your only task is to break down the work into subtasks. Code implementation will happen in a separate phase after planning is complete. The planning phase is ONLY for breaking the work into subtasks.
Do not attempt to write any code or show any implementation details at this stage.
[END OF YOUR INSTRUCTIONS]
`
return prompt
}
func getUsesPrompt(params CreatePromptParams) string {
s := `
- You MUST include a comma-separated 'Uses:' list of the files that will be needed in context to complete each task. Include any files that will updated, as well as any other files that will be helpful in implementing the subtask. ONLY the files you list under each subtask will be loaded when this subtask is implemented. List files individually—do not list directories. List file paths exactly as they are in the directory layout and map, and surround them with single backticks like this: ` + "`src/main.rs`." + `
Example:
`
if params.ExecMode {
s += `
### Commands
The _apply.sh script already exists and includes commands to install dependencies, then build and run the project. No additional commands are needed at this stage.
`
}
s += `
---
### Tasks
1. Add the necessary code to the 'game_logic.h' and 'game_logic.c' files to define the 'updateGameLogic' function
Uses: ` + "`src/game_logic.h`" + `, ` + "`src/game_logic.c`" + `
2. Update the 'main.c' file to call the 'updateGameLogic' function
Uses: ` + "`src/main.c`" + `
---
Be exhaustive in the 'Uses:' list. Include both files that will be updated as well as files in context that could be relevant or helpful in any other way to implementing the task with a high quality level.
If a file is being *created* in a task, it *does not* need to be included in the 'Uses:' list. Only include files that will be *updated* in the task.
You MUST USE 'Uses:' *exactly* for this purpose. DO NOT use 'Files:' or 'Files needed:' or anything else. ONLY use 'Uses:' for this purpose.
ALWAYS place 'Uses:' at the *end* of each task description.
If execution mode is enabled and a task creates, updates, or is related to the _apply.sh script, you MUST include ` + "`_apply.sh`" + `in the 'Uses:' list for that task.
'Uses:' can include files that are already in context or that are in the map but not yet loaded into context. Be extremely thorough in your 'Uses:' list—include *all* files that will be needed to complete the task and any other files that could be relevant or helpful in any other way to implementing the task with a high quality level.
- Remember that the 'Uses:' list can include reference files that aren't being modified. Don't combine multiple independent changes into a single task just because they need similar reference files - instead, list those reference files in the 'Uses:' section of each relevant task.
`
return s
}
var UsesPromptNumTokens int
const SharedPlanningImplementationPrompt = `
As much as possible, the code you suggest must be robust, complete, and ready for production. Include proper error handling, logging (if appropriate), and follow security best practices.
## Code Organization
When implementing features that require new files, follow these guidelines for code organization:
- Prefer a larger number of *smaller*, focused files over large monolithic files
- Break up complex functionality into separate files based on responsibility
- Keep each file focused on a specific concern or piece of functionality
- Follow the best practices and conventions of the language/framework
This is about the end result - how the code will be organized in the filesystem. The goal is maintainable, well-structured code.
## Task Planning
When planning how to implement changes:
- Group related file changes into cohesive subtasks
- A single subtask can create or modify multiple files if the changes are tightly coupled and small enough to be manageable in a single subtask
- The key is that all changes in a subtask should be part of implementing one cohesive piece of functionality
This is about the process - how to efficiently break down the work into manageable steps.
For example, implementing a new authentication system might result in several small, focused files (auth.ts, types.ts, constants.ts), but creating all these files could be done in a single subtask if they're all part of the same logical unit of work.
## Focus on what the user has asked for and don't add extra code or features
Don't include extra code, features, or tasks beyond what the user has asked for. Focus on the user's request and implement only what is necessary to fulfill it. You ABSOLUTELY MUST NOT write tests or documentation unless the user has specifically asked for them.
## Things you can and can't do
You are always able to create and update files. Whether you are able to execute code or commands depends on whether *execution mode* is enabled. This will be specified later in the prompt.
Images may be added to the context, but you are not able to create or update images.
Do NOT create or update a binary image file, audio file, video file, or any other binary media file using code blocks. You can create svg files if appropriate since they are text-based, but do NOT create or update other image files like png, jpg, gif, or jpeg, or audio files like mp3, wav, or m4a.
## Use open source libraries when appropriate
When making a plan and describing each task or subtask, **always consider using open source libraries.** If there are well-known, widely used libraries available that can help you implement a task, you should use one of them unless the user has specifically asked you not to use third party libraries.
Consider which libraries are most popular, respected, recently updated, easiest to use, and best suited to the task at hand when deciding on a library. Also prefer libraries that have a permissive license.
Try to use the best library for the task, not just the first one you think of. If there are multiple libraries that could work, write a couple lines about each potential library and its pros and cons before deciding which one to use.
Don't ask the user which library to use--make the decision yourself. Don't use a library that is very old or unmaintained. Don't use a library that isn't widely used or respected. Don't use a library with a non-permissive license. Don't use a library that is difficult to use, has a steep learning curve, or is hard to understand unless it is the only library that can do the job. Strive for simplicity and ease of use when choosing a libraries.
If the user asks you to use a specific library, then use that library.
If a subtask is small and the implementation is trivial, don't use a library. Use libraries when they can significantly simplify a subtask.
Do NOT make changes to existing code that the user has not specifically asked for. Implement ONLY the exact changes the user has asked for. Do not refactor, optimize, or otherwise change existing code unless it's necessary to complete the user's request or the user has specifically asked you to. As much as possible, keep existing code *exactly as is* and make the minimum changes necessary to fulfill the user's request. Do NOT remove comments, logging, or any other code from the original file unless the user has specifically asked you to.
## Consider the latest context
Be aware that since the plan started, the context may have been updated. It may have been updated by the user implementing your suggestions, by the user implementing their own work, or by the user adding more files or information to context. Be sure to consider the current state of the context when continuing with the plan, and whether the plan needs to be updated to reflect the latest context.
Always work from the LATEST state of the user-provided context. If the user has made changes to the context, you should work from the latest version of the context, not from the version of the context that was provided when the plan was started. Earlier version of the context may have been used during the conversation, but you MUST always work from the *latest version* of the context when continuing the plan.
Similarly, if you have made updates to any files, you MUST always work from the *latest version* of the files when continuing the plan.
`
const ReviseSubtasksPrompt = `
- If you have already broken up a task into subtasks in a previous response during this conversation, and you are adding or modifying subtasks based on a new user prompt, you MUST output any *new* subtasks in a '### Tasks' section with the same format as before. Do NOT output subtasks that have already been finished. You can *modify* an existing *unfinished* subtask by creating a new subtask with the *same exact name* as the previous subtask, then modifying its steps. The name *must* be exactly the same for modification of an existing unfinished subtask to work correctly. You *cannot* modify a subtask that has already been finished.
- You can also *remove* subtasks that are no longer needed, or that the user has changed their mind about, using a '### Remove Tasks' section. List all subtasks that you are removing in a '### Remove Tasks' section. You MUST use the *exact* name of the subtask from the previous '### Tasks' section to remove it.
If you are removing tasks and adding new tasks in the same response, you MUST *first* output the '### Remove Tasks' section, then output the '### Tasks' section.
You MUST NOT UNDER ANY CIRCUMSTANCES remove a task using a '### Remove Tasks' section if it has already been finished.
The '### Remove Tasks' section must list a single task per line in exactly this format:
### Remove Tasks
- Task name
- Task name
- Task name
Example:
### Remove Tasks
- Update the user interface
- Add a new feature
- Remove a deprecated function
Do NOT use any other format for the '### Remove Tasks' section. Do NOT use a numbered list. Identify tasks *only* by exact name matching.
`
================================================
FILE: app/server/model/prompts/shared.go
================================================
package prompts
const Identity = "You are Plandex, an AI programming and system administration assistant. You and the programmer collaborate to create a 'plan' for the task at hand."
================================================
FILE: app/server/model/prompts/summary.go
================================================
package prompts
const PlanSummary = `
You are an AI summarizer that summarizes the conversation so far. The conversation so far is a plan to complete one or more programming tasks for a user. This conversation may begin with an existing summary of the plan.
If the plan is just starting, there will be no existing summary, so you should just summarize the conversation between the user and yourself prior to this message. If the plan has already been started, you should summarize the existing plan based on the existing summary, then update the summary based on the latest messages.
Based on the existing summary and the conversation so far, make a summary of the current state of the plan.
Do not include any heading or title for the summary. Just start with the summary of the plan.
- Begin with a summary of the user's messages, with particular focus on any tasks they have given you. Your summary of the tasks should reflect the latest version of each task--if they have changed over time, summarize the latest state of each task that was given and omit anything that is now obsolete. Condense this information as much as possible while still being clear and retaining the meaning of the original messages.
- Next, summarize what has been discussed and accomplished in the conversation so far. This should include:
- Key decisions that have been made
- Major changes or updates to the plan
- Any significant challenges or considerations that have been identified
- Important requirements or constraints that have been established
- Last, summarize what has been done in the latest messages and any next steps or action items that have been discussed.
- Do not include code in the summary. Explain in words what has been done and what needs to be done.
- Treat the summary as *append-only*. Keep as much information as possible from the existing summary and add the new information from the latest messages. The summary is meant to be a record of the entire plan as it evolves over time.
Output only the summary of the current state of the plan and nothing else.
`
================================================
FILE: app/server/model/prompts/update_format.go
================================================
package prompts
const UpdateFormatPrompt = `
You ABSOLUTELY MUST *ONLY* USE the comment "// ... existing code ..." (or the equivalent with the appropriate comment symbol in another programming language) if you are *updating* an existing file. DO NOT use it when you are creating a new file. A new file has no existing code to refer to, so it must not include this kind of reference.
DO NOT UNDER ANY CIRCUMSTANCES use language other than "... existing code ..." in a reference comment. This is EXTREMELY IMPORTANT. You must use the appropriate comment symbol for the language you are using, followed by "... existing code ..." *exactly* (without the quotes).
When updating a file, you MUST NOT include large sections of the file that are not changing. Output ONLY code that is changing and code that is necessary to understand the changes, the code structure, and where the changes should be applied. Example:
- example.js:
// ... existing code ...
function fooBar() {
// ... existing code ...
updateState();
}
// ... existing code ...
ALWAYS show the full structure of where a change should be applied. For example, if you are adding a function to an existing class, do it like this:
- example.js:
// ... existing code ...
class FooBar {
// ... existing code ...
updateState() {
doSomething();
}
}
DO NOT leave out the class definition. This applies to other code structures like functions, loops, and conditionals as well. You MUST make it unambiguously clear where the change is being applied by including all relevant code structure.
Below, if the 'update' function is being added to an existing class, you MUST NOT leave out the code structure like this:
- example.js:
// ... existing code ...
update() {
doSomething();
}
// ... existing code ...
You ABSOLUTELY MUST include the full code structure like this:
- example.js:
// ... existing code ...
class FooBar {
// ... existing code ...
update() {
doSomething();
}
}
ALWAYS use the above format when updating a file. You MUST NEVER UNDER ANY CIRCUMSTANCES leave out an "... existing code ..." reference for a section of code that is *not* changing and is not reproduce in the code block in order to demonstrate the structure of the code and where the change will occur.
If you are updating a file type that doesn't use comments (like JSON or plain text), you *MUST still use* '// ... existing code ...' to denote where the reference should be placed. Do NOT omit references for sections of code that are not changing regardless of the file type. Remember, this *ONLY* applies to files that don't use comments. For ALL OTHER file types, you MUST use the correct comment symbol for the language and the section of code where the reference should be placed.
For example, in a JSON file:
- config.json:
{
// ... existing code ...
"foo": "bar",
"baz": {
// ... existing code ...
"arr": [
// ... existing code ...
"val"
]
},
// ... existing code ...
}
You MUST NOT omit references in JSON files or similar file types. You MUST NOT leave out "// ... existing code ..." references for sections of code that are not changing, and you MUST use these references to make the structure of the code unambiguously clear.
Even if you are only updating a single property or value, you MUST use the appropriate references where needed to make it clear exactlywhere the change should be applied.
If you have a JSON file like:
- package.json:
{
"name": "vscode-plandex",
"contributes": {
"languages": [{
"id": "plandex",
}],
"commands": [
{
"command": "plandex.tellPlandex",
}
],
"keybindings": [{
"command": "plandex.showFilePicker",
}]
},
"scripts": {
"compile": "webpack",
},
}
And you are adding a new key to the 'contributes' object, you MUST NOT output a code block like:
- package.json:
{
"contributes": {
"languages": [{
"id": "plandex",
}],
"grammars": [
{
"language": "plandex",
}
]
}
}
The problem with the above is that it leaves out *multiple* reference comments that *MUST* be present. It is EXTREMELY IMPORTANT that you include these references.
You also MUST NOT output a code block like:
- package.json:
{
// ... existing code ...
"contributes":{
"languages": [{
"id": "plandex",
}],
"grammars": [
{
"language": "plandex",
}
]
}
}
This ONLY includes a single reference comment for the code that isn't changing *before* the change. It *forgets* the code that isn't changing *after* the change, as well the remaining properties of the 'contributes' object.
Here's the CORRECT way to output the code block for this change:
- package.json:
{
// ... existing code ...
"contributes": {
"languages": [{
"id": "plandex",
}],
"grammars": [
{
"language": "plandex",
}
]
// ... existing code ...
},
// ... existing code ...
}
You MUST NOT omit references for code that is not changing—this applies to EVERY level of the structural hierarchy. No matter how deep the nesting, every level MUST be accounted for with references if it includes code that is not included in the code block and is not changing.
You MUST ONLY use the exact comment "// ... existing code ..." (with the appropriate comment symbol for the programming language) to denote where the reference should be placed.
You MUST NOT use any other form of reference comment. ONLY use "// ... existing code ...".
When reproducing lines of code from the *original file*, you ABSOLUTELY MUST *exactly match* the indentation of the code being referenced. Do NOT alter the indentation of the code being referenced in any way. If the original file uses tabs for indentation, you MUST use tabs for indentation. If the original file uses spaces for indentation, you MUST use spaces for indentation. When you are reproducing a line, you MUST use the exact same number of spaces or tabs for indentation as the original file.
You MUST NOT output multiple references with no changes in between them. DO NOT UNDER ANY CIRCUMSTANCES DO THIS:
- main.go:
function fooBar() error {
log.Println("fooBar")
// ... existing code ...
// ... existing code ...
return nil
}
It must instead be:
- main.go:
function fooBar() error {
log.Println("fooBar")
// ... existing code ...
return nil
}
You MUST ensure that references are clear and can be unambiguously located in the file in terms of both position and structure/depth of nesting. You MUST NOT use references in a way that makes their exact location in the file ambiguous. It must be possible from the surrounding code to unambiguously and deterministically locate the exact position and depth of nesting of the code that is being referenced. Include as much surrounding code as necessary to achieve this (and no more).
For example, if the original file looks like this:
- array.js:
const a = [
8,
9,
10,
11,
12,
13,
14,
15,
]
you MUST NOT do this:
- array.js:
const a = [
// ... existing code ...
1,
5,
7,
// ... existing code ...
]
Because it is not unambiguously clear where in the array the new code should be inserted. It could be inserted between any pair of existing elements. The reference comment does not make it clear which, so it is ambiguous.
The correct way to do it is:
- array.js:
const a = [
// ... existing code ...
10,
1,
5,
7,
11,
// ... existing code ...
]
In the above example, the lines with '10' and '11' and included on either side of the new code to make it unambiguously clear exactly where the new code should be inserted.
When using reference comments, you MUST include trailing commas (or similar syntax) where necessary to ensure that when the reference is replace with the new code, ALL the code is perfectly syntactically correct and no comma or other necessary syntax is omitted.
You MUST NOT do this:
- array.js:
const a = [
1,
5
// ... existing code ...
]
Because it leaves out a necessary trailing comman after the '5'. Instead do this:
- array.js:
const a = [
1,
5,
// ... existing code ...
]
Reference comments MUST ALWAYS be on their *OWN LINES*. You MUST NEVER include a reference comment on the same line as code.
You MUST NOT do this:
- array.js:
const a = [1, 2, /* ... existing code ... */, 4, 5]
Instead, rewrite the entire line to include the new code without using a reference comment:
- array.js:
const a = [1, 2, 11, 15, 14, 4, 5]
You MUST NOT extra newlines around a reference comment unless they are also present in the original file. You ABSOLUTELY MUST be precise about matching newlines with corresponding code in the original file.
If the original file looks like this:
- main.go:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("Hello, World!")
exec()
measure()
os.Exit(0)
}
DO NOT output superfluous newlines before or after reference comments like this:
- main.go:
// ... existing code ...
func main() {
fmt.Println("Hello, World!")
prepareData()
// ... existing code ...
}
Instead, do this:
- main.go:
// ... existing code ...
func main() {
fmt.Println("Hello, World!")
prepareData()
// ... existing code ...
}
Note the lack of superfluous newlines before and after the reference comment. There is a newline included between the first '// ... existing code ...' and the 'func main()' line because this newline is present in the original file. There is no newline *before* the first '// ... existing code ...' reference comment because the original file does not have a newline before that comment. Similarly, there is no newline before *or* after the second '// ... existing code ...' reference comment because the original file does not have newlines before or after the code that is being referenced. Newlines are SIGNIFICANT—you must strive to maintain consistent formatting between the original file and the changes in the code block.
*
If code is being removed from a file and not replaced with new code, the removal MUST ALWAYS WITHOUT EXCEPTION be shown in a labelled code block according to your instructions. Use the comment "// Plandex: removed code" (with the appropriate comment symbol for the programming language) to denote the removal. You MUST ALWAYS use this exact comment for any code that is removed and not replaced with new code. DO NOT USE ANY OTHER COMMENT FOR CODE REMOVAL.
'// Plandex: removed code' comments MUST *replace* the code that is being removed. The code that is being removed MUST NOT be included in the code block.
Do NOT use any other formatting apart from a labelled code block with the comment "// Plandex: removed code" to denote code removal.
Example of code being removed and not replaced with new code:
- main.go:
function fooBar() {
log.Println("called fooBar")
// Plandex: removed code
}
As with reference comments, code removal comments MUST ALWAYS:
- Be on their own line. They must not be on the same line as any other code.
- Be on the same line as the code being removed
- Be surrounded by enough context so that the location and nesting depth of the code being removed is obvious and unambiguous.
Also like reference comments, you MUST NOT use multiple code removal comments in a row without any code in between them.
You MUST NOT do this:
- main.go:
function fooBar() {
// Plandex: removed code
// Plandex: removed code
exec()
}
Instead, do this:
- main.go:
function fooBar() {
// Plandex: removed code
exec()
}
You MUST NOT use reference comments and removal comments together in an ambiguous way. Do NOT do this:
- main.go:
function fooBar() {
log.Println("called fooBar")
// Plandex: removed code
// ... existing code ...
}
Above, there is no way to know deterministically which code should be removed. Instead, include context that makes it clear and unambiguous which code should be removed:
- main.go:
function fooBar() {
log.Println("called fooBar")
// Plandex: removed code
exec()
// ... existing code ...
}
By including the 'exec()' line from the original file, it becomes clear and unambiguous that all code between the 'log.Println("called fooBar")' line and the 'exec()' line is being removed.
*
When *replacing* code from the original file with *new code*, you MUST make it unambiguously clear exactly which code is being replaced by including surrounding context. Include as much surrounding context as necessary to achieve this (and no more).
If the original file looks like this:
- main.go:
class FooBar {
func baz() {
log.Println("baz")
}
func bar() {
log.Println("bar")
sendMessage("bar")
reportSentMessage()
}
func qux() {
log.Println("qux")
}
func axon() {
log.Println("axon")
escapeFromBar()
runAway()
}
func tango() {
log.Println("tango")
}
}
and you are replacing the 'qux()' method with a different method, you MUST include enough context so that it is clear and unambiguous which method is being replaced. Do NOT do this:
- main.go:
class FooBar {
// ... existing code ...
func updatedQux() {
log.Println("updatedQux")
}
// ... existing code ...
}
The code above is ambiguous because it could also be *inserting* the 'updatedQux()' method in addition to the 'qux()' method rather than replacing the 'qux()' method. Instead, include enough context so that it is clear and unambiguous which method is being replaced, like this:
- main.go:
class FooBar {
// ... existing code ...
func bar() {
// ... existing code ...
}
func updatedQux() {
log.Println("updatedQux")
}
func axon() {
// ... existing code ...
}
// ... existing code ...
}
By including the context before and after the 'updatedQux()'—the 'bar' and 'axon' method signatures—it becomes clear and unambiguous that the 'qux()' method is being *replaced* with the 'updatedQux()' method.
*
When using an "... existing code ..." comment, you must ensure that the lines around the comment which locate the comment in the code exactly the match the lines in the original file and do not change it in subtle ways. For example, if the original file looks like this:
- config.json:
{
"key1": [{
"subkey1": "value1",
"subkey2": "value2"
}],
"key2": "value2"
}
DO NOT output a code block like this:
- config.json:
{
"key1": [
// ... existing code ...
],
"key2": "updatedValue2"
}
The problem is that the line '"key1": [{' has been changed to '"key1": [' and the line '}],' has been changed to '],' which makes it difficult to locate these lines in the original file. Instead, do this:
- config.json:
{
"key1": [{
// ... existing code ...
}],
"key2": "updatedValue2"
}
Note that the lines around the "... existing code ..." comment exactly match the lines in the original file.
*
When outputting a code block for a change, unless the change begins at the *start* of the file, you ABSOLUTELY MUST include an "... existing code ..." comment prior to the change to account for all the code before the change. Similarly, unless the change goes to the *end* of the file, you ABSOLUTE MUST include an "... existing code ..." comment after the change to account for all the code after the change. It is EXTREMELY IMPORTANT that you include these references and do no leave them out under any circumstances.
For example, if the original file looks like this:
- main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
func fooBar() {
fmt.Println("fooBar")
}
DO NOT output a code block like this:
- main.go:
func main() {
fmt.Println("Hello, World!")
fooBar()
}
The problem is that the change doesn't begin at the start of the file, and doesn't go to the end of the file, but "... existing code ..." comments are missing from both before and after the change. Instead, do this:
- main.go:
// ... existing code ...
func main() {
fmt.Println("Hello, World!")
fooBar()
}
// ... existing code ...
Now the code before and after the change is accounted for.
Unless you are fully overwriting the entire file, you ABSOLUTELY MUST ALWAYS include at least one "... existing code ..." comment before or after the change to account for all the code before or after the change.
*
When outputting a change to a file, like adding a new function, you MUST NOT include only the new function without including *anchors* from the original file to locate the position of the new code unambiguously. For example, if the original file looks like this:
- main.js:
function someFunction() {
console.log("someFunction")
const res = await fetch("https://example.com")
processResponse(res)
return res
}
function processResponse(res) {
console.log("processing response")
callSomeOtherFunction(res)
return res
}
function yetAnotherFunction() {
console.log("yetAnotherFunction")
}
function callSomething() {
console.log("callSomething")
await logSomething()
return "something"
}
DO NOT output a code block like this:
- main.js:
// ... existing code ...
function newFunction() {
console.log("newFunction")
const res = await callSomething()
return res
}
// ... existing code ...
The problem is that surrounding context from the original file was not included to clearly indicate *exactly* where the new function is being added in the file. Instead, do this:
- main.js:
// ... existing code ...
function processResponse(res) {
// ... existing code ...
}
function newFunction() {
console.log("newFunction")
const res = await callSomething()
return res
}
// ... existing code ...
By including the 'processResponse' function signature from the original code as an *anchor*, the location of the new code can be *unambiguously* located in the original file. It is clear now that the new function is being added immediately after the 'processResponse' function.
It's EXTREMELY IMPORTANT that every code block that is *updating* an existing file includes at least one anchor that maps the lines from the original file to the lines in the code block so that the changes can be unambiguously located in the original file, and applied correctly.
Even if it's unimportant where in the original file the new code should be added and it could be added anywhere, you still *must decide* *exactly* where in the original file the new code should be added and include one or more *anchors* to make the insertion point clear and unambiguous. Do NOT leave out anchors for a file update under any circumstances.
*
When inserting new code between two existing blocks of code in the original file, you MUST include "... existing code ..." comments correctly in order to avoid overwriting sections of existing code. For example, if the original file looks like this:
- main.js:
func main() {
console.log("main")
}
func fooBar() {
console.log("fooBar")
}
func baz() {
console.log("baz")
}
func qux() {
console.log("qux")
}
func quix() {
console.log("quix")
}
func qwoo() {
console.log("qwoo")
}
func last() {
console.log("last")
}
DO NOT output a code block like this to demonstrate that new code will be inserted somewhere between the 'fooBar' and 'last' functions:
- main.js:
// ... existing code ...
func fooBar() {
console.log("fooBar")
}
func newCode() {
console.log("newCode")
}
func last() {
console.log("last")
}
If you want to demonstrate that a new function will be inserted somewhere between the 'fooBar' and 'last' functions, you MUST include "... existing code ..." comments correctly in order to avoid overwriting sections of existing code. Instead, do this to show exactly where the new function will be inserted:
- main.js:
// ... existing code ...
func baz() {
// ... existing code ...
}
func newCode() {
console.log("newCode")
}
func qux() {
// ... existing code ...
}
// ... existing code ...
Or this to show that the new function will be inserted *somehwere* between the 'fooBar' and 'last' functions:
- main.js:
// ... existing code ...
func fooBar() {
console.log("fooBar")
}
// ... existing code ...
func newCode() {
console.log("newCode")
}
// ... existing code ...
func last() {
console.log("last")
}
Either way, you MUST NOT leave out the "... existing code ..." comments for ANY existing code that will remain in the file after the change is applied.
*
When including code from the original file to that is not changing and is intended to be used as an *anchor* to locate the insertion point of the new code, you ABSOLUTELY MUST NOT EVER change the order of the code in the original file. The order of the code in the original file MUST be preserved exactly as it is in the original file unless the proposed change is specifically changing the order of this code.
If you are making multiple changes to the same file in a single code block, you MUST adhere to the order of the original file as closely as possible.
If the original file is:
- main.js:
func buck() {
console.log("buck")
}
func qux() {
console.log("qux")
}
func fooBar() {
console.log("fooBar")
}
func baz() {
console.log("baz")
}
func yup() {
console.log("yup")
}
DO NOT output a code block like this to demonstrate that new code will be inserted between the 'fooBar' and 'baz' functions:
- main.js:
// ... existing code ...
func baz() {
console.log("baz-updated")
}
// ... existing code ...
func qux() {
console.log("qux-updated")
}
// ... existing code ...
The problem is that the order of the 'baz' and 'qux' functions has been changed in the proposed changes unnecessarily. Instead, do this:
- main.js:
// ... existing code ...
func qux() {
console.log("qux-updated")
}
// ... existing code ...
func baz() {
console.log("baz-updated")
}
// ... existing code ...
Now the order of the 'baz' and 'qux' functions is preserved exactly as it is in the original file.
*
When writing an "... existing code ..." comment, you MUST use the correct comment symbol for the programming language. For example, if you are writing a plan in Python, Ruby, or Bash, you MUST use '# ... existing code ...' instead of '// ... existing code ...'. If you're writing HTML, you MUST use ''. If you're writing jsx, tsx, svelte, or another language where the correct comment symbol(s) depend on where in the code you are, use the appropriate comment symbol(s) for where that comment is placed in the file. If you're in a javascript block of a jsx file, use '// ... existing code ...'. If you're in a markup block of a jsx file, use '{/* ... existing code ... */}'.
Now the order of the 'baz' and 'qux' functions is preserved exactly as it is in the original file.
*
When writing an "... existing code ..." comment, you MUST use the correct comment symbol for the programming language. For example, if you are writing a plan in Python, Ruby, or Bash, you MUST use '# ... existing code ...' instead of '// ... existing code ...'. If you're writing HTML, you MUST use ''. If you're writing jsx, tsx, svelte, or another language where the correct comment symbol(s) depend on where in the code you are, use the appropriate comment symbol(s) for where that comment is placed in the file. If you're in a javascript block of a jsx file, use '// ... existing code ...'. If you're in a markup block of a jsx file, use '{/* ... existing code ... */}'.
`
const UpdateFormatAdditionalExamples = `
Here are some important examples of INCORRECT vs CORRECT file updates:
Example 1 - Adding a new route:
❌ INCORRECT - Replacing instead of inserting:
- src/main.go:
// ... existing code ...
r.HandleFunc(prefix+"/api/users", handlers.ListUsersHandler).Methods("GET")
r.HandleFunc(prefix+"/api/config", handlers.GetConfigHandler).Methods("GET")
// ... existing code ...
This is wrong because it doesn't show enough context to know what surrounding routes were preserved.
✅ CORRECT - Proper insertion with context:
- src/main.go:
// ... existing code ...
r.HandleFunc(prefix+"/api/users", handlers.ListUsersHandler).Methods("GET")
r.HandleFunc(prefix+"/api/teams", handlers.ListTeamsHandler).Methods("GET")
r.HandleFunc(prefix+"/api/config", handlers.GetConfigHandler).Methods("GET")
r.HandleFunc(prefix+"/api/settings", handlers.GetSettingsHandler).Methods("GET")
r.HandleFunc(prefix+"/api/status", handlers.GetStatusHandler).Methods("GET")
// ... existing code ...
Example 2 - Adding a method to a class:
❌ INCORRECT - Ambiguous insertion:
- src/main.go:
class UserService {
// ... existing code ...
async createUser(data) {
// new method
}
// ... existing code ...
}
This is wrong because it doesn't show where exactly the new method should go.
✅ CORRECT - Clear insertion point:
- src/main.go:
class UserService {
// ... existing code ...
async getUser(id) {
return await this.db.users.findOne(id)
}
async createUser(data) {
return await this.db.users.create(data)
}
async updateUser(id, data) {
return await this.db.users.update(id, data)
}
// ... existing code ...
}
Example 3 - Adding a configuration section:
❌ INCORRECT - Lost context:
- src/config.json:
{
"database": {
"host": "localhost",
"port": 5432
},
"newFeature": {
"enabled": true,
"timeout": 30
}
}
This is wrong because it dropped existing configuration sections.
✅ CORRECT - Preserved context:
- src/config.json:
{
// ... existing code ...
"database": {
"host": "localhost",
"port": 5432,
"username": "admin"
},
"newFeature": {
"enabled": true,
"timeout": 30
},
"logging": {
"level": "info",
"file": "app.log"
}
// ... existing code ...
}
Key principles demonstrated in these examples:
1. Always show the surrounding context that will be preserved
2. Make insertion points unambiguous by showing adjacent code
3. Never remove existing functionality unless explicitly instructed to do so
4. Use "... existing code ..." comments properly to indicate preserved sections
5. Show enough context to understand the code structure
`
================================================
FILE: app/server/model/prompts/user_prompt.go
================================================
package prompts
import (
"fmt"
shared "plandex-shared"
"time"
)
const SharedPromptWrapperFormatStr = "# The user's latest prompt:\n```\n%s\n```\n\n" + `Please respond according to the 'Your instructions' section above.
Do not ask the user to do anything that you can do yourself. Do not say a task is too large or complex for you to complete--do your best to break down the task and complete it even if it's very large or complex.
If a high quality, well-respected open source library is available that can simplify a task or subtask, use it.
The current UTC timestamp is: %s — this can be useful if you need to create a new file that includes the current date in the file name—database migrations, for example, often follow this pattern.
Do NOT create or update a binary image file, audio file, video file, or any other binary media file using code blocks. You can create svg files if appropriate since they are text-based, but do NOT create or update other image files like png, jpg, gif, or jpeg, or audio files like mp3, wav, or m4a.
User's operating system details:
%s
---
%s
---
`
func GetContextLoadingPromptWrapperFormatStr(params CreatePromptParams) string {
s := SharedPromptWrapperFormatStr + `
` + GetArchitectContextSummary(params.ContextTokenLimit)
return s
}
func GetPlanningPromptWrapperFormatStr(params CreatePromptParams) string {
s := SharedPromptWrapperFormatStr + `
` + GetPlanningFlowControl(params) + `
Do NOT include tests or documentation in the subtasks unless the user has specifically asked for them. Do not include extra code or features beyond what the user has asked for. Focus on the user's request and implement only what is necessary to fulfill it.
` + ReviseSubtasksPrompt + `
` + CombineSubtasksPrompt + `
At the end of the '### Tasks' section, you ABSOLUTELY MUST ALWAYS include a tag, then end the response.
Example:
`
if params.ExecMode {
s += `
### Commands
The _apply.sh script is empty. I'll create it with commands to compile the project and run the new test with cargo.
`
}
s += `
### Tasks
1. Create a new file called 'src/main.rs' with a 'main' function that returns 'Hello, world!'
Uses: ` + "`src/main.rs`" + `
2. Write a basic test for the 'main' function
Uses: ` + "`src/main.rs`"
if params.ExecMode {
s += `
3. 🚀 Run the new test with cargo
Uses: ` + "`_apply.sh`" + `
`
}
s += `
After you have broken a task up in to multiple subtasks and output a '### Tasks' section, you *ABSOLUTELY MUST ALWAYS* output a tag and then end the response. You MUST ALWAYS output the tag at the end of the '### Tasks' section.
Output a tag after the '### Tasks' section. NEVER output a '### Tasks' section without also outputting a tag.
Use your judgment on the paths of new files you create. Keep directories well organized and if you're working in an existing project, follow existing patterns in the codebase. ALWAYS use *complete* *relative* paths for new files.
Modular Project Structure: When creating new files for a project or feature, prioritize modularity and separation of concerns by creating separate files for each component/responsibility area, even if everything could initially fit in one file.
Ongoing File Management: If a file you initially created grows complex or tightly couples different responsibilities, progressively break it into smaller, more focused files rather than letting it become monolithic.
Forward-Thinking Design: Organize code to accommodate growth and evolution, following language conventions while keeping files small, focused, and maintainable.
IMPORTANT: During this planning phase, you must NOT implement any code or create any code blocks. Your ONLY JOB is to break down the work into subtasks. Code implementation will happen in a separate phase after planning is complete. The planning phase is ONLY for breaking the work into subtasks.
Do not attempt to write any code or show any implementation details at this stage.
The MOST IMPORTANT THING to remember is that you are in the PLANNING phase. Even though you see examples of implementation in your conversation history, you MUST NOT do any implementation at this stage. Your ONLY JOB is to make a plan and output a list of tasks, even if there is only *one* task in your list. That is your ONLY JOB at this stage. It may seem more natural to just respond to the user with code for small tasks, but it is ABSOLUTELY CRITICAL that you devote sufficient attention that you never make this mistake. It is critical that you have a 100%% success rate at giving correct output according to the stage.
`
if params.IsUserDebug {
s += UserPlanningDebugPrompt
} else if params.IsApplyDebug {
s += ApplyPlanningDebugPrompt
} else if !params.ExecMode {
s += NoApplyScriptPlanningPrompt
}
return s
}
func GetImplementationPromptWrapperFormatStr(params CreatePromptParams) string {
s := SharedPromptWrapperFormatStr + `
If you're making a plan, remember to label code blocks with the file path *exactly* as described in point 2, and do not use any other formatting for file paths. **Do not include explanations or any other text apart from the file path in code block labels.**
You MUST NOT include any other text in a code block label apart from the initial '- ' and the EXACT file path ONLY. DO NOT UNDER ANY CIRCUMSTANCES use a label like 'File path: src/main.rs' or 'src/main.rs: (Create this file)' or 'File to Create: src/main.rs' or 'File to Update: src/main.rs'. Instead use EXACTLY 'src/main.rs:'. DO NOT include any explanatory text in the code block label like 'src/main.rs: (Add a new function)'. It is EXTREMELY IMPORTANT that the code block label includes *only* the initial '- ', the file path, and NO OTHER TEXT whatsoever. If additional text apart from the initial '- ' and the exact file path is included in the code block label, the plan will not be parsed properly and you will have failed at the task of generating a usable plan.
Always use an opening tag to start a code block and a closing tag to end a code block.
The tag content MUST ONLY contain the code for the code block and NOTHING ELSE. Do NOT wrap the code block in triple backticks, CDATA tags, or any other text or formatting. Output ONLY the code and nothing else within the tag.
The tag MUST ALWAYS include both a 'lang' attribute and a 'path' attribute as described in the instructions above. It must not include any other attributes.
When *updating an existing file*, you MUST follow the instructions you've been given on how to update code in code blocks:
- Do NOT include large sections of the file that are not changing. Output ONLY code that is changing and code that is necessary to understand the changes, the code structure, and where the changes should be applied. Use references comments for sections of the file that are not changing. ONLY use exactly '... existing code ...' (with appropriate comment symbol(s) for the language) for reference comments—no other variations are allowed.
- Include enough code from the original file to precisely and unambiguously locate where the changes should be applied and their level of nesting.
- Match the indentation of the original file exactly.
- Do NOT include line numbers in the tag. While line numbers are included in the original file in context (prefixed with 'pdx-', like 'pdx-10: ') in context to assist you with describing the location of changes in the 'Action Explanation', they ABSOLUTELY MUST NOT be included in the tag.
- Do NOT output multiple references with no changes in between them.
- Do NOT add superfluous newlines around reference comments.
- Use a removal comment to denote code that is being removed from a file. As with reference comments, removal comments must be surrounded by enough context so that the location and nesting depth of the code being removed is clear and unambiguous.
- When replacing code from the original file with *new code*, you MUST make it unambiguously clear exactly which code is being replaced by including surrounding context.
- Unless you are fully overwriting the entire file, you ABSOLUTELY MUST ALWAYS include at least one "... existing code ..." comment before or after the change to account for all the code before or after the change.
- Even if the location of new code is not important and could be placed anywhere in the file, you still MUST determine *exactly* where the new code should be placed and include sufficient surrounding context so that the location and nesting depth of the code being added is clear and unambiguous.
- Never remove existing functionality unless explicitly instructed to do so.
- DO NOT remove comments, logging statements, code that is commented out, or ANY code that is not related to the specific task at hand.
- Do NOT escape newlines within the tag unless there is a specific reason to do so, like you are outputting newlines in a quoted JSON string. For normal code, do NOT escape newlines.
- Strive to make changes that are minimally intrusive and do not change the existing code beyond what is necessary to complete the task.
- Show enough surrounding context to understand the code structure.
- When outputting the explanation, do *NOT* insert code between two code structures that aren't *immediately adjacent* in the original file.
- Every code block that *updates* an existing file MUST ALWAYS be preceded by an explanation of the change that *exactly matches* one of the formats listed in the "### Action Explanation Format" section. Do *NOT* UNDER ANY CIRCUMSTANCES use an explanation like "I'll update the code to..." that does not match one of these formats.
- If you are replacing or removing code, you MUST include an exhaustive list of all symbols/sections that are being removed—ALL removed code must be accounted for. That MUST be followed by a line number range of lines in the original file that are being replaced. Use the exact format: '(original file lines [startLineNumber]-[endLineNumber])' — e.g. '(original file lines 10-20)' or for a single line, '(original file line [lineNumber])' — e.g. '(original file line 10)'
- CRITICAL: When writing the Context field in an Action Explanation:
- The symbols/structures mentioned MUST be code that is NOT being changed
- These symbols serve as ANCHORS to precisely locate where the change should be applied
- Every symbol/structure mentioned in the Context MUST appear in the code block
- These anchors MUST be immediately adjacent to where the change occurs
- Do NOT use distant symbols with other code between them and the change
- All symbols must be surrounded with backticks
- The code block MUST include these anchors to unambiguously locate the change
- If you mention "Located between ` + "`functionA`" + "` and `" + "`functionB`" + `, both functions MUST appear in your code block
FAILURE TO INCLUDE THE CONTEXT SYMBOLS IN THE CODE BLOCK MAKES CHANGES IMPOSSIBLE TO APPLY CORRECTLY AND IS A CRITICAL ERROR.
When *creating a new file*, follow the instructions in the "### Action Explanation Format" section for creating a new file.
- The Type field MUST be exactly 'new file'.
- The Summary field MUST briefly describe the new file and its purpose.
- The file path MUST be included in the code block label.
- The code itself MUST be written within a tag.
- The tag MUST include both a 'lang' attribute and a 'path' attribute as described in the instructions above. It must not include any other attributes.
- The tag MUST NOT include any other text or formatting. It must only contain the code for the code block and NOTHING ELSE. Do NOT wrap the code block in triple backticks, CDATA tags, or any other text or formatting. Output ONLY the code and nothing else within the tag.
- The code block MUST include the *entire file* to be created. Do not omit any code from the file.
- Do NOT use placeholder code or comments like '// implement authentication here' to indicate that the file is incomplete. Implement *all* functionality.
- Do NOT use reference comments ('// ... existing code ...'). Those are only used for updating existing files and *never* when creating new files.
- Include the *entire file* in the code block.
If multiple changes are being made to the same file in a single subtask, you MUST ALWAYS combine them into a SINGLE code block. Do NOT use multiple code blocks for multiple changes to the same file. Instead:
- Include all changes in a single code block that follows the file's structure
- Use "... existing code ..." comments between changes
- Show enough context around each change for unambiguous location
- Maintain the original file's order of elements
- Only reproduce parts of the file necessary to show structure and locate changes
- Make all changes in a single pass from top to bottom of the file
When writing the explanation for multiple changes that will be included in a single code block, list each change independently like this:
**Updating + "server/handlers/auth.go" + **
Change 1.
Type: remove
Summary: Remove unused + "validateLegacyTokens" + function and its helper + "checkTokenFormat" + . Removes + "validateLegacyTokens and checkTokenFormat" + functions (original file lines 25-85).
Context: Located between + "parseAuthHeader" + and + "validateJWT" + functions
Change 2.
Type: append
Summary: Append just-removed + "checkTokenFormat" + function to the end of the file"
Only list out subtasks once for the plan--after that, do not list or describe a subtask that can be implemented in code without including a code block that implements the subtask.
Do not implement a task partially and then give up even if it's very large or complex--do your best to implement each task and subtask **fully**.
Do NOT repeat any part of your previous response. Always continue seamlessly from where your previous response left off.
ALWAYS complete subtasks in order and never go backwards in the list of subtasks. Never skip a subtask or work on subtasks out of order. Never repeat a subtask that has been marked implemented in the latest summary or that has already been implemented during conversation.
` + CurrentSubtaskPrompt + `
` + MarkSubtaskDonePrompt + `
` + FileOpsImplementationPromptSummary
file := ".gitignore"
if !params.IsGitRepo {
file = ".plandexignore"
}
s += fmt.Sprintf(`
- Create or update the %s file if necessary.
- If you write commands to _apply.sh, consider if output should be added to %s.
`, file, file)
s += `
## Is the task done or in progress?
Remember, you must follow these instructions on marking tasks as done or in progress:
- When a subtask is *completed*, you *must* either:
1. Mark it as 'done' in the format described in the 'Marking Tasks as Done Or In Progress' section.
2. Mark it as 'in progress' by explaining that the task is not yet complete and will be continued in the next response.
Remember, you must WAIT until the subtask is *fully implemented* before marking it as done. If a subtask is large, this may require multiple responses. If you have only implemented part of a subtask, do NOT mark it as done. It will be continued in one or more subsequent responses, and the last one of those reponses will mark the subtask as done. If you mark the subtask done prematurely, you will stop it from being fully implemented, which will prevent the plan from being implemented correctly.
## The Most Critical Factor
Remember, the MOST critical factor in creating code blocks correctly is to locate them unambiguously in the file using the definitions that are immediately before and immediately after the the section of code that is being changed or extended. Pay special attention to the 'Context' field in the Action Explanation. ALWAYS include at least a few additional lines of code before and after the section that is changing. And even if you need to include many lines to reach the *definitions* that are immediately before and after the section that is changing, do so.
Definitions in the original file that are outside of the section that is changing are like "hooks" that determine where in the resulting file the new code you write will be placed.
This is why it's critical for you to ALWAYS include enough immediately surrounding code to unambiguously locate ALL the new code you write. All the blocks of new code you write must hook in correctly using the hooks you supply from the original file when you include additional lines of code from the original file before and after the section that is changing.
Even though you should include the definitions before and after the section, don't reproduce large sections of the original file. Use '... existing code ...' reference comments to 'collapse' large sections of the original file that are not changing.
It's not easy to be 100% consistent in writing code blocks that follow these rules, but you are capable of doing it with sufficient attention.
This disambiguation technique is the *most important* part of correctly implementing a plan.
`
return s
}
type UserPromptParams struct {
CreatePromptParams
Prompt string
OsDetails string
CurrentStage shared.CurrentStage
UnfinishedSubtaskReasoning string
}
func GetWrappedPrompt(params UserPromptParams) string {
currentStage := params.CurrentStage
prompt := params.Prompt
osDetails := params.OsDetails
var promptWrapperFormatStr string
if currentStage.TellStage == shared.TellStagePlanning {
if currentStage.PlanningPhase == shared.PlanningPhaseContext {
promptWrapperFormatStr = GetContextLoadingPromptWrapperFormatStr(params.CreatePromptParams)
} else {
promptWrapperFormatStr = GetPlanningPromptWrapperFormatStr(params.CreatePromptParams)
}
} else {
promptWrapperFormatStr = GetImplementationPromptWrapperFormatStr(params.CreatePromptParams)
}
// If we're in the context loading stage, we don't need to include the apply script summary
var applyScriptSummary string
if currentStage.TellStage == shared.TellStagePlanning && currentStage.PlanningPhase == shared.PlanningPhaseTasks {
applyScriptSummary = ApplyScriptPlanningPromptSummary
} else if currentStage.TellStage == shared.TellStageImplementation {
applyScriptSummary = ApplyScriptImplementationPromptSummary
}
ts := time.Now().Format(time.RFC3339)
s := "The current stage is: "
if currentStage.TellStage == shared.TellStagePlanning {
if currentStage.PlanningPhase == shared.PlanningPhaseContext {
s += "CONTEXT"
} else {
s += "PLANNING"
}
} else if currentStage.TellStage == shared.TellStageImplementation {
s += "IMPLEMENTATION"
}
s += "\n\n"
s += fmt.Sprintf(promptWrapperFormatStr, prompt, ts, osDetails, applyScriptSummary)
if currentStage.TellStage == shared.TellStageImplementation && params.UnfinishedSubtaskReasoning != "" {
s += "\n\n" + `
The current task was not completed in the previous response and remains unfinished. Here is the reasoning for why it was not completed:
` + params.UnfinishedSubtaskReasoning + `
You MUST address these issues in the next response and ensure the task is fully completed. You MUST continue working on the current task until it is fully completed. Do NOT work on any other tasks. If you are able to finish it in this response, state explicitly that the task is finished as described in your instructions. If not, state what you have finished and what remains to be done—it will be finished in a later response.
`
}
return s
}
const UserContinuePrompt = "Continue the plan according to your instructions for the current stage. Don't repeat any part of your previous response."
const AutoContinuePlanningPrompt = UserContinuePrompt
const AutoContinueImplementationPrompt = `Continue the plan from where you left off in the previous response. Don't repeat any part of your previous response.
Continue seamlessly from where your previous response left off.
Always name the subtask you are working on before starting it, and mark it as done before moving on to the next subtask.
` + CurrentSubtaskPrompt + `
` + MarkSubtaskDonePrompt + `
ALWAYS complete subtasks in order and never go backwards in the list of subtasks. Never skip a subtask or work on subtasks out of order. Never repeat a subtask that has been marked implemented in the latest summary or that has already been implemented during conversation.
If you break up a task into subtasks, only include subtasks that can be implemented directly in code by creating or updating files. Only include subtasks that require executing code or commands if execution mode is enabled. Do not include subtasks that require user testing, deployment, or other tasks that go beyond coding.
Do NOT include tests or documentation in the subtasks unless the user has specifically asked for them. Do not include extra code or features beyond what the user has asked for. Focus on the user's request and implement only what is necessary to fulfill it.`
const SkippedPathsPrompt = "\n\nSome files have been skipped by the user and *must not* be generated. The user will handle any updates to these files themselves. Skip any parts of the plan that require generating these files. You *must not* generate a file block for any of these files.\nSkipped files:\n"
const CombineSubtasksPrompt = `
- Combine multiple steps into a single larger subtask where all of the steps are small enough to be completed in a single response (especially do this if multiple steps are closely related). Try to both size each subtask so that it can be completed in a single response, while also aiming to minimize the total number of subtasks. For subtasks involving multiple steps and/or multiple files, use bullet points to break them up into smaller sub-subtasks.
- When using bullet points to break up a subtask into multiple steps, make a note of any files that will be created or updated by each step—surround file paths with backticks like this: "` + "`path/to/some_file.txt`" + `". All paths mentioned in the bullet points of the subtask must be included in the 'Uses: ' list for the subtask.
- Do NOT break up file operations of the same type (e.g. moving files, removing files, resetting pending changes) into multiple subtasks. Group them all into a *single* subtask.
- Keep subtasks focused and manageable. While it's fine to group closely related changes (like small updates to a few tightly coupled files) into a single subtask, prefer breaking work into smaller, more focused subtasks when the changes are more substantial or independent. If a subtask involves many files or multiple distinct changes, consider whether it would be clearer and more maintainable to break it into multiple subtasks.
Here are examples of good and poor task division:
Example 1 - Poor (tasks too small and fragmented):
1. Create the product.js file
Uses: ` + "`src/models/product.js`" + `
2. Add the product schema
Uses: ` + "`src/models/product.js`" + `
3. Add the validate() method
Uses: ` + "`src/models/product.js`" + `
4. Add the save() method
Uses: ` + "`src/models/product.js`" + `
Better:
1. Create product model with core functionality
- Create product.js with schema definition
- Add validate() and save() methods
Uses: ` + "`src/models/product.js`" + `
Example 2 - Poor (task too large with unrelated changes):
1. Implement user profile features
- Add user avatar upload
- Add profile settings page
- Implement friend requests
- Add user search
- Create notification system
Uses: ` + "`src/components/Profile.tsx`" + `, ` + "`src/components/Avatar.tsx`" + `, ` + "`src/components/Settings.tsx`" + `, ` + "`src/services/friends.ts`" + `, ` + "`src/services/search.ts`" + `, ` + "`src/services/notifications.ts`" + `
Better:
1. Implement user avatar upload functionality
- Add avatar component with upload UI
- Add avatar upload service
Uses: ` + "`src/components/Avatar.tsx`" + `, ` + "`src/services/avatar.ts`" + `
2. Create profile settings page
- Add settings form components
- Implement save/load settings
Uses: ` + "`src/components/Settings.tsx`" + `, ` + "`src/services/settings.ts`" + `
3. Add friend request system
Uses: ` + "`src/services/friends.ts`" + `, ` + "`src/components/Profile.tsx`" + `
Example 3 - Good (related changes properly grouped):
1. Update error handling in authentication flow
- Add error handling to login function
- Add corresponding error states in auth context
- Update error display in login form
Uses: ` + "`src/auth/login.ts`" + `, ` + "`src/context/auth.tsx`" + `, ` + "`src/components/LoginForm.tsx`" + `
Example 4 - Good (tightly coupled file updates):
1. Rename UserType enum to AccountType
- Update enum definition
- Update all imports and usages
Uses: ` + "`src/types/user.ts`" + `, ` + "`src/auth/account.ts`" + `, ` + "`src/components/UserProfile.tsx`" + `
Notice in these examples:
- Tasks that are too granular waste responses on tiny changes
- Tasks that are too large mix unrelated changes and become hard to implement
- Good tasks group related changes that make sense to implement together
- Multiple files can be included when the changes are tightly coupled
- Bullet points describe steps in a cohesive change, not separate features
`
type ChatUserPromptParams struct {
CreatePromptParams
Prompt string
OsDetails string
}
func GetWrappedChatOnlyPrompt(params ChatUserPromptParams) string {
// Base wrapper that's always included
baseWrapper := "# The user's latest prompt:\n```\n%s\n```\n\n" + `Please respond according to the 'Your instructions' section above.
The current UTC timestamp is: %s
User's operating system details:
%s`
// Build additional instructions based on parameter combinations
var additionalInstructions string
// Execution mode handling
if params.ExecMode {
additionalInstructions += `
*Execution mode is enabled.*
- If you switch to tell mode, you can execute commands locally as needed
- While you remain in chat mode, you can discuss both file changes and command execution, but you cannot update files or execute commands (unless the user first switches to tell mode)
- Be specific about what commands would need to be run
- Consider build processes, testing, and deployment
- Distinguish between file changes and execution steps`
} else {
additionalInstructions += `
*Execution mode is disabled.*
- If you switch to tell mode, you cannot execute commands—keep this in mind when discussing the plan. If the plan requires commands to be run after switching to tell mode, the user would need to run them manually.
- You can discuss build/test/deploy conceptually, but you cannot execute commands either in chat mode or in tell mode
- Be clear when certain steps would need execution mode enabled`
}
additionalInstructions += `
Keep in mind:
- Stay conversational while being technically precise
- Reference and explain code when helpful, but don't output formal implementation blocks
- Focus on what's specifically asked - don't suggest extra features
- Consider existing codebase structure in your explanations
- When discussing libraries, focus on well-maintained, widely-used options
- If the user wants to implement changes, remind them about 'tell mode'
- Use error handling, logging, and security best practices in your suggestions
- Be thoughtful about code organization and structure
- Consider implications of suggested changes on the existing codebase
Remember you're in chat mode:
- Engage in natural technical discussion about code and context
- Help users understand their codebase and plan potential changes
- Provide explanations and answer questions thoroughly
- Include code snippets only when they help explain concepts
- Help debug issues by examining and explaining code
- Suggest approaches and discuss trade-offs
- Help evaluate different implementation strategies
- Consider and explain implications of different approaches
- Stay focused on understanding and planning rather than implementation
You cannot:
- Create or modify any files
- Output formal implementation code blocks
- Make plans using "### Tasks" sections
- Structure responses as if implementing changes
- Load context multiple times in consecutive responses
- Switch to implementation mode without user request
Even if a plan is in progress:
- Stay in discussion mode, don't attempt to implement anything
- You can discuss the current tasks and progress
- You can provide explanations and suggestions
- You can help debug issues or clarify approach
- But you must not output any implementation code
- Return to implementation only when user switches back to tell mode
Remember that users often:
- Switch between chat and tell mode during implementation
- Use chat mode to understand before implementing
- Need detailed technical discussion to plan effectively
- Want to explore options before committing to changes
- May need to debug or understand issues mid-implementation
- You may receive a list of tasks that are in progress, including a 'current subtask'. You MUST NOT implement any tasks—only discuss them.
`
promptWrapperFormatStr := baseWrapper + additionalInstructions
ts := time.Now().Format(time.RFC3339)
return fmt.Sprintf(promptWrapperFormatStr,
params.Prompt,
ts,
params.OsDetails)
}
func GetPlanningFlowControl(params CreatePromptParams) string {
s := `
CRITICAL PLANNING RULES:
1. For ANY update/revision to tasks:
`
if params.ExecMode {
s += `You MUST output a ### Commands section before the ### Tasks list. If you determine that commands should be added or updated in _apply.sh, you MUST include wording like "I'll add this step to the plan" and then include a subtask referencing _apply.sh in the ### Tasks list.`
}
s += `
- You MUST output a new/updated ### Tasks list
`
if params.ExecMode {
s += `
- If the ### Commands section indicates that commands should be added or updated in _apply.sh, you MUST also create a subtask referencing _apply.sh in the ### Tasks list
`
}
s += `
- You MUST NOT UNDER ANY CIRCUMSTANCES start implementing code, even if you have already made a plan in a previous response and are ready to implement it—you still ABSOLUTELY MUST NOT implement code at this stage. You MUST make a plan first in the format described above.
- You MUST follow planning phase format exactly
2. Even for small changes:
- Create/update task list first
- No implementation and NO CODE until planning is complete, and you have output a '### Tasks' section and a
- All changes must be in task list
3. The planning stage is *ALWAYS* required. You MUST NEVER skip ahead and start writing code in this response. You MUST complete the planning stage first and output a '### Tasks' section and a before you can start implementing code.
`
return s
}
// func GetFollowUpRequiredPrompt(params CreatePromptParams) string {
// s := `
// [MANDATORY FOLLOW-UP FLOW]
// CRITICAL FLOW CONTROL:
// 1. You MUST FIRST respond naturally to what the user has said/asked
// 2. Then classify the prompt as either:
// A. Update/revision to tasks (A1/A2/A3)
// B. Conversation prompt (question/comment)
// 3. IF classified as A (update/revision):
// - You MUST create/update the task list with ### Tasks
// - You MUST output immediately after the task list
// - You MUST end your response immediately after
// - You ABSOLUTELY MUST NOT proceed to implementation
// - You MUST follow planning format exactly
// Even if:
// - The change is small
// - You know the exact code to write
// - You're continuing an existing plan
// 4. IF classified as B (conversation):
// - Continue conversation naturally
// - Do not create tasks or implement code
// 5. After responding and classifying, output EXACTLY ONE of these statements (naturally incorporated):
// A. "I have the context I need to continue."
// B. "I have the context I need to respond."
// C. "I need more context to continue. "
// D. "I need more context to respond. "
// E. "This is a significant update to the plan. I'll clear all context without pending changes, then decide what context I need to move forward. "
// F. "This is a new task that is distinct from the plan. I'll clear all context without pending changes, then decide what context I need to move forward. "
// For statements A/B: You may rephrase naturally while keeping the meaning.
// For statements C/D: MUST include exact phrase "need more context" and .
// For statements E/F: MUST include exact phrase "clear all context" and .
// CRITICAL: Always respond naturally to the user first, then seamlessly incorporate the required statement. Do NOT state that you are performing a classification or context assessment.
// `
// return s
// }
================================================
FILE: app/server/model/summarize.go
================================================
package model
import (
"context"
"fmt"
"net/http"
"plandex-server/db"
"plandex-server/model/prompts"
"plandex-server/types"
"strings"
"time"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
type PlanSummaryParams struct {
Auth *types.ServerAuth
Plan *db.Plan
ModelStreamId string
ModelPackName string
Conversation []*types.ExtendedChatMessage
ConversationNumTokens int
LatestConvoMessageId string
LatestConvoMessageCreatedAt time.Time
NumMessages int
SessionId string
}
func PlanSummary(clients map[string]ClientInfo, authVars map[string]string, settings *shared.PlanSettings, orgUserConfig *shared.OrgUserConfig, config shared.ModelRoleConfig, params PlanSummaryParams, ctx context.Context) (*db.ConvoSummary, *shared.ApiError) {
messages := []types.ExtendedChatMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompts.Identity,
},
},
},
}
for _, message := range params.Conversation {
messages = append(messages, *message)
}
messages = append(messages, types.ExtendedChatMessage{
Role: openai.ChatMessageRoleUser,
Content: []types.ExtendedChatMessagePart{
{
Type: openai.ChatMessagePartTypeText,
Text: prompts.PlanSummary,
},
},
})
modelRes, err := ModelRequest(ctx, ModelRequestParams{
Clients: clients,
Auth: params.Auth,
AuthVars: authVars,
Plan: params.Plan,
ModelConfig: &config,
Purpose: "Conversation summary",
ConvoMessageId: params.LatestConvoMessageId,
ModelStreamId: params.ModelStreamId,
Messages: messages,
SessionId: params.SessionId,
Settings: settings,
OrgUserConfig: orgUserConfig,
})
if err != nil {
return nil, &shared.ApiError{
Type: shared.ApiErrorTypeOther,
Status: http.StatusInternalServerError,
Msg: fmt.Sprintf("error generating plan summary: %v", err),
}
}
summary := modelRes.Content
if !strings.HasPrefix(summary, "## Summary of the plan so far:") {
summary = "## Summary of the plan so far:\n\n" + summary
}
var tokens int
if modelRes.Usage != nil {
tokens = modelRes.Usage.CompletionTokens
}
return &db.ConvoSummary{
OrgId: params.Auth.OrgId,
PlanId: params.Plan.Id,
Summary: summary,
Tokens: tokens,
LatestConvoMessageId: params.LatestConvoMessageId,
LatestConvoMessageCreatedAt: params.LatestConvoMessageCreatedAt,
NumMessages: params.NumMessages,
}, nil
}
================================================
FILE: app/server/model/tokens.go
================================================
package model
import (
"plandex-server/types"
shared "plandex-shared"
"github.com/sashabaranov/go-openai"
)
const (
// Per OpenAI's documentation:
// Every message follows this format: {"role": "role_name", "content": "content"}
// which has a 4-token overhead per message
TokensPerMessage = 4
// System, user, or assistant - each role name costs 1 token
TokensPerName = 1
// Tokens per request
TokensPerRequest = 3
TokensPerExtendedPart = 6
)
func GetMessagesTokenEstimate(messages ...types.ExtendedChatMessage) int {
tokens := 0
for _, msg := range messages {
tokens += TokensPerMessage // Base message overhead
tokens += TokensPerName // Role name
if len(msg.Content) > 0 {
// For each extended part, we need to account for the JSON structure
// Each part follows format: {"type": "type_value", "text": "content"}
// or {"type": "type_value", "image_url": {"url": "url_value"}}
for _, part := range msg.Content {
if part.Type == openai.ChatMessagePartTypeText {
tokens += TokensPerExtendedPart // Overhead for the part object structure
tokens += shared.GetNumTokensEstimate(part.Text)
}
// images are handled separately
}
}
}
return tokens
}
================================================
FILE: app/server/notify/errors.go
================================================
package notify
import (
"log"
"runtime/debug"
)
// this allows Plandex Cloud to inject error monitoring
// all non-streaming handlers are already wrapped with different logic, so this is only needed for errors in streaming handlers
type Severity int
const (
SeverityInfo Severity = iota
SeverityError
)
var NotifyErrFn func(severity Severity, data ...interface{})
func RegisterNotifyErrFn(fn func(severity Severity, data ...interface{})) {
NotifyErrFn = fn
}
func NotifyErr(severity Severity, data ...interface{}) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in NotifyErr: %v\n%s", r, debug.Stack())
}
}()
if NotifyErrFn != nil {
NotifyErrFn(severity, data...)
}
}
================================================
FILE: app/server/routes/routes.go
================================================
package routes
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"plandex-server/handlers"
"plandex-server/hooks"
"github.com/gorilla/mux"
)
type PlandexHandler func(w http.ResponseWriter, r *http.Request)
type HandlePlandex func(router *mux.Router, path string, isStreaming bool, handler PlandexHandler) *mux.Route
var HandlePlandexFn HandlePlandex
func RegisterHandlePlandex(fn HandlePlandex) {
HandlePlandexFn = fn
}
func EnsureHandlePlandex() {
if HandlePlandexFn == nil {
panic("handlePlandexFn is not set")
}
}
func AddHealthRoutes(r *mux.Router) {
EnsureHandlePlandex()
HandlePlandexFn(r, "/health", false, func(w http.ResponseWriter, r *http.Request) {
_, apiErr := hooks.ExecHook(hooks.HealthCheck, hooks.HookParams{})
if apiErr != nil {
log.Printf("Error in health check hook: %v\n", apiErr)
http.Error(w, apiErr.Msg, apiErr.Status)
return
}
fmt.Fprint(w, "OK")
})
HandlePlandexFn(r, "/version", false, func(w http.ResponseWriter, r *http.Request) {
// Log the host
host := r.Host
log.Printf("Host header: %s", host)
execPath, err := os.Executable()
if err != nil {
log.Fatal("Error getting current directory: ", err)
}
currentDir := filepath.Dir(execPath)
// get version from version.txt
var path string
if os.Getenv("IS_CLOUD") != "" {
path = filepath.Join(currentDir, "..", "version.txt")
} else {
path = filepath.Join(currentDir, "version.txt")
}
bytes, err := os.ReadFile(path)
if err != nil {
http.Error(w, "Error getting version", http.StatusInternalServerError)
return
}
fmt.Fprint(w, string(bytes))
})
}
func AddApiRoutes(r *mux.Router) {
addApiRoutes(r, "")
}
func AddApiRoutesWithPrefix(r *mux.Router, prefix string) {
addApiRoutes(r, prefix)
}
func AddProxyableApiRoutes(r *mux.Router) {
addProxyableApiRoutes(r, "")
}
func AddProxyableApiRoutesWithPrefix(r *mux.Router, prefix string) {
addProxyableApiRoutes(r, prefix)
}
func addApiRoutes(r *mux.Router, prefix string) {
EnsureHandlePlandex()
HandlePlandexFn(r, prefix+"/accounts/email_verifications", false, handlers.CreateEmailVerificationHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/accounts/email_verifications/check_pin", false, handlers.CheckEmailPinHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/accounts/sign_in_codes", false, handlers.CreateSignInCodeHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/accounts/sign_in", false, handlers.SignInHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/accounts/sign_out", false, handlers.SignOutHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/accounts", false, handlers.CreateAccountHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/orgs/session", false, handlers.GetOrgSessionHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/orgs", false, handlers.ListOrgsHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/orgs", false, handlers.CreateOrgHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/users", false, handlers.ListUsersHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/orgs/users/{userId}", false, handlers.DeleteOrgUserHandler).Methods("DELETE")
HandlePlandexFn(r, prefix+"/orgs/roles", false, handlers.ListOrgRolesHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/invites", false, handlers.InviteUserHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/invites/pending", false, handlers.ListPendingInvitesHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/invites/accepted", false, handlers.ListAcceptedInvitesHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/invites/all", false, handlers.ListAllInvitesHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/invites/{inviteId}", false, handlers.DeleteInviteHandler).Methods("DELETE")
HandlePlandexFn(r, prefix+"/projects", false, handlers.CreateProjectHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/projects", false, handlers.ListProjectsHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/projects/{projectId}/set_plan", false, handlers.ProjectSetPlanHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/projects/{projectId}/rename", false, handlers.RenameProjectHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/projects/{projectId}/plans/current_branches", false, handlers.GetCurrentBranchByPlanIdHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans", false, handlers.ListPlansHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/archive", false, handlers.ListArchivedPlansHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/ps", false, handlers.ListPlansRunningHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/projects/{projectId}/plans", false, handlers.CreatePlanHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/projects/{projectId}/plans", false, handlers.DeleteAllPlansHandler).Methods("DELETE")
HandlePlandexFn(r, prefix+"/plans/{planId}", false, handlers.GetPlanHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}", false, handlers.DeletePlanHandler).Methods("DELETE")
HandlePlandexFn(r, prefix+"/plans/{planId}/current_plan/{sha}", false, handlers.CurrentPlanHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/current_plan", false, handlers.CurrentPlanHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/apply", false, handlers.ApplyPlanHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/archive", false, handlers.ArchivePlanHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/unarchive", false, handlers.UnarchivePlanHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/rename", false, handlers.RenamePlanHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/reject_all", false, handlers.RejectAllChangesHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/reject_file", false, handlers.RejectFileHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/reject_files", false, handlers.RejectFilesHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/diffs", false, handlers.GetPlanDiffsHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/context", false, handlers.ListContextHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/context", false, handlers.LoadContextHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/context/{contextId}/body", false, handlers.GetContextBodyHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/context", false, handlers.UpdateContextHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/context", false, handlers.DeleteContextHandler).Methods("DELETE")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/convo", false, handlers.ListConvoHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/rewind", false, handlers.RewindPlanHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/logs", false, handlers.ListLogsHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/branches", false, handlers.ListBranchesHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/branches/{branch}", false, handlers.DeleteBranchHandler).Methods("DELETE")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/branches", false, handlers.CreateBranchHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/settings", false, handlers.GetSettingsHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/settings", false, handlers.UpdateSettingsHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/status", false, handlers.GetPlanStatusHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/tell", true, handlers.TellPlanHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/build", true, handlers.BuildPlanHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/custom_models", false, handlers.ListCustomModelsHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/custom_models", false, handlers.UpsertCustomModelsHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/custom_models/{modelId}", false, handlers.GetCustomModelHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/custom_providers", false, handlers.ListCustomProvidersHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/custom_providers/{providerId}", false, handlers.GetCustomProviderHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/model_sets", false, handlers.ListModelPacksHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/model_sets", false, handlers.CreateModelPackHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/model_sets/{setId}", false, handlers.UpdateModelPackHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/default_settings", false, handlers.GetDefaultSettingsHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/default_settings", false, handlers.UpdateDefaultSettingsHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/file_map", false, handlers.GetFileMapHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/load_cached_file_map", false, handlers.LoadCachedFileMapHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans/{planId}/config", false, handlers.GetPlanConfigHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/plans/{planId}/config", false, handlers.UpdatePlanConfigHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/default_plan_config", false, handlers.GetDefaultPlanConfigHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/default_plan_config", false, handlers.UpdateDefaultPlanConfigHandler).Methods("PUT")
HandlePlandexFn(r, prefix+"/org_user_config", false, handlers.GetOrgUserConfigHandler).Methods("GET")
HandlePlandexFn(r, prefix+"/org_user_config", false, handlers.UpdateOrgUserConfigHandler).Methods("PUT")
}
func addProxyableApiRoutes(r *mux.Router, prefix string) {
EnsureHandlePlandex()
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/connect", true, handlers.ConnectPlanHandler).Methods("PATCH")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/stop", false, handlers.StopPlanHandler).Methods("DELETE")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/respond_missing_file", false, handlers.RespondMissingFileHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/auto_load_context", false, handlers.AutoLoadContextHandler).Methods("POST")
HandlePlandexFn(r, prefix+"/plans/{planId}/{branch}/build_status", false, handlers.GetBuildStatusHandler).Methods("GET")
}
================================================
FILE: app/server/setup/setup.go
================================================
package setup
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"plandex-server/db"
"plandex-server/host"
"plandex-server/model/plan"
"plandex-server/notify"
"plandex-server/shutdown"
"runtime/debug"
"syscall"
"time"
)
func MustLoadIp() {
err := host.LoadIp()
if err != nil {
log.Fatal("Error loading IP: ", err)
}
}
func MustInitDb() {
err := db.Connect()
if err != nil {
log.Fatal("Error initializing database: ", err)
}
err = db.MigrationsUp()
if err != nil {
log.Fatal("Error running migrations: ", err)
}
err = db.CacheOrgRoleIds()
if err != nil {
log.Fatal("Error caching org role ids: ", err)
}
}
var shutdownHooks []func()
func RegisterShutdownHook(hook func()) {
shutdownHooks = append(shutdownHooks, hook)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip logging for monitoring endpoints
if r.URL.Path == "/health" || r.URL.Path == "/version" {
next.ServeHTTP(w, r)
return
}
start := time.Now()
log.Printf("\n\nRequest: %s %s\n\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("\n\nCompleted: %s %s in %v\n\n", r.Method, r.URL.Path, time.Since(start))
})
}
func StartServer(handler http.Handler, configureFn func(handler http.Handler) http.Handler, afterStart func()) {
if os.Getenv("GOENV") == "development" {
log.Println("In development mode.")
}
shutdown.ShutdownCtx, shutdown.ShutdownCancel = context.WithCancel(context.Background())
defer shutdown.ShutdownCancel()
// Ensure database connection is closed
defer func() {
log.Println("Closing database connection...")
err := db.Conn.Close()
if err != nil {
log.Printf("Error closing database connection: %v", err)
}
log.Println("Database connection closed")
}()
// Get externalPort from the environment variable or default to 8099
externalPort := os.Getenv("PORT")
if externalPort == "" {
externalPort = "8099"
}
// Add logging middleware before the maxBytes middleware
handler = loggingMiddleware(handler)
// Apply the maxBytesMiddleware to limit request size to 1 GB
handler = maxBytesMiddleware(handler, 1000<<20) // 1 GB limit
if configureFn != nil {
handler = configureFn(handler)
}
server := &http.Server{
Addr: ":" + externalPort,
Handler: handler,
MaxHeaderBytes: 1 << 20, // 1 MB
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
log.Println("Started Plandex server on port " + externalPort)
if afterStart != nil {
afterStart()
}
// Capture SIGTERM and SIGINT signals
sigTermChan := make(chan os.Signal, 1)
signal.Notify(sigTermChan, syscall.SIGTERM, syscall.SIGINT)
sig := <-sigTermChan
log.Printf("Received signal %v, shutting down gracefully...\n", sig)
// Create a channel to track completion of active plans
plansDone := make(chan struct{})
// Start goroutine to monitor active plans
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in waitForActivePlans: %v\n%s", r, debug.Stack())
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in waitForActivePlans: %v\n%s", r, debug.Stack()))
}
close(plansDone)
}()
// First wait for active plans to complete or timeout
log.Println("Waiting for active plans to complete...")
activePlansCtx, cancel := context.WithTimeout(shutdown.ShutdownCtx, 60*time.Second)
defer cancel()
select {
case <-activePlansCtx.Done():
if activePlansCtx.Err() == context.DeadlineExceeded {
log.Println("Timeout waiting for active plans. Forcing shutdown.")
}
case <-waitForActivePlans():
log.Println("All active plans finished.")
}
// Then clean up any remaining locks
log.Println("Cleaning up any remaining locks...")
if err := db.CleanupActiveLocks(shutdown.ShutdownCtx); err != nil {
log.Printf("Error cleaning up locks: %v", err)
}
}()
// Wait for plans to finish or timeout
select {
case <-shutdown.ShutdownCtx.Done():
log.Println("Global shutdown timeout reached")
case <-plansDone:
log.Println("All cleanup tasks completed")
}
// Shutdown the HTTP server
log.Println("Shutting down http server...")
httpCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(httpCtx); err != nil {
log.Printf("Http server forced to shutdown: %v", err)
}
// Execute shutdown hooks
log.Println("Executing shutdown hooks...")
for _, hook := range shutdownHooks {
hook()
}
log.Println("Shutdown complete")
}
func waitForActivePlans() chan struct{} {
done := make(chan struct{})
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if plan.NumActivePlans() == 0 {
close(done)
return
}
}
}
}()
return done
}
func maxBytesMiddleware(next http.Handler, maxBytes int64) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// log the size of the request body
// log.Printf("Request body size: %d", r.ContentLength)
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
next.ServeHTTP(w, r)
})
}
================================================
FILE: app/server/shutdown/shutdown.go
================================================
package shutdown
import (
"context"
)
var ShutdownCtx context.Context
var ShutdownCancel context.CancelFunc
================================================
FILE: app/server/syntax/comments.go
================================================
package syntax
import shared "plandex-shared"
// // FindComments parses the given source code for the language implied by path
// // and returns a slice of all comment strings plus an IsRef field indicating
// // whether the comment is referencing original code (heuristic).
// func FindComments(ctx context.Context, parser *tree_sitter.Parser, source string) ([]Comment, error) {
// nodes, err := findCommentNodesForPath(ctx, parser, source)
// if err != nil {
// return nil, err
// }
// var comments []Comment
// for _, node := range nodes {
// start := node.StartByte()
// end := node.EndByte()
// raw := source[start:end]
// comments = append(comments, Comment{
// Txt: raw,
// IsRef: isRef(raw), // your existing logic
// })
// }
// return comments, nil
// }
// // StripComments removes all comments from the given source code using the appropriate parser
// func StripComments(ctx context.Context, parser *tree_sitter.Parser, source string) (string, error) {
// // Find the comment nodes first:
// commentNodes, err := findCommentNodesForPath(ctx, parser, source)
// if err != nil {
// // If parsing fails, return the source as-is along with an error.
// return source, fmt.Errorf("failed to parse the content: %v", err)
// }
// // If no parser is available or no comments found, just return the source unmodified.
// if len(commentNodes) == 0 {
// return source, nil
// }
// // Sort comment nodes in reverse order to remove them from the source safely.
// sort.Slice(commentNodes, func(i, j int) bool {
// return commentNodes[i].StartByte() > commentNodes[j].StartByte()
// })
// // Remove comments from the source.
// result := []byte(source)
// for _, node := range commentNodes {
// start := node.StartByte()
// end := node.EndByte()
// result = append(result[:start], result[end:]...)
// }
// return string(result), nil
// }
// func findCommentNodesForPath(ctx context.Context, parser *tree_sitter.Parser, source string) ([]*tree_sitter.Node, error) {
// if parser == nil {
// // If no parser is available for this file type, return empty.
// return nil, nil
// }
// // Use a context with timeout (from your existing parserTimeout).
// ctx, cancel := context.WithTimeout(ctx, parserTimeout)
// defer cancel()
// tree, err := parser.ParseCtx(ctx, nil, []byte(source))
// if err != nil {
// return nil, err
// }
// defer tree.Close()
// // Gather all comment nodes.
// root := tree.RootNode()
// commentNodes := findCommentNodes(root)
// return commentNodes, nil
// }
// func findCommentNodes(node *tree_sitter.Node) []*tree_sitter.Node {
// var commentNodes []*tree_sitter.Node
// visitNodes(node, func(n *tree_sitter.Node) {
// if n.Type() == "comment" {
// commentNodes = append(commentNodes, n)
// }
// })
// return commentNodes
// }
func GetCommentSymbols(lang shared.Language) (string, string) {
switch lang {
case shared.LanguageC, shared.LanguageCpp, shared.LanguageCsharp, shared.LanguageJava, shared.LanguageJavascript, shared.LanguageGo, shared.LanguageRust, shared.LanguageSwift, shared.LanguageKotlin, shared.LanguageGroovy, shared.LanguageScala, shared.LanguageTypescript, shared.LanguagePhp:
return "//", ""
case shared.LanguageBash, shared.LanguageDockerfile, shared.LanguageElixir, shared.LanguageHcl, shared.LanguagePython, shared.LanguageRuby, shared.LanguageToml, shared.LanguageYaml:
return "#", ""
case shared.LanguageLua, shared.LanguageElm:
return "--", ""
case shared.LanguageCss:
return "/*", "*/"
case shared.LanguageHtml:
return ""
case shared.LanguageOCaml:
return "(*", "*)"
case shared.LanguageSvelte, shared.LanguageJsx, shared.LanguageTsx, shared.LanguageJson:
return "", "" // comments are either not allowed or correct symbols depend on the context
}
return "", ""
}
================================================
FILE: app/server/syntax/file_map/cli/.gitignore
================================================
mapper
================================================
FILE: app/server/syntax/file_map/cli/go.mod
================================================
module mapper
go 1.23.3
replace plandex-server => ../../../
replace plandex-shared => ../../../../shared
replace plandex-cli => ../../../../cli
require (
plandex-cli v0.0.0-00010101000000-000000000000
plandex-server v0.0.0-00010101000000-000000000000
plandex-shared v0.0.0-00010101000000-000000000000
)
require (
github.com/Masterminds/semver v1.5.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alecthomas/chroma/v2 v2.18.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go v1.55.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/briandowns/spinner v1.23.2 // indirect
github.com/calmh/randomart v1.1.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.5 // indirect
github.com/charmbracelet/charm v0.8.7 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/glamour v0.10.0 // indirect
github.com/charmbracelet/glow v1.5.1 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250623112707-45752038d08d // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect
github.com/chromedp/chromedp v0.13.3 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cqroot/multichoose v0.1.1 // indirect
github.com/cqroot/prompt v0.9.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 // indirect
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang-migrate/migrate/v4 v4.18.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-tty v0.0.3 // indirect
github.com/meowgorithm/babyenv v1.3.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/gitcha v0.2.0 // indirect
github.com/muesli/go-app-paths v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/term v1.2.0-beta.2 // indirect
github.com/pkoukk/tiktoken-go v0.1.7 // indirect
github.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c // indirect
github.com/plandex-ai/survey/v2 v2.3.7 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/sashabaranov/go-openai v1.40.3 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.14.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.12 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
golang.org/x/image v0.28.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: app/server/syntax/file_map/cli/go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=
cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=
cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=
cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=
cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=
cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=
cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=
cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=
cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=
cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=
cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=
cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=
cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=
cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=
cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=
cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=
cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=
cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=
cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=
cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=
cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=
cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=
cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=
cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=
cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=
cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=
cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=
cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=
cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=
cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=
cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=
cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=
cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=
cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=
cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=
cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=
cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=
cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=
cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=
cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=
cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=
cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=
cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=
cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=
cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=
cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=
cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=
cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=
cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=
cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=
cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=
cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=
cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=
cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=
cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=
cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=
cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=
cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=
cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=
cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=
cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=
cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=
cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=
cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=
cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=
cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=
cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=
cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=
cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=
cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=
cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=
cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=
cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=
cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=
cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=
cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=
cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=
cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=
cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=
cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=
cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=
cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/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/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=
github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=
github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=
github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
github.com/calmh/randomart v1.1.0 h1:evl+iwc10LXtHdMZhzLxmsCQVmWnkXs44SbC6Uk0Il8=
github.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.7.5/go.mod h1:IRTORFvhEI6OUH7WhN2Ks8Z8miNGimk1BE6cmHijOkM=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
github.com/charmbracelet/bubbletea v1.3.0 h1:fPMyirm0u3Fou+flch7hlJN9krlnVURrkUVDwqXjoAc=
github.com/charmbracelet/bubbletea v1.3.0/go.mod h1:eTaHfqbIwvBhFQM/nlT1NsGc4kp8jhF8LfUK67XiTDM=
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
github.com/charmbracelet/charm v0.8.7 h1:FJ9b7IxWUWHOPR72zS/QJLEqtudOB2Mwfc+Sir0eZR8=
github.com/charmbracelet/charm v0.8.7/go.mod h1:ApJYwJljEjODkOYJgFDzbUqztLrCWQct9zyPD+xcVr4=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/glow v1.5.1 h1:o1mwT4xXXpkfUhJG6euQayNxLZf9yKctOCNHLztrwdE=
github.com/charmbracelet/glow v1.5.1/go.mod h1:rGgop0a2/4gXWiAxUW1iEQseoE+9Ctpb7M4sM9cY9CU=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
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/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.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/slice v0.0.0-20250623112707-45752038d08d h1:U/S+aA/k7pkJdYkBUhbmMOZXszU19WmauJ4bXe+7zRc=
github.com/charmbracelet/x/exp/slice v0.0.0-20250623112707-45752038d08d/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0=
github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cqroot/multichoose v0.1.1 h1:diGuKYKea9ePOTwUyUDor9zKRqKFWXGkYGqUa9+firU=
github.com/cqroot/multichoose v0.1.1/go.mod h1:BJzIGqbQZNADPDuA3IzhmTMpRc2F3fZKysMRYP+Ydw8=
github.com/cqroot/prompt v0.9.3 h1:00Sjiasl1QL7ttEphJ+1xAl0fKQi+7s2F3aY0x7wnz4=
github.com/cqroot/prompt v0.9.3/go.mod h1:NZvCTeuvR9ew9Hkk7xlrZ9xdVH4AmkO9R0eeBkzOHXQ=
github.com/cqroot/prompt v0.9.4 h1:uFRlhXuOP3CSD+Pii0Z8VJhgXpavSloFf7/KAERwjz8=
github.com/cqroot/prompt v0.9.4/go.mod h1:6BVZiEv7XkW1K64y1k2wdzToDwspL3n/RkUIyPjQ808=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 h1:ygs9POGDQpQGLJPlq4+0LBUmMBNox1N4JSpw+OETcvI=
github.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY=
github.com/hashicorp/consul/sdk v0.11.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/meowgorithm/babyenv v1.3.0/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=
github.com/meowgorithm/babyenv v1.3.1 h1:18ZEYIgbzoFQfRLF9+lxjRfk/ui6w8U0FWl07CgWvvc=
github.com/meowgorithm/babyenv v1.3.1/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/gitcha v0.2.0 h1:+wOgT2dI9s2Tznj1t1rb/qkK5e0cb6qD8c4IX2TR/YY=
github.com/muesli/gitcha v0.2.0/go.mod h1:Ri8m9TZS4+ORG4JVmVKUQcWZuxDvUW3UKxMdQfzG2zI=
github.com/muesli/go-app-paths v0.2.1/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI=
github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a h1:Hw/15RYEOUD6T9UCRkUmNBa33kJkH33Fui6hE4sRLKU=
github.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a/go.mod h1:+XG0ne5zXWBTSbbe7Z3/RWxaT8PZY6zaZ1dX6KjprYY=
github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=
github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c h1:bki/wkg5iFBOv3jCPUDNuH5yLngUPUdEJCSuvc2tiQ0=
github.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c/go.mod h1:SqEsJfsIr0GYUyLatvezDOBe6XsCw64E7v33QzeH5PM=
github.com/plandex-ai/survey/v2 v2.3.7 h1:u1o6bflbaBpW8i8krm+91Z2cOcvZcMVS+AjV+rgR8Rk=
github.com/plandex-ai/survey/v2 v2.3.7/go.mod h1:RiBOKRDB5fOQrOzsiAPAN57hYqFKPkCxgSK7twcDOys=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
github.com/sagikazarmark/crypt v0.8.0/go.mod h1:TmKwZAo97S4Fy4sfMH/HX/cQP5D+ijra2NyLpNNmttY=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sashabaranov/go-openai v1.40.0 h1:Peg9Iag5mUJtPW00aYatlsn97YML0iNULiLNe74iPrU=
github.com/sashabaranov/go-openai v1.40.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.40.3 h1:PkOw0SK34wrvYVOuXF1HZzuTBRh992qRZHil4kG3eYE=
github.com/sashabaranov/go-openai v1.40.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4=
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4=
go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/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-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/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-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
================================================
FILE: app/server/syntax/file_map/cli/main.go
================================================
package main
import (
"context"
"fmt"
"os"
"plandex-server/syntax/file_map"
"sync"
"time"
shared "plandex-shared"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
args := os.Args[1:]
if len(args) < 1 {
fmt.Println("usage: mapper [files-or-dirs...]")
os.Exit(1)
}
var parserTree bool = false
for i, arg := range args {
if arg == "--trees" {
parserTree = true
args = append(args[:i], args[i+1:]...)
break
}
}
paths := args
var filteredPaths []string
for _, path := range paths {
if shared.HasFileMapSupport(path) {
filteredPaths = append(filteredPaths, path)
}
}
errCh := make(chan error, len(filteredPaths))
fileInputs := map[string]string{}
var mu sync.Mutex
for _, path := range filteredPaths {
go func(path string) {
content, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("error reading file: %v", err)
return
}
mu.Lock()
fileInputs[path] = string(content)
mu.Unlock()
errCh <- nil
}(path)
}
for i := 0; i < len(filteredPaths); i++ {
err := <-errCh
if err != nil {
fmt.Printf("error reading file: %v\n", err)
os.Exit(1)
}
}
if parserTree {
trees, err := file_map.ProcessMapTrees(ctx, fileInputs)
if err != nil {
fmt.Printf("error processing map files: %v\n", err)
os.Exit(1)
}
fmt.Println(trees.CombinedTrees())
} else {
mapBodies, err := file_map.ProcessMapFiles(ctx, fileInputs)
if err != nil {
fmt.Printf("error processing map files: %v\n", err)
os.Exit(1)
}
fmt.Println(mapBodies.CombinedMap(map[string]int{}))
}
}
================================================
FILE: app/server/syntax/file_map/examples/bash_example.sh
================================================
#!/bin/bash
# Global variables
GLOBAL_VAR="Hello World"
readonly CONSTANT_VAR="This is constant"
# Function definition
function print_message() {
local message="$1"
echo "$message"
}
# Function with return value
get_date() {
echo $(date +%Y-%m-%d)
}
# Array declaration
declare -a fruits=("apple" "banana" "orange")
# Associative array
declare -A user_info=(
["name"]="John"
["age"]="30"
["city"]="New York"
)
# Main script execution
main() {
print_message "$GLOBAL_VAR"
current_date=$(get_date)
echo "Today is: $current_date"
# Loop through array
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
# Access associative array
echo "User ${user_info[name]} is ${user_info[age]} years old"
}
# Call main function
main
================================================
FILE: app/server/syntax/file_map/examples/c_example.c
================================================
#include
#include
#include
// Macro definitions
#define MAX_SIZE 100
#define SQUARE(x) ((x) * (x))
// Type definitions
typedef struct {
char name[50];
int age;
} Person;
typedef enum {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY
} Weekday;
// Global variables
static const double PI = 3.14159;
int globalCounter = 0;
// Function declarations
void printPerson(const Person* p);
int factorial(int n);
// Union example
union Data {
int i;
float f;
char str[20];
};
// Function pointer type
typedef int (*Operation)(int, int);
// Function implementations
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
void printPerson(const Person* p) {
printf("Name: %s, Age: %d\n", p->name, p->age);
}
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// Main function
int main() {
// Local variable declarations
Person person = {"John Doe", 30};
union Data data;
Operation op = add;
// Using structs
printPerson(&person);
// Using unions
data.i = 10;
printf("data.i: %d\n", data.i);
// Using function pointers
printf("10 + 20 = %d\n", op(10, 20));
op = subtract;
printf("10 - 20 = %d\n", op(10, 20));
// Using macros
printf("Square of 5 is %d\n", SQUARE(5));
// Using enums
Weekday today = WEDNESDAY;
printf("Day number: %d\n", today);
// Using global variables
globalCounter++;
printf("Global counter: %d\n", globalCounter);
return 0;
}
================================================
FILE: app/server/syntax/file_map/examples/cpp_example.cpp
================================================
#include
#include
#include
#include
#include
// Global variables at file scope
int globalInteger = 42;
const double PI = 3.14159;
static std::string globalString = "Global static string";
// Global enum and constant
enum Color { RED, GREEN, BLUE };
constexpr int MAX_SIZE = 100;
// Template class
template
class Container {
public:
static T defaultValue; // Static class member
const T maxCapacity = MAX_SIZE; // Class constant
void add(T item) { items.push_back(item); }
const std::vector& getItems() const { return items; }
private:
std::vector items;
};
// Static member definition
template
T Container::defaultValue = T();
// Abstract base class
class Animal {
public:
virtual ~Animal() = default;
virtual void makeSound() const = 0;
protected:
std::string name;
};
// Derived class with virtual inheritance
class Dog : virtual public Animal {
public:
Dog(const std::string& dogName) { name = dogName; }
void makeSound() const override {
std::cout << name << " says: Woof!" << std::endl;
}
};
// Namespace example
namespace Utils {
// Namespace-level variables
inline int counter = 0;
const std::string VERSION = "1.0.0";
// Function template
template
T max(T a, T b) {
return (a > b) ? a : b;
}
// Lambda function stored in variable
const auto printer = [](const std::string& msg) {
std::cout << "Message: " << msg << std::endl;
};
}
// Smart pointer and move semantics example
class Resource {
public:
Resource(const std::string& data) : data_(data) {
std::cout << "Resource constructed" << std::endl;
}
~Resource() {
std::cout << "Resource destroyed" << std::endl;
}
Resource(Resource&& other) noexcept : data_(std::move(other.data_)) {}
std::string getData() const { return data_; }
private:
std::string data_;
};
// Static member variable and function
class Counter {
public:
static int getCount() { return count; }
Counter() { ++count; }
~Counter() { --count; }
private:
static int count;
};
int Counter::count = 0;
// Friend function example
class Box {
friend std::ostream& operator<<(std::ostream& os, const Box& box);
public:
Box(int w, int h) : width(w), height(h) {}
private:
int width;
int height;
};
std::ostream& operator<<(std::ostream& os, const Box& box) {
return os << "Box(" << box.width << "x" << box.height << ")";
}
// Main function
int main() {
// Smart pointer usage
auto resource = std::make_unique("Hello");
std::cout << resource->getData() << std::endl;
// Template class usage
Container numbers;
numbers.add(1);
numbers.add(2);
// Polymorphism
std::unique_ptr dog = std::make_unique("Rex");
dog->makeSound();
// Template function
std::cout << "Max: " << Utils::max(10, 20) << std::endl;
// Lambda function
Utils::printer("Hello from lambda!");
// Counter static example
Counter c1, c2;
std::cout << "Count: " << Counter::getCount() << std::endl;
// Friend function
Box box(10, 20);
std::cout << box << std::endl;
return 0;
}
================================================
FILE: app/server/syntax/file_map/examples/csharp_example.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
// Namespace declaration
namespace ExampleApp
{
// Interface definition
public interface IProcessor
{
Task ProcessAsync(T input);
bool Validate(T input);
}
// Enum definition
public enum Status
{
Pending,
Active,
Completed,
Failed
}
// Delegate declaration
public delegate void StatusChangedEventHandler(Status oldStatus, Status newStatus);
// Generic class implementing interface
public class DataProcessor : IProcessor where T : class
{
// Event declaration
public event StatusChangedEventHandler StatusChanged;
// Auto-implemented property
public Status CurrentStatus { get; private set; }
// Static field
private static readonly Dictionary _processedItems = new();
// Constructor
public DataProcessor()
{
CurrentStatus = Status.Pending;
}
// Async method implementation
public async Task ProcessAsync(T input)
{
var oldStatus = CurrentStatus;
CurrentStatus = Status.Active;
OnStatusChanged(oldStatus, CurrentStatus);
await Task.Delay(100); // Simulate work
if (_processedItems.ContainsKey(typeof(T)))
_processedItems[typeof(T)]++;
else
_processedItems[typeof(T)] = 1;
CurrentStatus = Status.Completed;
OnStatusChanged(Status.Active, CurrentStatus);
return input;
}
// Interface method implementation
public bool Validate(T input) => input != null;
// Protected virtual method
protected virtual void OnStatusChanged(Status oldStatus, Status newStatus)
{
StatusChanged?.Invoke(oldStatus, newStatus);
}
// Static method
public static int GetProcessedCount() where TItem : class
{
return _processedItems.GetValueOrDefault(typeof(TItem));
}
}
// Record type (C# 9.0+)
public record Person(string Name, int Age)
{
// Property with validation
public string Email { get; init; } = string.Empty;
}
// Extension method
public static class StringExtensions
{
public static int WordCount(this string str)
{
return str.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
// Main program class
public class Program
{
public static async Task Main(string[] args)
{
var processor = new DataProcessor();
processor.StatusChanged += (old, @new) =>
Console.WriteLine($"Status changed from {old} to {@new}");
var person = new Person("John Doe", 30) { Email = "john@example.com" };
if (processor.Validate(person))
{
var result = await processor.ProcessAsync(person);
Console.WriteLine($"Processed person: {result.Name}");
Console.WriteLine($"Word count in name: {result.Name.WordCount()}");
}
Console.WriteLine($"Total processed persons: {DataProcessor.GetProcessedCount()}");
}
}
}
================================================
FILE: app/server/syntax/file_map/examples/css_example.css
================================================
/* Global variables */
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--text-color: #2c3e50;
--spacing-unit: 1rem;
--border-radius: 4px;
}
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
color: var(--text-color);
}
/* Layout containers */
.container {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-unit);
}
/* Grid system */
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--spacing-unit);
}
/* Flexbox components */
.flex-container {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Component styles */
.button {
padding: calc(var(--spacing-unit) / 2) var(--spacing-unit);
border-radius: var(--border-radius);
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
.button:hover {
opacity: 0.9;
}
.button--primary {
background-color: var(--primary-color);
color: white;
}
.button--secondary {
background-color: var(--secondary-color);
color: white;
}
/* Card component */
.card {
border-radius: var(--border-radius);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: var(--spacing-unit);
}
/* Media queries */
@media screen and (max-width: 768px) {
.grid {
grid-template-columns: repeat(6, 1fr);
}
}
@media screen and (max-width: 480px) {
.grid {
grid-template-columns: 1fr;
}
.flex-container {
flex-direction: column;
gap: var(--spacing-unit);
}
}
/* Animation keyframes */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
/* Form styles */
.form-group {
margin-bottom: var(--spacing-unit);
}
.form-input {
width: 100%;
padding: calc(var(--spacing-unit) / 2);
border: 1px solid #ddd;
border-radius: var(--border-radius);
}
/* Navigation */
.nav {
background-color: var(--primary-color);
padding: var(--spacing-unit);
}
.nav__list {
list-style: none;
display: flex;
gap: var(--spacing-unit);
}
.nav__link {
color: white;
text-decoration: none;
}
/* Utility classes */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-left { text-align: left; }
.mt-1 { margin-top: var(--spacing-unit); }
.mb-1 { margin-bottom: var(--spacing-unit); }
.ml-1 { margin-left: var(--spacing-unit); }
.mr-1 { margin-right: var(--spacing-unit); }
================================================
FILE: app/server/syntax/file_map/examples/cue_example.cue
================================================
// Package definition
package example
// Import statements
import (
"strings"
"time"
)
// Schema definitions
#Person: {
name: string
age: int & >=0 & <=120
email?: string & =~"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
address?: #Address
}
#Address: {
street: string
city: string
country: string
zip: string & =~"^[0-9]{5}$"
}
// Default values and constraints
#DefaultPerson: #Person & {
name: string | *"John Doe"
age: int | *30
}
// List type definition
#Team: {
name: string
members: [...#Person]
leader: #Person
}
// Computed fields
#Employee: #Person & {
role: string
salary: float
taxRate: float
// Computed field
netSalary: salary * (1 - taxRate)
}
// Concrete values
exampleTeam: #Team & {
name: "Engineering"
members: [
{
name: "Alice Smith"
age: 28
email: "alice@example.com"
},
{
name: "Bob Jones"
age: 35
email: "bob@example.com"
}
]
leader: {
name: "Carol Wilson"
age: 40
email: "carol@example.com"
}
}
// Configuration with references and templates
#Config: {
environment: "development" | "staging" | "production"
database: {
host: string
port: int & >0 & <65536
username: string
password: string
}
features: [string]: bool
}
// Template usage
productionConfig: #Config & {
environment: "production"
database: {
host: "db.example.com"
port: 5432
username: "admin"
password: "secret"
}
features: {
"feature1": true
"feature2": false
}
}
================================================
FILE: app/server/syntax/file_map/examples/dockerfile_example
================================================
# Multi-stage build example
FROM golang:1.21-alpine AS builder
# Build arguments
ARG VERSION=1.0.0
ARG BUILD_DATE
# Set working directory
WORKDIR /app
# Copy only necessary files for dependency resolution
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X main.Version=${VERSION} -X main.BuildDate=${BUILD_DATE}" -o /app/server
# Create final lightweight image
FROM alpine:latest
# Labels for metadata
LABEL maintainer="example@example.com" \
version="${VERSION}" \
description="Example Dockerfile with various syntax elements"
# Environment variables
ENV APP_ENV=production \
PORT=8080
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Install runtime dependencies
RUN apk add --no-cache \
ca-certificates \
tzdata
# Set working directory
WORKDIR /app
# Copy binary from builder stage
COPY --from=builder /app/server .
# Copy configuration files
COPY config/production.yaml /etc/app/config.yaml
# Create volume mount points
VOLUME ["/data", "/logs"]
# Expose ports
EXPOSE 8080 8443
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --quiet --tries=1 --spider http://localhost:${PORT}/health || exit 1
# Set entry point and default command
ENTRYPOINT ["/app/server"]
CMD ["--config", "/etc/app/config.yaml"]
================================================
FILE: app/server/syntax/file_map/examples/elixir_example.ex
================================================
defmodule ExampleApp do
# Module attributes
@default_timeout 5000
@version "1.0.0"
# Protocol definition
defprotocol Formatter do
@doc "Format the data for display"
def format(data)
end
# Protocol implementation
defimpl Formatter, for: Map do
def format(data) do
inspect(data, pretty: true)
end
end
# Struct definition
defstruct name: "", age: 0, email: nil
# Exception definition
defexception message: "A custom error occurred"
# Callback definition
@callback process(term) :: {:ok, term} | {:error, term}
@macrocallback validate(term) :: Macro.t()
# Public function
def calculate_age(birth_year) when is_integer(birth_year) do
current_year = DateTime.utc_now().year
current_year - birth_year
end
# Private function
defp validate_email(email) do
String.match?(email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/)
end
# Function with pattern matching
def handle_result({:ok, value}), do: "Success: #{value}"
def handle_result({:error, reason}), do: "Error: #{reason}"
def handle_result(_), do: "Unknown result"
# Macro definition
defmacro debug(expression) do
quote do
IO.puts "Debug: #{inspect(unquote(expression))}"
end
end
# Private macro
defmacrop log(message) do
quote do
IO.puts("[#{__MODULE__}] #{unquote(message)}")
end
end
# Guard definition
defguard is_positive(value) when is_integer(value) and value > 0
defguardp is_even(value) when is_integer(value) and rem(value, 2) == 0
# Function delegation
defdelegate parse_int(string), to: String, as: :to_integer
# Overridable function
defoverridable [process: 1]
# Using with for complex operations
def create_user(params) do
with {:ok, name} <- Map.fetch(params, "name"),
{:ok, email} <- Map.fetch(params, "email"),
true <- validate_email(email) do
%ExampleApp{name: name, email: email}
else
:error -> {:error, "Missing required fields"}
false -> {:error, "Invalid email format"}
end
end
end
================================================
FILE: app/server/syntax/file_map/examples/elm_example.elm
================================================
module Example exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
-- MODEL
type alias Model =
{ users : List User
, inputText : String
, error : Maybe String
}
type alias User =
{ id : Int
, name : String
, email : String
}
init : () -> (Model, Cmd Msg)
init _ =
( { users = []
, inputText = ""
, error = Nothing
}
, fetchUsers
)
-- UPDATE
type Msg
= GotUsers (Result Http.Error (List User))
| InputChanged String
| AddUser
| UserAdded (Result Http.Error User)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotUsers result ->
case result of
Ok users ->
( { model | users = users, error = Nothing }
, Cmd.none
)
Err _ ->
( { model | error = Just "Failed to fetch users" }
, Cmd.none
)
InputChanged text ->
( { model | inputText = text }
, Cmd.none
)
AddUser ->
( model
, addUser model.inputText
)
UserAdded result ->
case result of
Ok user ->
( { model
| users = user :: model.users
, inputText = ""
, error = Nothing
}
, Cmd.none
)
Err _ ->
( { model | error = Just "Failed to add user" }
, Cmd.none
)
-- HTTP
fetchUsers : Cmd Msg
fetchUsers =
Http.get
{ url = "/api/users"
, expect = Http.expectJson GotUsers usersDecoder
}
addUser : String -> Cmd Msg
addUser name =
Http.post
{ url = "/api/users"
, body = Http.jsonBody (userEncoder name)
, expect = Http.expectJson UserAdded userDecoder
}
-- JSON
userEncoder : String -> Encode.Value
userEncoder name =
Encode.object
[ ("name", Encode.string name)
]
userDecoder : Decoder User
userDecoder =
Decode.map3 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
(Decode.field "email" Decode.string)
usersDecoder : Decoder (List User)
usersDecoder =
Decode.list userDecoder
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h1 [] [ text "User Management" ]
, viewError model.error
, viewInput model.inputText
, viewUsers model.users
]
viewError : Maybe String -> Html Msg
viewError maybeError =
case maybeError of
Just error ->
div [ class "error" ] [ text error ]
Nothing ->
text ""
viewInput : String -> Html Msg
viewInput inputText =
div []
[ input
[ value inputText
, onInput InputChanged
, placeholder "Enter user name"
] []
, button [ onClick AddUser ] [ text "Add User" ]
]
viewUsers : List User -> Html Msg
viewUsers users =
div []
[ h2 [] [ text "Users" ]
, ul [] (List.map viewUser users)
]
viewUser : User -> Html Msg
viewUser user =
li []
[ text (user.name ++ " (" ++ user.email ++ ")")
]
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
================================================
FILE: app/server/syntax/file_map/examples/go_example.go
================================================
//go:build ignore
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
)
// Interface definition
type DataProcessor interface {
Process(ctx context.Context, data interface{}) error
Validate(data interface{}) bool
}
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}
// Struct with embedded type and tags
type User struct {
sync.Mutex
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// Type alias and constants
type UserID = int64
const (
MaxRetries = 3
DefaultLimit = 100
)
// single line const
const singleLineConst string = "single line const"
// Global variables
var (
defaultTimeout = time.Second * 30
processor DataProcessor
)
// single line var
var singleLineVar string = "single line var"
// Generic type
type Result[T any] struct {
Data T
Error error
Retries int
}
// Implementation of DataProcessor
type UserProcessor struct {
cache map[UserID]*User
mu sync.RWMutex
}
func NewUserProcessor() *UserProcessor {
return &UserProcessor{
cache: make(map[UserID]*User),
}
}
func (p *UserProcessor) Process(ctx context.Context, data interface{}) error {
user, ok := data.(*User)
if !ok {
return fmt.Errorf("invalid data type: expected *User")
}
p.mu.Lock()
defer p.mu.Unlock()
p.cache[user.ID] = user
return nil
}
func (p *UserProcessor) Validate(data interface{}) bool {
user, ok := data.(*User)
return ok && user.Name != "" && user.Email != ""
}
// Channel operations
func processUsers(ctx context.Context, users <-chan *User) <-chan *Result[*User] {
results := make(chan *Result[*User])
go func() {
defer close(results)
for user := range users {
select {
case <-ctx.Done():
return
case results <- &Result[*User]{Data: user}:
}
}
}()
return results
}
// Function with multiple return values and named returns
func createUser(name, email string) (user *User, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
user = &User{
Name: name,
Email: email,
CreatedAt: time.Now(),
}
if !processor.Validate(user) {
return nil, &ValidationError{Field: "user", Message: "invalid user data"}
}
return user, nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
processor = NewUserProcessor()
users := make(chan *User)
results := processUsers(ctx, users)
// Anonymous struct
config := struct {
Workers int
Buffer int
}{
Workers: 3,
Buffer: 10,
}
var wg sync.WaitGroup
wg.Add(config.Workers)
for i := 0; i < config.Workers; i++ {
go func(id int) {
defer wg.Done()
for result := range results {
if result.Error != nil {
log.Printf("Worker %d: Error processing user: %v", id, result.Error)
continue
}
log.Printf("Worker %d: Processed user: %+v", id, result.Data)
}
}(i)
}
// Cleanup
close(users)
wg.Wait()
}
================================================
FILE: app/server/syntax/file_map/examples/groovy_example.groovy
================================================
#!/usr/bin/env groovy
// Trait definition
trait Loggable {
def log(String message) {
println "[${new Date()}] $message"
}
}
// Abstract class definition
abstract class Vehicle {
String make
String model
Integer year
abstract void start()
abstract void stop()
}
// Class implementation - fixed inheritance syntax
class Car extends Vehicle implements Loggable {
// Properties with type definitions
private BigDecimal price
protected Boolean running = false
// Static fields with proper type declarations
static final String MANUFACTURER = "Generic Motors"
static int carCount = 0
// Constructor - fixed parameter initialization
Car(String make = 'Unknown', String model = 'Generic', Integer year = 2024) {
this.make = make
this.model = model
this.year = year
carCount++
}
// Lazy property evaluation
@Lazy String fullName = "$make $model ($year)"
// Method implementation with synchronized block
void start() {
running = true
log("Starting ${fullName}")
synchronized(this) {
println("Engine started")
}
}
void stop() {
running = false
log("Stopping ${fullName}")
}
// Operator overloading - fixed map construction
def plus(Car other) {
return new Car(
make: "${this.make}-${other.make}",
model: "${this.model}-${other.model}",
year: Math.max(this.year, other.year)
)
}
// Property accessors
private BigDecimal _price
void setPrice(BigDecimal price) {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative")
}
this._price = price
}
BigDecimal getPrice() {
return _price
}
}
// Enum definition
enum Status {
ACTIVE('A'),
INACTIVE('I'),
PENDING('P')
final String code
private Status(String code) {
this.code = code
}
@Override
String toString() {
return code
}
}
// Category class with explicit return
class StringExtensions {
static String truncate(String self, Integer length) {
return self.size() <= length ? self : self[0..HTML Syntax Example
Welcome to Our Site
This is an example of semantic HTML structure.
Main Article
Published on
Section One
This section demonstrates text content with emphasis and strong importance.
An example image with caption
Interactive Elements
Data Table
Monthly Sales Data
Month
Sales
Growth
January
$10,000
5%
February
$12,000
20%
Total
$22,000
25%
================================================
FILE: app/server/syntax/file_map/examples/java_example.java
================================================
package example;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
import java.time.*;
// Generic interface with type bounds
interface DataProcessor> {
CompletableFuture processAsync(T input);
boolean validate(T input);
}
// Enum with methods and fields
enum Status {
PENDING("P"),
ACTIVE("A"),
COMPLETED("C"),
FAILED("F");
private final String code;
Status(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
// Abstract class with generic type
abstract class BaseEntity {
protected ID id;
protected LocalDateTime createdAt;
protected LocalDateTime updatedAt;
public abstract void validate();
}
// Record type (Java 16+)
record UserDTO(
String name,
String email,
Set roles
) {}
// Annotation definition
@interface Audited {
String value() default "";
boolean required() default true;
}
// Main class with various Java features
public class Example extends BaseEntity implements DataProcessor {
// Static fields and initialization block
private static final int MAX_RETRIES = 3;
private static final Map CACHE;
static {
CACHE = new ConcurrentHashMap<>();
}
// Instance fields with different access modifiers
private final Queue queue;
protected Status status;
@Audited
public String name;
// Constructor with builder pattern
private Example(Builder builder) {
this.queue = new LinkedBlockingQueue<>();
this.status = Status.PENDING;
this.name = builder.name;
}
// Builder static class
public static class Builder {
private String name;
public Builder name(String name) {
this.name = name;
return this;
}
public Example build() {
return new Example(this);
}
}
// Interface implementation
@Override
public CompletableFuture processAsync(String input) {
return CompletableFuture.supplyAsync(() -> {
try {
queue.offer(input);
return input.toUpperCase();
} catch (Exception e) {
throw new CompletionException(e);
}
});
}
@Override
public boolean validate(String input) {
return input != null && !input.isEmpty();
}
// Abstract method implementation
@Override
public void validate() {
if (name == null || name.isEmpty()) {
throw new IllegalStateException("Name is required");
}
}
// Generic method with wildcards
public > List sort(Collection items) {
return items.stream()
.sorted()
.collect(Collectors.toList());
}
// Method with functional interfaces
public void processItems(
List items,
Predicate filter,
Consumer processor
) {
items.stream()
.filter(filter)
.forEach(processor);
}
// Exception class
public static class ProcessingException extends RuntimeException {
public ProcessingException(String message) {
super(message);
}
}
// Main method demonstrating usage
public static void main(String[] args) {
var example = new Builder()
.name("Test Example")
.build();
// Lambda and method reference usage
List items = Arrays.asList("a", "b", "c");
example.processItems(
items,
String::isEmpty,
System.out::println
);
// Stream API usage
Map statusCounts = items.stream()
.map(s -> Status.PENDING)
.collect(Collectors.groupingBy(
status -> status,
Collectors.counting()
));
// CompletableFuture with exception handling
example.processAsync("test")
.thenApply(String::toLowerCase)
.exceptionally(throwable -> {
System.err.println("Error: " + throwable.getMessage());
return "";
});
// Try-with-resources and Optional usage
try (var scanner = new Scanner(System.in)) {
Optional.of(scanner.nextLine())
.filter(example::validate)
.ifPresent(example::processAsync);
}
}
}
================================================
FILE: app/server/syntax/file_map/examples/javascript_example.js
================================================
// ES Module imports
import { EventEmitter } from 'events';
import { promisify } from 'util';
// Global constants
const MAX_RETRIES = 3;
const DEFAULT_TIMEOUT = 5000;
// Symbol for private properties
const privateState = Symbol('privateState');
// Class using ES6+ features
class DataProcessor extends EventEmitter {
// Private class field
#cache = new Map();
// Static class field
static version = '1.0.0';
// Constructor with parameter destructuring
constructor({ maxRetries = MAX_RETRIES, timeout = DEFAULT_TIMEOUT } = {}) {
super();
this[privateState] = { maxRetries, timeout };
}
// Async method with error handling
async processData(data) {
try {
const result = await this.#validateAndTransform(data);
this.emit('processed', result);
return result;
} catch (error) {
this.emit('error', error);
throw error;
}
}
// Private method
async #validateAndTransform(data) {
if (!data) throw new Error('Data is required');
return { ...data, timestamp: Date.now() };
}
// Generator method
*iterateCache() {
for (const [key, value] of this.#cache) {
yield { key, value };
}
}
}
// Decorator function (stage 3 proposal)
function deprecated(target, context) {
if (context.kind === 'method') {
const originalMethod = target;
return function(...args) {
console.warn(`Warning: ${context.name} is deprecated`);
return originalMethod.apply(this, args);
};
}
}
// Proxy example
const handler = {
get(target, prop) {
return prop in target ? target[prop] : 'Property not found';
}
};
const proxy = new Proxy({}, handler);
// Promise-based utility function
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// Async generator function
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await delay(100);
yield i;
}
}
// Higher-order function
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
};
// Custom error class
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// Object with getter/setter
const config = {
_theme: 'light',
get theme() {
return this._theme;
},
set theme(value) {
if (!['light', 'dark'].includes(value)) {
throw new ValidationError('Invalid theme', 'theme');
}
this._theme = value;
}
};
// Array methods and destructuring
const processItems = (items) => {
const [first, ...rest] = items;
return rest
.filter(item => item != null)
.map(item => ({ ...item, processed: true }))
.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
};
// Async/await with Promise.all
const fetchData = async (urls) => {
try {
const responses = await Promise.all(
urls.map(url => fetch(url).then(res => res.json()))
);
return responses;
} catch (error) {
console.error('Failed to fetch data:', error);
throw error;
}
};
// Export statement
export {
DataProcessor,
ValidationError,
processItems,
fetchData,
delay,
memoize,
config
};
================================================
FILE: app/server/syntax/file_map/examples/kotlin_example.kt
================================================
// Package declaration
package example
// Imports
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.time.LocalDateTime
import kotlin.properties.Delegates
// Interface with generic type parameter
interface DataProcessor {
suspend fun process(data: T): Result
fun validate(data: T): Boolean
}
// Sealed class for representing states
sealed class ProcessingState {
object Loading : ProcessingState()
data class Success(val data: T) : ProcessingState()
data class Error(val exception: Throwable) : ProcessingState()
}
// Data class with default parameters
data class User(
val id: String,
val name: String,
val email: String,
val roles: Set = emptySet(),
val createdAt: LocalDateTime = LocalDateTime.now()
)
// Enum class with properties and function
enum class Role(val permission: Int) {
ADMIN(0xFF) {
override fun toString() = "Administrator"
},
USER(0x0F) {
override fun toString() = "Regular User"
},
GUEST(0x00) {
override fun toString() = "Guest User"
}
}
// Object declaration (Singleton)
object Configuration {
const val API_VERSION = "1.0.0"
val defaultTimeout = 5000L
fun getConfig(key: String) = config[key]
private val config = mutableMapOf()
}
// Class with companion object and delegation
class UserProcessor : DataProcessor {
// Companion object with factory method
companion object {
fun create(): UserProcessor = UserProcessor()
}
// Property delegation
private var processingCount: Int by Delegates.observable(0) { _, old, new ->
println("Processing count changed from $old to $new")
}
// Property with custom getter
val isActive: Boolean
get() = processingCount > 0
// Suspending function implementation
override suspend fun process(data: User): Result = runCatching {
processingCount++
validateEmail(data.email)
data
}.also {
processingCount--
}
// Regular function implementation
override fun validate(data: User): Boolean =
data.email.isNotBlank() && data.name.isNotBlank()
// Extension function
private fun String.isValidEmail(): Boolean =
matches(Regex("^[A-Za-z0-9+_.-]+@(.+)$"))
// Inline function with reified type parameter
inline fun logType() =
println("Processing type: ${T::class.simpleName}")
// Private function using extension function
private fun validateEmail(email: String) {
require(email.isValidEmail()) { "Invalid email format" }
}
}
// Higher-order function with function type parameter
fun withRetry(
times: Int = 3,
action: suspend () -> T
): suspend () -> T = {
var lastException: Exception? = null
repeat(times) { attempt ->
try {
return@withRetry action()
} catch (e: Exception) {
lastException = e
println("Attempt ${attempt + 1} failed: ${e.message}")
}
}
throw lastException ?: IllegalStateException("All attempts failed")
}
// Coroutine scope extension
fun CoroutineScope.processUsers(users: List): Flow> = flow {
val processor = UserProcessor.create()
emit(ProcessingState.Loading)
users.forEach { user ->
processor.process(user)
.onSuccess { emit(ProcessingState.Success(it)) }
.onFailure { emit(ProcessingState.Error(it)) }
}
}
// Extension property
val User.displayName: String
get() = "$name (${email})"
// Main function demonstrating usage
suspend fun main() = coroutineScope {
val users = listOf(
User("1", "John Doe", "john@example.com"),
User("2", "Jane Smith", "jane@example.com", setOf(Role.ADMIN))
)
launch {
processUsers(users)
.collect { state ->
when (state) {
is ProcessingState.Loading -> println("Processing started")
is ProcessingState.Success -> println("Processed: ${state.data.displayName}")
is ProcessingState.Error -> println("Error: ${state.exception.message}")
}
}
}
}
================================================
FILE: app/server/syntax/file_map/examples/lua_example.lua
================================================
-- Module definition
local Example = {}
-- Constants
local MAX_RETRIES = 3
local DEFAULT_TIMEOUT = 5000
-- Private functions (local)
local function validateInput(input)
if type(input) ~= "string" then
error("Input must be a string")
end
return true
end
-- Metatable for creating classes
local function createClass(name)
local cls = {}
cls.__index = cls
cls.__name = name
-- Constructor
function cls.new(...)
local self = setmetatable({}, cls)
if self.init then
self:init(...)
end
return self
end
return cls
end
-- Class definition using metatables
local User = createClass("User")
function User:init(name, age)
self.name = name
self.age = age
self.created_at = os.time()
end
function User:toString()
return string.format("User(%s, %d)", self.name, self.age)
end
-- Table with custom metamethods
local DataStore = {
data = {},
__newindex = function(t, k, v)
print("Setting value:", k, v)
rawset(t.data, k, v)
end,
__index = function(t, k)
return t.data[k]
end
}
setmetatable(DataStore, DataStore)
-- Coroutine example
local function producer()
return coroutine.create(function()
for i = 1, 5 do
coroutine.yield(i)
end
end)
end
-- Iterator function
local function range(from, to, step)
step = step or 1
local i = from - step
return function()
i = i + step
if i <= to then
return i
end
end
end
-- Module functions
function Example.process(input)
assert(validateInput(input))
local result = {
original = input,
processed = string.upper(input),
timestamp = os.time()
}
return result
end
-- Function with multiple returns
function Example.divide(a, b)
if b == 0 then
return nil, "Division by zero"
end
return a / b
end
-- Closure example
function Example.counter(initial)
local count = initial or 0
return function()
count = count + 1
return count
end
end
-- Table manipulation
function Example.merge(t1, t2)
local result = {}
for k, v in pairs(t1) do
result[k] = v
end
for k, v in pairs(t2) do
result[k] = v
end
return result
end
-- Pattern matching example
function Example.extractEmails(text)
local emails = {}
for email in string.gmatch(text, "[%w%.%-_]+@[%w%.%-_]+%.%w+") do
table.insert(emails, email)
end
return emails
end
-- Event handling system
local EventEmitter = createClass("EventEmitter")
function EventEmitter:init()
self.handlers = {}
end
function EventEmitter:on(event, handler)
self.handlers[event] = self.handlers[event] or {}
table.insert(self.handlers[event], handler)
end
function EventEmitter:emit(event, ...)
if self.handlers[event] then
for _, handler in ipairs(self.handlers[event]) do
handler(...)
end
end
end
-- Add classes to module
Example.User = User
Example.EventEmitter = EventEmitter
-- Module return
return Example
================================================
FILE: app/server/syntax/file_map/examples/markdown_example.md
================================================
# SoundScape 🎵
An AI-powered music visualization and generation platform that creates real-time visual art from audio input.
## Features ✨
- Real-time audio processing using WebAudio API
- Dynamic visualization generation using Three.js
- AI-powered music analysis for enhanced visual mapping
- Multiple visualization styles (geometric, particle, liquid simulation)
- Audio recording and export capabilities
- Collaborative mode for live performances
## Getting Started 🚀
### Prerequisites
- Node.js (v18 or higher)
- GPU with WebGL 2.0 support
- Microphone access (for live input)
### Installation
1. Clone the repository:
```bash
git clone https://github.com/yourusername/soundscape.git
cd soundscape
```
2. Install dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm run dev
```
The application will be available at `http://localhost:3000`
## Architecture 🏗️
SoundScape uses a modular architecture with the following core components:
- **AudioEngine**: Handles audio input processing and analysis
- **VisualizationCore**: Manages the 3D rendering pipeline
- **AIProcessor**: Processes audio features for enhanced visualization
- **StateManager**: Handles application state and user preferences
## API Reference 📚
### Audio Processing
```typescript
interface AudioProcessor {
analyze(input: AudioBuffer): AudioFeatures;
extractBeat(features: AudioFeatures): BeatPattern;
generateVisuals(pattern: BeatPattern): Scene;
}
```
### Visualization
```typescript
interface VisualizationStyle {
name: string;
parameters: VisualParameters;
render(scene: Scene): void;
updateParams(params: Partial): void;
}
```
## Contributing 🤝
We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting a pull request.
### Development Workflow
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## Performance Optimization Tips 💡
- Use Web Workers for heavy audio processing
- Implement lazy loading for visualization styles
- Enable GPU acceleration when available
- Cache frequently used audio features
- Optimize render loops for smooth performance
## License 📄
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments 🙏
- Three.js community for 3D rendering support
- TensorFlow.js team for machine learning capabilities
- Web Audio API working group
- All our amazing contributors
## Contact 📧
Project Lead - [@projectlead](https://twitter.com/projectlead)
Project Link: [https://github.com/yourusername/soundscape](https://github.com/yourusername/soundscape)
---
Made with ❤️ by the SoundScape Team
================================================
FILE: app/server/syntax/file_map/examples/ocaml_example.ml
================================================
(* Module signature *)
module type DataProcessor = sig
type 'a t
val create : unit -> 'a t
val process : 'a t -> 'a -> ('a, string) result
val validate : 'a -> bool
end
(* Module implementation *)
module StringProcessor : DataProcessor with type 'a = string = struct
type 'a = string
type t = {
mutable processed_count: int;
created_at: float;
}
let create () = {
processed_count = 0;
created_at = Unix.time ();
}
let process t input =
t.processed_count <- t.processed_count + 1;
if String.length input > 0 then
Ok (String.uppercase_ascii input)
else
Error "Empty input"
let validate input =
String.length input > 0
end
(* Type definitions *)
type user = {
id: int;
name: string;
email: string;
created_at: float;
}
type 'a result =
| Success of 'a
| Error of string
(* Variant type *)
type message =
| Text of string
| Number of int
| Tuple of string * int
| Record of user
(* Functor definition *)
module type Comparable = sig
type t
val compare : t -> t -> int
end
module MakeSet (Item : Comparable) = struct
type element = Item.t
type t = element list
let empty = []
let rec add x = function
| [] -> [x]
| hd :: tl as l ->
match Item.compare x hd with
| 0 -> l
| n when n < 0 -> x :: l
| _ -> hd :: add x tl
let member x set =
List.exists (fun y -> Item.compare x y = 0) set
end
(* Exception definition *)
exception ValidationError of string
(* Higher-order function *)
let memoize f =
let cache = Hashtbl.create 16 in
fun x ->
try Hashtbl.find cache x
with Not_found ->
let result = f x in
Hashtbl.add cache x result;
result
(* Object-oriented features *)
class virtual ['a] queue = object(self)
val mutable items = []
method virtual push : 'a -> unit
method virtual pop : 'a option
method size = List.length items
method is_empty = items = []
method protected get_items = items
method protected set_items new_items = items <- new_items
end
class ['a] fifo_queue = object(self)
inherit ['a] queue
method push item =
self#set_items (self#get_items @ [item])
method pop =
match self#get_items with
| [] -> None
| hd::tl ->
self#set_items tl;
Some hd
end
(* Module for handling JSON-like data *)
module Json = struct
type t =
| Null
| Bool of bool
| Number of float
| String of string
| Array of t list
| Object of (string * t) list
let rec to_string = function
| Null -> "null"
| Bool b -> string_of_bool b
| Number n -> string_of_float n
| String s -> "\"" ^ String.escaped s ^ "\""
| Array items ->
"[" ^ String.concat ", " (List.map to_string items) ^ "]"
| Object pairs ->
let pair_to_string (k, v) =
"\"" ^ String.escaped k ^ "\": " ^ to_string v
in
"{" ^ String.concat ", " (List.map pair_to_string pairs) ^ "}"
end
(* Main execution *)
let () =
let processor = StringProcessor.create () in
let result = StringProcessor.process processor "hello world" in
match result with
| Ok processed -> Printf.printf "Processed: %s\n" processed
| Error msg -> Printf.eprintf "Error: %s\n" msg;
let queue = new fifo_queue in
queue#push 1;
queue#push 2;
queue#push 3;
let rec print_queue () =
match queue#pop with
| Some item ->
Printf.printf "Item: %d\n" item;
print_queue ()
| None -> ()
in
print_queue ()
================================================
FILE: app/server/syntax/file_map/examples/php_example.php
================================================
logger = $logger;
}
protected function log(string $message, array $context = []): void
{
$this->logger?->info($message, $context);
}
}
// Abstract class
abstract class Entity implements JsonSerializable
{
protected DateTime $createdAt;
protected ?DateTime $updatedAt = null;
public function __construct()
{
$this->createdAt = new DateTime();
}
abstract public function validate(): bool;
public function jsonSerialize(): mixed
{
return [
'createdAt' => $this->createdAt->format('c'),
'updatedAt' => $this->updatedAt?->format('c'),
];
}
}
// Enum definition (PHP 8.1+)
enum Status: string
{
case PENDING = 'pending';
case ACTIVE = 'active';
case COMPLETED = 'completed';
case FAILED = 'failed';
public function label(): string
{
return match($this) {
self::PENDING => 'Pending',
self::ACTIVE => 'Active',
self::COMPLETED => 'Completed',
self::FAILED => 'Failed',
};
}
}
// Class implementing interface and using trait
class User extends Entity implements DataProcessor
{
use Loggable;
private static int $instanceCount = 0;
public function __construct(
private string $name,
private string $email,
private Status $status = Status::PENDING,
private array $metadata = []
) {
parent::__construct();
self::$instanceCount++;
}
public static function getInstanceCount(): int
{
return self::$instanceCount;
}
// Property getter with validation
public function getEmail(): string
{
return $this->email;
}
// Property setter with validation
public function setEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email format');
}
$this->email = $email;
$this->updatedAt = new DateTime();
}
// Magic method implementation
public function __get(string $name)
{
return $this->metadata[$name] ?? null;
}
public function __set(string $name, mixed $value): void
{
$this->metadata[$name] = $value;
}
// Interface method implementations
public function process(mixed $data): mixed
{
if (!is_array($data)) {
throw new InvalidArgumentException('Data must be an array');
}
$this->log('Processing user data', ['user' => $this->name]);
foreach ($data as $key => $value) {
$this->metadata[$key] = $value;
}
$this->status = Status::COMPLETED;
return $this;
}
public function validate(mixed $data): bool
{
return is_array($data) && !empty($data);
}
// Abstract method implementation
public function validate(): bool
{
return !empty($this->name) && !empty($this->email);
}
// Method using arrow functions (PHP 7.4+)
public function getMetadataValues(): array
{
return array_map(
fn($value) => is_array($value) ? json_encode($value) : (string)$value,
$this->metadata
);
}
// Implementation of JsonSerializable
public function jsonSerialize(): mixed
{
return [
...parent::jsonSerialize(),
'name' => $this->name,
'email' => $this->email,
'status' => $this->status->value,
'metadata' => $this->metadata,
];
}
}
// Custom exception
class ProcessingException extends Exception
{
public function __construct(
string $message = "",
private ?string $errorCode = null,
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
public function getErrorCode(): ?string
{
return $this->errorCode;
}
}
// Anonymous class usage
$validator = new class {
public function validateUser(User $user): bool
{
return $user->validate();
}
};
// Example usage
try {
$user = new User("John Doe", "john@example.com");
$user->process(['role' => 'admin', 'preferences' => ['theme' => 'dark']]);
// Using magic methods
$user->customField = 'custom value';
echo $user->customField;
// JSON serialization
echo json_encode($user, JSON_PRETTY_PRINT);
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
================================================
FILE: app/server/syntax/file_map/examples/protobuf_example.proto
================================================
syntax = "proto3";
package example;
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
option go_package = "example/generated";
option java_package = "com.example.generated";
option java_multiple_files = true;
// Enum definition
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_PENDING = 1;
STATUS_ACTIVE = 2;
STATUS_COMPLETED = 3;
STATUS_FAILED = 4;
}
// Message with nested messages and various field types
message User {
// Nested message definition
message Address {
string street = 1;
string city = 2;
string state = 3;
string country = 4;
string postal_code = 5;
}
// Nested enum
enum Role {
ROLE_UNSPECIFIED = 0;
ROLE_ADMIN = 1;
ROLE_USER = 2;
ROLE_GUEST = 3;
}
// Fields with different types and options
string id = 1;
string name = 2 [(validate.rules).string = {
min_len: 1,
max_len: 100
}];
string email = 3 [(validate.rules).string.email = true];
repeated string phone_numbers = 4;
Role role = 5;
Status status = 6;
Address primary_address = 7;
repeated Address additional_addresses = 8;
map metadata = 9;
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11;
oneof verification {
string phone_verification = 12;
string email_verification = 13;
}
}
// Message using various repeated and map fields
message UserList {
repeated User users = 1;
int32 total_count = 2;
string next_page_token = 3;
}
// Request/Response messages
message CreateUserRequest {
User user = 1;
}
message UpdateUserRequest {
string user_id = 1;
User user = 2;
google.protobuf.FieldMask update_mask = 3;
}
message GetUserRequest {
string user_id = 1;
}
message DeleteUserRequest {
string user_id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
// Service definition with various RPC patterns
service UserService {
// Unary RPC
rpc CreateUser(CreateUserRequest) returns (User);
// Unary RPC with custom error responses
rpc GetUser(GetUserRequest) returns (User);
// Unary RPC with field mask
rpc UpdateUser(UpdateUserRequest) returns (User);
// Unary RPC returning empty response
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
// Server streaming RPC
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming RPC
rpc BatchCreateUsers(stream CreateUserRequest) returns (UserList);
// Bidirectional streaming RPC
rpc ProcessUsers(stream User) returns (stream User);
}
================================================
FILE: app/server/syntax/file_map/examples/python_example.py
================================================
#!/usr/bin/env python3
from __future__ import annotations
import asyncio
import dataclasses
import enum
from abc import ABC, abstractmethod
from datetime import datetime
from functools import wraps
from typing import (
Any, AsyncIterator, Callable, ClassVar, Dict, Generic,
List, Optional, Protocol, TypeVar, Union
)
# Type variable definitions
T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')
# Protocol definition
class Processable(Protocol):
def process(self) -> None: ...
def validate(self) -> bool: ...
# Enum definition
class Status(enum.Enum):
PENDING = "pending"
ACTIVE = "active"
COMPLETED = "completed"
FAILED = "failed"
def __str__(self) -> str:
return self.value
# Dataclass with frozen and slots options
@dataclasses.dataclass(frozen=True, slots=True)
class UserCredentials:
username: str
email: str
created_at: datetime = dataclasses.field(default_factory=datetime.now)
# Abstract base class
class BaseProcessor(ABC, Generic[T]):
def __init__(self) -> None:
self._items: List[T] = []
self._processed_count: int = 0
@abstractmethod
async def process_item(self, item: T) -> None:
pass
@property
def processed_count(self) -> int:
return self._processed_count
# Decorator definition
def log_execution(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"Executing {func.__name__}")
try:
result = await func(*args, **kwargs)
print(f"Completed {func.__name__}")
return result
except Exception as e:
print(f"Error in {func.__name__}: {e}")
raise
return wrapper
# Class implementing abstract base class and protocol
class DataProcessor(BaseProcessor[UserCredentials], Processable):
# Class variable
DEFAULT_BATCH_SIZE: ClassVar[int] = 100
def __init__(self, batch_size: Optional[int] = None) -> None:
super().__init__()
self.batch_size = batch_size or self.DEFAULT_BATCH_SIZE
self._status = Status.PENDING
# Property with getter and setter
@property
def status(self) -> Status:
return self._status
@status.setter
def status(self, value: Status) -> None:
if not isinstance(value, Status):
raise ValueError("Status must be a Status enum value")
self._status = value
# Context manager methods
async def __aenter__(self) -> DataProcessor:
self.status = Status.ACTIVE
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
self.status = Status.COMPLETED if exc_type is None else Status.FAILED
# Generator method
async def process_batch(self) -> AsyncIterator[List[UserCredentials]]:
for i in range(0, len(self._items), self.batch_size):
batch = self._items[i:i + self.batch_size]
yield batch
await asyncio.sleep(0.1)
# Implementation of abstract method
@log_execution
async def process_item(self, item: UserCredentials) -> None:
if not self.validate():
raise ValueError("Processor is not in a valid state")
self._items.append(item)
self._processed_count += 1
# Implementation of protocol method
def process(self) -> None:
if not self._items:
raise ValueError("No items to process")
self.status = Status.ACTIVE
def validate(self) -> bool:
return self.status != Status.FAILED
# Custom exception
class ProcessingError(Exception):
def __init__(self, message: str, item: Any) -> None:
self.item = item
super().__init__(f"Error processing {item}: {message}")
# Async main function
async def main() -> None:
async with DataProcessor(batch_size=10) as processor:
# Create test data
user = UserCredentials(
username="test_user",
email="test@example.com"
)
try:
await processor.process_item(user)
async for batch in processor.process_batch():
print(f"Processing batch of {len(batch)} items")
except ProcessingError as e:
print(f"Processing failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: app/server/syntax/file_map/examples/ruby_example.rb
================================================
#!/usr/bin/env ruby
global_var = "Hello, World!"
# Module for mixing in common functionality
module Loggable
def log(message)
puts "[#{Time.now}] #{message}"
end
end
# Module with class methods
module Utils
class << self
def generate_id
SecureRandom.uuid
end
end
end
# Abstract base class
class BaseProcessor
include Loggable
# Class instance variable
@processors = []
class << self
attr_reader :processors
def register(processor)
@processors << processor
end
end
# Instance variables with attr accessors
attr_reader :id, :created_at
attr_accessor :status
def initialize
@id = Utils.generate_id
@created_at = Time.now
@status = :pending
self.class.register(self)
end
# Abstract method
def process
raise NotImplementedError, "#{self.class} must implement process"
end
end
# Custom exception class
class ProcessingError < StandardError
attr_reader :item
def initialize(message, item)
@item = item
super(message)
end
end
# Struct definition
User = Struct.new(:name, :email, keyword_init: true) do
def valid?
name && email && email.include?('@')
end
end
# Enum-like module using freeze
module Status
PENDING = 'pending'.freeze
ACTIVE = 'active'.freeze
COMPLETED = 'completed'.freeze
FAILED = 'failed'.freeze
ALL = [PENDING, ACTIVE, COMPLETED, FAILED].freeze
end
# Class using inheritance and mixins
class DataProcessor < BaseProcessor
# Constants
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 5
# Class variable
@@instance_count = 0
def self.instance_count
@@instance_count
end
def initialize(options = {})
super()
@options = options
@items = []
@@instance_count += 1
end
# Method with keyword arguments and default value
def add_item(item:, priority: :normal)
validate_item(item)
@items << [item, priority]
end
# Private methods
private
def validate_item(item)
raise ArgumentError, "Invalid item" unless item.respond_to?(:valid?)
raise ProcessingError.new("Invalid item", item) unless item.valid?
end
# Method using block
def with_retry
retries = 0
begin
yield
rescue StandardError => e
retries += 1
retry if retries < MAX_RETRIES
raise
end
end
# Method using lambda
def process_items
sorter = ->(a, b) { a[1] <=> b[1] }
@items.sort(&sorter).each do |item, _priority|
process_item(item)
end
end
protected
def process_item(item)
log("Processing item: #{item}")
# Processing logic here
end
end
# Singleton class
require 'singleton'
class Configuration
include Singleton
def initialize
@settings = {}
end
def [](key)
@settings[key]
end
def []=(key, value)
@settings[key] = value
end
end
# Example usage
if __FILE__ == $PROGRAM_NAME
config = Configuration.instance
config[:timeout] = 30
processor = DataProcessor.new(timeout: config[:timeout])
user = User.new(name: "John Doe", email: "john@example.com")
begin
processor.add_item(item: user, priority: :high)
processor.process
rescue ProcessingError => e
puts "Failed to process #{e.item}: #{e.message}"
end
end
================================================
FILE: app/server/syntax/file_map/examples/rust_example.rs
================================================
use std::{
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
time::{Duration, SystemTime},
};
use tokio::sync::mpsc;
use serde::{Deserialize, Serialize};
// Type alias
type Result = std::result::Result>;
// Constants
const MAX_RETRIES: u32 = 3;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
// Custom error type
#[derive(Debug, thiserror::Error)]
pub enum ProcessError {
#[error("Validation failed: {0}")]
ValidationError(String),
#[error("Processing failed: {0}")]
ProcessingError(String),
#[error(transparent)]
Other(#[from] Box),
}
// Trait definition
#[async_trait::async_trait]
pub trait DataProcessor {
async fn process(&self, data: T) -> Result;
fn validate(&self, data: &T) -> bool;
}
// Struct with lifetime parameter and generic type
#[derive(Debug)]
pub struct ProcessorState<'a, T> {
name: &'a str,
data: T,
created_at: SystemTime,
}
// Enum with different variants
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Status {
Pending,
Active { started_at: SystemTime },
Completed { result: String },
Failed { error: String },
}
// Struct implementing trait
pub struct ItemProcessor {
items: Arc>>,
status: Status,
tx: mpsc::Sender,
}
// Trait implementation
#[async_trait::async_trait]
impl DataProcessor for ItemProcessor {
async fn process(&self, data: String) -> Result {
if !self.validate(&data) {
return Err(Box::new(ProcessError::ValidationError("Invalid data".into())));
}
Ok(data.to_uppercase())
}
fn validate(&self, data: &String) -> bool {
!data.is_empty()
}
}
// Struct with derive macros
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub name: String,
pub max_items: usize,
#[serde(default)]
pub timeout: Option,
}
// Implementation with associated types
pub trait Storage {
type Item;
type Error;
fn store(&mut self, item: Self::Item) -> std::result::Result<(), Self::Error>;
fn retrieve(&self, id: &str) -> std::result::Result