[
  {
    "path": ".dockerignore",
    "content": "dist-server/\ncli/\nnode_modules/\nplandex-server\nplandex-cloud\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.sh\t\ttext eol=lf"
  },
  {
    "path": ".github/workflows/docker-publish.yml",
    "content": "name: Build and publish Docker Image\n\non:\n  release:\n    types: [created]\n  workflow_dispatch: # enable manual triggering\n\njobs:\n  check_release:\n    runs-on: ubuntu-latest\n    outputs:\n      should_build: ${{ steps.check_release.outputs.should_build }}\n      tag: ${{ steps.check_release.outputs.tag }}\n    \n    steps:\n      - name: Check release tag and find latest server tag\n        id: check_release\n        run: |\n          if [ \"${{ github.event_name }}\" == \"release\" ]; then\n            # This is a release event - check if the tag starts with 'server'\n            if [[ \"${{ github.ref_name }}\" == server* ]]; then\n              echo \"This is a server release: ${{ github.ref_name }}\"\n              echo \"should_build=true\" >> $GITHUB_OUTPUT\n              echo \"tag=${{ github.ref_name }}\" >> $GITHUB_OUTPUT\n            else\n              echo \"This is not a server release. Skipping build.\"\n              echo \"should_build=false\" >> $GITHUB_OUTPUT\n            fi\n          else\n            # This is a manual workflow_dispatch event\n            echo \"This is a manual workflow trigger. Proceeding to find latest server tag.\"\n            echo \"should_build=true\" >> $GITHUB_OUTPUT\n            echo \"tag=latest_server\" >> $GITHUB_OUTPUT\n          fi\n\n  build_and_push:\n    needs: check_release\n    if: needs.check_release.outputs.should_build == 'true'\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0  # Fetch all history and tags\n\n      - name: Find latest server tag\n        id: find_tag\n        if: needs.check_release.outputs.tag == 'latest_server'\n        run: |\n          # Find the latest tag that starts with 'server'\n          LATEST_SERVER_TAG=$(git tag -l \"server*\" --sort=-creatordate | head -n 1)\n          \n          if [ -z \"$LATEST_SERVER_TAG\" ]; then\n            echo \"No tags starting with 'server' found.\"\n            echo \"skip=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"Found latest server tag: $LATEST_SERVER_TAG\"\n            echo \"skip=false\" >> $GITHUB_OUTPUT\n            echo \"tag=$LATEST_SERVER_TAG\" >> $GITHUB_OUTPUT\n          fi\n        shell: bash\n\n      - name: Set release tag\n        id: set_tag\n        if: needs.check_release.outputs.tag != 'latest_server'\n        run: |\n          echo \"skip=false\" >> $GITHUB_OUTPUT\n          echo \"tag=${{ needs.check_release.outputs.tag }}\" >> $GITHUB_OUTPUT\n\n      - name: Skip build if no server tag found\n        if: (steps.find_tag.outputs.skip == 'true' && needs.check_release.outputs.tag == 'latest_server')\n        run: |\n          echo \"Skipping build because no tag starting with 'server' was found.\"\n          exit 1\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v1\n\n      - name: Log in to Docker Hub\n        uses: docker/login-action@v1\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Determine tag to use\n        id: determine_tag\n        run: |\n          if [ \"${{ needs.check_release.outputs.tag }}\" == \"latest_server\" ]; then\n            TAG=\"${{ steps.find_tag.outputs.tag }}\"\n          else\n            TAG=\"${{ needs.check_release.outputs.tag }}\"\n          fi\n          echo \"Using tag: $TAG\"\n          echo \"tag=$TAG\" >> $GITHUB_OUTPUT\n\n      - name: Sanitize tag name\n        id: sanitize\n        run: echo \"SANITIZED_TAG_NAME=$(echo ${{ steps.determine_tag.outputs.tag }} | tr '/' '-' | tr '+' '-')\" >> $GITHUB_OUTPUT\n\n      - name: Build and push\n        uses: docker/build-push-action@v2\n        with:\n          context: ./app/\n          file: ./app/server/Dockerfile\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: |\n            plandexai/plandex-server:${{ steps.sanitize.outputs.SANITIZED_TAG_NAME }}\n            plandexai/plandex-server:latest"
  },
  {
    "path": ".gitignore",
    "content": ".plandex/\n.plandex-dev/\n.plandex-v2/\n.plandex-dev-v2/\n.envkey\n.env\n.env.*\nplandex\nplandex-dev\nplandex-server\n*.exe\nnode_modules/\n/tools/\n/static/\n/infra/\n/payments-dashboard/\n.DS_Store\n.goreleaser.yml\ndist/\n__pycache__/\n\n.aider.*\n*.code-workspace\n\n__pycache__/\n\n.repo_ignore"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 PlandexAI Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\r\n <a href=\"https://plandex.ai\">\r\n  <picture>\r\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"images/plandex-logo-dark-v2.png\"/>\r\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"images/plandex-logo-light-v2.png\"/>\r\n    <img width=\"400\" src=\"images/plandex-logo-dark-bg-v2.png\"/>\r\n </a>\r\n <br />\r\n</h1>\r\n<br />\r\n\r\n<div align=\"center\">\r\n\r\n<p align=\"center\">\r\n  <!-- Call to Action Links -->\r\n  <a href=\"#install\">\r\n    <b>30-Second Install</b>\r\n  </a>\r\n   ·\r\n  <a href=\"https://plandex.ai\">\r\n    <b>Website</b>\r\n  </a>\r\n   ·\r\n  <a href=\"https://docs.plandex.ai/\">\r\n    <b>Docs</b>\r\n  </a>\r\n   ·\r\n  <a href=\"#examples-\">\r\n    <b>Examples</b>\r\n  </a>\r\n   ·\r\n  <a href=\"https://docs.plandex.ai/hosting/self-hosting/local-mode-quickstart\">\r\n    <b>Local Self-Hosted Mode</b>\r\n  </a>\r\n</p>\r\n\r\n<br>\r\n\r\n[![Discord](https://img.shields.io/discord/1214825831973785600.svg?style=flat&logo=discord&label=Discord&refresh=1)](https://discord.gg/plandex-ai)\r\n[![GitHub Repo stars](https://img.shields.io/github/stars/plandex-ai/plandex?style=social)](https://github.com/plandex-ai/plandex)\r\n[![Twitter Follow](https://img.shields.io/twitter/follow/PlandexAI?style=social)](https://twitter.com/PlandexAI)\r\n\r\n</div>\r\n\r\n<p align=\"center\">\r\n  <!-- Badges -->\r\n<a href=\"https://github.com/plandex-ai/plandex/pulls\"><img src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg\" alt=\"PRs Welcome\" /></a> <a href=\"https://github.com/plandex-ai/plandex/releases?q=cli\"><img src=\"https://img.shields.io/github/v/release/plandex-ai/plandex?filter=cli*\" alt=\"Release\" /></a>\r\n<a href=\"https://github.com/plandex-ai/plandex/releases?q=server\"><img src=\"https://img.shields.io/github/v/release/plandex-ai/plandex?filter=server*\" alt=\"Release\" /></a>\r\n\r\n  <!-- <a href=\"https://github.com/your_username/your_project/issues\">\r\n    <img src=\"https://img.shields.io/github/issues-closed/your_username/your_project.svg\" alt=\"Issues Closed\" />\r\n  </a> -->\r\n\r\n</p>\r\n\r\n<br />\r\n\r\n<div align=\"center\">\r\n<a href=\"https://trendshift.io/repositories/8994\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/8994\" alt=\"plandex-ai%2Fplandex | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\r\n</div>\r\n\r\n<br>\r\n\r\n<h1 align=\"center\" >\r\n  An AI coding agent designed for large tasks and real world projects.<br/><br/>\r\n</h1>\r\n\r\n<!-- <h2 align=\"center\">\r\n  Designed for large tasks and real world projects.<br/><br/>\r\n  </h2> -->\r\n  <br/>\r\n\r\n<div align=\"center\">\r\n  <a href=\"https://www.youtube.com/watch?v=SFSu2vNmlLk\">\r\n    <img src=\"images/plandex-v2-yt.png\" alt=\"Plandex v2 Demo Video\" width=\"800\">\r\n  </a>\r\n</div>\r\n\r\n<br/>\r\n\r\n💻  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.\r\n\r\n🔬  **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.\r\n\r\n🧠  **Combine the best models** from Anthropic, OpenAI, Google, and open source providers to build entire features and apps with a robust terminal-based workflow.\r\n\r\n🚀  Plandex is capable of <strong>full autonomy</strong>—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.\r\n\r\n💪  Plandex is designed to be resilient to <strong>large projects and files</strong>. 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.\r\n\r\n## Smart context management that works in big projects\r\n\r\n- 🐘 **2M token effective context window** with default model pack. Plandex loads only what's needed for each step.\r\n\r\n- 🗄️ **Reliable in large projects and files.** Easily generate, review, revise, and apply changes spanning dozens of files.\r\n\r\n- 🗺️ **Fast project map generation** and syntax validation with tree-sitter. Supports 30+ languages.\r\n\r\n- 💰 **Context caching** is used across the board for OpenAI, Anthropic, and Google models, reducing costs and latency.\r\n\r\n## Tight control or full autonomy—it's up to you\r\n\r\n- 🚦 **Configurable autonomy:** go from full auto mode to fine-grained control depending on the task.\r\n\r\n- 🐞 **Automated debugging** of terminal commands (like builds, linters, tests, deployments, and scripts). If you have Chrome installed, you can also automatically debug browser applications.\r\n\r\n## Tools that help you get production-ready results\r\n\r\n- 💬 **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.\r\n\r\n- 🧠 **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.\r\n\r\n- 🛡️ **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.\r\n\r\n- 🔀 **Full-fledged version control** for every update to the plan, including branches for exploring multiple paths or comparing different models.\r\n\r\n- 📂 **Git integration** with commit message generation and optional automatic commits.\r\n\r\n## Dev-friendly, easy to install\r\n\r\n- 🧑‍💻 **REPL mode** with fuzzy auto-complete for commands and file loading. Just run `plandex` in any project to get started.\r\n\r\n- 🛠️ **CLI interface** for scripting or piping data into context.\r\n\r\n- 📦 **One-line, zero dependency CLI install**. Dockerized local mode for easily self-hosting the server. Cloud-hosting options for extra reliability and convenience.\r\n\r\n## Workflow  🔄\r\n\r\n<img src=\"images/plandex-workflow.png\" alt=\"Plandex workflow\" width=\"100%\"/>\r\n\r\n## Examples  🎥\r\n\r\n  <br/>\r\n\r\n<div align=\"center\">\r\n  <a href=\"https://www.youtube.com/watch?v=g-_76U_nK0Y\">\r\n    <img src=\"images/plandex-browser-debug-yt.png\" alt=\"Plandex Browser Debugging Example\" width=\"800\">\r\n  </a>\r\n</div>\r\n\r\n<br/>\r\n\r\n## Install  📥\r\n\r\n```bash\r\ncurl -sL https://plandex.ai/install.sh | bash\r\n```\r\n\r\n**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.\r\n\r\n[More installation options.](https://docs.plandex.ai/install)\r\n\r\n## Hosting  ⚖️\r\n\r\n| Option                     | Description                                                                                                                                                                                                                                                                                                                                                 |\r\n| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\r\n| **Plandex Cloud**          | Winding down as of 10/3/2025 and no longer accepting new users. <a href=\"https://plandex.ai/blog/winding-down\">Learn more.</a>                                                                                                                                                                                                                              |\r\n| **Self-hosted/Local Mode** | • Run Plandex locally with Docker or host on your own server.<br/>• Use your own [OpenRouter.ai](https://openrouter.ai) key (or [other model provider](https://docs.plandex.ai/models/model-providers) accounts and API keys).<br/>• Follow the [local-mode quickstart](https://docs.plandex.ai/hosting/self-hosting/local-mode-quickstart) to get started. |\r\n\r\n## Provider keys  🔑\r\n\r\n<!-- If you're going with a 'BYO API Key' option above (whether cloud or self-hosted), you'll need to set API keys for the [model providers](https://docs.plandex.ai/models/model-providers) you're using: -->\r\n\r\n```bash\r\nexport OPENROUTER_API_KEY=... # if using OpenRouter.ai\r\n```\r\n\r\n<br/>\r\n\r\n## Claude Pro/Max subscription  🖇️\r\n\r\nIf 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.\r\n\r\n<br/>\r\n\r\n## Get started  🚀\r\n\r\nFirst, `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.\r\n\r\n```bash\r\ncd your-project-dir\r\n```\r\n\r\nFor 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.\r\n\r\n```bash\r\ngit init\r\n```\r\n\r\nNow start the Plandex REPL in your project:\r\n\r\n```bash\r\nplandex\r\n```\r\n\r\nor for short:\r\n\r\n```bash\r\npdx\r\n```\r\n\r\n<!-- ☁️ _If you're using Plandex Cloud, you'll be prompted at this point to start a trial._\r\n\r\nThen just give the REPL help text a quick read, and you're ready go. The REPL starts in _chat mode_ by default, which is good for fleshing out ideas before moving to implementation. Once the task is clear, Plandex will prompt you to switch to _tell mode_ to make a detailed plan and start writing code. -->\r\n\r\n<br/>\r\n\r\n## Docs  🛠️\r\n\r\n### [👉  Full documentation.](https://docs.plandex.ai/)\r\n\r\n<br/>\r\n\r\n## Discussion and discord  💬\r\n\r\nPlease feel free to give your feedback, ask questions, report a bug, or just hang out:\r\n\r\n- [Discord](https://discord.gg/plandex-ai)\r\n- [Discussions](https://github.com/plandex-ai/plandex/discussions)\r\n- [Issues](https://github.com/plandex-ai/plandex/issues)\r\n\r\n## Follow and subscribe\r\n\r\n- [Follow @PlandexAI](https://x.com/PlandexAI)\r\n- [Follow @Danenania](https://x.com/Danenania) (Plandex's creator)\r\n- [Subscribe on YouTube](https://x.com/PlandexAI)\r\n\r\n<br/>\r\n\r\n## Contributors  👥\r\n\r\n⭐️  Please star, fork, explore, and contribute to Plandex. There's a lot of work to do and so much that can be improved.\r\n\r\n[Here's an overview on setting up a development environment.](https://docs.plandex.ai/development)\r\n"
  },
  {
    "path": "app/.dockerignore",
    "content": "cli/\nplandex-server"
  },
  {
    "path": "app/.gitignore",
    "content": ".env\n"
  },
  {
    "path": "app/clear_local.sh",
    "content": "#!/usr/bin/env bash\n\n# Get the absolute path to the script's directory, regardless of where it's run from\nSCRIPT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\"\n\n# Change to the app directory if we're not already there\ncd \"$SCRIPT_DIR\"\n\necho \"WARNING: This will delete all Plandex server data and reset the database.\"\necho \"This action cannot be undone.\"\nread -p \"Are you sure you want to continue? (y/N) \" -n 1 -r\necho\nif [[ ! $REPLY =~ ^[Yy]$ ]]\nthen\n    echo \"Reset cancelled.\"\n    exit 1\nfi\n\necho \"Resetting local mode...\"\necho \"Stopping containers and removing volumes...\"\n\n# Stop containers and remove volumes\ndocker compose down -v\n\necho \"Database and data directories cleared. Server stopped.\""
  },
  {
    "path": "app/cli/api/clients.go",
    "content": "package api\n\nimport (\n\t\"math\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/types\"\n\t\"time\"\n)\n\nconst dialTimeout = 10 * time.Second\nconst fastReqTimeout = 30 * time.Second\nconst slowReqTimeout = 5 * time.Minute\n\ntype Api struct{}\n\nvar CloudApiHost string\nvar Client types.ApiClient = (*Api)(nil)\n\nfunc init() {\n\tif os.Getenv(\"PLANDEX_ENV\") == \"development\" {\n\t\tCloudApiHost = os.Getenv(\"PLANDEX_API_HOST\")\n\t\tif CloudApiHost == \"\" {\n\t\t\tCloudApiHost = \"http://localhost:8099\"\n\t\t}\n\t} else {\n\t\tCloudApiHost = \"https://api-v2.plandex.ai\"\n\t}\n}\n\nfunc GetApiHost() string {\n\tif auth.Current == nil {\n\t\treturn CloudApiHost\n\t} else if auth.Current.IsCloud {\n\t\treturn CloudApiHost\n\t} else {\n\t\treturn auth.Current.Host\n\t}\n}\n\ntype authenticatedTransport struct {\n\tunderlyingTransport http.RoundTripper\n}\n\nfunc (t *authenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\terr := auth.SetAuthHeader(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tauth.SetVersionHeader(req)\n\treturn t.underlyingTransport.RoundTrip(req)\n}\n\ntype unauthenticatedTransport struct {\n\tunderlyingTransport http.RoundTripper\n}\n\nfunc (t *unauthenticatedTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tauth.SetVersionHeader(req)\n\treturn t.underlyingTransport.RoundTrip(req)\n}\n\ntype retryTransport struct {\n\tBase          http.RoundTripper\n\tMaxRetries    int\n\tBaseDelay     time.Duration\n\tMaxDelay      time.Duration\n\tJitter        time.Duration\n\tRetryStatuses map[int]bool\n}\n\nfunc (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\tif t.Base == nil {\n\t\tt.Base = http.DefaultTransport\n\t}\n\tvar resp *http.Response\n\tvar err error\n\n\tfor attempt := 0; attempt <= t.MaxRetries; attempt++ {\n\t\tresp, err = t.Base.RoundTrip(req)\n\n\t\t// If there's a low-level error (e.g. network), retry unless it's a timeout, as these are often transient.\n\t\tif err != nil {\n\t\t\tif netErr, ok := err.(net.Error); ok {\n\t\t\t\tif netErr.Timeout() {\n\t\t\t\t\treturn resp, err\n\t\t\t\t}\n\t\t\t}\n\t\t\t// continue to next attempt\n\t\t} else {\n\t\t\t// If status code not in our RetryStatuses, return immediately.\n\t\t\tif !t.RetryStatuses[resp.StatusCode] {\n\t\t\t\treturn resp, nil\n\t\t\t}\n\n\t\t\t// Close the body before retrying.\n\t\t\t_ = resp.Body.Close()\n\t\t}\n\n\t\t// If we reached the max, break out of loop (will return last resp).\n\t\tif attempt == t.MaxRetries {\n\t\t\tbreak\n\t\t}\n\n\t\t// Exponential backoff + jitter\n\t\tbackoff := float64(t.BaseDelay) * math.Pow(2, float64(attempt))\n\t\tif backoff > float64(t.MaxDelay) {\n\t\t\tbackoff = float64(t.MaxDelay)\n\t\t}\n\t\tsleepDuration := time.Duration(backoff) + time.Duration(rand.Int63n(int64(t.Jitter)))\n\t\ttime.Sleep(sleepDuration)\n\t}\n\treturn resp, err\n}\n\nvar netDialer = &net.Dialer{\n\tTimeout: dialTimeout,\n}\n\nvar baseTransport = &http.Transport{\n\tDial: netDialer.Dial,\n}\n\nvar sharedRetryTransport = &retryTransport{\n\tBase:          baseTransport,\n\tMaxRetries:    3,\n\tBaseDelay:     500 * time.Millisecond,\n\tMaxDelay:      5 * time.Second,\n\tJitter:        300 * time.Millisecond,\n\tRetryStatuses: map[int]bool{502: true, 503: true, 504: true},\n}\n\nvar unauthenticatedClient = &http.Client{\n\tTransport: &unauthenticatedTransport{\n\t\tunderlyingTransport: sharedRetryTransport,\n\t},\n\tTimeout: fastReqTimeout,\n}\n\nvar authenticatedFastClient = &http.Client{\n\tTransport: &authenticatedTransport{\n\t\tunderlyingTransport: sharedRetryTransport,\n\t},\n\tTimeout: fastReqTimeout,\n}\n\nvar authenticatedSlowClient = &http.Client{\n\tTransport: &authenticatedTransport{\n\t\tunderlyingTransport: sharedRetryTransport,\n\t},\n\tTimeout: slowReqTimeout,\n}\n\nvar authenticatedStreamingClient = &http.Client{\n\tTransport: &authenticatedTransport{\n\t\tunderlyingTransport: sharedRetryTransport,\n\t},\n}\n"
  },
  {
    "path": "app/cli/api/errors.go",
    "content": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc HandleApiError(r *http.Response, errBody []byte) *shared.ApiError {\n\t// Check if the response is JSON\n\tif r.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\treturn &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: r.StatusCode,\n\t\t\tMsg:    strings.TrimSpace(string(errBody)),\n\t\t}\n\t}\n\n\tvar apiError shared.ApiError\n\tif err := json.Unmarshal(errBody, &apiError); err != nil {\n\t\tlog.Printf(\"Error unmarshalling JSON: %v\\n\", err)\n\t\treturn &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: r.StatusCode,\n\t\t\tMsg:    strings.TrimSpace(string(errBody)),\n\t\t}\n\t}\n\n\t// return error if token/auth refresh is needed\n\tif apiError.Type == shared.ApiErrorTypeInvalidToken || apiError.Type == shared.ApiErrorTypeAuthOutdated {\n\t\treturn &apiError\n\t}\n\n\tterm.HandleApiError(&apiError)\n\n\treturn &apiError\n}\n\nfunc refreshAuthIfNeeded(apiErr *shared.ApiError) (bool, *shared.ApiError) {\n\tif apiErr.Type == shared.ApiErrorTypeInvalidToken {\n\t\terr := auth.RefreshInvalidToken()\n\t\tif err != nil {\n\t\t\treturn false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: \"error refreshing invalid token\"}\n\t\t}\n\t\treturn true, nil\n\t} else if apiErr.Type == shared.ApiErrorTypeAuthOutdated {\n\t\terr := auth.RefreshAuth()\n\t\tif err != nil {\n\t\t\treturn false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: \"error refreshing auth\"}\n\t\t}\n\n\t\treturn true, nil\n\t}\n\n\treturn false, apiErr\n}\n"
  },
  {
    "path": "app/cli/api/methods.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-cli/types\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/shopspring/decimal\"\n)\n\nfunc (a *Api) CreateCliTrialSession() (string, *shared.ApiError) {\n\tserverUrl := CloudApiHost + \"/accounts/cli_trial_session\"\n\n\tresp, err := unauthenticatedClient.Post(serverUrl, \"application/json\", nil)\n\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\treturn \"\", apiErr\n\t}\n\n\tbytes, err := io.ReadAll(resp.Body)\n\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error reading response: %v\", err)}\n\t}\n\n\treturn string(bytes), nil\n}\n\nfunc (a *Api) GetCliTrialSession(token string) (*shared.SessionResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/accounts/cli_trial_session/%s\", CloudApiHost, token)\n\n\tresp, err := unauthenticatedClient.Get(serverUrl)\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tif resp.StatusCode == 404 {\n\t\treturn nil, nil\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\treturn nil, apiErr\n\t}\n\n\tvar session shared.SessionResponse\n\terr = json.NewDecoder(resp.Body).Decode(&session)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &session, nil\n}\n\nfunc (a *Api) CreateProject(req shared.CreateProjectRequest) (*shared.CreateProjectResponse, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/projects\"\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.CreateProject(req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar respBody shared.CreateProjectResponse\n\terr = json.NewDecoder(resp.Body).Decode(&respBody)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &respBody, nil\n}\n\nfunc (a *Api) ListProjects() ([]*shared.Project, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/projects\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListProjects()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar projects []*shared.Project\n\terr = json.NewDecoder(resp.Body).Decode(&projects)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn projects, nil\n}\n\nfunc (a *Api) SetProjectPlan(projectId string, req shared.SetProjectPlanRequest) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/projects/%s/set_plan\", GetApiHost(), projectId)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.SetProjectPlan(projectId, req)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) RenameProject(projectId string, req shared.RenameProjectRequest) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/projects/%s/rename\", GetApiHost(), projectId)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.RenameProject(projectId, req)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\nfunc (a *Api) ListPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans?\", GetApiHost())\n\tparts := []string{}\n\tfor _, projectId := range projectIds {\n\t\tparts = append(parts, fmt.Sprintf(\"projectId=%s\", projectId))\n\t}\n\tserverUrl += strings.Join(parts, \"&\")\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.ListPlans(projectIds)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar plans []*shared.Plan\n\terr = json.NewDecoder(resp.Body).Decode(&plans)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn plans, nil\n}\n\nfunc (a *Api) ListArchivedPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/archive?\", GetApiHost())\n\tparts := []string{}\n\tfor _, projectId := range projectIds {\n\t\tparts = append(parts, fmt.Sprintf(\"projectId=%s\", projectId))\n\t}\n\tserverUrl += strings.Join(parts, \"&\")\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListArchivedPlans(projectIds)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar plans []*shared.Plan\n\terr = json.NewDecoder(resp.Body).Decode(&plans)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn plans, nil\n}\n\nfunc (a *Api) ListPlansRunning(projectIds []string, includeRecent bool) (*shared.ListPlansRunningResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/ps?\", GetApiHost())\n\tparts := []string{}\n\tfor _, projectId := range projectIds {\n\t\tparts = append(parts, fmt.Sprintf(\"projectId=%s\", projectId))\n\t}\n\tserverUrl += strings.Join(parts, \"&\")\n\tif includeRecent {\n\t\tserverUrl += \"&recent=true\"\n\t}\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListPlansRunning(projectIds, includeRecent)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar respBody *shared.ListPlansRunningResponse\n\terr = json.NewDecoder(resp.Body).Decode(&respBody)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn respBody, nil\n}\n\nfunc (a *Api) GetCurrentBranchByPlanId(projectId string, req shared.GetCurrentBranchByPlanIdRequest) (map[string]*shared.Branch, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/projects/%s/plans/current_branches\", GetApiHost(), projectId)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.GetCurrentBranchByPlanId(projectId, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar respBody map[string]*shared.Branch\n\terr = json.NewDecoder(resp.Body).Decode(&respBody)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn respBody, nil\n}\n\nfunc (a *Api) CreatePlan(projectId string, req shared.CreatePlanRequest) (*shared.CreatePlanResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/projects/%s/plans\", GetApiHost(), projectId)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.CreatePlan(projectId, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar respBody shared.CreatePlanResponse\n\terr = json.NewDecoder(resp.Body).Decode(&respBody)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &respBody, nil\n}\n\nfunc (a *Api) GetPlan(planId string) (*shared.Plan, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s\", GetApiHost(), planId)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetPlan(planId)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar plan shared.Plan\n\terr = json.NewDecoder(resp.Body).Decode(&plan)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &plan, nil\n}\n\nfunc (a *Api) DeletePlan(planId string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s\", GetApiHost(), planId)\n\n\treq, err := http.NewRequest(http.MethodDelete, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.DeletePlan(planId)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) DeleteAllPlans(projectId string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/projects/%s/plans\", GetApiHost(), projectId)\n\n\treq, err := http.NewRequest(http.MethodDelete, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\n\t\tif didRefresh {\n\t\t\treturn a.DeleteAllPlans(projectId)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) TellPlan(planId, branch string, req shared.TellPlanRequest, onStream types.OnStreamPlan) *shared.ApiError {\n\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/tell\", GetApiHost(), planId, branch)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tvar client *http.Client\n\tif req.ConnectStream {\n\t\tclient = authenticatedStreamingClient\n\t} else {\n\t\tclient = authenticatedFastClient\n\t}\n\n\tresp, err := client.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\n\t\tif didRefresh {\n\t\t\treturn a.TellPlan(planId, branch, req, onStream)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\tif req.ConnectStream {\n\t\tlog.Println(\"Connecting stream\")\n\t\tconnectPlanRespStream(resp.Body, onStream)\n\t} else {\n\t\t// log.Println(\"Background exec - not connecting stream\")\n\t\tresp.Body.Close()\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) BuildPlan(planId, branch string, req shared.BuildPlanRequest, onStream types.OnStreamPlan) *shared.ApiError {\n\n\tlog.Println(\"Calling BuildPlan\")\n\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/build\", GetApiHost(), planId, branch)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tvar client *http.Client\n\tif req.ConnectStream {\n\t\tclient = authenticatedStreamingClient\n\t} else {\n\t\tclient = authenticatedFastClient\n\t}\n\n\tresp, err := client.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tif resp.StatusCode >= 400 {\n\t\tlog.Println(\"Error response from build plan\", resp.StatusCode)\n\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\n\t\tif didRefresh {\n\t\t\treturn a.BuildPlan(planId, branch, req, onStream)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\tif req.ConnectStream {\n\t\tlog.Println(\"Connecting stream\")\n\t\tconnectPlanRespStream(resp.Body, onStream)\n\t} else {\n\t\t// log.Println(\"Background exec - not connecting stream\")\n\t\tresp.Body.Close()\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) RespondMissingFile(planId, branch string, req shared.RespondMissingFileRequest) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/respond_missing_file\", GetApiHost(), planId, branch)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPost, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\n\t\tif didRefresh {\n\t\t\treturn a.RespondMissingFile(planId, branch, req)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n\n}\n\nfunc (a *Api) ConnectPlan(planId, branch string, onStream types.OnStreamPlan) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/connect\", GetApiHost(), planId, branch)\n\n\treq, err := http.NewRequest(http.MethodPatch, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedStreamingClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\n\t\tif didRefresh {\n\t\t\treturn a.ConnectPlan(planId, branch, onStream)\n\t\t}\n\n\t\treturn apiErr\n\t}\n\n\tconnectPlanRespStream(resp.Body, onStream)\n\n\treturn nil\n}\n\nfunc (a *Api) StopPlan(ctx context.Context, planId, branch string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/stop\", GetApiHost(), planId, branch)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodDelete, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.StopPlan(ctx, planId, branch)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) GetCurrentPlanState(planId, branch string) (*shared.CurrentPlanState, *shared.ApiError) {\n\treturn a.getCurrentPlanState(planId, branch, \"\")\n}\n\nfunc (a *Api) GetCurrentPlanStateAtSha(planId, sha string) (*shared.CurrentPlanState, *shared.ApiError) {\n\treturn a.getCurrentPlanState(planId, \"\", sha)\n}\n\nfunc (a *Api) getCurrentPlanState(planId, branch, sha string) (*shared.CurrentPlanState, *shared.ApiError) {\n\tvar serverUrl string\n\tif sha != \"\" {\n\t\tserverUrl = fmt.Sprintf(\"%s/plans/%s/current_plan/%s\", GetApiHost(), planId, sha)\n\t} else {\n\t\tserverUrl = fmt.Sprintf(\"%s/plans/%s/%s/current_plan\", GetApiHost(), planId, branch)\n\t}\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.getCurrentPlanState(planId, branch, sha)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar state shared.CurrentPlanState\n\terr = json.NewDecoder(resp.Body).Decode(&state)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &state, nil\n}\n\nfunc (a *Api) ApplyPlan(planId, branch string, req shared.ApplyPlanRequest) (string, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/apply\", GetApiHost(), planId, branch)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.ApplyPlan(planId, branch, req)\n\t\t}\n\t\treturn \"\", apiErr\n\t}\n\n\t// Reading the body on success\n\tresponseData, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Msg: fmt.Sprintf(\"error reading response body: %v\", err)}\n\t}\n\n\treturn string(responseData), nil\n}\n\nfunc (a *Api) ArchivePlan(planId string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/archive\", GetApiHost(), planId)\n\n\treq, err := http.NewRequest(http.MethodPatch, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.ArchivePlan(planId)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) UnarchivePlan(planId string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/unarchive\", GetApiHost(), planId)\n\n\treq, err := http.NewRequest(http.MethodPatch, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.ArchivePlan(planId)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) RenamePlan(planId string, name string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/rename\", GetApiHost(), planId)\n\n\treqBytes, err := json.Marshal(shared.RenamePlanRequest{Name: name})\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))\n\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.RenamePlan(planId, name)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) RejectAllChanges(planId, branch string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/reject_all\", GetApiHost(), planId, branch)\n\n\treq, err := http.NewRequest(http.MethodPatch, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\treturn a.RejectAllChanges(planId, branch)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) RejectFile(planId, branch, filePath string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/reject_file\", GetApiHost(), planId, branch)\n\n\treqBytes, err := json.Marshal(shared.RejectFileRequest{FilePath: filePath})\n\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\treq, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\ta.RejectFile(planId, branch, filePath)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) RejectFiles(planId, branch string, paths []string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/reject_files\", GetApiHost(), planId, branch)\n\n\treqBytes, err := json.Marshal(shared.RejectFilesRequest{Paths: paths})\n\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\treq, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tdidRefresh, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif didRefresh {\n\t\t\ta.RejectFiles(planId, branch, paths)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) LoadContext(planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/context\", GetApiHost(), planId, branch)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\t// use the slow client since we may be uploading relatively large files\n\tresp, err := authenticatedSlowClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.LoadContext(planId, branch, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar loadContextResponse shared.LoadContextResponse\n\terr = json.NewDecoder(resp.Body).Decode(&loadContextResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &loadContextResponse, nil\n}\n\nfunc (a *Api) UpdateContext(planId, branch string, req shared.UpdateContextRequest) (*shared.UpdateContextResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/context\", GetApiHost(), planId, branch)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t// use the slow client since we may be uploading relatively large files\n\tresp, err := authenticatedSlowClient.Do(request)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.UpdateContext(planId, branch, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar updateContextResponse shared.UpdateContextResponse\n\terr = json.NewDecoder(resp.Body).Decode(&updateContextResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &updateContextResponse, nil\n}\n\nfunc (a *Api) DeleteContext(planId, branch string, req shared.DeleteContextRequest) (*shared.DeleteContextResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/context\", GetApiHost(), planId, branch)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodDelete, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.DeleteContext(planId, branch, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar deleteContextResponse shared.DeleteContextResponse\n\terr = json.NewDecoder(resp.Body).Decode(&deleteContextResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &deleteContextResponse, nil\n}\n\nfunc (a *Api) ListContext(planId, branch string) ([]*shared.Context, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/context\", GetApiHost(), planId, branch)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListContext(planId, branch)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar contexts []*shared.Context\n\terr = json.NewDecoder(resp.Body).Decode(&contexts)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn contexts, nil\n}\n\nfunc (a *Api) LoadCachedFileMap(planId, branch string, req shared.LoadCachedFileMapRequest) (*shared.LoadCachedFileMapResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/load_cached_file_map\", GetApiHost(), planId, branch)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\treturn nil, apiErr\n\t}\n\n\tvar loadResp shared.LoadCachedFileMapResponse\n\terr = json.NewDecoder(resp.Body).Decode(&loadResp)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &loadResp, nil\n}\n\nfunc (a *Api) ListConvo(planId, branch string) ([]*shared.ConvoMessage, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/convo\", GetApiHost(), planId, branch)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListConvo(planId, branch)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar convos []*shared.ConvoMessage\n\terr = json.NewDecoder(resp.Body).Decode(&convos)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn convos, nil\n}\n\nfunc (a *Api) GetPlanStatus(planId, branch string) (string, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/status\", GetApiHost(), planId, branch)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetPlanStatus(planId, branch)\n\t\t}\n\t\treturn \"\", apiErr\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error reading response body: %v\", err)}\n\t}\n\n\treturn string(body), nil\n}\n\nfunc (a *Api) GetPlanDiffs(planId, branch string, plain bool) (string, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/diffs\", GetApiHost(), planId, branch)\n\n\tif plain {\n\t\tserverUrl += \"?plain=true\"\n\t}\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetPlanDiffs(planId, branch, plain)\n\t\t}\n\t\treturn \"\", apiErr\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error reading response body: %v\", err)}\n\t}\n\n\treturn string(body), nil\n}\n\nfunc (a *Api) ListLogs(planId, branch string) (*shared.LogResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/logs\", GetApiHost(), planId, branch)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListLogs(planId, branch)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar logs shared.LogResponse\n\terr = json.NewDecoder(resp.Body).Decode(&logs)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &logs, nil\n}\n\nfunc (a *Api) RewindPlan(planId, branch string, req shared.RewindPlanRequest) (*shared.RewindPlanResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/rewind\", GetApiHost(), planId, branch)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPatch, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.RewindPlan(planId, branch, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar rewindPlanResponse shared.RewindPlanResponse\n\terr = json.NewDecoder(resp.Body).Decode(&rewindPlanResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &rewindPlanResponse, nil\n}\n\nfunc (a *Api) SignIn(req shared.SignInRequest, customHost string) (*shared.SessionResponse, *shared.ApiError) {\n\thost := customHost\n\tif host == \"\" {\n\t\thost = CloudApiHost\n\t}\n\tserverUrl := host + \"/accounts/sign_in\"\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := unauthenticatedClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\treturn nil, apiErr\n\t}\n\n\tvar sessionResponse shared.SessionResponse\n\terr = json.NewDecoder(resp.Body).Decode(&sessionResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &sessionResponse, nil\n}\n\nfunc (a *Api) CreateAccount(req shared.CreateAccountRequest, customHost string) (*shared.SessionResponse, *shared.ApiError) {\n\thost := customHost\n\tif host == \"\" {\n\t\thost = CloudApiHost\n\t}\n\tserverUrl := host + \"/accounts\"\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := unauthenticatedClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\treturn nil, apiErr\n\t}\n\n\tvar sessionResponse shared.SessionResponse\n\terr = json.NewDecoder(resp.Body).Decode(&sessionResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &sessionResponse, nil\n}\n\nfunc (a *Api) CreateOrg(req shared.CreateOrgRequest) (*shared.CreateOrgResponse, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/orgs\"\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.CreateOrg(req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar createOrgResponse shared.CreateOrgResponse\n\terr = json.NewDecoder(resp.Body).Decode(&createOrgResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &createOrgResponse, nil\n}\n\nfunc (a *Api) GetOrgSession() (*shared.Org, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/orgs/session\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetOrgSession()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar org *shared.Org\n\n\terr = json.NewDecoder(resp.Body).Decode(&org)\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn org, nil\n}\n\nfunc (a *Api) ListOrgs() ([]*shared.Org, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/orgs\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListOrgs()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar orgs []*shared.Org\n\terr = json.NewDecoder(resp.Body).Decode(&orgs)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn orgs, nil\n}\n\nfunc (a *Api) GetOrgUserConfig() (*shared.OrgUserConfig, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/org_user_config\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetOrgUserConfig()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar orgUserConfig shared.OrgUserConfig\n\terr = json.NewDecoder(resp.Body).Decode(&orgUserConfig)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &orgUserConfig, nil\n}\n\nfunc (a *Api) UpdateOrgUserConfig(c shared.OrgUserConfig) *shared.ApiError {\n\tserverUrl := GetApiHost() + \"/org_user_config\"\n\n\treqBytes, err := json.Marshal(c)\n\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.UpdateOrgUserConfig(c)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) DeleteUser(userId string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/orgs/users/%s\", GetApiHost(), userId)\n\treq, err := http.NewRequest(http.MethodDelete, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.DeleteUser(userId)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) ListOrgRoles() ([]*shared.OrgRole, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/orgs/roles\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListOrgRoles()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar roles []*shared.OrgRole\n\terr = json.NewDecoder(resp.Body).Decode(&roles)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %s\", err)}\n\t}\n\n\treturn roles, nil\n}\n\nfunc (a *Api) InviteUser(req shared.InviteRequest) *shared.ApiError {\n\tserverUrl := GetApiHost() + \"/invites\"\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.InviteUser(req)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) ListPendingInvites() ([]*shared.Invite, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/invites/pending\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListPendingInvites()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar invites []*shared.Invite\n\terr = json.NewDecoder(resp.Body).Decode(&invites)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn invites, nil\n}\n\nfunc (a *Api) ListAcceptedInvites() ([]*shared.Invite, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/invites/accepted\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListAcceptedInvites()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar invites []*shared.Invite\n\terr = json.NewDecoder(resp.Body).Decode(&invites)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn invites, nil\n}\n\nfunc (a *Api) ListAllInvites() ([]*shared.Invite, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/invites/all\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListAllInvites()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar invites []*shared.Invite\n\terr = json.NewDecoder(resp.Body).Decode(&invites)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn invites, nil\n}\n\nfunc (a *Api) DeleteInvite(inviteId string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/invites/%s\", GetApiHost(), inviteId)\n\treq, err := http.NewRequest(http.MethodDelete, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.DeleteInvite(inviteId)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) CreateEmailVerification(email, customHost, userId string) (*shared.CreateEmailVerificationResponse, *shared.ApiError) {\n\thost := customHost\n\tif host == \"\" {\n\t\thost = CloudApiHost\n\t}\n\tserverUrl := host + \"/accounts/email_verifications\"\n\treq := shared.CreateEmailVerificationRequest{Email: email, UserId: userId}\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := unauthenticatedClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, HandleApiError(resp, errorBody)\n\t}\n\n\tvar verificationResponse shared.CreateEmailVerificationResponse\n\terr = json.NewDecoder(resp.Body).Decode(&verificationResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &verificationResponse, nil\n}\n\nfunc (a *Api) CreateSignInCode() (string, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/accounts/sign_in_codes\"\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", nil)\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.CreateSignInCode()\n\t\t}\n\t\treturn \"\", apiErr\n\t}\n\n\tvar signInCode string\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error reading response body: %v\", err)}\n\t}\n\tsignInCode = string(body)\n\n\treturn signInCode, nil\n}\n\nfunc (a *Api) SignOut() *shared.ApiError {\n\tserverUrl := GetApiHost() + \"/accounts/sign_out\"\n\n\treq, err := http.NewRequest(http.MethodPost, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\treturn HandleApiError(resp, errorBody)\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) ListUsers() (*shared.ListUsersResponse, *shared.ApiError) {\n\tserverUrl := GetApiHost() + \"/users\"\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListUsers()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar r *shared.ListUsersResponse\n\terr = json.NewDecoder(resp.Body).Decode(&r)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn r, nil\n}\n\nfunc (a *Api) ListBranches(planId string) ([]*shared.Branch, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/branches\", GetApiHost(), planId)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListBranches(planId)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar branches []*shared.Branch\n\terr = json.NewDecoder(resp.Body).Decode(&branches)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %s\", err)}\n\t}\n\n\treturn branches, nil\n}\n\nfunc (a *Api) CreateBranch(planId, branch string, req shared.CreateBranchRequest) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/branches\", GetApiHost(), planId, branch)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %s\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.CreateBranch(planId, branch, req)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) DeleteBranch(planId, branch string) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/branches/%s\", GetApiHost(), planId, branch)\n\n\treq, err := http.NewRequest(http.MethodDelete, serverUrl, nil)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %s\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Do(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.DeleteBranch(planId, branch)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) GetSettings(planId, branch string) (*shared.PlanSettings, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/settings\", GetApiHost(), planId, branch)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetSettings(planId, branch)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar settings shared.PlanSettings\n\terr = json.NewDecoder(resp.Body).Decode(&settings)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %s\", err)}\n\t}\n\n\treturn &settings, nil\n}\n\nfunc (a *Api) UpdateSettings(planId, branch string, req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/settings\", GetApiHost(), planId, branch)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %s\", err)}\n\t}\n\n\t// log.Println(\"UpdateSettings\", string(reqBytes))\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %s\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.UpdateSettings(planId, branch, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar updateRes shared.UpdateSettingsResponse\n\terr = json.NewDecoder(resp.Body).Decode(&updateRes)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %s\", err)}\n\t}\n\n\treturn &updateRes, nil\n\n}\n\nfunc (a *Api) GetOrgDefaultSettings() (*shared.PlanSettings, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/default_settings\", GetApiHost())\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetOrgDefaultSettings()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar settings shared.PlanSettings\n\terr = json.NewDecoder(resp.Body).Decode(&settings)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %s\", err)}\n\t}\n\n\treturn &settings, nil\n}\n\nfunc (a *Api) UpdateOrgDefaultSettings(req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/default_settings\", GetApiHost())\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %s\", err)}\n\t}\n\n\t// log.Println(\"UpdateSettings\", string(reqBytes))\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %s\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %s\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.UpdateOrgDefaultSettings(req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar updateRes shared.UpdateSettingsResponse\n\terr = json.NewDecoder(resp.Body).Decode(&updateRes)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %s\", err)}\n\t}\n\n\treturn &updateRes, nil\n}\n\nfunc (a *Api) GetPlanConfig(planId string) (*shared.PlanConfig, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/config\", GetApiHost(), planId)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetPlanConfig(planId)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar res shared.GetPlanConfigResponse\n\terr = json.NewDecoder(resp.Body).Decode(&res)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn res.Config, nil\n}\n\nfunc (a *Api) UpdatePlanConfig(planId string, req shared.UpdatePlanConfigRequest) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/config\", GetApiHost(), planId)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.UpdatePlanConfig(planId, req)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) GetDefaultPlanConfig() (*shared.PlanConfig, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/default_plan_config\", GetApiHost())\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetDefaultPlanConfig()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar res shared.GetDefaultPlanConfigResponse\n\terr = json.NewDecoder(resp.Body).Decode(&res)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn res.Config, nil\n}\n\nfunc (a *Api) UpdateDefaultPlanConfig(req shared.UpdateDefaultPlanConfigRequest) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/default_plan_config\", GetApiHost())\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\trequest, err := http.NewRequest(http.MethodPut, serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := authenticatedFastClient.Do(request)\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.UpdateDefaultPlanConfig(req)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) CreateCustomModels(input *shared.ModelsInput) *shared.ApiError {\n\tserverUrl := fmt.Sprintf(\"%s/custom_models\", GetApiHost())\n\tbody, err := json.Marshal(input)\n\tif err != nil {\n\t\treturn &shared.ApiError{Msg: \"Failed to marshal model\"}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(body))\n\tif err != nil {\n\t\treturn &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.CreateCustomModels(input)\n\t\t}\n\t\treturn apiErr\n\t}\n\n\treturn nil\n}\n\nfunc (a *Api) ListCustomModels() ([]*shared.CustomModel, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/custom_models\", GetApiHost())\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListCustomModels()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar models []*shared.CustomModel\n\terr = json.NewDecoder(resp.Body).Decode(&models)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn models, nil\n}\n\nfunc (a *Api) ListCustomProviders() ([]*shared.CustomProvider, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/custom_providers\", GetApiHost())\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListCustomProviders()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar providers []*shared.CustomProvider\n\terr = json.NewDecoder(resp.Body).Decode(&providers)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn providers, nil\n}\n\nfunc (a *Api) ListModelPacks() ([]*shared.ModelPack, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/model_sets\", GetApiHost())\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.ListModelPacks()\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar sets []*shared.ModelPack\n\terr = json.NewDecoder(resp.Body).Decode(&sets)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn sets, nil\n\n}\n\nfunc (a *Api) GetCreditsTransactions(pageSize, pageNum int, req shared.CreditsLogRequest) (*shared.CreditsLogResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/billing/credits_transactions?size=%d&page=%d\", GetApiHost(), pageSize, pageNum)\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetCreditsTransactions(pageSize, pageNum, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar res *shared.CreditsLogResponse\n\terr = json.NewDecoder(resp.Body).Decode(&res)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn res, nil\n}\n\nfunc (a *Api) GetCreditsSummary(req shared.CreditsLogRequest) (*shared.CreditsSummaryResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/billing/credits_summary\", GetApiHost())\n\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedFastClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetCreditsSummary(req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar res *shared.CreditsSummaryResponse\n\terr = json.NewDecoder(resp.Body).Decode(&res)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn res, nil\n}\n\nfunc (a *Api) GetBalance() (decimal.Decimal, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/billing/balance\", GetApiHost())\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn decimal.Zero, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetBalance()\n\t\t}\n\t\treturn decimal.Zero, apiErr\n\t}\n\n\tvar res *shared.GetBalanceResponse\n\terr = json.NewDecoder(resp.Body).Decode(&res)\n\tif err != nil {\n\t\treturn decimal.Zero, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn res.Balance, nil\n}\n\nfunc (a *Api) GetFileMap(req shared.GetFileMapRequest) (*shared.GetFileMapResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/file_map\", GetApiHost())\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\tresp, err := authenticatedSlowClient.Post(serverUrl, \"application/json\", bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetFileMap(req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar respBody shared.GetFileMapResponse\n\terr = json.NewDecoder(resp.Body).Decode(&respBody)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &respBody, nil\n}\n\nfunc (a *Api) GetContextBody(planId, branch, contextId string) (*shared.GetContextBodyResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/context/%s/body\", GetApiHost(), planId, branch, contextId)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetContextBody(planId, branch, contextId)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar respBody shared.GetContextBodyResponse\n\terr = json.NewDecoder(resp.Body).Decode(&respBody)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &respBody, nil\n}\n\nfunc (a *Api) AutoLoadContext(ctx context.Context, planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/auto_load_context\", GetApiHost(), planId, branch)\n\treqBytes, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error marshalling request: %v\", err)}\n\t}\n\n\t// Create a new request with context\n\thttpReq, err := http.NewRequestWithContext(ctx, \"POST\", serverUrl, bytes.NewBuffer(reqBytes))\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error creating request: %v\", err)}\n\t}\n\n\t// Set the content type header\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\t// Use the slow client since we may be uploading relatively large files\n\tresp, err := authenticatedSlowClient.Do(httpReq)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.LoadContext(planId, branch, req)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar loadContextResponse shared.LoadContextResponse\n\terr = json.NewDecoder(resp.Body).Decode(&loadContextResponse)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &loadContextResponse, nil\n}\n\nfunc (a *Api) GetBuildStatus(planId, branch string) (*shared.GetBuildStatusResponse, *shared.ApiError) {\n\tserverUrl := fmt.Sprintf(\"%s/plans/%s/%s/build_status\", GetApiHost(), planId, branch)\n\n\tresp, err := authenticatedFastClient.Get(serverUrl)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error sending request: %v\", err)}\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\terrorBody, _ := io.ReadAll(resp.Body)\n\t\tapiErr := HandleApiError(resp, errorBody)\n\t\tauthRefreshed, apiErr := refreshAuthIfNeeded(apiErr)\n\t\tif authRefreshed {\n\t\t\treturn a.GetBuildStatus(planId, branch)\n\t\t}\n\t\treturn nil, apiErr\n\t}\n\n\tvar respBody shared.GetBuildStatusResponse\n\terr = json.NewDecoder(resp.Body).Decode(&respBody)\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: fmt.Sprintf(\"error decoding response: %v\", err)}\n\t}\n\n\treturn &respBody, nil\n}\n"
  },
  {
    "path": "app/cli/api/stream.go",
    "content": "package api\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"plandex-cli/types\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\n// 3 heartbeat misses = timeout\nconst HeartbeatTimeout = 16 * time.Second\n\nfunc connectPlanRespStream(body io.ReadCloser, onStream types.OnStreamPlan) {\n\treader := bufio.NewReader(body)\n\ttimer := time.NewTimer(HeartbeatTimeout)\n\tdefer timer.Stop()\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-timer.C:\n\t\t\t\tlog.Println(\"Connection to plan stream timed out due to missing heartbeats\")\n\t\t\t\tonStream(types.OnStreamPlanParams{Msg: nil, Err: fmt.Errorf(\"connection to plan stream timed out due to missing heartbeats\")})\n\t\t\t\tbody.Close()\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\ts, err := readUntilSeparator(reader, shared.STREAM_MESSAGE_SEPARATOR)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Error reading line:\", err)\n\t\t\t\tonStream(types.OnStreamPlanParams{Msg: nil, Err: err})\n\t\t\t\tbody.Close()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttimer.Reset(HeartbeatTimeout)\n\n\t\t\t// ignore heartbeats\n\t\t\tif s == string(shared.StreamMessageHeartbeat) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar msg shared.StreamMessage\n\t\t\terr = json.Unmarshal([]byte(s), &msg)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Error unmarshalling message:\", err)\n\t\t\t\tonStream(types.OnStreamPlanParams{Msg: nil, Err: err})\n\t\t\t\tbody.Close()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// log.Println(\"connectPlanRespStream: received message:\", msg)\n\n\t\t\tonStream(types.OnStreamPlanParams{Msg: &msg, Err: nil})\n\n\t\t\tif msg.Type == shared.StreamMessageFinished || msg.Type == shared.StreamMessageError || msg.Type == shared.StreamMessageAborted {\n\t\t\t\tbody.Close()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t}\n\t}()\n}\n\nfunc readUntilSeparator(reader *bufio.Reader, separator string) (string, error) {\n\tvar result []byte\n\tsepBytes := []byte(separator)\n\tfor {\n\t\tb, err := reader.ReadByte()\n\t\tif err != nil {\n\t\t\treturn string(result), err\n\t\t}\n\t\tresult = append(result, b)\n\t\tif len(result) >= len(sepBytes) && bytes.HasSuffix(result, sepBytes) {\n\t\t\treturn string(result[:len(result)-len(separator)]), nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/cli/auth/account.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n)\n\nconst AddAccountOption = \"Add another account\"\n\nfunc SelectOrSignInOrCreate() error {\n\taccounts, err := loadAccounts()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error loading accounts: %v\", err)\n\t}\n\n\tif len(accounts) == 0 {\n\t\terr := promptSignInNewAccount()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error signing in to new account: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tvar options []string\n\tfor _, account := range accounts {\n\t\toptions = append(options, fmt.Sprintf(\"<%s> %s\", account.UserName, account.Email))\n\t}\n\n\toptions = append(options, AddAccountOption)\n\n\t// either select from existing accounts or sign in/create account\n\n\tselectedOpt, err := term.SelectFromList(\"Select an account:\", options)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error selecting account: %v\", err)\n\t}\n\n\tif selectedOpt == AddAccountOption {\n\t\terr := promptSignInNewAccount()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error prompting for sign in to new account: %v\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tvar selected *shared.ClientAccount\n\tfor i, opt := range options {\n\t\tif selectedOpt == opt {\n\t\t\tselected = accounts[i]\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif selected == nil {\n\t\treturn fmt.Errorf(\"error selecting account: account not found\")\n\t}\n\n\tselectedAuth := *selected\n\n\tsetAuth(&shared.ClientAuth{\n\t\tClientAccount: selectedAuth,\n\t})\n\n\tterm.StartSpinner(\"\")\n\torgs, apiErr := apiClient.ListOrgs()\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error listing orgs: %v\", apiErr.Msg)\n\t}\n\n\torg, err := resolveOrgAuth(orgs, selectedAuth.IsLocalMode)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error resolving org: %v\", err)\n\t}\n\n\terr = setAuth(&shared.ClientAuth{\n\t\tClientAccount:        *selected,\n\t\tOrgId:                org.Id,\n\t\tOrgName:              org.Name,\n\t\tOrgIsTrial:           org.IsTrial,\n\t\tIntegratedModelsMode: org.IntegratedModelsMode,\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting auth: %v\", err)\n\t}\n\n\t_, apiErr = apiClient.GetOrgSession()\n\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error getting org session: %v\", apiErr.Msg)\n\t}\n\n\tfmt.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))\n\tfmt.Println()\n\n\tif !term.IsRepl {\n\t\tterm.PrintCmds(\"\", \"\")\n\t}\n\n\treturn nil\n}\n\nfunc SignInWithCode(code, host string) error {\n\tterm.StartSpinner(\"\")\n\tres, apiErr := apiClient.SignIn(shared.SignInRequest{\n\t\tPin:          code,\n\t\tIsSignInCode: true,\n\t}, host)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error signing in: %v\", apiErr.Msg)\n\t}\n\n\treturn handleSignInResponse(res, host)\n}\n\nfunc promptInitialAuth() error {\n\tfmt.Println(\"👋 Hey there!\\nIt looks like this is your first time using Plandex on this computer.\")\n\n\terr := SelectOrSignInOrCreate()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error selecting or signing in to account: %v\", err)\n\t}\n\n\treturn nil\n}\n\nconst (\n\t// SignInCloudOption = \"Plandex Cloud\"\n\tSignInLocalOption = \"Local mode host\"\n\tSignInOtherOption = \"Another host\"\n)\n\nfunc promptSignInNewAccount() error {\n\tselected, err := term.SelectFromList(\"Use local mode or another host?\", []string{SignInLocalOption, SignInOtherOption})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error selecting sign in option: %v\", err)\n\t}\n\n\tvar host string\n\tvar email string\n\n\tif selected == SignInLocalOption {\n\t\thost, err = term.GetRequiredUserStringInputWithDefault(\"Host:\", \"http://localhost:8099\")\n\t} else {\n\t\thost, err = term.GetRequiredUserStringInput(\"Host:\")\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error prompting host: %v\", err)\n\t}\n\n\tif selected == SignInLocalOption {\n\t\temail = \"local-admin@plandex.ai\"\n\t} else {\n\t\temail, err = term.GetRequiredUserStringInput(\"Your email:\")\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error prompting email: %v\", err)\n\t}\n\n\tres, err := verifyEmail(email, host)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error verifying email: %v\", err)\n\t}\n\n\tif res.hasAccount {\n\t\terr := signIn(email, res.pin, host)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error signing in: %v\", err)\n\t\t}\n\t} else {\n\t\terr := createAccount(email, res.pin, host, res.isLocalMode)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating account: %v\", err)\n\t\t}\n\t}\n\n\tif !term.IsRepl {\n\t\tterm.PrintCmds(\"\", \"\")\n\t}\n\n\treturn nil\n}\n\ntype verifyEmailRes struct {\n\thasAccount  bool\n\tisLocalMode bool\n\tpin         string\n}\n\nfunc verifyEmail(email, host string) (*verifyEmailRes, error) {\n\tterm.StartSpinner(\"\")\n\tres, apiErr := apiClient.CreateEmailVerification(email, host, \"\")\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\treturn nil, fmt.Errorf(\"error creating email verification: %v\", apiErr.Msg)\n\t}\n\n\tif res.IsLocalMode {\n\t\treturn &verifyEmailRes{\n\t\t\thasAccount:  res.HasAccount,\n\t\t\tisLocalMode: true,\n\t\t\tpin:         \"\",\n\t\t}, nil\n\t}\n\n\tfmt.Println(\"✉️  You'll now receive a 6 character pin by email. It will be valid for 5 minutes.\")\n\n\tpin, err := term.GetUserPasswordInput(\"Please enter your pin:\")\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error prompting pin: %v\", err)\n\t}\n\n\treturn &verifyEmailRes{\n\t\thasAccount:  res.HasAccount,\n\t\tisLocalMode: false,\n\t\tpin:         pin,\n\t}, nil\n}\n\nfunc signIn(email, pin, host string) error {\n\tterm.StartSpinner(\"\")\n\tres, apiErr := apiClient.SignIn(shared.SignInRequest{\n\t\tEmail: email,\n\t\tPin:   pin,\n\t}, host)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error signing in: %v\", apiErr.Msg)\n\t}\n\n\treturn handleSignInResponse(res, host)\n}\n\nfunc handleSignInResponse(res *shared.SessionResponse, host string) error {\n\tisLocalMode := host != \"\" && res.IsLocalMode\n\n\terr := setAuth(&shared.ClientAuth{\n\t\tClientAccount: shared.ClientAccount{\n\t\t\tEmail:       res.Email,\n\t\t\tUserId:      res.UserId,\n\t\t\tUserName:    res.UserName,\n\t\t\tToken:       res.Token,\n\t\t\tIsTrial:     false,\n\t\t\tIsCloud:     host == \"\",\n\t\t\tHost:        host,\n\t\t\tIsLocalMode: isLocalMode,\n\t\t},\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting auth: %v\", err)\n\t}\n\n\torg, err := resolveOrgAuth(res.Orgs, isLocalMode)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error resolving org: %v\", err)\n\t}\n\n\tCurrent.OrgId = org.Id\n\tCurrent.OrgName = org.Name\n\tCurrent.IntegratedModelsMode = org.IntegratedModelsMode\n\n\terr = writeCurrentAuth()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing auth: %v\", err)\n\t}\n\n\tfmt.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))\n\tfmt.Println()\n\n\treturn nil\n}\n\nfunc createAccount(email, pin, host string, isLocalMode bool) error {\n\tvar name string\n\n\tif isLocalMode {\n\t\tname = \"Local Admin\"\n\t} else {\n\t\tvar err error\n\t\tname, err = term.GetUserStringInput(\"Your name:\")\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error prompting name: %v\", err)\n\t\t}\n\t}\n\n\tterm.StartSpinner(\"🌟 Creating account...\")\n\tres, apiErr := apiClient.CreateAccount(shared.CreateAccountRequest{\n\t\tEmail:    email,\n\t\tUserName: name,\n\t\tPin:      pin,\n\t}, host)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error creating account: %v\", apiErr.Msg)\n\t}\n\n\tif res.IsLocalMode {\n\t\tisLocalMode = true\n\t}\n\n\terr := setAuth(&shared.ClientAuth{\n\t\tClientAccount: shared.ClientAccount{\n\t\t\tEmail:       res.Email,\n\t\t\tUserId:      res.UserId,\n\t\t\tUserName:    res.UserName,\n\t\t\tToken:       res.Token,\n\t\t\tIsTrial:     false,\n\t\t\tIsCloud:     host == \"\",\n\t\t\tHost:        host,\n\t\t\tIsLocalMode: isLocalMode,\n\t\t},\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting auth: %v\", err)\n\t}\n\n\torg, err := resolveOrgAuth(res.Orgs, isLocalMode)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error resolving org: %v\", err)\n\t}\n\n\tif org == nil {\n\t\treturn fmt.Errorf(\"no org selected\")\n\t}\n\n\tCurrent.OrgId = org.Id\n\tCurrent.OrgName = org.Name\n\tCurrent.IntegratedModelsMode = org.IntegratedModelsMode\n\n\terr = writeCurrentAuth()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing auth: %v\", err)\n\t}\n\n\tfmt.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))\n\tfmt.Println()\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/auth/api.go",
    "content": "package auth\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"plandex-cli/types\"\n\t\"plandex-cli/version\"\n\n\tshared \"plandex-shared\"\n)\n\nvar apiClient types.ApiClient\n\nfunc SetApiClient(client types.ApiClient) {\n\tapiClient = client\n}\n\nfunc SetAuthHeader(req *http.Request) error {\n\tif Current == nil {\n\t\treturn fmt.Errorf(\"error setting auth header: auth not loaded\")\n\t}\n\thash := Current.ToHash()\n\n\tauthHeader := shared.AuthHeader{\n\t\tToken: Current.Token,\n\t\tOrgId: Current.OrgId,\n\t\tHash:  hash,\n\t}\n\n\tbytes, err := json.Marshal(authHeader)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling auth header: %v\", err)\n\t}\n\n\t// base64 encode\n\ttoken := base64.URLEncoding.EncodeToString(bytes)\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\n\treturn nil\n}\n\nfunc SetVersionHeader(req *http.Request) {\n\treq.Header.Set(\"X-Client-Version\", version.Version)\n}\n"
  },
  {
    "path": "app/cli/auth/auth.go",
    "content": "package auth\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n)\n\nvar openUnauthenticatedCloudURL func(msg, path string)\nvar openAuthenticatedURL func(msg, path string)\n\nfunc SetOpenUnauthenticatedCloudURLFn(fn func(msg, path string)) {\n\topenUnauthenticatedCloudURL = fn\n}\n\nfunc SetOpenAuthenticatedURLFn(fn func(msg, path string)) {\n\topenAuthenticatedURL = fn\n}\n\nfunc MustResolveAuthWithOrg() {\n\tMustResolveAuth(true)\n}\n\nfunc MustResolveAuth(requireOrg bool) {\n\tif apiClient == nil {\n\t\tterm.OutputErrorAndExit(\"error resolving auth: api client not set\")\n\t}\n\n\t// load HomeAuthPath file into ClientAuth struct\n\tbytes, err := os.ReadFile(fs.HomeAuthPath)\n\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\terr = promptInitialAuth()\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"error resolving auth: %v\", err)\n\t\t\t}\n\n\t\t\treturn\n\t\t} else {\n\t\t\tterm.OutputErrorAndExit(\"error reading auth.json: %v\", err)\n\t\t}\n\t}\n\n\tvar auth shared.ClientAuth\n\terr = json.Unmarshal(bytes, &auth)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error unmarshalling auth.json: %v\", err)\n\t}\n\n\tCurrent = &auth\n\n\tif requireOrg && Current.OrgId == \"\" {\n\t\tterm.StartSpinner(\"\")\n\t\torgs, apiErr := apiClient.ListOrgs()\n\t\tterm.StopSpinner()\n\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error listing orgs: %v\", apiErr.Msg)\n\t\t}\n\n\t\torg, err := resolveOrgAuth(orgs, Current.IsLocalMode)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error resolving org: %v\", err)\n\t\t}\n\n\t\tif org.Id == \"\" {\n\t\t\t// still no org--exit now\n\t\t\tterm.OutputErrorAndExit(\"No org\")\n\t\t}\n\n\t\tCurrent.OrgId = org.Id\n\t\tCurrent.OrgName = org.Name\n\t\tCurrent.IntegratedModelsMode = org.IntegratedModelsMode\n\n\t\terr = writeCurrentAuth()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error writing auth: %v\", err)\n\t\t}\n\t}\n}\n\nfunc RefreshInvalidToken() error {\n\tif Current == nil {\n\t\treturn fmt.Errorf(\"error refreshing token: auth not loaded\")\n\t}\n\tres, err := verifyEmail(Current.Email, Current.Host)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error verifying email: %v\", err)\n\t}\n\n\tif res.hasAccount {\n\t\treturn signIn(Current.Email, res.pin, Current.Host)\n\t} else {\n\t\thost := Current.Host\n\t\tif host == \"\" {\n\t\t\thost = \"Plandex Cloud\"\n\t\t}\n\n\t\tterm.OutputErrorAndExit(\"Account %s not found on %s\", Current.Email, host)\n\t}\n\n\treturn nil\n}\n\nfunc RefreshAuth() error {\n\tif Current == nil {\n\t\treturn fmt.Errorf(\"error refreshing auth: auth not loaded\")\n\t}\n\n\torg, apiErr := apiClient.GetOrgSession()\n\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error getting org session: %v\", apiErr.Msg)\n\t}\n\n\tCurrent.OrgName = org.Name\n\tCurrent.OrgIsTrial = org.IsTrial\n\tCurrent.IntegratedModelsMode = org.IntegratedModelsMode\n\n\terr := writeCurrentAuth()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing auth: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/auth/org.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/term\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc resolveOrgAuth(orgs []*shared.Org, isLocalMode bool) (*shared.Org, error) {\n\tvar org *shared.Org\n\tvar err error\n\n\tif len(orgs) == 0 {\n\t\tif isLocalMode {\n\t\t\torg, err = createOrg(isLocalMode)\n\t\t} else {\n\t\t\torg, err = promptNoOrgs()\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error prompting no orgs: %v\", err)\n\t\t}\n\n\t} else if len(orgs) == 1 {\n\t\torg = orgs[0]\n\t} else {\n\t\torg, err = selectOrg(orgs, isLocalMode)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error selecting org: %v\", err)\n\t\t}\n\t}\n\n\treturn org, nil\n}\n\nfunc promptNoOrgs() (*shared.Org, error) {\n\tfmt.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.\")\n\n\tshouldCreate, err := term.ConfirmYesNo(\"Create a new org now?\")\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error prompting create org: %v\", err)\n\t}\n\n\tif shouldCreate {\n\t\treturn createOrg(false)\n\t}\n\n\treturn nil, nil\n}\n\nfunc createOrg(isLocalMode bool) (*shared.Org, error) {\n\tvar err error\n\tvar name string\n\tvar autoAddDomainUsers bool\n\n\tif isLocalMode {\n\t\tname = \"Local Org\"\n\t} else {\n\t\tname, err = term.GetRequiredUserStringInput(\"Org name:\")\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error prompting org name: %v\", err)\n\t}\n\n\tif !isLocalMode {\n\t\tautoAddDomainUsers, err = promptAutoAddUsersIfValid(Current.Email)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error prompting auto add domain users: %v\", err)\n\t\t}\n\t}\n\n\tterm.StartSpinner(\"\")\n\tres, apiErr := apiClient.CreateOrg(shared.CreateOrgRequest{\n\t\tName:               name,\n\t\tAutoAddDomainUsers: autoAddDomainUsers,\n\t})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\treturn nil, fmt.Errorf(\"error creating org: %v\", apiErr.Msg)\n\t}\n\n\treturn &shared.Org{Id: res.Id, Name: name}, nil\n}\n\nfunc promptAutoAddUsersIfValid(email string) (bool, error) {\n\tuserDomain := strings.Split(email, \"@\")[1]\n\tvar autoAddDomainUsers bool\n\tvar err error\n\tif !shared.IsEmailServiceDomain(userDomain) {\n\t\tfmt.Println(\"With domain auto-join, you can allow any user with an email ending in @\"+userDomain, \"to auto-join this org.\")\n\t\tautoAddDomainUsers, err = term.ConfirmYesNo(fmt.Sprintf(\"Enable auto-join for %s?\", userDomain))\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\treturn autoAddDomainUsers, nil\n}\n\nconst CreateOrgOption = \"Create a new org\"\n\nfunc selectOrg(orgs []*shared.Org, isLocalMode bool) (*shared.Org, error) {\n\tvar options []string\n\tfor _, org := range orgs {\n\t\toptions = append(options, org.Name)\n\t}\n\toptions = append(options, CreateOrgOption)\n\n\tselected, err := term.SelectFromList(\"Select an org:\", options)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error selecting org: %v\", err)\n\t}\n\n\tif selected == CreateOrgOption {\n\t\treturn createOrg(isLocalMode)\n\t}\n\n\tvar selectedOrg *shared.Org\n\tfor _, org := range orgs {\n\t\tif org.Name == selected {\n\t\t\tselectedOrg = org\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif selectedOrg == nil {\n\t\treturn nil, fmt.Errorf(\"error selecting org: org not found\")\n\t}\n\n\treturn selectedOrg, nil\n}\n"
  },
  {
    "path": "app/cli/auth/state.go",
    "content": "package auth\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/fs\"\n\n\tshared \"plandex-shared\"\n)\n\nvar Current *shared.ClientAuth\n\nfunc loadAccounts() ([]*shared.ClientAccount, error) {\n\tbytes, err := os.ReadFile(fs.HomeAccountsPath)\n\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\t// no accounts\n\t\t\treturn []*shared.ClientAccount{}, nil\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"error reading accounts.json: %v\", err)\n\t\t}\n\t}\n\n\tvar accounts []*shared.ClientAccount\n\terr = json.Unmarshal(bytes, &accounts)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling accounts.json: %v\", err)\n\t}\n\n\treturn accounts, nil\n}\n\nfunc setAuth(auth *shared.ClientAuth) error {\n\terr := storeAccount(&auth.ClientAccount)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error storing account: %v\", err)\n\t}\n\n\tCurrent = auth\n\n\terr = writeCurrentAuth()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing auth: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc storeAccount(toStore *shared.ClientAccount) error {\n\taccounts, err := loadAccounts()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error loading accounts: %v\", err)\n\t}\n\n\tfound := false\n\tfor i, account := range accounts {\n\t\tif account.UserId == toStore.UserId {\n\t\t\taccounts[i] = toStore\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\taccounts = append(accounts, toStore)\n\t}\n\n\tbytes, err := json.Marshal(accounts)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling accounts: %v\", err)\n\t}\n\n\terr = os.WriteFile(fs.HomeAccountsPath, bytes, os.ModePerm)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing accounts: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc writeCurrentAuth() error {\n\tif Current == nil {\n\t\treturn fmt.Errorf(\"error writing auth: auth not loaded\")\n\t}\n\n\tbytes, err := json.Marshal(Current)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling auth: %v\", err)\n\t}\n\n\terr = os.WriteFile(fs.HomeAuthPath, bytes, os.ModePerm)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing auth: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/auth/trial.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/term\"\n\t\"time\"\n)\n\nfunc ConvertTrial() {\n\topenAuthenticatedURL(\"Opening Plandex Cloud upgrade flow in your browser.\", \"/settings/billing?upgrade=1&cliUpgrade=1\")\n\n\tfmt.Println(\"\\nCommand will continue automatically once you've upgraded...\")\n\tfmt.Println()\n\tterm.StartSpinner(\"\")\n\n\tstartTime := time.Now()\n\texpirationTime := startTime.Add(1 * time.Hour)\n\n\tfor time.Now().Before(expirationTime) {\n\t\torg, apiErr := apiClient.GetOrgSession()\n\n\t\tif apiErr != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"error getting org session: %s\", apiErr.Msg)\n\t\t}\n\n\t\tif org != nil && !org.IsTrial {\n\t\t\tterm.StopSpinner()\n\t\t\tfmt.Println(\"🚀 Trial upgraded\")\n\t\t\tfmt.Println()\n\t\t\treturn\n\t\t}\n\n\t\ttime.Sleep(1500 * time.Millisecond)\n\t}\n\n\tterm.StopSpinner()\n\tterm.OutputErrorAndExit(\"Timed out waiting for upgrade. Please try again. Email support@plandex.ai if the problem persists.\")\n}\n\nfunc startTrial() {\n\tterm.StartSpinner(\"\")\n\tcliTrialToken, apiErr := apiClient.CreateCliTrialSession()\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"error starting trial: %s\", apiErr.Msg)\n\t}\n\n\topenUnauthenticatedCloudURL(\n\t\t\"Opening Plandex Cloud trial flow in your browser.\",\n\t\tfmt.Sprintf(\"/start?cliTrialToken=%s\", cliTrialToken),\n\t)\n\n\tfmt.Println(\"\\nCommand will continue automatically once you've started your trial...\")\n\tfmt.Println()\n\tterm.StartSpinner(\"\")\n\n\tstartTime := time.Now()\n\texpirationTime := startTime.Add(1 * time.Hour)\n\n\tfor time.Now().Before(expirationTime) {\n\t\tcliTrialSession, apiErr := apiClient.GetCliTrialSession(cliTrialToken)\n\n\t\tif apiErr != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"error getting cli trial session: %s\", apiErr.Msg)\n\t\t}\n\n\t\tif cliTrialSession != nil {\n\t\t\t// Trial session is valid, break the loop and sign in\n\t\t\tterm.StopSpinner()\n\t\t\tfmt.Println(\"🚀 Trial started\")\n\t\t\tfmt.Println()\n\t\t\terr := handleSignInResponse(cliTrialSession, \"\")\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"error signing in after trial started: %s\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\ttime.Sleep(1500 * time.Millisecond)\n\t}\n\n\tterm.StopSpinner()\n\tterm.OutputErrorAndExit(\"Timed out waiting for trial to start. Please try again. Email support@plandex.ai if the problem persists.\")\n}\n"
  },
  {
    "path": "app/cli/cmd/apply.go",
    "content": "package cmd\n\nimport (\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/plan_exec\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar autoCommit, skipCommit, autoExec bool\n\nfunc init() {\n\tinitApplyFlags(applyCmd, false)\n\tinitExecScriptFlags(applyCmd)\n\tRootCmd.AddCommand(applyCmd)\n\n\tapplyCmd.Flags().BoolVar(&fullAuto, \"full\", false, \"Apply the plan and debug in full auto mode\")\n}\n\nvar applyCmd = &cobra.Command{\n\tUse:     \"apply\",\n\tAliases: []string{\"ap\"},\n\tShort:   \"Apply a plan to the project\",\n\tRun:     apply,\n}\n\nfunc apply(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif fullAuto {\n\t\tterm.StartSpinner(\"\")\n\t\tconfig := lib.MustGetCurrentPlanConfig()\n\t\t_, updatedConfig, printFn := resolveAutoModeSilent(config)\n\t\tlib.SetCachedPlanConfig(updatedConfig)\n\t\tterm.StopSpinner()\n\t\tprintFn()\n\t}\n\n\tmustSetPlanExecFlags(cmd, true)\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tapplyFlags := types.ApplyFlags{\n\t\tAutoConfirm: true,\n\t\tAutoCommit:  autoCommit,\n\t\tNoCommit:    skipCommit,\n\t\tAutoExec:    autoExec,\n\t\tNoExec:      noExec,\n\t\tAutoDebug:   autoDebug,\n\t}\n\n\ttellFlags := types.TellFlags{\n\t\tTellBg:      tellBg,\n\t\tTellStop:    tellStop,\n\t\tTellNoBuild: tellNoBuild,\n\t\tAutoContext: tellAutoContext,\n\t\tExecEnabled: !noExec,\n\t\tAutoApply:   tellAutoApply,\n\t}\n\n\tlib.MustApplyPlan(lib.ApplyPlanParams{\n\t\tPlanId:     lib.CurrentPlanId,\n\t\tBranch:     lib.CurrentBranch,\n\t\tApplyFlags: applyFlags,\n\t\tTellFlags:  tellFlags,\n\t\tOnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),\n\t})\n}\n"
  },
  {
    "path": "app/cli/cmd/archive.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar archiveCmd = &cobra.Command{\n\tUse:     \"archive [name-or-index]\",\n\tAliases: []string{\"arc\"},\n\tShort:   \"Archive a plan\",\n\tArgs:    cobra.MaximumNArgs(1),\n\tRun:     archive,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(archiveCmd)\n}\n\nfunc archive(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tvar nameOrIdx string\n\tif len(args) > 0 {\n\t\tnameOrIdx = strings.TrimSpace(args[0])\n\t}\n\n\tvar plan *shared.Plan\n\n\tterm.StartSpinner(\"\")\n\tplans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plans: %v\", apiErr)\n\t}\n\n\tif len(plans) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No plans available to archive\")\n\t\treturn\n\t}\n\n\tif nameOrIdx == \"\" {\n\t\topts := make([]string, len(plans))\n\t\tfor i, p := range plans {\n\t\t\topts[i] = p.Name\n\t\t}\n\n\t\tselected, err := term.SelectFromList(\"Select a plan to archive\", opts)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting plan: %v\", err)\n\t\t}\n\n\t\tfor _, p := range plans {\n\t\t\tif p.Name == selected {\n\t\t\t\tplan = p\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tidx, err := strconv.Atoi(nameOrIdx)\n\t\tif err == nil && idx > 0 && idx <= len(plans) {\n\t\t\tplan = plans[idx-1]\n\t\t} else {\n\t\t\tfor _, p := range plans {\n\t\t\t\tif p.Name == nameOrIdx {\n\t\t\t\t\tplan = p\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif plan == nil {\n\t\tterm.OutputErrorAndExit(\"Plan not found\")\n\t}\n\n\terr := api.Client.ArchivePlan(plan.Id)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error archiving plan: %v\", err)\n\t}\n\n\tfmt.Printf(\"✅ Plan %s archived\\n\", color.New(color.Bold, term.ColorHiYellow).Sprint(plan.Name))\n\n\tfmt.Println()\n\n\tterm.PrintCmds(\"\", \"plans --archived\", \"unarchive\")\n}\n"
  },
  {
    "path": "app/cli/cmd/billing.go",
    "content": "package cmd\n\nimport (\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/ui\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar billingCmd = &cobra.Command{\n\tUse:   \"billing\",\n\tShort: \"Open the billing page in the browser\",\n\tRun:   billing,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(billingCmd)\n}\n\nfunc billing(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tif !auth.Current.IsCloud {\n\t\tterm.OutputErrorAndExit(\"This command is only available for Plandex Cloud accounts.\")\n\t}\n\n\tui.OpenAuthenticatedURL(\"Opening billing page in your default browser...\", \"/settings/billing\")\n}\n"
  },
  {
    "path": "app/cli/cmd/branches.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/format\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar branchesCmd = &cobra.Command{\n\tUse:     \"branches\",\n\tAliases: []string{\"br\"},\n\tShort:   \"List plan branches\",\n\tRun:     branches,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(branchesCmd)\n}\n\nfunc branches(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tbranches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)\n\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting branches: %v\", apiErr)\n\t\treturn\n\t}\n\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(false)\n\ttable.SetHeader([]string{\"#\", \"Name\", \"Updated\" /* \"Created\",*/, \"Context\", \"Convo\"})\n\n\tfor i, b := range branches {\n\t\tnum := strconv.Itoa(i + 1)\n\t\tif b.Name == lib.CurrentBranch {\n\t\t\tnum = color.New(color.Bold, term.ColorHiGreen).Sprint(num)\n\t\t}\n\n\t\tvar name string\n\t\tif b.Name == lib.CurrentBranch {\n\t\t\tname = color.New(color.Bold, term.ColorHiGreen).Sprint(b.Name) + \" 👈\"\n\t\t} else {\n\t\t\tname = b.Name\n\t\t}\n\n\t\trow := []string{\n\t\t\tnum,\n\t\t\tname,\n\t\t\tformat.Time(b.UpdatedAt),\n\t\t\t// format.Time(b.CreatedAt),\n\t\t\tstrconv.Itoa(b.ContextTokens) + \" 🪙\",\n\t\t\tstrconv.Itoa(b.ConvoTokens) + \" 🪙\",\n\t\t}\n\n\t\tvar style []tablewriter.Colors\n\t\tif b.Name == lib.CurrentPlanId {\n\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t{tablewriter.FgGreenColor, tablewriter.Bold},\n\t\t\t}\n\t\t} else {\n\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t{tablewriter.Bold},\n\t\t\t}\n\t\t}\n\n\t\ttable.Rich(row, style)\n\n\t}\n\ttable.Render()\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"checkout\", \"delete-branch\")\n\n}\n"
  },
  {
    "path": "app/cli/cmd/browser.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"syscall\"\n\t\"time\"\n\n\tchrome_log \"github.com/chromedp/cdproto/log\"\n\tchrome_runtime \"github.com/chromedp/cdproto/runtime\"\n\t\"github.com/chromedp/cdproto/target\"\n\t\"github.com/chromedp/chromedp\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar timeoutSeconds int\n\n// browserCmd is our cobra command\nvar browserCmd = &cobra.Command{\n\tUse:   \"browser [urls...]\",\n\tShort: \"Open browser windows with given URLs, capturing console logs and exiting on JS errors\",\n\tRunE:  browser,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(browserCmd)\n\tbrowserCmd.Flags().IntVar(&timeoutSeconds, \"timeout\", 10, \"Timeout in seconds for browser to load\")\n}\n\n// browser is the main function for our command.\nfunc browser(cmd *cobra.Command, args []string) error {\n\tif len(args) == 0 {\n\t\treturn errors.New(\"no URLs provided\")\n\t}\n\n\t// See if we can find Chrome or Firefox in PATH:\n\tchromePath, _ := findChrome()\n\n\tif chromePath != \"\" {\n\t\tfmt.Println(\"Using Chrome (not default browser, but found in PATH).\")\n\t\treturn openChromeWithLogs(args)\n\t}\n\n\t// Fallback: open with OS default tool (open/xdg-open)\n\tfmt.Println(\"No Chrome or Firefox found; falling back to OS default opener.\")\n\treturn openWithOSDefault(args)\n}\n\n// findChrome returns the path to Chrome/Chromium if found, or \"\" if not found.\nfunc findChrome() (string, error) {\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\t// macOS standard installation paths\n\t\tmacPaths := []string{\n\t\t\t\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n\t\t\t\"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n\t\t}\n\t\tfor _, p := range macPaths {\n\t\t\tif _, err := os.Stat(p); err == nil {\n\t\t\t\treturn p, nil\n\t\t\t}\n\t\t}\n\tcase \"linux\", \"freebsd\":\n\t\t// Linux/FreeBSD (usually in PATH)\n\t\tcandidates := []string{\n\t\t\t\"google-chrome\",\n\t\t\t\"google-chrome-stable\",\n\t\t\t\"chromium\",\n\t\t\t\"chromium-browser\",\n\t\t}\n\t\tfor _, c := range candidates {\n\t\t\tif path, err := exec.LookPath(c); err == nil {\n\t\t\t\treturn path, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", errors.New(\"Chrome/Chromium not found\")\n}\n\nvar pages = map[target.ID]string{}\n\n// openChromeWithLogs uses chromedp to open each URL in a visible Chrome browser,\n// logs JS console messages, and exits on the first JS error (or if user presses Ctrl+C).\nfunc openChromeWithLogs(urls []string) error {\n\tif len(urls) == 0 {\n\t\treturn errors.New(\"no URLs provided\")\n\t}\n\n\tfmt.Println(\"Launching Chrome with console log capture...\")\n\t// 1) Create a cancellable context that sets up a Chrome ExecAllocator\n\trootCtx, cancelAllocator := chromedp.NewExecAllocator(context.Background(),\n\t\tchromedp.Flag(\"headless\", false), // Visible, not headless\n\t\tchromedp.Flag(\"disable-gpu\", false),\n\t\tchromedp.Flag(\"no-first-run\", true),             // Avoid \"Welcome\" dialog\n\t\tchromedp.Flag(\"no-default-browser-check\", true), // Avoid default browser dialog\n\t\tchromedp.Flag(\"disable-default-apps\", true),     // Prevent default apps/extensions from loading\n\t\tchromedp.Flag(\"remote-debugging-port\", \"0\"),     // Ensures separate debug session\n\t)\n\tdefer cancelAllocator()\n\n\t// 2) Create a new browser context from the allocator\n\tbrowserCtx, cancelBrowser := chromedp.NewContext(rootCtx)\n\tdefer cancelBrowser()\n\n\t// Add this to get the initial target\n\tvar initialTargetID target.ID\n\tchromedp.ListenTarget(browserCtx, func(ev interface{}) {\n\t\tif e, ok := ev.(*target.EventTargetCreated); ok {\n\t\t\t// fmt.Printf(\"[DEBUG] Got event: %#v\\n\", e)\n\t\t\tif e.TargetInfo.Type == \"page\" {\n\t\t\t\tinitialTargetID = e.TargetInfo.TargetID\n\t\t\t}\n\t\t}\n\t})\n\n\tif err := chromedp.Run(\n\t\tbrowserCtx,\n\t\tchrome_runtime.Enable(),\n\t\tchrome_log.Enable(),\n\t); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// Listen for console API calls & JS exceptions\n\tchromedp.ListenBrowser(browserCtx, func(ev interface{}) {\n\t\t// fmt.Printf(\"[DEBUG] Got event: %#v\\n\", ev)\n\t\tswitch e := ev.(type) {\n\n\t\tcase *target.EventTargetCreated:\n\t\tcase *target.EventAttachedToTarget:\n\t\t\t// This sometimes also includes target info\n\t\t\tinfo := e.TargetInfo\n\t\t\tif info.Type == \"page\" {\n\t\t\t\turl := info.URL\n\t\t\t\tif url == \"about:blank\" {\n\t\t\t\t\turl = urls[0]\n\t\t\t\t}\n\t\t\t\tpages[info.TargetID] = url\n\t\t\t}\n\n\t\tcase *target.EventTargetDestroyed:\n\t\t\t// Something was destroyed\n\t\t\tdestroyedID := e.TargetID\n\t\t\t// Check if it was a top-level page\n\t\t\tif url, ok := pages[destroyedID]; ok {\n\t\t\t\tfmt.Printf(\"[CHROME] Closed page: %s\\n\", url)\n\t\t\t\t// remove from map\n\t\t\t\tdelete(pages, destroyedID)\n\t\t\t\t// If that was your last page, do something:\n\t\t\t\tif len(pages) == 0 {\n\t\t\t\t\tcancelBrowser()\n\t\t\t\t\tcancelAllocator()\n\t\t\t\t\tos.Exit(0)\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Printf(\"num pages left: %d\\n\", len(pages))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n\n\t// Channels to handle signals & error detection\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)\n\n\terrChan := make(chan bool, 1)\n\n\t// 3) Open all URLs in new tabs\n\tfor i, url := range urls {\n\t\t// Reuse the initial browser tab for the first URL\n\t\tvar ctx context.Context\n\t\tif i == 0 {\n\t\t\tctx, _ = chromedp.NewContext(browserCtx, chromedp.WithTargetID(initialTargetID))\n\t\t} else {\n\t\t\t// Create a new tab for each additional URL\n\t\t\tctx, _ = chromedp.NewContext(browserCtx)\n\t\t}\n\t\ttabCtx, _ := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)\n\n\t\tchromedp.ListenTarget(tabCtx, func(ev interface{}) {\n\t\t\tswitch e := ev.(type) {\n\t\t\tcase *chrome_runtime.EventConsoleAPICalled:\n\t\t\t\tlogType := e.Type.String()\n\t\t\t\tfor _, arg := range e.Args {\n\t\t\t\t\tval := arg.Value\n\t\t\t\t\tfmt.Printf(\"[CHROME:%s] %v\\n\", logType, val)\n\t\t\t\t\tif logType == \"error\" {\n\t\t\t\t\t\tfmt.Println(\"❌ Chrome console error\")\n\t\t\t\t\t\terrChan <- true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase *chrome_log.EventEntryAdded:\n\t\t\t\tfmt.Printf(\"[CHROME:Log:%s] %s\\n\", e.Entry.Level, e.Entry.Text)\n\t\t\tcase *chrome_runtime.EventExceptionThrown:\n\t\t\t\tfmt.Printf(\"[CHROME:Exception] %s\\n\", e.ExceptionDetails.Error())\n\t\t\t\tfmt.Println(\"❌ JavaScript exception\")\n\t\t\t\terrChan <- true\n\t\t\t}\n\t\t})\n\n\t\tif err := chromedp.Run(\n\t\t\ttabCtx,\n\t\t\tchrome_runtime.Enable(),\n\t\t\tchrome_log.Enable(),\n\t\t\tchromedp.Navigate(url),\n\t\t\tchromedp.WaitReady(\"body\", chromedp.ByQuery),\n\t\t); err != nil {\n\t\t\tfmt.Printf(\"Failed to load url %s: %v\", url, err)\n\t\t\tcancelBrowser()\n\t\t\tcancelAllocator()\n\t\t\tos.Exit(1)\n\t\t} else {\n\t\t\tfmt.Printf(\"Opened (%d/%d): %s\\n\", i+1, len(urls), url)\n\t\t}\n\t}\n\n\tfmt.Println(\"Chrome is running, waiting for error or interrupt...\")\n\n\t// 4) Wait forever, or until an error or signal\n\tselect {\n\tcase <-sigChan:\n\t\tfmt.Println(\"\\n⚠️  Plandex browser process interrupted\")\n\t\treturn nil\n\tcase <-rootCtx.Done():\n\t\tfmt.Println(\"\\n⚠️  Plandex browser process closed\")\n\t\treturn nil\n\tcase <-errChan:\n\t\tfmt.Println(\"\\n❌ Plandex browser process exited due to JavaScript error\")\n\t\tcancelBrowser()\n\t\tcancelAllocator()\n\t\tos.Exit(1)\n\t}\n\treturn nil\n}\n\nfunc openWithOSDefault(urls []string) error {\n\tvar openCmd string\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\topenCmd = \"open\"\n\tcase \"linux\", \"freebsd\":\n\t\topenCmd = \"xdg-open\"\n\tdefault:\n\t\treturn errors.New(\"unsupported OS for fallback\")\n\t}\n\n\tfor _, url := range urls {\n\t\tcmd := exec.Command(openCmd, url)\n\t\tif err := cmd.Start(); err != nil {\n\t\t\tlog.Printf(\"Failed to open %s with %s: %v\", url, openCmd, err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/cmd/build.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/plan_exec\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar buildCmd = &cobra.Command{\n\tUse:     \"build\",\n\tAliases: []string{\"b\"},\n\tShort:   \"Build pending changes\",\n\t// Long:  ``,\n\tArgs: cobra.NoArgs,\n\tRun:  build,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(buildCmd)\n\n\tinitExecFlags(buildCmd, initExecFlagsParams{\n\t\tomitFile:         true,\n\t\tomitNoBuild:      true,\n\t\tomitEditor:       true,\n\t\tomitStop:         true,\n\t\tomitAutoContext:  true,\n\t\tomitSmartContext: true,\n\t})\n}\n\nfunc build(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tmustSetPlanExecFlags(cmd, false)\n\n\tdidBuild, err := plan_exec.Build(plan_exec.ExecParams{\n\t\tCurrentPlanId: lib.CurrentPlanId,\n\t\tCurrentBranch: lib.CurrentBranch,\n\t\tAuthVars:      lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),\n\t\tCheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {\n\t\t\tauto := autoConfirm || tellAutoApply || tellAutoContext\n\t\t\treturn lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)\n\t\t},\n\t}, types.BuildFlags{\n\t\tBuildBg:   tellBg,\n\t\tAutoApply: tellAutoApply,\n\t})\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error building plan: %v\", err)\n\t}\n\n\tif !didBuild {\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"log\", \"tell\", \"continue\")\n\t\treturn\n\t}\n\n\tif tellBg {\n\t\tfmt.Println(\"🏗️ Building plan in the background\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"ps\", \"connect\", \"stop\")\n\t} else if tellAutoApply {\n\t\tapplyFlags := types.ApplyFlags{\n\t\t\tAutoConfirm: true,\n\t\t\tAutoCommit:  autoCommit,\n\t\t\tNoCommit:    !autoCommit,\n\t\t\tNoExec:      noExec,\n\t\t\tAutoExec:    autoExec,\n\t\t\tAutoDebug:   autoDebug,\n\t\t}\n\n\t\ttellFlags := types.TellFlags{\n\t\t\tAutoContext: tellAutoContext,\n\t\t\tExecEnabled: !noExec,\n\t\t\tAutoApply:   tellAutoApply,\n\t\t}\n\n\t\tlib.MustApplyPlan(lib.ApplyPlanParams{\n\t\t\tPlanId:     lib.CurrentPlanId,\n\t\t\tBranch:     lib.CurrentBranch,\n\t\t\tApplyFlags: applyFlags,\n\t\t\tTellFlags:  tellFlags,\n\t\t\tOnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),\n\t\t})\n\t} else {\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"diff\", \"diff --ui\", \"apply\", \"reject\", \"log\")\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/cd.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\tRootCmd.AddCommand(cdCmd)\n}\n\nvar cdCmd = &cobra.Command{\n\tUse:     \"cd [name-or-index]\",\n\tAliases: []string{\"set-plan\"},\n\tShort:   \"Set current plan by name or index\",\n\tArgs:    cobra.MaximumNArgs(1),\n\tRun:     cd,\n}\n\nfunc cd(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tvar nameOrIdx string\n\tif len(args) > 0 {\n\t\tnameOrIdx = strings.TrimSpace(args[0])\n\t}\n\n\tvar plan *shared.Plan\n\n\tterm.StartSpinner(\"\")\n\tplans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plans: %v\", apiErr)\n\t}\n\n\tif len(plans) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No plans\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"new\")\n\t\treturn\n\t}\n\n\tif nameOrIdx == \"\" {\n\n\t\topts := make([]string, len(plans))\n\t\tfor i, plan := range plans {\n\t\t\topts[i] = plan.Name\n\t\t}\n\n\t\tselected, err := term.SelectFromList(\"Select a plan\", opts)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting plan: %v\", err)\n\t\t}\n\n\t\tfor _, p := range plans {\n\t\t\tif p.Name == selected {\n\t\t\t\tplan = p\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// see if it's an index\n\t\tidx, err := strconv.Atoi(nameOrIdx)\n\n\t\tif err == nil {\n\t\t\tif idx > 0 && idx <= len(plans) {\n\t\t\t\tplan = plans[idx-1]\n\t\t\t} else {\n\t\t\t\tterm.OutputErrorAndExit(\"Plan index out of range\")\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, p := range plans {\n\t\t\t\tif p.Name == nameOrIdx {\n\t\t\t\t\tplan = p\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif plan == nil {\n\t\tterm.OutputErrorAndExit(\"Plan not found\")\n\t}\n\n\terr := lib.WriteCurrentPlan(plan.Id)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error setting current plan: %v\", err)\n\t}\n\n\t// reload current plan, which will also handle setting the right branch\n\tlib.MustLoadCurrentPlan()\n\n\t// fire and forget SetProjectPlan request (we don't care about the response or errors)\n\t// this only matters for setting the current plan on a new device (i.e. when the current plan is not set)\n\tgo api.Client.SetProjectPlan(lib.CurrentProjectId, shared.SetProjectPlanRequest{PlanId: plan.Id})\n\n\t// give the SetProjectPlan request some time to be sent before exiting\n\ttime.Sleep(50 * time.Millisecond)\n\n\tfmt.Println(\"✅ Changed current plan to \" + color.New(term.ColorHiGreen, color.Bold).Sprint(plan.Name))\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"current\")\n}\n"
  },
  {
    "path": "app/cli/cmd/chat.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/plan_exec\"\n\t\"plandex-cli/types\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar chatCmd = &cobra.Command{\n\tUse:     \"chat [prompt]\",\n\tAliases: []string{\"c\"},\n\tShort:   \"Chat without making changes\",\n\t// Long:  ``,\n\tArgs: cobra.RangeArgs(0, 1),\n\tRun:  doChat,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(chatCmd)\n\n\tinitExecFlags(chatCmd, initExecFlagsParams{\n\t\tomitNoBuild:      true,\n\t\tomitStop:         true,\n\t\tomitBg:           true,\n\t\tomitApply:        true,\n\t\tomitExec:         true,\n\t\tomitSmartContext: true,\n\t\tomitSkipMenu:     true,\n\t})\n\n}\n\nfunc doChat(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\tmustSetPlanExecFlags(cmd, false)\n\n\tprompt := getTellPrompt(args)\n\n\tif prompt == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No prompt to send\")\n\t\treturn\n\t}\n\n\tplan_exec.TellPlan(plan_exec.ExecParams{\n\t\tCurrentPlanId: lib.CurrentPlanId,\n\t\tCurrentBranch: lib.CurrentBranch,\n\t\tAuthVars:      lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),\n\t\tCheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {\n\t\t\tauto := autoConfirm || tellAutoApply || tellAutoContext\n\t\t\treturn lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)\n\t\t},\n\t}, prompt, types.TellFlags{\n\t\tIsChatOnly:      true,\n\t\tAutoContext:     tellAutoContext,\n\t\tSkipChangesMenu: tellSkipMenu,\n\t})\n}\n"
  },
  {
    "path": "app/cli/cmd/checkout.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tOptCreateNewBranch = \"Create a new branch\"\n)\n\nvar confirmCreateBranch bool\n\nvar checkoutCmd = &cobra.Command{\n\tUse:     \"checkout [name-or-index]\",\n\tAliases: []string{\"co\"},\n\tShort:   \"Checkout an existing plan branch or create a new one\",\n\tRun:     checkout,\n\tArgs:    cobra.MaximumNArgs(1),\n}\n\nfunc init() {\n\tRootCmd.AddCommand(checkoutCmd)\n\tcheckoutCmd.Flags().BoolVarP(&confirmCreateBranch, \"yes\", \"y\", false, \"Confirm creating a new branch\")\n}\n\nfunc checkout(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tbranchName := \"\"\n\twillCreate := false\n\n\tvar nameOrIdx string\n\tif len(args) > 0 {\n\t\tnameOrIdx = strings.TrimSpace(args[0])\n\t}\n\n\tterm.StartSpinner(\"\")\n\tbranches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting branches: %v\", apiErr)\n\t\treturn\n\t}\n\n\tif nameOrIdx != \"\" {\n\t\tidx, err := strconv.Atoi(nameOrIdx)\n\n\t\tif err == nil {\n\t\t\tif idx > 0 && idx <= len(branches) {\n\t\t\t\tbranchName = branches[idx-1].Name\n\t\t\t} else {\n\t\t\t\tterm.OutputErrorAndExit(\"Branch %d not found\", idx)\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, b := range branches {\n\t\t\t\tif b.Name == nameOrIdx {\n\t\t\t\t\tbranchName = b.Name\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif branchName == \"\" {\n\t\t\tfmt.Printf(\"🌱 Branch %s not found\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(nameOrIdx))\n\n\t\t\tif confirmCreateBranch {\n\t\t\t\tfmt.Println(\"✅ --yes flag set, will create branch\")\n\t\t\t\tbranchName = nameOrIdx\n\t\t\t\twillCreate = true\n\t\t\t} else {\n\t\t\t\tres, err := term.ConfirmYesNo(\"Create it now?\")\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error getting user input: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif res {\n\t\t\t\t\tbranchName = nameOrIdx\n\t\t\t\t\twillCreate = true\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif nameOrIdx == \"\" {\n\t\topts := make([]string, len(branches))\n\t\tfor i, branch := range branches {\n\t\t\topts[i] = branch.Name\n\t\t}\n\t\topts = append(opts, OptCreateNewBranch)\n\n\t\tselected, err := term.SelectFromList(\"Select a branch\", opts)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting branch: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tif selected == OptCreateNewBranch {\n\t\t\tbranchName, err = term.GetRequiredUserStringInput(\"Branch name\")\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting branch name: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\twillCreate = true\n\t\t} else {\n\t\t\tbranchName = selected\n\t\t}\n\t}\n\n\tif branchName == \"\" {\n\t\tterm.OutputErrorAndExit(\"Branch not found\")\n\t}\n\n\tterm.StartSpinner(\"\")\n\tif willCreate {\n\t\terr := api.Client.CreateBranch(lib.CurrentPlanId, lib.CurrentBranch, shared.CreateBranchRequest{Name: branchName})\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error creating branch: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// fmt.Printf(\"✅ Created branch %s\\n\", color.New(color.Bold, term.ColorHiGreen).Sprint(branchName))\n\t}\n\n\terr := lib.WriteCurrentBranch(branchName)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error setting current branch: %v\", err)\n\t\treturn\n\t}\n\n\tupdatedModelSettings, err := lib.SaveLatestPlanModelSettingsIfNeeded()\n\tterm.StopSpinner()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error saving model settings: %v\", err)\n\t}\n\n\tterm.StopSpinner()\n\n\tfmt.Printf(\"✅ Checked out branch %s\\n\", color.New(color.Bold, term.ColorHiGreen).Sprint(branchName))\n\n\tif updatedModelSettings {\n\t\tfmt.Println()\n\t\tfmt.Println(\"🧠 Model settings file updated → \", lib.GetPlanModelSettingsPath(lib.CurrentPlanId))\n\t}\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"load\", \"tell\", \"branches\", \"delete-branch\")\n\n}\n"
  },
  {
    "path": "app/cli/cmd/claude_max.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar connectClaudeCmd = &cobra.Command{\n\tUse:   \"connect-claude\",\n\tShort: \"Connect your Claude Pro or Max subscription\",\n\tRun:   connectClaude,\n}\n\nvar disconnectClaudeCmd = &cobra.Command{\n\tUse:   \"disconnect-claude\",\n\tShort: \"Disconnect your Claude Pro or Max subscription\",\n\tRun:   disconnectClaude,\n}\n\nvar claudeStatusCmd = &cobra.Command{\n\tUse:   \"claude-status\",\n\tShort: \"Check the status of your Claude Pro or Max subscription\",\n\tRun:   claudeStatus,\n}\n\nfunc connectClaude(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.ConnectClaudeMax()\n}\n\nfunc disconnectClaude(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.DisconnectClaudeMax()\n}\n\nfunc claudeStatus(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tcreds, err := lib.GetAccountCredentials()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting account credentials: %v\", err)\n\t}\n\n\torgUserConfig := lib.MustGetOrgUserConfig()\n\n\tconnected := creds.ClaudeMax != nil && orgUserConfig.UseClaudeSubscription\n\n\tif connected {\n\t\tfmt.Println(\"✅ Claude Pro or Max subscription is connected\")\n\n\t\t// if orgUserConfig.IsClaudeSubscriptionCooldownActive() {\n\t\tif true {\n\t\t\tfmt.Println()\n\t\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"⏳ You've reached your Claude Pro or Max subscription quota\")\n\t\t\tfmt.Println(\"The next provider with valid credentials will be used for Anthropic models until the quota resets\")\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tterm.PrintCmds(\"\", \"disconnect-claude\")\n\t} else {\n\t\tfmt.Println(\"❌ No Claude Pro or Max subscription is connected\")\n\t\tterm.PrintCmds(\"\", \"connect-claude\")\n\t}\n}\n\nfunc init() {\n\tRootCmd.AddCommand(connectClaudeCmd)\n\tRootCmd.AddCommand(disconnectClaudeCmd)\n\tRootCmd.AddCommand(claudeStatusCmd)\n}\n"
  },
  {
    "path": "app/cli/cmd/clear.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar clearCmd = &cobra.Command{\n\tUse:   \"clear\",\n\tShort: \"Clear all context\",\n\tLong:  `Clear all context.`,\n\tRun:   clearAllContext,\n}\n\nfunc clearAllContext(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor _, context := range contexts {\n\t\tdeleteIds[context.Id] = true\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n\n}\n\nfunc init() {\n\tRootCmd.AddCommand(clearCmd)\n}\n"
  },
  {
    "path": "app/cli/cmd/config.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\tRootCmd.AddCommand(configCmd)\n\tconfigCmd.AddCommand(defaultConfigCmd)\n}\n\nvar configCmd = &cobra.Command{\n\tUse:   \"config\",\n\tShort: \"Show plan config\",\n\tRun:   config,\n}\n\nvar defaultConfigCmd = &cobra.Command{\n\tUse:   \"default\",\n\tShort: \"Show default config for new plans\",\n\tRun:   defaultConfig,\n}\n\nfunc config(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tconfig, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)\n\tif apiErr != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"Error getting config: %v\", apiErr.Msg)\n\t\treturn\n\t}\n\n\tterm.StopSpinner()\n\n\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"⚙️  Plan Config\")\n\tlib.ShowPlanConfig(config, \"\")\n\tfmt.Println()\n\n\tterm.PrintCmds(\"\", \"set-config\", \"config default\", \"set-config default\")\n}\n\nfunc defaultConfig(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuth(false)\n\n\tterm.StartSpinner(\"\")\n\tconfig, err := api.Client.GetDefaultPlanConfig()\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting default config: %v\", err)\n\t\treturn\n\t}\n\n\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"⚙️  Default Config\")\n\tlib.ShowPlanConfig(config, \"\")\n\tfmt.Println()\n\n\tterm.PrintCmds(\"\", \"set-config default\", \"config\", \"set-config\")\n}\n"
  },
  {
    "path": "app/cli/cmd/connect.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/stream\"\n\tstreamtui \"plandex-cli/stream_tui\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar connectCmd = &cobra.Command{\n\tUse:     \"connect [stream-id-or-plan] [branch]\",\n\tAliases: []string{\"conn\"},\n\tShort:   \"Connect to an active stream\",\n\t// Long:  ``,\n\tArgs: cobra.MaximumNArgs(2),\n\tRun:  connect,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(connectCmd)\n\n}\n\nfunc connect(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tplanId, branch, shouldContinue := lib.SelectActiveStream(args)\n\n\tif !shouldContinue {\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tapiErr := api.Client.ConnectPlan(planId, branch, stream.OnStreamPlan)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error connecting to stream: %v\", apiErr)\n\t}\n\n\tgo func() {\n\t\terr := streamtui.StartStreamUI(\"\", false, true)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error starting stream UI\", err)\n\t\t}\n\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"diff\", \"diff --ui\", \"apply\", \"reject\", \"log\")\n\n\t\tos.Exit(0)\n\t}()\n\n\t// Wait for the stream to finish\n\tselect {}\n}\n"
  },
  {
    "path": "app/cli/cmd/context_show.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"strconv\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\tRootCmd.AddCommand(contextShowCmd)\n}\n\nvar contextShowCmd = &cobra.Command{\n\tUse:   \"show [name-or-index]\",\n\tShort: \"Show the body of a context by name or list index\",\n\tArgs:  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tauth.MustResolveAuthWithOrg()\n\t\tlib.MustResolveProject()\n\n\t\tnameOrIndex := args[0]\n\n\t\t// Get list of contexts first\n\t\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error listing contexts: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error listing contexts: %v\", err)\n\t\t}\n\n\t\tvar contextId string\n\n\t\t// Try parsing as index first\n\t\tif idx, err := strconv.Atoi(nameOrIndex); err == nil {\n\t\t\t// Convert to 0-based index\n\t\t\tidx--\n\t\t\tif idx < 0 || idx >= len(contexts) {\n\t\t\t\treturn fmt.Errorf(\"invalid context index: %s\", nameOrIndex)\n\t\t\t}\n\t\t\tcontextId = contexts[idx].Id\n\t\t} else {\n\t\t\t// Try finding by name\n\t\t\tfound := false\n\t\t\tfor _, ctx := range contexts {\n\t\t\t\tif ctx.Name == nameOrIndex || ctx.FilePath == nameOrIndex {\n\t\t\t\t\tcontextId = ctx.Id\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\treturn fmt.Errorf(\"no context found with name: %s\", nameOrIndex)\n\t\t\t}\n\t\t}\n\n\t\tres, apiErr := api.Client.GetContextBody(lib.CurrentPlanId, lib.CurrentBranch, contextId)\n\t\tif apiErr != nil {\n\t\t\tlog.Printf(\"Error getting context body: %v\\n\", apiErr)\n\t\t\treturn fmt.Errorf(\"error getting context body: %v\", apiErr)\n\t\t}\n\n\t\tfmt.Println(res.Body)\n\t\treturn nil\n\t},\n}\n"
  },
  {
    "path": "app/cli/cmd/continue.go",
    "content": "package cmd\n\nimport (\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/plan_exec\"\n\t\"plandex-cli/types\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\tchatOnly bool\n)\n\nvar continueCmd = &cobra.Command{\n\tUse:     \"continue\",\n\tAliases: []string{\"c\"},\n\tShort:   \"Continue the plan\",\n\tRun:     doContinue,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(continueCmd)\n\n\tcontinueCmd.Flags().BoolVar(&chatOnly, \"chat\", false, \"Continue in chat mode (no file changes)\")\n\n\tinitExecFlags(continueCmd, initExecFlagsParams{\n\t\tomitFile:   true,\n\t\tomitEditor: true,\n\t})\n}\n\nfunc doContinue(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\tmustSetPlanExecFlags(cmd, false)\n\n\ttellFlags := types.TellFlags{\n\t\tTellBg:          tellBg,\n\t\tTellStop:        tellStop,\n\t\tTellNoBuild:     tellNoBuild,\n\t\tIsUserContinue:  true,\n\t\tExecEnabled:     !noExec,\n\t\tAutoContext:     tellAutoContext,\n\t\tSmartContext:    tellSmartContext,\n\t\tAutoApply:       tellAutoApply,\n\t\tIsChatOnly:      chatOnly,\n\t\tSkipChangesMenu: tellSkipMenu,\n\t}\n\n\tplan_exec.TellPlan(plan_exec.ExecParams{\n\t\tCurrentPlanId: lib.CurrentPlanId,\n\t\tCurrentBranch: lib.CurrentBranch,\n\t\tAuthVars:      lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),\n\t\tCheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {\n\t\t\tauto := autoConfirm || tellAutoApply || tellAutoContext\n\n\t\t\treturn lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)\n\t\t},\n\t}, \"\", tellFlags)\n\n\tif tellAutoApply {\n\t\tapplyFlags := types.ApplyFlags{\n\t\t\tAutoConfirm: true,\n\t\t\tAutoCommit:  autoCommit,\n\t\t\tNoCommit:    !autoCommit,\n\t\t\tAutoExec:    autoExec,\n\t\t\tNoExec:      noExec,\n\t\t\tAutoDebug:   autoDebug,\n\t\t}\n\n\t\tlib.MustApplyPlan(lib.ApplyPlanParams{\n\t\t\tPlanId:     lib.CurrentPlanId,\n\t\t\tBranch:     lib.CurrentBranch,\n\t\t\tApplyFlags: applyFlags,\n\t\t\tTellFlags:  tellFlags,\n\t\t\tOnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/convo.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar plainTextOutput bool\nvar convoRaw bool\n\n// convoCmd represents the convo command\nvar convoCmd = &cobra.Command{\n\tUse:   \"convo [msg-range]\",\n\tShort: \"Display complete conversation history\",\n\tLong:  `Display complete conversation history. Optionally specify a message number or range of messages (e.g. '1' or '5' or '1-5' or '5-')`,\n\tRun:   convo,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(convoCmd)\n\n\tconvoCmd.Flags().BoolVarP(&plainTextOutput, \"plain\", \"p\", false, \"Output conversation in plain text with no ANSI codes\")\n\n\t// for debugging output\n\tconvoCmd.Flags().BoolVar(&convoRaw, \"raw\", false, \"Output conversation in raw format\")\n\tconvoCmd.Flags().MarkHidden(\"raw\")\n}\n\nconst stoppedEarlyMsg = \"You stopped the reply early\"\n\nfunc convo(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tterm.StartSpinner(\"\")\n\tconversation, apiErr := api.Client.ListConvo(lib.CurrentPlanId, lib.CurrentBranch)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error loading conversation: %v\", apiErr.Msg)\n\t}\n\n\tif len(conversation) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No conversation history\")\n\t\treturn\n\t}\n\n\tvar msgRange string\n\tvar msgRangeStart, msgRangeEnd int\n\tif len(args) > 0 {\n\t\tmsgRange = args[0]\n\t}\n\tif msgRange != \"\" {\n\t\t// validate either a number or a range of numbers\n\t\tif strings.Contains(msgRange, \"-\") {\n\t\t\t_, err := fmt.Sscanf(msgRange, \"%d-%d\", &msgRangeStart, &msgRangeEnd)\n\t\t\tif err != nil {\n\t\t\t\t_, err := fmt.Sscanf(msgRange, \"%d-\", &msgRangeStart)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Invalid message range: %s\", msgRange)\n\t\t\t\t}\n\n\t\t\t\tmsgRangeEnd = len(conversation)\n\t\t\t}\n\t\t} else {\n\t\t\t_, err := fmt.Sscanf(msgRange, \"%d\", &msgRangeStart)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Invalid message number: %s\", msgRange)\n\t\t\t}\n\t\t\tmsgRangeEnd = msgRangeStart\n\t\t}\n\t}\n\n\tvar convo string\n\tvar totalTokens int\n\tvar didCut bool\n\tfor i, msg := range conversation {\n\t\tif msgRangeStart > 0 && msg.Num < msgRangeStart {\n\t\t\tdidCut = true\n\t\t\tcontinue\n\t\t}\n\t\tif msgRangeEnd > 0 && msg.Num > msgRangeEnd {\n\t\t\tdidCut = true\n\t\t\tbreak\n\t\t}\n\n\t\tvar author string\n\t\tif msg.Role == \"assistant\" {\n\t\t\tauthor = \"🤖 Plandex\"\n\t\t} else if msg.Role == \"user\" {\n\t\t\tauthor = \"💬 You\"\n\t\t} else {\n\t\t\tauthor = msg.Role\n\t\t}\n\n\t\treplyTags := msg.Flags.GetReplyTags()\n\n\t\t// format as above but start with day of week\n\t\tformattedTs := msg.CreatedAt.Local().Format(\"Mon Jan 2, 2006 | 3:04pm MST\")\n\n\t\t// if it's today then use 'Today' instead of the date\n\t\tif msg.CreatedAt.Day() == time.Now().Day() {\n\t\t\tformattedTs = msg.CreatedAt.Local().Format(\"Today | 3:04pm MST\")\n\t\t}\n\n\t\t// if it's yesterday then use 'Yesterday' instead of the date\n\t\tif msg.CreatedAt.Day() == time.Now().AddDate(0, 0, -1).Day() {\n\t\t\tformattedTs = msg.CreatedAt.Local().Format(\"Yesterday | 3:04pm MST\")\n\t\t}\n\n\t\tvar header string\n\t\tif len(replyTags) > 0 {\n\t\t\theader = fmt.Sprintf(\"#### %d | %s | %s | %s | %d 🪙 \", i+1,\n\t\t\t\tauthor, strings.Join(replyTags, \" | \"), formattedTs, msg.Tokens)\n\t\t} else {\n\t\t\theader = fmt.Sprintf(\"#### %d | %s | %s | %d 🪙 \", i+1,\n\t\t\t\tauthor, formattedTs, msg.Tokens)\n\t\t}\n\n\t\ttxt := msg.Message\n\t\tif !convoRaw {\n\t\t\ttxt = convertCodeBlocks(msg.Message)\n\t\t}\n\n\t\tif plainTextOutput {\n\t\t\tconvo += header + \"\\n\" + txt + \"\\n\\n\"\n\t\t} else {\n\t\t\tmd, err := term.GetMarkdown(header + \"\\n\" + txt + \"\\n\\n\")\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error creating markdown representation: %v\", err)\n\t\t\t}\n\t\t\tconvo += md\n\t\t}\n\n\t\tif !didCut && msg.Stopped {\n\t\t\tif plainTextOutput {\n\t\t\t\tconvo += fmt.Sprintf(\" 🛑 %s\\n\\n\", stoppedEarlyMsg)\n\t\t\t} else {\n\t\t\t\tconvo += fmt.Sprintf(\" 🛑 %s\\n\\n\", color.New(color.Bold).Sprint(stoppedEarlyMsg))\n\t\t\t}\n\t\t}\n\n\t\ttotalTokens += msg.Tokens\n\t}\n\n\tif !plainTextOutput {\n\t\tconvo = strings.ReplaceAll(convo, stoppedEarlyMsg, color.New(term.ColorHiRed).Sprint(stoppedEarlyMsg))\n\t}\n\n\toutput :=\n\t\tfmt.Sprintf(\"\\n%s\", convo)\n\n\tif !plainTextOutput && !didCut {\n\t\toutput += term.GetDivisionLine() +\n\t\t\tcolor.New(color.Bold, term.ColorHiCyan).Sprint(\"  Conversation size →\") + fmt.Sprintf(\" %d 🪙\", totalTokens) + \"\\n\\n\"\n\t}\n\n\tif plainTextOutput {\n\t\tfmt.Println(output)\n\t} else {\n\t\tterm.PageOutput(output)\n\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"convo 1\", \"convo 2-5\", \"convo --plain\", \"log\")\n\t}\n}\n\nvar codeBlockPattern = regexp.MustCompile(`<PlandexBlock\\s+lang=\"(.+?)\".*?>([\\s\\S]+?)</PlandexBlock>`)\n\nfunc convertCodeBlocks(msg string) string {\n\treturn codeBlockPattern.ReplaceAllStringFunc(msg, func(match string) string {\n\t\t// Extract language and content from the match\n\t\tsubmatches := codeBlockPattern.FindStringSubmatch(match)\n\t\tlang := submatches[1]\n\t\tcontent := submatches[2]\n\n\t\t// Escape any backticks in the content\n\t\tcontent = strings.ReplaceAll(content, \"```\", \"\\\\`\\\\`\\\\`\")\n\n\t\t// Return markdown code block format\n\t\treturn fmt.Sprintf(\"```%s%s```\", lang, content)\n\t})\n}\n"
  },
  {
    "path": "app/cli/cmd/current.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar currentCmd = &cobra.Command{\n\tUse:     \"current\",\n\tAliases: []string{\"cu\"},\n\tShort:   \"Get the current plan\",\n\tRun:     current,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(currentCmd)\n}\n\nfunc current(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MaybeResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\tplan, err := api.Client.GetPlan(lib.CurrentPlanId)\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plan: %v\", err)\n\t\treturn\n\t}\n\n\tcurrentBranchesByPlanId, err := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{\n\t\tCurrentBranchByPlanId: map[string]string{\n\t\t\tlib.CurrentPlanId: lib.CurrentBranch,\n\t\t},\n\t})\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current branches: %v\", err)\n\t}\n\n\ttable := lib.GetCurrentPlanTable(plan, currentBranchesByPlanId, nil)\n\tfmt.Println(table)\n\n\tterm.PrintCmds(\"\", \"tell\", \"ls\", \"plans\")\n\n}\n"
  },
  {
    "path": "app/cli/cmd/debug.go",
    "content": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/plan_exec\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst DebugDefaultTries = 5\n\nvar debugCmd = &cobra.Command{\n\tUse:     \"debug [tries] <cmd>\",\n\tAliases: []string{\"db\"},\n\tShort:   \"Debug a failing command with Plandex\",\n\tArgs:    cobra.MinimumNArgs(1),\n\tRun:     doDebug,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(debugCmd)\n\tdebugCmd.Flags().BoolVarP(&autoCommit, \"commit\", \"c\", false, \"Commit changes after successful execution\")\n}\n\nfunc doDebug(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\tmustSetPlanExecFlags(cmd, false)\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\t// Parse tries and command\n\ttries := DebugDefaultTries\n\tcmdArgs := args\n\n\t// Check if first arg is tries count\n\tif val, err := strconv.Atoi(args[0]); err == nil {\n\t\tif val <= 0 {\n\t\t\tterm.OutputErrorAndExit(\"Tries must be greater than 0\")\n\t\t}\n\t\ttries = val\n\t\tcmdArgs = args[1:]\n\t\tif len(cmdArgs) == 0 {\n\t\t\tterm.OutputErrorAndExit(\"No command specified\")\n\t\t}\n\t}\n\n\t// Get current working directory\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to get working directory: %v\", err)\n\t}\n\n\tcmdStr := strings.Join(cmdArgs, \" \")\n\n\t// Execute command and handle retries\n\tfor attempt := 0; attempt < tries; attempt++ {\n\t\t// Use shell to handle operators like && and |\n\t\tshellCmdStr := \"set -euo pipefail; \" + cmdStr\n\t\texecCmd := exec.Command(\"sh\", \"-c\", shellCmdStr)\n\t\texecCmd.Dir = cwd\n\t\texecCmd.Env = os.Environ()\n\t\tlib.SetPlatformSpecificAttrs(execCmd)\n\n\t\tpipe, err := execCmd.StdoutPipe()\n\t\tif err != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"Failed to create pipe: %v\", err)\n\t\t}\n\t\texecCmd.Stderr = execCmd.Stdout\n\n\t\tif err := execCmd.Start(); err != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"Failed to start command: %v\", err)\n\t\t}\n\n\t\tmaybeDeleteCgroup := lib.MaybeIsolateCgroup(execCmd)\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tvar interrupted atomic.Bool\n\t\tvar interruptHandled atomic.Bool\n\t\tvar interruptWG sync.WaitGroup\n\n\t\tsigChan := make(chan os.Signal, 1)\n\t\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)\n\n\t\tinterruptWG.Add(1)\n\t\tgo func() {\n\t\t\tdefer interruptWG.Done()\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase sig := <-sigChan:\n\t\t\t\t\tif interruptHandled.CompareAndSwap(false, true) {\n\t\t\t\t\t\tfmt.Println()\n\t\t\t\t\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"\\n👉 Caught interrupt. Exiting gracefully...\")\n\t\t\t\t\t\tinterrupted.Store(true)\n\n\t\t\t\t\t\tvar sysSig syscall.Signal\n\n\t\t\t\t\t\tswitch sig {\n\t\t\t\t\t\tcase os.Interrupt:\n\t\t\t\t\t\t\t// user pressed Ctrl+C\n\t\t\t\t\t\t\tsysSig = syscall.SIGINT\n\t\t\t\t\t\tcase syscall.SIGTERM:\n\t\t\t\t\t\t\t// a polite \"kill\" request\n\t\t\t\t\t\t\tsysSig = syscall.SIGTERM\n\t\t\t\t\t\tcase syscall.SIGHUP:\n\t\t\t\t\t\t\tsysSig = syscall.SIGHUP\n\t\t\t\t\t\tcase syscall.SIGQUIT:\n\t\t\t\t\t\t\tsysSig = syscall.SIGQUIT\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tsysSig = syscall.SIGINT\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif err := lib.KillProcessGroup(execCmd, sysSig); err != nil {\n\t\t\t\t\t\t\tlog.Printf(\"Failed to send signal %s to process group: %v\", sysSig, err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase <-time.After(2 * time.Second):\n\t\t\t\t\t\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"👉 Commands didn't exit after 2 seconds. Sending SIGKILL.\")\n\t\t\t\t\t\t\tif err := lib.KillProcessGroup(execCmd, syscall.SIGKILL); err != nil {\n\t\t\t\t\t\t\t\tlog.Printf(\"Failed to send SIGKILL to process group: %v\", err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmaybeDeleteCgroup()\n\t\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\t\tmaybeDeleteCgroup()\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tmaybeDeleteCgroup()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tvar outputBuilder strings.Builder\n\t\tscanner := bufio.NewScanner(pipe)\n\t\tgo func() {\n\t\t\tfor scanner.Scan() {\n\t\t\t\tline := scanner.Text()\n\t\t\t\tfmt.Println(line)\n\t\t\t\toutputBuilder.WriteString(line + \"\\n\")\n\t\t\t}\n\t\t}()\n\n\t\twaitErr := execCmd.Wait()\n\n\t\tcancel()\n\t\tinterruptWG.Wait()\n\t\tsignal.Stop(sigChan)\n\t\tclose(sigChan)\n\n\t\tif scanErr := scanner.Err(); scanErr != nil {\n\t\t\tlog.Printf(\"⚠️ Scanner error reading subprocess output: %v\", scanErr)\n\t\t}\n\n\t\tterm.StopSpinner()\n\n\t\toutputStr := outputBuilder.String()\n\t\tif outputStr == \"\" && waitErr != nil {\n\t\t\toutputStr = waitErr.Error()\n\t\t}\n\n\t\tif outputStr != \"\" {\n\t\t\tfmt.Println(outputStr)\n\t\t}\n\n\t\tdidSucceed := waitErr == nil\n\n\t\tif interrupted.Load() {\n\t\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"👉  Execution interrupted\")\n\n\t\t\tres, canceled, err := term.ConfirmYesNoCancel(\"Did the command succeed?\")\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Failed to get confirmation user input: %s\", err)\n\t\t\t}\n\n\t\t\tdidSucceed = res\n\n\t\t\tif canceled {\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t}\n\n\t\tif didSucceed {\n\t\t\tif attempt == 0 {\n\t\t\t\tfmt.Printf(\"✅ Command %s succeeded on first try\\n\", color.New(color.Bold, term.ColorHiCyan).Sprintf(cmdStr))\n\t\t\t} else {\n\t\t\t\tlbl := \"attempts\"\n\t\t\t\tif attempt == 1 {\n\t\t\t\t\tlbl = \"attempt\"\n\t\t\t\t}\n\t\t\t\tfmt.Printf(\"✅ Command %s succeeded after %d fix %s\\n\", color.New(color.Bold, term.ColorHiCyan).Sprintf(cmdStr), attempt, lbl)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif attempt == tries-1 {\n\t\t\tfmt.Printf(\"Command failed after %d tries\\n\", tries)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\t// Prepare prompt for TellPlan\n\t\texitErr, ok := waitErr.(*exec.ExitError)\n\t\tstatus := -1\n\t\tif ok {\n\t\t\tstatus = exitErr.ExitCode()\n\t\t}\n\n\t\tprompt := fmt.Sprintf(\"'%s' failed with exit status %d. Output:\\n\\n%s\\n\\n--\\n\\n\",\n\t\t\tstrings.Join(cmdArgs, \" \"), status, outputStr)\n\n\t\ttellFlags := types.TellFlags{\n\t\t\tAutoContext: tellAutoContext,\n\t\t\tExecEnabled: false,\n\t\t\tIsUserDebug: true,\n\t\t}\n\n\t\tplan_exec.TellPlan(plan_exec.ExecParams{\n\t\t\tCurrentPlanId: lib.CurrentPlanId,\n\t\t\tCurrentBranch: lib.CurrentBranch,\n\t\t\tAuthVars:      lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),\n\t\t\tCheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {\n\t\t\t\treturn lib.CheckOutdatedContextWithOutput(true, true, maybeContexts, projectPaths)\n\t\t\t},\n\t\t}, prompt, tellFlags)\n\n\t\tapplyFlags := types.ApplyFlags{\n\t\t\tAutoConfirm: true,\n\t\t\tAutoCommit:  autoCommit,\n\t\t\tNoCommit:    !autoCommit,\n\t\t\tNoExec:      false,\n\t\t\tAutoExec:    true,\n\t\t}\n\n\t\tlib.MustApplyPlan(lib.ApplyPlanParams{\n\t\t\tPlanId:      lib.CurrentPlanId,\n\t\t\tBranch:      lib.CurrentBranch,\n\t\t\tApplyFlags:  applyFlags,\n\t\t\tTellFlags:   tellFlags,\n\t\t\tOnExecFail:  plan_exec.GetOnApplyExecFailWithCommand(applyFlags, tellFlags, cmdStr),\n\t\t\tExecCommand: cmdStr,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/delete_branch.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar deleteBranchCmd = &cobra.Command{\n\tUse:     \"delete-branch\",\n\tAliases: []string{\"dlb\"},\n\tShort:   \"Delete a plan branch by name or index\",\n\tRun:     deleteBranch,\n\tArgs:    cobra.MaximumNArgs(1),\n}\n\nfunc init() {\n\tRootCmd.AddCommand(deleteBranchCmd)\n}\n\nfunc deleteBranch(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tvar branch string\n\tvar nameOrIdx string\n\n\tif len(args) > 0 {\n\t\tnameOrIdx = strings.TrimSpace(args[0])\n\t}\n\n\tif nameOrIdx == \"main\" {\n\t\tfmt.Println(\"🚨 Cannot delete main branch\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tbranches, apiErr := api.Client.ListBranches(lib.CurrentPlanId)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting branches: %v\", apiErr)\n\t\treturn\n\t}\n\n\tif nameOrIdx == \"\" {\n\t\topts := make([]string, len(branches))\n\t\tfor i, branch := range branches {\n\t\t\tif branch.Name == \"main\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\topts[i] = branch.Name\n\t\t}\n\n\t\tif len(opts) == 0 {\n\t\t\tfmt.Println(\"🤷‍♂️ No branches to delete\")\n\t\t\treturn\n\t\t}\n\n\t\tsel, err := term.SelectFromList(\"Select a branch to delete\", opts)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting branch: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tbranch = sel\n\t}\n\n\t// see if it's an index\n\tidx, err := strconv.Atoi(nameOrIdx)\n\n\tif err == nil {\n\t\tif idx > 0 && idx <= len(branches) {\n\t\t\tbranch = branches[idx-1].Name\n\t\t} else {\n\t\t\tterm.OutputErrorAndExit(\"Branch index out of range\")\n\t\t}\n\t} else {\n\t\tfor _, b := range branches {\n\t\t\tif b.Name == nameOrIdx {\n\t\t\t\tbranch = b.Name\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif branch == \"\" {\n\t\t\tterm.OutputErrorAndExit(\"Branch not found\")\n\t\t}\n\t}\n\n\tfound := false\n\tfor _, b := range branches {\n\t\tif b.Name == branch {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tfmt.Printf(\"🤷‍♂️ Branch %s does not exist\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(branch))\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tapiErr = api.Client.DeleteBranch(lib.CurrentPlanId, branch)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error deleting branch: %v\", apiErr)\n\t\treturn\n\t}\n\n\tfmt.Printf(\"✅ Deleted branch %s\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(branch))\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"branches\")\n}\n"
  },
  {
    "path": "app/cli/cmd/delete_plan.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar all bool\n\nfunc init() {\n\trmCmd.Flags().BoolVar(&all, \"all\", false, \"Delete all plans\")\n\tRootCmd.AddCommand(rmCmd)\n}\n\n// rmCmd represents the rm command\nvar rmCmd = &cobra.Command{\n\tUse:     \"delete-plan [name-or-index]\",\n\tAliases: []string{\"dp\"},\n\tShort:   \"Delete a plan by name, index, range, or pattern, or select from a list. Delete all plans with --all flag.\",\n\tArgs:    cobra.RangeArgs(0, 1),\n\tRun:     del,\n}\n\nfunc matchPlansByPattern(pattern string, plans []*shared.Plan) []*shared.Plan {\n\tvar matched []*shared.Plan\n\tfor _, plan := range plans {\n\t\tif isMatched, err := path.Match(pattern, plan.Name); err == nil && isMatched {\n\t\t\tmatched = append(matched, plan)\n\t\t}\n\t}\n\treturn matched\n}\n\nfunc del(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif all {\n\t\tdelAll()\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tplans, apiErr := api.Client.ListPlans([]string{lib.CurrentProjectId})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plans: %v\", apiErr)\n\t}\n\n\tif len(plans) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No plans\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"new\")\n\t\treturn\n\t}\n\n\tvar plansToDelete []*shared.Plan\n\n\tif len(args) == 0 {\n\t\t// Interactive selection\n\t\topts := make([]string, len(plans))\n\t\tfor i, plan := range plans {\n\t\t\topts[i] = plan.Name\n\t\t}\n\n\t\tselected, err := term.SelectFromList(\"Select a plan:\", opts)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting plan: %v\", err)\n\t\t}\n\n\t\tfor _, p := range plans {\n\t\t\tif p.Name == selected {\n\t\t\t\tplansToDelete = append(plansToDelete, p)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tnameOrPattern := strings.TrimSpace(args[0])\n\n\t\t// Check if it's a range of indices\n\t\tif strings.Contains(nameOrPattern, \"-\") {\n\t\t\t// Create single-element slice with the range pattern\n\t\t\trangeArgs := []string{nameOrPattern}\n\t\t\tindices := parseIndices(rangeArgs)\n\t\t\tfor idx := range indices {\n\t\t\t\tif idx >= 0 && idx < len(plans) {\n\t\t\t\t\tplansToDelete = append(plansToDelete, plans[idx])\n\t\t\t\t}\n\t\t\t}\n\t\t} else if strings.Contains(nameOrPattern, \"*\") {\n\t\t\t// Wildcard pattern matching\n\t\t\tplansToDelete = matchPlansByPattern(nameOrPattern, plans)\n\t\t} else {\n\t\t\t// Try as index first\n\t\t\tidx, err := strconv.Atoi(nameOrPattern)\n\t\t\tif err == nil {\n\t\t\t\tif idx > 0 && idx <= len(plans) {\n\t\t\t\t\tplansToDelete = append(plansToDelete, plans[idx-1])\n\t\t\t\t} else {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Plan index out of range\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Try exact name match\n\t\t\t\tfor _, p := range plans {\n\t\t\t\t\tif p.Name == nameOrPattern {\n\t\t\t\t\t\tplansToDelete = append(plansToDelete, p)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(plansToDelete) == 0 {\n\t\tterm.OutputErrorAndExit(\"No matching plans found\")\n\t}\n\n\t// Show confirmation with list of plans to be deleted\n\tfmt.Printf(\"\\nThe following %d plan(s) will be deleted:\\n\", len(plansToDelete))\n\tfor _, p := range plansToDelete {\n\t\tfmt.Printf(\"  - %s\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(p.Name))\n\t}\n\tfmt.Println()\n\n\tconfirmed, err := term.ConfirmYesNo(\"Are you sure you want to delete these plans?\")\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting confirmation: %v\", err)\n\t}\n\tif !confirmed {\n\t\tfmt.Println(\"Operation cancelled\")\n\t\treturn\n\t}\n\n\t// Delete the plans\n\tterm.StartSpinner(\"\")\n\tfor _, p := range plansToDelete {\n\t\tapiErr = api.Client.DeletePlan(p.Id)\n\t\tif apiErr != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"Error deleting plan %s: %s\", p.Name, apiErr.Msg)\n\t\t}\n\n\t\tif lib.CurrentPlanId == p.Id {\n\t\t\terr := lib.ClearCurrentPlan()\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error clearing current plan: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\tterm.StopSpinner()\n\n\tif len(plansToDelete) == 1 {\n\t\tfmt.Printf(\"✅ Deleted plan '%s'\\n\", plansToDelete[0].Name)\n\t} else {\n\t\tfmt.Printf(\"✅ Deleted %d plans\\n\", len(plansToDelete))\n\t}\n}\n\nfunc delAll() {\n\tterm.StartSpinner(\"\")\n\terr := api.Client.DeleteAllPlans(lib.CurrentProjectId)\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error deleting all  plans: %v\", err)\n\t}\n\n\tfmt.Println(\"✅ Deleted all plans\")\n}\n"
  },
  {
    "path": "app/cli/cmd/diffs.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/ui\"\n\n\t\"github.com/eiannone/keyboard\"\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar showDiffUi bool = true\nvar diffUiSideBySide = true\nvar diffUiLineByLine bool\nvar diffGit bool\n\nvar fromTellMenu bool\n\nvar diffsCmd = &cobra.Command{\n\tUse:     \"diff\",\n\tAliases: []string{\"diffs\"},\n\tShort:   \"Review pending changes\",\n\tRun:     diffs,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(diffsCmd)\n\n\tdiffsCmd.Flags().BoolVarP(&plainTextOutput, \"plain\", \"p\", false, \"Output diffs in plain text with no ANSI codes\")\n\tdiffsCmd.Flags().BoolVar(&showDiffUi, \"ui\", false, \"Show diffs in a browser UI\")\n\tdiffsCmd.Flags().BoolVar(&diffGit, \"git\", true, \"Show diffs in git diff format\")\n\tdiffsCmd.Flags().BoolVarP(&diffUiSideBySide, \"side\", \"s\", true, \"Show diffs UI in side-by-side view\")\n\tdiffsCmd.Flags().BoolVarP(&diffUiLineByLine, \"line\", \"l\", false, \"Show diffs UI in line-by-line view\")\n\n\tdiffsCmd.Flags().BoolVar(&fromTellMenu, \"from-tell-menu\", false, \"Show diffs from the tell menu\")\n\tdiffsCmd.Flags().MarkHidden(\"from-tell-menu\")\n\n}\n\nfunc diffs(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MaybeResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tif showDiffUi {\n\t\tdiffGit = false\n\t} else if diffGit || plainTextOutput {\n\t\tshowDiffUi = false\n\t} else {\n\t\tdiffGit = true\n\t}\n\n\tdiffs, err := api.Client.GetPlanDiffs(lib.CurrentPlanId, lib.CurrentBranch, plainTextOutput || showDiffUi)\n\tterm.StopSpinner()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plan diffs: %v\", err)\n\t\treturn\n\t}\n\n\tif len(diffs) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No pending changes\")\n\t\treturn\n\t}\n\n\tif showDiffUi {\n\t\tgetNewListener := func() net.Listener {\n\t\t\toutputFormat := \"line-by-line\"\n\t\t\tif diffUiSideBySide {\n\t\t\t\toutputFormat = \"side-by-side\"\n\t\t\t} else if diffUiLineByLine {\n\t\t\t\toutputFormat = \"line-by-line\"\n\t\t\t}\n\n\t\t\t// Properly escape the diff content for JavaScript\n\t\t\tdiffJSON, err := json.Marshal(diffs)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error encoding diff content: %v\", err)\n\t\t\t}\n\n\t\t\t// Create template data\n\t\t\tdata := struct {\n\t\t\t\tDiffContent  template.JS\n\t\t\t\tOutputFormat string\n\t\t\t}{\n\t\t\t\tDiffContent:  template.JS(diffJSON),\n\t\t\t\tOutputFormat: outputFormat,\n\t\t\t}\n\n\t\t\t// Parse and execute the template\n\t\t\ttmpl, err := template.New(\"diff\").Parse(htmlTemplate)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error parsing template: %v\", err)\n\t\t\t}\n\n\t\t\t// Use :0 to let the OS pick an available port\n\t\t\tlistener, err := net.Listen(\"tcp\", \":0\")\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error starting server: %v\", err)\n\t\t\t}\n\n\t\t\t// Get the actual port chosen\n\t\t\tport := listener.Addr().(*net.TCPAddr).Port\n\n\t\t\t// Start web server\n\t\t\tgo func() {\n\t\t\t\thttp.HandleFunc(\"/\"+outputFormat, func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Header().Set(\"Content-Type\", \"text/html; charset=utf-8\")\n\t\t\t\t\terr := tmpl.Execute(w, data)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\thttp.Serve(listener, nil)\n\t\t\t}()\n\n\t\t\tui.OpenURL(\"Showing \"+outputFormat+\" diffs in your default browser...\", fmt.Sprintf(\"http://localhost:%d/%s\", port, outputFormat))\n\n\t\t\tfmt.Println()\n\n\t\t\treturn listener\n\t\t}\n\n\t\tlistener := getNewListener()\n\t\tdefer listener.Close()\n\n\t\tvar relaunch bool\n\n\t\tfor {\n\t\t\tif relaunch {\n\t\t\t\tlistener.Close()\n\t\t\t\tlistener = getNewListener()\n\t\t\t\tdefer listener.Close()\n\t\t\t\trelaunch = false\n\t\t\t}\n\n\t\t\tif diffUiLineByLine {\n\t\t\t\tfmt.Printf(\"%s for side-by-side view\\n\", color.New(color.Bold, term.ColorHiGreen).Sprintf(\"(s)\"))\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\"%s for line-by-line view\\n\", color.New(color.Bold, term.ColorHiGreen).Sprintf(\"(l)\"))\n\t\t\t}\n\n\t\t\tfmt.Printf(\"%s for git diff format\\n\", color.New(color.Bold, term.ColorHiGreen).Sprintf(\"(g)\"))\n\t\t\t// fmt.Printf(\"%s to quit\\n\", color.New(color.Bold, term.ColorHiGreen).Sprintf(\"(q)\"))\n\n\t\t\ts := \"to exit menu/continue\"\n\t\t\tif fromTellMenu {\n\t\t\t\ts = \"to go back\"\n\t\t\t}\n\n\t\t\tfmt.Printf(\"%s %s %s %s %s\",\n\t\t\t\tcolor.New(term.ColorHiMagenta, color.Bold).Sprint(\"Press a hotkey,\"),\n\t\t\t\tcolor.New(color.FgHiWhite, color.Bold).Sprintf(\"↓\"),\n\t\t\t\tcolor.New(term.ColorHiMagenta, color.Bold).Sprintf(\"to select, or\"),\n\t\t\t\tcolor.New(color.FgHiWhite, color.Bold).Sprintf(\"enter\"),\n\t\t\t\tcolor.New(term.ColorHiMagenta, color.Bold).Sprintf(\"%s>\", s),\n\t\t\t)\n\n\t\t\tchar, key, err := term.GetUserKeyInput()\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting key: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Println()\n\n\t\t\tif key == keyboard.KeyArrowDown {\n\t\t\t\toptions := []string{}\n\t\t\t\tif diffUiLineByLine {\n\t\t\t\t\toptions = append(options, \"side-by-side\")\n\t\t\t\t} else {\n\t\t\t\t\toptions = append(options, \"line-by-line\")\n\t\t\t\t}\n\n\t\t\t\toptions = append(options, \"git diff\")\n\t\t\t\toptions = append(options, \"exit menu\")\n\n\t\t\t\tselected, err := term.SelectFromList(\n\t\t\t\t\t\"Select an action\",\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error selecting action: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif selected == \"side-by-side\" {\n\t\t\t\t\tdiffUiSideBySide = true\n\t\t\t\t\tdiffUiLineByLine = false\n\t\t\t\t\trelaunch = true\n\t\t\t\t} else if selected == \"line-by-line\" {\n\t\t\t\t\tdiffUiSideBySide = false\n\t\t\t\t\tdiffUiLineByLine = true\n\t\t\t\t\trelaunch = true\n\t\t\t\t} else if selected == \"git diff\" {\n\t\t\t\t\tshowGitDiff()\n\t\t\t\t} else if selected == \"exit menu\" {\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t} else if string(char) == \"g\" {\n\t\t\t\tshowGitDiff()\n\t\t\t} else if string(char) == \"s\" {\n\t\t\t\tdiffUiSideBySide = true\n\t\t\t\tdiffUiLineByLine = false\n\t\t\t\trelaunch = true\n\t\t\t} else if string(char) == \"l\" {\n\t\t\t\tdiffUiSideBySide = false\n\t\t\t\tdiffUiLineByLine = true\n\t\t\t\trelaunch = true\n\t\t\t} else if key == 13 || key == 10 || string(char) == \"q\" { // Check raw key codes for Enter/Return\n\t\t\t\tif term.IsRepl {\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tbreak\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t} else if string(char) == \"\\x03\" { // Ctrl+C\n\t\t\t\tos.Exit(0)\n\t\t\t} else {\n\t\t\t\tfmt.Println()\n\t\t\t\tterm.OutputSimpleError(\"Invalid hotkey\")\n\t\t\t\tfmt.Println()\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif plainTextOutput {\n\t\t\tfmt.Println(diffs)\n\t\t} else {\n\t\t\tterm.PageOutput(diffs)\n\t\t}\n\t\tfmt.Println()\n\t}\n}\n\nfunc showGitDiff() {\n\t_, err := lib.ExecPlandexCommandWithParams([]string{\"diff\", \"--git\"}, lib.ExecPlandexCommandParams{\n\t\tDisableSuggestions: true,\n\t})\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error showing git diff: %v\", err)\n\t}\n}\n\nvar htmlTemplate = `<!doctype html>\n<html lang=\"en-us\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <!-- Make sure to load the highlight.js CSS file before the Diff2Html CSS file -->\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css\" />\n    <link\n      rel=\"stylesheet\"\n      type=\"text/css\"\n      href=\"https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css\"\n    />\n    <script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js\"></script>\n  </head>\n  <script>\n    // Parse the JSON-encoded diff content\n    const diffString = {{.DiffContent}};\n\n    document.addEventListener('DOMContentLoaded', function () {\n      var targetElement = document.getElementById('myDiffElement');\n      var configuration = {\n        outputFormat: '{{.OutputFormat}}'\n      };\n      var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);\n      diff2htmlUi.draw();\n      diff2htmlUi.highlightCode();\n    });\n  </script>\n  <body>\n    <div id=\"myDiffElement\"></div>\n  </body>\n</html>`\n"
  },
  {
    "path": "app/cli/cmd/invite.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar inviteCmd = &cobra.Command{\n\tUse:   \"invite [email] [name] [org-role]\",\n\tShort: \"Invite a new user to the org\",\n\tRun:   invite,\n\tArgs:  cobra.MaximumNArgs(3),\n}\n\nfunc init() {\n\tRootCmd.AddCommand(inviteCmd)\n}\n\nfunc invite(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\temail, name, orgRoleName := \"\", \"\", \"\"\n\tif len(args) >= 1 {\n\t\temail = args[0]\n\t}\n\tif len(args) >= 2 {\n\t\tname = args[1]\n\t}\n\tif len(args) == 3 {\n\t\torgRoleName = args[2]\n\t}\n\n\tterm.StartSpinner(\"\")\n\torgRoles, err := api.Client.ListOrgRoles()\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to list org roles: %v\", err)\n\t}\n\n\tif email == \"\" {\n\t\tvar err error\n\t\temail, err = term.GetRequiredUserStringInput(\"Email:\")\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Failed to get email: %v\", err)\n\t\t}\n\t}\n\tif name == \"\" {\n\t\tvar err error\n\t\tname, err = term.GetRequiredUserStringInput(\"Name:\")\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Failed to get name: %v\", err)\n\t\t}\n\t}\n\n\tif orgRoleName == \"\" {\n\t\tvar orgRoleNames []string\n\t\tfor _, orgRole := range orgRoles {\n\t\t\torgRoleNames = append(orgRoleNames, orgRole.Label)\n\t\t}\n\n\t\tvar err error\n\t\torgRoleName, err = term.SelectFromList(\"Org role:\", orgRoleNames)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Failed to select org role: %v\", err)\n\t\t}\n\t}\n\n\tvar orgRoleId string\n\tfor _, orgRole := range orgRoles {\n\t\tif orgRole.Label == orgRoleName {\n\t\t\torgRoleId = orgRole.Id\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif orgRoleId == \"\" {\n\t\tterm.OutputErrorAndExit(\"Org role '%s' not found\", orgRoleName)\n\t}\n\n\tinviteRequest := shared.InviteRequest{\n\t\tEmail:     email,\n\t\tName:      name,\n\t\tOrgRoleId: orgRoleId,\n\t}\n\n\tterm.StartSpinner(\"\")\n\tapiErr := api.Client.InviteUser(inviteRequest)\n\tterm.StopSpinner()\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to invite user: %s\", apiErr.Msg)\n\t}\n\n\tfmt.Println(\"✅ Invite sent\")\n}\n"
  },
  {
    "path": "app/cli/cmd/load.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\trecursive       bool\n\tnamesOnly       bool\n\tnote            string\n\tforceSkipIgnore bool\n\timageDetail     string\n\tdefsOnly        bool\n)\n\nvar contextLoadCmd = &cobra.Command{\n\tUse:     \"load [files-or-urls...]\",\n\tAliases: []string{\"l\", \"add\"},\n\tShort:   \"Load context from various inputs\",\n\tLong:    `Load context from a file path, a directory, a URL, an image, a note, or piped data.`,\n\tRun:     contextLoad,\n}\n\nfunc init() {\n\tcontextLoadCmd.Flags().StringVarP(&note, \"note\", \"n\", \"\", \"Add a note to the context\")\n\tcontextLoadCmd.Flags().BoolVarP(&recursive, \"recursive\", \"r\", false, \"Search directories recursively\")\n\tcontextLoadCmd.Flags().BoolVar(&namesOnly, \"tree\", false, \"Load directory tree with file names only\")\n\tcontextLoadCmd.Flags().BoolVarP(&forceSkipIgnore, \"force\", \"f\", false, \"Load files even when ignored by .gitignore or .plandexignore\")\n\tcontextLoadCmd.Flags().StringVarP(&imageDetail, \"detail\", \"d\", \"high\", \"Image detail level (high or low)\")\n\tcontextLoadCmd.Flags().BoolVar(&defsOnly, \"map\", false, \"Load file maps (function/method/class signatures, variable names, types, etc.)\")\n\tRootCmd.AddCommand(contextLoadCmd)\n}\n\nfunc contextLoad(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t\treturn\n\t}\n\n\tlib.MustLoadContext(args, &types.LoadContextParams{\n\t\tNote:            note,\n\t\tRecursive:       recursive,\n\t\tNamesOnly:       namesOnly,\n\t\tForceSkipIgnore: forceSkipIgnore,\n\t\tImageDetail:     openai.ImageURLDetail(imageDetail),\n\t\tDefsOnly:        defsOnly,\n\t\tSessionId:       os.Getenv(\"PLANDEX_REPL_SESSION_ID\"),\n\t})\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"ls\", \"tell\", \"debug\")\n}\n"
  },
  {
    "path": "app/cli/cmd/log.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// logCmd represents the log command\nvar logCmd = &cobra.Command{\n\tUse:     \"log\",\n\tAliases: []string{\"history\", \"logs\"},\n\tShort:   \"Show plan history\",\n\tLong:    `Show plan history`,\n\tArgs:    cobra.NoArgs,\n\tRun:     runLog,\n}\n\nfunc init() {\n\t// Add log command\n\tRootCmd.AddCommand(logCmd)\n}\n\nfunc runLog(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\tres, apiErr := api.Client.ListLogs(lib.CurrentPlanId, lib.CurrentBranch)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting logs: %v\", apiErr)\n\t}\n\n\twithLocalTimestamps, err := convertTimestampsToLocal(res.Body)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error converting timestamps: %v\", err)\n\t}\n\n\tterm.PageOutput(withLocalTimestamps)\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"rewind\", \"continue\", \"convo\", \"convo 1\", \"convo 2-5\")\n\n}\n\nfunc convertTimestampsToLocal(input string) (string, error) {\n\tt := time.Now()\n\tzone, _ := t.Zone()\n\tre := lib.GitLogTimestampRegex\n\n\t// Function to convert matched timestamps assuming they are in UTC to local time.\n\treplaceFunc := func(match string) string {\n\t\tt, err := time.Parse(lib.GitLogTimestampFormat, match)\n\t\tif err != nil {\n\t\t\t// In case of an error, return the original match.\n\t\t\treturn match\n\t\t}\n\n\t\tlocalDt := t.Local()\n\t\tformattedTs := localDt.Format(\"Mon Jan 2, 2006 | 3:04:05pm\")\n\n\t\tif localDt.Day() == time.Now().Day() {\n\t\t\tformattedTs = localDt.Format(\"Today | 3:04:05pm\")\n\t\t} else if localDt.Day() == time.Now().AddDate(0, 0, -1).Day() {\n\t\t\tformattedTs = localDt.Format(\"Yesterday | 3:04:05pm\")\n\t\t}\n\n\t\t// Convert to local time and format back to a string without the timezone to match the original format.\n\t\treturn formattedTs + \" \" + zone\n\t}\n\n\t// Find all matches and replace them.\n\tresult := re.ReplaceAllStringFunc(input, replaceFunc)\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "app/cli/cmd/ls.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/format\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar contextCmd = &cobra.Command{\n\tUse:     \"ls\",\n\tAliases: []string{\"list-context\"},\n\tShort:   \"List everything in context\",\n\tRun:     listContext,\n}\n\nfunc listContext(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error listing context: %v\", err)\n\t}\n\n\tplanConfig, err := api.Client.GetPlanConfig(lib.CurrentPlanId)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plan config: %v\", err)\n\t}\n\tterm.StopSpinner()\n\n\ttotalTokens := 0\n\ttotalPlannerTokens := 0\n\ttotalMapTokens := 0\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetHeader([]string{\"#\", \"Name\", \"Type\", \"🪙\", \"Added\", \"Updated\"})\n\ttable.SetAutoWrapText(false)\n\n\tif len(contexts) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No context\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"load\")\n\t\treturn\n\t}\n\n\tfor i, context := range contexts {\n\t\ttotalTokens += context.NumTokens\n\n\t\tif context.ContextType == shared.ContextMapType {\n\t\t\ttotalMapTokens += context.NumTokens\n\t\t} else {\n\t\t\ttotalPlannerTokens += context.NumTokens\n\t\t}\n\n\t\tt, icon := context.TypeAndIcon()\n\n\t\tname := context.Name\n\t\tif name == \"\" {\n\t\t\tname = context.FilePath\n\t\t}\n\t\tif len(name) > 40 {\n\t\t\tname = name[:20] + \"⋯\" + name[len(name)-20:]\n\t\t}\n\n\t\trow := []string{\n\t\t\tstrconv.Itoa(i + 1),\n\t\t\t\" \" + icon + \" \" + name,\n\t\t\tt,\n\t\t\tstrconv.Itoa(context.NumTokens), //+ \" 🪙\",\n\t\t\tformat.Time(context.CreatedAt),\n\t\t\tformat.Time(context.UpdatedAt),\n\t\t}\n\t\ttable.Rich(row, []tablewriter.Colors{\n\t\t\t{tablewriter.Bold},\n\t\t\t{tablewriter.FgHiGreenColor, tablewriter.Bold},\n\t\t})\n\t}\n\n\ttable.Render()\n\n\ttokensTbl := tablewriter.NewWriter(os.Stdout)\n\ttokensTbl.SetAutoWrapText(false)\n\n\tif planConfig.AutoLoadContext {\n\t\ttokensTbl.Append([]string{\n\t\t\tcolor.New(term.ColorHiCyan, color.Bold).Sprintf(\"Map tokens →\") + color.New(color.Bold).Sprintf(\" %d 🪙\", totalMapTokens),\n\t\t\tcolor.New(term.ColorHiCyan, color.Bold).Sprintf(\"Context tokens →\") + color.New(color.Bold).Sprintf(\" %d 🪙\", totalPlannerTokens),\n\t\t})\n\t} else {\n\n\t\ttokensTbl.Append([]string{color.New(term.ColorHiCyan, color.Bold).Sprintf(\"Total tokens →\") + color.New(color.Bold).Sprintf(\" %d 🪙\", totalTokens)})\n\t}\n\ttokensTbl.Render()\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"load\", \"rm\", \"clear\")\n\n}\n\nfunc init() {\n\tRootCmd.AddCommand(contextCmd)\n\n}\n"
  },
  {
    "path": "app/cli/cmd/model_helpers.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n)\n\nfunc warnModelsFileLocalChanges(path, cmd string) (bool, error) {\n\tcmdPrefix := \"\\\\\"\n\tif !term.IsRepl {\n\t\tcmdPrefix = \"plandex \"\n\t}\n\n\tcolor.New(color.Bold, term.ColorHiYellow).Println(\"⚠️  The models file has local changes\")\n\n\tfmt.Println()\n\tfmt.Println(\"Path → \" + path)\n\tfmt.Println()\n\n\tfmt.Println(\"If you continue, local changes will be dropped in favor of the latest server state\")\n\n\tfmt.Println()\n\tfmt.Printf(\"To keep the local version instead, quit and run %s\\n\", color.New(color.Bold, color.BgCyan, color.FgHiWhite).\n\t\tSprintf(\" %s%s --save \", cmdPrefix, cmd))\n\tfmt.Println()\n\treturn term.ConfirmYesNo(\"Drop local changes and continue?\")\n\n}\n\ntype maybePromptAndOpenModelsFileResult struct {\n\tshouldReturn bool\n\tjsonData     []byte\n}\n\nfunc maybePromptAndOpenModelsFile(filePath, pathArg, cmd string, defaultConfig *shared.PlanConfig, planConfig *shared.PlanConfig) maybePromptAndOpenModelsFileResult {\n\n\tprintManual := func() {\n\t\tcmdPrefix := \"\\\\\"\n\t\tif !term.IsRepl {\n\t\t\tcmdPrefix = \"plandex \"\n\t\t}\n\t\tfmt.Println(\"To save changes, run \" +\n\t\t\tfmt.Sprintf(\" %s \", color.New(color.Bold, color.BgCyan, color.FgHiWhite).\n\t\t\t\tSprintf(\" %s%s --save%s \", cmdPrefix, cmd, pathArg)))\n\t}\n\n\tselectedEditor := lib.MaybePromptAndOpen(filePath, defaultConfig, planConfig)\n\n\tif selectedEditor {\n\t\tfmt.Println(\"📝 Opened in editor\")\n\t\tfmt.Println()\n\n\t\tconfirmed, err := term.ConfirmYesNo(\"Ready to save?\")\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error confirming: %v\", err)\n\t\t\treturn maybePromptAndOpenModelsFileResult{shouldReturn: true}\n\t\t}\n\n\t\tif !confirmed {\n\t\t\tfmt.Println(\"🙅‍♂️ Update canceled\")\n\t\t\tfmt.Println()\n\n\t\t\tprintManual()\n\t\t\treturn maybePromptAndOpenModelsFileResult{shouldReturn: true}\n\t\t}\n\n\t\t// get updated file state\n\t\tjsonData, err := os.ReadFile(filePath)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error reading JSON file: %v\", err)\n\t\t\treturn maybePromptAndOpenModelsFileResult{shouldReturn: true}\n\t\t}\n\n\t\treturn maybePromptAndOpenModelsFileResult{shouldReturn: false, jsonData: jsonData}\n\t} else {\n\t\t// No editor available or user chose manual\n\t\tfmt.Println(\"👨‍💻 Edit the file in your JSON editor of choice\")\n\t\tfmt.Println()\n\n\t\tprintManual()\n\t\treturn maybePromptAndOpenModelsFileResult{shouldReturn: true}\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/model_packs.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar customModelPacksOnly bool\n\nvar modelPacksCmd = &cobra.Command{\n\tUse:   \"model-packs\",\n\tShort: \"List all model packs\",\n\tRun:   listModelPacks,\n}\n\nvar createModelPackCmd = &cobra.Command{\n\tUse:   \"create\",\n\tShort: \"Create a model pack\",\n\tRun:   customModelsNotImplemented,\n}\n\nvar deleteModelPackCmd = &cobra.Command{\n\tUse:     \"delete\",\n\tAliases: []string{\"rm\"},\n\tShort:   \"Delete a model pack by name or index\",\n\tRun:     customModelsNotImplemented,\n}\n\nvar updateModelPackCmd = &cobra.Command{\n\tUse:   \"update\",\n\tShort: \"Update a model pack by name\",\n\tRun:   customModelsNotImplemented,\n}\n\nvar showModelPackCmd = &cobra.Command{\n\tUse:   \"show [name]\",\n\tShort: \"Show a model pack by name\",\n\tArgs:  cobra.MaximumNArgs(1),\n\tRun:   showModelPack,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(modelPacksCmd)\n\tmodelPacksCmd.AddCommand(createModelPackCmd)\n\tmodelPacksCmd.AddCommand(deleteModelPackCmd)\n\tmodelPacksCmd.AddCommand(updateModelPackCmd)\n\tmodelPacksCmd.AddCommand(showModelPackCmd)\n\tmodelPacksCmd.Flags().BoolVarP(&customModelPacksOnly, \"custom\", \"c\", false, \"Only show custom model packs\")\n\tmodelPacksCmd.Flags().BoolVarP(&allProperties, \"all\", \"a\", false, \"Show all properties\")\n}\n\nfunc listModelPacks(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tterm.StartSpinner(\"\")\n\tbuiltInModelPacks := shared.BuiltInModelPacks\n\n\tif auth.Current.IsCloud {\n\t\tfiltered := []*shared.ModelPack{}\n\t\tfor _, mp := range builtInModelPacks {\n\t\t\tif mp.LocalProvider == \"\" {\n\t\t\t\tfiltered = append(filtered, mp)\n\t\t\t}\n\t\t}\n\t\tbuiltInModelPacks = filtered\n\t}\n\n\tcustomModelPacks, err := api.Client.ListModelPacks()\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error fetching model packs: %v\", err)\n\t\treturn\n\t}\n\n\tif !customModelPacksOnly {\n\t\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🏠 Built-in Model Packs\")\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.SetAutoWrapText(true)\n\t\ttable.SetRowLine(true)\n\t\ttable.SetHeader([]string{\"Name\", \"Description\"})\n\t\tfor _, set := range builtInModelPacks {\n\t\t\ttable.Append([]string{set.Name, set.Description})\n\t\t}\n\t\ttable.Render()\n\t\tfmt.Println()\n\t}\n\n\tif len(customModelPacks) > 0 {\n\t\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🛠️  Custom Model Packs\")\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.SetAutoWrapText(true)\n\t\ttable.SetRowLine(true)\n\t\ttable.SetHeader([]string{\"#\", \"Name\", \"Description\"})\n\t\tfor i, set := range customModelPacks {\n\t\t\ttable.Append([]string{fmt.Sprintf(\"%d\", i+1), set.Name, set.Description})\n\t\t}\n\t\ttable.Render()\n\n\t\tfmt.Println()\n\t} else if customModelPacksOnly {\n\t\tfmt.Println(\"🤷‍♂️ No custom model packs\")\n\t\tfmt.Println()\n\t}\n\n\tif customModelPacksOnly && len(customModelPacks) > 0 {\n\t\tterm.PrintCmds(\"\", \"model-packs show\", \"models custom\")\n\t} else if len(customModelPacks) > 0 {\n\t\tterm.PrintCmds(\"\", \"model-packs --custom\", \"model-packs show\", \"models custom\")\n\t} else {\n\t\tterm.PrintCmds(\"\", \"model-packs show\", \"models custom\")\n\t}\n\n}\n\nfunc showModelPack(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tterm.StartSpinner(\"\")\n\tcustomModelPacks, apiErr := api.Client.ListModelPacks()\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error fetching models: %v\", apiErr)\n\t}\n\tcustomModels, err := api.Client.ListCustomModels()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error fetching custom models: %v\", err)\n\t}\n\tcustomModelsById := make(map[shared.ModelId]*shared.CustomModel)\n\tfor _, m := range customModels {\n\t\tcustomModelsById[m.ModelId] = m\n\t}\n\n\tterm.StopSpinner()\n\n\tmodelPacks := []*shared.ModelPack{}\n\tmodelPacks = append(modelPacks, customModelPacks...)\n\n\tbuiltInModelPacks := shared.BuiltInModelPacks\n\tif auth.Current.IsCloud {\n\t\tfiltered := []*shared.ModelPack{}\n\t\tfor _, mp := range builtInModelPacks {\n\t\t\tif mp.LocalProvider == \"\" {\n\t\t\t\tfiltered = append(filtered, mp)\n\t\t\t}\n\t\t}\n\t\tbuiltInModelPacks = filtered\n\t}\n\tmodelPacks = append(modelPacks, builtInModelPacks...)\n\n\tvar name string\n\tif len(args) > 0 {\n\t\tname = args[0]\n\t}\n\n\tvar modelPack *shared.ModelPack\n\n\tif name == \"\" {\n\t\topts := make([]string, len(modelPacks))\n\t\tfor i, mp := range modelPacks {\n\t\t\topts[i] = mp.Name\n\t\t}\n\n\t\tselected, err := term.SelectFromList(\"Select a model pack:\", opts)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting model pack: %v\", err)\n\t\t}\n\n\t\tfor _, mp := range modelPacks {\n\t\t\tif mp.Name == selected {\n\t\t\t\tmodelPack = mp\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, mp := range modelPacks {\n\t\t\tif mp.Name == name {\n\t\t\t\tmodelPack = mp\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif modelPack == nil {\n\t\tterm.OutputErrorAndExit(\"Model pack not found\")\n\t\treturn\n\t}\n\n\trenderModelPack(modelPack, customModelsById, allProperties)\n\n\tfmt.Println()\n\n\tterm.PrintCmds(\"\", \"set-model\", \"set-model default\", \"models custom\")\n}\n\n// func getModelRoleConfig(customModels []*shared.CustomModel, modelRole shared.ModelRole) shared.ModelRoleConfig {\n// \t_, modelConfig := getModelWithRoleConfig(customModels, modelRole)\n// \treturn modelConfig\n// }\n\n// func getModelWithRoleConfig(customModels []*shared.CustomModel, modelRole shared.ModelRole) (*shared.CustomModel, shared.ModelRoleConfig) {\n// \trole := string(modelRole)\n\n// \tmodelId := getModelIdForRole(customModels, modelRole)\n\n// \ttemperatureStr, err := term.GetUserStringInputWithDefault(\"Temperature for \"+role+\":\", fmt.Sprintf(\"%.1f\", shared.DefaultConfigByRole[modelRole].Temperature))\n// \tif err != nil {\n// \t\tterm.OutputErrorAndExit(\"Error reading temperature: %v\", err)\n// \t}\n// \ttemperature, err := strconv.ParseFloat(temperatureStr, 32)\n// \tif err != nil {\n// \t\tterm.OutputErrorAndExit(\"Invalid number for temperature: %v\", err)\n// \t}\n\n// \ttopPStr, err := term.GetUserStringInputWithDefault(\"Top P for \"+role+\":\", fmt.Sprintf(\"%.1f\", shared.DefaultConfigByRole[modelRole].TopP))\n// \tif err != nil {\n// \t\tterm.OutputErrorAndExit(\"Error reading top P: %v\", err)\n// \t}\n// \ttopP, err := strconv.ParseFloat(topPStr, 32)\n// \tif err != nil {\n// \t\tterm.OutputErrorAndExit(\"Invalid number for top P: %v\", err)\n// \t}\n\n// \tvar reservedOutputTokens int\n// \tif modelRole == shared.ModelRoleBuilder || modelRole == shared.ModelRolePlanner || modelRole == shared.ModelRoleWholeFileBuilder {\n// \t\treservedOutputTokensStr, err := term.GetUserStringInputWithDefault(\"Reserved output tokens for \"+role+\":\", fmt.Sprintf(\"%d\", model.ReservedOutputTokens))\n// \t\tif err != nil {\n// \t\t\tterm.OutputErrorAndExit(\"Error reading reserved output tokens: %v\", err)\n// \t\t}\n// \t\treservedOutputTokens, err = strconv.Atoi(reservedOutputTokensStr)\n// \t\tif err != nil {\n// \t\t\tterm.OutputErrorAndExit(\"Invalid number for reserved output tokens: %v\", err)\n// \t\t}\n// \t}\n\n// \treturn model, shared.ModelRoleConfig{\n// \t\tModelId:              model.ModelId,\n// \t\tRole:                 modelRole,\n// \t\tTemperature:          float32(temperature),\n// \t\tTopP:                 float32(topP),\n// \t\tReservedOutputTokens: reservedOutputTokens,\n// \t}\n// }\n\n// func getPlannerRoleConfig(customModels []*shared.CustomModel) shared.PlannerRoleConfig {\n// \tmodel, modelConfig := getModelWithRoleConfig(customModels, shared.ModelRolePlanner)\n\n// \treturn shared.PlannerRoleConfig{\n// \t\tModelRoleConfig: modelConfig,\n// \t\tPlannerModelConfig: shared.PlannerModelConfig{\n// \t\t\tMaxConvoTokens: model.DefaultMaxConvoTokens,\n// \t\t},\n// \t}\n// }\n\n// func getModelIdForRole(customModels []*shared.CustomModel, role shared.ModelRole) shared.ModelId {\n// \tcolor.New(color.Bold).Printf(\"Select a model for the %s role 👇\\n\", role)\n// \treturn lib.SelectModelIdForRole(customModels, role)\n// }\n"
  },
  {
    "path": "app/cli/cmd/model_providers.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar customProvidersOnly bool\n\nvar providersCmd = &cobra.Command{\n\tUse:   \"providers\",\n\tShort: \"List built-in and custom model providers\",\n\tRun:   listProviders,\n}\n\nvar addProviderCmd = &cobra.Command{\n\tUse:     \"add\",\n\tAliases: []string{\"create\"},\n\tShort:   \"Add a custom model provider\",\n\tRun:     customModelsNotImplemented,\n}\n\nvar updateProviderCmd = &cobra.Command{\n\tUse:     \"update\",\n\tAliases: []string{\"edit\"},\n\tShort:   \"Update a custom model provider\",\n\tRun:     customModelsNotImplemented,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(providersCmd)\n\tprovidersCmd.Flags().BoolVarP(&customProvidersOnly, \"custom\", \"c\", false, \"List custom providers only\")\n\tprovidersCmd.AddCommand(addProviderCmd)\n\tprovidersCmd.AddCommand(updateProviderCmd)\n}\n\nfunc listProviders(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tvar customProviders []*shared.CustomProvider\n\tvar apiErr *shared.ApiError\n\n\tif customProvidersOnly && auth.Current.IsCloud {\n\t\tterm.OutputErrorAndExit(\"Custom providers are not supported on Plandex Cloud\")\n\t\treturn\n\t}\n\n\tif !auth.Current.IsCloud {\n\t\tterm.StartSpinner(\"\")\n\t\tcustomProviders, apiErr = api.Client.ListCustomProviders()\n\t\tterm.StopSpinner()\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error fetching providers: %v\", apiErr.Msg)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif customProvidersOnly && len(customProviders) == 0 {\n\t\tfmt.Println(\"🤷‍♂️  No custom providers\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"models custom\")\n\t\treturn\n\t}\n\n\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🏠 Built-in Providers\")\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(true)\n\n\tvar header []string\n\tif auth.Current.IsCloud {\n\t\theader = []string{\"ID\", \"API Key\", \"Other Vars\"}\n\t} else {\n\t\theader = []string{\"ID\", \"Base URL\", \"API Key\", \"Other Vars\"}\n\t}\n\ttable.SetHeader(header)\n\tfor _, p := range shared.AllModelProviders {\n\t\tif p == shared.ModelProviderCustom {\n\t\t\tcontinue\n\t\t}\n\t\tconfig := shared.BuiltInModelProviderConfigs[p]\n\t\tif config.LocalOnly && auth.Current.IsCloud {\n\t\t\tcontinue\n\t\t}\n\t\tvar apiKey string\n\t\tif config.ApiKeyEnvVar != \"\" {\n\t\t\tapiKey = config.ApiKeyEnvVar\n\t\t} else if config.SkipAuth {\n\t\t\tapiKey = \"No Auth\"\n\t\t} else if config.HasClaudeMaxAuth {\n\t\t\tapiKey = \"Claude Max Oauth\"\n\t\t}\n\n\t\textraVars := []string{}\n\t\tif config.Provider == shared.ModelProviderAmazonBedrock {\n\t\t\textraVars = append(extraVars, \"PLANDEX_AWS_PROFILE\")\n\t\t}\n\t\tfor _, v := range config.ExtraAuthVars {\n\t\t\textraVars = append(extraVars, v.Var)\n\t\t}\n\n\t\tif auth.Current.IsCloud {\n\t\t\ttable.Append([]string{\n\t\t\t\tstring(p),\n\t\t\t\tapiKey,\n\t\t\t\tstrings.Join(extraVars, \"\\n\"),\n\t\t\t})\n\t\t} else {\n\t\t\ttable.Append([]string{\n\t\t\t\tstring(p),\n\t\t\t\tconfig.BaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tstrings.Join(extraVars, \"\\n\"),\n\t\t\t})\n\t\t}\n\t}\n\ttable.Render()\n\tfmt.Println()\n\n\tif len(customProviders) > 0 {\n\t\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🛠️  Custom Providers\")\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"#\", \"Name\", \"Base URL\", \"API Key\", \"Other Vars\"})\n\t\tfor i, p := range customProviders {\n\t\t\textraVars := []string{}\n\t\t\tfor _, v := range p.ExtraAuthVars {\n\t\t\t\textraVars = append(extraVars, v.Var)\n\t\t\t}\n\t\t\tapiKey := p.ApiKeyEnvVar\n\t\t\tif apiKey == \"\" && p.SkipAuth {\n\t\t\t\tapiKey = \"No Auth\"\n\t\t\t}\n\n\t\t\ttable.Append([]string{\n\t\t\t\tstrconv.Itoa(i + 1),\n\t\t\t\tp.Name,\n\t\t\t\tp.BaseUrl,\n\t\t\t\tapiKey,\n\t\t\t\tstrings.Join(extraVars, \"\\n\"),\n\t\t\t})\n\t\t}\n\t\ttable.Render()\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(color.New(color.Bold, term.ColorHiCyan).Sprint(\"\\n📖 Per-provider instructions\"))\n\tfmt.Println(\"Go to → \" + color.New(color.Bold).Sprint(\"https://docs.plandex.ai/models/model-providers\"))\n\tfmt.Println()\n\n\tterm.PrintCmds(\"\", \"models custom\")\n}\n"
  },
  {
    "path": "app/cli/cmd/models.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar customModelsOnly bool\nvar allProperties bool\nvar saveCustomModels bool\nvar customModelsPath string\n\nfunc init() {\n\tRootCmd.AddCommand(modelsCmd)\n\n\tmodelsCmd.Flags().BoolVarP(&allProperties, \"all\", \"a\", false, \"Show all properties\")\n\n\tmodelsCmd.AddCommand(listAvailableModelsCmd)\n\tmodelsCmd.AddCommand(addCustomModelCmd)\n\tmodelsCmd.AddCommand(updateCustomModelCmd)\n\tmodelsCmd.AddCommand(manageCustomModelsCmd)\n\tmodelsCmd.AddCommand(deleteCustomModelCmd)\n\tmodelsCmd.AddCommand(defaultModelsCmd)\n\n\tmanageCustomModelsCmd.Flags().BoolVar(&saveCustomModels, \"save\", false, \"Save custom models\")\n\tmanageCustomModelsCmd.Flags().StringVarP(&customModelsPath, \"file\", \"f\", \"\", \"Path to custom models file\")\n\n\tlistAvailableModelsCmd.Flags().BoolVarP(&customModelsOnly, \"custom\", \"c\", false, \"List custom models only\")\n}\n\nvar modelsCmd = &cobra.Command{\n\tUse:   \"models\",\n\tShort: \"Show plan model settings\",\n\tRun:   models,\n}\n\nvar defaultModelsCmd = &cobra.Command{\n\tUse:   \"default\",\n\tShort: \"Show default model settings for new plans\",\n\tRun:   defaultModels,\n}\n\nvar listAvailableModelsCmd = &cobra.Command{\n\tUse:     \"available\",\n\tAliases: []string{\"avail\"},\n\tShort:   \"List all available models\",\n\tRun:     listAvailableModels,\n}\n\nvar manageCustomModelsCmd = &cobra.Command{\n\tUse:   \"custom\",\n\tShort: \"Manage custom models, providers, and model packs\",\n\tRun:   manageCustomModels,\n}\n\nvar addCustomModelCmd = &cobra.Command{\n\tUse:     \"add\",\n\tAliases: []string{\"create\"},\n\tShort:   \"Add a custom model\",\n\tRun:     customModelsNotImplemented,\n}\n\nvar updateCustomModelCmd = &cobra.Command{\n\tUse:     \"update\",\n\tAliases: []string{\"edit\"},\n\tShort:   \"Update a custom model\",\n\tRun:     customModelsNotImplemented,\n}\n\nvar deleteCustomModelCmd = &cobra.Command{\n\tUse:     \"rm\",\n\tAliases: []string{\"remove\", \"delete\"},\n\tShort:   \"Remove a custom model\",\n\tArgs:    cobra.MaximumNArgs(1),\n\tRun:     customModelsNotImplemented,\n}\n\nfunc manageCustomModels(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tterm.StartSpinner(\"\")\n\n\tvar serverModelsInput *shared.ModelsInput\n\tvar defaultConfig *shared.PlanConfig\n\n\terrCh := make(chan error, 2)\n\n\tgo func() {\n\t\tvar err error\n\t\tserverModelsInput, err = lib.GetServerModelsInput()\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting server models input: %v\", err)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tvar apiErr *shared.ApiError\n\t\tdefaultConfig, apiErr = api.Client.GetDefaultPlanConfig()\n\t\tif apiErr != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting default config: %v\", apiErr.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(err.Error())\n\t\t\treturn\n\t\t}\n\t}\n\n\tusingDefaultPath := false\n\tif customModelsPath == \"\" {\n\t\tusingDefaultPath = true\n\t\tcustomModelsPath = lib.GetCustomModelsPath(auth.Current.UserId)\n\t}\n\n\texists, err := fs.FileExists(customModelsPath)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error checking custom models file: %v\", err)\n\t\treturn\n\t}\n\n\tif saveCustomModels {\n\t\tif !exists {\n\t\t\tterm.OutputErrorAndExit(\"File not found: %s\", customModelsPath)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif serverModelsInput.IsEmpty() {\n\t\t\tjsonData, err := json.MarshalIndent(getExampleTemplate(auth.Current.IsCloud, auth.Current.IntegratedModelsMode), \"\", \"  \")\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error marshalling template: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terr = os.MkdirAll(filepath.Dir(customModelsPath), 0755)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error creating directory: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terr = os.WriteFile(customModelsPath, jsonData, 0644)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error writing template file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tterm.StopSpinner()\n\n\t\t\tfmt.Printf(\"🧠 Example models file → %s\\n\", customModelsPath)\n\t\t\tfmt.Println(\"👨‍💻 Edit it, then come back here to save\")\n\t\t\tfmt.Println()\n\t\t} else {\n\t\t\tserverClientModelsInput := serverModelsInput.ToClientModelsInput()\n\t\t\tserverClientModelsInput.PrepareUpdate()\n\n\t\t\tvar localModelsInput shared.ModelsInput\n\n\t\t\tif exists {\n\t\t\t\t// we only do a conflict check on the default path in the home dir\n\t\t\t\t// if user specifies the path and the file exists, just open the file without checking for conflicts\n\t\t\t\tif usingDefaultPath {\n\t\t\t\t\tres, err := lib.CustomModelsCheckLocalChanges(customModelsPath)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error checking local changes: %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tlocalModelsInput = res.LocalModelsInput\n\t\t\t\t\tif res.HasLocalChanges {\n\t\t\t\t\t\tterm.StopSpinner()\n\n\t\t\t\t\t\tres, err := warnModelsFileLocalChanges(customModelsPath, \"models custom\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error confirming: %v\", err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif !res {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfmt.Println()\n\t\t\t\t\t\tterm.StartSpinner(\"\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !serverModelsInput.Equals(localModelsInput) {\n\t\t\t\terr := lib.WriteCustomModelsFile(customModelsPath, serverModelsInput)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error saving custom models file: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tterm.StopSpinner()\n\n\t\t\tfmt.Printf(\"🧠 %s → %s\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(\"Models file\"), customModelsPath)\n\t\t\tfmt.Println(\"👨‍💻 Edit it, then come back here to save\")\n\t\t\tfmt.Println()\n\t\t}\n\n\t\tpathArg := \"\"\n\t\tif !usingDefaultPath {\n\t\t\tpathArg = fmt.Sprintf(\" --file %s\", customModelsPath)\n\t\t}\n\n\t\tres := maybePromptAndOpenModelsFile(customModelsPath, pathArg, \"models custom\", defaultConfig, nil)\n\t\tif res.shouldReturn {\n\t\t\treturn\n\t\t}\n\t}\n\n\tdidUpdate := lib.MustSyncCustomModels(customModelsPath, serverModelsInput)\n\n\tif !didUpdate {\n\t\tfmt.Println(\"🤷‍♂️ No changes to custom models/providers/model packs\")\n\t}\n}\n\nfunc models(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tplan, err := api.Client.GetPlan(lib.CurrentPlanId)\n\n\tif err != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"Error getting plan: %v\", err)\n\t\treturn\n\t}\n\n\tsettings, err := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting settings: %v\", err)\n\t\treturn\n\t}\n\n\ttitle := fmt.Sprintf(\"%s Model Settings\", color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name))\n\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(false)\n\ttable.Append([]string{title})\n\ttable.Render()\n\tfmt.Println()\n\n\trenderSettings(settings, allProperties)\n\n\tterm.PrintCmds(\"\", \"set-model\", \"models available\", \"models default\", \"models custom\")\n}\n\nfunc defaultModels(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tterm.StartSpinner(\"\")\n\tsettings, err := api.Client.GetOrgDefaultSettings()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting default model settings: %v\", err)\n\t\treturn\n\t}\n\n\tterm.StopSpinner()\n\n\ttitle := fmt.Sprintf(\"%s Model Settings\", color.New(color.Bold, term.ColorHiGreen).Sprint(\"Org-Wide Default\"))\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(false)\n\ttable.Append([]string{title})\n\ttable.Render()\n\tfmt.Println()\n\n\trenderSettings(settings, allProperties)\n\n\tterm.PrintCmds(\"\", \"set-model default\", \"models available\", \"models\")\n}\n\nfunc listAvailableModels(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tif customModelsOnly && auth.Current.IntegratedModelsMode {\n\t\tterm.OutputErrorAndExit(\"Custom models are not supported in Integrated Models mode on Plandex Cloud\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tcustomModels, err := api.Client.ListCustomModels()\n\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error fetching custom models: %v\", err)\n\t\treturn\n\t}\n\n\tif !customModelsOnly {\n\t\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🏠 Built-in Models\")\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"Model\", \"Input\", \"Output\", \"Reserved\"})\n\t\tfor _, model := range shared.BuiltInBaseModels {\n\t\t\tif auth.Current.IsCloud && model.IsLocalOnly() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttable.Append([]string{string(model.ModelId), fmt.Sprintf(\"%d 🪙\", model.MaxTokens), fmt.Sprintf(\"%d 🪙\", model.MaxOutputTokens), fmt.Sprintf(\"%d 🪙\", model.ReservedOutputTokens)})\n\t\t}\n\t\ttable.Render()\n\t\tfmt.Println()\n\t}\n\n\tif len(customModels) > 0 {\n\t\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🛠️  Custom Models\")\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"#\", \"ID\", \"🪙\"})\n\t\tfor i, model := range customModels {\n\t\t\ttable.Append([]string{fmt.Sprintf(\"%d\", i+1), string(model.ModelId), strconv.Itoa(model.MaxTokens)})\n\t\t}\n\t\ttable.Render()\n\t} else if customModelsOnly {\n\t\tfmt.Println(\"🤷‍♂️ No custom models\")\n\t}\n\tfmt.Println()\n\n\tif customModelsOnly {\n\t\tif len(customModels) > 0 {\n\t\t\tterm.PrintCmds(\"\", \"models\", \"set-model\", \"models custom\")\n\t\t} else {\n\t\t\tterm.PrintCmds(\"\", \"models custom\")\n\t\t}\n\t} else {\n\t\tterm.PrintCmds(\"\", \"models available --custom\", \"models\", \"set-model\", \"models custom\")\n\t}\n}\n\nfunc renderSettings(settings *shared.PlanSettings, allProperties bool) {\n\tmodelPack := settings.GetModelPack()\n\n\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🎛️  Current Model Pack\")\n\trenderModelPack(modelPack, settings.CustomModelsById, allProperties)\n\n\tif allProperties {\n\t\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🧠 Planner Defaults\")\n\t\ttable := tablewriter.NewWriter(os.Stdout)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"Max Tokens\", \"Max Convo Tokens\"})\n\t\ttable.Append([]string{\n\t\t\tfmt.Sprintf(\"%d\", modelPack.Planner.GetFinalLargeContextFallback().GetSharedBaseConfig(settings).MaxTokens),\n\t\t\tfmt.Sprintf(\"%d\", modelPack.Planner.GetMaxConvoTokens(settings)),\n\t\t})\n\t\ttable.Render()\n\t\tfmt.Println()\n\t}\n}\n\nfunc renderModelPack(modelPack *shared.ModelPack, customModelsById map[shared.ModelId]*shared.CustomModel, allProperties bool) {\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoFormatHeaders(false)\n\ttable.SetAutoWrapText(true)\n\ttable.SetColWidth(64)\n\ttable.SetHeader([]string{modelPack.Name})\n\ttable.Append([]string{modelPack.Description})\n\ttable.Render()\n\tfmt.Println()\n\n\tcolor.New(color.Bold, term.ColorHiCyan).Println(\"🤖 Models\")\n\ttable = tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(false)\n\tcols := []string{\n\t\t\"Role\",\n\t\t\"Model\",\n\t}\n\talign := []int{\n\t\ttablewriter.ALIGN_LEFT, // Role\n\t\ttablewriter.ALIGN_LEFT, // Model\n\t}\n\tif allProperties {\n\t\tcols = append(cols, []string{\n\t\t\t\"Temperature\",\n\t\t\t\"Top P\",\n\t\t\t\"Max Input\",\n\t\t}...)\n\t\talign = append(align, []int{\n\t\t\ttablewriter.ALIGN_RIGHT, // Temperature\n\t\t\ttablewriter.ALIGN_RIGHT, // Top P\n\t\t\ttablewriter.ALIGN_RIGHT, // Max Input\n\t\t}...)\n\t}\n\ttable.SetHeader(cols)\n\ttable.SetColumnAlignment(align)\n\n\tanyRoleParamsDisabled := false\n\n\tvar addModelRow func(role string, config shared.ModelRoleConfig, indent int)\n\taddModelRow = func(role string, config shared.ModelRoleConfig, indent int) {\n\t\tif indent > 0 {\n\t\t\trole = \"└─ \" + role\n\t\t\tfor i := 0; i < indent-1; i++ {\n\t\t\t\trole = \" \" + role\n\t\t\t}\n\t\t}\n\n\t\tvar temp float32\n\t\tvar topP float32\n\t\tvar disabled bool\n\n\t\tsharedBaseConfig := config.GetSharedBaseConfigWithCustomModels(customModelsById)\n\n\t\tif sharedBaseConfig.RoleParamsDisabled {\n\t\t\ttemp = 1\n\t\t\ttopP = 1\n\t\t\tdisabled = true\n\t\t\tanyRoleParamsDisabled = true\n\t\t} else {\n\t\t\ttemp = config.Temperature\n\t\t\ttopP = config.TopP\n\t\t}\n\n\t\ttempStr := fmt.Sprintf(\"%.1f\", temp)\n\t\tif disabled {\n\t\t\ttempStr = \"*\" + tempStr\n\t\t}\n\n\t\ttopPStr := fmt.Sprintf(\"%.1f\", topP)\n\t\tif disabled {\n\t\t\ttopPStr = \"*\" + topPStr\n\t\t}\n\n\t\trow := []string{\n\t\t\trole,\n\t\t\tstring(config.GetModelId()),\n\t\t}\n\n\t\tif allProperties {\n\t\t\trow = append(row, []string{\n\t\t\t\ttempStr,\n\t\t\t\ttopPStr,\n\t\t\t\tfmt.Sprintf(\"%d 🪙\", sharedBaseConfig.MaxTokens-config.GetReservedOutputTokens(customModelsById)),\n\t\t\t}...)\n\t\t}\n\t\ttable.Append(row)\n\n\t\t// Add large context and large output fallback(s) if present\n\t\tif config.LargeContextFallback != nil {\n\t\t\taddModelRow(\"large-context\", *config.LargeContextFallback, indent+1)\n\t\t}\n\n\t\tif config.LargeOutputFallback != nil {\n\t\t\taddModelRow(\"large-output\", *config.LargeOutputFallback, indent+1)\n\t\t}\n\n\t\tif config.StrongModel != nil {\n\t\t\taddModelRow(\"strong\", *config.StrongModel, indent+1)\n\t\t}\n\n\t\tif config.ErrorFallback != nil {\n\t\t\taddModelRow(\"error\", *config.ErrorFallback, indent+1)\n\t\t}\n\t}\n\n\taddModelRow(string(shared.ModelRolePlanner), modelPack.Planner.ModelRoleConfig, 0)\n\n\taddModelRow(string(shared.ModelRoleArchitect), modelPack.GetArchitect(), 0)\n\taddModelRow(string(shared.ModelRoleCoder), modelPack.GetCoder(), 0)\n\taddModelRow(string(shared.ModelRolePlanSummary), modelPack.PlanSummary, 0)\n\taddModelRow(string(shared.ModelRoleBuilder), modelPack.Builder, 0)\n\taddModelRow(string(shared.ModelRoleWholeFileBuilder), modelPack.GetWholeFileBuilder(), 0)\n\taddModelRow(string(shared.ModelRoleName), modelPack.Namer, 0)\n\taddModelRow(string(shared.ModelRoleCommitMsg), modelPack.CommitMsg, 0)\n\taddModelRow(string(shared.ModelRoleExecStatus), modelPack.ExecStatus, 0)\n\ttable.Render()\n\n\tif anyRoleParamsDisabled && allProperties {\n\t\tfmt.Println(\"* these models do not support changing temperature or top p\")\n\t}\n\n\tfmt.Println()\n\n}\n\nfunc customModelsNotImplemented(cmd *cobra.Command, args []string) {\n\tcolor.New(color.Bold, color.FgHiRed).Println(\"⛔️ Not implemented\")\n\tfmt.Println()\n\tfmt.Println(\"Use \" + color.New(color.BgCyan, color.FgHiWhite).Sprint(\" plandex models custom \") + \" to manage custom models, providers, and model packs\")\n\tos.Exit(1)\n}\n\nfunc getExampleTemplate(isCloud, isCloudIntegratedModels bool) shared.ClientModelsInput {\n\texampleProviderName := \"togetherai\"\n\n\tvar customProviders []*shared.CustomProvider\n\tusesProviders := []shared.BaseModelUsesProvider{}\n\tif !isCloud {\n\t\tcustomProviders = append(customProviders, &shared.CustomProvider{\n\t\t\tName:         exampleProviderName,\n\t\t\tBaseUrl:      \"https://api.together.xyz/v1\",\n\t\t\tApiKeyEnvVar: \"TOGETHER_API_KEY\",\n\t\t})\n\n\t\tusesProviders = append(usesProviders, shared.BaseModelUsesProvider{\n\t\t\tProvider:       shared.ModelProviderCustom,\n\t\t\tCustomProvider: &exampleProviderName,\n\t\t\tModelName:      \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\",\n\t\t})\n\t}\n\tusesProviders = append(usesProviders, shared.BaseModelUsesProvider{\n\t\tProvider:  shared.ModelProviderOpenRouter,\n\t\tModelName: \"meta-llama/llama-4-maverick\",\n\t})\n\n\tvar customModels []*shared.CustomModel\n\tif !isCloudIntegratedModels {\n\t\tcustomModels = []*shared.CustomModel{\n\t\t\t{\n\t\t\t\tModelId:     shared.ModelId(\"meta-llama/llama-4-maverick\"),\n\t\t\t\tPublisher:   shared.ModelPublisher(\"meta-llama\"),\n\t\t\t\tDescription: \"Meta Llama 4 Maverick\",\n\n\t\t\t\tBaseModelShared: shared.BaseModelShared{\n\t\t\t\t\tDefaultMaxConvoTokens: 75000,\n\t\t\t\t\tMaxTokens:             1048576,\n\t\t\t\t\tMaxOutputTokens:       16000,\n\t\t\t\t\tReservedOutputTokens:  16000,\n\t\t\t\t\tModelCompatibility:    shared.FullCompatibility,\n\t\t\t\t\tPreferredOutputFormat: shared.ModelOutputFormatXml,\n\t\t\t\t},\n\n\t\t\t\tProviders: usesProviders,\n\t\t\t},\n\t\t}\n\t}\n\n\tlightModelId := \"meta-llama/llama-4-maverick\"\n\tif len(customModels) == 0 {\n\t\tlightModelId = \"mistral/devstral-small\"\n\t}\n\n\treturn shared.ClientModelsInput{\n\t\tSchemaUrl:       shared.SchemaUrlInputConfig,\n\t\tCustomProviders: customProviders,\n\t\tCustomModels:    customModels,\n\t\tCustomModelPacks: []*shared.ClientModelPackSchema{\n\t\t\t{\n\t\t\t\tName:        \"example-model-pack\",\n\t\t\t\tDescription: \"Example model pack\",\n\t\t\t\tClientModelPackSchemaRoles: shared.ClientModelPackSchemaRoles{\n\t\t\t\t\tPlanner:   \"deepseek/r1\",\n\t\t\t\t\tArchitect: \"deepseek/r1\",\n\t\t\t\t\tCoder: &shared.ModelRoleConfigSchema{\n\t\t\t\t\t\tModelId: \"deepseek/v3\",\n\t\t\t\t\t\tLargeContextFallback: &shared.ModelRoleConfigSchema{\n\t\t\t\t\t\t\tModelId: \"google/gemini-2.5-pro\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tErrorFallback: &shared.ModelRoleConfigSchema{\n\t\t\t\t\t\t\tModelId: \"deepseek/r1-hidden\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPlanSummary: lightModelId,\n\t\t\t\t\tBuilder: shared.ModelRoleConfigSchema{\n\t\t\t\t\t\tModelId: \"deepseek/r1-hidden\",\n\t\t\t\t\t\tStrongModel: &shared.ModelRoleConfigSchema{\n\t\t\t\t\t\t\tModelId: \"openai/o3-medium\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tWholeFileBuilder: shared.ModelRoleConfigSchema{\n\t\t\t\t\t\tModelId: \"deepseek/r1-hidden\",\n\t\t\t\t\t\tLargeContextFallback: &shared.ModelRoleConfigSchema{\n\t\t\t\t\t\t\tModelId: \"google/gemini-2.5-pro\",\n\t\t\t\t\t\t\tLargeOutputFallback: &shared.ModelRoleConfigSchema{\n\t\t\t\t\t\t\t\tModelId: \"openai/o3-low\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tLargeOutputFallback: &shared.ModelRoleConfigSchema{\n\t\t\t\t\t\t\tModelId: \"openai/o3-low\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tExecStatus: \"deepseek/r1-hidden\",\n\t\t\t\t\tNamer:      lightModelId,\n\t\t\t\t\tCommitMsg:  lightModelId,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/new.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar name string\nvar contextBaseDir string\n\n// newCmd represents the new command\nvar newCmd = &cobra.Command{\n\tUse:     \"new\",\n\tAliases: []string{\"n\"},\n\tShort:   \"Start a new plan\",\n\t// Long:  ``,\n\tArgs: cobra.ExactArgs(0),\n\tRun:  new,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(newCmd)\n\tnewCmd.Flags().StringVarP(&name, \"name\", \"n\", \"\", \"Name of the new plan\")\n\tnewCmd.Flags().StringVar(&contextBaseDir, \"context-dir\", \".\", \"Base directory to auto-load context from\")\n\n\tAddNewPlanFlags(newCmd)\n}\n\nfunc new(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveOrCreateProject()\n\n\tterm.StartSpinner(\"\")\n\n\terrCh := make(chan error, 2)\n\n\tvar planId string\n\tvar config *shared.PlanConfig\n\n\tgo func() {\n\t\tres, apiErr := api.Client.CreatePlan(lib.CurrentProjectId, shared.CreatePlanRequest{Name: name})\n\t\tif apiErr != nil {\n\t\t\terrCh <- fmt.Errorf(\"error creating plan: %v\", apiErr.Msg)\n\t\t\treturn\n\t\t}\n\t\tplanId = res.Id\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tvar apiErr *shared.ApiError\n\t\tconfig, apiErr = api.Client.GetDefaultPlanConfig()\n\t\tif apiErr != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting plan config: %v\", apiErr.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error: %v\", err)\n\t\t}\n\t}\n\n\terr := lib.WriteCurrentPlan(planId)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error setting current plan: %v\", err)\n\t}\n\n\terr = lib.WriteCurrentBranch(\"main\")\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error setting current branch: %v\", err)\n\t}\n\n\tif name == \"\" {\n\t\tname = \"draft\"\n\t}\n\n\tterm.StopSpinner()\n\n\tfmt.Printf(\"✅ Started new plan %s and set it to current plan\\n\", color.New(color.Bold, term.ColorHiGreen).Sprint(name))\n\tfmt.Printf(\"⚙️  Using default config\\n\")\n\n\tresolveAutoMode(config)\n\n\tresolveModelPack()\n\n\t// autoModeLabel := shared.ConfigSettingsByKey[\"automode\"].KeyToLabel(string(config.AutoMode))\n\t// fmt.Println(\"⚡️ Auto-mode:\", autoModeLabel)\n\n\tif config.AutoLoadContext {\n\t\tfmt.Println(\"📥 Automatic context loading is enabled\")\n\n\t\tbaseDir := contextBaseDir\n\t\tif baseDir == \"\" {\n\t\t\tbaseDir = \".\"\n\t\t}\n\n\t\tlib.MustLoadContext([]string{baseDir}, &types.LoadContextParams{\n\t\t\tDefsOnly:          true,\n\t\t\tSkipIgnoreWarning: true,\n\t\t\tAutoLoaded:        true,\n\t\t})\n\t} else {\n\t\tfmt.Println()\n\t}\n\n\tvar cmds []string\n\tif term.IsRepl {\n\t\tcmds = []string{\"config\", \"plans\", \"cd\", \"models\"}\n\t} else {\n\t\tcmds = []string{\"tell\", \"chat\", \"config\"}\n\t}\n\n\tif !config.AutoLoadContext {\n\t\tcmds = append([]string{\"load\"}, cmds...)\n\t}\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", cmds...)\n}\n"
  },
  {
    "path": "app/cli/cmd/plan_exec_helpers.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tEditorTypeVim  string = \"vim\"\n\tEditorTypeNano string = \"nano\"\n)\n\nvar defaultEditor = EditorTypeVim\n\nconst defaultAutoDebugTries = 5\n\nvar autoConfirm bool\n\nvar tellPromptFile string\nvar tellBg bool\nvar tellStop bool\nvar tellNoBuild bool\nvar tellAutoApply bool\nvar tellAutoContext bool\nvar tellSmartContext bool\nvar tellSkipMenu bool\nvar noExec bool\nvar autoDebug int\n\nvar editor = EditorTypeVim // default to vim\nvar editorSetByFlag bool\n\nfunc init() {\n\tenvEditor := os.Getenv(\"EDITOR\")\n\tif envEditor == \"\" {\n\t\tenvEditor = os.Getenv(\"VISUAL\")\n\t}\n\n\tif envEditor != \"\" {\n\t\tdefaultEditor = envEditor\n\t}\n}\n\ntype initExecFlagsParams struct {\n\tomitFile         bool\n\tomitNoBuild      bool\n\tomitEditor       bool\n\tomitStop         bool\n\tomitBg           bool\n\tomitApply        bool\n\tomitExec         bool\n\tomitAutoContext  bool\n\tomitSmartContext bool\n\tomitSkipMenu     bool\n}\n\nfunc initExecFlags(cmd *cobra.Command, params initExecFlagsParams) {\n\tif !params.omitFile {\n\t\tcmd.Flags().StringVarP(&tellPromptFile, \"file\", \"f\", \"\", \"File containing prompt\")\n\t}\n\n\tif !params.omitBg {\n\t\tcmd.Flags().BoolVar(&tellBg, \"bg\", false, \"Execute autonomously in the background\")\n\t}\n\n\tif !params.omitStop {\n\t\tcmd.Flags().BoolVarP(&tellStop, \"stop\", \"s\", false, \"Stop after a single reply\")\n\t}\n\n\tif !params.omitNoBuild {\n\t\tcmd.Flags().BoolVarP(&tellNoBuild, \"no-build\", \"n\", false, \"Don't build files\")\n\t}\n\n\tcmd.Flags().BoolVar(&autoConfirm, \"auto-update-context\", false, shared.ConfigSettingsByKey[\"auto-update-context\"].Desc)\n\n\tif !params.omitAutoContext {\n\t\tcmd.Flags().BoolVar(&tellAutoContext, \"auto-load-context\", false, shared.ConfigSettingsByKey[\"auto-load-context\"].Desc)\n\t}\n\n\tif !params.omitSmartContext {\n\t\tcmd.Flags().BoolVar(&tellSmartContext, \"smart-context\", false, shared.ConfigSettingsByKey[\"smart-context\"].Desc)\n\t}\n\n\tif !params.omitApply {\n\t\tcmd.Flags().BoolVar(&tellAutoApply, \"apply\", false, \"Automatically apply changes\")\n\t\tinitApplyFlags(cmd, true)\n\t}\n\n\tif !params.omitExec {\n\t\tinitExecScriptFlags(cmd)\n\t}\n\n\tif !params.omitEditor {\n\t\tcmd.Flags().Var(newEditorValue(&editor), \"editor\", \"Write prompt in system editor\")\n\t\tcmd.Flag(\"editor\").NoOptDefVal = defaultEditor\n\t}\n\n\tif !params.omitSkipMenu {\n\t\tcmd.Flags().BoolVar(&tellSkipMenu, \"skip-menu\", false, shared.ConfigSettingsByKey[\"skip-changes-menu\"].Desc)\n\t}\n}\n\nfunc initApplyFlags(cmd *cobra.Command, applyFlag bool) {\n\tcommitDesc := \"Commit changes to git\"\n\tif applyFlag {\n\t\tcommitDesc += \" when --apply is passed\"\n\t}\n\n\tskipCommitDesc := \"Skip committing changes to git\"\n\tif applyFlag {\n\t\tskipCommitDesc += \" when --apply is passed\"\n\t}\n\tcmd.Flags().BoolVarP(&autoCommit, \"commit\", \"c\", false, commitDesc)\n\tcmd.Flags().BoolVar(&skipCommit, \"skip-commit\", false, skipCommitDesc)\n}\n\nfunc initExecScriptFlags(cmd *cobra.Command) {\n\tcmd.Flags().BoolVar(&noExec, \"no-exec\", false, \"Disable command execution\")\n\tcmd.Flags().BoolVar(&autoExec, \"auto-exec\", false, \"Automatically execute commands without confirmation\")\n\tcmd.Flags().Var(newAutoDebugValue(&autoDebug), \"debug\", \"Automatically execute and debug failing commands (optionally specify number of tries—default is 5)\")\n\tcmd.Flag(\"debug\").NoOptDefVal = strconv.Itoa(defaultAutoDebugTries)\n}\n\nfunc validatePlanExecFlags(isApply bool) {\n\tif autoDebug > 0 && noExec {\n\t\tterm.OutputErrorAndExit(\"--debug can't be used with --no-exec\")\n\t}\n\n\tif tellAutoContext && tellBg {\n\t\tterm.OutputErrorAndExit(\"--auto-context/-c can't be used with --bg\")\n\t}\n\n\tif !isApply {\n\t\tif autoDebug > 0 && !tellAutoApply {\n\t\t\tterm.OutputErrorAndExit(\"--debug can only be used with --apply\")\n\t\t}\n\n\t\tif autoExec && !tellAutoApply {\n\t\t\tterm.OutputErrorAndExit(\"--auto-exec can only be used with --apply\")\n\t\t}\n\n\t\tif tellAutoApply && tellNoBuild {\n\t\t\tterm.OutputErrorAndExit(\"--apply can't be used with --no-build/-n\")\n\t\t}\n\t\tif tellAutoApply && tellBg {\n\t\t\tterm.OutputErrorAndExit(\"--apply can't be used with --bg\")\n\t\t}\n\t}\n}\n\nfunc mustSetPlanExecFlags(cmd *cobra.Command, isApply bool) {\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tconfig := lib.MustGetCurrentPlanConfig()\n\n\t// Set flag vars from config when flags aren't explicitly set\n\tif !cmd.Flags().Changed(\"stop\") {\n\t\ttellStop = !config.AutoContinue\n\t}\n\n\tif !cmd.Flags().Changed(\"no-build\") {\n\t\ttellNoBuild = !config.AutoBuild\n\t}\n\n\tif !cmd.Flags().Changed(\"auto-update-context\") {\n\t\tautoConfirm = config.AutoUpdateContext\n\t}\n\tif !cmd.Flags().Changed(\"apply\") {\n\t\ttellAutoApply = config.AutoApply\n\t}\n\tif !cmd.Flags().Changed(\"skip-commit\") {\n\t\tskipCommit = config.SkipCommit\n\t}\n\tif !cmd.Flags().Changed(\"commit\") {\n\t\tautoCommit = config.AutoCommit\n\t}\n\tif !cmd.Flags().Changed(\"auto-load-context\") {\n\t\ttellAutoContext = config.AutoLoadContext\n\t}\n\tif !cmd.Flags().Changed(\"smart-context\") {\n\t\ttellSmartContext = config.SmartContext\n\t}\n\tif !cmd.Flags().Changed(\"no-exec\") {\n\t\tnoExec = !config.CanExec\n\t}\n\tif !cmd.Flags().Changed(\"auto-exec\") {\n\t\tautoExec = config.AutoExec\n\t}\n\tif !cmd.Flags().Changed(\"debug\") {\n\t\tautoDebug = config.AutoDebugTries\n\t\t// Only set autoDebug if AutoDebug is enabled in config\n\t\tif !config.AutoDebug {\n\t\t\tautoDebug = 0\n\t\t}\n\t}\n\n\tif !cmd.Flags().Changed(\"skip-menu\") {\n\t\ttellSkipMenu = config.SkipChangesMenu\n\t}\n\n\t// tell command editor is no longer tied to config *unless* it's set to vim or nano\n\t// otherwise, the flag or EDITOR env var are used\n\t// config.Editor is now used for mainly for JSON editing (and perhaps other purposes)\n\t// this is because it's pretty rare to use the editor for writing prompts now rather than the REPL\n\tif !editorSetByFlag && (config.Editor == shared.EditorTypeVim || config.Editor == shared.EditorTypeNano) {\n\t\teditor = config.Editor\n\t}\n\n\tvalidatePlanExecFlags(isApply)\n}\n\n// AutoDebugValue implements the flag.Value interface\ntype autoDebugValue struct {\n\tvalue *int\n}\n\nfunc newAutoDebugValue(p *int) *autoDebugValue {\n\t*p = 0 // Default to 0 (disabled)\n\treturn &autoDebugValue{p}\n}\n\nfunc (f *autoDebugValue) Set(s string) error {\n\tif s == \"\" {\n\t\t*f.value = defaultAutoDebugTries\n\t\treturn nil\n\t}\n\tv, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid value for --debug: %v\", err)\n\t}\n\tif v <= 0 {\n\t\treturn fmt.Errorf(\"--debug value must be greater than 0\")\n\t}\n\t*f.value = v\n\treturn nil\n}\n\nfunc (f *autoDebugValue) String() string {\n\tif f.value == nil {\n\t\treturn \"0\"\n\t}\n\treturn strconv.Itoa(*f.value)\n}\n\nfunc (f *autoDebugValue) Type() string {\n\treturn \"int\"\n}\n\n// EditorValue implements the flag.Value interface\ntype editorValue struct {\n\tvalue *string\n}\n\nfunc newEditorValue(p *string) *editorValue {\n\t*p = defaultEditor\n\treturn &editorValue{p}\n}\n\nfunc (f *editorValue) Set(s string) error {\n\tif s == \"\" {\n\t\t*f.value = defaultEditor\n\t\treturn nil\n\t}\n\t*f.value = s\n\teditorSetByFlag = true\n\treturn nil\n}\n\nfunc (f *editorValue) String() string {\n\tif f.value == nil {\n\t\treturn \"\"\n\t}\n\treturn *f.value\n}\n\nfunc (f *editorValue) Type() string {\n\treturn \"string\"\n}\n"
  },
  {
    "path": "app/cli/cmd/plan_start_helpers.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar (\n\t// Tier flags\n\tnoAuto    bool\n\tbasicAuto bool\n\tplusAuto  bool\n\tsemiAuto  bool\n\tfullAuto  bool\n\n\t// Type flags\n\tdailyModels             bool\n\treasoningModels         bool\n\tstrongModels            bool\n\tossModels               bool\n\tcheapModels             bool\n\tgeminiPlannerModels     bool\n\to3PlannerModels         bool\n\tr1PlannerModels         bool\n\tperplexityPlannerModels bool\n\topusPlannerModels       bool\n)\n\nfunc AddNewPlanFlags(cmd *cobra.Command) {\n\t// Add tier flags\n\tcmd.Flags().BoolVar(&noAuto, \"no-auto\", false, shared.AutoModeDescriptions[shared.AutoModeNone])\n\tcmd.Flags().BoolVar(&basicAuto, \"basic\", false, shared.AutoModeDescriptions[shared.AutoModeBasic])\n\tcmd.Flags().BoolVar(&plusAuto, \"plus\", false, shared.AutoModeDescriptions[shared.AutoModePlus])\n\tcmd.Flags().BoolVar(&semiAuto, \"semi\", false, shared.AutoModeDescriptions[shared.AutoModeSemi])\n\tcmd.Flags().BoolVar(&fullAuto, \"full\", false, shared.AutoModeDescriptions[shared.AutoModeFull])\n\n\t// Add type flags\n\tcmd.Flags().BoolVar(&dailyModels, \"daily\", false, shared.DailyDriverModelPack.Description)\n\tcmd.Flags().BoolVar(&reasoningModels, \"reasoning\", false, shared.ReasoningModelPack.Description)\n\tcmd.Flags().BoolVar(&strongModels, \"strong\", false, shared.StrongModelPack.Description)\n\tcmd.Flags().BoolVar(&cheapModels, \"cheap\", false, shared.CheapModelPack.Description)\n\tcmd.Flags().BoolVar(&ossModels, \"oss\", false, shared.OSSModelPack.Description)\n\n\tcmd.Flags().BoolVar(&geminiPlannerModels, \"gemini-planner\", false, shared.GeminiPlannerModelPack.Description)\n\tcmd.Flags().BoolVar(&o3PlannerModels, \"o3-planner\", false, shared.O3PlannerModelPack.Description)\n\tcmd.Flags().BoolVar(&r1PlannerModels, \"r1-planner\", false, shared.R1PlannerModelPack.Description)\n\tcmd.Flags().BoolVar(&perplexityPlannerModels, \"perplexity-planner\", false, shared.PerplexityPlannerModelPack.Description)\n\tcmd.Flags().BoolVar(&opusPlannerModels, \"opus-planner\", false, shared.OpusPlannerModelPack.Description)\n}\n\nfunc resolveAutoMode(config *shared.PlanConfig) (bool, *shared.PlanConfig) {\n\tdidUpdate, updatedConfig, _ := resolveAutoModeWithArgs(config, false)\n\treturn didUpdate, updatedConfig\n}\n\nfunc resolveAutoModeSilent(config *shared.PlanConfig) (bool, *shared.PlanConfig, func()) {\n\treturn resolveAutoModeWithArgs(config, true)\n}\n\nfunc resolveAutoModeWithArgs(config *shared.PlanConfig, silent bool) (bool, *shared.PlanConfig, func()) {\n\tcurrentAutoMode := config.AutoMode\n\tvar toSetAutoMode shared.AutoModeType\n\tif noAuto {\n\t\ttoSetAutoMode = shared.AutoModeNone\n\t} else if basicAuto {\n\t\ttoSetAutoMode = shared.AutoModeBasic\n\t} else if plusAuto {\n\t\ttoSetAutoMode = shared.AutoModePlus\n\t} else if semiAuto {\n\t\ttoSetAutoMode = shared.AutoModeSemi\n\t} else if fullAuto {\n\t\ttoSetAutoMode = shared.AutoModeFull\n\t}\n\n\tif toSetAutoMode != \"\" && toSetAutoMode != currentAutoMode {\n\t\tif !silent {\n\t\t\tterm.StartSpinner(\"\")\n\t\t}\n\t\t_, updatedConfig := updateConfig([]string{\"auto-mode\", string(toSetAutoMode)}, config)\n\t\tapiErr := api.Client.UpdatePlanConfig(lib.CurrentPlanId, shared.UpdatePlanConfigRequest{\n\t\t\tConfig: updatedConfig,\n\t\t})\n\t\tlib.SetCachedPlanConfig(updatedConfig)\n\t\tif !silent {\n\t\t\tterm.StopSpinner()\n\t\t}\n\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error updating config auto-mode: %v\", apiErr)\n\t\t}\n\n\t\tfn := func() {\n\t\t\tprintAutoModeTable(config)\n\t\t}\n\n\t\tif !silent {\n\t\t\tfn()\n\t\t\treturn true, updatedConfig, fn\n\t\t}\n\n\t\treturn true, updatedConfig, fn\n\t}\n\n\treturn false, config, nil\n}\n\nfunc resolveModelPack() {\n\tresolveModelPackWithArgs(nil, false)\n}\n\nfunc resolveModelPackSilent(settings *shared.PlanSettings) (*shared.PlanSettings, func()) {\n\treturn resolveModelPackWithArgs(settings, true)\n}\n\nfunc resolveModelPackWithArgs(settings *shared.PlanSettings, silent bool) (*shared.PlanSettings, func()) {\n\n\tvar originalSettings *shared.PlanSettings\n\tvar apiErr *shared.ApiError\n\tif settings == nil {\n\t\tif !silent {\n\t\t\tterm.StartSpinner(\"\")\n\t\t}\n\t\toriginalSettings, apiErr = api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)\n\t} else {\n\t\toriginalSettings = settings\n\t}\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current settings: %v\", apiErr)\n\t\treturn nil, nil\n\t}\n\n\tvar packName string\n\n\tif ossModels {\n\t\tpackName = shared.OSSModelPack.Name\n\t} else if strongModels {\n\t\tpackName = shared.StrongModelPack.Name\n\t} else if cheapModels {\n\t\tpackName = shared.CheapModelPack.Name\n\t} else if reasoningModels {\n\t\tpackName = shared.ReasoningModelPack.Name\n\t} else if dailyModels {\n\t\tpackName = shared.DailyDriverModelPack.Name\n\t} else if geminiPlannerModels {\n\t\tpackName = shared.GeminiPlannerModelPack.Name\n\t} else if o3PlannerModels {\n\t\tpackName = shared.O3PlannerModelPack.Name\n\t} else if r1PlannerModels {\n\t\tpackName = shared.R1PlannerModelPack.Name\n\t} else if perplexityPlannerModels {\n\t\tpackName = shared.PerplexityPlannerModelPack.Name\n\t} else if opusPlannerModels {\n\t\tpackName = shared.OpusPlannerModelPack.Name\n\t}\n\n\tif packName != \"\" && packName != originalSettings.GetModelPack().Name {\n\t\tif !silent {\n\t\t\tterm.StartSpinner(\"\")\n\t\t}\n\t\tupdatedSettings := updateModelSettings([]string{packName}, originalSettings, \"\")\n\t\t_, apiErr = api.Client.UpdateSettings(lib.CurrentPlanId, lib.CurrentBranch, shared.UpdateSettingsRequest{\n\t\t\tModelPackName: updatedSettings.ModelPackName,\n\t\t\tModelPack:     updatedSettings.ModelPack,\n\t\t})\n\t\tif !silent {\n\t\t\tterm.StopSpinner()\n\t\t}\n\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error setting model pack: %v\", apiErr)\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tfn := func() {\n\t\t\tprintModelPackTable(packName)\n\t\t}\n\n\t\tif !silent {\n\t\t\tfn()\n\t\t\treturn updatedSettings, fn\n\t\t}\n\n\t\treturn updatedSettings, fn\n\t} else {\n\t\tif !silent {\n\t\t\tterm.StopSpinner()\n\t\t}\n\t\tfn := func() {\n\t\t\tprintModelPackTable(originalSettings.GetModelPack().Name)\n\t\t}\n\n\t\tif !silent {\n\t\t\tfn()\n\t\t\treturn originalSettings, fn\n\t\t}\n\n\t\treturn originalSettings, fn\n\t}\n}\n\nfunc printModelPackTable(packName string) {\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetHeader([]string{\"🧠 Model Pack\"})\n\ttable.Append([]string{color.New(color.Bold, term.ColorHiMagenta).Sprint(packName)})\n\ttable.Render()\n}\n\nfunc printAutoModeTable(config *shared.PlanConfig) {\n\tvar contextMode string\n\tif config.AutoLoadContext {\n\t\tcontextMode = \"auto\"\n\t} else {\n\t\tcontextMode = \"manual\"\n\t}\n\n\tvar applyMode string\n\tif config.AutoApply {\n\t\tapplyMode = \"auto\"\n\t} else {\n\t\tapplyMode = \"approve\"\n\t}\n\n\tvar executionMode string\n\tif config.AutoExec {\n\t\texecutionMode = \"auto\"\n\t} else if config.CanExec {\n\t\texecutionMode = \"approve\"\n\t} else {\n\t\texecutionMode = \"disabled\"\n\t}\n\n\tvar commitMode string\n\tif config.AutoCommit {\n\t\tcommitMode = \"auto\"\n\t} else if config.SkipCommit {\n\t\tcommitMode = \"skip\"\n\t} else {\n\t\tcommitMode = \"manual\"\n\t}\n\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(false)\n\ttable.SetHeader([]string{\n\t\t\"🚀 Auto Mode\",\n\t\t\"Context\",\n\t\t\"Apply\",\n\t\t\"Execution\",\n\t\t\"Commits\",\n\t})\n\ttable.Append([]string{\n\t\tcolor.New(color.Bold, term.ColorHiMagenta).Sprint(config.AutoMode),\n\t\tcontextMode,\n\t\tapplyMode,\n\t\texecutionMode,\n\t\tcommitMode,\n\t})\n\ttable.Render()\n\n}\n"
  },
  {
    "path": "app/cli/cmd/plans.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/format\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/xlab/treeprint\"\n)\n\nvar archivedOnly bool\n\nfunc init() {\n\tRootCmd.AddCommand(plansCmd)\n\tplansCmd.Flags().BoolVarP(&archivedOnly, \"archived\", \"a\", false, \"List archived plans\")\n}\n\n// plansCmd represents the list command\nvar plansCmd = &cobra.Command{\n\tUse:     \"plans\",\n\tAliases: []string{\"pl\"},\n\tShort:   \"List plans\",\n\tRun:     plans,\n}\n\nfunc plans(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MaybeResolveProject()\n\n\tif archivedOnly {\n\t\tlistArchived()\n\t} else {\n\t\tlistActive()\n\t}\n}\n\nfunc listActive() {\n\terrCh := make(chan error)\n\n\tvar parentProjectIdsWithPaths [][2]string\n\tvar childProjectIdsWithPaths [][2]string\n\n\tgo func() {\n\t\tres, err := fs.GetParentProjectIdsWithPaths(auth.Current.UserId)\n\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting parent project ids with paths: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tparentProjectIdsWithPaths = res\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\t\tdefer cancel()\n\n\t\tres, err := fs.GetChildProjectIdsWithPaths(ctx, auth.Current.UserId)\n\n\t\tif err != nil {\n\t\t\tlog.Println(err.Error())\n\n\t\t\tif err.Error() == \"context timeout\" {\n\t\t\t\terrCh <- nil\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- fmt.Errorf(\"error getting child project ids with paths: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tchildProjectIdsWithPaths = res\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"%v\", err)\n\t\t}\n\t}\n\n\tvar projectIds []string\n\n\tif lib.CurrentProjectId != \"\" {\n\t\tprojectIds = append(projectIds, lib.CurrentProjectId)\n\t}\n\n\tfor _, p := range parentProjectIdsWithPaths {\n\t\tprojectIds = append(projectIds, p[1])\n\t}\n\tfor _, p := range childProjectIdsWithPaths {\n\t\tprojectIds = append(projectIds, p[1])\n\t}\n\n\tif len(projectIds) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No plans\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"new\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tplans, apiErr := api.Client.ListPlans(projectIds)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plans: %v\", apiErr)\n\t}\n\n\tif len(plans) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No plans\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"new\")\n\t\treturn\n\t}\n\n\tplansByProjectId := make(map[string][]*shared.Plan)\n\tvar currentProjectPlanIds []string\n\tfor _, p := range plans {\n\t\tplansByProjectId[p.ProjectId] = append(plansByProjectId[p.ProjectId], p)\n\t\tif p.ProjectId == lib.CurrentProjectId {\n\t\t\tcurrentProjectPlanIds = append(currentProjectPlanIds, p.Id)\n\t\t}\n\t}\n\n\tfor projectId, plans := range plansByProjectId {\n\t\tif projectId != lib.CurrentProjectId {\n\t\t\t// sort non-current-project plans alphabetically\n\t\t\tsort.Slice(plans, func(i, j int) bool {\n\t\t\t\treturn plans[i].Name < plans[j].Name\n\t\t\t})\n\t\t}\n\t}\n\n\t// remove paths with no plans from parentProjectIdsWithPaths and childProjectIdsWithPaths\n\tvar parentProjectIdsWithPathsFiltered [][2]string\n\tfor _, p := range parentProjectIdsWithPaths {\n\t\tif len(plansByProjectId[p[1]]) > 0 {\n\t\t\tparentProjectIdsWithPathsFiltered = append(parentProjectIdsWithPathsFiltered, p)\n\t\t}\n\t}\n\tparentProjectIdsWithPaths = parentProjectIdsWithPathsFiltered\n\n\tvar childProjectIdsWithPathsFiltered [][2]string\n\tfor _, p := range childProjectIdsWithPaths {\n\t\tif len(plansByProjectId[p[1]]) > 0 {\n\t\t\tchildProjectIdsWithPathsFiltered = append(childProjectIdsWithPathsFiltered, p)\n\t\t}\n\t}\n\tchildProjectIdsWithPaths = childProjectIdsWithPathsFiltered\n\n\tvar b strings.Builder\n\n\tif len(currentProjectPlanIds) > 0 {\n\t\tcurrentBranchNamesByPlanId, err := lib.GetCurrentBranchNamesByPlanId(currentProjectPlanIds)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error getting current branches: %v\", err)\n\t\t}\n\n\t\tcurrentBranchesByPlanId, apiErr := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{\n\t\t\tCurrentBranchByPlanId: currentBranchNamesByPlanId,\n\t\t})\n\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error getting current branches: %v\", apiErr)\n\t\t}\n\n\t\ttable := tablewriter.NewWriter(&b)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"#\", \"Name\", \"Updated\" /*, \"Created\" /*\"Branches\",*/, \"Branch\", \"Context\", \"Convo\"})\n\n\t\tcurrentProjectPlans := plansByProjectId[lib.CurrentProjectId]\n\t\tif len(parentProjectIdsWithPaths) > 0 || len(childProjectIdsWithPaths) > 0 {\n\t\t\tb.WriteString(color.New(color.Bold, term.ColorHiGreen).Sprint(\"Plans in current directory\\n\"))\n\t\t} else {\n\t\t\tb.WriteString(\"\\n\")\n\t\t}\n\t\tfor i, p := range currentProjectPlans {\n\t\t\tnum := strconv.Itoa(i + 1)\n\t\t\tif p.Id == lib.CurrentPlanId {\n\t\t\t\tnum = color.New(color.Bold, term.ColorHiGreen).Sprint(num)\n\t\t\t}\n\n\t\t\tvar name string\n\t\t\tif p.Id == lib.CurrentPlanId {\n\t\t\t\tname = color.New(color.Bold, term.ColorHiGreen).Sprint(p.Name) + fmt.Sprint(\" 👈\")\n\t\t\t} else {\n\t\t\t\tname = p.Name\n\t\t\t}\n\n\t\t\tcurrentBranch := currentBranchesByPlanId[p.Id]\n\n\t\t\trow := []string{\n\t\t\t\tnum,\n\t\t\t\tname,\n\t\t\t\tformat.Time(p.UpdatedAt),\n\t\t\t\t// format.Time(p.CreatedAt),\n\t\t\t\t// strconv.Itoa(p.ActiveBranches),\n\t\t\t\tcurrentBranch.Name,\n\t\t\t\tstrconv.Itoa(currentBranch.ContextTokens) + \" 🪙\",\n\t\t\t\tstrconv.Itoa(currentBranch.ConvoTokens) + \" 🪙\",\n\t\t\t}\n\n\t\t\tvar style []tablewriter.Colors\n\t\t\tif p.Name == lib.CurrentPlanId {\n\t\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t\t{tablewriter.FgHiGreenColor, tablewriter.Bold},\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t\t{tablewriter.Bold},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttable.Rich(row, style)\n\n\t\t}\n\t\ttable.Render()\n\n\t} else {\n\t\tb.WriteString(\"🤷‍♂️ No plans in current directory\\n\")\n\t}\n\n\tvar addPathToTreeFn func(tree treeprint.Tree, basePath, localPath, projectId string, isParent bool)\n\taddPathToTreeFn = func(tree treeprint.Tree, basePath, localPath, projectId string, isParent bool) {\n\t\tvar base string\n\t\tvar tail string\n\t\tsplit := strings.Split(localPath, string(os.PathSeparator))\n\n\t\tvar baseBranch treeprint.Tree\n\t\tfor _, part := range split {\n\t\t\tbase = filepath.Join(base, part)\n\t\t\ttail = strings.TrimPrefix(localPath, base+string(os.PathSeparator))\n\n\t\t\tvar searchBranch string\n\t\t\tif isParent {\n\t\t\t\tbaseFull := filepath.Join(fs.HomeDir, basePath, base)\n\t\t\t\tbaseRel, _ := filepath.Rel(fs.Cwd, baseFull)\n\t\t\t\tsearchBranch = fmt.Sprintf(\"%s (%s)\", base, baseRel)\n\n\t\t\t\t// \tlog.Println(\"Project root:\", fs.Cwd)\n\t\t\t\t// \tlog.Println(\"searchBranch:\", searchBranch)\n\t\t\t\t// \tlog.Println(\"base:\", base)\n\t\t\t\t// \tlog.Println(\"tail:\", tail)\n\t\t\t\t// \tlog.Println(\"basePath:\", basePath)\n\t\t\t\t// \tlog.Println(\"baseFull:\", baseFull)\n\t\t\t\t// \tlog.Println(\"baseRel:\", baseRel)\n\t\t\t} else {\n\t\t\t\tsearchBranch = base\n\t\t\t}\n\n\t\t\tbaseBranch = tree.FindByValue(searchBranch)\n\t\t\tif baseBranch != nil {\n\t\t\t\taddPathToTreeFn(baseBranch, filepath.Join(basePath, base), tail, projectId, isParent)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif baseBranch == nil {\n\t\t\tlabel := localPath\n\t\t\tif isParent {\n\t\t\t\tpathFull := filepath.Join(fs.HomeDir, basePath, localPath)\n\t\t\t\tpathRel, _ := filepath.Rel(fs.Cwd, pathFull)\n\t\t\t\tlabel = fmt.Sprintf(\"%s (%s)\", localPath, pathRel)\n\n\t\t\t}\n\n\t\t\tbranch := tree.AddBranch(label)\n\t\t\tplans := plansByProjectId[projectId]\n\n\t\t\tfor _, p := range plans {\n\t\t\t\tbranch.AddNode(color.New(term.ColorHiCyan).Sprint(p.Name))\n\t\t\t}\n\t\t}\n\t}\n\n\tvar c color.Attribute\n\tif term.IsDarkBg {\n\t\tc = color.FgWhite\n\t} else {\n\t\tc = color.FgBlack\n\t}\n\n\tif len(parentProjectIdsWithPaths) > 0 {\n\t\tb.WriteString(\"\\n\")\n\n\t\tb.WriteString(color.New(color.Bold).Sprint(\"Plans in parent directories\\n\"))\n\t\tb.WriteString(color.New(c).Sprint(\"cd into a directory to work on a plan in that directory\\n\"))\n\t\tparentTree := treeprint.NewWithRoot(\"~\")\n\n\t\tfor i := len(parentProjectIdsWithPaths) - 1; i >= 0; i-- {\n\t\t\tp := parentProjectIdsWithPaths[i]\n\n\t\t\trel, err := filepath.Rel(fs.HomeDir, p[0])\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting relative path: %v\", err)\n\t\t\t}\n\n\t\t\taddPathToTreeFn(parentTree, \"\", rel, p[1], true)\n\t\t}\n\t\tb.WriteString(parentTree.String())\n\t}\n\n\tif len(childProjectIdsWithPaths) > 0 {\n\t\tb.WriteString(\"\\n\")\n\t\tb.WriteString(color.New(color.Bold).Sprint(\"Plans in child directories\\n\"))\n\t\tb.WriteString(color.New(c).Sprint(\"cd into a directory to work on a plan in that directory\\n\"))\n\t\tchildTree := treeprint.New()\n\t\tfor _, p := range childProjectIdsWithPaths {\n\t\t\trel, err := filepath.Rel(fs.Cwd, p[0])\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting relative path: %v\", err)\n\t\t\t}\n\n\t\t\taddPathToTreeFn(childTree, \"\", rel, p[1], false)\n\t\t}\n\t\tb.WriteString(childTree.String())\n\t} else {\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\tterm.PageOutput(b.String())\n\n\tfmt.Println()\n\tif len(currentProjectPlanIds) > 0 {\n\t\tterm.PrintCmds(\"\", \"new\", \"cd\", \"delete-plan\", \"plans --archived\", \"archive\")\n\t} else {\n\t\tterm.PrintCmds(\"\", \"new\", \"plans --archived\")\n\t}\n}\n\nfunc listArchived() {\n\tvar projectIds []string\n\n\tif lib.CurrentProjectId != \"\" {\n\t\tprojectIds = append(projectIds, lib.CurrentProjectId)\n\t}\n\n\tterm.StartSpinner(\"\")\n\tplans, apiErr := api.Client.ListArchivedPlans(projectIds)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plans: %v\", apiErr)\n\t}\n\n\tif len(plans) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No archived plans\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"archive\")\n\t\treturn\n\t}\n\n\tvar b strings.Builder\n\ttable := tablewriter.NewWriter(&b)\n\ttable.SetAutoWrapText(false)\n\ttable.SetHeader([]string{\"#\", \"Name\", \"Updated\"})\n\n\tfor i, p := range plans {\n\t\tnum := strconv.Itoa(i + 1)\n\t\tif p.Id == lib.CurrentPlanId {\n\t\t\tnum = color.New(color.Bold, term.ColorHiGreen).Sprint(num)\n\t\t}\n\n\t\trow := []string{\n\t\t\tnum,\n\t\t\tp.Name,\n\t\t\tformat.Time(p.UpdatedAt),\n\t\t}\n\n\t\tvar style []tablewriter.Colors\n\t\tif p.Name == lib.CurrentPlanId {\n\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t{tablewriter.FgHiGreenColor, tablewriter.Bold},\n\t\t\t}\n\t\t} else {\n\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t{tablewriter.Bold},\n\t\t\t}\n\t\t}\n\n\t\ttable.Rich(row, style)\n\n\t}\n\ttable.Render()\n\n\tterm.PageOutput(b.String())\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"unarchive\")\n}\n"
  },
  {
    "path": "app/cli/cmd/ps.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/format\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar psCmd = &cobra.Command{\n\tUse:   \"ps\",\n\tShort: \"List plans with active or recently finished streams\",\n\tRun:   ps,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(psCmd)\n}\n\nfunc ps(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\tres, apiErr := api.Client.ListPlansRunning([]string{lib.CurrentProjectId}, true)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting running plans: %v\", apiErr)\n\t\treturn\n\t}\n\n\tif len(res.Branches) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No active or recently finished streams\")\n\t\treturn\n\t}\n\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(false)\n\ttable.SetHeader([]string{\"Pid\", \"Plan\", \"Branch\", \"Started\", \"Status\"})\n\n\tfor _, b := range res.Branches {\n\t\tid := res.StreamIdByBranchId[b.Id]\n\t\tplan := res.PlansById[b.PlanId]\n\n\t\tstatus := \"Active\"\n\t\tfinishedAt := res.StreamFinishedAtByBranchId[b.Id]\n\t\tswitch b.Status {\n\t\tcase shared.PlanStatusFinished:\n\t\t\tstatus = \"Finished \" + format.Time(finishedAt)\n\t\tcase shared.PlanStatusError:\n\t\t\tstatus = \"Error \" + format.Time(finishedAt)\n\t\tcase shared.PlanStatusStopped:\n\t\t\tstatus = \"Stopped \" + format.Time(finishedAt)\n\t\tcase shared.PlanStatusMissingFile:\n\t\t\tstatus = \"Missing file\"\n\t\t}\n\n\t\trow := []string{\n\t\t\tid[:4],\n\t\t\tplan.Name,\n\t\t\tb.Name,\n\t\t\tformat.Time(res.StreamStartedAtByBranchId[b.Id]),\n\t\t\tstatus,\n\t\t}\n\n\t\tvar style []tablewriter.Colors\n\t\tif b.Name == lib.CurrentPlanId {\n\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t{tablewriter.FgGreenColor, tablewriter.Bold},\n\t\t\t}\n\t\t} else {\n\t\t\tstyle = []tablewriter.Colors{\n\t\t\t\t{tablewriter.Bold},\n\t\t\t}\n\t\t}\n\n\t\ttable.Rich(row, style)\n\n\t}\n\ttable.Render()\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"connect\", \"stop\")\n\n}\n"
  },
  {
    "path": "app/cli/cmd/reject.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"sort\"\n\n\t\"github.com/plandex-ai/survey/v2\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar rejectAll bool\n\nfunc init() {\n\tRootCmd.AddCommand(rejectCmd)\n\n\trejectCmd.Flags().BoolVarP(&rejectAll, \"all\", \"a\", false, \"Reject all pending changes\")\n}\n\nvar rejectCmd = &cobra.Command{\n\tUse:     \"reject [files...]\",\n\tAliases: []string{\"rj\"},\n\tShort:   \"Reject pending changes\",\n\tRun:     reject,\n}\n\nfunc reject(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tcurrentPlanState, apiErr := api.Client.GetCurrentPlanState(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif apiErr != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"Error getting current plan state: %v\", apiErr)\n\t}\n\n\tcurrentFiles := currentPlanState.CurrentPlanFiles.Files\n\n\tif len(currentFiles) == 0 {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"No pending changes to reject\")\n\t}\n\n\tif rejectAll {\n\t\tnumToReject := len(currentFiles)\n\t\tsuffix := \"\"\n\t\tif numToReject > 1 {\n\t\t\tsuffix = \"s\"\n\t\t}\n\n\t\tapiErr := api.Client.RejectAllChanges(lib.CurrentPlanId, lib.CurrentBranch)\n\n\t\tif apiErr != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"Error rejecting all changes: %v\", apiErr)\n\t\t}\n\n\t\tterm.StopSpinner()\n\t\tfmt.Printf(\"✅ Rejected changes to %d file%s\\n\", numToReject, suffix)\n\n\t\tsortedFiles := make([]string, 0, len(currentFiles))\n\t\tfor file := range currentFiles {\n\t\t\tsortedFiles = append(sortedFiles, file)\n\t\t}\n\t\tsort.Strings(sortedFiles)\n\n\t\tfor _, file := range sortedFiles {\n\t\t\tfmt.Printf(\"• 📄 %s\\n\", file)\n\t\t}\n\n\t\treturn\n\t}\n\n\tif len(args) > 0 {\n\t\tfor _, path := range args {\n\t\t\tif _, ok := currentFiles[path]; !ok {\n\t\t\t\tterm.StopSpinner()\n\t\t\t\tterm.OutputErrorAndExit(\"File %s not found in plan or has no changes to reject\", path)\n\t\t\t}\n\t\t}\n\n\t\tnumToReject := len(args)\n\t\tsuffix := \"\"\n\t\tif numToReject > 1 {\n\t\t\tsuffix = \"s\"\n\t\t}\n\n\t\tapiErr = api.Client.RejectFiles(lib.CurrentPlanId, lib.CurrentBranch, args)\n\t\tterm.StopSpinner()\n\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error rejecting changes: %v\", apiErr)\n\t\t}\n\n\t\tfmt.Printf(\"✅ Rejected changes to %d file%s\\n\", numToReject, suffix)\n\n\t\tsortedFiles := append([]string{}, args...)\n\t\tsort.Strings(sortedFiles)\n\n\t\tfor _, file := range sortedFiles {\n\t\t\tfmt.Printf(\"• 📄 %s\\n\", file)\n\t\t}\n\n\t\treturn\n\t}\n\n\t// No args provided - use survey multiselect\n\tterm.StopSpinner()\n\n\tpathsToSort := make([]string, 0, len(currentFiles))\n\tfor path := range currentFiles {\n\t\tpathsToSort = append(pathsToSort, path)\n\t}\n\tsort.Strings(pathsToSort)\n\n\tvar selectedFiles []string\n\tprompt := &survey.MultiSelect{\n\t\tMessage: \"Select files to reject:\",\n\t\tOptions: pathsToSort,\n\t}\n\n\terr := survey.AskOne(prompt, &selectedFiles)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting file selection: %v\", err)\n\t}\n\n\tif len(selectedFiles) == 0 {\n\t\tfmt.Println(\"No files selected\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tapiErr = api.Client.RejectFiles(lib.CurrentPlanId, lib.CurrentBranch, selectedFiles)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error rejecting changes: %v\", apiErr)\n\t}\n\n\tsuffix := \"\"\n\tif len(selectedFiles) > 1 {\n\t\tsuffix = \"s\"\n\t}\n\tfmt.Printf(\"✅ Rejected changes to %d file%s\\n\", len(selectedFiles), suffix)\n\n\tsortedFiles := append([]string{}, selectedFiles...)\n\tsort.Strings(sortedFiles)\n\n\tfor _, file := range sortedFiles {\n\t\tfmt.Printf(\"• 📄 %s\\n\", file)\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/rename.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar renameCmd = &cobra.Command{\n\tUse:   \"rename [new-name]\",\n\tShort: \"Rename the current plan\",\n\tArgs:  cobra.MaximumNArgs(1),\n\tRun:   rename,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(renameCmd)\n}\n\nfunc rename(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tvar newName string\n\tif len(args) > 0 {\n\t\tnewName = args[0]\n\t} else {\n\t\tvar err error\n\t\tnewName, err = term.GetRequiredUserStringInput(\"New name:\")\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error reading new name: %v\", err)\n\t\t}\n\t}\n\n\tif newName == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No new name provided\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\terr := api.Client.RenamePlan(lib.CurrentPlanId, newName)\n\tterm.StopSpinner()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error renaming plan: %v\", err)\n\t}\n\n\tfmt.Printf(\"✅ Plan renamed to %s\\n\", color.New(color.Bold, term.ColorHiGreen).Sprint(newName))\n}\n"
  },
  {
    "path": "app/cli/cmd/repl.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"plandex-cli/version\"\n\tshared \"plandex-shared\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/google/uuid\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/plandex-ai/go-prompt\"\n\tpstrings \"github.com/plandex-ai/go-prompt/strings\"\n\n\t\"github.com/lithammer/fuzzysearch/fuzzy\"\n)\n\nvar replCmd = &cobra.Command{\n\tUse:   \"repl\",\n\tShort: \"Start interactive Plandex REPL\",\n\tRun:   runRepl,\n}\n\nvar cliSuggestions []prompt.Suggest\nvar projectPaths *types.ProjectPaths\nvar currentPrompt *prompt.Prompt\n\nvar replConfig *shared.PlanConfig\n\nvar sessionId string\n\nfunc init() {\n\tRootCmd.AddCommand(replCmd)\n\n\treplCmd.Flags().BoolP(\"chat\", \"c\", false, \"Start in chat mode\")\n\treplCmd.Flags().BoolP(\"tell\", \"t\", false, \"Start in tell mode\")\n\n\tAddNewPlanFlags(replCmd)\n\n\tfor _, config := range term.CliCommands {\n\t\tif config.Repl {\n\t\t\tdesc := config.Desc\n\t\t\tif config.Alias != \"\" {\n\t\t\t\tdesc = fmt.Sprintf(\"(\\\\%s) %s\", config.Alias, desc)\n\t\t\t}\n\t\t\tcliSuggestions = append(cliSuggestions, prompt.Suggest{Text: \"\\\\\" + config.Cmd, Description: desc})\n\t\t}\n\t}\n}\n\nfunc setReplConfig() {\n\treplConfig = lib.MustGetCurrentPlanConfig()\n}\n\nfunc runRepl(cmd *cobra.Command, args []string) {\n\tsessionId = uuid.New().String()\n\n\tterm.SetIsRepl(true)\n\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveOrCreateProject()\n\n\tterm.StartSpinner(\"\")\n\tlib.LoadState()\n\n\tchatFlag, err := cmd.Flags().GetBool(\"chat\")\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting chat flag: %v\", err)\n\t}\n\n\ttellFlag, err := cmd.Flags().GetBool(\"tell\")\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting tell flag: %v\", err)\n\t}\n\n\tif chatFlag && tellFlag {\n\t\tterm.OutputErrorAndExit(\"Cannot specify both --chat and --tell flags\")\n\t}\n\n\tif chatFlag {\n\t\tlib.CurrentReplState.Mode = lib.ReplModeChat\n\t\tlib.WriteState()\n\t} else if tellFlag {\n\t\tlib.CurrentReplState.Mode = lib.ReplModeTell\n\t\tlib.WriteState()\n\t}\n\n\tafterNew := false\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tos.Setenv(\"PLANDEX_DISABLE_SUGGESTIONS\", \"1\")\n\t\targs := []string{}\n\n\t\tif noAuto {\n\t\t\targs = append(args, \"--no-auto\")\n\t\t} else if basicAuto {\n\t\t\targs = append(args, \"--basic\")\n\t\t} else if plusAuto {\n\t\t\targs = append(args, \"--plus\")\n\t\t} else if semiAuto {\n\t\t\targs = append(args, \"--semi\")\n\t\t} else if fullAuto {\n\t\t\targs = append(args, \"--full\")\n\t\t}\n\n\t\tif ossModels {\n\t\t\targs = append(args, \"--oss\")\n\t\t} else if strongModels {\n\t\t\targs = append(args, \"--strong\")\n\t\t} else if cheapModels {\n\t\t\targs = append(args, \"--cheap\")\n\t\t} else if dailyModels {\n\t\t\targs = append(args, \"--daily\")\n\t\t} else if reasoningModels {\n\t\t\targs = append(args, \"--reasoning\")\n\t\t} else if geminiPlannerModels {\n\t\t\targs = append(args, \"--gemini-planner\")\n\t\t} else if o3PlannerModels {\n\t\t\targs = append(args, \"--o3-planner\")\n\t\t} else if r1PlannerModels {\n\t\t\targs = append(args, \"--r1-planner\")\n\t\t} else if perplexityPlannerModels {\n\t\t\targs = append(args, \"--perplexity-planner\")\n\t\t} else if opusPlannerModels {\n\t\t\targs = append(args, \"--opus-planner\")\n\t\t}\n\n\t\tnewCmd.Run(newCmd, args)\n\t\tos.Setenv(\"PLANDEX_DISABLE_SUGGESTIONS\", \"\")\n\t\tafterNew = true\n\t}\n\n\tsetReplConfig()\n\n\tlib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode)\n\n\tprojectPaths, err = fs.GetProjectPaths(fs.Cwd)\n\tif err != nil {\n\t\tcolor.New(term.ColorHiRed).Printf(\"Error getting project paths: %v\\n\", err)\n\t}\n\n\tsettings, apiErr := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting settings: %v\", apiErr.Msg)\n\t}\n\n\tvar printAutoFn func()\n\tvar printModelFn func()\n\tif !afterNew {\n\t\tvar didUpdateConfig bool\n\t\tvar updatedConfig *shared.PlanConfig\n\t\tvar updatedSettings *shared.PlanSettings\n\t\tdidUpdateConfig, updatedConfig, printAutoFn = resolveAutoModeSilent(replConfig)\n\t\tupdatedSettings, printModelFn = resolveModelPackSilent(settings)\n\n\t\tif didUpdateConfig {\n\t\t\tloadMapIfNeeded(replConfig, updatedConfig)\n\t\t\tremoveMapIfNeeded(replConfig, updatedConfig)\n\n\t\t\tif updatedConfig != nil {\n\t\t\t\treplConfig = updatedConfig\n\t\t\t}\n\t\t}\n\n\t\tif updatedSettings != nil {\n\t\t\tsettings = updatedSettings\n\t\t}\n\t}\n\n\treplWelcome(replWelcomeParams{\n\t\tafterNew:     afterNew,\n\t\tisHelp:       false,\n\t\tprintAutoFn:  printAutoFn,\n\t\tprintModelFn: printModelFn,\n\t\tconfig:       replConfig,\n\t\tpackName:     settings.GetModelPack().Name,\n\t})\n\n\tvar p *prompt.Prompt\n\tp = prompt.New(\n\t\tfunc(in string) { executor(in, p) },\n\t\tprompt.WithPrefixCallback(func() string {\n\t\t\t// Get last part of current working directory\n\t\t\t// cwd := fs.Cwd\n\t\t\t// dirName := filepath.Base(cwd)\n\n\t\t\t// Build prefix with directory and mode indicator\n\t\t\tvar modeIcon string\n\t\t\tif lib.CurrentReplState.Mode == lib.ReplModeTell {\n\t\t\t\tmodeIcon = \"⚡️\"\n\t\t\t\tif replConfig.AutoApply && replConfig.AutoExec {\n\t\t\t\t\tmodeIcon += \"❗️\" // warning reminder for auto apply and auto exec\n\t\t\t\t}\n\t\t\t} else if lib.CurrentReplState.Mode == lib.ReplModeChat {\n\t\t\t\tmodeIcon = \"💬\"\n\t\t\t}\n\t\t\treturn fmt.Sprintf(\"%s \", modeIcon)\n\t\t}),\n\t\tprompt.WithTitle(\"Plandex \"+version.Version),\n\t\tprompt.WithSelectedSuggestionBGColor(prompt.LightGray),\n\t\tprompt.WithSuggestionBGColor(prompt.DarkGray),\n\t\tprompt.WithCompletionOnDown(),\n\t\tprompt.WithCompleter(completer),\n\t\tprompt.WithExecuteOnEnterCallback(executeOnEnter),\n\t\tprompt.WithHistory(lib.GetHistory()),\n\t)\n\tcurrentPrompt = p\n\tp.Run()\n}\n\nfunc getSuggestions() []prompt.Suggest {\n\tsuggestions := []prompt.Suggest{}\n\n\tif lib.CurrentReplState.IsMulti {\n\t\tsuggestions = append(suggestions, []prompt.Suggest{\n\t\t\t{Text: \"\\\\send\", Description: \"(\\\\s) Send the current prompt\"},\n\t\t\t{Text: \"\\\\multi\", Description: \"(\\\\m) Turn multi-line mode off\"},\n\t\t\t{Text: \"\\\\run\", Description: \"(\\\\r) Run a file through tell/chat based on current mode\"},\n\t\t\t{Text: \"\\\\quit\", Description: \"(\\\\q) Exit the REPL\"},\n\t\t}...)\n\n\t}\n\n\tif lib.CurrentReplState.Mode == lib.ReplModeTell {\n\t\tsuggestions = append(suggestions, []prompt.Suggest{\n\t\t\t{Text: \"\\\\chat\", Description: \"(\\\\ch) Switch to 'chat' mode to have a conversation without making changes\"},\n\t\t}...)\n\t} else if lib.CurrentReplState.Mode == lib.ReplModeChat {\n\t\tsuggestions = append(suggestions, []prompt.Suggest{\n\t\t\t{Text: \"\\\\tell\", Description: \"(\\\\t) Switch to 'tell' mode for implementation\"},\n\t\t}...)\n\t}\n\n\tif !lib.CurrentReplState.IsMulti {\n\t\tsuggestions = append(suggestions, []prompt.Suggest{\n\t\t\t{Text: \"\\\\multi\", Description: \"(\\\\m) Turn multi-line mode on\"},\n\t\t\t{Text: \"\\\\run\", Description: \"(\\\\r) Run a file through tell/chat based on current mode\"},\n\t\t\t{Text: \"\\\\quit\", Description: \"(\\\\q) Exit the REPL\"},\n\t\t}...)\n\t}\n\n\t// Add help command suggestion\n\tsuggestions = append(suggestions, prompt.Suggest{Text: \"\\\\help\", Description: \"(\\\\h) REPL info and list of commands\"})\n\tsuggestions = append(suggestions, cliSuggestions...)\n\n\tfor path := range projectPaths.ActivePaths {\n\t\tif path == \".\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tisDir := projectPaths.ActiveDirs[path]\n\n\t\tif isDir {\n\t\t\tpath += \"/\"\n\t\t}\n\n\t\tsuggestions = append(suggestions, prompt.Suggest{Text: \"@\" + path})\n\n\t\tloadArgs := path\n\t\tif isDir {\n\t\t\tloadArgs += \" -r\"\n\t\t}\n\t\tsuggestions = append(suggestions, prompt.Suggest{Text: \"\\\\load \" + loadArgs})\n\n\t\tif isDir {\n\t\t\tloadArgs = path\n\t\t\tloadArgs += \" --map\"\n\t\t\tsuggestions = append(suggestions, prompt.Suggest{Text: \"\\\\load \" + loadArgs})\n\n\t\t\tloadArgs = path\n\t\t\tloadArgs += \" --tree\"\n\t\t\tsuggestions = append(suggestions, prompt.Suggest{Text: \"\\\\load \" + loadArgs})\n\t\t}\n\n\t\tif filepath.Ext(path) == \".md\" || filepath.Ext(path) == \".txt\" {\n\t\t\tsuggestions = append(suggestions, prompt.Suggest{Text: \"\\\\run \" + path})\n\t\t}\n\t}\n\n\treturn suggestions\n}\n\nfunc executeOnEnter(p *prompt.Prompt, indentSize int) (int, bool) {\n\tinput := p.Buffer().Text()\n\tcmd, _ := parseCommand(input)\n\n\tif cmd != \"\" {\n\t\treturn 0, true\n\t}\n\n\tif lib.CurrentReplState.IsMulti {\n\t\treturn 0, false\n\t}\n\n\treturn 0, true\n}\n\nconst cancelOpt = \"Cancel\"\n\nfunc executor(in string, p *prompt.Prompt) {\n\tdefer lib.WriteHistory(in)\n\n\tin = strings.TrimSpace(in)\n\tlines := strings.Split(in, \"\\n\")\n\tlastLine := lines[len(lines)-1]\n\tlastLine = strings.TrimSpace(lastLine)\n\n\ttrimmedInput := strings.TrimSpace(in)\n\tif trimmedInput == \"\" {\n\t\treturn\n\t}\n\t// condense whitespace\n\tcondensedInput := strings.Join(strings.Fields(trimmedInput), \" \")\n\n\t// Handle plandex/pdx command prefix\n\tif strings.HasPrefix(lastLine, \"plandex \") || strings.HasPrefix(lastLine, \"pdx \") {\n\t\tfmt.Println()\n\t\tparts := strings.Fields(lastLine)\n\t\tif len(parts) > 1 {\n\t\t\targs := parts[1:] // Skip the \"plandex\" or \"pdx\" command\n\t\t\t_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{\n\t\t\t\tSessionId: sessionId,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error executing command: %v\\n\", err)\n\t\t\t}\n\t\t}\n\t\tfmt.Println()\n\t\treturn\n\t}\n\n\t// Find the last \\ or @ in the last line\n\tlastBackslashIndex := strings.LastIndex(lastLine, \"\\\\\")\n\tlastAtIndex := strings.LastIndex(lastLine, \"@\")\n\n\tvar preservedBuffer string\n\tif len(lines) > 1 {\n\t\tpreservedBuffer = strings.Join(lines[:len(lines)-1], \"\\n\") + \"\\n\"\n\t}\n\n\tsuggestions, _, _ := completer(prompt.Document{Text: in})\n\n\t// Handle file references\n\tif lastAtIndex != -1 && lastAtIndex > lastBackslashIndex {\n\t\tpaths := strings.Split(lastLine, \"@\")\n\t\tnumPaths := len(paths)\n\n\t\tfilteredPaths := []string{}\n\n\t\tfor i, path := range paths {\n\t\t\tp := strings.TrimSpace(path)\n\t\t\tif i == 0 {\n\t\t\t\t// text before the @\n\t\t\t\tpreservedBuffer += p + \" \"\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (p == \"\" || !projectPaths.ActivePaths[p]) && len(suggestions) > 0 && i == numPaths-1 {\n\t\t\t\tp = strings.Replace(suggestions[0].Text, \"@\", \"\", 1)\n\t\t\t\tfilteredPaths = append(filteredPaths, p)\n\t\t\t} else if projectPaths.ActivePaths[p] {\n\t\t\t\tfilteredPaths = append(filteredPaths, p)\n\t\t\t}\n\t\t}\n\n\t\tif len(filteredPaths) > 0 {\n\t\t\targs := []string{\"load\"}\n\t\t\targs = append(args, filteredPaths...)\n\t\t\targs = append(args, \"-r\")\n\t\t\tfmt.Println()\n\t\t\t_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{\n\t\t\t\tSessionId: sessionId,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error executing command: %v\\n\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t\tif preservedBuffer != \"\" {\n\t\t\t\tp.InsertTextMoveCursor(preservedBuffer, true)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Handle commands\n\tif lastBackslashIndex != -1 {\n\t\tcmdString := strings.TrimSpace(lastLine[lastBackslashIndex+1:])\n\t\tif cmdString == \"\" {\n\t\t\treturn\n\t\t}\n\t\tres := execWithInput(execWithInputParams{\n\t\t\tcmdString:          cmdString,\n\t\t\tin:                 condensedInput,\n\t\t\tlastBackslashIndex: lastBackslashIndex,\n\t\t\tpreservedBuffer:    preservedBuffer,\n\t\t\tp:                  p,\n\t\t\tlastLine:           lastLine,\n\t\t\tcondensedInput:     condensedInput,\n\t\t\ttrimmedInput:       trimmedInput,\n\t\t\tlines:              lines,\n\t\t\tsuggestions:        suggestions,\n\t\t})\n\t\tif res.shouldReturn {\n\t\t\treturn\n\t\t}\n\t\tcondensedInput = res.condensedInput\n\t\ttrimmedInput = res.trimmedInput\n\t} else if len(lines) == 1 {\n\t\t// Check for likely accidental command inputs (with no backslash) and confirm with user\n\t\tvar allCommands []string\n\t\tfor replCmd := range lib.ReplCmdAliases {\n\t\t\tallCommands = append(allCommands, replCmd)\n\t\t}\n\t\tfor _, config := range term.CliCommands {\n\t\t\tif config.Repl {\n\t\t\t\tallCommands = append(allCommands, config.Cmd)\n\t\t\t}\n\t\t}\n\n\t\t// Only suggest commands if they're close enough matches\n\t\tmaybeCmds := findSimilarCommands(lastLine, allCommands)\n\n\t\tif len(maybeCmds) > 0 {\n\t\t\tres := suggestCmds(maybeCmds, getPromptOpt(lastLine))\n\t\t\tif res.shouldReturn {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmatchedCmd := res.matchedCmd\n\t\t\tif matchedCmd != \"\" {\n\t\t\t\tres := execWithInput(execWithInputParams{\n\t\t\t\t\tcmdString:          matchedCmd,\n\t\t\t\t\tin:                 condensedInput,\n\t\t\t\t\tlastBackslashIndex: lastBackslashIndex,\n\t\t\t\t\tpreservedBuffer:    preservedBuffer,\n\t\t\t\t\tp:                  p,\n\t\t\t\t\tlastLine:           lastLine,\n\t\t\t\t\tcondensedInput:     condensedInput,\n\t\t\t\t\ttrimmedInput:       trimmedInput,\n\t\t\t\t\tlines:              lines,\n\t\t\t\t\tsuggestions:        suggestions,\n\t\t\t\t})\n\t\t\t\tif res.shouldReturn {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcondensedInput = res.condensedInput\n\t\t\t\ttrimmedInput = res.trimmedInput\n\t\t\t}\n\t\t}\n\t}\n\n\t// Handle non-command input based on mode\n\tif lib.CurrentReplState.Mode == lib.ReplModeTell {\n\t\tfmt.Println()\n\t\targs := []string{\"tell\", trimmedInput}\n\t\tvar err error\n\t\t_, err = lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{\n\t\t\tSessionId: sessionId,\n\t\t})\n\t\tif err != nil {\n\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error executing tell: %v\\n\", err)\n\t\t}\n\t} else if lib.CurrentReplState.Mode == lib.ReplModeChat {\n\t\tfmt.Println()\n\t\targs := []string{\"chat\", trimmedInput}\n\t\toutput, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{\n\t\t\tSessionId: sessionId,\n\t\t})\n\t\tif err != nil {\n\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error executing chat: %v\\n\", err)\n\t\t}\n\n\t\treplacer := strings.NewReplacer(\"'\", \"\", \"\\\"\", \"\", \"*\", \"\", \"`\", \"\", \"_\", \"\")\n\t\toutput = replacer.Replace(output)\n\n\t\trx := regexp.MustCompile(`(?i)(switch|start|continue|begin|change|chang|move|proceed|go|transition)(ing)?( to | with | into )?(tell|implementation|coding|development)( mode)?`)\n\n\t\tif rx.MatchString(output) {\n\t\t\tfmt.Println()\n\t\t\tres, err := term.ConfirmYesNo(\"Switch to tell mode for implementation?\")\n\t\t\tif err != nil {\n\t\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error confirming yes/no: %v\\n\", err)\n\t\t\t}\n\t\t\tif res {\n\t\t\t\tlib.CurrentReplState.Mode = lib.ReplModeTell\n\t\t\t\tlib.WriteState()\n\t\t\t\tfmt.Println()\n\t\t\t\tcolor.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(\" ⚡️ Tell mode is enabled \")\n\t\t\t\tfmt.Println()\n\t\t\t\tfmt.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.\")\n\t\t\t\tfmt.Println()\n\t\t\t\tbeginImplOpt := \"Begin implementation\"\n\t\t\t\tanotherPromptOpt := \"Send another prompt\"\n\t\t\t\tsel, err := term.SelectFromList(\"What would you like to do?\", []string{beginImplOpt, anotherPromptOpt})\n\t\t\t\tif err != nil {\n\t\t\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error selecting from list: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tif sel == beginImplOpt {\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\targs := []string{\"tell\", \"--from-chat\"}\n\t\t\t\t\t_, err := lib.ExecPlandexCommandWithParams(args, lib.ExecPlandexCommandParams{\n\t\t\t\t\t\tSessionId: sessionId,\n\t\t\t\t\t})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error executing tell: %v\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfmt.Println()\n}\n\nfunc completer(in prompt.Document) ([]prompt.Suggest, pstrings.RuneNumber, pstrings.RuneNumber) {\n\t// Don't show suggestions if we're navigating history\n\tif currentPrompt.IsNavigatingHistory() {\n\t\treturn []prompt.Suggest{}, 0, 0\n\t}\n\n\tendIndex := in.CurrentRuneIndex()\n\n\tlines := strings.Split(in.Text, \"\\n\")\n\tcurrentLineNum := strings.Count(in.TextBeforeCursor(), \"\\n\")\n\n\t// Don't show suggestions if we're not on the last line\n\tif currentLineNum < len(lines)-1 {\n\t\treturn []prompt.Suggest{}, 0, 0\n\t}\n\n\tlastLine := lines[len(lines)-1]\n\tif strings.TrimSpace(lastLine) == \"\" && len(lines) > 1 {\n\t\tlastLine = lines[len(lines)-2]\n\t}\n\n\t// Handle plandex/pdx command prefix\n\tif strings.HasPrefix(lastLine, \"plandex \") || strings.HasPrefix(lastLine, \"pdx \") {\n\t\tparts := strings.Fields(lastLine)\n\t\tvar prefix string\n\t\tif len(parts) > 1 {\n\t\t\tprefix = parts[len(parts)-1]\n\t\t}\n\t\tstartIndex := endIndex - pstrings.RuneNumber(len(prefix))\n\n\t\tsuggestions := []prompt.Suggest{}\n\t\tfor _, config := range term.CliCommands {\n\t\t\tsuggestions = append(suggestions, prompt.Suggest{\n\t\t\t\tText:        config.Cmd,\n\t\t\t\tDescription: config.Desc,\n\t\t\t})\n\t\t}\n\n\t\tfiltered := prompt.FilterFuzzy(suggestions, prefix, true)\n\t\treturn filtered, startIndex, endIndex\n\t}\n\n\t// Find the last valid \\ or @ in the current line\n\tlastBackslashIndex := -1\n\tlastAtIndex := -1\n\n\t// Helper function to check if character at index is valid (start of line or after space)\n\tisValidPosition := func(str string, index int) bool {\n\t\tif index <= 0 {\n\t\t\treturn true // Start of line\n\t\t}\n\t\treturn unicode.IsSpace(rune(str[index-1])) // After whitespace\n\t}\n\n\t// Find last valid backslash\n\tfor i := len(lastLine) - 1; i >= 0; i-- {\n\t\tif lastLine[i] == '\\\\' && isValidPosition(lastLine, i) {\n\t\t\tlastBackslashIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Find last valid @\n\tfor i := len(lastLine) - 1; i >= 0; i-- {\n\t\tif lastLine[i] == '@' && isValidPosition(lastLine, i) {\n\t\t\tlastAtIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tvar w string\n\tvar startIndex pstrings.RuneNumber\n\n\tif lastBackslashIndex == -1 && lastAtIndex == -1 {\n\t\treturn []prompt.Suggest{}, 0, 0\n\t}\n\n\t// Use the rightmost special character\n\tif lastBackslashIndex > lastAtIndex {\n\t\t// Get everything after the last backslash\n\t\tw = lastLine[lastBackslashIndex:]\n\t\tstartIndex = endIndex - pstrings.RuneNumber(len(w))\n\t} else if lastAtIndex != -1 {\n\t\t// Get everything after the last @\n\t\tw = lastLine[lastAtIndex:]\n\t\tstartIndex = endIndex - pstrings.RuneNumber(len(w))\n\t}\n\n\t// Verify this is at the end of the line (allowing for trailing spaces)\n\tif !strings.HasSuffix(strings.TrimSpace(lastLine), strings.TrimSpace(w)) {\n\t\treturn []prompt.Suggest{}, 0, 0\n\t}\n\n\twTrimmed := strings.TrimSpace(strings.TrimPrefix(w, \"\\\\\"))\n\tparts := strings.Split(wTrimmed, \" \")\n\twCmd := parts[0]\n\n\t// For commands, verify it starts with an actual command\n\tif strings.HasPrefix(w, \"\\\\\") {\n\t\tisValidCommand := false\n\t\tfor _, config := range term.CliCommands {\n\t\t\tif !config.Repl {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasPrefix(config.Cmd, wCmd) ||\n\t\t\t\t(config.Alias != \"\" && strings.HasPrefix(config.Alias, wCmd)) {\n\t\t\t\tisValidCommand = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\t// Also check built-in REPL commands\n\t\tif strings.HasPrefix(\"quit\", wCmd) ||\n\t\t\tstrings.HasPrefix(\"multi\", wCmd) ||\n\t\t\tstrings.HasPrefix(\"tell\", wCmd) ||\n\t\t\tstrings.HasPrefix(\"chat\", wCmd) ||\n\t\t\tstrings.HasPrefix(\"send\", wCmd) ||\n\t\t\tstrings.HasPrefix(\"run\", wCmd) {\n\t\t\tisValidCommand = true\n\t\t}\n\t\tif !isValidCommand && wCmd != \"\" {\n\t\t\treturn []prompt.Suggest{}, 0, 0\n\t\t}\n\t}\n\n\tfuzzySuggestions := prompt.FilterFuzzy(getSuggestions(), w, true)\n\tprefixMatches := prompt.FilterHasPrefix(getSuggestions(), w, true)\n\n\trunFilteredFuzzy := []prompt.Suggest{}\n\trunFilteredPrefixMatches := []prompt.Suggest{}\n\tfor _, s := range fuzzySuggestions {\n\t\tif strings.HasPrefix(s.Text, \"\\\\run \") {\n\t\t\tif wCmd == \"run\" {\n\t\t\t\trunFilteredFuzzy = append(runFilteredFuzzy, s)\n\t\t\t}\n\t\t} else {\n\t\t\trunFilteredFuzzy = append(runFilteredFuzzy, s)\n\t\t}\n\t}\n\tfor _, s := range prefixMatches {\n\t\tif strings.HasPrefix(s.Text, \"\\\\run \") {\n\t\t\tif wCmd == \"run\" {\n\t\t\t\trunFilteredPrefixMatches = append(runFilteredPrefixMatches, s)\n\t\t\t}\n\t\t} else {\n\t\t\trunFilteredPrefixMatches = append(runFilteredPrefixMatches, s)\n\t\t}\n\t}\n\tfuzzySuggestions = runFilteredFuzzy\n\tprefixMatches = runFilteredPrefixMatches\n\n\tloadFilteredFuzzy := []prompt.Suggest{}\n\tloadFilteredPrefixMatches := []prompt.Suggest{}\n\tfor _, s := range fuzzySuggestions {\n\t\tif strings.HasPrefix(s.Text, \"\\\\load \") {\n\t\t\tif wCmd == \"load\" {\n\t\t\t\tloadFilteredFuzzy = append(loadFilteredFuzzy, s)\n\t\t\t}\n\t\t} else {\n\t\t\tloadFilteredFuzzy = append(loadFilteredFuzzy, s)\n\t\t}\n\t}\n\tfor _, s := range prefixMatches {\n\t\tif strings.HasPrefix(s.Text, \"\\\\load \") {\n\t\t\tif wCmd == \"load\" {\n\t\t\t\tloadFilteredPrefixMatches = append(loadFilteredPrefixMatches, s)\n\t\t\t}\n\t\t} else {\n\t\t\tloadFilteredPrefixMatches = append(loadFilteredPrefixMatches, s)\n\t\t}\n\t}\n\tfuzzySuggestions = loadFilteredFuzzy\n\tprefixMatches = loadFilteredPrefixMatches\n\n\tif strings.TrimSpace(w) != \"\\\\\" {\n\t\tsort.Slice(prefixMatches, func(i, j int) bool {\n\t\t\tiTxt := prefixMatches[i].Text\n\t\t\tjTxt := prefixMatches[j].Text\n\t\t\tif iTxt == \"\\\\chat\" || iTxt == \"\\\\tell\" || iTxt == \"\\\\multi\" || iTxt == \"\\\\quit\" || iTxt == \"\\\\send\" || iTxt == \"\\\\run\" {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif jTxt == \"\\\\chat\" || jTxt == \"\\\\tell\" || jTxt == \"\\\\multi\" || jTxt == \"\\\\quit\" || jTxt == \"\\\\send\" || jTxt == \"\\\\run\" {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn prefixMatches[i].Text < prefixMatches[j].Text\n\t\t})\n\t}\n\n\tif len(prefixMatches) > 0 {\n\t\t// Remove prefix matches from fuzzy results to avoid duplicates\n\t\tprefixMatchSet := make(map[string]bool)\n\t\tfor _, s := range prefixMatches {\n\t\t\tprefixMatchSet[s.Text] = true\n\t\t}\n\n\t\tnonPrefixFuzzy := make([]prompt.Suggest, 0)\n\t\tfor _, s := range fuzzySuggestions {\n\t\t\tif !prefixMatchSet[s.Text] {\n\t\t\t\tnonPrefixFuzzy = append(nonPrefixFuzzy, s)\n\t\t\t}\n\t\t}\n\n\t\tfuzzySuggestions = append(prefixMatches, nonPrefixFuzzy...)\n\t}\n\n\tvar aliasMatch string\n\n\tif lib.ReplCmdAliases[wTrimmed] != \"\" {\n\t\taliasMatch = \"\\\\\" + lib.ReplCmdAliases[wTrimmed]\n\t} else {\n\t\tfor _, s := range term.CliCommands {\n\t\t\tif s.Alias == wTrimmed {\n\t\t\t\taliasMatch = \"\\\\\" + s.Cmd\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif aliasMatch != \"\" {\n\t\t// put the suggestion with the alias match at the beginning\n\t\tvar matched prompt.Suggest\n\t\tfound := false\n\t\tfor _, s := range fuzzySuggestions {\n\t\t\tif s.Text == aliasMatch {\n\t\t\t\tmatched = s\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif found {\n\t\t\tnewSuggestions := []prompt.Suggest{}\n\t\t\tnewSuggestions = append(newSuggestions, matched)\n\t\t\tfor _, s := range fuzzySuggestions {\n\t\t\t\tif s.Text != aliasMatch {\n\t\t\t\t\tnewSuggestions = append(newSuggestions, s)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfuzzySuggestions = newSuggestions\n\t\t}\n\t}\n\n\treturn fuzzySuggestions, startIndex, endIndex\n}\n\ntype replWelcomeParams struct {\n\tafterNew     bool\n\tisHelp       bool\n\tprintAutoFn  func()\n\tprintModelFn func()\n\tpackName     string\n\tconfig       *shared.PlanConfig\n}\n\nfunc replWelcome(params replWelcomeParams) {\n\t// print REPL welcome message and basic info\n\t// have to make these requests serially in case re-authentication is needed\n\n\tafterNew := params.afterNew\n\tisHelp := params.isHelp\n\tprintAutoFn := params.printAutoFn\n\tprintModelFn := params.printModelFn\n\tpackName := params.packName\n\n\tplan, apiErr := api.Client.GetPlan(lib.CurrentPlanId)\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plan: %v\", apiErr.Msg)\n\t}\n\n\tconfig := params.config\n\tif config == nil {\n\t\tconfig, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error getting plan config: %v\", apiErr.Msg)\n\t\t}\n\t}\n\n\tcurrentBranchesByPlanId, err := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{\n\t\tCurrentBranchByPlanId: map[string]string{\n\t\t\tlib.CurrentPlanId: lib.CurrentBranch,\n\t\t},\n\t})\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current branches: %v\", err)\n\t}\n\n\tterm.StopSpinner()\n\n\tif !afterNew {\n\t\tfmt.Println()\n\t}\n\n\tcolor.New(color.FgHiWhite, color.BgBlue, color.Bold).Print(\" 👋 Welcome to Plandex \")\n\n\tversionStr := version.Version\n\tif versionStr != \"development\" {\n\t\tcolor.New(color.FgHiWhite, color.BgHiBlack).Printf(\" v%s \", versionStr)\n\t}\n\n\tfmt.Println()\n\tfmt.Println()\n\n\tfmt.Println(lib.GetCurrentPlanTable(plan, currentBranchesByPlanId, nil))\n\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(false)\n\n\tvar contextMode string\n\tif config.AutoLoadContext {\n\t\tcontextMode = \"auto\"\n\t} else {\n\t\tcontextMode = \"manual\"\n\t}\n\n\tfilesStr := \"%s for loading files into context\"\n\tif contextMode == \"auto\" {\n\t\tfilesStr += \" manually (optional)\"\n\t}\n\tfilesStr += \"\\n\"\n\n\tcolor.New(color.FgHiWhite).Printf(\"%s for commands\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\\"))\n\tcolor.New(color.FgHiWhite).Printf(filesStr, color.New(term.ColorHiCyan, color.Bold).Sprint(\"@\"))\n\tcolor.New(color.FgHiWhite).Printf(\"%s (\\\\h) for help\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\help\"))\n\tcolor.New(color.FgHiWhite).Printf(\"%s (\\\\q) to exit\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\quit\"))\n\n\tfmt.Println()\n\n\tif printAutoFn != nil {\n\t\tprintAutoFn()\n\t} else {\n\t\tprintAutoModeTable(config)\n\t}\n\n\tcolor.New(color.FgHiWhite).Printf(\"%s to change auto mode\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\set-auto\"))\n\tcolor.New(color.FgHiWhite).Printf(\"%s to see config\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\config\"))\n\tcolor.New(color.FgHiWhite).Printf(\"%s to customize config\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\set-config\"))\n\tfmt.Println()\n\n\tif printModelFn != nil {\n\t\tprintModelFn()\n\t} else {\n\t\tprintModelPackTable(packName)\n\t}\n\tcolor.New(color.FgHiWhite).Printf(\"%s to see model details\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\models\"))\n\tcolor.New(color.FgHiWhite).Printf(\"%s to change models\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\set-model\"))\n\n\tshowReplMode()\n\tshowMultiLineMode()\n\tfmt.Println()\n\n\tif !isHelp {\n\t\tif lib.CurrentReplState.Mode == lib.ReplModeTell {\n\t\t\tcolor.New(color.FgHiWhite, color.BgBlue, color.Bold).Println(\" Describe a coding task 👇 \")\n\t\t} else {\n\t\t\tcolor.New(color.FgHiWhite, color.BgBlue, color.Bold).Println(\" Ask a question or chat 👇 \")\n\t\t}\n\n\t\tfmt.Println()\n\t}\n}\n\nfunc replHelp() {\n\treplWelcome(replWelcomeParams{\n\t\tafterNew: false,\n\t\tisHelp:   true,\n\t})\n\tterm.PrintHelpAllCommands()\n}\n\nfunc showReplMode() {\n\tfmt.Println()\n\tif lib.CurrentReplState.Mode == lib.ReplModeTell {\n\t\tcolor.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(\" ⚡️ Tell mode is enabled \")\n\t\tcolor.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\"))\n\t} else if lib.CurrentReplState.Mode == lib.ReplModeChat {\n\t\tcolor.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(\" 💬 Chat mode is enabled \")\n\t\tcolor.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\"))\n\t}\n\tfmt.Println()\n}\n\nfunc showMultiLineMode() {\n\tif lib.CurrentReplState.IsMulti {\n\t\tcolor.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(\" 🔢 Multi-line mode is enabled \")\n\t\tfmt.Printf(\"%s to exit multi-line mode\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\multi\"))\n\t\tfmt.Printf(\"%s for line breaks\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"enter\"))\n\t\tfmt.Printf(\"%s to send prompt\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\send\"))\n\t} else {\n\t\tcolor.New(color.BgMagenta, color.FgHiWhite, color.Bold).Println(\" 1️⃣  Multi-line mode is disabled \")\n\t\tfmt.Printf(\"%s for multi-line editing mode\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"\\\\multi\"))\n\t\tfmt.Printf(\"%s to send prompt\\n\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"enter\"))\n\t}\n}\n\nfunc parseCommand(in string) (string, string) {\n\tin = strings.TrimSpace(in)\n\tlines := strings.Split(in, \"\\n\")\n\tlastLine := lines[len(lines)-1]\n\tlastLine = strings.TrimSpace(lastLine)\n\n\tinput := strings.TrimSpace(in)\n\tif input == \"\" {\n\t\treturn \"\", \"\"\n\t}\n\n\t// Handle plandex/pdx command prefix\n\tif strings.HasPrefix(lastLine, \"plandex \") || strings.HasPrefix(lastLine, \"pdx \") {\n\t\treturn lastLine, lastLine\n\t}\n\n\t// Find the last \\ or @ in the last line\n\tlastBackslashIndex := strings.LastIndex(lastLine, \"\\\\\")\n\tlastAtIndex := strings.LastIndex(lastLine, \"@\")\n\n\tsuggestions, _, _ := completer(prompt.Document{Text: in})\n\n\t// Handle file references\n\tif lastAtIndex != -1 && lastAtIndex > lastBackslashIndex {\n\t\tpaths := strings.Split(lastLine, \"@\")\n\t\tsplit2 := strings.SplitN(lastLine, \"@\", 2)\n\t\tnumPaths := len(paths)\n\n\t\tfilteredPaths := []string{}\n\n\t\tfor i, path := range paths {\n\t\t\tp := strings.TrimSpace(path)\n\t\t\tif i == 0 {\n\t\t\t\t// text before the @\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (p == \"\" || !projectPaths.ActivePaths[p]) && len(suggestions) > 0 && i == numPaths-1 {\n\t\t\t\tp = strings.Replace(suggestions[0].Text, \"@\", \"\", 1)\n\t\t\t\tfilteredPaths = append(filteredPaths, p)\n\t\t\t} else if projectPaths.ActivePaths[p] {\n\t\t\t\tfilteredPaths = append(filteredPaths, p)\n\t\t\t}\n\t\t}\n\n\t\tif len(filteredPaths) > 0 {\n\t\t\tres := \"\"\n\t\t\tfor _, p := range filteredPaths {\n\t\t\t\tres += \"@\" + p + \" \"\n\t\t\t}\n\t\t\treturn res, split2[1]\n\t\t}\n\t}\n\n\t// Handle commands\n\tif lastBackslashIndex != -1 {\n\t\tcmdString := strings.TrimSpace(lastLine[lastBackslashIndex+1:])\n\t\tif cmdString == \"\" {\n\t\t\treturn \"\", \"\"\n\t\t}\n\n\t\t// Split into command and args\n\t\tparts := strings.Fields(cmdString)\n\t\tcmd := parts[0]\n\t\targs := parts[1:]\n\n\t\t// Handle built-in REPL commands\n\t\tswitch cmd {\n\t\tcase \"quit\", lib.ReplCmdAliases[\"quit\"]:\n\t\t\treturn \"\\\\quit\", \"\\\\\" + cmdString\n\n\t\tcase \"help\", lib.ReplCmdAliases[\"help\"]:\n\t\t\treturn \"\\\\help\", \"\\\\\" + cmdString\n\n\t\tcase \"multi\", lib.ReplCmdAliases[\"multi\"]:\n\t\t\treturn \"\\\\multi\", \"\\\\\" + cmdString\n\n\t\tcase \"send\", lib.ReplCmdAliases[\"send\"]:\n\t\t\treturn \"\\\\send\", \"\\\\\" + cmdString\n\n\t\tcase \"tell\", lib.ReplCmdAliases[\"tell\"]:\n\t\t\treturn \"\\\\tell\", \"\\\\\" + cmdString\n\n\t\tcase \"chat\", lib.ReplCmdAliases[\"chat\"]:\n\t\t\treturn \"\\\\chat\", \"\\\\\" + cmdString\n\n\t\tcase \"run\", lib.ReplCmdAliases[\"run\"]:\n\t\t\treturn \"\\\\run\", \"\\\\\" + cmdString\n\n\t\tdefault:\n\t\t\t// Check CLI commands\n\t\t\tvar matchedCmd string\n\n\t\t\tfor _, config := range term.CliCommands {\n\n\t\t\t\tif (cmd == config.Cmd || (config.Alias != \"\" && cmd == config.Alias)) && config.Repl {\n\t\t\t\t\tmatchedCmd = config.Cmd\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif matchedCmd == \"\" {\n\t\t\t\tfor _, config := range term.CliCommands {\n\t\t\t\t\tif strings.HasPrefix(config.Cmd, cmd) && config.Repl {\n\t\t\t\t\t\tmatchedCmd = config.Cmd\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif matchedCmd != \"\" {\n\t\t\t\tres := matchedCmd\n\t\t\t\tif len(args) > 0 {\n\t\t\t\t\tres += \" \" + strings.Join(args, \" \")\n\t\t\t\t}\n\t\t\t\treturn res, \"\\\\\" + cmdString\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", \"\"\n}\n\nfunc isFileInProjectPaths(filePath string) bool {\n\t// Convert to absolute path\n\tabsPath, err := filepath.Abs(filePath)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// Check if file is within any project path\n\tfor path := range projectPaths.ActivePaths {\n\t\tprojectAbs, err := filepath.Abs(path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(absPath, projectAbs) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc handleRunCommand(args []string) error {\n\tif len(args) != 1 {\n\t\treturn fmt.Errorf(\"run command requires exactly one file path argument\")\n\t}\n\n\tfilePath := args[0]\n\n\t// Check if file exists\n\tif _, err := os.Stat(filePath); os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"file does not exist: %s\", filePath)\n\t}\n\n\t// Build command based on current mode\n\tvar cmdArgs []string\n\tif lib.CurrentReplState.Mode == lib.ReplModeTell {\n\t\tcmdArgs = []string{\"tell\", \"-f\", filePath}\n\t} else {\n\t\tcmdArgs = []string{\"chat\", \"-f\", filePath}\n\t}\n\n\t// Execute the command\n\t_, err := lib.ExecPlandexCommand(cmdArgs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error executing command: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc getPromptOpt(cmd string) string {\n\tasPrompt := cmd\n\tif len(asPrompt) > 20 {\n\t\tasPrompt = asPrompt[:20] + \"...\"\n\t}\n\treturn fmt.Sprintf(\"Send '%s' as a prompt to the AI model\", asPrompt)\n}\n\ntype suggestCmdsResult struct {\n\tshouldReturn bool\n\tmatchedCmd   string\n}\n\nfunc suggestCmds(cmds []string, promptOpt string) suggestCmdsResult {\n\tvar matchedCmd string\n\n\tfmt.Println()\n\topts := []string{}\n\n\tfor _, match := range cmds {\n\t\topts = append(opts, \"\\\\\"+match)\n\t}\n\topts = append(opts, cancelOpt, promptOpt)\n\tsel, err := term.SelectFromList(\"🤔 Did you mean to type one of these commands?\", opts)\n\tif err != nil {\n\t\tcolor.New(term.ColorHiRed).Printf(\"Error selecting from list: %v\\n\", err)\n\t}\n\tif sel == cancelOpt {\n\t\treturn suggestCmdsResult{shouldReturn: true}\n\t} else if sel != promptOpt {\n\t\tmatchedCmd = strings.Replace(sel, \"\\\\\", \"\", 1)\n\t}\n\n\treturn suggestCmdsResult{matchedCmd: matchedCmd}\n}\n\ntype execWithInputParams struct {\n\tcmdString          string\n\tin                 string\n\tlastBackslashIndex int\n\tpreservedBuffer    string\n\tp                  *prompt.Prompt\n\tlastLine           string\n\tcondensedInput     string\n\ttrimmedInput       string\n\tlines              []string\n\tsuggestions        []prompt.Suggest\n}\n\ntype execWithInputResult struct {\n\tshouldReturn   bool\n\tcondensedInput string\n\ttrimmedInput   string\n}\n\nfunc execWithInput(params execWithInputParams) execWithInputResult {\n\tcmdString := params.cmdString\n\tin := params.in\n\tlastBackslashIndex := params.lastBackslashIndex\n\tpreservedBuffer := params.preservedBuffer\n\tlastLine := params.lastLine\n\tp := params.p\n\tcondensedInput := params.condensedInput\n\ttrimmedInput := params.trimmedInput\n\tlines := params.lines\n\tsuggestions := params.suggestions\n\n\t// Split into command and args\n\tparts := strings.Fields(cmdString)\n\tcmd := parts[0]\n\targs := parts[1:]\n\n\tvar fuzzyNEQCheckCmds []string\n\tfor replCmd := range lib.ReplCmdAliases {\n\t\tif replCmd != cmd {\n\t\t\tfuzzyNEQCheckCmds = append(fuzzyNEQCheckCmds, replCmd)\n\t\t}\n\t}\n\tfor _, config := range term.CliCommands {\n\t\tif !config.Repl {\n\t\t\tcontinue\n\t\t}\n\t\tif config.Cmd != cmd {\n\t\t\tfuzzyNEQCheckCmds = append(fuzzyNEQCheckCmds, config.Cmd)\n\t\t}\n\t}\n\n\tfuzzyNEQMatches := findSimilarCommands(cmd, fuzzyNEQCheckCmds)\n\n\t// Handle built-in REPL commands\n\tswitch {\n\tcase cmd == \"quit\" || cmd == lib.ReplCmdAliases[\"quit\"]:\n\t\tlib.WriteHistory(in)\n\t\tos.Exit(0)\n\n\tcase cmd == \"help\" || cmd == lib.ReplCmdAliases[\"help\"]:\n\t\tif lastBackslashIndex > 0 {\n\t\t\tpreservedBuffer += lastLine[:lastBackslashIndex]\n\t\t}\n\t\treplHelp()\n\t\tfmt.Println()\n\t\tif preservedBuffer != \"\" {\n\t\t\tp.InsertTextMoveCursor(preservedBuffer, true)\n\t\t}\n\t\treturn execWithInputResult{shouldReturn: true}\n\n\tcase cmd == \"multi\" || cmd == lib.ReplCmdAliases[\"multi\"]:\n\t\tif lastBackslashIndex > 0 {\n\t\t\tpreservedBuffer += lastLine[:lastBackslashIndex]\n\t\t}\n\t\tfmt.Println()\n\t\tlib.CurrentReplState.IsMulti = !lib.CurrentReplState.IsMulti\n\t\tshowMultiLineMode()\n\t\tlib.WriteState()\n\t\tfmt.Println()\n\t\tif preservedBuffer != \"\" {\n\t\t\tp.InsertTextMoveCursor(preservedBuffer, true)\n\t\t}\n\t\treturn execWithInputResult{shouldReturn: true}\n\n\tcase cmd == \"send\" || cmd == lib.ReplCmdAliases[\"send\"]:\n\t\tcondensedSplit := strings.Split(condensedInput, \"\\\\s\")\n\t\tcondensedInput = strings.TrimSpace(condensedSplit[0])\n\t\tcondensedInput = strings.TrimSpace(condensedInput)\n\n\t\ttrimmedSplit := strings.Split(trimmedInput, \"\\\\s\")\n\t\ttrimmedInput = strings.TrimSpace(trimmedSplit[0])\n\t\ttrimmedInput = strings.TrimSpace(trimmedInput)\n\n\t\tif condensedInput == \"\" {\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"🤷‍♂️ No prompt to send\")\n\t\t\tfmt.Println()\n\t\t\treturn execWithInputResult{shouldReturn: true}\n\t\t}\n\n\tcase cmd == \"tell\" || cmd == lib.ReplCmdAliases[\"tell\"]:\n\t\tif lastBackslashIndex > 0 {\n\t\t\tpreservedBuffer += lastLine[:lastBackslashIndex]\n\t\t}\n\t\tlib.CurrentReplState.Mode = lib.ReplModeTell\n\t\tlib.WriteState()\n\t\tshowReplMode()\n\t\tif preservedBuffer != \"\" {\n\t\t\tp.InsertTextMoveCursor(preservedBuffer, true)\n\t\t}\n\t\treturn execWithInputResult{shouldReturn: true}\n\n\tcase cmd == \"chat\" || cmd == lib.ReplCmdAliases[\"chat\"]:\n\t\tif lastBackslashIndex > 0 {\n\t\t\tpreservedBuffer += lastLine[:lastBackslashIndex]\n\t\t}\n\t\tlib.CurrentReplState.Mode = lib.ReplModeChat\n\t\tlib.WriteState()\n\t\tshowReplMode()\n\t\tif preservedBuffer != \"\" {\n\t\t\tp.InsertTextMoveCursor(preservedBuffer, true)\n\t\t}\n\t\treturn execWithInputResult{shouldReturn: true}\n\n\tcase cmd == \"run\" || cmd == lib.ReplCmdAliases[\"run\"]:\n\t\tif lastBackslashIndex > 0 {\n\t\t\tpreservedBuffer += lastLine[:lastBackslashIndex]\n\t\t}\n\t\tfmt.Println()\n\t\tif err := handleRunCommand(args); err != nil {\n\t\t\tcolor.New(term.ColorHiRed).Printf(\"Run command failed: %v\\n\", err)\n\t\t}\n\t\tfmt.Println()\n\t\tif preservedBuffer != \"\" {\n\t\t\tp.InsertTextMoveCursor(preservedBuffer, true)\n\t\t}\n\t\treturn execWithInputResult{shouldReturn: true}\n\n\tdefault:\n\t\t// Check CLI commands\n\t\tvar matchedCmd string\n\n\t\tfor _, config := range term.CliCommands {\n\t\t\tif (cmd == config.Cmd || (config.Alias != \"\" && cmd == config.Alias)) && config.Repl {\n\t\t\t\tmatchedCmd = config.Cmd\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif matchedCmd == \"\" && len(suggestions) > 0 {\n\t\t\tmatchedCmd = strings.Replace(suggestions[0].Text, \"\\\\\", \"\", 1)\n\t\t\treturn execWithInput(execWithInputParams{\n\t\t\t\tcmdString:          matchedCmd,\n\t\t\t\tin:                 condensedInput,\n\t\t\t\tlastBackslashIndex: lastBackslashIndex,\n\t\t\t\tpreservedBuffer:    preservedBuffer,\n\t\t\t\tp:                  p,\n\t\t\t\tlastLine:           lastLine,\n\t\t\t\tcondensedInput:     condensedInput,\n\t\t\t\ttrimmedInput:       trimmedInput,\n\t\t\t\tlines:              lines,\n\t\t\t\tsuggestions:        suggestions,\n\t\t\t})\n\t\t}\n\n\t\tif matchedCmd == \"\" {\n\n\t\t\tpromptOpt := getPromptOpt(cmd)\n\t\t\tif len(fuzzyNEQMatches) > 0 {\n\t\t\t\tres := suggestCmds(fuzzyNEQMatches, promptOpt)\n\t\t\t\tif res.shouldReturn {\n\t\t\t\t\treturn execWithInputResult{shouldReturn: true}\n\t\t\t\t}\n\t\t\t\tmatchedCmd = res.matchedCmd\n\t\t\t\treturn execWithInput(execWithInputParams{\n\t\t\t\t\tcmdString:          matchedCmd,\n\t\t\t\t\tin:                 condensedInput,\n\t\t\t\t\tlastBackslashIndex: lastBackslashIndex,\n\t\t\t\t\tpreservedBuffer:    preservedBuffer,\n\t\t\t\t\tp:                  p,\n\t\t\t\t\tlastLine:           lastLine,\n\t\t\t\t\tcondensedInput:     condensedInput,\n\t\t\t\t\ttrimmedInput:       trimmedInput,\n\t\t\t\t\tlines:              lines,\n\t\t\t\t\tsuggestions:        suggestions,\n\t\t\t\t})\n\t\t\t} else if len(lines) == 1 && strings.HasPrefix(trimmedInput, \"\\\\\") {\n\t\t\t\tshowCmdsOpt := \"Show available commands\"\n\t\t\t\topts := []string{cancelOpt, showCmdsOpt, promptOpt}\n\t\t\t\tsel, err := term.SelectFromList(\"🤔 Couldn't find a matching command. What do you want to do?\", opts)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error selecting from list: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\tif sel == cancelOpt {\n\t\t\t\t\treturn execWithInputResult{shouldReturn: true}\n\t\t\t\t} else if sel == showCmdsOpt {\n\t\t\t\t\treplHelp()\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\treturn execWithInputResult{shouldReturn: true}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif matchedCmd != \"\" {\n\t\t\t// fmt.Println(\"> plandex \" + config.Cmd)\n\t\t\tif lastBackslashIndex > 0 {\n\t\t\t\tpreservedBuffer += lastLine[:lastBackslashIndex]\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t\texecArgs := []string{matchedCmd}\n\t\t\tif matchedCmd == \"continue\" && chatOnly {\n\t\t\t\texecArgs = append(execArgs, \"--chat\")\n\t\t\t}\n\t\t\texecArgs = append(execArgs, args...)\n\t\t\t_, err := lib.ExecPlandexCommandWithParams(execArgs, lib.ExecPlandexCommandParams{\n\t\t\t\tSessionId: sessionId,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tcolor.New(term.ColorHiRed).Printf(\"Error executing command: %v\\n\", err)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t\tif preservedBuffer != \"\" {\n\t\t\t\tp.InsertTextMoveCursor(preservedBuffer, true)\n\t\t\t}\n\n\t\t\tif strings.HasPrefix(matchedCmd, \"set-auto\") || strings.HasPrefix(matchedCmd, \"set-config\") {\n\t\t\t\tterm.StartSpinner(\"\")\n\t\t\t\tsetReplConfig()\n\t\t\t\tterm.StopSpinner()\n\t\t\t}\n\t\t\treturn execWithInputResult{shouldReturn: true}\n\t\t}\n\t}\n\n\treturn execWithInputResult{\n\t\tcondensedInput: condensedInput,\n\t\ttrimmedInput:   trimmedInput,\n\t}\n}\n\nfunc findSimilarCommands(input string, commands []string) []string {\n\tinput = strings.TrimSpace(input)\n\tinput = strings.ToLower(input)\n\tinput = strings.Trim(input, \"/\")\n\n\t// Get ranked matches\n\tranks := fuzzy.RankFind(input, commands)\n\n\t// Filter strictly by distance\n\tvar filtered []string\n\tfor _, rank := range ranks {\n\t\t// include if either is a substring of the other\n\t\tif strings.Contains(rank.Target, input) || strings.Contains(input, rank.Target) {\n\t\t\tfiltered = append(filtered, rank.Target)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Normalize threshold based on command length\n\t\tmaxLen := len(input)\n\t\tif len(rank.Target) > maxLen {\n\t\t\tmaxLen = len(rank.Target)\n\t\t}\n\n\t\tthreshold := 4 // Base threshold\n\t\tif maxLen < 5 {\n\t\t\tthreshold = 1 // Stricter for very short commands\n\t\t}\n\n\t\tif rank.Distance <= threshold {\n\t\t\tfiltered = append(filtered, rank.Target)\n\t\t}\n\t}\n\n\treturn filtered\n}\n"
  },
  {
    "path": "app/cli/cmd/revoke.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar revokeCmd = &cobra.Command{\n\tUse:   \"revoke [email]\",\n\tShort: \"Revoke an invite or remove a user from the org\",\n\tRun:   revoke,\n\tArgs:  cobra.MaximumNArgs(1),\n}\n\nfunc init() {\n\tRootCmd.AddCommand(revokeCmd)\n}\n\nfunc revoke(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\temail := \"\"\n\tif len(args) > 0 {\n\t\temail = args[0]\n\t}\n\n\tvar userResp *shared.ListUsersResponse\n\tvar pendingInvites []*shared.Invite\n\terrCh := make(chan error)\n\n\tterm.StartSpinner(\"\")\n\n\tgo func() {\n\t\tvar err *shared.ApiError\n\t\tuserResp, err = api.Client.ListUsers()\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error fetching users: %s\", err.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tvar err *shared.ApiError\n\t\tpendingInvites, err = api.Client.ListPendingInvites()\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error fetching pending invites: %s\", err.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(err.Error())\n\t\t}\n\t}\n\n\tterm.StopSpinner()\n\n\ttype userInfo struct {\n\t\tId       string\n\t\tIsInvite bool\n\t}\n\n\temailToUserMap := make(map[string]userInfo)\n\tlabelToEmail := make(map[string]string)\n\n\t// Combine users and invites for selection\n\tcombinedList := make([]string, 0, len(userResp.Users)+len(pendingInvites))\n\tfor _, user := range userResp.Users {\n\t\tlabel := fmt.Sprintf(\"%s <%s>\", user.Name, user.Email)\n\t\tlabelToEmail[label] = user.Email\n\t\tcombinedList = append(combinedList, label)\n\t\temailToUserMap[user.Email] = userInfo{Id: user.Id, IsInvite: false}\n\t}\n\tfor _, invite := range pendingInvites {\n\t\tlabel := fmt.Sprintf(\"%s <%s> (invite pending)\", invite.Name, invite.Email)\n\t\tlabelToEmail[label] = invite.Email\n\t\tcombinedList = append(combinedList, label)\n\t\temailToUserMap[invite.Email] = userInfo{Id: invite.Id, IsInvite: true}\n\t}\n\n\tif email == \"\" {\n\t\tselected, err := term.SelectFromList(\"Select a user or invite:\", combinedList)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting item to revoke: %v\", err)\n\t\t}\n\n\t\temail = labelToEmail[selected]\n\t}\n\n\tif email == \"\" {\n\t\tterm.OutputErrorAndExit(\"No user or invite selected\")\n\t}\n\n\t// Determine if email belongs to a user or an invite and revoke accordingly\n\tif userInfo, exists := emailToUserMap[email]; exists {\n\t\tif userInfo.IsInvite {\n\t\t\tif err := api.Client.DeleteInvite(userInfo.Id); err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Failed to revoke invite: %v\", err)\n\t\t\t}\n\t\t\tfmt.Println(\"✅ Invite revoked\")\n\t\t} else {\n\t\t\tif err := api.Client.DeleteUser(userInfo.Id); err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Failed to remove user: %v\", err)\n\t\t\t}\n\t\t\tfmt.Println(\"✅ User removed\")\n\t\t}\n\t} else {\n\t\tterm.OutputErrorAndExit(\"No user or pending invite found for email '%s'\", email)\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/rewind.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar rewindCmd = &cobra.Command{\n\tUse:     \"rewind [steps-or-sha]\",\n\tAliases: []string{\"rw\"},\n\tShort:   \"Rewind plan state and optionally revert project files\",\n\tLong: `Rewind plan state and optionally revert project files to match.\n\t\nYou can pass a \"steps\" number or a commit sha. If a steps number is passed, \nthe plan will be rewound that many steps. If a commit sha is passed, the \nplan will be rewound to that commit. If neither is passed, you will be \nprompted to select a step from the history.\n\nBy default, you will be prompted whether to revert project files to match \nthe rewound plan state. You can use --revert to automatically revert \nfiles, or configure this behavior with the 'auto-revert' plan config setting.\n\nIf project files have changes, you will always be prompted before updating.`,\n\tArgs: cobra.MaximumNArgs(1),\n\tRun:  rewind,\n}\n\nvar revert bool\nvar skipRevert bool\n\nfunc init() {\n\tRootCmd.AddCommand(rewindCmd)\n\trewindCmd.Flags().BoolVar(&revert, \"revert\", false, \"Also revert project files to match plan state\")\n\trewindCmd.Flags().BoolVar(&skipRevert, \"skip-revert\", false, \"Skip reverting project files to match plan state\")\n\trewindCmd.Flags().BoolVar(&autoCommit, \"commit\", false, \"Commit changes to git when --revert is passed\")\n\trewindCmd.Flags().BoolVar(&skipCommit, \"skip-commit\", false, \"Skip committing changes to git when --revert is passed\")\n\n}\n\nfunc rewind(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tif skipRevert && revert {\n\t\tterm.OutputErrorAndExit(\"Cannot pass both --revert and --skip-revert\")\n\t}\n\n\t// Get logs\n\tterm.StartSpinner(\"\")\n\tlogsRes, apiErr := api.Client.ListLogs(lib.CurrentPlanId, lib.CurrentBranch)\n\tif apiErr != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"Error getting logs: %v\", apiErr)\n\t}\n\n\tvar targetSha string\n\tvar steps int\n\tvar isSha bool\n\n\tparseGitLog := func(log string) (string, string) {\n\t\t// Parse the log entry\n\t\tlines := strings.Split(log, \"\\n\")\n\t\tif len(lines) < 2 {\n\t\t\treturn \"\", \"\"\n\t\t}\n\n\t\t// Extract sha and timestamp from first line\n\t\tparts := strings.Split(lines[0], \"|\")\n\t\tif len(parts) < 2 {\n\t\t\treturn \"\", \"\"\n\t\t}\n\n\t\tshaLine := strings.TrimSpace(parts[0])\n\t\tsha := regexp.MustCompile(`📝 Update (\\w+)`).FindStringSubmatch(shaLine)[1]\n\t\tmsg := strings.TrimSpace(lines[1])\n\n\t\treturn sha, msg\n\t}\n\n\tlogEntries := strings.Split(logsRes.Body, \"\\n\\n\")\n\n\tif len(args) == 0 {\n\t\t// No arguments - show selection list\n\t\toptions := make([]string, 0)\n\n\t\tfor _, log := range logEntries {\n\t\t\tif log == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsha, msg := parseGitLog(log)\n\n\t\t\tif sha == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Format the two-line option\n\t\t\toption := fmt.Sprintf(\"%s | %s\", sha, formatLogMessage(msg))\n\t\t\toptions = append(options, option)\n\t\t}\n\n\t\tterm.StopSpinner()\n\t\tselected, err := term.SelectFromList(\"Select step to rewind to:\", options)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting step: %v\", err)\n\t\t}\n\n\t\t// Parse selected option to get sha\n\t\tparts := strings.Split(selected, \" | \")\n\t\tif len(parts) < 2 {\n\t\t\tterm.OutputErrorAndExit(\"Invalid selection\")\n\t\t}\n\n\t\ttargetSha = parts[0]\n\t\tisSha = true\n\n\t} else {\n\t\t// Arguments provided - use direct rewind logic\n\t\tstepsOrSha := args[0]\n\t\tsteps, err := strconv.Atoi(stepsOrSha)\n\n\t\tif err == nil && steps > 0 && steps < 999 {\n\t\t\t// Rewind by the specified number of steps\n\t\t\ttargetSha = logsRes.Shas[steps]\n\t\t} else if sha := stepsOrSha; sha != \"\" {\n\t\t\t// Rewind to the specified Sha\n\t\t\ttargetSha = sha\n\t\t\tisSha = true\n\t\t} else {\n\t\t\tterm.OutputErrorAndExit(\"Invalid steps or sha. Steps must be a positive integer, and sha must be a valid commit hash.\")\n\t\t}\n\t}\n\n\tdoRewind := func() {\n\t\tvar updatedModelSettings bool\n\n\t\tterm.StartSpinner(\"\")\n\t\t_, apiErr := api.Client.RewindPlan(lib.CurrentPlanId, lib.CurrentBranch, shared.RewindPlanRequest{\n\t\t\tSha: targetSha,\n\t\t})\n\n\t\tif apiErr != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"Error rewinding plan: %v\", apiErr)\n\t\t}\n\n\t\tvar err error\n\t\tupdatedModelSettings, err = lib.SaveLatestPlanModelSettingsIfNeeded()\n\t\tterm.StopSpinner()\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error saving model settings: %v\", err)\n\t\t}\n\n\t\tvar msg string\n\t\tif isSha {\n\t\t\tmsg = \"✅ Rewound to \" + targetSha\n\t\t} else {\n\t\t\tpostfix := \"s\"\n\t\t\tif steps == 1 {\n\t\t\t\tpostfix = \"\"\n\t\t\t}\n\t\t\tmsg = fmt.Sprintf(\"✅ Rewound %d step%s to %s\", steps, postfix, targetSha)\n\t\t}\n\n\t\tfmt.Println(msg)\n\n\t\tif updatedModelSettings {\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"🧠 Model settings file updated → \", lib.GetPlanModelSettingsPath(lib.CurrentPlanId))\n\t\t}\n\t\tfmt.Println()\n\t}\n\n\tprintNoChanges := func() {\n\t\tfmt.Println(\"🙅‍♂️ No project files were modified\")\n\t\tfmt.Println()\n\t}\n\n\tprintCmds := func() {\n\t\tterm.PrintCmds(\"\", \"log\", \"continue\")\n\t}\n\n\t// get the timestamp of the target sha\n\tvar targetShaTimestamp time.Time\n\tfor _, log := range logEntries {\n\t\tif log == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tsha, _ := parseGitLog(log)\n\t\tif sha == targetSha {\n\t\t\ttimestamp, err := lib.GetGitLogTimestamp(log)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttargetShaTimestamp = timestamp\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif targetShaTimestamp.IsZero() {\n\t\tterm.OutputErrorAndExit(\"Error getting timestamp for target sha: \" + targetSha)\n\t}\n\n\t// Get current plan state to check for undone applies\n\tterm.StartSpinner(\"\")\n\tcurrentState, apiErr := api.Client.GetCurrentPlanState(lib.CurrentPlanId, lib.CurrentBranch)\n\tif apiErr != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"Error getting plan state: %v\", apiErr)\n\t}\n\n\t// Get list of applies that will be undone\n\tundonePlanApplies := lib.GetUndonePlanApplies(currentState, targetShaTimestamp)\n\n\t// If no applies are being undone, skip revert entirely\n\tif len(undonePlanApplies) == 0 {\n\t\t// Just do the rewind, no need for any file operations\n\t\tdoRewind()\n\t\tprintNoChanges()\n\t\tprintCmds()\n\t\treturn\n\t}\n\n\t// Get the set of affected file paths\n\n\t// Determine if we should revert based on flag/config\n\tvar shouldRevert bool\n\tneedsPrompt := true\n\tvar config *shared.PlanConfig\n\n\tif cmd.Flags().Changed(\"revert\") || cmd.Flags().Changed(\"skip-revert\") {\n\t\tif skipRevert {\n\t\t\tshouldRevert = false\n\t\t} else {\n\t\t\tshouldRevert = revert\n\t\t}\n\t\tneedsPrompt = false\n\t} else {\n\t\tconfig, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error getting plan config: %v\", apiErr)\n\t\t}\n\t\tshouldRevert = config.AutoRevertOnRewind\n\t\tneedsPrompt = false\n\t}\n\n\tvar targetState *shared.CurrentPlanState\n\tvar analysis *lib.RewindAnalysis\n\n\tif shouldRevert || needsPrompt {\n\t\t// First preview the rewind to check for conflicts\n\t\ttargetState, apiErr = api.Client.GetCurrentPlanStateAtSha(lib.CurrentPlanId, targetSha)\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error previewing rewind: %v\", apiErr)\n\t\t}\n\n\t\tif targetState == nil {\n\t\t\tterm.OutputErrorAndExit(\"Error previewing rewind - no state found at sha: \" + targetSha)\n\t\t}\n\n\t\tvar err error\n\t\tanalysis, err = lib.AnalyzeRewind(targetState, currentState)\n\t\tif err != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"Error analyzing rewind: %v\", err)\n\t\t}\n\n\t\tif len(analysis.RequiredChanges) == 0 {\n\t\t\t// No file changes - proceed with rewind\n\t\t\tdoRewind()\n\t\t\tprintNoChanges()\n\t\t\tprintCmds()\n\t\t\treturn\n\t\t}\n\n\t\t// Show file differences\n\t\tterm.StopSpinner()\n\n\t\t// Group changes by type for display\n\t\ttoAdd := make([]string, 0)\n\t\ttoRemove := make([]string, 0)\n\t\ttoModify := make([]string, 0)\n\n\t\tfor path, content := range analysis.RequiredChanges {\n\t\t\tif content == \"\" {\n\t\t\t\ttoRemove = append(toRemove, path)\n\t\t\t} else if currentState.ContextsByPath[path] == nil {\n\t\t\t\ttoAdd = append(toAdd, path)\n\t\t\t} else {\n\t\t\t\ttoModify = append(toModify, path)\n\t\t\t}\n\t\t}\n\n\t\tif needsPrompt || len(analysis.Conflicts) > 0 {\n\t\t\ts := \"files\"\n\t\t\tif len(analysis.RequiredChanges) == 1 {\n\t\t\t\ts = \"file\"\n\t\t\t}\n\n\t\t\tfmt.Printf(\"⏪ %d local project %s differ from target plan state\\n\", len(analysis.RequiredChanges), s)\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"Reverting the %s will make these changes locally 👇\\n\", s)\n\t\t\tfmt.Println()\n\n\t\t\tif len(toAdd) > 0 {\n\t\t\t\tfmt.Println(\"To add:\")\n\t\t\t\tfor _, path := range toAdd {\n\t\t\t\t\tfmt.Printf(\" • %s\\n\", path)\n\t\t\t\t}\n\t\t\t\tfmt.Println()\n\t\t\t}\n\n\t\t\tif len(toRemove) > 0 {\n\t\t\t\tfmt.Println(\"To remove:\")\n\t\t\t\tfor _, path := range toRemove {\n\t\t\t\t\tfmt.Printf(\" • %s\\n\", path)\n\t\t\t\t}\n\t\t\t\tfmt.Println()\n\t\t\t}\n\n\t\t\tif len(toModify) > 0 {\n\t\t\t\tfmt.Println(\"To update:\")\n\t\t\t\tfor _, path := range toModify {\n\t\t\t\t\tfmt.Printf(\" • %s\\n\", path)\n\t\t\t\t}\n\t\t\t\tfmt.Println()\n\t\t\t}\n\n\t\t\tif len(analysis.Conflicts) > 0 {\n\t\t\t\t// Always prompt if there are conflicts\n\t\t\t\ts := \" These project files have\"\n\t\t\t\tif len(analysis.Conflicts) == 1 {\n\t\t\t\t\ts = \" A project file has\"\n\t\t\t\t}\n\t\t\t\tfmt.Printf(\"⚠️  %s been updated outside of Plandex since the latest apply:\\n\", s)\n\t\t\t\tfor path := range analysis.Conflicts {\n\t\t\t\t\tfmt.Printf(\" • %s\\n\", path)\n\t\t\t\t}\n\t\t\t\tfmt.Println()\n\n\t\t\t\tfmt.Println(\"If you revert, you will lose those changes.\")\n\t\t\t\tfmt.Println()\n\n\t\t\t\ts = \"files\"\n\t\t\t\tif len(analysis.RequiredChanges) == 1 {\n\t\t\t\t\ts = \"file\"\n\t\t\t\t}\n\n\t\t\t\toptions := []string{\n\t\t\t\t\tfmt.Sprintf(\"Revert project %s to match rewound plan state (overwrite changes)\", s),\n\t\t\t\t\tfmt.Sprintf(\"Rewind plan, but skip reverting project %s\", s),\n\t\t\t\t\t\"Cancel rewind\",\n\t\t\t\t}\n\n\t\t\t\tselected, err := term.SelectFromList(\"What do you want to do?\", options)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error getting user input: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tswitch selected {\n\t\t\t\tcase options[0]:\n\t\t\t\t\tshouldRevert = true\n\t\t\t\tcase options[1]:\n\t\t\t\t\tshouldRevert = false\n\t\t\t\tcase options[2]:\n\t\t\t\t\tos.Exit(0)\n\t\t\t\t}\n\n\t\t\t\tneedsPrompt = false\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now that we've handled the file state decision, perform the actual rewind\n\tdoRewind()\n\n\tdidRevert := false\n\n\tif shouldRevert || needsPrompt {\n\t\tif needsPrompt {\n\t\t\tterm.StopSpinner()\n\t\t\ts := \"files\"\n\t\t\tif len(analysis.RequiredChanges) == 1 {\n\t\t\t\ts = \"file\"\n\t\t\t}\n\t\t\tconfirmed, err := term.ConfirmYesNo(fmt.Sprintf(\"Revert project %s to match rewound plan state?\", s))\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting user confirmation: %v\", err)\n\t\t\t}\n\n\t\t\tshouldRevert = confirmed\n\t\t}\n\n\t\tif shouldRevert && len(analysis.RequiredChanges) > 0 {\n\t\t\tterm.StartSpinner(\"\")\n\t\t\terr := lib.ApplyRewindChanges(analysis.RequiredChanges)\n\t\t\tterm.StopSpinner()\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error restoring file state: %v\", err)\n\t\t\t}\n\n\t\t\tdidRevert = true\n\n\t\t\ts := \"files were\"\n\t\t\tif len(analysis.RequiredChanges) == 1 {\n\t\t\t\ts = \"file was\"\n\t\t\t}\n\n\t\t\tfmt.Printf(\"⏪ %d project %s reverted\\n\", len(analysis.RequiredChanges), s)\n\t\t\tfor path := range analysis.RequiredChanges {\n\t\t\t\tfmt.Printf(\" • %s\\n\", path)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t}\n\t}\n\n\tif didRevert {\n\t\tshouldCommit := false\n\t\tneedsPrompt := true\n\n\t\tif !fs.ProjectRootIsGitRepo() {\n\t\t\tshouldCommit = false\n\t\t\tneedsPrompt = false\n\t\t} else {\n\t\t\tif cmd.Flags().Changed(\"commit\") || cmd.Flags().Changed(\"skip-commit\") {\n\t\t\t\tif skipCommit {\n\t\t\t\t\tshouldCommit = false\n\t\t\t\t} else {\n\t\t\t\t\tshouldCommit = autoCommit\n\t\t\t\t}\n\t\t\t\tneedsPrompt = false\n\t\t\t} else {\n\t\t\t\tif config == nil {\n\t\t\t\t\tconfig, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)\n\t\t\t\t\tif apiErr != nil {\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error getting plan config: %v\", apiErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tshouldCommit = config.AutoCommit\n\t\t\t\tneedsPrompt = false\n\t\t\t}\n\t\t}\n\n\t\tif needsPrompt {\n\t\t\tterm.StopSpinner()\n\t\t\tconfirmed, err := term.ConfirmYesNo(\"Commit changes to git?\")\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting user confirmation: %v\", err)\n\t\t\t}\n\t\t\tshouldCommit = confirmed\n\t\t}\n\n\t\tif shouldCommit {\n\t\t\tmsg := \"🤖 Plandex → rewound plan state and reverted these changes:\"\n\t\t\tfor _, apply := range undonePlanApplies {\n\t\t\t\tmsg += fmt.Sprintf(\"\\n   • %s\", apply.CommitMsg)\n\t\t\t}\n\n\t\t\tpaths := []string{}\n\t\t\tfor path := range analysis.RequiredChanges {\n\t\t\t\tpaths = append(paths, path)\n\t\t\t}\n\n\t\t\terr := lib.GitAddAndCommitPaths(fs.ProjectRoot, msg, paths, true)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error committing changes: %v\", err)\n\t\t\t}\n\t\t}\n\n\t} else {\n\t\tprintNoChanges()\n\t}\n\n\tprintCmds()\n}\n\nfunc formatLogMessage(msg string) string {\n\tvar res string\n\n\t// Check for message type patterns\n\tswitch {\n\tcase strings.Contains(msg, \"User prompt\"):\n\t\tres = \"💬 \" + msg\n\tcase strings.Contains(msg, \"Plandex reply\"):\n\t\tif coins := regexp.MustCompile(`(\\d+) 🪙`).FindStringSubmatch(msg); len(coins) >= 2 {\n\t\t\tres = \"🤖 AI Response | \" + coins[1] + \" 🪙\"\n\t\t}\n\t\tres = \"🤖 \" + msg\n\tcase strings.Contains(msg, \"Build pending\"):\n\t\tres = \"🏗️  Building changes\"\n\tcase strings.Contains(msg, \"Loaded\"):\n\t\tres = \"📚 \" + msg\n\tdefault:\n\t\tres = msg\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "app/cli/cmd/rm.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar contextRmCmd = &cobra.Command{\n\tUse:     \"rm\",\n\tAliases: []string{\"remove\", \"unload\"},\n\tShort:   \"Remove context\",\n\tLong: `Remove context by index, range, name, or glob.\n\t\n\tplandex rm 1 # Remove by index in the 'plandex ls' list\n\tplandex rm 1-3\n\tplandex rm some-file.ts\n\tplandex rm app/*.py\n\t`,\n\tArgs: cobra.MinimumNArgs(1),\n\tRun:  contextRm,\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\tindices := parseIndices(args)\n\n\tfor i, context := range contexts {\n\t\tif indices[i+1] {\n\t\t\tdeleteIds[context.Id] = true\n\t\t\tcontinue\n\t\t}\n\t\tfor _, id := range args {\n\t\t\tif context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n\nfunc parseIndices(args []string) map[int]bool {\n\tindices := map[int]bool{}\n\tfor _, arg := range args {\n\t\tif strings.Contains(arg, \"-\") {\n\t\t\tparts := strings.Split(arg, \"-\")\n\t\t\tstart, err1 := strconv.Atoi(parts[0])\n\t\t\tend, err2 := strconv.Atoi(parts[1])\n\t\t\tif err1 == nil && err2 == nil && start <= end {\n\t\t\t\tfor i := start; i <= end; i++ {\n\t\t\t\t\tindices[i] = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tindex, err := strconv.Atoi(arg)\n\t\t\tif err == nil {\n\t\t\t\tindices[index] = true\n\t\t\t}\n\t\t}\n\t}\n\treturn indices\n}\n"
  },
  {
    "path": "app/cli/cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/term\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar helpShowAll bool\n\n// RootCmd represents the base command when called without any subcommands\nvar RootCmd = &cobra.Command{\n\tUse: `plandex [command] [flags]`,\n\t// Short: \"Plandex: iterative development with AI\",\n\tSilenceErrors: true,\n\tSilenceUsage:  true,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\trun(cmd, args)\n\t},\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\t// if no arguments were passed, start the repl\n\tif len(os.Args) == 1 ||\n\t\t(len(os.Args) == 2 && strings.HasPrefix(os.Args[1], \"--\") && os.Args[1] != \"--help\") ||\n\t\t(len(os.Args) == 3 && strings.HasPrefix(os.Args[1], \"--\") && os.Args[1] != \"--help\" && strings.HasPrefix(os.Args[2], \"--\") && os.Args[2] != \"--help\") {\n\n\t\t// Instead of directly calling replCmd.Run, parse the flags first\n\t\treplCmd.ParseFlags(os.Args[1:])\n\t\treplCmd.Run(replCmd, []string{})\n\t\treturn\n\t}\n\n\tif err := RootCmd.Execute(); err != nil {\n\t\t// term.OutputErrorAndExit(\"Error executing root command: %v\", err)\n\t\t// log.Fatalf(\"Error executing root command: %v\", err)\n\n\t\t// output the error message to stderr\n\t\tterm.OutputSimpleError(\"Error: %v\", err)\n\n\t\tfmt.Println()\n\n\t\tcolor.New(color.Bold, color.BgGreen, color.FgHiWhite).Println(\" Usage \")\n\t\tcolor.New(color.Bold).Println(\"  plandex [command] [flags]\")\n\t\tcolor.New(color.Bold).Println(\"  pdx [command] [flags]\")\n\t\tfmt.Println()\n\n\t\tcolor.New(color.Bold, color.BgGreen, color.FgHiWhite).Println(\" Help \")\n\t\tcolor.New(color.Bold).Println(\"  plandex help # show basic usage\")\n\t\tcolor.New(color.Bold).Println(\"  plandex help --all # show all commands\")\n\t\tcolor.New(color.Bold).Println(\"  plandex [command] --help\")\n\t\tfmt.Println()\n\n\t\tcolor.New(color.Bold, color.BgGreen, color.FgHiWhite).Println(\" Common Commands \")\n\t\tcolor.New(color.Bold).Println(\"  plandex new # create a new plan\")\n\t\tcolor.New(color.Bold).Println(\"  plandex tell # tell the plan what to do\")\n\t\tcolor.New(color.Bold).Println(\"  plandex continue # continue the current plan\")\n\t\tcolor.New(color.Bold).Println(\"  plandex settings # show plan settings\")\n\t\tcolor.New(color.Bold).Println(\"  plandex set # update plan settings\")\n\t\tfmt.Println()\n\n\t\tos.Exit(1)\n\t}\n}\n\nfunc run(cmd *cobra.Command, args []string) {\n\n}\n\nfunc init() {\n\tvar helpCmd = &cobra.Command{\n\t\tUse:     \"help\",\n\t\tAliases: []string{\"h\"},\n\t\tShort:   \"Display help for Plandex\",\n\t\tLong:    `Display help for Plandex.`,\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tterm.PrintCustomHelp(helpShowAll)\n\t\t},\n\t}\n\n\tRootCmd.AddCommand(helpCmd)\n\tRootCmd.AddCommand(connectClaudeCmd)\n\tRootCmd.AddCommand(disconnectClaudeCmd)\n\n\t// add an --all/-a flag\n\thelpCmd.Flags().BoolVarP(&helpShowAll, \"all\", \"a\", false, \"Show all commands\")\n}\n"
  },
  {
    "path": "app/cli/cmd/set_config.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc init() {\n\tRootCmd.AddCommand(setConfigCmd)\n\tsetConfigCmd.AddCommand(defaultSetConfigCmd)\n\tRootCmd.AddCommand(setAutoCmd)\n\tsetAutoCmd.AddCommand(setAutoDefaultCmd)\n}\n\nvar setConfigCmd = &cobra.Command{\n\tUse:   \"set-config [setting] [value]\",\n\tShort: \"Update current plan config\",\n\tRun:   setConfig,\n\tArgs:  cobra.MaximumNArgs(2),\n}\n\nvar defaultSetConfigCmd = &cobra.Command{\n\tUse:   \"default [setting] [value]\",\n\tShort: \"Update default plan config\",\n\tRun:   defaultSetConfig,\n\tArgs:  cobra.MaximumNArgs(2),\n}\n\nvar setAutoCmd = &cobra.Command{\n\tUse:   \"set-auto [value]\",\n\tShort: \"Update config auto-mode\",\n\tRun:   setAuto,\n\tArgs:  cobra.MaximumNArgs(1),\n}\n\nvar setAutoDefaultCmd = &cobra.Command{\n\tUse:   \"default [value]\",\n\tShort: \"Update default config auto-mode\",\n\tRun:   setAutoDefault,\n\tArgs:  cobra.MaximumNArgs(1),\n}\n\nfunc setAuto(cmd *cobra.Command, args []string) {\n\tsetConfig(cmd, append([]string{\"auto-mode\"}, args...))\n}\n\nfunc setAutoDefault(cmd *cobra.Command, args []string) {\n\tdefaultSetConfig(cmd, append([]string{\"auto-mode\"}, args...))\n}\n\nfunc setConfig(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tterm.StartSpinner(\"\")\n\tconfig, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current config: %v\", apiErr)\n\t\treturn\n\t}\n\n\tif config == nil {\n\t\tconfig = &shared.PlanConfig{}\n\t}\n\n\tkey, updatedConfig := updateConfig(args, config)\n\tif updatedConfig == nil {\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tapiErr = api.Client.UpdatePlanConfig(lib.CurrentPlanId, shared.UpdatePlanConfigRequest{\n\t\tConfig: updatedConfig,\n\t})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error updating config: %v\", apiErr)\n\t\treturn\n\t}\n\n\tfmt.Println(\"✅ Config updated\")\n\tlib.ShowPlanConfig(updatedConfig, key)\n\tfmt.Println()\n\n\tloadMapIfNeeded(config, updatedConfig)\n\tremoveMapIfNeeded(config, updatedConfig)\n\n\tif !(config.AutoApply && config.AutoExec) && updatedConfig.AutoApply && updatedConfig.AutoExec {\n\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"⚠️  You enabled automatic apply and execution.\")\n\n\t\tfmt.Println()\n\t} else if !config.AutoApply && updatedConfig.AutoApply {\n\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"⚠️  You enabled automatic apply.\")\n\t\tfmt.Println()\n\t} else if !config.AutoExec && updatedConfig.AutoExec {\n\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"⚠️  You enabled automatic execution.\")\n\t\tfmt.Println()\n\t}\n\n\tterm.StopSpinner()\n\n\tterm.PrintCmds(\"\", \"config\", \"config default\", \"set-config default\")\n}\n\nfunc defaultSetConfig(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tterm.StartSpinner(\"\")\n\tconfig, apiErr := api.Client.GetDefaultPlanConfig()\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current config: %v\", apiErr)\n\t\treturn\n\t}\n\n\tif config == nil {\n\t\tconfig = &shared.PlanConfig{}\n\t}\n\n\tkey, updatedConfig := updateConfig(args, config)\n\tif updatedConfig == nil {\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tapiErr = api.Client.UpdateDefaultPlanConfig(shared.UpdateDefaultPlanConfigRequest{\n\t\tConfig: updatedConfig,\n\t})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error updating config: %v\", apiErr)\n\t\treturn\n\t}\n\n\tfmt.Println(\"✅ Default config updated\")\n\tlib.ShowPlanConfig(updatedConfig, key)\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"config default\", \"config\", \"set-config\")\n}\n\ntype sortableSetting struct {\n\tsortKey string\n\tcfg     shared.ConfigSetting\n}\n\nfunc updateConfig(args []string, originalConfig *shared.PlanConfig) (string, *shared.PlanConfig) {\n\tvar setting, value string\n\n\tif len(args) > 0 {\n\t\tsetting = strings.ToLower(strings.ReplaceAll(args[0], \"-\", \"\"))\n\t}\n\n\tif len(args) > 1 {\n\t\tvalue = args[1]\n\t}\n\n\tif setting == \"\" {\n\t\tvar sorted []sortableSetting\n\n\t\tfor key, cfg := range shared.ConfigSettingsByKey {\n\t\t\tvar sortKey string\n\t\t\tif cfg.SortKey != \"\" {\n\t\t\t\tsortKey = cfg.SortKey\n\t\t\t} else {\n\t\t\t\tsortKey = key\n\t\t\t}\n\t\t\tsorted = append(sorted, sortableSetting{sortKey, cfg})\n\t\t}\n\n\t\tslices.SortFunc(sorted, func(a, b sortableSetting) int {\n\t\t\treturn strings.Compare(a.sortKey, b.sortKey)\n\t\t})\n\n\t\tvar opts []string\n\t\tfor _, opt := range sorted {\n\t\t\topts = append(opts, fmt.Sprintf(\"%s → %s\", opt.cfg.Name, opt.cfg.Desc))\n\t\t}\n\n\t\tselection, err := term.SelectFromList(\"Choose a setting to update:\", opts)\n\t\tif err != nil {\n\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\treturn \"\", nil\n\t\t\t}\n\t\t\tterm.OutputErrorAndExit(\"Error selecting setting: %v\", err)\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\tsetting = strings.Split(selection, \" →\")[0]\n\t\tsetting = strings.ToLower(strings.ReplaceAll(setting, \"-\", \"\"))\n\t}\n\n\tconfig := *originalConfig\n\tcfgSetting, exists := shared.ConfigSettingsByKey[setting]\n\tif !exists {\n\t\tterm.OutputErrorAndExit(\"Unknown setting: %s\\n\", setting)\n\t\treturn \"\", nil\n\t}\n\n\tif value == \"\" {\n\t\tif cfgSetting.BoolSetter != nil {\n\t\t\toptions := []string{\"Enabled\", \"Disabled\"}\n\t\t\tselection, err := term.SelectFromList(fmt.Sprintf(\"Set %s:\", cfgSetting.Name), options)\n\t\t\tif err != nil {\n\t\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t}\n\t\t\t\tterm.OutputErrorAndExit(\"Error selecting value: %v\", err)\n\t\t\t\treturn \"\", nil\n\t\t\t}\n\t\t\tcfgSetting.BoolSetter(&config, selection == \"Enabled\")\n\t\t} else if cfgSetting.IntSetter != nil {\n\t\t\tvalue, err := term.GetRequiredUserStringInput(fmt.Sprintf(\"Set %s (number)\", cfgSetting.Name))\n\t\t\tif err != nil {\n\t\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t}\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting value: %v\", err)\n\t\t\t\treturn \"\", nil\n\t\t\t}\n\t\t\tn, err := strconv.Atoi(value)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Invalid number value for %s (%s)\", cfgSetting.Name, value)\n\t\t\t\treturn \"\", nil\n\t\t\t}\n\t\t\tcfgSetting.IntSetter(&config, n)\n\t\t} else if cfgSetting.StringSetter != nil {\n\t\t\tvar selection string\n\t\t\tvar err error\n\t\t\tchoices := *cfgSetting.Choices\n\t\t\tif len(choices) > 0 {\n\t\t\t\tif cfgSetting.HasCustomChoice {\n\t\t\t\t\tchoices = append(choices, \"Other\")\n\t\t\t\t}\n\t\t\t\tselection, err = term.SelectFromList(fmt.Sprintf(\"Set %s:\", cfgSetting.Name), choices)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\t\t\treturn \"\", nil\n\t\t\t\t\t}\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error selecting value: %v\", err)\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t}\n\t\t\t\tif selection == \"Other\" {\n\t\t\t\t\tselection, err = term.GetRequiredUserStringInput(fmt.Sprintf(\"Enter value for %s\", cfgSetting.Name))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\t\t\t\treturn \"\", nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error getting value: %v\", err)\n\t\t\t\t\t\treturn \"\", nil\n\t\t\t\t\t}\n\t\t\t\t} else if cfgSetting.ChoiceToKey != nil {\n\t\t\t\t\tselection = cfgSetting.ChoiceToKey(selection)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tselection, err = term.GetRequiredUserStringInput(fmt.Sprintf(\"Set %s\", cfgSetting.Name))\n\t\t\t\tif err != nil {\n\t\t\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\t\t\treturn \"\", nil\n\t\t\t\t\t}\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error getting value: %v\", err)\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tcfgSetting.StringSetter(&config, selection)\n\t\t} else if cfgSetting.EditorSetter != nil {\n\t\t\teditor := lib.SelectEditor(false)\n\t\t\tcfgSetting.EditorSetter(&config, editor.Name, editor.Cmd, editor.Args)\n\t\t}\n\t} else {\n\t\tif cfgSetting.BoolSetter != nil {\n\t\t\tb, err := parseBooleanArg(value)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Invalid value for %s (%s)\", cfgSetting.Name, value)\n\t\t\t\treturn \"\", nil\n\t\t\t}\n\t\t\tcfgSetting.BoolSetter(&config, b)\n\t\t} else if cfgSetting.IntSetter != nil {\n\t\t\tn, err := strconv.Atoi(value)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Invalid number value for %s (%s)\", cfgSetting.Name, value)\n\t\t\t\treturn \"\", nil\n\t\t\t}\n\t\t\tcfgSetting.IntSetter(&config, n)\n\t\t} else if cfgSetting.StringSetter != nil {\n\t\t\tcfgSetting.StringSetter(&config, value)\n\t\t} else if cfgSetting.EditorSetter != nil {\n\t\t\tfields := strings.Fields(value)\n\t\t\tcmd := fields[0]\n\t\t\tvar cmdArgs []string\n\t\t\tif len(fields) > 1 {\n\t\t\t\tcmdArgs = fields[1:]\n\t\t\t}\n\t\t\tcfgSetting.EditorSetter(&config, value, cmd, cmdArgs)\n\t\t}\n\t}\n\n\treturn setting, &config\n}\n\nfunc parseBooleanArg(value string) (bool, error) {\n\tswitch value {\n\tcase \"enabled\", \"true\", \"t\", \"yes\", \"y\", \"1\":\n\t\treturn true, nil\n\tcase \"disabled\", \"false\", \"f\", \"no\", \"n\", \"0\":\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, fmt.Errorf(\"invalid value: %s\", value)\n\t}\n\n}\n\nfunc loadMapIfNeeded(originalConfig, updatedConfig *shared.PlanConfig) {\n\tif updatedConfig.AutoLoadContext && !originalConfig.AutoLoadContext {\n\t\thasMap := false\n\n\t\tterm.StartSpinner(\"\")\n\t\tcontext, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\t\tif err == nil {\n\t\t\tfor _, c := range context {\n\t\t\t\tif c.ContextType == shared.ContextMapType {\n\t\t\t\t\thasMap = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !hasMap {\n\t\t\t\tlib.MustLoadAutoContextMap()\n\t\t\t\tfmt.Println()\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc removeMapIfNeeded(originalConfig, updatedConfig *shared.PlanConfig) {\n\tif originalConfig.AutoLoadContext && !updatedConfig.AutoLoadContext {\n\t\tterm.StartSpinner(\"\")\n\t\tcontext, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\t\tif err == nil {\n\t\t\tfor _, c := range context {\n\t\t\t\tif c.ContextType == shared.ContextMapType && (c.AutoLoaded || c.FilePath == \".\") {\n\t\t\t\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\t\t\t\tIds: map[string]bool{c.Id: true},\n\t\t\t\t\t})\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tfmt.Println(\"✅ \" + res.Msg)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/set_model.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar setModelUseJsonFile bool\nvar setModelJsonFilePath string\nvar setModelSave bool\n\nfunc init() {\n\tRootCmd.AddCommand(modelsSetCmd)\n\n\tmodelsSetCmd.AddCommand(defaultModelSetCmd)\n\n\tmodelsSetCmd.Flags().BoolVar(&setModelUseJsonFile, \"json\", false, \"Use a JSON file to set model settings\")\n\tmodelsSetCmd.Flags().StringVarP(&setModelJsonFilePath, \"file\", \"f\", \"\", \"Path to model settings JSON file\")\n\tmodelsSetCmd.Flags().BoolVar(&setModelSave, \"save\", false, \"Save model settings from JSON file\")\n\n\tdefaultModelSetCmd.Flags().BoolVar(&setModelUseJsonFile, \"json\", false, \"Use a JSON file to set model settings\")\n\tdefaultModelSetCmd.Flags().StringVarP(&setModelJsonFilePath, \"file\", \"f\", \"\", \"Path to model settings JSON file\")\n\tdefaultModelSetCmd.Flags().BoolVar(&setModelSave, \"save\", false, \"Save model settings from JSON file\")\n}\n\nvar modelsSetCmd = &cobra.Command{\n\tUse:     \"set-model [model-pack-name]\",\n\tAliases: []string{\"set-models\"},\n\tShort:   \"Update current plan model settings\",\n\tRun:     modelsSet,\n\tArgs:    cobra.MaximumNArgs(1),\n}\n\nvar defaultModelSetCmd = &cobra.Command{\n\tUse:   \"default [model-pack-name]\",\n\tShort: \"Update org-wide default model settings\",\n\tRun:   defaultModelsSet,\n\tArgs:  cobra.MaximumNArgs(1),\n}\n\nfunc modelsSet(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tterm.StartSpinner(\"\")\n\toriginalSettings, apiErr := api.Client.GetSettings(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current settings: %v\", apiErr)\n\t\treturn\n\t}\n\n\tdefaultPath := lib.GetPlanModelSettingsPath(lib.CurrentPlanId)\n\n\tsettings := updateModelSettings(args, originalSettings, defaultPath)\n\n\tif settings == nil {\n\t\treturn\n\t}\n\n\tres, apiErr := api.Client.UpdateSettings(\n\t\tlib.CurrentPlanId,\n\t\tlib.CurrentBranch,\n\t\tshared.UpdateSettingsRequest{\n\t\t\tModelPackName: settings.ModelPackName,\n\t\t\tModelPack:     settings.ModelPack,\n\t\t})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error updating settings: %v\", apiErr)\n\t\treturn\n\t}\n\n\tif res == nil {\n\t\treturn\n\t}\n\n\tfmt.Println(res.Msg)\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"models\", \"set-model default\", \"log\")\n}\n\nfunc defaultModelsSet(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tterm.StartSpinner(\"\")\n\toriginalSettings, apiErr := api.Client.GetOrgDefaultSettings()\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current settings: %v\", apiErr)\n\t\treturn\n\t}\n\n\tdefaultPath := lib.DefaultModelSettingsPath\n\n\tsettings := updateModelSettings(args, originalSettings, defaultPath)\n\n\tif settings == nil {\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tres, apiErr := api.Client.UpdateOrgDefaultSettings(\n\t\tshared.UpdateSettingsRequest{\n\t\t\tModelPackName: settings.ModelPackName,\n\t\t\tModelPack:     settings.ModelPack,\n\t\t})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error updating settings: %v\", apiErr)\n\t\treturn\n\t}\n\n\tif res == nil {\n\t\treturn\n\t}\n\n\tfmt.Println(res.Msg)\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"models\", \"set-model default\", \"log\")\n}\n\nfunc updateModelSettings(args []string, originalSettings *shared.PlanSettings, defaultPath string) *shared.PlanSettings {\n\tsettings, err := originalSettings.DeepCopy()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error copying settings: %v\", err)\n\t\treturn nil\n\t}\n\n\tbuiltInModelPacks := shared.BuiltInModelPacks\n\tif auth.Current.IsCloud {\n\t\tfiltered := []*shared.ModelPack{}\n\t\tfor _, ms := range builtInModelPacks {\n\t\t\tif ms.LocalProvider == \"\" {\n\t\t\t\tfiltered = append(filtered, ms)\n\t\t\t}\n\t\t}\n\t\tbuiltInModelPacks = filtered\n\t}\n\n\tvar customModelPacks []*shared.ModelPack\n\tvar defaultConfig *shared.PlanConfig\n\tvar planConfig *shared.PlanConfig\n\n\terrCh := make(chan error, 3)\n\n\tgo func() {\n\t\tvar apiErr *shared.ApiError\n\t\tcustomModelPacks, apiErr = api.Client.ListModelPacks()\n\t\tif apiErr != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting custom model packs: %v\", apiErr.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tvar apiErr *shared.ApiError\n\t\tdefaultConfig, apiErr = api.Client.GetDefaultPlanConfig()\n\t\tif apiErr != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting default config: %v\", apiErr.Msg)\n\t\t\treturn\n\t\t}\n\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tif lib.CurrentPlanId != \"\" {\n\t\t\tvar apiErr *shared.ApiError\n\t\t\tplanConfig, apiErr = api.Client.GetPlanConfig(lib.CurrentPlanId)\n\t\t\tif apiErr != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan config: %v\", apiErr.Msg)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 3; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(err.Error())\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tuseJsonFile := setModelUseJsonFile || setModelSave\n\n\tvar nameArg string\n\tif len(args) > 0 {\n\t\tnameArg = args[0]\n\t}\n\n\tif !useJsonFile {\n\t\tif nameArg == \"\" {\n\t\t\tterm.StopSpinner()\n\t\t\tconst modelPackOpt = \"Select a model pack\"\n\t\t\tconst jsonOpt = \"Edit model settings JSON\"\n\n\t\t\tselection, err := term.SelectFromList(\"Select a model pack or edit settings?\", []string{modelPackOpt, jsonOpt})\n\t\t\tif err != nil {\n\t\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif selection == modelPackOpt {\n\t\t\t\tuseJsonFile = false\n\t\t\t} else {\n\t\t\t\tuseJsonFile = true\n\t\t\t\tterm.StartSpinner(\"\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif useJsonFile {\n\t\tusingDefaultPath := false\n\t\tif setModelJsonFilePath == \"\" {\n\t\t\tusingDefaultPath = true\n\t\t\tsetModelJsonFilePath = defaultPath\n\t\t}\n\n\t\texists, err := fs.FileExists(setModelJsonFilePath)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error checking model settings file: %v\", err)\n\t\t\treturn nil\n\t\t}\n\n\t\tif setModelSave {\n\t\t\tif !exists {\n\t\t\t\tterm.OutputErrorAndExit(\"File not found: %s\", customModelsPath)\n\t\t\t}\n\t\t} else {\n\t\t\tif usingDefaultPath && exists {\n\t\t\t\tmodelSettingsCheckLocalChangesResult, err := lib.ModelSettingsCheckLocalChanges(setModelJsonFilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error checking model settings file: %v\", err)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tif modelSettingsCheckLocalChangesResult.HasLocalChanges {\n\t\t\t\t\tterm.StopSpinner()\n\n\t\t\t\t\tres, err := warnModelsFileLocalChanges(setModelJsonFilePath, \"set-model\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error confirming: %v\", err)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\tif !res {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tterm.StartSpinner(\"\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr = lib.WriteModelSettingsFile(setModelJsonFilePath, originalSettings)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error writing model settings file: %v\", err)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tterm.StopSpinner()\n\t\t\tfmt.Printf(\"🧠 %s → %s\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(\"Models file\"), setModelJsonFilePath)\n\t\t\tfmt.Println(\"👨‍💻 Edit it, then come back here to save\")\n\t\t\tfmt.Println()\n\n\t\t\tpathArg := \"\"\n\t\t\tif !usingDefaultPath {\n\t\t\t\tpathArg = fmt.Sprintf(\" --file %s\", setModelJsonFilePath)\n\t\t\t}\n\n\t\t\tres := maybePromptAndOpenModelsFile(setModelJsonFilePath, pathArg, \"set-model\", defaultConfig, planConfig)\n\t\t\tif res.shouldReturn {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tterm.StartSpinner(\"\")\n\n\t\tsettings, err = lib.ApplyModelSettings(setModelJsonFilePath, originalSettings)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error applying model settings: %v\", err)\n\t\t\treturn nil\n\t\t}\n\n\t} else {\n\t\tif nameArg == \"\" {\n\t\t\tvar names []string\n\t\t\tvar opts []string\n\t\t\tfor _, ms := range builtInModelPacks {\n\t\t\t\tnames = append(names, ms.Name)\n\t\t\t\topts = append(opts, \"Built-in | \"+ms.Name)\n\t\t\t}\n\t\t\tfor _, ms := range customModelPacks {\n\t\t\t\tnames = append(names, ms.Name)\n\t\t\t\topts = append(opts, \"Custom | \"+ms.Name)\n\t\t\t}\n\n\t\t\tterm.StopSpinner()\n\t\t\tselection, err := term.SelectFromList(\"Select a model pack:\", opts)\n\t\t\tif err != nil {\n\t\t\t\tif err.Error() == \"interrupt\" {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor i, opt := range opts {\n\t\t\t\tif opt == selection {\n\t\t\t\t\tnameArg = names[i]\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar modelPackName string\n\t\tcompare := strings.ToLower(strings.TrimSpace(nameArg))\n\t\tif compare == \"daily\" {\n\t\t\tcompare = \"daily-driver\"\n\t\t}\n\t\tif compare == \"opus-4-planner\" {\n\t\t\tcompare = \"opus-planner\"\n\t\t}\n\n\t\tfor _, ms := range builtInModelPacks {\n\t\t\tif strings.EqualFold(ms.Name, compare) {\n\t\t\t\tmodelPackName = ms.Name\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor _, ms := range customModelPacks {\n\t\t\tif strings.EqualFold(ms.Name, compare) {\n\t\t\t\tmodelPackName = ms.Name\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif modelPackName == \"\" {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputSimpleError(\"No model pack found with name '%s'\", nameArg)\n\t\t\tfmt.Println()\n\t\t\tterm.PrintCmds(\"\", \"model-packs\")\n\t\t\tos.Exit(1)\n\t\t\treturn nil\n\t\t}\n\n\t\tsettings.SetModelPackByName(modelPackName)\n\n\t\t// clear the default settings file and hash file if they exist, ignoring errors\n\t\tos.Remove(defaultPath)\n\t\tos.Remove(defaultPath + \".hash\")\n\t}\n\n\tterm.StopSpinner()\n\n\tif originalSettings.Equals(settings) {\n\t\tfmt.Println(\"🤷‍♂️ No model settings were updated\")\n\t\treturn nil\n\t} else {\n\t\treturn settings\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/sign_in.go",
    "content": "package cmd\n\nimport (\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar pin string\n\nvar signInCmd = &cobra.Command{\n\tUse:   \"sign-in\",\n\tShort: \"Sign in to a Plandex account\",\n\tArgs:  cobra.NoArgs,\n\tRun:   signIn,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(signInCmd)\n\n\tsignInCmd.Flags().StringVar(&pin, \"pin\", \"\", \"Sign in with a pin from the Plandex Cloud web UI\")\n}\n\nfunc signIn(cmd *cobra.Command, args []string) {\n\tif pin != \"\" {\n\t\terr := auth.SignInWithCode(pin, \"\")\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error signing in: %v\", err)\n\t\t}\n\n\t\treturn\n\t}\n\n\terr := auth.SelectOrSignInOrCreate()\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error signing in: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "app/cli/cmd/stop.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar stopCmd = &cobra.Command{\n\tUse:   \"stop [stream-id-or-plan] [branch]\",\n\tShort: \"Connect to an active stream\",\n\t// Long:  ``,\n\tArgs: cobra.MaximumNArgs(2),\n\tRun:  stop,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(stopCmd)\n}\n\nfunc stop(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tterm.OutputNoCurrentPlanErrorAndExit()\n\t}\n\n\tplanId, branch, shouldContinue := lib.SelectActiveStream(args)\n\n\tif !shouldContinue {\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tapiErr := api.Client.StopPlan(context.Background(), planId, branch)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error stopping stream: %v\", apiErr.Msg)\n\t}\n\n\tfmt.Println(\"✅ Plan stream stopped\")\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"convo\", \"log\")\n\n}\n"
  },
  {
    "path": "app/cli/cmd/summary.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar summaryPlain bool\n\nvar statusCmd = &cobra.Command{\n\tUse:   \"summary\",\n\tShort: \"Show the latest summary of the current plan\",\n\tRun:   status,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(statusCmd)\n\n\tstatusCmd.Flags().BoolVarP(&summaryPlain, \"plain\", \"p\", false, \"Output summary in plain text with no ANSI codes\")\n}\n\nfunc status(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tterm.StartSpinner(\"\")\n\tstatus, apiErr := api.Client.GetPlanStatus(lib.CurrentPlanId, lib.CurrentBranch)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error loading conversation: %v\", apiErr.Msg)\n\t}\n\n\tif status == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No summary available\")\n\t}\n\n\tif summaryPlain {\n\t\tfmt.Println(status)\n\t\treturn\n\t}\n\n\tmd, err := term.GetMarkdown(status)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error formatting markdown: %v\", err)\n\t}\n\n\tfmt.Println(md)\n}\n"
  },
  {
    "path": "app/cli/cmd/tell.go",
    "content": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/plan_exec\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar isImplementationOfChat bool\n\n// tellCmd represents the prompt command\nvar tellCmd = &cobra.Command{\n\tUse:     \"tell [prompt]\",\n\tAliases: []string{\"t\"},\n\tShort:   \"Send a prompt for the current plan\",\n\t// Long:  ``,\n\tArgs: cobra.RangeArgs(0, 1),\n\tRun:  doTell,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(tellCmd)\n\n\tinitExecFlags(tellCmd, initExecFlagsParams{})\n\n\ttellCmd.Flags().BoolVar(&isImplementationOfChat, \"from-chat\", false, \"Begin implementation based on conversation so far\")\n}\n\nfunc doTell(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\tmustSetPlanExecFlags(cmd, false)\n\n\tif isImplementationOfChat && len(args) > 0 {\n\t\tterm.OutputErrorAndExit(\"Error: --from-chat cannot be used with a prompt\")\n\t}\n\n\tvar prompt string\n\tif !isImplementationOfChat {\n\t\tprompt = getTellPrompt(args)\n\n\t\tif prompt == \"\" {\n\t\t\tfmt.Println(\"🤷‍♂️ No prompt to send\")\n\t\t\treturn\n\t\t}\n\t}\n\n\ttellFlags := types.TellFlags{\n\t\tTellBg:                 tellBg,\n\t\tTellStop:               tellStop,\n\t\tTellNoBuild:            tellNoBuild,\n\t\tAutoContext:            tellAutoContext,\n\t\tSmartContext:           tellSmartContext,\n\t\tExecEnabled:            !noExec,\n\t\tAutoApply:              tellAutoApply,\n\t\tIsImplementationOfChat: isImplementationOfChat,\n\t\tSkipChangesMenu:        tellSkipMenu,\n\t}\n\n\tplan_exec.TellPlan(plan_exec.ExecParams{\n\t\tCurrentPlanId: lib.CurrentPlanId,\n\t\tCurrentBranch: lib.CurrentBranch,\n\t\tAuthVars:      lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode),\n\t\tCheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {\n\t\t\tauto := autoConfirm || tellAutoApply || tellAutoContext\n\t\t\treturn lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)\n\t\t},\n\t}, prompt, tellFlags)\n\n\tif tellAutoApply {\n\t\tapplyFlags := types.ApplyFlags{\n\t\t\tAutoConfirm: true,\n\t\t\tAutoCommit:  autoCommit,\n\t\t\tNoCommit:    !autoCommit,\n\t\t\tNoExec:      noExec,\n\t\t\tAutoExec:    autoExec || autoDebug > 0,\n\t\t\tAutoDebug:   autoDebug,\n\t\t}\n\n\t\tlib.MustApplyPlan(lib.ApplyPlanParams{\n\t\t\tPlanId:     lib.CurrentPlanId,\n\t\t\tBranch:     lib.CurrentBranch,\n\t\t\tApplyFlags: applyFlags,\n\t\t\tTellFlags:  tellFlags,\n\t\t\tOnExecFail: plan_exec.GetOnApplyExecFail(applyFlags, tellFlags),\n\t\t})\n\t}\n}\n\nfunc getTellPrompt(args []string) string {\n\tvar prompt string\n\tvar pipedData string\n\n\tif len(args) > 0 {\n\t\tprompt = args[0]\n\t} else if tellPromptFile != \"\" {\n\t\tbytes, err := os.ReadFile(tellPromptFile)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error reading prompt file: %v\", err)\n\t\t}\n\t\tprompt = string(bytes)\n\t}\n\n\t// Check if there's piped input\n\tfileInfo, err := os.Stdin.Stat()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to stat stdin: %v\", err)\n\t}\n\n\tif fileInfo.Mode()&os.ModeNamedPipe != 0 {\n\t\treader := bufio.NewReader(os.Stdin)\n\t\tpipedDataBytes, err := io.ReadAll(reader)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Failed to read piped data: %v\", err)\n\t\t}\n\t\tpipedData = string(pipedDataBytes)\n\t}\n\n\tif prompt == \"\" && pipedData == \"\" {\n\t\tprompt = getEditorPrompt()\n\t} else if pipedData != \"\" {\n\t\tif prompt != \"\" {\n\t\t\tprompt = fmt.Sprintf(\"%s\\n\\n---\\n\\n%s\", prompt, pipedData)\n\t\t} else {\n\t\t\tprompt = pipedData\n\t\t}\n\t}\n\n\treturn prompt\n}\n\nfunc prepareEditorCommand(editor string, filename string) *exec.Cmd {\n\tswitch editor {\n\tcase \"vim\":\n\t\treturn exec.Command(editor, \"+normal G$\", \"+startinsert!\", filename)\n\tcase \"nano\":\n\t\treturn exec.Command(editor, \"+99999999\", filename)\n\tdefault:\n\t\treturn exec.Command(editor, filename)\n\t}\n}\n\nfunc getEditorInstructions() string {\n\tif editor == EditorTypeVim {\n\t\treturn \"👉  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\"\n\t}\n\n\tif editor == EditorTypeNano {\n\t\treturn \"👉  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\"\n\t}\n\n\treturn \"👉  Write your prompt below, then save and exit to send it to Plandex.\\n\\n\\n\"\n}\n\nfunc getEditorPrompt() string {\n\ttempFile, err := os.CreateTemp(os.TempDir(), \"plandex_prompt_*\")\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to create temporary file: %v\", err)\n\t}\n\n\tinstructions := getEditorInstructions()\n\tfilename := tempFile.Name()\n\terr = os.WriteFile(filename, []byte(instructions), 0644)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to write instructions to temporary file: %v\", err)\n\t}\n\n\teditorCmd := prepareEditorCommand(editor, filename)\n\teditorCmd.Stdin = os.Stdin\n\teditorCmd.Stdout = os.Stdout\n\teditorCmd.Stderr = os.Stderr\n\terr = editorCmd.Run()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error opening editor: %v\", err)\n\t}\n\n\tbytes, err := os.ReadFile(tempFile.Name())\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error reading temporary file: %v\", err)\n\t}\n\n\tprompt := string(bytes)\n\n\terr = os.Remove(tempFile.Name())\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error removing temporary file: %v\", err)\n\t}\n\n\tprompt = strings.TrimPrefix(prompt, strings.TrimSpace(instructions))\n\tprompt = strings.TrimSpace(prompt)\n\n\treturn prompt\n\n}\n\n// func maybeShowDiffs() {\n// \tdiffs, err := api.Client.GetPlanDiffs(lib.CurrentPlanId, lib.CurrentBranch, plainTextOutput || showDiffUi)\n// \tif err != nil {\n// \t\tterm.OutputErrorAndExit(\"Error getting plan diffs: %v\", err)\n// \t\treturn\n// \t}\n\n// \tif len(diffs) > 0 {\n// \t\tcmd := exec.Command(os.Args[0], \"diffs\", \"--ui\")\n\n// \t\t// Create a context that's cancelled when the program exits\n// \t\tctx, cancel := context.WithCancel(context.Background())\n\n// \t\t// Ensure cleanup on program exit\n// \t\tgo func() {\n// \t\t\t// Wait for program exit signal\n// \t\t\tc := make(chan os.Signal, 1)\n// \t\t\tsignal.Notify(c, os.Interrupt, syscall.SIGTERM)\n// \t\t\t<-c\n\n// \t\t\t// Cancel context and kill the process\n// \t\t\tcancel()\n// \t\t\tif cmd.Process != nil {\n// \t\t\t\tcmd.Process.Kill()\n// \t\t\t}\n// \t\t}()\n\n// \t\tgo func() {\n// \t\t\tif err := cmd.Start(); err != nil {\n// \t\t\t\tfmt.Fprintf(os.Stderr, \"Error starting diffs command: %v\\n\", err)\n// \t\t\t\treturn\n// \t\t\t}\n\n// \t\t\t// Wait in a separate goroutine\n// \t\t\tgo cmd.Wait()\n\n// \t\t\t// Wait for either context cancellation or process completion\n// \t\t\t<-ctx.Done()\n// \t\t\tif cmd.Process != nil {\n// \t\t\t\tcmd.Process.Kill()\n// \t\t\t}\n// \t\t}()\n\n// \t\t// Give the UI a moment to start\n// \t\ttime.Sleep(100 * time.Millisecond)\n// \t}\n// }\n"
  },
  {
    "path": "app/cli/cmd/unarchive.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar unarchiveCmd = &cobra.Command{\n\tUse:     \"unarchive [name-or-index]\",\n\tAliases: []string{\"unarc\"},\n\tShort:   \"Unarchive a plan\",\n\tArgs:    cobra.MaximumNArgs(1),\n\tRun:     unarchive,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(unarchiveCmd)\n}\n\nfunc unarchive(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tvar nameOrIdx string\n\tif len(args) > 0 {\n\t\tnameOrIdx = strings.TrimSpace(args[0])\n\t}\n\n\tvar plan *shared.Plan\n\n\tterm.StartSpinner(\"\")\n\tplans, apiErr := api.Client.ListArchivedPlans([]string{lib.CurrentProjectId})\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting archived plans: %v\", apiErr)\n\t}\n\n\tif len(plans) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No archived plans\")\n\t\treturn\n\t}\n\n\tif nameOrIdx == \"\" {\n\t\topts := make([]string, len(plans))\n\t\tfor i, p := range plans {\n\t\t\topts[i] = p.Name\n\t\t}\n\n\t\tselected, err := term.SelectFromList(\"Select a plan:\", opts)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error selecting plan: %v\", err)\n\t\t}\n\n\t\tfor _, p := range plans {\n\t\t\tif p.Name == selected {\n\t\t\t\tplan = p\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tidx, err := strconv.Atoi(nameOrIdx)\n\t\tif err == nil && idx > 0 && idx <= len(plans) {\n\t\t\tplan = plans[idx-1]\n\t\t} else {\n\t\t\tfor _, p := range plans {\n\t\t\t\tif p.Name == nameOrIdx {\n\t\t\t\t\tplan = p\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif plan == nil {\n\t\tterm.OutputErrorAndExit(\"Plan not found\")\n\t}\n\n\terr := api.Client.UnarchivePlan(plan.Id)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error unarchiving plan: %v\", err)\n\t}\n\n\tfmt.Printf(\"✅ Plan %s unarchived\\n\", color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name))\n\n\tfmt.Println()\n\tterm.PrintCmds(\"\", \"plans\", \"current\")\n}\n"
  },
  {
    "path": "app/cli/cmd/update.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar updateCmd = &cobra.Command{\n\tUse:     \"update \",\n\tAliases: []string{\"u\"},\n\tShort:   \"Update outdated context\",\n\tArgs:    cobra.MaximumNArgs(1),\n\tRun:     update,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(updateCmd)\n\n}\n\nfunc update(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tterm.StartSpinner(\"\")\n\n\tcontexts, apiErr := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif apiErr != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"failed to list context: %s\", apiErr)\n\t}\n\n\tpaths, err := fs.GetProjectPaths(fs.ProjectRoot)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error getting project paths: %v\", err)\n\t}\n\n\toutdated, err := lib.CheckOutdatedContext(contexts, paths)\n\n\tif err != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"failed to check outdated context: %s\", err)\n\t}\n\n\tif len(outdated.UpdatedContexts) == 0 {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"✅ Context is up to date\")\n\t\treturn\n\t}\n\n\tlib.UpdateContextWithOutput(lib.UpdateContextParams{\n\t\tContexts:    contexts,\n\t\tOutdatedRes: *outdated,\n\t\tReqFn:       outdated.ReqFn,\n\t})\n}\n"
  },
  {
    "path": "app/cli/cmd/usage.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\tshared \"plandex-shared\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/eiannone/keyboard\"\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/shopspring/decimal\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst MaxCreditsLogPageSize = 500\n\nvar logCreditsPageSize int\nvar logCreditsPage int\n\nvar logCreditsDebitsOnly bool\nvar logCreditsCreditsOnly bool\n\nvar showUsageLog bool\n\nvar creditsSession bool\nvar creditsToday bool\nvar creditsMonth bool\nvar creditsCurrentPlan bool\n\nvar usageCmd = &cobra.Command{\n\tUse:   \"usage\",\n\tShort: \"Display credits balance and usage report\",\n\tRun:   usage,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(usageCmd)\n\n\tusageCmd.Flags().BoolVar(&showUsageLog, \"log\", false, \"Show usage log\")\n\tusageCmd.Flags().IntVarP(&logCreditsPageSize, \"page-size\", \"s\", 100, \"Number of transactions to display per page\")\n\tusageCmd.Flags().IntVarP(&logCreditsPage, \"page\", \"p\", 1, \"Page number to display\")\n\tusageCmd.Flags().BoolVar(&logCreditsDebitsOnly, \"debits\", false, \"Show only debits in the log\")\n\tusageCmd.Flags().BoolVar(&logCreditsCreditsOnly, \"purchases\", false, \"Show only purchases in the log\")\n\n\tusageCmd.Flags().BoolVar(&creditsToday, \"today\", false, \"Show usage for today\")\n\tusageCmd.Flags().BoolVar(&creditsMonth, \"month\", false, \"Show usage for current billing month\")\n\tusageCmd.Flags().BoolVar(&creditsCurrentPlan, \"plan\", false, \"Show usage for the current plan\")\n}\n\nfunc usage(cmd *cobra.Command, args []string) {\n\tif showUsageLog {\n\t\tshowLog(cmd, args)\n\t} else {\n\t\tshowUsage()\n\t}\n}\n\nfunc showUsage() {\n\tauth.MustResolveAuthWithOrg()\n\n\tterm.StartSpinner(\"\")\n\n\tif !(creditsSession || creditsToday || creditsMonth || creditsCurrentPlan) {\n\t\tif os.Getenv(\"PLANDEX_REPL_SESSION_ID\") != \"\" {\n\t\t\tcreditsSession = true\n\t\t} else {\n\t\t\tcreditsToday = true\n\t\t}\n\t}\n\n\tvar sessionId string\n\tif creditsSession {\n\t\tsessionId = os.Getenv(\"PLANDEX_REPL_SESSION_ID\")\n\t\tif sessionId == \"\" {\n\t\t\tterm.OutputErrorAndExit(\"Session ID is not set. The --session flag should be used in the Plandex REPL.\")\n\t\t}\n\t}\n\n\tvar dayStart *time.Time\n\tif creditsToday {\n\t\tnow := time.Now()\n\t\tmidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())\n\t\tdayStart = &midnight\n\t}\n\n\tvar planId string\n\tvar currentPlanName string\n\tif creditsCurrentPlan {\n\t\tlib.MustResolveProject()\n\t\tplanId = lib.CurrentPlanId\n\t\tplan, apiErr := api.Client.GetPlan(planId)\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error getting plan: %v\", apiErr)\n\t\t}\n\t\tcurrentPlanName = plan.Name\n\t}\n\n\treq := shared.CreditsLogRequest{\n\t\tSessionId: sessionId,\n\t\tDayStart:  dayStart,\n\t\tMonth:     creditsMonth,\n\t\tPlanId:    planId,\n\t}\n\n\tres, apiErr := api.Client.GetCreditsSummary(req)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting credits summary: %v\", apiErr)\n\t}\n\n\tbuilder := strings.Builder{}\n\n\tbalance := res.Balance\n\tbalanceStr := formatSpend(balance)\n\n\tspendLbl := \"💸 Spent\"\n\tif creditsSession {\n\t\tspendLbl += \" This Session\"\n\t} else if creditsToday {\n\t\tspendLbl += \" Today\"\n\t} else if creditsMonth {\n\t\tspendLbl += \" This Billing Month\"\n\t\tspendLbl += fmt.Sprintf(\" (since %s)\", res.MonthStart.Format(\"Jan 2\"))\n\t} else if creditsCurrentPlan {\n\t\tspendLbl += fmt.Sprintf(\" On Plan 📋 %s\", currentPlanName)\n\t}\n\n\tvar spendStr string\n\tif res.TotalSpend.IsZero() {\n\t\tspendStr = \"$0.00\"\n\t} else {\n\t\tspendStr = formatSpend(res.TotalSpend)\n\t}\n\n\ttable := tablewriter.NewWriter(&builder)\n\ttable.SetAutoWrapText(false)\n\ttable.SetHeader([]string{\"💰 Current Balance\", spendLbl})\n\ttable.Append([]string{balanceStr, spendStr})\n\ttable.Render()\n\tfmt.Fprintln(&builder)\n\n\tif !res.CacheSavings.IsZero() {\n\t\ttable := tablewriter.NewWriter(&builder)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"🎯 Cache Savings\"})\n\t\ttable.Append([]string{formatSpend(res.CacheSavings)})\n\t\ttable.Render()\n\t\tfmt.Fprintln(&builder)\n\t}\n\n\tamountByStr := map[string]float64{}\n\tif len(res.ByPlanId) > 0 {\n\t\tif !creditsCurrentPlan {\n\t\t\ttable := tablewriter.NewWriter(&builder)\n\t\t\ttable.SetAutoWrapText(false)\n\t\t\ttable.SetHeader([]string{\"📋 Plan\", \"💸 Spent\"})\n\n\t\t\trows := [][]string{}\n\n\t\t\tfor id, spend := range res.ByPlanId {\n\t\t\t\tname := res.PlanNamesById[id]\n\t\t\t\tspendStr := formatSpend(spend)\n\t\t\t\trows = append(rows, []string{name, spendStr})\n\t\t\t\tamountByStr[spendStr] = spend.InexactFloat64()\n\t\t\t}\n\t\t\tsort.Slice(rows, func(i, j int) bool {\n\t\t\t\treturn amountByStr[rows[i][1]] > amountByStr[rows[j][1]]\n\t\t\t})\n\t\t\tfor _, row := range rows {\n\t\t\t\ttable.Append(row)\n\t\t\t}\n\n\t\t\ttable.Render()\n\t\t\tfmt.Fprintln(&builder)\n\t\t}\n\t}\n\n\tif len(res.ByPurpose) > 0 {\n\t\ttable = tablewriter.NewWriter(&builder)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"⚡️ Purpose\", \"💸 Spent\"})\n\n\t\trows := [][]string{}\n\t\tfor name, spend := range res.ByPurpose {\n\t\t\tspendStr := formatSpend(spend)\n\t\t\trows = append(rows, []string{name, spendStr})\n\t\t\tamountByStr[spendStr] = spend.InexactFloat64()\n\t\t}\n\t\tsort.Slice(rows, func(i, j int) bool {\n\t\t\treturn amountByStr[rows[i][1]] > amountByStr[rows[j][1]]\n\t\t})\n\t\tfor _, row := range rows {\n\t\t\ttable.Append(row)\n\t\t}\n\n\t\ttable.Render()\n\t\tfmt.Fprintln(&builder)\n\t}\n\n\tif len(res.ByModelName) > 0 {\n\t\ttable = tablewriter.NewWriter(&builder)\n\t\ttable.SetAutoWrapText(false)\n\t\ttable.SetHeader([]string{\"🤖 Model\", \"💸 Spent\"})\n\n\t\trows := [][]string{}\n\t\tfor name, spend := range res.ByModelName {\n\t\t\tspendStr := formatSpend(spend)\n\t\t\trows = append(rows, []string{name, spendStr})\n\t\t\tamountByStr[spendStr] = spend.InexactFloat64()\n\t\t}\n\t\tsort.Slice(rows, func(i, j int) bool {\n\t\t\treturn amountByStr[rows[i][1]] > amountByStr[rows[j][1]]\n\t\t})\n\t\tfor _, row := range rows {\n\t\t\ttable.Append(row)\n\t\t}\n\t\ttable.Render()\n\t\tfmt.Fprintln(&builder)\n\t}\n\n\tterm.PageOutput(builder.String())\n\n\tterm.PrintCmds(\"\", \"usage\", \"billing\")\n}\n\nfunc showLog(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tif !(creditsSession || creditsToday || creditsMonth || creditsCurrentPlan) {\n\t\tif os.Getenv(\"PLANDEX_REPL_SESSION_ID\") != \"\" {\n\t\t\tcreditsSession = true\n\t\t} else {\n\t\t\tcreditsToday = true\n\t\t}\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tvar transactionType shared.CreditsTransactionType\n\n\tif logCreditsDebitsOnly {\n\t\ttransactionType = shared.CreditsTransactionTypeDebit\n\t} else if logCreditsCreditsOnly {\n\t\ttransactionType = shared.CreditsTransactionTypeCredit\n\t}\n\n\tvar sessionId string\n\tif creditsSession {\n\t\tsessionId = os.Getenv(\"PLANDEX_REPL_SESSION_ID\")\n\t\tif sessionId == \"\" {\n\t\t\tterm.OutputErrorAndExit(\"Session ID is not set. The --session flag should be used in the Plandex REPL.\")\n\t\t}\n\t}\n\n\tvar dayStart *time.Time\n\tif creditsToday {\n\t\tnow := time.Now()\n\t\tmidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())\n\t\tdayStart = &midnight\n\t}\n\n\tvar planId string\n\tvar planName string\n\tif creditsCurrentPlan {\n\t\tlib.MustResolveProject()\n\t\tplanId = lib.CurrentPlanId\n\n\t\tplan, err := api.Client.GetPlan(planId)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error getting plan: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tplanName = plan.Name\n\t}\n\n\treq := shared.CreditsLogRequest{\n\t\tTransactionType: transactionType,\n\t\tSessionId:       sessionId,\n\t\tDayStart:        dayStart,\n\t\tMonth:           creditsMonth,\n\t\tPlanId:          planId,\n\t}\n\n\tres, apiErr := api.Client.GetCreditsTransactions(logCreditsPageSize, logCreditsPage, req)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting credits transactions: %v\", apiErr)\n\t\treturn\n\t}\n\n\ttransactions := res.Transactions\n\n\tif len(transactions) == 0 {\n\t\tlbl := \"🤷‍♂️ No usage\"\n\t\tif sessionId != \"\" {\n\t\t\tlbl = \"🤷‍♂️ No usage so far this session\"\n\t\t} else if creditsToday {\n\t\t\ttz, _ := time.Now().Zone()\n\t\t\tlbl = fmt.Sprintf(\"🤷‍♂️ No usage so far today (since midnight %s)\", tz)\n\t\t} else if creditsMonth {\n\t\t\tlbl = fmt.Sprintf(\"🤷‍♂️ No usage so far this billing month (since %s)\", res.MonthStart.Format(\"Jan 2\"))\n\t\t} else if creditsCurrentPlan {\n\t\t\tlbl = \"🤷‍♂️ No usage so far for current plan 👉 \" + planName\n\t\t}\n\t\tfmt.Println(lbl)\n\t\treturn\n\t}\n\n\ttableString := &strings.Builder{}\n\ttable := tablewriter.NewWriter(tableString)\n\ttable.SetAutoWrapText(false)\n\ttable.SetHeader([]string{\"Amount\", \"Balance\", \"Transaction\"})\n\n\tfor _, transaction := range transactions {\n\t\tvar sign string\n\t\tvar c color.Attribute\n\n\t\tdesc := transaction.CreatedAt.Local().Format(\"2006-01-02 15:04:05.000 EST\") + \"\\n\"\n\n\t\tif transaction.TransactionType == \"debit\" {\n\t\t\tsign = \"-\"\n\t\t\tc = term.ColorHiRed\n\n\t\t\tif transaction.DebitPlanId != nil {\n\t\t\t\tplanName := res.PlanNamesById[*transaction.DebitPlanId]\n\t\t\t\tdesc += fmt.Sprintf(\"Plan → %s\\n\", planName)\n\t\t\t}\n\n\t\t\tsurchargePct := transaction.DebitSurcharge.Div(*transaction.DebitBaseAmount)\n\n\t\t\tinputPrice := transaction.DebitModelInputPricePerToken.Mul(decimal.NewFromInt(1000000)).Mul(surchargePct.Add(decimal.NewFromInt(1))).StringFixed(4)\n\t\t\toutputPrice := transaction.DebitModelOutputPricePerToken.Mul(decimal.NewFromInt(1000000)).Mul(surchargePct.Add(decimal.NewFromInt(1))).StringFixed(4)\n\n\t\t\tvar cacheDiscountStr string\n\t\t\tvar cacheDiscountPct float64\n\t\t\tif transaction.DebitCacheDiscount != nil {\n\t\t\t\tcacheDiscountStr = transaction.DebitCacheDiscount.StringFixed(4)\n\t\t\t\ttotalAmount := transaction.DebitBaseAmount.Add(*transaction.DebitCacheDiscount)\n\t\t\t\tcacheDiscountPct = transaction.DebitCacheDiscount.Div(totalAmount).Mul(decimal.NewFromInt(100)).InexactFloat64()\n\t\t\t}\n\n\t\t\tfor i := 0; i < 2; i++ {\n\t\t\t\tinputPrice = strings.TrimSuffix(inputPrice, \"0\")\n\t\t\t\toutputPrice = strings.TrimSuffix(outputPrice, \"0\")\n\t\t\t\tcacheDiscountStr = strings.TrimSuffix(cacheDiscountStr, \"0\")\n\t\t\t}\n\n\t\t\tdesc += fmt.Sprintf(\"⚡️ %s\\n\", *transaction.DebitPurpose)\n\t\t\tdesc += fmt.Sprintf(\"🧠 %s\\n\", transaction.ModelString())\n\t\t\tdesc += fmt.Sprintf(\"💳 Price → $%s input / $%s output per 1M\\n\", inputPrice, outputPrice)\n\t\t\tdesc += fmt.Sprintf(\"🪙 Used → %d input / %d output\\n\", *transaction.DebitInputTokens, *transaction.DebitOutputTokens)\n\n\t\t\tif cacheDiscountStr != \"\" {\n\t\t\t\tdesc += fmt.Sprintf(\"🎯 Cache discount → $%s (%d%%)\\n\", cacheDiscountStr, int(cacheDiscountPct))\n\t\t\t}\n\n\t\t} else {\n\t\t\tsign = \"+\"\n\t\t\tc = term.ColorHiGreen\n\n\t\t\tswitch *transaction.CreditType {\n\t\t\tcase shared.CreditTypeGrant:\n\t\t\t\tdesc += \"Monthly subscription payment\"\n\t\t\tcase shared.CreditTypeTrial:\n\t\t\t\tdesc += \"Started trial\"\n\t\t\tcase shared.CreditTypePurchase:\n\t\t\t\tdesc += \"Purchased credits\"\n\t\t\tcase shared.CreditTypeSwitch:\n\t\t\t\tdesc += \"Switched to Integrated Models mode\"\n\t\t\t}\n\n\t\t\tdesc += \"\\n\"\n\t\t}\n\n\t\tamountStr := transaction.Amount.StringFixed(6)\n\t\tfor i := 0; i < 4; i++ {\n\t\t\tamountStr = strings.TrimSuffix(amountStr, \"0\")\n\t\t}\n\n\t\tbalanceStr := transaction.EndBalance.StringFixed(4)\n\t\tfor i := 0; i < 2; i++ {\n\t\t\tbalanceStr = strings.TrimSuffix(balanceStr, \"0\")\n\t\t}\n\n\t\ttable.Append([]string{\n\t\t\tcolor.New(c).Sprint(sign + \"$\" + amountStr),\n\t\t\t\"$\" + balanceStr,\n\n\t\t\tdesc,\n\t\t})\n\t}\n\n\ttable.Render()\n\n\tvar output string\n\tvar pageLine string\n\n\tif res.NumPages > 1 {\n\t\tpageLine = fmt.Sprintf(\"Page size %d. Showing page %d of %d\", logCreditsPageSize, logCreditsPage, res.NumPages)\n\t\tif res.NumPagesMax {\n\t\t\tpageLine = \"+\"\n\t\t}\n\t\toutput = pageLine + \"\\n\\n\" + tableString.String()\n\t} else {\n\t\toutput = tableString.String()\n\t}\n\n\tterm.PageOutput(output)\n\n\tvar inputFn func()\n\tinputFn = func() {\n\t\tfmt.Println(\"\\n\" + pageLine)\n\n\t\tprompts := []string{}\n\n\t\tif res.NumPages > 1 && logCreditsPage < res.NumPages {\n\t\t\tprompts = append(prompts, \"Press 'n' for next page\")\n\t\t}\n\n\t\tif logCreditsPage > 1 {\n\t\t\tprompts = append(prompts, \"Press 'p' for previous page\")\n\t\t}\n\n\t\tprompts = append(prompts, \"Type any number and press enter to jump to a page\")\n\n\t\tprompts = append(prompts, \"Press 'q' to quit\")\n\n\t\tcolor.New(term.ColorHiMagenta, color.Bold).Println(strings.Join(prompts, \"\\n\"))\n\t\tcolor.New(term.ColorHiMagenta, color.Bold).Print(\"> \")\n\n\t\tchar, _, err := term.GetUserKeyInput()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Failed to get user input: %v\", err)\n\t\t}\n\n\t\t// Check if the input is a digit\n\t\tif unicode.IsDigit(char) {\n\t\t\tvar numberInput strings.Builder\n\t\t\tnumberInput.WriteRune(char)\n\n\t\t\tfmt.Print(string(char)) // Show the initial digit\n\n\t\t\tfor {\n\t\t\t\tchar, key, err := term.GetUserKeyInput()\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Failed to get user input: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// If Enter is pressed, commit the input\n\t\t\t\tif key == keyboard.KeyEnter {\n\t\t\t\t\tpageNumber, err := strconv.Atoi(numberInput.String())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tfmt.Println(\"Invalid page number.\")\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if the page number is valid\n\t\t\t\t\tif pageNumber >= 1 && (pageNumber <= res.NumPages || res.NumPagesMax) {\n\t\t\t\t\t\tlogCreditsPage = pageNumber\n\t\t\t\t\t\tshowLog(cmd, args) // Re-run the log command with the new page\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfmt.Println()\n\t\t\t\t\t\tfmt.Println(\"Invalid page number.\")\n\t\t\t\t\t\tinputFn()\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// If another digit is pressed, add it to the input\n\t\t\t\tif unicode.IsDigit(char) {\n\t\t\t\t\tnumberInput.WriteRune(char)\n\t\t\t\t\tfmt.Print(string(char)) // Show the digit\n\t\t\t\t} else if key == keyboard.KeyBackspace || key == keyboard.KeyBackspace2 {\n\t\t\t\t\t// Handle backspace\n\t\t\t\t\tif numberInput.Len() > 0 {\n\t\t\t\t\t\t// Remove the last rune\n\t\t\t\t\t\tinput := numberInput.String()\n\t\t\t\t\t\tnumberInput.Reset()\n\t\t\t\t\t\tnumberInput.WriteString(input[:len(input)-1])\n\t\t\t\t\t\tfmt.Print(\"\\b \\b\") // Erase the digit\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\t\t\t\t\t// Handle invalid input while typing a number\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tfmt.Println(\"\\nInvalid input. Please enter a valid page number.\")\n\t\t\t\t\tinputFn()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Handle non-digit hotkeys\n\t\tfmt.Print(string(char))\n\t\tswitch char {\n\t\tcase 'n':\n\t\t\tif logCreditsPage < res.NumPages || res.NumPagesMax {\n\t\t\t\tlogCreditsPage++\n\t\t\t\tshowLog(cmd, args)\n\t\t\t} else {\n\t\t\t\tfmt.Println()\n\t\t\t\tfmt.Println(\"Already on last page.\")\n\t\t\t\tinputFn()\n\t\t\t}\n\t\tcase 'p':\n\t\t\tif logCreditsPage > 1 {\n\t\t\t\tlogCreditsPage--\n\t\t\t\tshowLog(cmd, args)\n\t\t\t} else {\n\t\t\t\tfmt.Println()\n\t\t\t\tfmt.Println(\"Already on first page.\")\n\t\t\t\tinputFn()\n\t\t\t}\n\t\tcase 'q':\n\t\t\tfmt.Println()\n\t\t\treturn\n\t\tdefault:\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"Invalid input.\")\n\t\t\tinputFn()\n\t\t}\n\n\t}\n\n\tif res.NumPages > 1 {\n\t\tinputFn()\n\t}\n}\n\nfunc formatSpend(spend decimal.Decimal) string {\n\tif spend.IsZero() {\n\t\treturn \"$0.00\"\n\t}\n\n\tspendStr := fmt.Sprintf(\"$%s\", spend.StringFixed(4))\n\tfor i := 0; i < 2; i++ {\n\t\tif strings.HasSuffix(spendStr, \"0\") {\n\t\t\tspendStr = spendStr[:len(spendStr)-1]\n\t\t}\n\t}\n\tif spendStr == \"$0.00\" {\n\t\treturn \"<$0.0001\"\n\t}\n\treturn spendStr\n}\n"
  },
  {
    "path": "app/cli/cmd/users.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar usersCmd = &cobra.Command{\n\tUse:   \"users\",\n\tShort: \"List all users and pending invites and the current org\",\n\tRun:   listUsersAndInvites,\n}\n\nfunc init() {\n\tRootCmd.AddCommand(usersCmd)\n}\n\nfunc listUsersAndInvites(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\n\tvar userResp *shared.ListUsersResponse\n\tvar pendingInvites []*shared.Invite\n\tvar orgRoles []*shared.OrgRole\n\n\terrCh := make(chan error)\n\n\tterm.StartSpinner(\"\")\n\n\tgo func() {\n\t\tvar err *shared.ApiError\n\t\tuserResp, err = api.Client.ListUsers()\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error fetching users: %s\", err.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tvar err *shared.ApiError\n\t\tpendingInvites, err = api.Client.ListPendingInvites()\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error fetching pending invites: %s\", err.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tvar err *shared.ApiError\n\t\torgRoles, err = api.Client.ListOrgRoles()\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error fetching org roles: %s\", err.Msg)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\n\t}()\n\n\tfor i := 0; i < 3; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"%v\", err)\n\t\t}\n\t}\n\n\tterm.StopSpinner()\n\n\torgRolesById := make(map[string]*shared.OrgRole)\n\tfor _, role := range orgRoles {\n\t\torgRolesById[role.Id] = role\n\t}\n\n\t// Display users and pending invites in a table\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetHeader([]string{\"Email\", \"Name\", \"Role\", \"Status\"})\n\n\tfor _, user := range userResp.Users {\n\t\ttable.Append([]string{user.Email, user.Name, orgRolesById[userResp.OrgUsersByUserId[user.Id].OrgRoleId].Label, \"Active\"})\n\t}\n\n\tfor _, invite := range pendingInvites {\n\t\ttable.Append([]string{invite.Email, invite.Name, orgRolesById[invite.OrgRoleId].Label, \"Pending\"})\n\t}\n\n\ttable.Render()\n}\n"
  },
  {
    "path": "app/cli/cmd/version.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"plandex-cli/version\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar versionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Print the version number of Plandex\",\n\tLong:  `All software has versions. This is Plandex's`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tfmt.Println(version.Version)\n\t},\n}\n\nfunc init() {\n\tRootCmd.AddCommand(versionCmd)\n}\n"
  },
  {
    "path": "app/cli/dev.sh",
    "content": "#!/usr/bin/env bash\n\nOUT=\"${PLANDEX_DEV_CLI_OUT_DIR:-/usr/local/bin}\"\nNAME=\"${PLANDEX_DEV_CLI_NAME:-plandex-dev}\"\nALIAS=\"${PLANDEX_DEV_CLI_ALIAS:-pdxd}\"\n\n# Double quote to prevent globbing and word splitting.\ngo build -o \"$NAME\" &&\n    rm -f \"$OUT\"/\"$NAME\" &&\n    cp \"$NAME\" \"$OUT\"/\"$NAME\" &&\n    ln -sf \"$OUT\"/\"$NAME\" \"$OUT\"/\"$ALIAS\" &&\n    echo built \"$NAME\" cli and added \"$ALIAS\" alias to \"$OUT\"\n"
  },
  {
    "path": "app/cli/format/file.go",
    "content": "package format\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc GetFileNameWithoutExt(path string) string {\n\tname := path[:len(path)-len(filepath.Ext(path))]\n\n\tname = strings.ToLower(name)\n\tname = strings.ReplaceAll(name, \"_\", \"-\")\n\tname = strings.ReplaceAll(name, \" \", \"-\")\n\tname = strings.ReplaceAll(name, \".\", \"-\")\n\tname = strings.ReplaceAll(name, \"/\", \"-\")\n\tname = strings.ReplaceAll(name, \"\\\\\", \"-\")\n\tname = strings.ReplaceAll(name, \"'\", \"\")\n\tname = strings.ReplaceAll(name, \"`\", \"\")\n\tname = strings.ReplaceAll(name, \"\\\"\", \"\")\n\n\treturn name\n}\n"
  },
  {
    "path": "app/cli/format/time.go",
    "content": "// Adapted from https://raw.githubusercontent.com/dustin/go-humanize/master/times.go\n\npackage format\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n)\n\n// Seconds-based time units\nconst (\n\tDay      = 24 * time.Hour\n\tWeek     = 7 * Day\n\tMonth    = 30 * Day\n\tYear     = 12 * Month\n\tLongTime = 37 * Year\n)\n\n// Time formats a time into a relative string.\n//\n// Time(someT) -> \"3 weeks ago\"\nfunc Time(then time.Time) string {\n\treturn relTime(then.UTC(), time.Now().UTC(), \"ago\", \"from now\")\n}\n\n// A relTimeMagnitude struct contains a relative time point at which\n// the relative format of time will switch to a new format string.  A\n// slice of these in ascending order by their \"D\" field is passed to\n// CustomRelTime to format durations.\n//\n// The Format field is a string that may contain a \"%s\" which will be\n// replaced with the appropriate signed label (e.g. \"ago\" or \"from\n// now\") and a \"%d\" that will be replaced by the quantity.\n//\n// The DivBy field is the amount of time the time difference must be\n// divided by in order to display correctly.\n//\n// e.g. if D is 2*time.Minute and you want to display \"%d minutes %s\"\n// DivBy should be time.Minute so whatever the duration is will be\n// expressed in minutes.\ntype relTimeMagnitude struct {\n\tD     time.Duration\n\tFn    func(diff time.Duration, lbl string) string\n\tDivBy time.Duration\n}\n\nvar defaultMagnitudes = []relTimeMagnitude{\n\t{time.Second, func(diff time.Duration, lbl string) string { return \"just now\" }, time.Second},\n\t{2 * time.Second, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"1s %s\", lbl) }, 1},\n\t{time.Minute, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"%ds %s\", diff, lbl) }, time.Second},\n\t{2 * time.Minute, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"1m %s\", lbl) }, 1},\n\t{time.Hour, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"%dm %s\", diff, lbl) }, time.Minute},\n\t{2 * time.Hour, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"1h %s\", lbl) }, 1},\n\t{Day, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"%dh %s\", diff, lbl) }, time.Hour},\n\t{2 * Day, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"1d %s\", lbl) }, 1},\n\t{Week, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"%dd %s\", diff, lbl) }, Day},\n\t{2 * Week, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"1w %s\", lbl) }, 1},\n\t{Month, func(diff time.Duration, lbl string) string { return fmt.Sprintf(\"%dw %s\", diff, lbl) }, Week},\n}\n\n// RelTime(timeInPast, timeInFuture, \"earlier\", \"later\") -> \"3 weeks earlier\"\nfunc relTime(a, b time.Time, albl, blbl string) string {\n\treturn customRelTime(a, b, albl, blbl, defaultMagnitudes)\n}\n\nfunc customRelTime(a, b time.Time, albl, blbl string, magnitudes []relTimeMagnitude) string {\n\tlbl := albl\n\tdiff := b.Sub(a)\n\n\tif a.After(b) {\n\t\tlbl = blbl\n\t\tdiff = a.Sub(b)\n\t}\n\n\t// Find the largest magnitude\n\tlargestMagnitude := magnitudes[len(magnitudes)-1].D\n\n\t// If the difference is greater than the largest magnitude, format the date in local time\n\tif diff >= largestMagnitude {\n\t\treturn a.Local().Format(\"Jan 2 2006\")\n\t}\n\n\tn := sort.Search(len(magnitudes), func(i int) bool {\n\t\treturn magnitudes[i].D > diff\n\t})\n\n\t// If no magnitude is large enough, use the largest magnitude available\n\tif n >= len(magnitudes) {\n\t\tn = len(magnitudes) - 1\n\t}\n\tmag := magnitudes[n]\n\n\tif mag.DivBy == 1 {\n\t\treturn mag.Fn(diff, lbl)\n\t}\n\n\treturn mag.Fn(diff/mag.DivBy, lbl)\n}\n"
  },
  {
    "path": "app/cli/fs/fs.go",
    "content": "package fs\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"plandex-cli/term\"\n)\n\nvar Cwd string\nvar PlandexDir string\nvar ProjectRoot string\nvar HomePlandexDir string\nvar CacheDir string\n\nvar HomeDir string\nvar HomeAuthPath string\nvar HomeAccountsPath string\n\nfunc init() {\n\tvar err error\n\tCwd, err = os.Getwd()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting current working directory: %v\", err)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Couldn't find home dir: %v\", err.Error())\n\t}\n\tHomeDir = home\n\n\tif os.Getenv(\"PLANDEX_ENV\") == \"development\" {\n\t\tHomePlandexDir = filepath.Join(home, \".plandex-home-dev-v2\")\n\t} else {\n\t\tHomePlandexDir = filepath.Join(home, \".plandex-home-v2\")\n\t}\n\n\t// Create the home plandex directory if it doesn't exist\n\terr = os.MkdirAll(HomePlandexDir, os.ModePerm)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(err.Error())\n\t}\n\n\tCacheDir = filepath.Join(HomePlandexDir, \"cache\")\n\tHomeAuthPath = filepath.Join(HomePlandexDir, \"auth.json\")\n\tHomeAccountsPath = filepath.Join(HomePlandexDir, \"accounts.json\")\n\n\terr = os.MkdirAll(filepath.Join(CacheDir, \"tiktoken\"), os.ModePerm)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(err.Error())\n\t}\n\terr = os.Setenv(\"TIKTOKEN_CACHE_DIR\", CacheDir)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(err.Error())\n\t}\n\n\tFindPlandexDir()\n\tif PlandexDir != \"\" {\n\t\tProjectRoot = Cwd\n\t}\n}\n\nfunc FindOrCreatePlandex() (string, bool, error) {\n\tFindPlandexDir()\n\tif PlandexDir != \"\" {\n\t\tProjectRoot = Cwd\n\t\treturn PlandexDir, false, nil\n\t}\n\n\t// Determine the directory path\n\tvar dir string\n\tif os.Getenv(\"PLANDEX_ENV\") == \"development\" {\n\t\tdir = filepath.Join(Cwd, \".plandex-dev-v2\")\n\t} else {\n\t\tdir = filepath.Join(Cwd, \".plandex-v2\")\n\t}\n\n\terr := os.Mkdir(dir, os.ModePerm)\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\tPlandexDir = dir\n\tProjectRoot = Cwd\n\n\treturn dir, true, nil\n}\n\nfunc ProjectRootIsGitRepo() bool {\n\tif ProjectRoot == \"\" {\n\t\treturn false\n\t}\n\n\treturn IsGitRepo(ProjectRoot)\n}\n\nfunc IsGitRepo(dir string) bool {\n\tisGitRepo := false\n\n\tif isCommandAvailable(\"git\") {\n\t\t// check whether we're in a git repo\n\t\tcmd := exec.Command(\"git\", \"rev-parse\", \"--is-inside-work-tree\")\n\n\t\tcmd.Dir = dir\n\n\t\terr := cmd.Run()\n\n\t\tif err == nil {\n\t\t\tisGitRepo = true\n\t\t}\n\t}\n\n\treturn isGitRepo\n}\n\nfunc FindPlandexDir() {\n\tPlandexDir = findPlandex(Cwd)\n}\n\nfunc findPlandex(baseDir string) string {\n\tvar dir string\n\tif os.Getenv(\"PLANDEX_ENV\") == \"development\" {\n\t\tdir = filepath.Join(baseDir, \".plandex-dev-v2\")\n\t} else {\n\t\tdir = filepath.Join(baseDir, \".plandex-v2\")\n\t}\n\tif _, err := os.Stat(dir); !os.IsNotExist(err) {\n\t\treturn dir\n\t}\n\n\treturn \"\"\n}\n\nfunc isCommandAvailable(name string) bool {\n\tcmd := exec.Command(name, \"--version\")\n\tif err := cmd.Run(); err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "app/cli/fs/paths.go",
    "content": "package fs\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"plandex-cli/types\"\n\t\"strings\"\n\t\"sync\"\n\n\tshared \"plandex-shared\"\n\n\tignore \"github.com/sabhiram/go-gitignore\"\n)\n\nfunc GetProjectPaths(baseDir string) (*types.ProjectPaths, error) {\n\tif ProjectRoot == \"\" {\n\t\treturn nil, fmt.Errorf(\"no project root found\")\n\t}\n\n\treturn GetPaths(baseDir, ProjectRoot)\n}\n\nfunc GetPaths(baseDir, currentDir string) (*types.ProjectPaths, error) {\n\tignored, err := GetPlandexIgnore(currentDir)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tallPaths := map[string]bool{}\n\tactivePaths := map[string]bool{}\n\n\tallDirs := map[string]bool{}\n\tactiveDirs := map[string]bool{}\n\tgitIgnoredDirs := map[string]bool{}\n\n\tisGitRepo := IsGitRepo(baseDir)\n\n\terrCh := make(chan error)\n\tvar mu sync.Mutex\n\tnumRoutines := 0\n\n\tdeletedFiles := map[string]bool{}\n\n\tif isGitRepo {\n\n\t\t// Use git status to find deleted files\n\t\tnumRoutines++\n\t\tgo func() {\n\t\t\tcmd := exec.Command(\"git\", \"rev-parse\", \"--show-toplevel\")\n\t\t\toutput, err := cmd.Output()\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting git root: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trepoRoot := strings.TrimSpace(string(output))\n\n\t\t\tcmd = exec.Command(\"git\", \"status\", \"--porcelain\")\n\t\t\tcmd.Dir = baseDir\n\t\t\tout, err := cmd.Output()\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting git status: %s\", err)\n\t\t\t}\n\n\t\t\tlines := strings.Split(string(out), \"\\n\")\n\n\t\t\tfor _, line := range lines {\n\t\t\t\tline = strings.TrimSpace(line)\n\t\t\t\tif strings.HasPrefix(line, \"D \") {\n\t\t\t\t\tpath := strings.TrimSpace(line[2:])\n\t\t\t\t\tabsPath := filepath.Join(repoRoot, path)\n\t\t\t\t\trelPath, err := filepath.Rel(currentDir, absPath)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"error getting relative path: %s\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tdeletedFiles[relPath] = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\t// combine `git ls-files` and `git ls-files --others --exclude-standard`\n\t\t// to get all files in the repo\n\n\t\tnumRoutines++\n\t\tgo func() {\n\t\t\t// get all tracked files in the repo\n\t\t\tcmd := exec.Command(\"git\", \"ls-files\")\n\t\t\tcmd.Dir = baseDir\n\t\t\tout, err := cmd.Output()\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting files in git repo: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfiles := strings.Split(string(out), \"\\n\")\n\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\t\t\tfor _, file := range files {\n\t\t\t\tabsFile := filepath.Join(baseDir, file)\n\t\t\t\trelFile, err := filepath.Rel(currentDir, absFile)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting relative path: %s\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif ignored != nil && ignored.MatchesPath(relFile) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tactivePaths[relFile] = true\n\n\t\t\t\tparentDir := relFile\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tparentDir = filepath.Dir(parentDir)\n\t\t\t\t\tactiveDirs[parentDir] = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\t// get all untracked non-ignored files in the repo\n\t\tnumRoutines++\n\t\tgo func() {\n\t\t\tcmd := exec.Command(\"git\", \"ls-files\", \"--others\", \"--exclude-standard\")\n\t\t\tcmd.Dir = baseDir\n\t\t\tout, err := cmd.Output()\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting untracked files in git repo: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfiles := strings.Split(string(out), \"\\n\")\n\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\t\t\tfor _, file := range files {\n\t\t\t\tabsFile := filepath.Join(baseDir, file)\n\t\t\t\trelFile, err := filepath.Rel(currentDir, absFile)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting relative path: %s\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif ignored != nil && ignored.MatchesPath(relFile) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tactivePaths[relFile] = true\n\n\t\t\t\tparentDir := relFile\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tparentDir = filepath.Dir(parentDir)\n\t\t\t\t\tactiveDirs[parentDir] = true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\t// get all ignored paths/dirs in the repo\n\t\t// in some cases when entire directories are ignored, git will just list the directory and not the files within it\n\t\tnumRoutines++\n\t\tgo func() {\n\t\t\tcmd := exec.Command(\"git\", \"ls-files\", \"--others\", \"--ignored\", \"--exclude-standard\")\n\t\t\tcmd.Dir = baseDir\n\t\t\tout, err := cmd.Output()\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting untracked files in git repo: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpaths := strings.Split(string(out), \"\\n\")\n\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\n\t\t\tfor _, file := range paths {\n\t\t\t\tabsFile := filepath.Join(baseDir, file)\n\t\t\t\trelFile, err := filepath.Rel(currentDir, absFile)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting relative path: %s\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// check if git is ignoring the entire directory, meaning it won't list the files within it\n\t\t\t\tif strings.HasSuffix(file, \"/\") {\n\t\t\t\t\tallDirs[relFile] = true\n\t\t\t\t\tallPaths[relFile] = true\n\t\t\t\t\tgitIgnoredDirs[relFile+\"/\"] = true\n\t\t\t\t} else {\n\t\t\t\t\tallPaths[relFile] = true\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t} else {\n\n\t\t// get all paths in the directory\n\t\tnumRoutines++\n\t\tgo func() {\n\t\t\terr = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif info.IsDir() {\n\t\t\t\t\tif ShouldSkipDir(info.Name()) {\n\t\t\t\t\t\treturn filepath.SkipDir\n\t\t\t\t\t}\n\n\t\t\t\t\trelPath, err := filepath.Rel(currentDir, path)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tallDirs[relPath] = true\n\n\t\t\t\t\tif ignored != nil && ignored.MatchesPath(relPath) {\n\t\t\t\t\t\treturn filepath.SkipDir\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\trelPath, err := filepath.Rel(currentDir, path)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tallPaths[relPath] = true\n\n\t\t\t\t\tif ignored != nil && ignored.MatchesPath(relPath) {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tif !isGitRepo {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\t\tactivePaths[relPath] = true\n\n\t\t\t\t\t\tparentDir := relPath\n\t\t\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\t\t\tparentDir = filepath.Dir(parentDir)\n\t\t\t\t\t\t\tactiveDirs[parentDir] = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error walking directory: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\t}\n\n\tfor i := 0; i < numRoutines; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tfor dir := range activeDirs {\n\t\tallDirs[dir] = true\n\t}\n\n\tfor dir := range allDirs {\n\t\tallPaths[dir] = true\n\t}\n\n\tfor dir := range activeDirs {\n\t\tactivePaths[dir] = true\n\t}\n\n\tremoveDirs := map[string]bool{}\n\tfor dir := range allDirs {\n\t\tif ShouldSkipDir(dir) {\n\t\t\tremoveDirs[dir] = true\n\t\t}\n\t}\n\n\tfor dir := range removeDirs {\n\t\tdelete(allDirs, dir)\n\t\tdelete(activeDirs, dir)\n\t\tdelete(allPaths, dir)\n\t\tdelete(activePaths, dir)\n\t}\n\n\t// remove deleted files from active paths\n\tfor path := range deletedFiles {\n\t\tdelete(activePaths, path)\n\t}\n\n\tfor path := range activePaths {\n\t\tallPaths[path] = true\n\t}\n\n\tremovePaths := map[string]bool{}\n\tfor path := range allPaths {\n\t\tif IsInSkippedDir(path) {\n\t\t\tremovePaths[path] = true\n\t\t}\n\t}\n\n\tfor path := range removePaths {\n\t\tdelete(allPaths, path)\n\t\tdelete(activePaths, path)\n\t\tdelete(activeDirs, path)\n\t\tdelete(allDirs, path)\n\t}\n\n\tignoredPaths := map[string]string{}\n\tfor path := range allPaths {\n\t\tif _, ok := activePaths[path]; !ok {\n\t\t\tif ignored != nil && ignored.MatchesPath(path) {\n\t\t\t\tignoredPaths[path] = \"plandex\"\n\t\t\t} else {\n\t\t\t\tignoredPaths[path] = \"git\"\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &types.ProjectPaths{\n\t\tActivePaths:    activePaths,\n\t\tAllPaths:       allPaths,\n\t\tActiveDirs:     activeDirs,\n\t\tAllDirs:        allDirs,\n\t\tPlandexIgnored: ignored,\n\t\tIgnoredPaths:   ignoredPaths,\n\t\tGitIgnoredDirs: gitIgnoredDirs,\n\t}, nil\n}\n\nfunc GetPlandexIgnore(dir string) (*ignore.GitIgnore, error) {\n\tignorePath := filepath.Join(dir, \".plandexignore\")\n\n\tif _, err := os.Stat(ignorePath); err == nil {\n\t\tignored, err := ignore.CompileIgnoreFile(ignorePath)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading .plandexignore file: %s\", err)\n\t\t}\n\n\t\treturn ignored, nil\n\t} else if !os.IsNotExist(err) {\n\t\treturn nil, fmt.Errorf(\"error checking for .plandexignore file: %s\", err)\n\t}\n\n\treturn nil, nil\n}\n\nfunc GetBaseDirForContexts(contexts []*shared.Context) string {\n\tvar paths []string\n\n\tfor _, context := range contexts {\n\t\tif context.FilePath != \"\" {\n\t\t\tpaths = append(paths, context.FilePath)\n\t\t}\n\t}\n\n\treturn GetBaseDirForFilePaths(paths)\n}\n\nfunc GetBaseDirForFilePaths(paths []string) string {\n\tbaseDir := ProjectRoot\n\tdirsUp := 0\n\n\tfor _, path := range paths {\n\t\tcurrentDir := ProjectRoot\n\n\t\tpathSplit := strings.Split(path, string(os.PathSeparator))\n\n\t\tn := 0\n\t\tfor _, p := range pathSplit {\n\t\t\tif p == \"..\" {\n\t\t\t\tn++\n\t\t\t\tcurrentDir = filepath.Dir(currentDir)\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif n > dirsUp {\n\t\t\tdirsUp = n\n\t\t\tbaseDir = currentDir\n\t\t}\n\t}\n\n\treturn baseDir\n}\n\n// isSubpathOf checks if 'child' is within 'parent' (same path or deeper).\n// Both 'parent' and 'child' can be absolute or relative to 'baseDir';\n// we’ll convert them to absolute paths based on 'baseDir' and then compare.\nfunc IsSubpathOf(parent, child, baseDir string) (bool, error) {\n\t// Convert 'parent' -> absolute\n\tabsParent := parent\n\tif !filepath.IsAbs(parent) {\n\t\tabsParent = filepath.Join(baseDir, parent)\n\t}\n\tabsParent = filepath.Clean(absParent)\n\n\t// Convert 'child' -> absolute\n\tabsChild := child\n\tif !filepath.IsAbs(child) {\n\t\tabsChild = filepath.Join(baseDir, child)\n\t}\n\tabsChild = filepath.Clean(absChild)\n\n\t// filepath.Rel(absParent, absChild) will be something like:\n\t//   - \".\",  \"foo\",  \"foo/bar\", or \"..\" references\n\trel, err := filepath.Rel(absParent, absChild)\n\tif err != nil {\n\t\t// If there's some I/O error or invalid path, just fail safe\n\t\treturn false, fmt.Errorf(\"error getting relative path: %s\", err)\n\t}\n\n\t// If rel starts with \"..\", then absChild is outside of absParent\n\t// or at a higher level (e.g. absParent/../sibling).\n\tif strings.HasPrefix(rel, \"..\") {\n\t\treturn false, nil\n\t}\n\n\t// If we want \"absChild == absParent\" to count as inside,\n\t// then !HasPrefix(rel, \"..\") is enough.\n\t// This means child == parent or child is deeper within parent.\n\treturn true, nil\n}\n\nfunc IsIgnored(paths *types.ProjectPaths, path, baseDir string) (bool, string, error) {\n\tif !paths.AllPaths[path] {\n\t\t// if the path isn't in AllPaths, it either:\n\t\t// 1. doesn't exist (in which case we shouldn't be calling this function)\n\t\t// 2. is a subpath of a git ignored dir\n\n\t\tfor dir := range paths.GitIgnoredDirs {\n\t\t\tsubpath, err := IsSubpathOf(dir, path, baseDir)\n\t\t\tif err != nil {\n\t\t\t\treturn false, \"\", fmt.Errorf(\"error checking if %s is a subpath of %s: %s\", path, dir, err)\n\t\t\t}\n\t\t\tif subpath {\n\t\t\t\treturn true, \"git\", nil\n\t\t\t}\n\t\t}\n\t\treturn false, \"\", fmt.Errorf(\"path %s is not in the project\", path)\n\t}\n\n\tif paths.ActivePaths[path] {\n\t\treturn false, \"\", nil\n\t}\n\n\tif paths.PlandexIgnored != nil && paths.PlandexIgnored.MatchesPath(path) {\n\t\treturn true, \"plandex\", nil\n\t}\n\n\treturn true, \"git\", nil\n}\n\nvar skipDirs = map[string]bool{\n\t\".git\":              true,\n\t\"node_modules\":      true,\n\t\"venv\":              true,\n\t\".cache\":            true,\n\t\"__pycache__\":       true,\n\t\"cue.mod\":           true,\n\t\"_build\":            true,\n\t\".build\":            true,\n\t\"DerivedData\":       true,\n\t\".gradle\":           true,\n\t\".terraform\":        true,\n\t\".terragrunt-cache\": true,\n\t\".next\":             true,\n\t\".nuxt\":             true,\n\t\".bundle\":           true,\n\t\".rvm\":              true,\n\t\".rbenv\":            true,\n\t\".pyenv\":            true,\n\t\".nodenv\":           true,\n\t\".plenv\":            true,\n\t\".nvm\":              true,\n\t\"vendor\":            true,\n\t\".plandex\":          true,\n\t\".plandex-dev\":      true,\n\t\".plandex-v2\":       true,\n\t\".plandex-dev-v2\":   true,\n}\n\nfunc ShouldSkipDir(path string) bool {\n\tif skipDirs[path] {\n\t\treturn true\n\t}\n\n\tfor k := range skipDirs {\n\t\tsplitPath := strings.Split(path, string(os.PathSeparator))\n\t\tfor _, p := range splitPath {\n\t\t\tif p == k {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc IsInSkippedDir(path string) bool {\n\t// Check the direct parent directory\n\tdirName := filepath.Dir(path)\n\tif skipDirs[dirName] {\n\t\treturn true\n\t}\n\n\t// Handle cases where the directory might include path separators\n\tpathComponents := strings.Split(dirName, string(os.PathSeparator))\n\tfor _, component := range pathComponents {\n\t\tif skipDirs[component] {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "app/cli/fs/projects.go",
    "content": "package fs\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"strings\"\n)\n\nfunc GetParentProjectIdsWithPaths(currentUserId string) ([][2]string, error) {\n\tvar parentProjectIds [][2]string\n\tcurrentDir := filepath.Dir(Cwd)\n\n\tfor currentDir != \"/\" {\n\t\tplandexDir := findPlandex(currentDir)\n\t\tprojectSettingsPath := filepath.Join(plandexDir, \"projects-v2.json\")\n\t\tif _, err := os.Stat(projectSettingsPath); err == nil {\n\t\t\tbytes, err := os.ReadFile(projectSettingsPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error reading projectId file: %s\", err)\n\t\t\t}\n\n\t\t\tvar settingsByAccount types.CurrentProjectSettingsByAccount\n\t\t\terr = json.Unmarshal(bytes, &settingsByAccount)\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"error unmarshalling projects-v2.json: %v\", err)\n\t\t\t}\n\n\t\t\tsettings := settingsByAccount[currentUserId]\n\n\t\t\tif settings == nil {\n\t\t\t\treturn parentProjectIds, nil\n\t\t\t}\n\n\t\t\tprojectId := string(settings.Id)\n\t\t\tparentProjectIds = append(parentProjectIds, [2]string{currentDir, projectId})\n\t\t}\n\t\tcurrentDir = filepath.Dir(currentDir)\n\t}\n\n\treturn parentProjectIds, nil\n}\n\nfunc GetChildProjectIdsWithPaths(ctx context.Context, currentUserId string) ([][2]string, error) {\n\tvar childProjectIds [][2]string\n\n\terr := filepath.Walk(Cwd, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\t// if permission denied, skip the path\n\t\t\tif os.IsPermission(err) {\n\t\t\t\tif info.IsDir() {\n\t\t\t\t\treturn filepath.SkipDir\n\t\t\t\t} else {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\n\t\tif strings.HasPrefix(info.Name(), \".\") {\n\t\t\tif info.IsDir() {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn fmt.Errorf(\"context timeout\")\n\t\tdefault:\n\t\t}\n\n\t\tif info.IsDir() && path != Cwd {\n\t\t\tplandexDir := findPlandex(path)\n\t\t\tprojectSettingsPath := filepath.Join(plandexDir, \"projects-v2.json\")\n\t\t\tif _, err := os.Stat(projectSettingsPath); err == nil {\n\t\t\t\tbytes, err := os.ReadFile(projectSettingsPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error reading projectId file: %s\", err)\n\t\t\t\t}\n\t\t\t\tvar settingsByAccount types.CurrentProjectSettingsByAccount\n\t\t\t\terr = json.Unmarshal(bytes, &settingsByAccount)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"error unmarshalling projects-v2.json: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tsettings := settingsByAccount[currentUserId]\n\n\t\t\t\tif settings == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tprojectId := string(settings.Id)\n\t\t\t\tchildProjectIds = append(childProjectIds, [2]string{path, projectId})\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tif err.Error() == \"context timeout\" {\n\t\t\treturn childProjectIds, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error walking the path %s: %s\", Cwd, err)\n\t}\n\n\treturn childProjectIds, nil\n}\n"
  },
  {
    "path": "app/cli/fs/utils.go",
    "content": "package fs\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc FileExists(path string) (bool, error) {\n\t_, err := os.Stat(path)\n\tif err == nil {\n\t\treturn true, nil\n\t} else if os.IsNotExist(err) {\n\t\treturn false, nil\n\t} else {\n\t\treturn false, fmt.Errorf(\"error checking if file exists: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "app/cli/go.mod",
    "content": "module plandex-cli\n\ngo 1.23.3\n\nrequire (\n\tgithub.com/aws/aws-sdk-go-v2/config v1.29.14\n\tgithub.com/charmbracelet/lipgloss v1.0.0\n\tgithub.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8\n\tgithub.com/chromedp/chromedp v0.13.3\n\tgithub.com/coreos/go-systemd/v22 v22.5.0\n\tgithub.com/davecgh/go-spew v1.1.1\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/godbus/dbus/v5 v5.1.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/lithammer/fuzzysearch v1.1.8\n\tgithub.com/muesli/reflow v0.3.0\n\tgithub.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a\n\tgithub.com/olekukonko/tablewriter v0.0.5\n\tgithub.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c\n\tgithub.com/plandex-ai/survey/v2 v2.3.7\n\tgithub.com/sashabaranov/go-openai v1.38.1\n\tgithub.com/shopspring/decimal v1.4.0\n\tgithub.com/spf13/cobra v1.8.0\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415\n\tgithub.com/xeipuuv/gojsonschema v1.2.0\n\tgolang.org/x/term v0.19.0\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1\n)\n\nrequire (\n\tgithub.com/alecthomas/chroma v0.10.0 // indirect\n\tgithub.com/andybalholm/cascadia v1.3.2 // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.36.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect\n\tgithub.com/aws/smithy-go v1.22.2 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.8.0 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.1 // indirect\n\tgithub.com/chromedp/sysutil v1.1.0 // indirect\n\tgithub.com/cqroot/multichoose v0.1.1 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect\n\tgithub.com/gobwas/httphead v0.1.0 // indirect\n\tgithub.com/gobwas/pool v0.2.1 // indirect\n\tgithub.com/gobwas/ws v1.4.0 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/jinzhu/copier v0.4.0 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-tty v0.0.3 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect\n\tgithub.com/microcosm-cc/bluemonday v1.0.26 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/pkg/term v1.2.0-beta.2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect\n\tgithub.com/yuin/goldmark v1.6.0 // indirect\n\tgithub.com/yuin/goldmark-emoji v1.0.2 // indirect\n\tgolang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect\n\tgolang.org/x/net v0.18.0 // indirect\n\tgolang.org/x/sync v0.12.0 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n)\n\nrequire (\n\tgithub.com/Masterminds/semver v1.5.0\n\tgithub.com/PuerkitoBio/goquery v1.8.1\n\tgithub.com/briandowns/spinner v1.23.0\n\tgithub.com/charmbracelet/bubbles v0.20.0\n\tgithub.com/charmbracelet/bubbletea v1.3.0\n\tgithub.com/charmbracelet/glamour v0.6.0\n\tgithub.com/charmbracelet/glow v1.5.1\n\tgithub.com/cqroot/prompt v0.9.4\n\tgithub.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203\n\tgithub.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c\n\tgithub.com/pkoukk/tiktoken-go v0.1.7 // indirect\n\tgithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/xlab/treeprint v1.2.0\n\tgolang.org/x/image v0.25.0 // indirect\n\tgolang.org/x/text v0.23.0 // indirect\n\tplandex-shared v0.0.0-00010101000000-000000000000\n)\n\nreplace plandex-shared => ../shared\n"
  },
  {
    "path": "app/cli/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=\ncloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=\ncloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=\ncloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=\ncloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=\ncloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=\ncloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=\ncloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=\ncloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=\ncloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=\ncloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=\ncloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=\ncloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=\ncloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=\ncloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=\ncloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=\ncloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=\ncloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=\ncloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=\ncloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=\ncloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=\ncloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=\ncloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=\ncloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=\ncloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=\ncloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=\ncloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=\ncloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=\ncloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=\ncloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=\ncloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=\ncloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=\ncloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=\ncloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=\ncloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=\ncloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=\ncloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=\ncloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=\ncloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=\ncloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=\ncloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=\ncloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=\ncloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=\ncloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=\ncloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=\ncloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=\ncloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=\ncloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=\ncloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=\ncloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=\ncloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=\ncloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=\ncloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=\ncloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=\ncloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=\ncloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=\ncloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=\ncloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=\ncloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=\ncloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=\ncloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=\ncloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=\ncloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=\ncloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=\ncloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=\ncloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=\ncloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=\ncloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=\ncloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=\ncloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=\ncloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=\ncloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=\ncloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=\ncloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=\ncloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=\ncloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=\ncloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=\ncloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=\ncloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=\ncloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=\ncloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=\ncloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=\ncloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=\ncloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=\ncloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=\ncloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=\ncloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=\ncloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=\ncloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=\ncloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=\ncloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=\ncloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=\ncloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=\ncloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=\ncloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=\ncloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=\ncloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=\ncloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=\ncloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=\ncloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=\ncloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=\ncloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=\ncloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=\ncloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=\ncloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=\ncloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=\ncloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=\ncloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=\ncloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=\ncloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=\ncloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=\ncloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=\ncloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=\ncloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=\ncloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=\ncloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=\ncloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=\ncloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ncloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=\ncloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=\ncloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=\ncloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=\ncloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=\ncloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=\ncloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=\ncloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=\ncloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=\ncloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=\ncloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=\ncloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=\ncloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=\ngithub.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=\ngithub.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=\ngithub.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=\ngithub.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=\ngithub.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=\ngithub.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=\ngithub.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=\ngithub.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=\ngithub.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=\ngithub.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=\ngithub.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=\ngithub.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=\ngithub.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charmbracelet/bubbles v0.7.5/go.mod h1:IRTORFvhEI6OUH7WhN2Ks8Z8miNGimk1BE6cmHijOkM=\ngithub.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=\ngithub.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=\ngithub.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=\ngithub.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=\ngithub.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=\ngithub.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=\ngithub.com/charmbracelet/bubbletea v1.3.0 h1:fPMyirm0u3Fou+flch7hlJN9krlnVURrkUVDwqXjoAc=\ngithub.com/charmbracelet/bubbletea v1.3.0/go.mod h1:eTaHfqbIwvBhFQM/nlT1NsGc4kp8jhF8LfUK67XiTDM=\ngithub.com/charmbracelet/charm v0.8.7/go.mod h1:ApJYwJljEjODkOYJgFDzbUqztLrCWQct9zyPD+xcVr4=\ngithub.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=\ngithub.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=\ngithub.com/charmbracelet/glow v1.5.1 h1:o1mwT4xXXpkfUhJG6euQayNxLZf9yKctOCNHLztrwdE=\ngithub.com/charmbracelet/glow v1.5.1/go.mod h1:rGgop0a2/4gXWiAxUW1iEQseoE+9Ctpb7M4sM9cY9CU=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=\ngithub.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=\ngithub.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=\ngithub.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=\ngithub.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=\ngithub.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=\ngithub.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0=\ngithub.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=\ngithub.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=\ngithub.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cqroot/multichoose v0.1.1 h1:diGuKYKea9ePOTwUyUDor9zKRqKFWXGkYGqUa9+firU=\ngithub.com/cqroot/multichoose v0.1.1/go.mod h1:BJzIGqbQZNADPDuA3IzhmTMpRc2F3fZKysMRYP+Ydw8=\ngithub.com/cqroot/prompt v0.9.4 h1:uFRlhXuOP3CSD+Pii0Z8VJhgXpavSloFf7/KAERwjz8=\ngithub.com/cqroot/prompt v0.9.4/go.mod h1:6BVZiEv7XkW1K64y1k2wdzToDwspL3n/RkUIyPjQ808=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=\ngithub.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=\ngithub.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=\ngithub.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=\ngithub.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=\ngithub.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=\ngithub.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=\ngithub.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=\ngithub.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY=\ngithub.com/hashicorp/consul/sdk v0.11.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=\ngithub.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=\ngithub.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=\ngithub.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=\ngithub.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=\ngithub.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/meowgorithm/babyenv v1.3.0/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=\ngithub.com/meowgorithm/babyenv v1.3.1/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=\ngithub.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=\ngithub.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=\ngithub.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/gitcha v0.2.0/go.mod h1:Ri8m9TZS4+ORG4JVmVKUQcWZuxDvUW3UKxMdQfzG2zI=\ngithub.com/muesli/go-app-paths v0.2.1/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=\ngithub.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=\ngithub.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a/go.mod h1:+XG0ne5zXWBTSbbe7Z3/RWxaT8PZY6zaZ1dX6KjprYY=\ngithub.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=\ngithub.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=\ngithub.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=\ngithub.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=\ngithub.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=\ngithub.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=\ngithub.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=\ngithub.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=\ngithub.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=\ngithub.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=\ngithub.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=\ngithub.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c h1:bki/wkg5iFBOv3jCPUDNuH5yLngUPUdEJCSuvc2tiQ0=\ngithub.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c/go.mod h1:SqEsJfsIr0GYUyLatvezDOBe6XsCw64E7v33QzeH5PM=\ngithub.com/plandex-ai/survey/v2 v2.3.7 h1:u1o6bflbaBpW8i8krm+91Z2cOcvZcMVS+AjV+rgR8Rk=\ngithub.com/plandex-ai/survey/v2 v2.3.7/go.mod h1:RiBOKRDB5fOQrOzsiAPAN57hYqFKPkCxgSK7twcDOys=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=\ngithub.com/sagikazarmark/crypt v0.8.0/go.mod h1:TmKwZAo97S4Fy4sfMH/HX/cQP5D+ijra2NyLpNNmttY=\ngithub.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=\ngithub.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=\ngithub.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=\ngithub.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=\ngithub.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=\ngithub.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=\ngithub.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=\ngo.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4=\ngo.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=\ngolang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=\ngolang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=\ngolang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=\ngolang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=\ngolang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=\ngolang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=\ngolang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=\ngolang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=\ngoogle.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=\ngoogle.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=\ngoogle.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=\ngoogle.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=\ngoogle.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=\ngoogle.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=\ngoogle.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=\ngoogle.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=\ngoogle.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=\ngoogle.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=\ngoogle.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=\ngoogle.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=\ngoogle.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=\ngoogle.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=\ngoogle.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=\ngoogle.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "app/cli/install.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nSCRIPT_DIR=$( cd -- \"$( dirname -- \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\n\nPLATFORM=\nARCH=\nVERSION=\nRELEASES_URL=\"https://github.com/plandex-ai/plandex/releases/download\"\n\n # Ensure cleanup happens on exit and on specific signals\ntrap cleanup EXIT\ntrap cleanup INT TERM\n\ncleanup () {\n  cd \"${SCRIPT_DIR}\"\n  rm -rf plandex_install_tmp\n}\n\n# Set platform\ncase \"$(uname -s)\" in\n Darwin)\n   PLATFORM='darwin'\n   ;;\n\n Linux)\n   PLATFORM='linux'\n   ;;\n\n FreeBSD)\n   PLATFORM='freebsd'\n   ;;\n\n CYGWIN*|MINGW*|MSYS*)\n   PLATFORM='windows'\n   ;;\n\n *)\n   echo \"Platform may or may not be supported. Will attempt to install.\"\n   PLATFORM='linux'\n   ;;\nesac\n\nif [[ \"$PLATFORM\" == \"windows\" ]]; then\n  echo \"🚨 Windows is only supported via WSL. It doesn't work in the Windows CMD prompt or PowerShell.\"\n  echo \"How to install WSL 👉 https://learn.microsoft.com/en-us/windows/wsl/about\"\n  exit 1\nfi\n\n# Set arch\nif [[ \"$(uname -m)\" == 'x86_64' ]]; then\n  ARCH=\"amd64\"\nelif [[ \"$(uname -m)\" == 'arm64' || \"$(uname -m)\" == 'aarch64' ]]; then\n  ARCH=\"arm64\"\nfi\n\nif [[ \"$(cat /proc/1/cgroup 2> /dev/null | grep docker | wc -l)\" > 0 ]] || [ -f /.dockerenv ]; then\n  IS_DOCKER=true\nelse\n  IS_DOCKER=false\nfi\n\n# Set Version\nif [[ -z \"${PLANDEX_VERSION}\" ]]; then\n  VERSION=$(curl -sL https://plandex.ai/v2/cli-version.txt)\nelse\n  VERSION=$PLANDEX_VERSION\n  echo \"Using custom version $VERSION\"\nfi\n\n\nwelcome_plandex () {\n  echo \"\"\n  echo \"$(printf '%*s' \"$(tput cols)\" '' | tr ' ' -)\"\n  echo \"\"\n  echo \"🚀 Plandex v$VERSION • Quick Install\"\n  echo \"\"\n  echo \"$(printf '%*s' \"$(tput cols)\" '' | tr ' ' -)\"\n  echo \"\"\n}\n\ndownload_plandex () {\n  ENCODED_TAG=\"cli%2Fv${VERSION}\"\n\n  url=\"${RELEASES_URL}/${ENCODED_TAG}/plandex_${VERSION}_${PLATFORM}_${ARCH}.tar.gz\"\n\n  mkdir -p plandex_install_tmp\n  cd plandex_install_tmp\n\n  echo \"📥 Downloading Plandex tarball\"\n  echo \"\"\n  echo \"👉 $url\"\n  echo \"\"\n  curl -s -L -o plandex.tar.gz \"${url}\"\n\n  tar zxf plandex.tar.gz 1> /dev/null\n\n  should_sudo=false\n\n  if [ \"$PLATFORM\" == \"darwin\" ] || $IS_DOCKER ; then\n    if [[ -d /usr/local/bin ]]; then\n      if ! mv plandex /usr/local/bin/ 2>/dev/null; then\n        echo \"Permission denied when attempting to move Plandex to /usr/local/bin.\"\n        if hash sudo 2>/dev/null; then\n          should_sudo=true\n          echo \"Attempting to use sudo to complete installation.\"\n          sudo mv plandex /usr/local/bin/\n          if [[ $? -eq 0 ]]; then\n            echo \"✅ Plandex is installed in /usr/local/bin\"\n            echo \"\"\n          else\n            echo \"Failed to install Plandex using sudo. Please manually move Plandex to a directory in your PATH.\"\n            exit 1\n          fi\n        else\n          echo \"sudo not found. Please manually move Plandex to a directory in your PATH.\"\n          exit 1\n        fi\n      else\n        echo \"✅ Plandex is installed in /usr/local/bin\"\n      fi\n    else\n      echo >&2 'Error: /usr/local/bin does not exist. Create this directory with appropriate permissions, then re-install.'\n      exit 1\n    fi\n  else\n    if [ $UID -eq 0 ]\n    then\n      # we are root\n      mv plandex /usr/local/bin/\n    elif hash sudo 2>/dev/null;\n    then\n      # not root, but can sudo\n      sudo mv plandex /usr/local/bin/\n      should_sudo=true\n    else\n      echo \"ERROR: This script must be run as root or be able to sudo to complete the installation.\"\n      exit 1\n    fi\n\n    echo \"✅ Plandex is installed in /usr/local/bin\"\n  fi\n\n  # create 'pdx' alias, but don't overwrite existing pdx command\n  if [ ! -x \"$(command -v pdx)\" ]; then\n    echo \"🎭 Creating pdx alias...\"\n    LOC=$(which plandex)\n    BIN_DIR=$(dirname \"$LOC\")\n\n    if [ \"$should_sudo\" = true ]; then\n      sudo ln -s \"$LOC\" \"$BIN_DIR/pdx\" && \\\n        echo \"✅ Successfully created 'pdx' alias with sudo.\" || \\\n        echo \"⚠️ Failed to create 'pdx' alias even with sudo. Please create it manually.\"\n    else\n      ln -s \"$LOC\" \"$BIN_DIR/pdx\" && \\\n        echo \"✅ Successfully created 'pdx' alias.\" || \\\n        echo \"⚠️ Failed to create 'pdx' alias. Please create it manually.\"\n    fi\n  fi\n}\n\ncheck_existing_installation () {\n  if command -v plandex >/dev/null 2>&1; then\n    existing_version=$(plandex version 2>/dev/null || echo \"unknown\")\n    # Check if version starts with 1.x.x\n    if [[ \"$existing_version\" =~ ^1\\. ]]; then\n      echo \"Found existing Plandex v1.x installation ($existing_version). Renaming to 'plandex1' before installing v2...\"\n      \n      # Get the location of existing binary\n      existing_binary=$(which plandex)\n      binary_dir=$(dirname \"$existing_binary\")\n      \n      # Rename plandex to plandex1\n      if ! mv \"$existing_binary\" \"${binary_dir}/plandex1\" 2>/dev/null; then\n        sudo mv \"$existing_binary\" \"${binary_dir}/plandex1\"\n      fi\n      \n      # Rename pdx to pdx1 if it exists\n      if [ -L \"${binary_dir}/pdx\" ]; then\n        if ! mv \"${binary_dir}/pdx\" \"${binary_dir}/pdx1\" 2>/dev/null; then\n          sudo mv \"${binary_dir}/pdx\" \"${binary_dir}/pdx1\"\n        fi\n        echo \"Renamed 'pdx' alias to 'pdx1'\"\n      fi\n      \n      echo \"Your v1.x installation is now accessible as 'plandex1' and 'pdx1'\"\n    fi\n  fi\n}\n\nwelcome_plandex\ncheck_existing_installation\ndownload_plandex\n\necho \"\"\necho \"🎉 Installation complete\"\necho \"\"\necho \"$(printf '%*s' \"$(tput cols)\" '' | tr ' ' -)\"\necho \"\"\necho \"⚡️ Run 'plandex' or 'pdx' in any project directory and start building!\"\necho \"\"\necho \"$(printf '%*s' \"$(tput cols)\" '' | tr ' ' -)\"\necho \"\"\necho \"📚 Need help? 👉 https://docs.plandex.ai\"\necho \"\"\necho \"👋 Join a community of AI builders 👉 https://discord.gg/plandex-ai\"\necho \"\"\necho \"$(printf '%*s' \"$(tput cols)\" '' | tr ' ' -)\"\necho \"\"\n\n"
  },
  {
    "path": "app/cli/lib/active_stream.go",
    "content": "package lib\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc SelectActiveStream(args []string) (string, string, bool) {\n\tterm.StartSpinner(\"\")\n\tres, apiErr := api.Client.ListPlansRunning([]string{CurrentProjectId}, false)\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting running plans: %v\", apiErr)\n\t\treturn \"\", \"\", false\n\t}\n\n\tif len(res.Branches) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No active plan stream\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"ps\")\n\t\treturn \"\", \"\", false\n\t}\n\n\tvar planId string\n\tvar branch string\n\tvar streamIdOrPlan string\n\n\tif len(args) > 0 {\n\t\tstreamIdOrPlan = args[0]\n\t}\n\n\tif streamIdOrPlan != \"\" {\n\t\tfor _, b := range res.Branches {\n\t\t\tid := res.StreamIdByBranchId[b.Id]\n\t\t\tplan := res.PlansById[b.PlanId]\n\t\t\tif id == streamIdOrPlan || plan.Name == streamIdOrPlan {\n\t\t\t\tplanId = b.PlanId\n\t\t\t\tbranch = b.Name\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif planId == \"\" {\n\t\tif len(res.PlansById) == 1 {\n\t\t\tfor _, p := range res.PlansById {\n\t\t\t\tif p.Id == CurrentPlanId {\n\t\t\t\t\tplanId = p.Id\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif planId == \"\" {\n\t\t\tvar opts []string\n\t\t\taddedPlans := make(map[string]bool)\n\n\t\t\tfor _, plan := range res.PlansById {\n\t\t\t\tif addedPlans[plan.Id] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\topts = append(opts, plan.Name)\n\t\t\t\taddedPlans[plan.Id] = true\n\t\t\t}\n\n\t\t\tselected, err := term.SelectFromList(\"Select an active plan\", opts)\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error selecting plan: %v\", err)\n\t\t\t}\n\n\t\t\tfor _, p := range res.PlansById {\n\t\t\t\tif p.Name == selected {\n\t\t\t\t\tplanId = p.Id\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\tif planId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No active plan stream\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"ps\")\n\n\t\treturn \"\", \"\", false\n\t}\n\n\tvar planBranches []*shared.Branch\n\tfor _, b := range res.Branches {\n\t\tif b.PlanId == planId {\n\t\t\tplanBranches = append(planBranches, b)\n\t\t}\n\t}\n\n\tif len(planBranches) == 0 {\n\t\tfmt.Println(\"🤷‍♂️ No active plan stream\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"ps\")\n\n\t\treturn \"\", \"\", false\n\t}\n\n\tif len(args) > 1 {\n\t\tmaybeBranch := args[1]\n\t\tfor _, b := range planBranches {\n\t\t\tif b.Name == maybeBranch {\n\t\t\t\tbranch = maybeBranch\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif branch == \"\" {\n\t\tif len(planBranches) == 1 {\n\t\t\tname := planBranches[0].Name\n\t\t\tif name == CurrentBranch {\n\t\t\t\tbranch = name\n\t\t\t}\n\t\t}\n\n\t\tif branch == \"\" {\n\t\t\topts := make([]string, len(planBranches))\n\n\t\t\tfor i, b := range planBranches {\n\t\t\t\topts[i] = b.Name\n\t\t\t}\n\n\t\t\tselected, err := term.SelectFromList(\"Select a branch\", opts)\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error selecting branch: %v\", err)\n\t\t\t}\n\n\t\t\tbranch = selected\n\t\t}\n\t}\n\n\tif branch == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No active plan stream\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"ps\")\n\n\t\treturn \"\", \"\", false\n\t}\n\n\treturn planId, branch, true\n}\n"
  },
  {
    "path": "app/cli/lib/apply.go",
    "content": "package lib\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n)\n\ntype ApplyPlanParams struct {\n\tPlanId      string\n\tBranch      string\n\tApplyFlags  types.ApplyFlags\n\tTellFlags   types.TellFlags\n\tOnExecFail  types.OnApplyExecFailFn\n\tExecCommand string\n}\n\nfunc MustApplyPlan(\n\tparams ApplyPlanParams,\n) {\n\tMustApplyPlanAttempt(params, 0)\n}\n\nfunc MustApplyPlanAttempt(\n\tparams ApplyPlanParams,\n\tattempt int,\n) {\n\tlog.Println(\"Applying plan\")\n\n\tapplyFlags := params.ApplyFlags\n\tplanId := params.PlanId\n\tbranch := params.Branch\n\tonExecFail := params.OnExecFail\n\n\tautoConfirm := applyFlags.AutoConfirm\n\tautoCommit := applyFlags.AutoCommit\n\tnoCommit := applyFlags.NoCommit\n\tnoExec := applyFlags.NoExec\n\n\tterm.StartSpinner(\"\")\n\n\terr := PromptSyncModelsIfNeeded()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error syncing models: %v\", err)\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tcurrentPlanState, apiErr := api.Client.GetCurrentPlanState(planId, branch)\n\n\tif apiErr != nil {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"Error getting current plan state: %v\", apiErr)\n\t}\n\n\tif currentPlanState.HasPendingBuilds() {\n\t\tplansRunningRes, apiErr := api.Client.ListPlansRunning([]string{CurrentProjectId}, false)\n\n\t\tif apiErr != nil {\n\t\t\tterm.StopSpinner()\n\t\t\tterm.OutputErrorAndExit(\"Error getting running plans: %v\", apiErr)\n\t\t}\n\n\t\tfor _, b := range plansRunningRes.Branches {\n\t\t\tif b.PlanId == planId && b.Name == branch {\n\t\t\t\tfmt.Println(\"This plan is currently active. Please wait for it to finish before applying.\")\n\t\t\t\tfmt.Println()\n\t\t\t\tterm.PrintCmds(\"\", \"ps\", \"connect\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tterm.StopSpinner()\n\n\t\tfmt.Println(\"This plan has changes that need to be built before applying\")\n\t\tfmt.Println()\n\n\t\tshouldBuild, err := term.ConfirmYesNo(\"Build changes now?\")\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"failed to get confirmation user input: %s\", err)\n\t\t}\n\n\t\tif !shouldBuild {\n\t\t\tfmt.Println(\"Apply plan canceled\")\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\t_, err = buildPlanInlineFn(autoConfirm, nil)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"failed to build plan: %v\", err)\n\t\t}\n\t}\n\n\tpaths, err := fs.GetProjectPaths(fs.ProjectRoot)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error getting project paths: %v\", err)\n\t}\n\n\tanyOutdated, didUpdate, err := CheckOutdatedContextWithOutput(true, autoConfirm, nil, paths)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error checking outdated context: %v\", err)\n\t}\n\n\tif anyOutdated && !didUpdate {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"Apply plan canceled\")\n\t\tos.Exit(0)\n\t}\n\n\tterm.ResumeSpinner()\n\n\tcurrentPlanFiles := currentPlanState.CurrentPlanFiles\n\tisRepo := fs.ProjectRootIsGitRepo()\n\n\ttoApply := currentPlanFiles.Files\n\ttoRemove := currentPlanFiles.Removed\n\thasExec := currentPlanFiles.Files[\"_apply.sh\"] != \"\"\n\n\tlog.Printf(\"Files to apply: %d, Has exec script: %v\", len(toApply), hasExec)\n\n\tif len(toApply) == 0 && !hasExec {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No changes to apply\")\n\t\treturn\n\t}\n\n\thasFileChanges := !hasExec || len(toApply) > 1\n\n\tvar toRollback *types.ApplyRollbackPlan\n\tvar updatedFiles []string\n\n\tonErr := func(errMsg string, errArgs ...interface{}) {\n\t\tterm.StopSpinner()\n\t\t// if toRollback != nil && toRollback.HasChanges() {\n\t\t// \tRollback(toRollback, true)\n\t\t// }\n\t\tterm.OutputErrorAndExit(errMsg, errArgs...)\n\t}\n\n\tonGitErr := func(errMsg, unformattedErrMsg string) {\n\t\tterm.StopSpinner()\n\t\tfmt.Println()\n\t\tterm.OutputSimpleError(errMsg, unformattedErrMsg)\n\t}\n\n\tlog.Println(\"Has file changes:\", hasFileChanges)\n\n\tif hasFileChanges {\n\t\tif !autoConfirm {\n\t\t\tlog.Println(\"Asking user to confirm applying changes\")\n\n\t\t\tterm.StopSpinner()\n\t\t\tnumToApply := len(toApply)\n\t\t\tsuffix := \"\"\n\t\t\tif numToApply > 1 {\n\t\t\t\tsuffix = \"s\"\n\t\t\t}\n\t\t\tshouldContinue, err := term.ConfirmYesNo(\"Apply changes to %d file%s?\", numToApply, suffix)\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"failed to get confirmation user input: %s\", err)\n\t\t\t}\n\n\t\t\tif !shouldContinue {\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t\tterm.ResumeSpinner()\n\t\t}\n\n\t\tlog.Println(\"Applying plan files\")\n\n\t\tif hasExec {\n\t\t\tterm.StopSpinner()\n\t\t\tfmt.Println(\"🔄 Tentatively applying changes\")\n\t\t\tterm.ResumeSpinner()\n\t\t}\n\n\t\tupdatedFiles, toRollback, err = ApplyFiles(toApply, toRemove, paths)\n\n\t\tif err != nil {\n\t\t\tonErr(\"failed to apply files: %s\", err)\n\t\t}\n\n\t\tlog.Println(\"Applying plan files complete\")\n\t}\n\n\tonExecSuccess := func() {\n\t\tterm.StartSpinner(\"\")\n\t\tcommitSummary, err := apiApplyPlan(planId, branch)\n\n\t\tif err != nil {\n\t\t\tonErr(\"apply plan server error: %s\", err)\n\t\t}\n\n\t\tif len(updatedFiles) == 0 {\n\t\t\tterm.StopSpinner()\n\t\t\tfmt.Println(\"✅ Applied changes, but no files were updated\")\n\t\t} else {\n\t\t\tappliedMsgFn := func() {\n\t\t\t\tsuffix := \"\"\n\t\t\t\tif len(updatedFiles) > 1 {\n\t\t\t\t\tsuffix = \"s\"\n\t\t\t\t}\n\t\t\t\tfmt.Printf(\"✅ Applied changes, %d file%s updated\\n\", len(updatedFiles), suffix)\n\t\t\t\tfor _, file := range updatedFiles {\n\t\t\t\t\tfmt.Println(\" • 📄 \" + file)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif isRepo && !noCommit {\n\t\t\t\tterm.StopSpinner()\n\t\t\t\tgitErr := commitApplied(autoCommit, commitSummary, updatedFiles, currentPlanState)\n\t\t\t\tappliedMsgFn()\n\t\t\t\tif gitErr != nil {\n\t\t\t\t\tonGitErr(\"Failed to commit changes:\", gitErr.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tterm.StopSpinner()\n\t\t\t\tappliedMsgFn()\n\t\t\t}\n\t\t}\n\t}\n\n\tif _, ok := toApply[\"_apply.sh\"]; ok && !noExec {\n\t\thandleApplyScript(params, toApply, onErr, toRollback, onExecFail, attempt, onExecSuccess)\n\t} else {\n\t\tonExecSuccess()\n\t}\n}\n\nfunc handleApplyScript(\n\tparams ApplyPlanParams,\n\ttoApply map[string]string,\n\tonErr types.OnErrFn,\n\ttoRollback *types.ApplyRollbackPlan,\n\tonExecFail types.OnApplyExecFailFn,\n\tattempt int,\n\tonSuccess func(),\n) {\n\tlog.Println(\"Handling apply script\")\n\n\tterm.StopSpinner()\n\n\tcolor.New(term.ColorHiCyan, color.Bold).Println(\"🚀 Commands to execute 👇\")\n\n\tvar content string\n\tif params.ExecCommand != \"\" {\n\t\tcontent = params.ExecCommand\n\t} else {\n\t\tcontent = toApply[\"_apply.sh\"]\n\t}\n\n\tmd, err := term.GetMarkdown(\"```bash\\n\" + content + \"\\n```\")\n\n\tif err != nil {\n\t\tonErr(\"failed to get markdown representation: %s\", err)\n\t}\n\n\tfmt.Println(strings.TrimSpace(md))\n\n\tlog.Println(\"Asking user to confirm executing apply script\")\n\n\tvar confirmed bool\n\tif params.ApplyFlags.AutoExec {\n\t\tconfirmed = true\n\t} else {\n\t\tconfirmed, err = term.ConfirmYesNo(\"Execute now?\")\n\t\tif err != nil {\n\t\t\tonErr(\"failed to get confirmation user input: %s\", err)\n\t\t}\n\t}\n\n\tif confirmed {\n\t\tlog.Println(\"Executing apply script\")\n\t\texecApplyScript(params, toApply, onErr, toRollback, onExecFail, attempt, onSuccess)\n\t} else {\n\t\tif toRollback != nil && toRollback.HasChanges() {\n\t\t\tres, err := term.SelectFromList(\"Skipping execution. Apply file changes or roll back?\", []string{string(types.ApplyRollbackOptionKeep), string(types.ApplyRollbackOptionRollback)})\n\n\t\t\tif err != nil {\n\t\t\t\tonErr(\"failed to get rollback confirmation user input: %s\", err)\n\t\t\t}\n\n\t\t\tif res == string(types.ApplyRollbackOptionRollback) {\n\t\t\t\tRollback(toRollback, true)\n\t\t\t\tos.Exit(0)\n\t\t\t} else {\n\t\t\t\tonSuccess()\n\t\t\t}\n\t\t} else {\n\t\t\tfmt.Println(\"🙅‍♂️ Skipped execution\")\n\t\t\tfmt.Println(\"🤷‍♂️ No changes to apply\")\n\t\t}\n\t}\n}\n\nvar shellShebangs = map[string]string{\n\t\"/bin/bash\": `#!/bin/bash\n`,\n\t\"/bin/zsh\": `#!/bin/zsh\n`,\n}\n\nvar applyScriptErrorHandling = map[string]string{\n\t\"/bin/bash\": `set -euo pipefail`,\n\t\"/bin/zsh\":  `set -euo pipefail`,\n}\n\nfunc execApplyScript(\n\tparams ApplyPlanParams,\n\ttoApply map[string]string,\n\tonErr types.OnErrFn,\n\ttoRollback *types.ApplyRollbackPlan,\n\tonExecFail types.OnApplyExecFailFn,\n\tattempt int,\n\tonSuccess func(),\n) {\n\tlog.Println(\"Executing apply script\")\n\n\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"👉 For long-running commands, use ctrl+c to exit\")\n\tcolor.New(term.ColorHiCyan, color.Bold).Println(\"🚀 Executing... output below 👇\")\n\n\tfmt.Println()\n\n\tvar content string\n\n\tif params.ExecCommand != \"\" {\n\t\tcontent = params.ExecCommand\n\t} else {\n\t\tcontent = toApply[\"_apply.sh\"]\n\t}\n\n\tscriptPath := filepath.Join(fs.ProjectRoot, \"_apply.sh\")\n\tlines := strings.Split(content, \"\\n\")\n\tfilteredLines := []string{}\n\n\tfor _, line := range lines {\n\t\ttrimmed := strings.TrimSpace(line)\n\t\tif strings.HasPrefix(trimmed, \"#!/\") {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(trimmed, \"set -\") || strings.HasSuffix(trimmed, \"pipefail\") {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(trimmed, \"trap\") {\n\t\t\tcontinue\n\t\t}\n\t\tfilteredLines = append(filteredLines, line)\n\t}\n\n\t// Detect shell\n\tshell := os.Getenv(\"SHELL\")\n\tif shell == \"\" {\n\t\tshell = \"/bin/bash\" // fallback\n\t}\n\n\t// Get appropriate header\n\tshebang := shellShebangs[shell]\n\tif shebang == \"\" {\n\t\tshebang = shellShebangs[\"/bin/bash\"] // fallback if shell not supported\n\t}\n\terrorHandling := applyScriptErrorHandling[shell]\n\n\tif errorHandling == \"\" {\n\t\terrorHandling = applyScriptErrorHandling[\"/bin/bash\"] // fallback if shell not supported\n\t}\n\n\theader := shebang + \"\\n\" + errorHandling\n\tcontent = header + \"\\n\" + strings.Join(filteredLines, \"\\n\")\n\terr := os.WriteFile(scriptPath, []byte(content), 0755)\n\n\tif err != nil {\n\t\tonErr(\"failed to write _apply.sh: %s\", err)\n\t}\n\n\texecCmd := exec.Command(shell, \"-c\", scriptPath)\n\texecCmd.Dir = fs.ProjectRoot\n\texecCmd.Env = os.Environ()\n\texecCmd.Stdin = os.Stdin\n\n\t// Create a pipe for both stdout and stderr\n\tpipe, err := execCmd.StdoutPipe()\n\tif err != nil {\n\t\t// best effort cleanup\n\t\tos.Remove(scriptPath)\n\t\tonErr(\"failed to create stdout pipe: %s\", err)\n\t}\n\texecCmd.Stderr = execCmd.Stdout\n\n\t// Set platform-specific process attributes\n\tSetPlatformSpecificAttrs(execCmd)\n\n\tif err := execCmd.Start(); err != nil {\n\t\t// best effort cleanup\n\t\tos.Remove(scriptPath)\n\t\tonErr(\"failed to start command: %s\", err)\n\t}\n\n\tmaybeDeleteCgroup := MaybeIsolateCgroup(execCmd)\n\n\tpgid, err := syscall.Getpgid(execCmd.Process.Pid)\n\tif err != nil {\n\t\tlog.Printf(\"Getpgid error: %v\", err)\n\t} else {\n\t\tlog.Printf(\"Child PID=%d PGID=%d\", execCmd.Process.Pid, pgid)\n\t}\n\n\t// Create a context that we can cancel\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\t// Use atomic variable to prevent data races\n\tvar interrupted atomic.Bool\n\n\t// Handle SIGINT and SIGTERM\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)\n\n\tvar interruptHandled atomic.Bool\n\tvar interruptWG sync.WaitGroup\n\n\t// Start the interrupt handler goroutine\n\tinterruptWG.Add(1)\n\tgo func() {\n\t\tdefer interruptWG.Done()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase sig := <-sigChan:\n\t\t\t\tif interruptHandled.CompareAndSwap(false, true) {\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"👉 Caught interrupt. Exiting gracefully...\")\n\t\t\t\t\tinterrupted.Store(true)\n\n\t\t\t\t\tvar sysSig syscall.Signal\n\n\t\t\t\t\tswitch sig {\n\t\t\t\t\tcase os.Interrupt:\n\t\t\t\t\t\t// user pressed Ctrl+C\n\t\t\t\t\t\tsysSig = syscall.SIGINT\n\t\t\t\t\tcase syscall.SIGTERM:\n\t\t\t\t\t\t// a polite \"kill\" request\n\t\t\t\t\t\tsysSig = syscall.SIGTERM\n\t\t\t\t\tcase syscall.SIGHUP:\n\t\t\t\t\t\tsysSig = syscall.SIGHUP\n\t\t\t\t\tcase syscall.SIGQUIT:\n\t\t\t\t\t\tsysSig = syscall.SIGQUIT\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tsysSig = syscall.SIGINT\n\t\t\t\t\t}\n\n\t\t\t\t\tif err := KillProcessGroup(execCmd, sysSig); err != nil {\n\t\t\t\t\t\tlog.Printf(\"Failed to send signal %s to process group: %v\", sysSig, err)\n\t\t\t\t\t}\n\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-time.After(2 * time.Second):\n\t\t\t\t\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"👉 Commands didn't exit after 2 seconds. Sending SIGKILL.\")\n\t\t\t\t\t\tif err := KillProcessGroup(execCmd, syscall.SIGKILL); err != nil {\n\t\t\t\t\t\t\tlog.Printf(\"Failed to terminate process group: %v\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpipe.Close()\n\t\t\t\t\t\tif maybeDeleteCgroup != nil {\n\t\t\t\t\t\t\tmaybeDeleteCgroup()\n\t\t\t\t\t\t}\n\t\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\t\tif maybeDeleteCgroup != nil {\n\t\t\t\t\t\t\tmaybeDeleteCgroup()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase <-ctx.Done():\n\t\t\t\t// If no interrupts occurred, this will be the normal exit path\n\t\t\t\tif maybeDeleteCgroup != nil {\n\t\t\t\t\tmaybeDeleteCgroup()\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Read and display output in real-time\n\tscanner := bufio.NewScanner(pipe)\n\tvar outputBuilder strings.Builder\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tfmt.Println(line)\n\t\toutputBuilder.WriteString(line + \"\\n\")\n\t}\n\n\t// Check for scanner errors\n\tif scanErr := scanner.Err(); scanErr != nil {\n\t\tlog.Printf(\"⚠️ Scanner error reading subprocess output: %v\", scanErr)\n\t}\n\n\terr = execCmd.Wait()\n\n\t// Ensure interrupt handler fully completes before proceeding\n\tcancel()           // cancel the context, if not already\n\tinterruptWG.Wait() // wait until the interrupt handler goroutine finishes\n\tsignal.Stop(sigChan)\n\tclose(sigChan)\n\n\tsuccess := err == nil\n\n\tif interrupted.Load() {\n\t\tos.Remove(scriptPath)\n\n\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"👉 Execution interrupted\")\n\n\t\tdidSucceed, canceled, err := term.ConfirmYesNoCancel(\"Did the commands succeed?\")\n\n\t\tif err != nil {\n\t\t\tonErr(\"failed to get confirmation user input: %s\", err)\n\t\t}\n\n\t\tsuccess = didSucceed\n\n\t\tif canceled {\n\t\t\t// rollback and exit\n\t\t\tRollback(toRollback, true)\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\t// remove _apply.sh without overwriting err val\n\t{\n\t\terr := os.Remove(scriptPath)\n\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\tonErr(\"failed to remove _apply.sh: %s\", err)\n\t\t}\n\t}\n\n\tif !success {\n\t\tfmt.Println()\n\t\tcolor.New(term.ColorHiRed, color.Bold).Println(\"🚨 Commands failed\")\n\n\t\texitErr, ok := err.(*exec.ExitError)\n\t\tstatus := -1\n\t\tif ok {\n\t\t\tstatus = exitErr.ExitCode()\n\t\t}\n\t\tonExecFail(status, outputBuilder.String(), attempt, toRollback, onErr, onSuccess)\n\t} else {\n\t\tfmt.Println()\n\t\tfmt.Println(\"✅ Commands succeeded\")\n\t\tonSuccess()\n\t}\n}\n\nfunc apiApplyPlan(planId, branch string) (string, error) {\n\tauthVars := MustVerifyAuthVarsSilent(auth.Current.IntegratedModelsMode)\n\n\tvar commitSummary string\n\n\tlog.Println(\"Applying plan with API call\")\n\n\tcommitSummary, apiErr := api.Client.ApplyPlan(planId, branch, shared.ApplyPlanRequest{\n\t\tAuthVars: authVars,\n\t})\n\n\tif apiErr != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to set pending results applied: %s\", apiErr.Msg)\n\t}\n\n\treturn commitSummary, nil\n}\n\nfunc commitApplied(autoCommit bool, commitSummary string, updatedFiles []string, currentPlanState *shared.CurrentPlanState) (err error) {\n\tconfirmed := autoCommit\n\tif !autoCommit {\n\t\tfmt.Println(\"✏️  Plandex can commit these updates with an automatically generated message.\")\n\t\tfmt.Println()\n\t\t// 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.\")\n\t\t// fmt.Println()\n\t\tconfirmed, err = term.ConfirmYesNo(\"Commit Plandex updates now?\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get confirmation user input: %s\", err)\n\t\t}\n\t}\n\n\tif confirmed {\n\t\t// Commit the changes\n\t\tmsg := currentPlanState.PendingChangesSummaryForApply(commitSummary)\n\t\t// log.Println(\"Committing changes with message:\")\n\t\t// log.Println(msg)\n\t\t// spew.Dump(currentPlanState)\n\t\terr = GitAddAndCommitPaths(fs.ProjectRoot, msg, updatedFiles, true)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to commit changes: %s\", err.Error())\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc ApplyFiles(toApply map[string]string, toRemove map[string]bool, projectPaths *types.ProjectPaths) ([]string, *types.ApplyRollbackPlan, error) {\n\tvar updatedFiles []string\n\ttoRevert := map[string]types.ApplyReversion{}\n\tvar toRemoveOnRollback []string\n\n\tvar mu sync.Mutex\n\ttotalOps := len(toApply) + len(toRemove)\n\terrCh := make(chan error, totalOps)\n\n\tfor path, content := range toApply {\n\t\tif path == \"_apply.sh\" {\n\t\t\terrCh <- nil\n\t\t\tcontinue\n\t\t}\n\t\tgo func(path, content string) {\n\t\t\t// Compute destination path\n\t\t\tdstPath := filepath.Join(fs.ProjectRoot, path)\n\t\t\tcontent = strings.ReplaceAll(content, \"\\\\`\\\\`\\\\`\", \"```\")\n\t\t\t// Check if the file exists\n\t\t\tvar exists bool\n\t\t\tvar mode os.FileMode\n\t\t\tinfo, err := os.Stat(dstPath)\n\t\t\tif err == nil {\n\t\t\t\texists = true\n\t\t\t\tmode = info.Mode()\n\t\t\t} else {\n\t\t\t\tif os.IsNotExist(err) {\n\t\t\t\t\texists = false\n\t\t\t\t} else {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to check if %s exists: %s\", dstPath, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif exists {\n\t\t\t\t// read file content\n\t\t\t\tbytes, err := os.ReadFile(dstPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to read %s: %s\", dstPath, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Check if the file has changed\n\t\t\t\tif string(bytes) == content {\n\t\t\t\t\t// log.Println(\"File is unchanged, skipping\")\n\t\t\t\t\terrCh <- nil\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tupdatedFiles = append(updatedFiles, path)\n\t\t\t\t\ttoRevert[dstPath] = types.ApplyReversion{Content: string(bytes), Mode: mode}\n\t\t\t\t\tmu.Unlock()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tmu.Lock()\n\t\t\t\tupdatedFiles = append(updatedFiles, path)\n\t\t\t\ttoRemoveOnRollback = append(toRemoveOnRollback, dstPath)\n\t\t\t\tmu.Unlock()\n\t\t\t\t// Create the directory if it doesn't exist\n\t\t\t\terr := os.MkdirAll(filepath.Dir(dstPath), 0755)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to create directory %s: %s\", filepath.Dir(dstPath), err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Write the file\n\t\t\terr = os.WriteFile(dstPath, []byte(content), 0644)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to write %s: %s\", dstPath, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path, content)\n\t}\n\n\tfor path, remove := range toRemove {\n\t\tgo func(path string, remove bool) {\n\t\t\tif !remove {\n\t\t\t\terrCh <- nil\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Compute destination path\n\t\t\tdstPath := filepath.Join(fs.ProjectRoot, path)\n\t\t\t// Check if the file exists\n\t\t\tvar exists bool\n\t\t\tvar mode os.FileMode\n\t\t\tinfo, err := os.Stat(dstPath)\n\t\t\tif err == nil {\n\t\t\t\texists = true\n\t\t\t\tmode = info.Mode()\n\t\t\t} else {\n\t\t\t\tif os.IsNotExist(err) {\n\t\t\t\t\texists = false\n\t\t\t\t} else {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to check if %s exists: %s\", dstPath, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tif exists {\n\t\t\t\tcontent, err := os.ReadFile(dstPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to read %s: %s\", dstPath, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\terr = os.Remove(dstPath)\n\t\t\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to remove %s: %s\", dstPath, err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tmu.Lock()\n\t\t\t\ttoRevert[dstPath] = types.ApplyReversion{Content: string(content), Mode: mode}\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path, remove)\n\t}\n\n\tfor i := 0; i < totalOps; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\n\treturn updatedFiles, &types.ApplyRollbackPlan{\n\t\tPreviousProjectPaths: projectPaths,\n\t\tToRevert:             toRevert,\n\t\tToRemove:             toRemoveOnRollback,\n\t}, nil\n}\n\nfunc Rollback(rollbackPlan *types.ApplyRollbackPlan, msg bool) error {\n\tnumRoutines := len(rollbackPlan.ToRevert) + len(rollbackPlan.ToRemove) + 1\n\terrCh := make(chan error, numRoutines)\n\tfor path, revert := range rollbackPlan.ToRevert {\n\t\tgo func(path string, revert types.ApplyReversion) {\n\t\t\terr := os.WriteFile(path, []byte(revert.Content), revert.Mode)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to write %s: %s\", path, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path, revert)\n\t}\n\n\tfor _, path := range rollbackPlan.ToRemove {\n\t\tgo func(path string) {\n\t\t\terr := os.Remove(path)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to remove %s: %s\", path, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path)\n\t}\n\n\tgo func() {\n\t\tvar err error\n\t\tupdatedProjectPaths, err := fs.GetProjectPaths(fs.ProjectRoot)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"failed to get project paths: %v\", err)\n\t\t}\n\t\tvar toRemove []string\n\t\tfor path := range updatedProjectPaths.AllPaths {\n\t\t\tif _, ok := rollbackPlan.PreviousProjectPaths.AllPaths[path]; !ok {\n\t\t\t\ttoRemove = append(toRemove, path)\n\t\t\t}\n\t\t}\n\t\tpathsErrCh := make(chan error, len(toRemove))\n\t\tfor _, path := range toRemove {\n\t\t\tgo func(path string) {\n\t\t\t\terr := os.Remove(path)\n\t\t\t\tpathsErrCh <- err\n\t\t\t}(path)\n\t\t}\n\t\tfor range toRemove {\n\t\t\terr := <-pathsErrCh\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to remove %s: %s\", toRemove, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\terrs := []error{}\n\tfor i := 0; i < numRoutines; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\tif len(errs) > 0 {\n\t\treturn fmt.Errorf(\"failed to rollback: %s\", errs)\n\t}\n\tif msg {\n\t\tfmt.Println(\"🚫 Rolled back all changes\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/lib/apply_cgroup_linux.go",
    "content": "//go:build linux\n// +build linux\n\npackage lib\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os/exec\"\n\t\"time\"\n\n\tsystemdDbus \"github.com/coreos/go-systemd/v22/dbus\"\n\t\"github.com/godbus/dbus/v5\"\n\t\"github.com/google/uuid\"\n)\n\nconst cgroupCallTimeout = 1 * time.Second\n\nfunc MaybeIsolateCgroup(cmd *exec.Cmd) (deleteFn func()) {\n\tnoop := func() {}\n\tpid := cmd.Process.Pid\n\n\t// 1. Connect to the user manager (no prompt on typical distros).\n\tctx, _ := context.WithTimeout(context.Background(), cgroupCallTimeout)\n\n\tconn, err := systemdDbus.NewUserConnectionContext(ctx)\n\tif err != nil {\n\t\tlog.Printf(\"⚠️  Could not connect to user systemd manager. No cgroup isolation for PID %d. Error: %v\", pid, err)\n\t\treturn noop\n\t}\n\t// We'll keep 'conn' open while scope is active. The scope isn't strictly tied\n\t// to the connection's lifetime, but it's nice to keep it in case we want to stop the unit.\n\n\tscopeName := fmt.Sprintf(\"plandex-%s.scope\", uuid.New().String())\n\n\tprops := []systemdDbus.Property{\n\t\tsystemdDbus.PropDescription(\"Plandex user-scope isolation\"),\n\t\t// Under system manager, user.slice means “treat it as a user process.”\n\t\t// If this is truly in the user manager, it may ignore the slice or map it differently.\n\t\tsystemdDbus.PropSlice(\"user.slice\"),\n\n\t\t// KillMode=control-group: stopping the scope kills all processes in cgroup.\n\t\tsystemdDbus.Property{Name: \"KillMode\", Value: dbus.MakeVariant(\"control-group\")},\n\n\t\t// Attach the existing process by PID\n\t\tsystemdDbus.PropPids(uint32(pid)),\n\n\t\t// Optional: auto-remove the scope once no processes remain.\n\t\tsystemdDbus.Property{Name: \"CollectMode\", Value: dbus.MakeVariant(\"inactive-or-failed\")},\n\t}\n\n\t_, err = conn.StartTransientUnitContext(ctx, scopeName, \"replace\", props, nil)\n\tif err != nil {\n\t\t// Fallback, no isolation\n\t\tlog.Printf(\"⚠️  Failed to start transient scope for PID %d: %v\", pid, err)\n\t\treturn noop\n\t}\n\n\treturn func() {\n\t\t// Close the connection to the user manager.\n\t\tdefer conn.Close()\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), cgroupCallTimeout)\n\t\tdefer cancel()\n\n\t\t// Attempt to stop the scope (killing all processes if any remain).\n\t\t_, stopErr := conn.StopUnitContext(ctx, scopeName, \"replace\", nil)\n\t\tif stopErr != nil {\n\t\t\tlog.Printf(\"⚠️  Failed to stop scope %s: %v\", scopeName, stopErr)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/cli/lib/apply_cgroup_other.go",
    "content": "//go:build !linux\n// +build !linux\n\npackage lib\n\nimport \"os/exec\"\n\nfunc MaybeIsolateCgroup(cmd *exec.Cmd) (deleteFn func()) {\n\treturn func() {}\n}\n"
  },
  {
    "path": "app/cli/lib/apply_proc.go",
    "content": "package lib\n\nimport (\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc SetPlatformSpecificAttrs(cmd *exec.Cmd) {\n\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tSetpgid: true,\n\t}\n}\n\nfunc KillProcessGroup(cmd *exec.Cmd, signal syscall.Signal) error {\n\treturn syscall.Kill(-cmd.Process.Pid, signal)\n}\n"
  },
  {
    "path": "app/cli/lib/build.go",
    "content": "package lib\n\nimport shared \"plandex-shared\"\n\nvar buildPlanInlineFn func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error)\n\nfunc SetBuildPlanInlineFn(fn func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error)) {\n\tbuildPlanInlineFn = fn\n}\n"
  },
  {
    "path": "app/cli/lib/claude_max.go",
    "content": "package lib\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"plandex-cli/ui\"\n\tshared \"plandex-shared\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n)\n\nconst claudeMaxClientId = \"9d1c250a-e61b-44d9-88ed-5944d1962f5e\"\nconst claudeMaxScopes = \"org:create_api_key user:profile user:inference\"\nconst claudeMaxRedirect = \"https://console.anthropic.com/oauth/code/callback\"\nconst claudeMaxTokenUrl = \"https://console.anthropic.com/v1/oauth/token\"\n\nfunc hasAnthropicModels(opts shared.ModelProviderOptions) bool {\n\tfor _, opt := range opts {\n\t\tif opt.Config.Provider == shared.ModelProviderAnthropic {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc promptClaudeMaxIfNeeded() bool {\n\torgUserConfig := MustGetOrgUserConfig()\n\tif orgUserConfig.PromptedClaudeMax {\n\t\treturn false\n\t}\n\n\tterm.StopSpinner()\n\tfmt.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\")\n\n\tres, err := term.ConfirmYesNo(\"Connect your Claude subscription?\")\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error confirming claude connection: %v\", err)\n\t}\n\n\t// update org user config to avoid prompting again\n\torgUserConfig.PromptedClaudeMax = true\n\tMustUpdateOrgUserConfig(*orgUserConfig)\n\n\tif !res {\n\t\tfmt.Println()\n\t\tfmt.Println(\"To connect a Claude subscription later, run:\\n\" + term.ShowCmd(\"connect-claude\"))\n\t\tfmt.Println()\n\t\treturn false\n\t}\n\n\tConnectClaudeMax()\n\n\treturn true\n}\n\nfunc connectClaudeMaxIfNeeded() bool {\n\taccountCreds, err := GetAccountCredentials()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting account credentials: %v\", err)\n\t}\n\n\tif accountCreds == nil || accountCreds.ClaudeMax == nil {\n\n\t\tterm.StopSpinner()\n\t\tfmt.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\")\n\n\t\tres, err := term.ConfirmYesNo(\"Connect your Claude subscription?\")\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error confirming claude connection: %v\", err)\n\t\t}\n\n\t\tif res {\n\t\t\tConnectClaudeMax()\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc refreshClaudeMaxCredsIfNeeded() {\n\taccountCreds, err := GetAccountCredentials()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting account credentials: %v\", err)\n\t}\n\tif accountCreds == nil || accountCreds.ClaudeMax == nil {\n\t\treturn\n\t}\n\tif !needsRefresh(accountCreds.ClaudeMax) || accountCreds.ClaudeMax.RefreshToken == \"\" {\n\t\treturn\n\t}\n\n\t_, status, err := refreshCreds(accountCreds)\n\tif err != nil {\n\t\tif status == http.StatusUnauthorized {\n\t\t\tterm.StopSpinner()\n\t\t\tcolor.New(color.FgHiYellow, color.Bold).Println(\"⚠️ Your Claude subscription's connection has been lost\")\n\t\t\tfmt.Println()\n\t\t\tres, err := term.ConfirmYesNo(\"Reconnect your Claude subscription?\")\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error confirming claude connection: %v\", err)\n\t\t\t}\n\t\t\tif !res {\n\t\t\t\taccountCreds.ClaudeMax = nil\n\t\t\t\tif err := SetAccountCredentials(accountCreds); err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error clearing Claude credentials: %v\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tConnectClaudeMax()\n\t\t\treturn\n\t\t}\n\t\tterm.OutputErrorAndExit(\"Error refreshing Claude credentials: %v\", err)\n\t}\n}\n\nfunc ConnectClaudeMax() {\n\tconnectClaudeMaxOauth()\n\n\tterm.StartSpinner(\"\")\n\torgUserConfig := MustGetOrgUserConfig()\n\torgUserConfig.UseClaudeSubscription = true\n\tMustUpdateOrgUserConfig(*orgUserConfig)\n\n\tterm.StopSpinner()\n\tfmt.Println()\n\tfmt.Println(\"✅ Your Claude subscription is now connected\")\n\tfmt.Println()\n\n\tfmt.Println(\"To disconnect, run:\\n\" + term.ShowCmd(\"disconnect-claude\"))\n\tfmt.Println()\n}\n\nfunc DisconnectClaudeMax() {\n\tterm.StartSpinner(\"\")\n\n\torgUserConfig := MustGetOrgUserConfig()\n\torgUserConfig.UseClaudeSubscription = false\n\tMustUpdateOrgUserConfig(*orgUserConfig)\n\n\taccountCreds, err := GetAccountCredentials()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting account credentials: %v\", err)\n\t}\n\n\tif accountCreds != nil {\n\t\taccountCreds.ClaudeMax = nil\n\t\tif err := SetAccountCredentials(accountCreds); err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error clearing Claude credentials: %v\", err)\n\t\t}\n\t}\n\n\tterm.StopSpinner()\n\n\tfmt.Println(\"✅ Your Claude subscription has been disconnected\")\n\tfmt.Println()\n\tfmt.Println(\"To reconnect, run:\\n\" + term.ShowCmd(\"connect-claude\"))\n\tfmt.Println()\n}\n\nfunc connectClaudeMaxOauth() {\n\tverifier, err := genCodeVerifier()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error generating code verifier: %v\", err)\n\t}\n\tchallenge := sha256Base64(verifier)\n\n\tstate, err := genCodeVerifier()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error generating state: %v\", err)\n\t}\n\n\tauthURL := fmt.Sprintf(\n\t\t\"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\",\n\t\tclaudeMaxClientId, url.QueryEscape(claudeMaxScopes), url.QueryEscape(claudeMaxRedirect), challenge, state,\n\t)\n\n\tterm.StopSpinner()\n\n\tfmt.Println()\n\tui.OpenURL(\"Opening Claude authentication page in your default browser...\", authURL)\n\tfmt.Println()\n\n\tcolor.New(color.FgHiGreen, color.Bold).Println(\"📋 Click 'Authorize', copy the Authentication Code, then paste it below.\")\n\tfmt.Println()\n\n\tpastedCode, err := term.GetUserPasswordInput(\"Authentication Code:\")\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error reading pasted authentication code: %v\", err)\n\t}\n\n\tsplit := strings.SplitN(pastedCode, \"#\", 2)\n\tif len(split) != 2 {\n\t\tterm.OutputErrorAndExit(\"Invalid authentication code: %s\", pastedCode)\n\t}\n\tcode := split[0]\n\tpastedState := split[1]\n\n\tif code == \"\" || pastedState != state {\n\t\tterm.OutputErrorAndExit(\"Claude authentication failed: missing or mismatched oauth code/state\")\n\t}\n\tterm.StartSpinner(\"\")\n\n\ttokens, err := exchangeCode(code, verifier, state)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error exchanging code: %v\", err)\n\t}\n\n\tcreds := types.AccountCredentials{\n\t\tClaudeMax: &types.OauthCreds{\n\t\t\tOauthResponse: *tokens,\n\t\t\tExpiresAt:     time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second),\n\t\t},\n\t}\n\tif err := SetAccountCredentials(&creds); err != nil {\n\t\tterm.OutputErrorAndExit(\"Error setting account credentials: %v\", err)\n\t}\n}\n\nfunc exchangeCode(code, verifier, state string) (*types.OauthResponse, error) {\n\tbody, _ := json.Marshal(map[string]any{\n\t\t\"grant_type\":    \"authorization_code\",\n\t\t\"code\":          code,\n\t\t\"state\":         state,\n\t\t\"code_verifier\": verifier,\n\t\t\"redirect_uri\":  claudeMaxRedirect,\n\t\t\"client_id\":     claudeMaxClientId,\n\t})\n\treq, err := http.NewRequest(\"POST\", claudeMaxTokenUrl, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"token exchange failed - error creating request: %s\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"anthropic-beta\", shared.AnthropicClaudeMaxBetaHeader)\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"token exchange failed - error reading body: %s\", err)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"token exchange failed - status: %d, body: %s\", resp.StatusCode, b)\n\t}\n\tvar t types.OauthResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&t); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &t, nil\n}\n\nfunc genCodeVerifier() (string, error) {\n\tbuf := make([]byte, 32)\n\tif _, err := rand.Read(buf); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(buf), nil\n}\n\nfunc sha256Base64(verifier string) string {\n\tsum := sha256.Sum256([]byte(verifier))\n\treturn base64.RawURLEncoding.EncodeToString(sum[:])\n}\n\nfunc needsRefresh(creds *types.OauthCreds) bool {\n\t// refresh an hour early so we can make multiple calls before it expires\n\treturn time.Now().After(creds.ExpiresAt.Add(-1 * time.Hour))\n}\n\nfunc refreshCreds(accountCreds *types.AccountCredentials) (*types.OauthCreds, int, error) {\n\tcreds := accountCreds.ClaudeMax\n\tif creds == nil {\n\t\treturn nil, 0, fmt.Errorf(\"no stored Claude credentials\")\n\t}\n\n\tbody, err := json.Marshal(map[string]any{\n\t\t\"grant_type\":    \"refresh_token\",\n\t\t\"refresh_token\": creds.RefreshToken,\n\t\t\"client_id\":     claudeMaxClientId,\n\t})\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - marshal: %w\", err)\n\t}\n\n\treq, err := http.NewRequest(\"POST\", claudeMaxTokenUrl, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - create request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"anthropic-beta\", shared.AnthropicClaudeMaxBetaHeader)\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - http: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, 0, fmt.Errorf(\"refresh failed - read body: %w\", err)\n\t\t}\n\t\treturn nil, resp.StatusCode, fmt.Errorf(\"refresh failed - status %d: %s\", resp.StatusCode, b)\n\t}\n\n\tvar r types.OauthResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&r); err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - decode: %w\", err)\n\t}\n\n\tnewCreds := &types.OauthCreds{\n\t\tOauthResponse: r,\n\t\tExpiresAt:     time.Now().Add(time.Duration(r.ExpiresIn) * time.Second),\n\t}\n\n\t// persist updated creds\n\taccountCreds.ClaudeMax = newCreds\n\tif err := SetAccountCredentials(accountCreds); err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"refresh failed - save: %w\", err)\n\t}\n\n\treturn newCreds, resp.StatusCode, nil\n}\n"
  },
  {
    "path": "app/cli/lib/context_auto_load.go",
    "content": "package lib\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/types\"\n\tshared \"plandex-shared\"\n\t\"sync\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc AutoLoadContextFiles(ctx context.Context, files []string) (string, error) {\n\tcontexts, err := api.Client.ListContext(CurrentPlanId, CurrentBranch)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get contexts: %v\", err)\n\t}\n\n\tvar totalSize int64\n\ttotalContexts := len(contexts)\n\n\tfor _, context := range contexts {\n\t\ttotalSize += context.BodySize\n\t}\n\n\tloadContextReqsByIndex := make(map[int]*shared.LoadContextParams)\n\tfilesSkippedTooLarge := []filePathWithSize{}\n\tfilesSkippedAfterSizeLimit := []string{}\n\n\tvar mu sync.Mutex\n\terrCh := make(chan error, len(files))\n\n\tfor i, path := range files {\n\t\ttotalContexts++\n\t\tif totalContexts > shared.MaxContextCount {\n\t\t\tlog.Println(\"Skipping file\", path, \"because it would exceed the max context count\", totalContexts)\n\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)\n\t\t\terrCh <- nil\n\t\t\tcontinue\n\t\t}\n\n\t\tgo func(index int, path string) {\n\t\t\tfileInfo, err := os.Stat(path)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to get file info for %s: %v\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif fileInfo.IsDir() {\n\t\t\t\tlog.Println(\"Skipping directory\", path)\n\t\t\t\terrCh <- nil // skip directories\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsize := fileInfo.Size()\n\n\t\t\tmu.Lock()\n\t\t\tif size > shared.MaxContextBodySize {\n\t\t\t\tlog.Println(\"Skipping file\", path, \"because it's too large\", size)\n\t\t\t\tfilesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: path, Size: size})\n\t\t\t\tmu.Unlock()\n\t\t\t\terrCh <- nil\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif totalSize+size > shared.MaxTotalContextSize {\n\t\t\t\tlog.Println(\"Skipping file\", path, \"because it would exceed the max context body size\", totalSize+size)\n\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)\n\t\t\t\tmu.Unlock()\n\t\t\t\terrCh <- nil\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttotalSize += size\n\t\t\tmu.Unlock()\n\n\t\t\tb, err := os.ReadFile(path)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to read file %s: %v\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar contextType shared.ContextType\n\t\t\tisImage := shared.IsImageFile(path)\n\t\t\tif isImage {\n\t\t\t\tcontextType = shared.ContextImageType\n\t\t\t} else {\n\t\t\t\tcontextType = shared.ContextFileType\n\t\t\t}\n\n\t\t\tvar imageDetail openai.ImageURLDetail\n\t\t\tif isImage {\n\t\t\t\timageDetail = openai.ImageURLDetailHigh\n\t\t\t}\n\n\t\t\tvar body string\n\t\t\tif isImage {\n\t\t\t\tbody = base64.StdEncoding.EncodeToString(b)\n\t\t\t} else {\n\t\t\t\tbody = string(shared.NormalizeEOL(b))\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tloadContextReqsByIndex[index] = &shared.LoadContextParams{\n\t\t\t\tContextType: contextType,\n\t\t\t\tFilePath:    path,\n\t\t\t\tName:        path,\n\t\t\t\tBody:        body,\n\t\t\t\tAutoLoaded:  true,\n\t\t\t\tImageDetail: imageDetail,\n\t\t\t}\n\t\t\tmu.Unlock()\n\t\t\terrCh <- nil\n\t\t}(i, path)\n\t}\n\n\tfor range files {\n\t\tif e := <-errCh; e != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to load context: %v\", e)\n\t\t}\n\t}\n\n\t// Convert map back to ordered slice\n\tloadContextReqs := make(shared.LoadContextRequest, 0, len(loadContextReqsByIndex))\n\tfor i := 0; i < len(files); i++ {\n\t\tif req := loadContextReqsByIndex[i]; req != nil {\n\t\t\tloadContextReqs = append(loadContextReqs, req)\n\t\t}\n\t}\n\n\t// 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\n\tres, apiErr := api.Client.AutoLoadContext(ctx, CurrentPlanId, CurrentBranch, loadContextReqs)\n\tif apiErr != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to load context: %v\", apiErr.Msg)\n\t}\n\n\tif res.MaxTokensExceeded {\n\t\toverage := res.TotalTokens - res.MaxTokens\n\t\treturn \"\", fmt.Errorf(\"update would add %d 🪙 and exceed token limit (%d) by %d 🪙\", res.TokensAdded, res.MaxTokens, overage)\n\t}\n\n\tmsg := res.Msg\n\n\t// Print skip info if any\n\tif len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 {\n\t\tmsg += \"\\n\\n\" + getSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit, nil, nil)\n\t}\n\n\treturn msg, nil\n}\n\nfunc MustLoadAutoContextMap() {\n\tMustLoadContext([]string{\".\"}, &types.LoadContextParams{\n\t\tDefsOnly:          true,\n\t\tSkipIgnoreWarning: true,\n\t\tAutoLoaded:        true,\n\t})\n}\n"
  },
  {
    "path": "app/cli/lib/context_conflict.go",
    "content": "package lib\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/fatih/color\"\n)\n\nfunc checkContextConflicts(filesByPath map[string]string) (bool, error) {\n\t// log.Println(\"Checking for context conflicts.\")\n\t// log.Println(spew.Sdump(filesByPath))\n\n\tcurrentPlan, err := api.Client.GetCurrentPlanState(CurrentPlanId, CurrentBranch)\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error getting current plan state: %v\", err)\n\t}\n\n\tconflictedPaths := currentPlan.PlanResult.FileResultsByPath.ConflictedPaths(filesByPath)\n\n\t// log.Println(\"Conflicted paths:\", conflictedPaths)\n\n\tif len(conflictedPaths) > 0 {\n\t\tterm.StopSpinner()\n\t\tcolor.New(color.Bold, term.ColorHiYellow).Println(\"⚠️  Some updates conflict with pending changes:\")\n\t\tfor path := range conflictedPaths {\n\t\t\tfmt.Println(\"📄 \" + path)\n\t\t}\n\n\t\tfmt.Println()\n\n\t\tres, err := term.ConfirmYesNo(\"Update context and rebuild changes?\")\n\n\t\tif err != nil {\n\t\t\treturn false, fmt.Errorf(\"error confirming update and rebuild: %v\", err)\n\t\t}\n\n\t\tif !res {\n\t\t\tfmt.Println(\"Context update canceled\")\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\treturn len(conflictedPaths) > 0, nil\n}\n"
  },
  {
    "path": "app/cli/lib/context_display.go",
    "content": "package lib\n\nimport shared \"plandex-shared\"\n\nfunc GetContextLabelAndIcon(contextType shared.ContextType) (string, string) {\n\tvar icon string\n\tvar lbl string\n\tswitch contextType {\n\tcase shared.ContextFileType:\n\t\ticon = \"📄\"\n\t\tlbl = \"file\"\n\tcase shared.ContextURLType:\n\t\ticon = \"🌎\"\n\t\tlbl = \"url\"\n\tcase shared.ContextDirectoryTreeType:\n\t\ticon = \"🗂 \"\n\t\tlbl = \"tree\"\n\tcase shared.ContextNoteType:\n\t\ticon = \"✏️ \"\n\t\tlbl = \"note\"\n\tcase shared.ContextPipedDataType:\n\t\ticon = \"↔️ \"\n\t\tlbl = \"piped\"\n\tcase shared.ContextImageType:\n\t\ticon = \"🖼️ \"\n\t\tlbl = \"image\"\n\tcase shared.ContextMapType:\n\t\ticon = \"🗺️ \"\n\t\tlbl = \"map\"\n\t}\n\n\treturn lbl, icon\n}\n\nfunc FindContextByIndex(contexts []*shared.Context, index int) *shared.Context {\n\t// Convert to 0-based index\n\tindex--\n\tif index < 0 || index >= len(contexts) {\n\t\treturn nil\n\t}\n\treturn contexts[index]\n}\n\nfunc FindContextByName(contexts []*shared.Context, name string) *shared.Context {\n\tfor _, ctx := range contexts {\n\t\tif ctx.Name == name || ctx.FilePath == name {\n\t\t\treturn ctx\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/lib/context_load.go",
    "content": "package lib\n\nimport (\n\t\"bufio\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"plandex-cli/url\"\n\t\"strings\"\n\t\"sync\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n)\n\nconst maxSkippedFileList = 20\n\nfunc MustLoadContext(resources []string, params *types.LoadContextParams) {\n\tif params.DefsOnly {\n\t\t// 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\n\t\tif len(resources) > 1 {\n\t\t\tterm.OutputErrorAndExit(\"Please load a single map directory at a time\")\n\t\t}\n\n\t\tterm.LongSpinnerWithWarning(\"🗺️  Building project map...\", \"🗺️  This can take a while in larger projects...\")\n\t} else if params.NamesOnly {\n\t\tterm.LongSpinnerWithWarning(\"🌳 Loading directory tree...\", \"🌳 This can take a while in larger projects...\")\n\t} else {\n\t\tterm.StartSpinner(\"📥 Loading context...\")\n\t}\n\n\tonErr := func(err error) {\n\t\tterm.StopSpinner()\n\t\tterm.OutputErrorAndExit(\"Failed to load context: %v\", err)\n\t}\n\n\tvar loadContextReq shared.LoadContextRequest\n\n\tfileInfo, err := os.Stdin.Stat()\n\tif err != nil {\n\t\tonErr(fmt.Errorf(\"failed to stat stdin: %v\", err))\n\t}\n\n\tvar authVars map[string]string\n\tvar openAIBase string\n\n\tif params.Note != \"\" || fileInfo.Mode()&os.ModeNamedPipe != 0 {\n\t\tauthVars = MustVerifyAuthVarsSilent(auth.Current.IntegratedModelsMode)\n\t}\n\n\tif params.Note != \"\" {\n\t\tloadContextReq = append(loadContextReq, &shared.LoadContextParams{\n\t\t\tContextType: shared.ContextNoteType,\n\t\t\tBody:        params.Note,\n\t\t\tApiKeys:     authVars,\n\t\t\tOpenAIBase:  openAIBase,\n\t\t\tOpenAIOrgId: os.Getenv(\"OPENAI_ORG_ID\"),\n\t\t\tSessionId:   params.SessionId,\n\t\t\tAutoLoaded:  params.AutoLoaded,\n\t\t})\n\t}\n\n\tif fileInfo.Mode()&os.ModeNamedPipe != 0 {\n\t\treader := bufio.NewReader(os.Stdin)\n\t\tpipedData, err := io.ReadAll(reader)\n\t\tif err != nil {\n\t\t\tonErr(fmt.Errorf(\"failed to read piped data: %v\", err))\n\t\t}\n\n\t\tif len(pipedData) > 0 {\n\t\t\tloadContextReq = append(loadContextReq, &shared.LoadContextParams{\n\t\t\t\tContextType: shared.ContextPipedDataType,\n\t\t\t\tBody:        string(pipedData),\n\t\t\t\tApiKeys:     authVars,\n\t\t\t\tOpenAIBase:  openAIBase,\n\t\t\t\tOpenAIOrgId: os.Getenv(\"OPENAI_ORG_ID\"),\n\t\t\t\tSessionId:   params.SessionId,\n\t\t\t\tAutoLoaded:  params.AutoLoaded,\n\t\t\t})\n\t\t}\n\t}\n\n\tvar inputUrls []string\n\tvar inputFilePaths []string\n\n\tif len(resources) > 0 {\n\t\tfor _, resource := range resources {\n\t\t\t// so far resources are either files or urls\n\t\t\tif url.IsValidURL(resource) {\n\t\t\t\tinputUrls = append(inputUrls, resource)\n\t\t\t} else {\n\t\t\t\tif strings.HasPrefix(resource, \".\"+string(os.PathSeparator)) {\n\t\t\t\t\tresource = resource[2:]\n\t\t\t\t}\n\n\t\t\t\tinputFilePaths = append(inputFilePaths, resource)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar contextMu sync.Mutex\n\n\terrCh := make(chan error)\n\tignoredPaths := make(map[string]string)\n\n\tmapFilesTruncatedTooLarge := []filePathWithSize{}\n\tmapFilesSkippedAfterSizeLimit := []string{}\n\n\t// We'll reuse these for all skipping, including directory-tree partial skipping and URLs\n\tfilesSkippedTooLarge := []filePathWithSize{}\n\tfilesSkippedAfterSizeLimit := []string{}\n\n\tvar totalSize int64\n\n\tnumRoutines := 0\n\n\t// filter out already loaded contexts\n\talreadyLoadedByComposite := make(map[string]*shared.Context)\n\texistingContexts, apiErr := api.Client.ListContext(CurrentPlanId, CurrentBranch)\n\tif apiErr != nil {\n\t\tonErr(fmt.Errorf(\"failed to list contexts: %v\", apiErr.Msg))\n\t}\n\n\texistsByComposite := make(map[string]*shared.Context)\n\tfor _, context := range existingContexts {\n\t\tswitch context.ContextType {\n\t\tcase shared.ContextFileType, shared.ContextDirectoryTreeType, shared.ContextMapType, shared.ContextImageType:\n\t\t\texistsByComposite[strings.Join([]string{string(context.ContextType), context.FilePath}, \"|\")] = context\n\t\tcase shared.ContextURLType:\n\t\t\texistsByComposite[strings.Join([]string{string(context.ContextType), context.Url}, \"|\")] = context\n\t\t}\n\t}\n\n\tvar cachedMapPaths map[string]bool\n\tvar cachedMapLoadRes *shared.LoadContextResponse\n\n\tmapInputShas := map[string]string{}\n\tmapInputTokens := map[string]int{}\n\tmapInputSizes := map[string]int64{}\n\n\ttoLoadMapPaths := []string{}\n\tmapInputPathsForPaths := map[string]string{}\n\n\tcurrentMapInputBatch := shared.FileMapInputs{}\n\tmapInputBatches := []shared.FileMapInputs{currentMapInputBatch}\n\n\tsem := make(chan struct{}, ContextMapMaxClientConcurrency)\n\n\tif len(inputFilePaths) > 0 {\n\n\t\tvar mapSize int64\n\n\t\tif params.DefsOnly {\n\t\t\tfor _, inputFilePath := range inputFilePaths {\n\t\t\t\tcomposite := strings.Join([]string{string(shared.ContextMapType), inputFilePath}, \"|\")\n\t\t\t\tif existsByComposite[composite] != nil {\n\t\t\t\t\talreadyLoadedByComposite[composite] = existsByComposite[composite]\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ttoLoadMapPaths = append(toLoadMapPaths, inputFilePath)\n\t\t\t}\n\n\t\t\tvar uncachedMapPaths []string\n\n\t\t\tres, err := api.Client.LoadCachedFileMap(CurrentPlanId, CurrentBranch, shared.LoadCachedFileMapRequest{\n\t\t\t\tFilePaths: toLoadMapPaths,\n\t\t\t})\n\n\t\t\tif err != nil {\n\t\t\t\tonErr(fmt.Errorf(\"error checking cached file map: %v\", err))\n\t\t\t}\n\n\t\t\tif res.LoadRes != nil {\n\t\t\t\tif res.LoadRes.MaxTokensExceeded {\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\toverage := res.LoadRes.TotalTokens - res.LoadRes.MaxTokens\n\n\t\t\t\t\tterm.OutputErrorAndExit(\"Update would add %d 🪙 and exceed token limit (%d) by %d 🪙\\n\", res.LoadRes.TokensAdded, res.LoadRes.MaxTokens, overage)\n\t\t\t\t}\n\n\t\t\t\tcachedMapLoadRes = res.LoadRes\n\t\t\t\tcachedMapPaths = res.CachedByPath\n\n\t\t\t\tfor _, path := range toLoadMapPaths {\n\t\t\t\t\tif !cachedMapPaths[path] {\n\t\t\t\t\t\tuncachedMapPaths = append(uncachedMapPaths, path)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tuncachedMapPaths = toLoadMapPaths\n\t\t\t}\n\n\t\t\ttoLoadMapPaths = uncachedMapPaths\n\t\t\tinputFilePaths = toLoadMapPaths\n\t\t}\n\n\t\tif len(inputFilePaths) > 0 {\n\t\t\tbaseDir := fs.GetBaseDirForFilePaths(inputFilePaths)\n\n\t\t\tpaths, err := fs.GetProjectPaths(baseDir)\n\t\t\tif err != nil {\n\t\t\t\tonErr(fmt.Errorf(\"failed to get project paths: %v\", err))\n\t\t\t}\n\n\t\t\tif !params.ForceSkipIgnore {\n\t\t\t\tvar filteredPaths []string\n\t\t\t\tfor _, inputFilePath := range inputFilePaths {\n\t\t\t\t\tif _, ok := paths.ActivePaths[inputFilePath]; !ok {\n\t\t\t\t\t\tignored, reason, err := fs.IsIgnored(paths, inputFilePath, baseDir)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tonErr(fmt.Errorf(\"failed to check if %s is ignored: %v\", inputFilePath, err))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ignored {\n\t\t\t\t\t\t\tignoredPaths[inputFilePath] = reason\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfilteredPaths = append(filteredPaths, inputFilePath)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tinputFilePaths = filteredPaths\n\n\t\t\t}\n\n\t\t\tif params.NamesOnly {\n\t\t\t\t// \"params.NamesOnly\" => we create directory-tree contexts (ContextDirectoryTreeType)\n\t\t\t\t// Partial skipping of subpaths\n\t\t\t\tfor _, inputFilePath := range inputFilePaths {\n\t\t\t\t\tcomposite := strings.Join([]string{string(shared.ContextDirectoryTreeType), inputFilePath}, \"|\")\n\t\t\t\t\tif existsByComposite[composite] != nil {\n\t\t\t\t\t\talreadyLoadedByComposite[composite] = existsByComposite[composite]\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tnumRoutines++\n\t\t\t\t\tgo func(inputFilePath string) {\n\t\t\t\t\t\tsem <- struct{}{}\n\t\t\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\t\t\tflattenedPaths, err := ParseInputPaths(ParseInputPathsParams{\n\t\t\t\t\t\t\tFileOrDirPaths: []string{inputFilePath},\n\t\t\t\t\t\t\tBaseDir:        baseDir,\n\t\t\t\t\t\t\tProjectPaths:   paths,\n\t\t\t\t\t\t\tLoadParams:     params,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terrCh <- fmt.Errorf(\"failed to parse input paths: %v\", err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !params.ForceSkipIgnore {\n\t\t\t\t\t\t\tvar filteredPaths []string\n\t\t\t\t\t\t\tfor _, path := range flattenedPaths {\n\t\t\t\t\t\t\t\tif _, ok := paths.ActivePaths[path]; ok {\n\t\t\t\t\t\t\t\t\tfilteredPaths = append(filteredPaths, path)\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tignored, reason, err := fs.IsIgnored(paths, path, baseDir)\n\t\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\t\terrCh <- fmt.Errorf(\"failed to check if %s is ignored: %v\", path, err)\n\t\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif ignored {\n\t\t\t\t\t\t\t\t\t\tignoredPaths[path] = reason\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tflattenedPaths = filteredPaths\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// PARTIAL skipping of subpaths\n\t\t\t\t\t\tvar keptPaths []string\n\t\t\t\t\t\tfor _, p := range flattenedPaths {\n\t\t\t\t\t\t\tlineSize := int64(len(p))\n\n\t\t\t\t\t\t\tcontextMu.Lock()\n\t\t\t\t\t\t\tif lineSize > shared.MaxContextBodySize {\n\t\t\t\t\t\t\t\tfilesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: p, Size: lineSize})\n\t\t\t\t\t\t\t\tcontextMu.Unlock()\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif totalSize+lineSize > shared.MaxContextBodySize {\n\t\t\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, p)\n\t\t\t\t\t\t\t\tcontextMu.Unlock()\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttotalSize += lineSize\n\t\t\t\t\t\t\tcontextMu.Unlock()\n\n\t\t\t\t\t\t\tkeptPaths = append(keptPaths, p)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbody := strings.Join(keptPaths, \"\\n\")\n\n\t\t\t\t\t\tname := inputFilePath\n\t\t\t\t\t\tif name == \".\" {\n\t\t\t\t\t\t\tname = \"cwd\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif name == \"..\" {\n\t\t\t\t\t\t\tname = \"parent\"\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcontextMu.Lock()\n\t\t\t\t\t\tloadContextReq = append(loadContextReq, &shared.LoadContextParams{\n\t\t\t\t\t\t\tContextType:     shared.ContextDirectoryTreeType,\n\t\t\t\t\t\t\tName:            name,\n\t\t\t\t\t\t\tBody:            body,\n\t\t\t\t\t\t\tFilePath:        inputFilePath,\n\t\t\t\t\t\t\tForceSkipIgnore: params.ForceSkipIgnore,\n\t\t\t\t\t\t\tAutoLoaded:      params.AutoLoaded,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tcontextMu.Unlock()\n\n\t\t\t\t\t\terrCh <- nil\n\t\t\t\t\t}(inputFilePath)\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tflattenedPaths, err := ParseInputPaths(ParseInputPathsParams{\n\t\t\t\t\tFileOrDirPaths: inputFilePaths,\n\t\t\t\t\tBaseDir:        baseDir,\n\t\t\t\t\tProjectPaths:   paths,\n\t\t\t\t\tLoadParams:     params,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tonErr(fmt.Errorf(\"failed to parse input paths: %v\", err))\n\t\t\t\t}\n\n\t\t\t\tif !params.ForceSkipIgnore {\n\t\t\t\t\tvar filteredPaths []string\n\t\t\t\t\tfor _, path := range flattenedPaths {\n\t\t\t\t\t\tif _, ok := paths.ActivePaths[path]; ok {\n\t\t\t\t\t\t\tfilteredPaths = append(filteredPaths, path)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tignored, reason, err := fs.IsIgnored(paths, path, baseDir)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tonErr(fmt.Errorf(\"failed to check if %s is ignored: %v\", path, err))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif ignored {\n\t\t\t\t\t\t\t\tignoredPaths[path] = reason\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tflattenedPaths = filteredPaths\n\n\t\t\t\t}\n\n\t\t\t\tvar numPaths int\n\t\t\t\tif params.DefsOnly {\n\t\t\t\t\tfiltered := []string{}\n\t\t\t\t\tfor _, path := range flattenedPaths {\n\t\t\t\t\t\tif shared.HasFileMapSupport(path) {\n\t\t\t\t\t\t\tnumPaths++\n\n\t\t\t\t\t\t\tif numPaths > shared.MaxContextMapPaths {\n\t\t\t\t\t\t\t\tmapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, path)\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfiltered = append(filtered, path)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tflattenedPaths = filtered\n\t\t\t\t} else if params.NamesOnly {\n\t\t\t\t\tfiltered := []string{}\n\t\t\t\t\tfor _, path := range flattenedPaths {\n\t\t\t\t\t\tnumPaths++\n\n\t\t\t\t\t\tif numPaths > shared.MaxContextMapPaths {\n\t\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfiltered = append(filtered, path)\n\t\t\t\t\t}\n\t\t\t\t\tflattenedPaths = filtered\n\t\t\t\t} else {\n\t\t\t\t\tfiltered := []string{}\n\t\t\t\t\tfor _, path := range flattenedPaths {\n\t\t\t\t\t\tnumPaths++\n\n\t\t\t\t\t\tif numPaths > shared.MaxContextCount {\n\t\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfiltered = append(filtered, path)\n\t\t\t\t\t}\n\t\t\t\t\tflattenedPaths = filtered\n\t\t\t\t}\n\n\t\t\t\tinputFilePaths = flattenedPaths\n\n\t\t\t\tfor _, path := range flattenedPaths {\n\t\t\t\t\tvar mapInputPath string\n\t\t\t\t\tif params.DefsOnly {\n\t\t\t\t\t\tfor _, inputPath := range toLoadMapPaths {\n\t\t\t\t\t\t\tabsPath, err := filepath.Abs(path)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tabsInputPath, err := filepath.Abs(inputPath)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif absPath == absInputPath ||\n\t\t\t\t\t\t\t\tstrings.HasPrefix(absPath+string(os.PathSeparator), absInputPath+string(os.PathSeparator)) {\n\t\t\t\t\t\t\t\tmapInputPath = inputPath\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif mapInputPath == \"\" {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmapInputPathsForPaths[path] = mapInputPath\n\t\t\t\t\t}\n\n\t\t\t\t\tvar contextType shared.ContextType\n\t\t\t\t\tisImage := shared.IsImageFile(path)\n\t\t\t\t\tif isImage {\n\t\t\t\t\t\tcontextType = shared.ContextImageType\n\t\t\t\t\t} else if params.DefsOnly {\n\t\t\t\t\t\tcontextType = shared.ContextMapType\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontextType = shared.ContextFileType\n\t\t\t\t\t}\n\n\t\t\t\t\tif !params.DefsOnly {\n\t\t\t\t\t\tcomposite := strings.Join([]string{string(contextType), path}, \"|\")\n\t\t\t\t\t\tif existsByComposite[composite] != nil {\n\t\t\t\t\t\t\talreadyLoadedByComposite[composite] = existsByComposite[composite]\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tnumRoutines++\n\n\t\t\t\t\tgo func(path string) {\n\t\t\t\t\t\tsem <- struct{}{}\n\t\t\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\t\t\tvar size int64\n\n\t\t\t\t\t\tfileInfo, err := os.Stat(path)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terrCh <- fmt.Errorf(\"failed to get file info for %s: %v\", path, err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsize = fileInfo.Size()\n\n\t\t\t\t\t\tif !params.DefsOnly && size > shared.MaxContextBodySize {\n\t\t\t\t\t\t\tcontextMu.Lock()\n\t\t\t\t\t\t\tfilesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: path, Size: size})\n\t\t\t\t\t\t\tcontextMu.Unlock()\n\t\t\t\t\t\t\terrCh <- nil\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !params.DefsOnly {\n\t\t\t\t\t\t\tcontextMu.Lock()\n\t\t\t\t\t\t\tif totalSize+size > shared.MaxContextBodySize {\n\t\t\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, path)\n\t\t\t\t\t\t\t\tcontextMu.Unlock()\n\t\t\t\t\t\t\t\terrCh <- nil\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttotalSize += size\n\t\t\t\t\t\t\tcontextMu.Unlock()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif params.DefsOnly {\n\t\t\t\t\t\t\tres, err := getMapFileDetails(path, size, totalSize)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\terrCh <- fmt.Errorf(\"failed to get map file details for %s: %v\", path, err)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcontextMu.Lock()\n\t\t\t\t\t\t\tdefer contextMu.Unlock()\n\n\t\t\t\t\t\t\tif currentMapInputBatch.NumFiles()+1 > shared.ContextMapMaxBatchSize || currentMapInputBatch.TotalSize()+size > shared.ContextMapMaxBatchBytes {\n\t\t\t\t\t\t\t\tcurrentMapInputBatch = shared.FileMapInputs{}\n\t\t\t\t\t\t\t\tmapInputBatches = append(mapInputBatches, currentMapInputBatch)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcurrentMapInputBatch[path] = res.mapContent\n\t\t\t\t\t\t\tmapSize += res.size\n\t\t\t\t\t\t\tmapInputShas[path] = res.shaVal\n\t\t\t\t\t\t\tmapInputTokens[path] = res.tokens\n\t\t\t\t\t\t\tmapInputSizes[path] = res.size\n\n\t\t\t\t\t\t\tif len(res.mapFilesTruncatedTooLarge) > 0 {\n\t\t\t\t\t\t\t\tmapFilesTruncatedTooLarge = append(mapFilesTruncatedTooLarge, res.mapFilesTruncatedTooLarge...)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif len(res.mapFilesSkippedAfterSizeLimit) > 0 {\n\t\t\t\t\t\t\t\tmapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, res.mapFilesSkippedAfterSizeLimit...)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t} else if isImage {\n\t\t\t\t\t\t\tfileContent, err := os.ReadFile(path)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\terrCh <- fmt.Errorf(\"failed to read the file %s: %v\", path, err)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcontextMu.Lock()\n\t\t\t\t\t\t\tdefer contextMu.Unlock()\n\n\t\t\t\t\t\t\tloadContextReq = append(loadContextReq, &shared.LoadContextParams{\n\t\t\t\t\t\t\t\tContextType: shared.ContextImageType,\n\t\t\t\t\t\t\t\tName:        path,\n\t\t\t\t\t\t\t\tBody:        base64.StdEncoding.EncodeToString(fileContent),\n\t\t\t\t\t\t\t\tFilePath:    path,\n\t\t\t\t\t\t\t\tImageDetail: params.ImageDetail,\n\t\t\t\t\t\t\t\tAutoLoaded:  params.AutoLoaded,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tfileContent, err := os.ReadFile(path)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\terrCh <- fmt.Errorf(\"failed to read the file %s: %v\", path, err)\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfileContent = shared.NormalizeEOL(fileContent)\n\n\t\t\t\t\t\t\tcontextMu.Lock()\n\t\t\t\t\t\t\tdefer contextMu.Unlock()\n\n\t\t\t\t\t\t\tloadContextReq = append(loadContextReq, &shared.LoadContextParams{\n\t\t\t\t\t\t\t\tContextType: shared.ContextFileType,\n\t\t\t\t\t\t\t\tName:        path,\n\t\t\t\t\t\t\t\tBody:        string(fileContent),\n\t\t\t\t\t\t\t\tFilePath:    path,\n\t\t\t\t\t\t\t\tAutoLoaded:  params.AutoLoaded,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\terrCh <- nil\n\t\t\t\t\t}(path)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(inputUrls) > 0 {\n\t\tfor _, u := range inputUrls {\n\t\t\tcomposite := strings.Join([]string{string(shared.ContextURLType), u}, \"|\")\n\t\t\tif existsByComposite[composite] != nil {\n\t\t\t\talreadyLoadedByComposite[composite] = existsByComposite[composite]\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tnumRoutines++\n\t\t\tgo func(u string) {\n\t\t\t\tsem <- struct{}{}\n\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\tbody, err := url.FetchURLContent(u)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to fetch content from URL %s: %v\", u, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tname := url.SanitizeURL(u)\n\t\t\t\t// show the first 20 characters, then ellipsis then the last 20 characters of 'name'\n\t\t\t\tif len(name) > 40 {\n\t\t\t\t\tname = name[:20] + \"⋯\" + name[len(name)-20:]\n\t\t\t\t}\n\n\t\t\t\t// Check the size of the URL body, just like a file:\n\t\t\t\tsize := int64(len(body))\n\n\t\t\t\tcontextMu.Lock()\n\t\t\t\tdefer contextMu.Unlock()\n\n\t\t\t\tif size > shared.MaxContextBodySize {\n\t\t\t\t\tfilesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: u, Size: size})\n\t\t\t\t\terrCh <- nil\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif totalSize+size > shared.MaxContextBodySize {\n\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, u)\n\t\t\t\t\terrCh <- nil\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttotalSize += size\n\n\t\t\t\tloadContextReq = append(loadContextReq, &shared.LoadContextParams{\n\t\t\t\t\tContextType: shared.ContextURLType,\n\t\t\t\t\tName:        name,\n\t\t\t\t\tBody:        body,\n\t\t\t\t\tUrl:         u,\n\t\t\t\t\tAutoLoaded:  params.AutoLoaded,\n\t\t\t\t})\n\n\t\t\t\terrCh <- nil\n\t\t\t}(u)\n\t\t}\n\t}\n\n\tfor i := 0; i < numRoutines; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tonErr(err)\n\t\t}\n\t}\n\n\tif params.DefsOnly {\n\t\tallMapBodies, err := processMapBatches(mapInputBatches)\n\t\tif err != nil {\n\t\t\tonErr(fmt.Errorf(\"failed to process map batches: %v\", err))\n\t\t}\n\n\t\tfor _, inputPath := range toLoadMapPaths {\n\t\t\tvar name string\n\t\t\tif inputPath == \".\" {\n\t\t\t\tname = \"cwd\"\n\t\t\t} else if inputPath == \"..\" {\n\t\t\t\tname = \"parent\"\n\t\t\t} else {\n\t\t\t\tname = inputPath\n\t\t\t}\n\n\t\t\tpathBodies := shared.FileMapBodies{}\n\t\t\tpathShas := map[string]string{}\n\t\t\tpathTokens := map[string]int{}\n\t\t\tpathSizes := map[string]int64{}\n\t\t\tfor path, body := range allMapBodies {\n\t\t\t\tmapInputPath := mapInputPathsForPaths[path]\n\t\t\t\tif mapInputPath == inputPath {\n\t\t\t\t\tpathBodies[path] = body\n\t\t\t\t\tpathShas[path] = mapInputShas[path]\n\t\t\t\t\tpathTokens[path] = mapInputTokens[path]\n\t\t\t\t\tpathSizes[path] = mapInputSizes[path]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// load the map even if it's empty (no paths)\n\t\t\t// it needs to exist so it can be updated later\n\t\t\tloadContextReq = append(loadContextReq, &shared.LoadContextParams{\n\t\t\t\tContextType: shared.ContextMapType,\n\t\t\t\tName:        name,\n\t\t\t\tMapBodies:   pathBodies,\n\t\t\t\tInputShas:   pathShas,\n\t\t\t\tInputTokens: pathTokens,\n\t\t\t\tInputSizes:  pathSizes,\n\t\t\t\tFilePath:    inputPath,\n\t\t\t\tAutoLoaded:  params.AutoLoaded,\n\t\t\t})\n\n\t\t}\n\t}\n\n\tfilesToLoad := map[string]string{}\n\tfor _, context := range loadContextReq {\n\t\tif context.ContextType == shared.ContextFileType {\n\t\t\tfilesToLoad[context.FilePath] = context.Body\n\t\t}\n\t}\n\n\thasConflicts, err := checkContextConflicts(filesToLoad)\n\n\tif err != nil {\n\t\tonErr(fmt.Errorf(\"failed to check context conflicts: %v\", err))\n\t}\n\n\tif len(loadContextReq)+len(cachedMapPaths) == 0 {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context loaded\")\n\n\t\tdidOutputReason := false\n\t\tif len(alreadyLoadedByComposite) > 0 {\n\t\t\tprintAlreadyLoadedMsg(alreadyLoadedByComposite)\n\t\t\tdidOutputReason = true\n\t\t}\n\t\tif len(ignoredPaths) > 0 && !params.SkipIgnoreWarning {\n\t\t\tprintIgnoredMsg()\n\t\t\tdidOutputReason = true\n\t\t}\n\n\t\tif !didOutputReason {\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"Use %s to load a file or URL:\", color.New(color.BgCyan, color.FgHiWhite).Sprint(\" plandex load [file-path|url] \"))\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"plandex load file.c file.h\")\n\t\t\tfmt.Println(\"plandex load https://github.com/some-org/some-repo/README.md\")\n\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"%s with the --recursive/-r flag:\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(\"Load a whole directory\"))\n\t\t\tfmt.Println(\"plandex load app/src -r\")\n\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"%s with the --tree flag:\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(\"Load a directory layout (file names only)\"))\n\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"%s file paths are relative to the current directory\\n\", color.New(color.Bold, term.ColorHiYellow).Sprint(\"Note:\"))\n\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"%s with the -n flag:\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(\"Load a note\"))\n\t\t\tfmt.Println(\"plandex load -n 'Some note here'\")\n\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"%s from any command:\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(\"Pipe data in\"))\n\t\t\tfmt.Println(\"npm test | plandex load\")\n\t\t}\n\n\t\tos.Exit(0)\n\t}\n\n\tvar res *shared.LoadContextResponse\n\tif cachedMapLoadRes != nil {\n\t\tres = cachedMapLoadRes\n\t} else {\n\t\tres, apiErr = api.Client.LoadContext(CurrentPlanId, CurrentBranch, loadContextReq)\n\t\tif apiErr != nil {\n\t\t\tonErr(fmt.Errorf(\"failed to load context: %v\", apiErr.Msg))\n\t\t}\n\t}\n\n\tterm.StopSpinner()\n\n\tif hasConflicts {\n\t\tterm.StartSpinner(\"🏗️  Starting build...\")\n\t\t_, err := buildPlanInlineFn(false, nil)\n\t\tif err != nil {\n\t\t\tonErr(fmt.Errorf(\"failed to build plan: %v\", err))\n\t\t}\n\t\tfmt.Println()\n\t}\n\n\tfmt.Println(\"✅ \" + res.Msg)\n\n\tif len(alreadyLoadedByComposite) > 0 {\n\t\tprintAlreadyLoadedMsg(alreadyLoadedByComposite)\n\t}\n\n\tif len(ignoredPaths) > 0 && !params.SkipIgnoreWarning {\n\t\tprintIgnoredMsg()\n\t}\n\n\tif len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 ||\n\t\tlen(mapFilesTruncatedTooLarge) > 0 || len(mapFilesSkippedAfterSizeLimit) > 0 {\n\t\tprintSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit,\n\t\t\tmapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit)\n\t}\n}\n\nfunc printAlreadyLoadedMsg(alreadyLoadedByComposite map[string]*shared.Context) {\n\tfmt.Println()\n\tpronoun := \"they're\"\n\tif len(alreadyLoadedByComposite) == 1 {\n\t\tpronoun = \"it's\"\n\t}\n\tfmt.Printf(\"🙅‍♂️ Skipped because %s already in context:\\n\", pronoun)\n\tfor _, context := range alreadyLoadedByComposite {\n\t\t_, icon := context.TypeAndIcon()\n\n\t\tfmt.Printf(\"  • %s %s\\n\", icon, context.Name)\n\t}\n}\n\nfunc printIgnoredMsg() {\n\tfmt.Println()\n\tfmt.Println(\"ℹ️  \" + color.New(color.FgWhite).Sprint(\"Due to .gitignore or .plandexignore, some paths weren't loaded.\\nUse --force / -f to load ignored paths.\"))\n}\n"
  },
  {
    "path": "app/cli/lib/context_paths.go",
    "content": "package lib\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/types\"\n)\n\ntype ParseInputPathsParams struct {\n\tFileOrDirPaths []string\n\tBaseDir        string\n\tProjectPaths   *types.ProjectPaths\n\tLoadParams     *types.LoadContextParams\n}\n\nfunc ParseInputPaths(params ParseInputPathsParams) ([]string, error) {\n\tfileOrDirPaths := params.FileOrDirPaths\n\tbaseDir := params.BaseDir\n\tprojectPaths := params.ProjectPaths\n\tloadParams := params.LoadParams\n\n\tresPaths := []string{}\n\n\tfor path := range projectPaths.AllPaths {\n\t\t// see if it's a child of any of the fileOrDirPaths\n\t\tfound := false\n\t\tfor _, p := range fileOrDirPaths {\n\t\t\tvar err error\n\t\t\tfound, err = fs.IsSubpathOf(p, path, baseDir)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error checking if %s is a subpath of %s: %s\", path, p, err)\n\t\t\t}\n\t\t\tif found {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tif projectPaths.AllDirs[path] {\n\t\t\tif !(loadParams.Recursive || loadParams.NamesOnly || loadParams.DefsOnly) {\n\t\t\t\t// log.Println(\"path\", path, \"info.Name()\", info.Name())\n\t\t\t\treturn nil, fmt.Errorf(\"cannot process directory %s: requires --recursive/-r, --tree, or --map flag\", path)\n\t\t\t}\n\n\t\t\t// calculate directory depth from base\n\t\t\t// depth := strings.Count(path[len(p):], string(filepath.Separator))\n\t\t\t// if params.MaxDepth != -1 && depth > params.MaxDepth {\n\t\t\t// \treturn filepath.SkipDir\n\t\t\t// }\n\n\t\t\tif loadParams.NamesOnly {\n\t\t\t\t// add directory name to results\n\t\t\t\tresPaths = append(resPaths, path)\n\t\t\t}\n\t\t} else {\n\t\t\t// add file path to results\n\t\t\tresPaths = append(resPaths, path)\n\t\t}\n\t}\n\n\treturn resPaths, nil\n}\n"
  },
  {
    "path": "app/cli/lib/context_shared.go",
    "content": "package lib\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"strings\"\n\t\"sync\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nconst ContextMapMaxClientConcurrency = 250\n\ntype filePathWithSize struct {\n\tPath string\n\tSize int64\n}\n\ntype mapFileDetails struct {\n\tsize                          int64\n\ttokens                        int\n\tshaVal                        string\n\tmapFilesSkippedAfterSizeLimit []string\n\tmapFilesTruncatedTooLarge     []filePathWithSize\n\tmapContent                    string\n}\n\nfunc getMapFileDetails(path string, size, mapSize int64) (mapFileDetails, error) {\n\tvar isImage bool\n\tvar totalMapSizeExceeded bool\n\n\tres := mapFileDetails{\n\t\tsize:                          size,\n\t\tmapFilesSkippedAfterSizeLimit: []string{},\n\t\tmapFilesTruncatedTooLarge:     []filePathWithSize{},\n\t}\n\n\tif !shared.HasFileMapSupport(path) {\n\t\tif shared.IsImageFile(path) {\n\t\t\tisImage = true\n\n\t\t\tvar err error\n\t\t\tres.tokens, err = readImageTokensForDefsOnly(path, size, openai.ImageURLDetailHigh, 8*1024)\n\t\t\tif err != nil {\n\t\t\t\treturn mapFileDetails{}, fmt.Errorf(\"failed to read image tokens for %s: %v\", path, err)\n\t\t\t}\n\t\t} else {\n\t\t\tres.tokens = shared.GetBytesToTokensEstimate(size)\n\t\t}\n\t} else {\n\t\tvar truncated bool\n\t\tif size > shared.MaxContextMapSingleInputSize {\n\t\t\tsize = shared.MaxContextMapSingleInputSize\n\t\t\ttruncated = true\n\t\t\tres.tokens = shared.GetBytesToTokensEstimate(size)\n\t\t}\n\n\t\t// should go in either skip list *or* truncated list, not both\n\t\tif mapSize+size > shared.MaxContextMapTotalInputSize {\n\t\t\ttotalMapSizeExceeded = true\n\t\t\tres.mapFilesSkippedAfterSizeLimit = append(res.mapFilesSkippedAfterSizeLimit, path)\n\t\t\tres.tokens = shared.GetBytesToTokensEstimate(size)\n\t\t} else if truncated {\n\t\t\tres.mapFilesTruncatedTooLarge = append(res.mapFilesTruncatedTooLarge, filePathWithSize{Path: path, Size: size})\n\t\t}\n\t}\n\n\tif totalMapSizeExceeded || !shared.HasFileMapSupport(path) || isImage {\n\t\tshaVal := sha256.Sum256([]byte(fmt.Sprintf(\"%d\", res.tokens)))\n\t\tres.shaVal = hex.EncodeToString(shaVal[:])\n\n\t\tres.mapContent = \"\"\n\t\tres.size = 0\n\t} else {\n\t\t// partial read for the map\n\t\tcontentRes, err := getMapFileContent(path)\n\t\tif err != nil {\n\t\t\treturn mapFileDetails{}, fmt.Errorf(\"failed to read file %s: %v\", path, err)\n\t\t}\n\n\t\tres.mapContent = contentRes.content\n\t\tres.shaVal = contentRes.shaVal\n\n\t\tif contentRes.truncated {\n\t\t\tres.mapFilesTruncatedTooLarge = append(res.mapFilesTruncatedTooLarge, filePathWithSize{Path: path, Size: shared.MaxContextMapSingleInputSize})\n\t\t\tres.size = shared.MaxContextMapSingleInputSize\n\t\t\tres.tokens = shared.GetBytesToTokensEstimate(shared.MaxContextMapSingleInputSize)\n\t\t} else {\n\t\t\t// do the actual token count if we didn't truncate\n\t\t\tres.tokens = shared.GetNumTokensEstimate(res.mapContent)\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\ntype mapFileContent struct {\n\tmapData   []byte\n\tcontent   string\n\tshaVal    string\n\ttruncated bool\n}\n\nfunc getMapFileContent(path string) (mapFileContent, error) {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn mapFileContent{}, err\n\t}\n\tdefer f.Close()\n\n\tinfo, err := f.Stat()\n\tif err != nil {\n\t\treturn mapFileContent{}, err\n\t}\n\tsize := info.Size()\n\n\tlimit := int64(shared.MaxContextMapSingleInputSize)\n\ttruncated := size > limit\n\n\tlimitReader := io.LimitReader(f, limit)\n\tbytes, err := io.ReadAll(limitReader)\n\tif err != nil {\n\t\treturn mapFileContent{}, err\n\t}\n\n\tsum := sha256.Sum256(bytes)\n\tshaVal := hex.EncodeToString(sum[:])\n\n\treturn mapFileContent{mapData: bytes, content: string(bytes), shaVal: shaVal, truncated: truncated}, nil\n}\n\nfunc processMapBatches(mapInputBatches []shared.FileMapInputs) (shared.FileMapBodies, error) {\n\tallMapBodies := shared.FileMapBodies{}\n\n\tvar mapMu sync.Mutex\n\terrCh := make(chan error, len(mapInputBatches))\n\n\tfor _, batch := range mapInputBatches {\n\t\tif len(batch) == 0 {\n\t\t\terrCh <- nil\n\t\t\tcontinue\n\t\t}\n\n\t\tgo func(batch shared.FileMapInputs) {\n\t\t\tmapRes, apiErr := api.Client.GetFileMap(shared.GetFileMapRequest{\n\t\t\t\tMapInputs: batch,\n\t\t\t})\n\t\t\tif apiErr != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to get file map: %v\", apiErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmapMu.Lock()\n\t\t\tfor path, bodies := range mapRes.MapBodies {\n\t\t\t\tallMapBodies[path] = bodies\n\t\t\t}\n\t\t\tmapMu.Unlock()\n\t\t\terrCh <- nil\n\t\t}(batch)\n\t}\n\n\tfor i := 0; i < len(mapInputBatches); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn allMapBodies, nil\n}\n\nfunc readImageTokensForDefsOnly(path string, size int64, detail openai.ImageURLDetail, headerBytes int64) (int, error) {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to open file %s: %w\", path, err)\n\t}\n\tdefer file.Close()\n\n\ttokens, err := shared.GetImageTokensFromHeader(file, detail, headerBytes)\n\tif err != nil {\n\t\ttokens = shared.GetImageTokensEstimateFromBytes(size)\n\t}\n\treturn tokens, nil\n}\n\nfunc printSkippedFilesMsg(\n\tfilesSkippedTooLarge []filePathWithSize,\n\tfilesSkippedAfterSizeLimit []string,\n\tmapFilesTruncatedTooLarge []filePathWithSize,\n\tmapFilesSkippedAfterSizeLimit []string,\n) {\n\tfmt.Println()\n\tfmt.Println(getSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit, mapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit))\n}\n\nfunc getSkippedFilesMsg(\n\tfilesSkippedTooLarge []filePathWithSize,\n\tfilesSkippedAfterSizeLimit []string,\n\tmapFilesTruncatedTooLarge []filePathWithSize,\n\tmapFilesSkippedAfterSizeLimit []string,\n) string {\n\tvar builder strings.Builder\n\n\tif len(filesSkippedTooLarge) > 0 {\n\t\tfmt.Fprintf(&builder, \"ℹ️  These files were skipped because they're too large:\\n\")\n\t\tfor i, file := range filesSkippedTooLarge {\n\t\t\tif i >= maxSkippedFileList {\n\t\t\t\tfmt.Fprintf(&builder, \"  • and %d more\\n\", len(filesSkippedTooLarge)-maxSkippedFileList)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfmt.Fprintf(&builder, \"  • %s - %d MB\\n\", file.Path, file.Size/1024/1024)\n\t\t}\n\t}\n\tif len(mapFilesTruncatedTooLarge) > 0 {\n\t\tfmt.Fprintf(&builder, \"ℹ️  These files were truncated because they're too large to map fully:\\n\")\n\t\tfor i, file := range mapFilesTruncatedTooLarge {\n\t\t\tif i >= maxSkippedFileList {\n\t\t\t\tfmt.Fprintf(&builder, \"  • and %d more\\n\", len(mapFilesTruncatedTooLarge)-maxSkippedFileList)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif file.Size > 1024*1024 {\n\t\t\t\tfmt.Fprintf(&builder, \"  • %s - %d MB\\n\", file.Path, file.Size/1024/1024)\n\t\t\t} else {\n\t\t\t\tfmt.Fprintf(&builder, \"  • %s - %d KB\\n\", file.Path, file.Size/1024)\n\t\t\t}\n\t\t}\n\t\tif len(mapFilesTruncatedTooLarge) > 0 {\n\t\t\tfmt.Fprintf(&builder, \"They will still be included in the map, but only the first %d KB will be mapped.\\n\", shared.MaxContextMapSingleInputSize/1024)\n\t\t}\n\t}\n\tif len(filesSkippedAfterSizeLimit) > 0 {\n\t\tfmt.Fprintf(&builder, \"ℹ️  These files were skipped because the total size limit was exceeded:\\n\")\n\t\tfor i, file := range filesSkippedAfterSizeLimit {\n\t\t\tif i >= maxSkippedFileList {\n\t\t\t\tfmt.Fprintf(&builder, \"  • and %d more\\n\", len(filesSkippedAfterSizeLimit)-maxSkippedFileList)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfmt.Fprintf(&builder, \"  • %s\\n\", file)\n\t\t}\n\t}\n\tif len(mapFilesSkippedAfterSizeLimit) > 0 {\n\t\tfmt.Fprintf(&builder, \"ℹ️  These files were skipped because the total map size limit was exceeded:\\n\")\n\t\tfor i, file := range mapFilesSkippedAfterSizeLimit {\n\t\t\tif i >= maxSkippedFileList {\n\t\t\t\tfmt.Fprintf(&builder, \"  • and %d more\\n\", len(mapFilesSkippedAfterSizeLimit)-maxSkippedFileList)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfmt.Fprintf(&builder, \"  • %s\\n\", file)\n\t\t}\n\t\tif len(mapFilesSkippedAfterSizeLimit) > 0 {\n\t\t\tfmt.Fprintf(&builder, \"They will still be included in the map as paths in the project, but no maps will be generated for them.\\n\")\n\t\t}\n\t}\n\treturn builder.String()\n}\n"
  },
  {
    "path": "app/cli/lib/context_update.go",
    "content": "package lib\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"plandex-cli/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n)\n\nfunc CheckOutdatedContextWithOutput(quiet, autoConfirm bool, maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (contextOutdated, updated bool, err error) {\n\tif !quiet {\n\t\tterm.StartSpinner(\"🔬 Checking context...\")\n\t}\n\n\tvar contexts []*shared.Context\n\n\tif maybeContexts != nil {\n\t\tcontexts = maybeContexts\n\t} else {\n\t\tres, err := api.Client.ListContext(CurrentPlanId, CurrentBranch)\n\t\tif err != nil {\n\t\t\tterm.StopSpinner()\n\t\t\treturn false, false, fmt.Errorf(\"failed to list context: %s\", err)\n\t\t}\n\t\tcontexts = res\n\t}\n\n\toutdatedRes, err := CheckOutdatedContext(contexts, projectPaths)\n\tif err != nil {\n\t\tterm.StopSpinner()\n\t\treturn false, false, fmt.Errorf(\"failed to check outdated context: %s\", err)\n\t}\n\n\tif !quiet {\n\t\tterm.StopSpinner()\n\t}\n\n\tif len(outdatedRes.UpdatedContexts) == 0 && len(outdatedRes.RemovedContexts) == 0 {\n\t\tif !quiet {\n\t\t\tfmt.Println(\"✅ Context is up to date\")\n\t\t}\n\t\treturn false, false, nil\n\t}\n\tif len(outdatedRes.UpdatedContexts) > 0 {\n\t\ttypes := []string{}\n\t\tif outdatedRes.NumFiles > 0 {\n\t\t\tlbl := \"file\"\n\t\t\tif outdatedRes.NumFiles > 1 {\n\t\t\t\tlbl = \"files\"\n\t\t\t}\n\t\t\tlbl = strconv.Itoa(outdatedRes.NumFiles) + \" \" + lbl\n\t\t\ttypes = append(types, lbl)\n\t\t}\n\t\tif outdatedRes.NumUrls > 0 {\n\t\t\tlbl := \"url\"\n\t\t\tif outdatedRes.NumUrls > 1 {\n\t\t\t\tlbl = \"urls\"\n\t\t\t}\n\t\t\tlbl = strconv.Itoa(outdatedRes.NumUrls) + \" \" + lbl\n\t\t\ttypes = append(types, lbl)\n\t\t}\n\t\tif outdatedRes.NumTrees > 0 {\n\t\t\tlbl := \"directory tree\"\n\t\t\tif outdatedRes.NumTrees > 1 {\n\t\t\t\tlbl = \"directory trees\"\n\t\t\t}\n\t\t\tlbl = strconv.Itoa(outdatedRes.NumTrees) + \" \" + lbl\n\t\t\ttypes = append(types, lbl)\n\t\t}\n\t\tif outdatedRes.NumMaps > 0 {\n\t\t\tlbl := \"map\"\n\t\t\tif outdatedRes.NumMaps > 1 {\n\t\t\t\tlbl = \"maps\"\n\t\t\t}\n\t\t\tlbl = strconv.Itoa(outdatedRes.NumMaps) + \" \" + lbl\n\t\t\ttypes = append(types, lbl)\n\t\t}\n\n\t\tvar msg string\n\t\tif len(types) <= 2 {\n\t\t\tmsg += strings.Join(types, \" and \")\n\t\t} else {\n\t\t\tfor i, add := range types {\n\t\t\t\tif i == len(types)-1 {\n\t\t\t\t\tmsg += \", and \" + add\n\t\t\t\t} else {\n\t\t\t\t\tmsg += \", \" + add\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tphrase := \"have been\"\n\t\tif len(outdatedRes.UpdatedContexts) == 1 {\n\t\t\tphrase = \"has been\"\n\t\t}\n\n\t\tif !quiet {\n\t\t\tterm.StopSpinner()\n\n\t\t\tcolor.New(term.ColorHiCyan, color.Bold).Printf(\"%s in context %s modified 👇\\n\\n\", msg, phrase)\n\n\t\t\ttableString := tableForContextOutdated(outdatedRes.UpdatedContexts, outdatedRes.TokenDiffsById)\n\t\t\tfmt.Println(tableString)\n\t\t}\n\t}\n\n\tif len(outdatedRes.RemovedContexts) > 0 {\n\t\ttypes := []string{}\n\t\tif outdatedRes.NumFilesRemoved > 0 {\n\t\t\tlbl := \"file\"\n\t\t\tif outdatedRes.NumFilesRemoved > 1 {\n\t\t\t\tlbl = \"files\"\n\t\t\t}\n\t\t\tlbl = strconv.Itoa(outdatedRes.NumFilesRemoved) + \" \" + lbl\n\t\t\ttypes = append(types, lbl)\n\t\t}\n\t\tif outdatedRes.NumTreesRemoved > 0 {\n\t\t\tlbl := \"directory tree\"\n\t\t\tif outdatedRes.NumTreesRemoved > 1 {\n\t\t\t\tlbl = \"directory trees\"\n\t\t\t}\n\t\t\tlbl = strconv.Itoa(outdatedRes.NumTreesRemoved) + \" \" + lbl\n\t\t\ttypes = append(types, lbl)\n\t\t}\n\n\t\tvar msg string\n\t\tif len(types) <= 2 {\n\t\t\tmsg += strings.Join(types, \" and \")\n\t\t} else {\n\t\t\tfor i, add := range types {\n\t\t\t\tif i == len(types)-1 {\n\t\t\t\t\tmsg += \", and \" + add\n\t\t\t\t} else {\n\t\t\t\t\tmsg += \", \" + add\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tphrase := \"have been\"\n\t\tif len(outdatedRes.RemovedContexts) == 1 {\n\t\t\tphrase = \"has been\"\n\t\t}\n\n\t\tif !quiet {\n\t\t\tterm.StopSpinner()\n\n\t\t\tcolor.New(term.ColorHiCyan, color.Bold).Printf(\"%s in context %s removed 👇\\n\\n\", msg, phrase)\n\n\t\t\ttableString := tableForContextOutdated(outdatedRes.RemovedContexts, outdatedRes.TokenDiffsById)\n\t\t\tfmt.Println(tableString)\n\t\t}\n\t}\n\n\tconfirmed := autoConfirm\n\n\tif !autoConfirm {\n\t\tconfirmed, err = term.ConfirmYesNo(\"Update context now?\")\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"failed to get user input: %s\", err)\n\t\t}\n\t}\n\n\tif confirmed {\n\t\treqFn := outdatedRes.ReqFn\n\t\tif reqFn == nil {\n\t\t\treturn false, false, fmt.Errorf(\"no update request function provided\")\n\t\t}\n\t\t_, err = UpdateContextWithOutput(UpdateContextParams{\n\t\t\tContexts:    contexts,\n\t\t\tOutdatedRes: *outdatedRes,\n\t\t\tReqFn:       reqFn,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn false, false, fmt.Errorf(\"error updating context: %v\", err)\n\t\t}\n\t\treturn true, true, nil\n\t} else {\n\t\treturn true, false, nil\n\t}\n\n}\n\ntype UpdateContextParams struct {\n\tContexts    []*shared.Context\n\tOutdatedRes types.ContextOutdatedResult\n\tReqFn       func() (map[string]*shared.UpdateContextParams, error)\n}\n\ntype UpdateContextResult struct {\n\tHasConflicts bool\n\tMsg          string\n}\n\nfunc UpdateContextWithOutput(params UpdateContextParams) (UpdateContextResult, error) {\n\tterm.StartSpinner(\"🔄 Updating context...\")\n\n\tupdateRes, err := UpdateContext(params)\n\tif err != nil {\n\t\treturn UpdateContextResult{}, err\n\t}\n\n\tterm.StopSpinner()\n\n\tfmt.Println(\"✅ \" + updateRes.Msg)\n\n\treturn updateRes, nil\n}\n\nfunc UpdateContext(params UpdateContextParams) (UpdateContextResult, error) {\n\tvar err error\n\treqFn := params.ReqFn\n\tif reqFn == nil {\n\t\treturn UpdateContextResult{}, fmt.Errorf(\"no update request function provided\")\n\t}\n\treq, err := reqFn()\n\tif err != nil {\n\t\treturn UpdateContextResult{}, fmt.Errorf(\"error getting update request: %v\", err)\n\t}\n\tvar hasConflicts bool\n\tvar msg string\n\n\tcontextsById := map[string]*shared.Context{}\n\tfor _, context := range params.Contexts {\n\t\tcontextsById[context.Id] = context\n\t}\n\tdeleteIds := map[string]bool{}\n\tfor _, context := range params.OutdatedRes.RemovedContexts {\n\t\tdeleteIds[context.Id] = true\n\t}\n\n\tfilesToLoad := map[string]string{}\n\tfor id := range req {\n\t\tcontext := contextsById[id]\n\t\tif context.ContextType == shared.ContextFileType {\n\t\t\tfilesToLoad[context.FilePath] = context.Body\n\t\t}\n\t}\n\tfor id := range deleteIds {\n\t\tcontext := contextsById[id]\n\t\tif context.ContextType == shared.ContextFileType {\n\t\t\tfilesToLoad[context.FilePath] = \"\"\n\t\t}\n\t}\n\n\thasConflicts, err = checkContextConflicts(filesToLoad)\n\tif err != nil {\n\t\treturn UpdateContextResult{}, fmt.Errorf(\"failed to check context conflicts: %v\", err)\n\t}\n\n\tif len(req) > 0 {\n\t\tres, apiErr := api.Client.UpdateContext(CurrentPlanId, CurrentBranch, req)\n\t\tif apiErr != nil {\n\t\t\treturn UpdateContextResult{}, fmt.Errorf(\"failed to update context: %v\", apiErr)\n\t\t}\n\t\tmsg = res.Msg\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, apiErr := api.Client.DeleteContext(CurrentPlanId, CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tif apiErr != nil {\n\t\t\treturn UpdateContextResult{}, fmt.Errorf(\"failed to delete contexts: %v\", apiErr)\n\t\t}\n\t\tmsg += \" \" + res.Msg\n\t}\n\n\treturn UpdateContextResult{\n\t\tHasConflicts: hasConflicts,\n\t\tMsg:          strings.TrimSpace(msg),\n\t}, nil\n}\n\n// CheckOutdatedContext is where we replicate your partial-read logic for map files\n// so that large map files or newly added map files do not read more than MaxContextMapSingleInputSize\nfunc CheckOutdatedContext(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (*types.ContextOutdatedResult, error) {\n\treturn checkOutdatedAndMaybeUpdateContext(false, maybeContexts, projectPaths)\n}\n\ntype mapState struct {\n\tremovedMapPaths      []string\n\tmapInputShas         map[string]string\n\tmapInputTokens       map[string]int\n\tmapInputSizes        map[string]int64\n\ttotalMapSize         int64\n\tcurrentMapInputBatch shared.FileMapInputs\n\tmapInputBatches      []shared.FileMapInputs\n}\n\nfunc checkOutdatedAndMaybeUpdateContext(doUpdate bool, maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (*types.ContextOutdatedResult, error) {\n\tvar contexts []*shared.Context\n\n\tif maybeContexts == nil {\n\t\tcontextsRes, apiErr := api.Client.ListContext(CurrentPlanId, CurrentBranch)\n\t\tif apiErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"error retrieving context: %v\", apiErr)\n\t\t}\n\t\tcontexts = contextsRes\n\t} else {\n\t\tcontexts = maybeContexts\n\t}\n\n\ttotalTokens := 0\n\tfor _, c := range contexts {\n\t\ttotalTokens += c.NumTokens\n\t}\n\n\tvar errs []error\n\n\treqFns := map[string]func() (*shared.UpdateContextParams, error){}\n\n\tvar updatedContexts []*shared.Context\n\tvar tokenDiffsById = map[string]int{}\n\tvar numFiles int\n\tvar numUrls int\n\tvar numTrees int\n\tvar numMaps int\n\tvar numFilesRemoved int\n\tvar numTreesRemoved int\n\tvar mu sync.Mutex\n\tvar wg sync.WaitGroup\n\tcontextsById := make(map[string]*shared.Context)\n\tdeleteIds := make(map[string]bool)\n\n\tpaths := projectPaths\n\n\t// We track skipped items for final warnings\n\tvar filesSkippedTooLarge []filePathWithSize\n\tvar filesSkippedAfterSizeLimit []string\n\n\tvar mapFilesTruncatedTooLarge []filePathWithSize\n\tvar mapFilesSkippedAfterSizeLimit []string\n\n\tmapFilesTruncatedSet := map[string]bool{}\n\tmapFilesSkippedAfterSizeLimitSet := map[string]bool{}\n\n\tmapFileInfoByPath := map[string]os.FileInfo{}\n\tmapFileRemovedByPath := map[string]bool{}\n\n\tvar totalSize int64\n\tvar totalBodySize int64\n\tvar totalContextCount int\n\n\tsem := make(chan struct{}, ContextMapMaxClientConcurrency)\n\n\tfor _, c := range contexts {\n\t\tcontextsById[c.Id] = c\n\t}\n\n\tfor _, context := range contexts {\n\t\tswitch context.ContextType {\n\t\tcase shared.ContextFileType:\n\t\t\twg.Add(1)\n\t\t\tgo func(ctx *shared.Context) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tsem <- struct{}{}\n\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\tif _, err := os.Stat(ctx.FilePath); os.IsNotExist(err) {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\tdeleteIds[ctx.Id] = true\n\t\t\t\t\tnumFilesRemoved++\n\t\t\t\t\ttokenDiffsById[ctx.Id] = -ctx.NumTokens\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfileContent, err := os.ReadFile(ctx.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to read the file %s: %v\", ctx.FilePath, err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfileContent = shared.NormalizeEOL(fileContent)\n\n\t\t\t\tfileInfo, err := os.Stat(ctx.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to get file info for %s: %v\", ctx.FilePath, err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tsize := fileInfo.Size()\n\n\t\t\t\t// Individual skip checks\n\t\t\t\tif size > shared.MaxContextBodySize {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\tfilesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: ctx.FilePath, Size: size})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif totalSize+size > shared.MaxContextBodySize {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// Compare new sha\n\t\t\t\thash := sha256.Sum256(fileContent)\n\t\t\t\tsha := hex.EncodeToString(hash[:])\n\n\t\t\t\tif sha != ctx.Sha {\n\t\t\t\t\tif totalContextCount >= shared.MaxContextCount {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\toldBodySize := int64(len(ctx.Body))\n\t\t\t\t\tnewBodySize := int64(len(fileContent))\n\t\t\t\t\tif totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// Accept\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\ttotalSize += size\n\t\t\t\t\ttotalContextCount++\n\t\t\t\t\ttotalBodySize += (newBodySize - oldBodySize)\n\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\tvar numTokens int\n\t\t\t\t\tif shared.IsImageFile(ctx.FilePath) {\n\t\t\t\t\t\ttokens, err := shared.GetImageTokens(base64.StdEncoding.EncodeToString(fileContent), ctx.ImageDetail)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to get image tokens for %s: %v\", ctx.FilePath, err))\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnumTokens = tokens\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnumTokens = shared.GetNumTokensEstimate(string(fileContent))\n\t\t\t\t\t}\n\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\ttokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens\n\t\t\t\t\tnumFiles++\n\t\t\t\t\tupdatedContexts = append(updatedContexts, ctx)\n\n\t\t\t\t\treqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {\n\t\t\t\t\t\treturn &shared.UpdateContextParams{\n\t\t\t\t\t\t\tBody: string(fileContent),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}(context)\n\n\t\tcase shared.ContextDirectoryTreeType:\n\t\t\twg.Add(1)\n\t\t\tgo func(ctx *shared.Context) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tsem <- struct{}{}\n\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\tif _, err := os.Stat(ctx.FilePath); os.IsNotExist(err) {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdeleteIds[ctx.Id] = true\n\t\t\t\t\tnumTreesRemoved++\n\t\t\t\t\ttokenDiffsById[ctx.Id] = -ctx.NumTokens\n\t\t\t\t\tmu.Unlock()\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tbaseDir := fs.GetBaseDirForFilePaths([]string{ctx.FilePath})\n\t\t\t\tflattenedPaths, err := ParseInputPaths(ParseInputPathsParams{\n\t\t\t\t\tFileOrDirPaths: []string{ctx.FilePath},\n\t\t\t\t\tBaseDir:        baseDir,\n\t\t\t\t\tProjectPaths:   paths,\n\t\t\t\t\tLoadParams: &types.LoadContextParams{\n\t\t\t\t\t\tNamesOnly:       true,\n\t\t\t\t\t\tForceSkipIgnore: ctx.ForceSkipIgnore,\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to get directory tree %s: %v\", ctx.FilePath, err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif !ctx.ForceSkipIgnore && paths != nil {\n\t\t\t\t\tvar filtered []string\n\t\t\t\t\tfor _, p := range flattenedPaths {\n\t\t\t\t\t\tif _, ok := paths.ActivePaths[p]; ok {\n\t\t\t\t\t\t\tfiltered = append(filtered, p)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tflattenedPaths = filtered\n\t\t\t\t}\n\n\t\t\t\t// Partial skipping for sub-paths\n\t\t\t\tvar kept []string\n\t\t\t\tmu.Lock()\n\t\t\t\tfor _, p := range flattenedPaths {\n\t\t\t\t\tlineSize := int64(len(p))\n\t\t\t\t\t// If line is individually too large, skip\n\t\t\t\t\tif lineSize > shared.MaxContextBodySize {\n\t\t\t\t\t\tfilesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: p, Size: lineSize})\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif totalSize+lineSize > shared.MaxContextBodySize {\n\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, p)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// Accept\n\t\t\t\t\ttotalSize += lineSize\n\t\t\t\t\tkept = append(kept, p)\n\t\t\t\t}\n\t\t\t\tmu.Unlock()\n\n\t\t\t\tbody := strings.Join(kept, \"\\n\")\n\t\t\t\tnewHash := sha256.Sum256([]byte(body))\n\t\t\t\tnewSha := hex.EncodeToString(newHash[:])\n\n\t\t\t\tif newSha != ctx.Sha {\n\t\t\t\t\tif totalContextCount >= shared.MaxContextCount {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\toldBodySize := int64(len(ctx.Body))\n\t\t\t\t\tnewBodySize := int64(len(body))\n\t\t\t\t\tif totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.FilePath)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tnumTokens := shared.GetNumTokensEstimate(body)\n\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\ttotalContextCount++\n\t\t\t\t\ttotalBodySize += (newBodySize - oldBodySize)\n\t\t\t\t\ttokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens\n\t\t\t\t\tnumTrees++\n\t\t\t\t\tupdatedContexts = append(updatedContexts, ctx)\n\t\t\t\t\treqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {\n\t\t\t\t\t\treturn &shared.UpdateContextParams{\n\t\t\t\t\t\t\tBody: body,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}(context)\n\n\t\tcase shared.ContextMapType:\n\t\t\t// Instead of reading all files in the same goroutine,\n\t\t\t// we now spawn one goroutine per map-file to mirror the loading logic concurrency.\n\t\t\twg.Add(1)\n\t\t\tgo func(ctx *shared.Context) {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\t// We collect paths from the existing map\n\t\t\t\tvar mapPaths []string\n\t\t\t\tfor path := range ctx.MapShas {\n\t\t\t\t\tmapPaths = append(mapPaths, path)\n\t\t\t\t}\n\n\t\t\t\t// Next, see if there are newly added files\n\t\t\t\tbaseDir := fs.GetBaseDirForFilePaths([]string{ctx.FilePath})\n\t\t\t\tflattenedPaths, err := ParseInputPaths(ParseInputPathsParams{\n\t\t\t\t\tFileOrDirPaths: []string{ctx.FilePath},\n\t\t\t\t\tBaseDir:        baseDir,\n\t\t\t\t\tProjectPaths:   projectPaths,\n\t\t\t\t\tLoadParams:     &types.LoadContextParams{Recursive: true},\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to get the directory tree %s: %v\", ctx.FilePath, err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tvar filtered []string\n\t\t\t\tif projectPaths != nil {\n\t\t\t\t\tfor _, p := range flattenedPaths {\n\t\t\t\t\t\tif _, ok := projectPaths.ActivePaths[p]; ok {\n\t\t\t\t\t\t\tfiltered = append(filtered, p)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tflattenedPaths = filtered\n\t\t\t\t}\n\n\t\t\t\t// If a path was not already in the map, it's newly added\n\t\t\t\tfor _, p := range flattenedPaths {\n\t\t\t\t\tif _, ok := ctx.MapShas[p]; !ok {\n\t\t\t\t\t\tmapPaths = append(mapPaths, p)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttotalMapPaths := len(mapPaths)\n\n\t\t\t\tcurrentMapInputBatch := shared.FileMapInputs{}\n\t\t\t\tstate := mapState{\n\t\t\t\t\tremovedMapPaths:      []string{},\n\t\t\t\t\tmapInputShas:         map[string]string{},\n\t\t\t\t\tmapInputTokens:       map[string]int{},\n\t\t\t\t\tmapInputSizes:        map[string]int64{},\n\t\t\t\t\ttotalMapSize:         0,\n\t\t\t\t\tcurrentMapInputBatch: currentMapInputBatch,\n\t\t\t\t\tmapInputBatches:      []shared.FileMapInputs{currentMapInputBatch},\n\t\t\t\t}\n\n\t\t\t\tinnerExistenceErrCh := make(chan error, len(mapPaths))\n\n\t\t\t\t// Existence: check each path in its own goroutine:\n\n\t\t\t\tfor _, path := range mapPaths {\n\t\t\t\t\tgo func(path string) {\n\t\t\t\t\t\tsem <- struct{}{}\n\t\t\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\t\t\tvar removed bool\n\n\t\t\t\t\t\tvar hasFileInfo bool\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tif _, ok := mapFileInfoByPath[path]; ok {\n\t\t\t\t\t\t\thasFileInfo = true\n\t\t\t\t\t\t} else if _, ok := mapFileRemovedByPath[path]; ok {\n\t\t\t\t\t\t\tremoved = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\tif !(hasFileInfo || removed) {\n\t\t\t\t\t\t\tfileInfo, err := os.Stat(path)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tif os.IsNotExist(err) {\n\t\t\t\t\t\t\t\t\tremoved = true\n\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tinnerExistenceErrCh <- fmt.Errorf(\"failed to stat map file %s: %v\", path, err)\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\t\tprevTokens := ctx.MapTokens[path]\n\t\t\t\t\t\t\tprevSize := ctx.MapSizes[path]\n\n\t\t\t\t\t\t\tif removed {\n\t\t\t\t\t\t\t\tmapFileRemovedByPath[path] = true\n\t\t\t\t\t\t\t\ttotalMapPaths--\n\t\t\t\t\t\t\t\tif _, existed := ctx.MapShas[path]; existed {\n\t\t\t\t\t\t\t\t\tstate.removedMapPaths = append(state.removedMapPaths, path)\n\t\t\t\t\t\t\t\t\ttokenDiffsById[ctx.Id] -= prevTokens\n\t\t\t\t\t\t\t\t\tstate.totalMapSize -= prevSize\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tmapFileInfoByPath[path] = fileInfo\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmu.Unlock()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tinnerExistenceErrCh <- nil\n\t\t\t\t\t}(path)\n\t\t\t\t}\n\n\t\t\t\tfor range mapPaths {\n\t\t\t\t\terr := <-innerExistenceErrCh\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Updates: check each path in its own goroutine:\n\t\t\t\tinnerUpdatesErrCh := make(chan error, len(mapPaths))\n\n\t\t\t\tfor _, path := range mapPaths {\n\t\t\t\t\tgo func(path string) {\n\t\t\t\t\t\tsem <- struct{}{}\n\t\t\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\t\t\tvar removed bool\n\t\t\t\t\t\tvar fileInfo os.FileInfo\n\n\t\t\t\t\t\tvar hasFileInfo bool\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tif _, ok := mapFileInfoByPath[path]; ok {\n\t\t\t\t\t\t\tfileInfo = mapFileInfoByPath[path]\n\t\t\t\t\t\t\thasFileInfo = true\n\t\t\t\t\t\t} else if _, ok := mapFileRemovedByPath[path]; ok {\n\t\t\t\t\t\t\tremoved = true\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\tif removed {\n\t\t\t\t\t\t\tinnerUpdatesErrCh <- nil\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !hasFileInfo {\n\t\t\t\t\t\t\tinnerUpdatesErrCh <- fmt.Errorf(\"failed to get map file info for %s - should already be set\", path)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsize := fileInfo.Size()\n\t\t\t\t\t\tvar totalMapSize int64\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tprevTokens := ctx.MapTokens[path]\n\t\t\t\t\t\tprevSize := ctx.MapSizes[path]\n\t\t\t\t\t\tprevSha := ctx.MapShas[path]\n\t\t\t\t\t\ttotalMapSize = state.totalMapSize\n\t\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\t\tres, err := getMapFileDetails(path, size, totalMapSize)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tinnerUpdatesErrCh <- fmt.Errorf(\"failed to get map file details for %s: %v\", path, err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif res.shaVal == prevSha {\n\t\t\t\t\t\t\t// no change\n\t\t\t\t\t\t\tinnerUpdatesErrCh <- nil\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmu.Lock()\n\n\t\t\t\t\t\ttotalMapPaths++\n\n\t\t\t\t\t\tif totalMapPaths > shared.MaxContextMapPaths {\n\t\t\t\t\t\t\tif _, ok := mapFilesSkippedAfterSizeLimitSet[path]; !ok {\n\t\t\t\t\t\t\t\tmapFilesSkippedAfterSizeLimitSet[path] = true\n\t\t\t\t\t\t\t\tmapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, path)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmu.Unlock()\n\t\t\t\t\t\t\tinnerUpdatesErrCh <- nil\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\t\tif state.currentMapInputBatch.NumFiles()+1 > shared.ContextMapMaxBatchSize || state.totalMapSize+res.size > shared.ContextMapMaxBatchBytes {\n\t\t\t\t\t\t\tstate.currentMapInputBatch = shared.FileMapInputs{}\n\t\t\t\t\t\t\tstate.mapInputBatches = append(state.mapInputBatches, state.currentMapInputBatch)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsizeChange := int64(res.size) - prevSize\n\t\t\t\t\t\tstate.totalMapSize += sizeChange\n\t\t\t\t\t\ttokenDiffsById[ctx.Id] += (res.tokens - prevTokens)\n\n\t\t\t\t\t\tstate.mapInputShas[path] = res.shaVal\n\t\t\t\t\t\tstate.mapInputTokens[path] = res.tokens\n\t\t\t\t\t\tstate.currentMapInputBatch[path] = res.mapContent\n\t\t\t\t\t\tstate.mapInputSizes[path] = res.size\n\n\t\t\t\t\t\tif len(res.mapFilesTruncatedTooLarge) > 0 {\n\t\t\t\t\t\t\tfor _, file := range res.mapFilesTruncatedTooLarge {\n\t\t\t\t\t\t\t\tif _, ok := mapFilesTruncatedSet[file.Path]; !ok {\n\t\t\t\t\t\t\t\t\tmapFilesTruncatedSet[file.Path] = true\n\t\t\t\t\t\t\t\t\tmapFilesTruncatedTooLarge = append(mapFilesTruncatedTooLarge, file)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif len(res.mapFilesSkippedAfterSizeLimit) > 0 {\n\t\t\t\t\t\t\tfor _, file := range res.mapFilesSkippedAfterSizeLimit {\n\t\t\t\t\t\t\t\tif _, ok := mapFilesSkippedAfterSizeLimitSet[file]; !ok {\n\t\t\t\t\t\t\t\t\tmapFilesSkippedAfterSizeLimitSet[file] = true\n\t\t\t\t\t\t\t\t\tmapFilesSkippedAfterSizeLimit = append(mapFilesSkippedAfterSizeLimit, file)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tinnerUpdatesErrCh <- nil\n\t\t\t\t\t}(path)\n\t\t\t\t}\n\n\t\t\t\tfor range mapPaths {\n\t\t\t\t\terr := <-innerUpdatesErrCh\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\thasAnyUpdate := len(state.removedMapPaths) > 0 || len(state.mapInputShas) > 0\n\n\t\t\t\tif hasAnyUpdate {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\tupdatedContexts = append(updatedContexts, ctx)\n\n\t\t\t\t\tnumMaps++\n\n\t\t\t\t\treqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {\n\t\t\t\t\t\tupdatedMapBodies, err := processMapBatches(state.mapInputBatches)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"failed to process map batches: %v\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn &shared.UpdateContextParams{\n\t\t\t\t\t\t\tMapBodies:       updatedMapBodies,\n\t\t\t\t\t\t\tInputShas:       state.mapInputShas,\n\t\t\t\t\t\t\tInputTokens:     state.mapInputTokens,\n\t\t\t\t\t\t\tInputSizes:      state.mapInputSizes,\n\t\t\t\t\t\t\tRemovedMapPaths: state.removedMapPaths,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}(context)\n\n\t\tcase shared.ContextURLType:\n\t\t\twg.Add(1)\n\t\t\tgo func(ctx *shared.Context) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tsem <- struct{}{}\n\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\tbody, err := url.FetchURLContent(ctx.Url)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to fetch the URL %s: %v\", ctx.Url, err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tsize := int64(len(body))\n\t\t\t\tif size > shared.MaxContextBodySize {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\tfilesSkippedTooLarge = append(filesSkippedTooLarge, filePathWithSize{Path: ctx.Url, Size: size})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif totalSize+size > shared.MaxContextBodySize {\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\thash := sha256.Sum256([]byte(body))\n\t\t\t\tnewSha := hex.EncodeToString(hash[:])\n\t\t\t\tif newSha != ctx.Sha {\n\t\t\t\t\tif totalContextCount >= shared.MaxContextCount {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\toldBodySize := int64(len(ctx.Body))\n\t\t\t\t\tnewBodySize := size\n\t\t\t\t\tif totalBodySize+(newBodySize-oldBodySize) > shared.MaxContextBodySize {\n\t\t\t\t\t\tmu.Lock()\n\t\t\t\t\t\tdefer mu.Unlock()\n\t\t\t\t\t\tfilesSkippedAfterSizeLimit = append(filesSkippedAfterSizeLimit, ctx.Url)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tnumTokens := shared.GetNumTokensEstimate(string(body))\n\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tdefer mu.Unlock()\n\n\t\t\t\t\ttotalSize += size\n\t\t\t\t\ttotalContextCount++\n\t\t\t\t\ttotalBodySize += (newBodySize - oldBodySize)\n\n\t\t\t\t\ttokenDiffsById[ctx.Id] = numTokens - ctx.NumTokens\n\n\t\t\t\t\tnumUrls++\n\t\t\t\t\tupdatedContexts = append(updatedContexts, ctx)\n\t\t\t\t\treqFns[ctx.Id] = func() (*shared.UpdateContextParams, error) {\n\t\t\t\t\t\treturn &shared.UpdateContextParams{\n\t\t\t\t\t\t\tBody: string(body),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}(context)\n\t\t}\n\t}\n\n\twg.Wait()\n\n\tif len(errs) > 0 {\n\t\treturn nil, fmt.Errorf(\"failed to check context outdated: %v\", errs)\n\t}\n\n\t// Identify contexts to remove\n\tvar removedContexts []*shared.Context\n\tfor id := range deleteIds {\n\t\tremovedContexts = append(removedContexts, contextsById[id])\n\t}\n\n\t// If nothing changed\n\tif len(reqFns) == 0 && len(removedContexts) == 0 {\n\t\treturn &types.ContextOutdatedResult{\n\t\t\tMsg: \"Context is up to date\",\n\t\t}, nil\n\t}\n\n\treqFn := func() (map[string]*shared.UpdateContextParams, error) {\n\t\treq := map[string]*shared.UpdateContextParams{}\n\t\tvar mu sync.Mutex\n\n\t\terrCh := make(chan error, len(reqFns))\n\t\tfor id, fn := range reqFns {\n\t\t\tgo func(id string, fn func() (*shared.UpdateContextParams, error)) {\n\t\t\t\tres, err := fn()\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tmu.Lock()\n\t\t\t\treq[id] = res\n\t\t\t\tmu.Unlock()\n\t\t\t\terrCh <- nil\n\t\t\t}(id, fn)\n\t\t}\n\t\tfor i := 0; i < len(reqFns); i++ {\n\t\t\terr := <-errCh\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn req, nil\n\t}\n\n\t// Build final result\n\toutdatedRes := types.ContextOutdatedResult{\n\t\tUpdatedContexts: updatedContexts,\n\t\tRemovedContexts: removedContexts,\n\t\tTokenDiffsById:  tokenDiffsById,\n\t\tNumFiles:        numFiles,\n\t\tNumUrls:         numUrls,\n\t\tNumTrees:        numTrees,\n\t\tNumMaps:         numMaps,\n\t\tNumFilesRemoved: numFilesRemoved,\n\t\tNumTreesRemoved: numTreesRemoved,\n\t\tReqFn:           reqFn,\n\t}\n\n\tvar hasConflicts bool\n\tvar msg string\n\tif doUpdate {\n\t\tres, err := UpdateContext(UpdateContextParams{\n\t\t\tContexts:    contexts,\n\t\t\tOutdatedRes: outdatedRes,\n\t\t\tReqFn:       reqFn,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to update context: %v\", err)\n\t\t}\n\t\thasConflicts = res.HasConflicts\n\t\tmsg = res.Msg\n\t\toutdatedRes.Msg = msg\n\t} else {\n\t\tvar tokensDiff int\n\t\tfor _, diff := range tokenDiffsById {\n\t\t\ttokensDiff += diff\n\t\t}\n\t\tnewTotal := totalTokens + tokensDiff\n\t\toutdatedRes.Msg = shared.SummaryForUpdateContext(shared.SummaryForUpdateContextParams{\n\t\t\tNumFiles:    numFiles,\n\t\t\tNumTrees:    numTrees,\n\t\t\tNumUrls:     numUrls,\n\t\t\tNumMaps:     numMaps,\n\t\t\tTokensDiff:  tokensDiff,\n\t\t\tTotalTokens: newTotal,\n\t\t})\n\t}\n\n\tif hasConflicts {\n\t\tterm.StartSpinner(\"🏗️  Starting build...\")\n\t\t_, err := buildPlanInlineFn(false, nil)\n\t\tterm.StopSpinner()\n\t\tfmt.Println()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to build plan: %v\", err)\n\t\t}\n\t}\n\n\t// Warn about any items skipped\n\tif len(filesSkippedTooLarge) > 0 || len(filesSkippedAfterSizeLimit) > 0 ||\n\t\tlen(mapFilesTruncatedTooLarge) > 0 || len(mapFilesSkippedAfterSizeLimit) > 0 {\n\t\tprintSkippedFilesMsg(filesSkippedTooLarge, filesSkippedAfterSizeLimit,\n\t\t\tmapFilesTruncatedTooLarge, mapFilesSkippedAfterSizeLimit)\n\t}\n\n\treturn &outdatedRes, nil\n}\n\nfunc tableForContextOutdated(updatedContexts []*shared.Context, tokenDiffsById map[string]int) string {\n\tif len(updatedContexts) == 0 {\n\t\treturn \"\"\n\t}\n\n\ttableString := &strings.Builder{}\n\ttable := tablewriter.NewWriter(tableString)\n\ttable.SetHeader([]string{\"Name\", \"Type\", \"🪙\"})\n\ttable.SetAutoWrapText(false)\n\n\tfor _, ctx := range updatedContexts {\n\t\tt, icon := ctx.TypeAndIcon()\n\t\tdiff := tokenDiffsById[ctx.Id]\n\t\tdiffStr := \"+\" + strconv.Itoa(diff)\n\t\ttableColor := tablewriter.FgHiGreenColor\n\n\t\tif diff < 0 {\n\t\t\tdiffStr = strconv.Itoa(diff)\n\t\t\ttableColor = tablewriter.FgHiRedColor\n\t\t}\n\n\t\trow := []string{\n\t\t\t\" \" + icon + \" \" + ctx.Name,\n\t\t\tt,\n\t\t\tdiffStr,\n\t\t}\n\n\t\ttable.Rich(row, []tablewriter.Colors{\n\t\t\t{tableColor, tablewriter.Bold},\n\t\t\t{tableColor},\n\t\t\t{tableColor},\n\t\t})\n\t}\n\n\ttable.Render()\n\treturn tableString.String()\n}\n"
  },
  {
    "path": "app/cli/lib/current.go",
    "content": "package lib\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/format\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n)\n\nvar CurrentProjectId string\nvar CurrentPlanId string\nvar CurrentBranch string\nvar HomeCurrentProjectDir string\nvar HomeCurrentPlanPath string\n\nfunc MustResolveOrCreateProject() {\n\tresolveProject(true, true)\n}\n\nfunc MustResolveProject() {\n\tresolveProject(true, false)\n}\n\nfunc MaybeResolveProject() {\n\tresolveProject(false, false)\n}\n\nfunc resolveProject(mustResolve, shouldCreate bool) {\n\tif fs.PlandexDir == \"\" {\n\t\tvar err error\n\t\tif shouldCreate {\n\t\t\t_, _, err = fs.FindOrCreatePlandex()\n\t\t} else {\n\t\t\tfs.FindPlandexDir()\n\t\t}\n\n\t\tif err != nil && mustResolve {\n\t\t\tterm.OutputErrorAndExit(\"error finding or creating plandex: %v\", err)\n\t\t}\n\t}\n\n\tif (fs.PlandexDir == \"\" || fs.ProjectRoot == \"\") && mustResolve {\n\t\tfmt.Printf(\n\t\t\t\"🤷‍♂️ No plans in current directory\\nTry %s to create a plan or %s to see plans in nearby directories\\n\",\n\t\t\tcolor.New(color.Bold, term.ColorHiCyan).Sprint(\"plandex new\"),\n\t\t\tcolor.New(color.Bold, term.ColorHiCyan).Sprint(\"plandex plans\"))\n\t\tos.Exit(0)\n\t}\n\n\tif fs.PlandexDir == \"\" {\n\t\treturn\n\t}\n\n\tMigrateLegacyProjectFile(auth.Current.UserId)\n\n\t// check if projects-v2.json exists in PlandexDir\n\tpath := filepath.Join(fs.PlandexDir, \"projects-v2.json\")\n\t_, err := os.Stat(path)\n\n\tif os.IsNotExist(err) {\n\t\tlog.Println(\"projects-v2.json does not exist\")\n\t\tlog.Println(\"Initializing project\")\n\t\tmustInitProject(nil)\n\t} else if err != nil {\n\t\tterm.OutputErrorAndExit(\"error checking if projects-v2.json exists: %v\", err)\n\t}\n\n\tvar settings *types.CurrentProjectSettings\n\tvar loadProjectSettings func()\n\tloadProjectSettings = func() {\n\t\t// read projects-v2.json\n\t\tbytes, err := os.ReadFile(path)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error reading projects-v2.json: %v\", err)\n\t\t}\n\n\t\tvar settingsByAccount types.CurrentProjectSettingsByAccount\n\t\terr = json.Unmarshal(bytes, &settingsByAccount)\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error unmarshalling projects-v2.json: %v\", err)\n\t\t}\n\n\t\tsettings = settingsByAccount[auth.Current.UserId]\n\t\tif settings == nil {\n\t\t\tmustInitProject(&settingsByAccount)\n\t\t\tloadProjectSettings()\n\t\t}\n\t}\n\n\tloadProjectSettings()\n\n\tCurrentProjectId = settings.Id\n\tMigrateLegacyCurrentPlanFile(auth.Current.UserId)\n\n\tHomeCurrentProjectDir = filepath.Join(fs.HomePlandexDir, CurrentProjectId)\n\tHomeCurrentPlanPath = filepath.Join(HomeCurrentProjectDir, \"current-plans-v2.json\")\n\n\terr = os.MkdirAll(HomeCurrentProjectDir, os.ModePerm)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error creating project dir: %v\", err)\n\t}\n\n\tMustLoadCurrentPlan()\n\tMigrateLegacyPlanSettingsFile(auth.Current.UserId)\n}\n\nfunc MustLoadCurrentPlan() {\n\tif CurrentProjectId == \"\" {\n\t\tterm.OutputErrorAndExit(\"No current project\")\n\t}\n\n\t// Check if the file exists\n\t_, err := os.Stat(HomeCurrentPlanPath)\n\n\tif os.IsNotExist(err) {\n\t\treturn\n\t} else if err != nil {\n\t\tterm.OutputErrorAndExit(\"error checking if current-plans-v2.json exists: %v\", err)\n\t}\n\n\t// Read the contents of the file\n\tfileBytes, err := os.ReadFile(HomeCurrentPlanPath)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error reading current-plans-v2.json: %v\", err)\n\t}\n\n\tvar currentPlansByAccount types.CurrentPlanSettingsByAccount\n\terr = json.Unmarshal(fileBytes, &currentPlansByAccount)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error unmarshalling current-plans-v2.json: %v\", err)\n\t}\n\n\tcurrentPlan := currentPlansByAccount[auth.Current.UserId]\n\n\tif currentPlan != nil {\n\t\tCurrentPlanId = currentPlan.Id\n\t}\n\n\tif CurrentPlanId != \"\" {\n\t\terr = loadCurrentBranch()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error loading current branch: %v\", err)\n\t\t}\n\n\t\tif CurrentBranch == \"\" {\n\t\t\terr = WriteCurrentBranch(\"main\")\n\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"error setting current branch: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc loadCurrentBranch() error {\n\t// Load plan-specific settings\n\tif CurrentPlanId == \"\" {\n\t\treturn fmt.Errorf(\"no current plan\")\n\t}\n\n\tpath := filepath.Join(HomeCurrentProjectDir, CurrentPlanId, \"settings-v2.json\")\n\n\t// Check if the file exists\n\t_, err := os.Stat(path)\n\n\tif os.IsNotExist(err) {\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"error checking if settings-v2.json exists: %v\", err)\n\t}\n\n\tfileBytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error reading settings-v2.json: %v\", err)\n\t}\n\n\tvar settingsByAccount types.PlanSettingsByAccount\n\terr = json.Unmarshal(fileBytes, &settingsByAccount)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error unmarshalling settings-v2.json: %v\", err)\n\t}\n\tsettings := settingsByAccount[auth.Current.UserId]\n\n\tif settings == nil {\n\t\treturn nil\n\t}\n\n\tCurrentBranch = settings.Branch\n\n\treturn nil\n}\n\nfunc GetCurrentPlanTable(plan *shared.Plan, currentBranchesByPlanId map[string]*shared.Branch, onlyCols []string) string {\n\tb := &strings.Builder{}\n\ttable := tablewriter.NewWriter(b)\n\ttable.SetAutoWrapText(false)\n\n\tvar cols []string\n\n\tif onlyCols == nil {\n\t\tcols = []string{\"Current Plan\", \"Updated\", \"Created\" /*\"Branches\",*/, \"Branch\", \"Context\", \"Convo\"}\n\t} else {\n\t\tcols = onlyCols\n\t}\n\n\ttable.SetHeader(cols)\n\n\tname := color.New(color.Bold, term.ColorHiGreen).Sprint(plan.Name)\n\tbranch := currentBranchesByPlanId[CurrentPlanId]\n\n\tvar row []string\n\n\tfor _, col := range cols {\n\t\tswitch col {\n\t\tcase \"Current Plan\":\n\t\t\trow = append(row, name)\n\t\tcase \"Updated\":\n\t\t\trow = append(row, format.Time(plan.UpdatedAt))\n\t\tcase \"Created\":\n\t\t\trow = append(row, format.Time(plan.CreatedAt))\n\t\tcase \"Branch\":\n\t\t\trow = append(row, CurrentBranch)\n\t\tcase \"Context\":\n\t\t\trow = append(row, strconv.Itoa(branch.ContextTokens)+\" 🪙\")\n\t\tcase \"Convo\":\n\t\t\trow = append(row, strconv.Itoa(branch.ConvoTokens)+\" 🪙\")\n\t\t}\n\t}\n\n\tstyle := []tablewriter.Colors{\n\t\t{tablewriter.FgGreenColor, tablewriter.Bold},\n\t}\n\n\ttable.Rich(row, style)\n\ttable.Render()\n\n\treturn b.String()\n}\n\nfunc mustInitProject(existingSettings *types.CurrentProjectSettingsByAccount) {\n\tres, apiErr := api.Client.CreateProject(shared.CreateProjectRequest{Name: filepath.Base(fs.ProjectRoot)})\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"error creating project: %v\", apiErr.Msg)\n\t}\n\n\tlog.Println(\"Project created:\", res.Id)\n\n\tCurrentProjectId = res.Id\n\n\tvar settingsByAccount types.CurrentProjectSettingsByAccount\n\tif existingSettings != nil {\n\t\tsettingsByAccount = *existingSettings\n\t} else {\n\t\tsettingsByAccount = types.CurrentProjectSettingsByAccount{}\n\t}\n\n\tsettingsByAccount[auth.Current.UserId] = &types.CurrentProjectSettings{\n\t\tId: CurrentProjectId,\n\t}\n\n\t// write projects-v2.json\n\tpath := filepath.Join(fs.PlandexDir, \"projects-v2.json\")\n\tbytes, err := json.Marshal(settingsByAccount)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error marshalling project settings: %v\", err)\n\t}\n\n\terr = os.WriteFile(path, bytes, os.ModePerm)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error writing projects-v2.json: %v\", err)\n\t}\n\n\tlog.Println(\"Wrote projects-v2.json\")\n\n\t// write current-plans-v2.json to PlandexHomeDir/[projectId]/current-plans-v2.json\n\tdir := filepath.Join(fs.HomePlandexDir, CurrentProjectId)\n\terr = os.MkdirAll(dir, os.ModePerm)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error creating project dir: %v\", err)\n\t}\n\n\tpath = filepath.Join(dir, \"current-plans-v2.json\")\n\tbytes, err = json.Marshal(types.CurrentPlanSettingsByAccount{\n\t\tauth.Current.UserId: &types.CurrentPlanSettings{\n\t\t\tId: \"\",\n\t\t},\n\t})\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error marshalling plan settings: %v\", err)\n\t}\n\n\terr = os.WriteFile(path, bytes, os.ModePerm)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error writing current-plans-v2.json: %v\", err)\n\t}\n\n\tlog.Println(\"Wrote current-plans-v2.json\")\n}\n"
  },
  {
    "path": "app/cli/lib/custom_models.go",
    "content": "package lib\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/schema\"\n\t\"plandex-cli/term\"\n\tshared \"plandex-shared\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n)\n\ntype CustomModelsCheckLocalChangesResult struct {\n\tHasLocalChanges  bool\n\tLocalModelsInput shared.ModelsInput\n}\n\nfunc GetCustomModelsPath(userId string) string {\n\treturn filepath.Join(fs.HomePlandexDir, \"accounts\", userId, \"custom-models.json\")\n}\n\nfunc GetServerModelsInput() (*shared.ModelsInput, error) {\n\terrCh := make(chan *shared.ApiError, 3)\n\tvar (\n\t\tcustomModels     []*shared.CustomModel\n\t\tcustomProviders  []*shared.CustomProvider\n\t\tcustomModelPacks []*shared.ModelPackSchema\n\t)\n\n\tgo func() {\n\t\tmodels, apiErr := api.Client.ListCustomModels()\n\t\tif apiErr != nil {\n\t\t\terrCh <- apiErr\n\t\t\treturn\n\t\t}\n\t\tcustomModels = models\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\t// custom providers are not supported on cloud\n\t\tif auth.Current.IsCloud {\n\t\t\terrCh <- nil\n\t\t\treturn\n\t\t}\n\t\tproviders, apiErr := api.Client.ListCustomProviders()\n\t\tif apiErr != nil {\n\t\t\terrCh <- apiErr\n\t\t\treturn\n\t\t}\n\t\tcustomProviders = providers\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tmodelPacks, apiErr := api.Client.ListModelPacks()\n\t\tif apiErr != nil {\n\t\t\terrCh <- apiErr\n\t\t\treturn\n\t\t}\n\n\t\tschemas := make([]*shared.ModelPackSchema, len(modelPacks))\n\t\tfor i, modelPack := range modelPacks {\n\t\t\tschemas[i] = modelPack.ToModelPackSchema()\n\t\t}\n\n\t\tcustomModelPacks = schemas\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 3; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error fetching custom models: %v\", err.Msg)\n\t\t}\n\t}\n\n\tserverModelsInput := &shared.ModelsInput{\n\t\tCustomModels:     customModels,\n\t\tCustomProviders:  customProviders,\n\t\tCustomModelPacks: customModelPacks,\n\t}\n\n\treturn serverModelsInput, nil\n}\n\nfunc CustomModelsCheckLocalChanges(path string) (CustomModelsCheckLocalChangesResult, error) {\n\thashPath := path + \".hash\"\n\n\texists, err := fs.FileExists(path)\n\tif err != nil {\n\t\treturn CustomModelsCheckLocalChangesResult{}, err\n\t}\n\n\tif !exists {\n\t\treturn CustomModelsCheckLocalChangesResult{}, nil\n\t}\n\n\tlocalJsonData, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn CustomModelsCheckLocalChangesResult{}, fmt.Errorf(\"error reading JSON file: %v\", err)\n\t}\n\n\tvar localClientModelsInput shared.ClientModelsInput\n\terr = json.Unmarshal(localJsonData, &localClientModelsInput)\n\tif err != nil {\n\t\treturn CustomModelsCheckLocalChangesResult{}, fmt.Errorf(\"error unmarshalling JSON file: %v\", err)\n\t}\n\n\tlocalModelsInput := localClientModelsInput.ToModelsInput()\n\n\tlastSavedHash, err := os.ReadFile(hashPath)\n\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn CustomModelsCheckLocalChangesResult{}, fmt.Errorf(\"error reading hash file: %v\", err)\n\t}\n\n\tcurrentHash, err := localModelsInput.Hash()\n\tif err != nil {\n\t\treturn CustomModelsCheckLocalChangesResult{}, fmt.Errorf(\"error hashing models: %v\", err)\n\t}\n\n\treturn CustomModelsCheckLocalChangesResult{\n\t\tHasLocalChanges:  currentHash != string(lastSavedHash),\n\t\tLocalModelsInput: localModelsInput,\n\t}, nil\n}\n\nfunc WriteCustomModelsFile(path string, modelsInput *shared.ModelsInput) error {\n\terr := os.MkdirAll(filepath.Dir(path), 0755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating directory: %v\", err)\n\t}\n\n\tclientModelsInput := modelsInput.ToClientModelsInput()\n\tclientModelsInput.PrepareUpdate()\n\n\tjsonData, err := json.MarshalIndent(clientModelsInput, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling models: %v\", err)\n\t}\n\n\terr = os.WriteFile(path, jsonData, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing file: %v\", err)\n\t}\n\n\terr = SaveCustomModelsHash(path, modelsInput)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error saving hash file: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc SaveCustomModelsHash(basePath string, modelsInput *shared.ModelsInput) error {\n\thashPath := basePath + \".hash\"\n\n\thash, err := modelsInput.Hash()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error hashing models: %v\", err)\n\t}\n\n\terr = os.WriteFile(hashPath, []byte(hash), 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing hash file: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc MustSyncCustomModels(path string, serverModelsInput *shared.ModelsInput) bool {\n\tterm.StartSpinner(\"\")\n\n\tjsonData, err := os.ReadFile(path)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error reading custom models file: %v\", err)\n\t\treturn false\n\t}\n\n\tclientModelsInput, err := schema.ValidateModelsInputJSON(jsonData)\n\tif err != nil {\n\t\tterm.StopSpinner()\n\t\tcolor.New(color.Bold, term.ColorHiRed).Println(\"🚨 Error validating custom models file\")\n\t\tfmt.Println(err.Error())\n\t\treturn false\n\t}\n\n\tmodelsInput := clientModelsInput.ToModelsInput()\n\n\tnoDuplicates, errMsg := modelsInput.CheckNoDuplicates()\n\tif !noDuplicates {\n\t\tterm.StopSpinner()\n\t\tcolor.New(color.Bold, term.ColorHiRed).Println(\"🚨 Some items in custom models file are duplicated:\")\n\t\tfmt.Println()\n\t\tfmt.Println(errMsg)\n\t\treturn false\n\t}\n\n\tif modelsInput.Equals(*serverModelsInput) {\n\t\tterm.StopSpinner()\n\t\treturn false\n\t}\n\n\tapiErr := api.Client.CreateCustomModels(&modelsInput)\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error importing models: %v\", apiErr.Msg)\n\t\treturn false\n\t}\n\n\terr = SaveCustomModelsHash(path, &modelsInput)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error saving hash file: %v\", err)\n\t\treturn false\n\t}\n\n\tinputModelIds := map[string]bool{}\n\tinputProviderNames := map[string]bool{}\n\tinputModelPackNames := map[string]bool{}\n\tfor _, model := range clientModelsInput.CustomModels {\n\t\tinputModelIds[string(model.ModelId)] = true\n\t}\n\tfor _, provider := range clientModelsInput.CustomProviders {\n\t\tinputProviderNames[provider.Name] = true\n\t}\n\tfor _, modelPack := range clientModelsInput.CustomModelPacks {\n\t\tinputModelPackNames[modelPack.Name] = true\n\t}\n\n\tupdatedModelsInput := modelsInput.FilterUnchanged(serverModelsInput)\n\n\tcustomModels := serverModelsInput.CustomModels\n\tcustomProviders := serverModelsInput.CustomProviders\n\tcustomModelPacks := serverModelsInput.CustomModelPacks\n\n\tterm.StopSpinner()\n\n\tadded := strings.Builder{}\n\tupdated := strings.Builder{}\n\tdeleted := strings.Builder{}\n\n\texistsById := map[string]bool{}\n\tfor _, model := range customModels {\n\t\texistsById[string(model.ModelId)] = true\n\t}\n\tfor _, provider := range customProviders {\n\t\texistsById[provider.Name] = true\n\t}\n\tfor _, modelPack := range customModelPacks {\n\t\texistsById[modelPack.Name] = true\n\t}\n\n\tfor _, provider := range updatedModelsInput.CustomProviders {\n\t\taction := \"✅ Added\"\n\t\tbuilder := &added\n\t\tif existsById[provider.Name] {\n\t\t\taction = \"🔄 Updated\"\n\t\t\tbuilder = &updated\n\t\t}\n\t\tbuilder.WriteString(fmt.Sprintf(\"%s custom %s → %s\\n\",\n\t\t\taction,\n\t\t\tcolor.New(term.ColorHiCyan).Sprint(\"provider\"),\n\t\t\tcolor.New(color.Bold, term.ColorHiGreen).Sprint(provider.Name)))\n\t}\n\tfor _, provider := range customProviders {\n\t\tif !inputProviderNames[provider.Name] {\n\t\t\tdeleted.WriteString(fmt.Sprintf(\"❌ Removed custom %s → %s\\n\",\n\t\t\t\tcolor.New(term.ColorHiCyan).Sprint(\"provider\"),\n\t\t\t\tcolor.New(color.Bold, term.ColorHiRed).Sprint(provider.Name)))\n\t\t}\n\t}\n\n\tfor _, model := range updatedModelsInput.CustomModels {\n\t\taction := \"✅ Added\"\n\t\tbuilder := &added\n\t\tif existsById[string(model.ModelId)] {\n\t\t\taction = \"🔄 Updated\"\n\t\t\tbuilder = &updated\n\t\t}\n\t\tbuilder.WriteString(fmt.Sprintf(\"%s custom %s → %s\\n\",\n\t\t\taction,\n\t\t\tcolor.New(term.ColorHiCyan).Sprint(\"model\"),\n\t\t\tcolor.New(color.Bold, term.ColorHiGreen).Sprint(string(model.ModelId))))\n\t}\n\tfor _, model := range customModels {\n\t\tif !inputModelIds[string(model.ModelId)] {\n\t\t\tdeleted.WriteString(fmt.Sprintf(\"❌ Removed custom %s → %s\\n\",\n\t\t\t\tcolor.New(term.ColorHiCyan).Sprint(\"model\"),\n\t\t\t\tcolor.New(color.Bold, term.ColorHiRed).Sprint(string(model.ModelId))))\n\t\t}\n\t}\n\n\tfor _, modelPack := range updatedModelsInput.CustomModelPacks {\n\t\taction := \"✅ Added\"\n\t\tbuilder := &added\n\t\tif existsById[modelPack.Name] {\n\t\t\taction = \"🔄 Updated\"\n\t\t\tbuilder = &updated\n\t\t}\n\t\tbuilder.WriteString(fmt.Sprintf(\"%s custom %s → %s\\n\",\n\t\t\taction,\n\t\t\tcolor.New(term.ColorHiCyan).Sprint(\"model pack\"),\n\t\t\tcolor.New(color.Bold, term.ColorHiGreen).Sprint(modelPack.Name)))\n\t}\n\tfor _, modelPack := range customModelPacks {\n\t\tif !inputModelPackNames[modelPack.Name] {\n\t\t\tdeleted.WriteString(fmt.Sprintf(\"❌ Removed custom %s → %s\\n\",\n\t\t\t\tcolor.New(term.ColorHiCyan).Sprint(\"model pack\"),\n\t\t\t\tcolor.New(color.Bold, term.ColorHiRed).Sprint(modelPack.Name)))\n\t\t}\n\t}\n\n\tif updated.Len()+added.Len()+deleted.Len() == 0 {\n\t\treturn false\n\t}\n\n\tfmt.Print(added.String())\n\tfmt.Print(updated.String())\n\tfmt.Print(deleted.String())\n\n\treturn true\n}\n\nfunc SyncCustomModels() error {\n\tuserId := auth.Current.UserId\n\tif userId == \"\" {\n\t\treturn fmt.Errorf(\"auth.Current.UserId is empty\")\n\t}\n\n\tserverModelsInput, err := GetServerModelsInput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting server models input: %v\", err)\n\t}\n\n\tMustSyncCustomModels(GetCustomModelsPath(userId), serverModelsInput)\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/lib/editor.go",
    "content": "package lib\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/term\"\n\tshared \"plandex-shared\"\n\t\"sort\"\n\t\"strings\"\n)\n\nfunc MaybePromptAndOpen(path string, defaultConfig *shared.PlanConfig, planConfig *shared.PlanConfig) bool {\n\tvar cmd string\n\tvar args []string\n\tvar openManually bool\n\n\tvar checkConfig *shared.PlanConfig\n\tif planConfig != nil {\n\t\tcheckConfig = planConfig\n\t} else if defaultConfig != nil {\n\t\tcheckConfig = defaultConfig\n\t} else {\n\t\tterm.OutputErrorAndExit(\"Missing config\")\n\t}\n\n\tif checkConfig.EditorOpenManually {\n\t\treturn false\n\t}\n\n\tif checkConfig.EditorCommand == \"\" {\n\t\teditorRes := SelectEditor(true)\n\t\tcmd = editorRes.Cmd\n\t\targs = editorRes.Args\n\t\topenManually = editorRes.OpenManually\n\n\t\t// update the default editor config\n\t\ttoUpdateDefault := *defaultConfig\n\t\ttoUpdateDefault.Editor = editorRes.Name\n\t\ttoUpdateDefault.EditorCommand = cmd\n\t\ttoUpdateDefault.EditorArgs = args\n\t\ttoUpdateDefault.EditorOpenManually = openManually\n\n\t\tapiErr := api.Client.UpdateDefaultPlanConfig(shared.UpdateDefaultPlanConfigRequest{\n\t\t\tConfig: &toUpdateDefault,\n\t\t})\n\t\tif apiErr != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error updating default config: %v\", apiErr)\n\t\t}\n\n\t\t// also update the current plan config\n\t\tif planConfig != nil {\n\t\t\ttoUpdate := *planConfig\n\t\t\ttoUpdate.Editor = editorRes.Name\n\t\t\ttoUpdate.EditorCommand = cmd\n\t\t\ttoUpdate.EditorArgs = args\n\t\t\ttoUpdate.EditorOpenManually = openManually\n\t\t\tapiErr = api.Client.UpdatePlanConfig(CurrentPlanId, shared.UpdatePlanConfigRequest{\n\t\t\t\tConfig: &toUpdate,\n\t\t\t})\n\t\t\tif apiErr != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error updating plan config: %v\", apiErr)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tcmd = checkConfig.EditorCommand\n\t\targs = checkConfig.EditorArgs\n\t}\n\n\tif openManually {\n\t\treturn false\n\t}\n\n\terr := exec.Command(cmd, append(args, path)...).Start()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error opening template: %v\", err)\n\t}\n\n\treturn true\n}\n\ntype SelectEditorResult struct {\n\tName         string\n\tCmd          string\n\tArgs         []string\n\tOpenManually bool\n}\n\nfunc SelectEditor(includeOpenManuallyOpt bool) SelectEditorResult {\n\teditors := detectEditors()\n\n\topts := []string{}\n\tfor _, c := range editors {\n\t\topts = append(opts, c.name)\n\t}\n\tconst otherOpt = \"Other (custom command)\"\n\topts = append(opts, otherOpt)\n\n\tconst openManuallyOpt = \"Open files manually\"\n\tif includeOpenManuallyOpt {\n\t\topts = append(opts, openManuallyOpt)\n\t}\n\n\tchoice, err := term.SelectFromList(\"What's your preferred editor?\", opts)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error selecting editor: %v\", err)\n\t}\n\n\tvar name string\n\tvar cmd string\n\tvar args []string\n\n\tif choice == otherOpt {\n\t\tchoice, err = term.GetRequiredUserStringInput(\"Enter the command to open the editor\")\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error getting editor command: %v\", err)\n\t\t}\n\t\tname = choice\n\t\tparts := strings.Fields(choice)\n\t\tif len(parts) == 0 {\n\t\t\tterm.OutputErrorAndExit(\"Invalid editor command: %s\", choice)\n\t\t}\n\t\tcmd = parts[0]\n\t\tif len(parts) > 1 {\n\t\t\targs = parts[1:]\n\t\t}\n\t} else if choice == openManuallyOpt {\n\t\treturn SelectEditorResult{\n\t\t\tName:         \"Open manually\",\n\t\t\tCmd:          \"\",\n\t\t\tArgs:         []string{},\n\t\t\tOpenManually: true,\n\t\t}\n\t} else {\n\t\tvar candidate editorCandidate\n\t\tfor _, c := range editors {\n\t\t\tif c.name == choice {\n\t\t\t\tcandidate = c\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tname = candidate.name\n\t\tcmd = candidate.cmd\n\t\targs = candidate.args\n\t}\n\n\treturn SelectEditorResult{\n\t\tName: name,\n\t\tCmd:  cmd,\n\t\tArgs: args,\n\t}\n}\n\ntype editorCandidate struct {\n\tname        string\n\tcmd         string\n\targs        []string\n\tisJetBrains bool\n}\n\nconst maxEditorOpts = 5\n\nfunc detectEditors() []editorCandidate {\n\tguess := []editorCandidate{\n\t\t// Popular non-JetBrains launchers\n\t\t{\"VS Code\", \"code\", nil, false},\n\t\t{\"Cursor\", \"cursor\", nil, false},\n\t\t{\"Zed\", \"zed\", nil, false},\n\t\t{\"Neovim\", \"nvim\", nil, false},\n\n\t\t// JetBrains IDE-specific launchers\n\t\t{\"IntelliJ IDEA\", \"idea\", nil, true},\n\t\t{\"GoLand\", \"goland\", nil, true},\n\t\t{\"PyCharm\", \"pycharm\", nil, true},\n\t\t{\"CLion\", \"clion\", nil, true},\n\t\t{\"WebStorm\", \"webstorm\", nil, true},\n\t\t{\"PhpStorm\", \"phpstorm\", nil, true},\n\t\t{\"DataGrip\", \"datagrip\", nil, true},\n\t\t{\"RubyMine\", \"rubymine\", nil, true},\n\t\t{\"Rider\", \"rider\", nil, true},\n\t\t{\"DataSpell\", \"dataspell\", nil, true},\n\n\t\t// JetBrains universal CLI (2023.2+)\n\t\t{\"JetBrains (jb)\", \"jb\", []string{\"open\"}, true},\n\n\t\t{\"Vim\", \"vim\", nil, false},\n\t\t{\"Nano\", \"nano\", nil, false},\n\t\t{\"Helix\", \"hx\", nil, false},\n\t\t{\"Micro\", \"micro\", nil, false},\n\t\t{\"Sublime Text\", \"subl\", nil, false},\n\t\t{\"TextMate\", \"mate\", nil, false},\n\t\t{\"Kakoune\", \"kak\", nil, false},\n\t\t{\"Emacs\", \"emacs\", nil, false},\n\t\t{\"Kate\", \"kate\", nil, false},\n\t}\n\tpref := map[string]bool{}\n\tfor _, env := range []string{\"VISUAL\", \"EDITOR\"} {\n\t\tif v := os.Getenv(env); v != \"\" {\n\t\t\t// keep only the binary name, drop path/flags\n\t\t\tcmd := filepath.Base(strings.Fields(v)[0])\n\t\t\tpref[cmd] = true\n\t\t}\n\t}\n\n\t_, err := exec.LookPath(\"jb\") // true if universal launcher exists\n\tjbOnPath := err == nil\n\n\tvar found []editorCandidate\n\tfor _, c := range guess {\n\t\tif _, err := exec.LookPath(c.cmd); err != nil {\n\t\t\tcontinue // not on PATH\n\t\t}\n\n\t\t// If jb is present, drop per-IDE launchers *unless* this exact cmd\n\t\t// is marked preferred by VISUAL/EDITOR.\n\t\tif jbOnPath && c.isJetBrains && !pref[c.cmd] {\n\t\t\tcontinue\n\t\t}\n\t\tfound = append(found, c)\n\t}\n\n\tfor cmd := range pref {\n\t\tif _, err := exec.LookPath(cmd); err == nil {\n\t\t\talready := false\n\t\t\tfor _, c := range found {\n\t\t\t\tif c.cmd == cmd {\n\t\t\t\t\talready = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !already {\n\t\t\t\tfound = append(found, editorCandidate{name: cmd, cmd: cmd})\n\t\t\t}\n\t\t}\n\t}\n\tsort.SliceStable(found, func(i, j int) bool {\n\t\tpi, pj := pref[found[i].cmd], pref[found[j].cmd]\n\t\tif pi == pj {\n\t\t\treturn false // keep original order\n\t\t}\n\t\treturn pi // true → i comes before j\n\t})\n\tif len(found) > maxEditorOpts {\n\t\tfound = found[:maxEditorOpts]\n\t}\n\n\treturn found\n}\n"
  },
  {
    "path": "app/cli/lib/git.go",
    "content": "package lib\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar gitMutex sync.Mutex\n\nfunc GitAddAndCommit(dir, message string, lockMutex bool) error {\n\tif lockMutex {\n\t\tgitMutex.Lock()\n\t\tdefer gitMutex.Unlock()\n\t}\n\n\terr := GitAdd(dir, \".\", false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\terr = GitCommit(dir, message, nil, false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn nil\n}\n\nfunc GitAddAndCommitPaths(dir, message string, paths []string, lockMutex bool) error {\n\tif len(paths) == 0 {\n\t\treturn nil\n\t}\n\n\tif lockMutex {\n\t\tgitMutex.Lock()\n\t\tdefer gitMutex.Unlock()\n\t}\n\n\tfor _, path := range paths {\n\t\terr := GitAdd(dir, path, false)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error adding file %s to git repository for dir: %s, err: %v\", path, dir, err)\n\t\t}\n\t}\n\n\terr := GitCommit(dir, message, paths, false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn nil\n}\n\nfunc GitAdd(repoDir, path string, lockMutex bool) error {\n\tif lockMutex {\n\t\tgitMutex.Lock()\n\t\tdefer gitMutex.Unlock()\n\t}\n\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"add\", path).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc GitCommit(repoDir, commitMsg string, paths []string, lockMutex bool) error {\n\tif lockMutex {\n\t\tgitMutex.Lock()\n\t\tdefer gitMutex.Unlock()\n\t}\n\n\targs := []string{\"-C\", repoDir, \"commit\", \"-m\", commitMsg, \"--allow-empty\"}\n\n\tif len(paths) > 0 {\n\t\targs = append(args, paths...)\n\t}\n\n\tres, err := exec.Command(\"git\", args...).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc CheckUncommittedChanges() (bool, error) {\n\tgitMutex.Lock()\n\tdefer gitMutex.Unlock()\n\n\t// Check if there are any changes\n\tres, err := exec.Command(\"git\", \"status\", \"--porcelain\").CombinedOutput()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error checking for uncommitted changes: %v, output: %s\", err, string(res))\n\t}\n\n\t// If there's output, there are uncommitted changes\n\treturn strings.TrimSpace(string(res)) != \"\", nil\n}\n\nfunc GitStashCreate(message string) error {\n\tgitMutex.Lock()\n\tdefer gitMutex.Unlock()\n\n\tres, err := exec.Command(\"git\", \"stash\", \"push\", \"--include-untracked\", \"-m\", message).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating git stash: %v, output: %s\", err, string(res))\n\t}\n\n\treturn nil\n}\n\n// this matches output for git version 2.39.3\n// need to test on other versions and check for more variations\n// there isn't any structured way to get stash conflicts from git, unfortunately\nconst PopStashConflictMsg = \"overwritten by merge\"\nconst ConflictMsgFilesEnd = \"commit your changes\"\n\nfunc GitStashPop(forceOverwrite bool) error {\n\tgitMutex.Lock()\n\tdefer gitMutex.Unlock()\n\n\tres, err := exec.Command(\"git\", \"stash\", \"pop\").CombinedOutput()\n\n\t// we should no longer have conflicts since we are forcing an update before\n\t// running the 'apply' command as well as resetting any files with uncommitted change\n\t// still leaving this though in case something goes wrong\n\n\tif err != nil {\n\t\tlog.Println(\"Error popping git stash:\", string(res))\n\n\t\tif strings.Contains(string(res), PopStashConflictMsg) {\n\t\t\tlog.Println(\"Conflicts detected\")\n\n\t\t\tif !forceOverwrite {\n\t\t\t\treturn fmt.Errorf(\"conflict popping git stash: %s\", string(res))\n\t\t\t}\n\n\t\t\t// Parse the output to find which files have conflicts\n\t\t\tconflictFiles := parseConflictFiles(string(res))\n\n\t\t\tlog.Println(\"Conflicting files:\", conflictFiles)\n\n\t\t\tfor _, file := range conflictFiles {\n\t\t\t\t// Reset each conflicting file individually\n\t\t\t\tcheckoutRes, err := exec.Command(\"git\", \"checkout\", \"--ours\", file).CombinedOutput()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error resetting file %s: %v\", file, string(checkoutRes))\n\t\t\t\t}\n\t\t\t}\n\t\t\tdropRes, err := exec.Command(\"git\", \"stash\", \"drop\").CombinedOutput()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error dropping git stash: %v\", string(dropRes))\n\t\t\t}\n\t\t\treturn nil\n\t\t} else {\n\t\t\tlog.Println(\"No conflicts detected\")\n\n\t\t\treturn fmt.Errorf(\"error popping git stash: %v\", string(res))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc GitClearUncommittedChanges() error {\n\tgitMutex.Lock()\n\tdefer gitMutex.Unlock()\n\n\t// Reset staged changes\n\tres, err := exec.Command(\"git\", \"reset\", \"--hard\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error resetting staged changes | err: %v, output: %s\", err, string(res))\n\t}\n\n\t// Clean untracked files\n\tres, err = exec.Command(\"git\", \"clean\", \"-d\", \"-f\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error cleaning untracked files | err: %v, output: %s\", err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc GitFileHasUncommittedChanges(path string) (bool, error) {\n\tgitMutex.Lock()\n\tdefer gitMutex.Unlock()\n\n\tres, err := exec.Command(\"git\", \"status\", \"--porcelain\", path).CombinedOutput()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error checking for uncommitted changes for file %s | err: %v, output: %s\", path, err, string(res))\n\t}\n\n\treturn strings.TrimSpace(string(res)) != \"\", nil\n}\n\nfunc GitCheckoutFile(path string) error {\n\tgitMutex.Lock()\n\tdefer gitMutex.Unlock()\n\n\tres, err := exec.Command(\"git\", \"checkout\", path).CombinedOutput()\n\tif err != nil {\n\t\tlog.Println(\"Error checking out file:\", string(res))\n\n\t\treturn fmt.Errorf(\"error checking out file %s | err: %v, output: %s\", path, err, string(res))\n\t}\n\n\treturn nil\n}\n\nconst GitLogTimestampFormat = \"Mon Jan 2, 2006 | 3:04:05pm\"\n\nvar GitLogTimestampRegex = regexp.MustCompile(`\\w{3} \\w{3} \\d{1,2}, \\d{4} \\| \\d{1,2}:\\d{2}:\\d{2}(am|pm) UTC`)\n\nfunc GetGitLogTimestamp(log string) (time.Time, error) {\n\tmatches := GitLogTimestampRegex.FindStringSubmatch(log)\n\tif len(matches) < 2 {\n\t\treturn time.Time{}, fmt.Errorf(\"no timestamp found in log\")\n\t}\n\n\treturn time.Parse(GitLogTimestampFormat, strings.TrimSuffix(matches[0], \" UTC\"))\n}\n\nfunc parseConflictFiles(gitOutput string) []string {\n\tvar conflictFiles []string\n\tlines := strings.Split(gitOutput, \"\\n\")\n\n\tinFilesSection := false\n\n\tfor _, line := range lines {\n\t\tif inFilesSection {\n\t\t\tfile := strings.TrimSpace(line)\n\t\t\tif file == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconflictFiles = append(conflictFiles, strings.TrimSpace(line))\n\t\t} else if strings.Contains(line, PopStashConflictMsg) {\n\t\t\tinFilesSection = true\n\t\t} else if strings.Contains(line, ConflictMsgFilesEnd) {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn conflictFiles\n}\n"
  },
  {
    "path": "app/cli/lib/legacy_files.go",
    "content": "package lib\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n)\n\nfunc MigrateLegacyProjectFile(currentUserId string) {\n\tif fs.PlandexDir == \"\" {\n\t\treturn\n\t}\n\n\tif currentUserId == \"\" {\n\t\treturn\n\t}\n\n\t// Migrate project.json to projects-v2.json\n\t// New formats map to a user id so we can handle multiple accounts in the same plandex dir\n\tprojectPath := filepath.Join(fs.PlandexDir, \"project.json\")\n\tif _, err := os.Stat(projectPath); err == nil {\n\t\tvar settings types.CurrentProjectSettings\n\t\tbytes, err := os.ReadFile(projectPath)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error reading project.json: %v\", err)\n\t\t}\n\n\t\terr = json.Unmarshal(bytes, &settings)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error unmarshalling project.json: %v\", err)\n\t\t}\n\n\t\tv2Path := filepath.Join(fs.PlandexDir, \"projects-v2.json\")\n\t\tsettingsByAccount := types.CurrentProjectSettingsByAccount{\n\t\t\tcurrentUserId: &settings,\n\t\t}\n\t\tbytes, err = json.Marshal(settingsByAccount)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error marshalling projects-v2.json: %v\", err)\n\t\t}\n\n\t\terr = os.WriteFile(v2Path, bytes, 0644)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error writing projects-v2.json: %v\", err)\n\t\t}\n\n\t\t// Delete the v1 file after successful migration\n\t\tif err := os.Remove(projectPath); err != nil {\n\t\t\tterm.OutputErrorAndExit(\"could not delete old project.json: %v\", err)\n\t\t}\n\t\tlog.Println(\"Migrated project.json to projects-v2.json\")\n\n\t} else if !os.IsNotExist(err) {\n\t\tterm.OutputErrorAndExit(\"error checking for project.json: %v\", err)\n\t}\n}\n\nfunc MigrateLegacyCurrentPlanFile(currentUserId string) {\n\tif fs.PlandexDir == \"\" {\n\t\treturn\n\t}\n\n\tif currentUserId == \"\" {\n\t\treturn\n\t}\n\n\tif CurrentProjectId == \"\" {\n\t\treturn\n\t}\n\n\t// Migrate current_plan.json to current-plans-v2.json\n\tplanPath := filepath.Join(fs.HomePlandexDir, CurrentProjectId, \"current_plan.json\")\n\n\tif _, err := os.Stat(planPath); err == nil {\n\t\tvar settings types.CurrentPlanSettings\n\t\tbytes, err := os.ReadFile(planPath)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error reading current_plan.json: %v\", err)\n\t\t}\n\n\t\terr = json.Unmarshal(bytes, &settings)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error unmarshalling current_plan.json: %v\", err)\n\t\t}\n\n\t\tv2Path := filepath.Join(fs.HomePlandexDir, CurrentProjectId, \"current-plans-v2.json\")\n\t\tsettingsByAccount := types.CurrentPlanSettingsByAccount{\n\t\t\tcurrentUserId: &settings,\n\t\t}\n\t\tbytes, err = json.Marshal(settingsByAccount)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error marshalling current-plans-v2.json: %v\", err)\n\t\t}\n\n\t\terr = os.WriteFile(v2Path, bytes, 0644)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error writing current-plans-v2.json: %v\", err)\n\t\t}\n\n\t\t// Delete the v1 file after successful migration\n\t\tif err := os.Remove(planPath); err != nil {\n\t\t\tterm.OutputErrorAndExit(\"could not delete old current_plan.json: %v\", err)\n\t\t}\n\t\tlog.Println(\"Migrated current_plan.json to current-plans-v2.json\")\n\t} else if !os.IsNotExist(err) {\n\t\tterm.OutputErrorAndExit(\"error checking for current_plan.json: %v\", err)\n\t}\n}\n\nfunc MigrateLegacyPlanSettingsFile(currentUserId string) {\n\tif fs.PlandexDir == \"\" {\n\t\treturn\n\t}\n\n\tif currentUserId == \"\" {\n\t\treturn\n\t}\n\n\tif CurrentPlanId == \"\" {\n\t\treturn\n\t}\n\n\t// Migrate settings.json to settings-v2.json for current plan\n\tsettingsPath := filepath.Join(fs.HomePlandexDir, CurrentProjectId, CurrentPlanId, \"settings.json\")\n\tif _, err := os.Stat(settingsPath); err == nil {\n\t\tvar settings types.PlanSettings\n\t\tbytes, err := os.ReadFile(settingsPath)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error reading settings.json: %v\", err)\n\t\t}\n\n\t\terr = json.Unmarshal(bytes, &settings)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error unmarshalling settings.json: %v\", err)\n\t\t}\n\n\t\tv2Path := filepath.Join(fs.HomePlandexDir, CurrentProjectId, CurrentPlanId, \"settings-v2.json\")\n\t\tsettingsByAccount := types.PlanSettingsByAccount{\n\t\t\tcurrentUserId: &settings,\n\t\t}\n\t\tbytes, err = json.Marshal(settingsByAccount)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error marshalling settings-v2.json: %v\", err)\n\t\t}\n\n\t\terr = os.WriteFile(v2Path, bytes, 0644)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"error writing settings-v2.json: %v\", err)\n\t\t}\n\n\t\t// Delete the v1 file after successful migration\n\t\tif err := os.Remove(settingsPath); err != nil {\n\t\t\tterm.OutputErrorAndExit(\"could not delete old settings.json: %v\", err)\n\t\t}\n\t\tlog.Println(\"Migrated settings.json to settings-v2.json\")\n\t} else if !os.IsNotExist(err) {\n\t\tterm.OutputErrorAndExit(\"error checking for settings.json: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "app/cli/lib/log_format.go",
    "content": "package lib\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Regular expressions for parsing log entries\nvar (\n\t// Match the update ID and timestamp\n\tupdateRegex = regexp.MustCompile(`📝 Update ([a-f0-9]+)\\[0;22m \\| \\[36m(.*?)\\[0m`)\n\t\n\t// Match message number and type\n\tmessageRegex = regexp.MustCompile(`Message #(\\d+) \\| (.+?) \\|`)\n\t\n\t// Match coin count\n\tcoinRegex = regexp.MustCompile(`(\\d+) 🪙`)\n\t\n\t// Match context load summary\n\tcontextRegex = regexp.MustCompile(`Loaded (\\d+) .+ into context`)\n)\n\n// LogEntry represents a parsed log entry\ntype LogEntry struct {\n\tID        string\n\tTimestamp string\n\tType      string\n\tMessage   string\n}\n\n// ParseLogEntry parses a raw log entry string into a structured LogEntry\nfunc ParseLogEntry(raw string) LogEntry {\n\tlines := strings.Split(raw, \"\\n\")\n\tentry := LogEntry{}\n\t\n\t// Parse first line for update ID and timestamp\n\tif matches := updateRegex.FindStringSubmatch(lines[0]); len(matches) >= 3 {\n\t\tentry.ID = matches[1]\n\t\tentry.Timestamp = parseTimestamp(matches[2])\n\t}\n\t\n\t// Parse second line for message type and details\n\tif len(lines) > 1 {\n\t\tif matches := messageRegex.FindStringSubmatch(lines[1]); len(matches) >= 3 {\n\t\t\tentry.Type = cleanType(matches[2])\n\t\t\tentry.Message = lines[1]\n\t\t} else if matches := contextRegex.FindStringSubmatch(lines[1]); len(matches) >= 2 {\n\t\t\tentry.Type = \"Context Load\"\n\t\t\tentry.Message = lines[1]\n\t\t}\n\t}\n\t\n\treturn entry\n}\n\n// FormatCompactSummary creates a compact one-line summary of the log entry\nfunc FormatCompactSummary(entry LogEntry) string {\n\tvar summary strings.Builder\n\t\n\t// Add timestamp\n\tsummary.WriteString(entry.Timestamp)\n\tsummary.WriteString(\" | \")\n\t\n\t// Add type indicator and summary based on type\n\tswitch {\n\tcase strings.Contains(entry.Type, \"User prompt\"):\n\t\tsummary.WriteString(\"💬 User: \")\n\t\tmsg := extractFirstLine(entry.Message)\n\t\tif len(msg) > 40 {\n\t\t\tmsg = msg[:37] + \"...\"\n\t\t}\n\t\tsummary.WriteString(msg)\n\t\t\n\tcase strings.Contains(entry.Type, \"Plandex reply\"):\n\t\tsummary.WriteString(\"🤖 AI: \")\n\t\tif coins := coinRegex.FindStringSubmatch(entry.Message); len(coins) >= 2 {\n\t\t\tsummary.WriteString(coins[1] + \"🪙\")\n\t\t}\n\t\t\n\tcase strings.Contains(entry.Type, \"Context Load\"):\n\t\tsummary.WriteString(\"📚 \")\n\t\tif matches := contextRegex.FindStringSubmatch(entry.Message); len(matches) >= 2 {\n\t\t\tsummary.WriteString(\"Loaded \" + matches[1] + \" items\")\n\t\t}\n\t\t\n\tcase strings.Contains(entry.Type, \"Build\"):\n\t\tsummary.WriteString(\"🏗️ Build changes\")\n\t\t\n\tdefault:\n\t\tsummary.WriteString(entry.Type)\n\t}\n\t\n\treturn summary.String()\n}\n\n// Helper functions\n\nfunc parseTimestamp(ts string) string {\n\t// Convert timestamp to a consistent format\n\tts = strings.TrimSpace(ts)\n\tif ts == \"Today\" {\n\t\treturn time.Now().Format(\"15:04:05\")\n\t}\n\tif ts == \"Yesterday\" {\n\t\treturn \"Yesterday\"\n\t}\n\treturn ts\n}\n\nfunc cleanType(t string) string {\n\t// Remove ANSI color codes and clean up type string\n\tt = strings.TrimSpace(t)\n\treturn strings.ReplaceAll(t, \"[0m\", \"\")\n}\n\nfunc extractFirstLine(s string) string {\n\tif idx := strings.Index(s, \"\\n\"); idx != -1 {\n\t\treturn s[:idx]\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "app/cli/lib/model_credentials.go",
    "content": "package lib\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\tshared \"plandex-shared\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/fatih/color\"\n)\n\ntype ProviderAuthStatus int\n\nconst (\n\tFullySatisfied ProviderAuthStatus = iota\n\tPartiallySatisfied\n\tFullyMissing\n)\n\ntype ProviderCredentialStatus struct {\n\tProviderComposite string\n\tStatus            ProviderAuthStatus\n\tMissingVars       []string\n}\n\ntype PublisherCredentialStatus struct {\n\tPublisher        shared.ModelPublisher\n\tSelectedProvider *ProviderCredentialStatus\n\tPartialProviders []ProviderCredentialStatus\n}\n\ntype CredentialCheckResult struct {\n\tAllSatisfied bool\n\tPublishers   []PublisherCredentialStatus\n\tAuthVars     map[string]string\n}\n\nfunc CheckCredentialStatus(opts shared.ModelProviderOptions, claudeMaxEnabled bool) (CredentialCheckResult, error) {\n\tpublishersToProviders := groupProvidersByPublisher(opts)\n\n\tselectedAuthVars := map[string]string{}\n\tvar publisherStatuses []PublisherCredentialStatus\n\tallSatisfied := true\n\n\tfor publisher, providers := range publishersToProviders {\n\t\tvar selectedProvider *ProviderCredentialStatus\n\t\tpartialProviders := []ProviderCredentialStatus{}\n\n\t\tfor _, provider := range providers {\n\t\t\tif provider.Config.HasClaudeMaxAuth && !claudeMaxEnabled {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tauthVars, err := ResolveProviderAuthVars(provider.Config)\n\t\t\tif err != nil {\n\t\t\t\treturn CredentialCheckResult{}, fmt.Errorf(\"error checking API keys/credentials: %v\", err)\n\t\t\t}\n\t\t\tstatus, missingVars, err := checkProviderCredentialStatus(provider.Config, authVars)\n\t\t\tif err != nil {\n\t\t\t\treturn CredentialCheckResult{}, fmt.Errorf(\"error checking API keys/credentials: %v\", err)\n\t\t\t}\n\n\t\t\tproviderStatus := ProviderCredentialStatus{\n\t\t\t\tProviderComposite: provider.Config.ToComposite(),\n\t\t\t\tStatus:            status,\n\t\t\t\tMissingVars:       missingVars,\n\t\t\t}\n\n\t\t\tif status == FullySatisfied {\n\t\t\t\tselectedProvider = &providerStatus\n\t\t\t\tmergeAuthVars(selectedAuthVars, authVars)\n\t\t\t\tbreak // first fully satisfied provider found, stop looking further\n\t\t\t} else if status == PartiallySatisfied {\n\t\t\t\tpartialProviders = append(partialProviders, providerStatus)\n\t\t\t}\n\t\t\t// otherwise,if fully missing, don't set selected provider\n\t\t}\n\n\t\tif selectedProvider == nil {\n\t\t\tallSatisfied = false\n\t\t}\n\n\t\tpublisherStatuses = append(publisherStatuses, PublisherCredentialStatus{\n\t\t\tPublisher:        publisher,\n\t\t\tSelectedProvider: selectedProvider,\n\t\t\tPartialProviders: partialProviders,\n\t\t})\n\t}\n\n\treturn CredentialCheckResult{\n\t\tAllSatisfied: allSatisfied,\n\t\tPublishers:   publisherStatuses,\n\t\tAuthVars:     selectedAuthVars,\n\t}, nil\n}\n\nfunc groupProvidersByPublisher(opts shared.ModelProviderOptions) map[shared.ModelPublisher][]shared.ModelProviderOption {\n\tgrouped := map[shared.ModelPublisher][]shared.ModelProviderOption{}\n\tfor _, option := range opts {\n\t\tfor pub := range option.Publishers {\n\t\t\tgrouped[pub] = append(grouped[pub], option)\n\t\t}\n\t}\n\t// stable priority sort\n\tfor pub := range grouped {\n\t\tsort.SliceStable(grouped[pub], func(i, j int) bool {\n\t\t\treturn grouped[pub][i].Priority < grouped[pub][j].Priority\n\t\t})\n\t}\n\treturn grouped\n}\n\nfunc checkProviderCredentialStatus(cfg *shared.ModelProviderConfigSchema, authVars map[string]string) (ProviderAuthStatus, []string, error) {\n\tvar missing []string\n\n\tif cfg.SkipAuth {\n\t\treturn FullySatisfied, nil, nil\n\t}\n\n\tif cfg.HasClaudeMaxAuth {\n\t\tcreds, err := GetAccountCredentials()\n\t\tif err != nil {\n\t\t\treturn FullyMissing, nil, fmt.Errorf(\"error getting account credentials: %v\", err)\n\t\t}\n\t\tif creds == nil || creds.ClaudeMax == nil {\n\t\t\treturn FullyMissing, nil, nil\n\t\t}\n\t}\n\n\tif cfg.ApiKeyEnvVar != \"\" && authVars[cfg.ApiKeyEnvVar] == \"\" {\n\t\tmissing = append(missing, cfg.ApiKeyEnvVar)\n\t}\n\n\tfor _, extra := range cfg.ExtraAuthVars {\n\t\tif extra.Required && authVars[extra.Var] == \"\" {\n\t\t\tmissing = append(missing, extra.Var)\n\t\t}\n\t}\n\n\tnumRequired := 0\n\tfor _, extra := range cfg.ExtraAuthVars {\n\t\tif extra.Required {\n\t\t\tnumRequired++\n\t\t}\n\t}\n\tif cfg.ApiKeyEnvVar != \"\" {\n\t\tnumRequired++\n\t}\n\n\tswitch {\n\tcase len(missing) == 0:\n\t\treturn FullySatisfied, nil, nil\n\tcase len(missing) == numRequired:\n\t\treturn FullyMissing, missing, nil\n\tdefault:\n\t\treturn PartiallySatisfied, missing, nil\n\t}\n}\n\nfunc MustVerifyAuthVars(integratedModels bool) map[string]string {\n\treturn mustVerifyAuthVars(integratedModels, false)\n}\n\nfunc MustVerifyAuthVarsSilent(integratedModels bool) map[string]string {\n\treturn mustVerifyAuthVars(integratedModels, true)\n}\n\nfunc mustVerifyAuthVars(integratedModels, silent bool) map[string]string {\n\tif !silent {\n\t\tterm.StartSpinner(\"\")\n\t}\n\n\tplanSettings, apiErr := api.Client.GetSettings(CurrentPlanId, CurrentBranch)\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting settings: %v\", apiErr)\n\t}\n\n\torgUserConfig := MustGetOrgUserConfig()\n\n\topts := planSettings.GetModelProviderOptions()\n\n\tif !silent {\n\t\tif hasAnthropicModels(opts) {\n\t\t\tdidConnect := promptClaudeMaxIfNeeded()\n\t\t\tterm.StartSpinner(\"\")\n\t\t\tif !didConnect && orgUserConfig.UseClaudeSubscription {\n\t\t\t\tdidConnect = connectClaudeMaxIfNeeded()\n\t\t\t\tif !didConnect {\n\t\t\t\t\trefreshClaudeMaxCredsIfNeeded()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// For IntegratedModelsMode on Cloud, we only send the connected Claude subscription api key—nothing else\n\t// If we're in IntegratedModelsMode and there's no connected Claude sub, return nil\n\tif integratedModels {\n\t\tif orgUserConfig.UseClaudeSubscription {\n\t\t\tcreds, err := GetAccountCredentials()\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Error getting Claude subscription credentials: %v\", err)\n\t\t\t}\n\n\t\t\tif creds != nil && creds.ClaudeMax != nil {\n\t\t\t\treturn map[string]string{\n\t\t\t\t\tshared.AnthropicClaudeMaxTokenEnvVar: creds.ClaudeMax.AccessToken,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tcheckResult, err := CheckCredentialStatus(opts, orgUserConfig.UseClaudeSubscription)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error checking API keys/credentials: %v\", err)\n\t}\n\tif checkResult.AllSatisfied {\n\t\treturn checkResult.AuthVars\n\t}\n\n\tshowCredentialErrorMessage(checkResult, opts)\n\tos.Exit(1)\n\treturn nil\n}\n\nfunc ResolveProviderAuthVars(cfg *shared.ModelProviderConfigSchema) (map[string]string, error) {\n\tauthVars := map[string]string{}\n\n\tif cfg.SkipAuth {\n\t\treturn authVars, nil\n\t}\n\n\tif cfg.HasAWSAuth {\n\t\t// PLANDEX_AWS_PROFILE enables credential file loading from ~/.aws/credentials\n\t\t// this ensures it's opt-in so we only use bedrock if user explicitly intends to\n\t\tprofile := os.Getenv(\"PLANDEX_AWS_PROFILE\")\n\t\tif profile != \"\" {\n\t\t\tos.Setenv(\"AWS_PROFILE\", profile)\n\t\t\tif err := loadAWSVars(authVars); err == nil {\n\t\t\t\treturn authVars, nil\n\t\t\t}\n\t\t}\n\t\t// if no PLANDEX_AWS_PROFILE is set OR loading aws vars fails, just silently fall through to the default env var checks\n\n\t\t// 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\n\t}\n\n\tif cfg.HasClaudeMaxAuth {\n\t\tcreds, err := GetAccountCredentials()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting account credentials: %v\", err)\n\t\t}\n\n\t\tif creds != nil && creds.ClaudeMax != nil {\n\t\t\ttoken := creds.ClaudeMax.AccessToken\n\t\t\tauthVars[shared.AnthropicClaudeMaxTokenEnvVar] = token\n\t\t}\n\t}\n\n\tif cfg.ApiKeyEnvVar != \"\" {\n\t\tval := os.Getenv(cfg.ApiKeyEnvVar)\n\t\tif val != \"\" {\n\t\t\tauthVars[cfg.ApiKeyEnvVar] = val\n\t\t}\n\t}\n\n\tfor _, extra := range cfg.ExtraAuthVars {\n\t\tval := os.Getenv(extra.Var)\n\t\tif val == \"\" && extra.Default != \"\" {\n\t\t\tval = extra.Default\n\t\t}\n\n\t\tif extra.MaybeJSONFilePath {\n\t\t\tif val == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontent, err := maybeLoadFile(val)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to load file for %s: %v\", extra.Var, err)\n\t\t\t}\n\t\t\tauthVars[extra.Var] = content\n\t\t} else if val != \"\" {\n\t\t\tauthVars[extra.Var] = val\n\t\t}\n\t}\n\n\treturn authVars, nil\n}\n\nfunc maybeLoadFile(pathOrJson string) (string, error) {\n\tif strings.HasPrefix(strings.TrimSpace(pathOrJson), \"{\") {\n\t\t// var contains json directly, so we can return it as is\n\t\treturn pathOrJson, nil\n\t}\n\n\t// see if it's base64 encoded json\n\tdecoded, err := base64.StdEncoding.DecodeString(pathOrJson)\n\tif err == nil {\n\t\ts := string(decoded)\n\t\tif strings.HasPrefix(strings.TrimSpace(s), \"{\") {\n\t\t\treturn s, nil\n\t\t}\n\t}\n\n\tcontent, err := os.ReadFile(pathOrJson)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(content), nil\n}\n\nfunc loadAWSVars(vars map[string]string) error {\n\t// disable IMDS to prevent slow request\n\tos.Setenv(\"AWS_EC2_METADATA_DISABLED\", \"true\")\n\n\tcfg, err := config.LoadDefaultConfig(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load AWS config: %v\", err)\n\t}\n\n\tcreds, err := cfg.Credentials.Retrieve(context.Background())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to retrieve AWS credentials: %v\", err)\n\t}\n\n\tvars[\"AWS_ACCESS_KEY_ID\"] = creds.AccessKeyID\n\tvars[\"AWS_SECRET_ACCESS_KEY\"] = creds.SecretAccessKey\n\tvars[\"AWS_REGION\"] = cfg.Region\n\tif creds.SessionToken != \"\" {\n\t\tvars[\"AWS_SESSION_TOKEN\"] = creds.SessionToken\n\t}\n\n\treturn nil\n}\n\nfunc mergeAuthVars(dest, src map[string]string) {\n\tfor k, v := range src {\n\t\tdest[k] = v\n\t}\n}\n\nfunc showCredentialErrorMessage(res CredentialCheckResult, opts shared.ModelProviderOptions) {\n\tterm.StopSpinner()\n\tboldRed := color.New(color.Bold, term.ColorHiRed)\n\tcyanChip := color.New(color.BgCyan, color.FgHiWhite)\n\tfmt.Println(boldRed.Sprint(\"🚨 Required API key(s) or model credentials are missing\"))\n\n\tsomeOK, someMissing := false, false\n\tfor _, p := range res.Publishers {\n\t\tif p.SelectedProvider != nil && p.SelectedProvider.Status == FullySatisfied {\n\t\t\tsomeOK = true\n\t\t} else {\n\t\t\tsomeMissing = true\n\t\t}\n\t}\n\tif someOK && someMissing {\n\t\tfmt.Println()\n\t\tfmt.Println(color.New(color.Bold, term.ColorHiYellow).Sprint(\"⚠️  Some models are missing a provider\"))\n\t\tsorted := make([]PublisherCredentialStatus, 0, len(res.Publishers))\n\t\tfor _, p := range res.Publishers {\n\t\t\tsorted = append(sorted, p)\n\t\t}\n\t\tsort.Slice(sorted, func(i, j int) bool {\n\t\t\treadyI := sorted[i].SelectedProvider != nil && sorted[i].SelectedProvider.Status == FullySatisfied\n\t\t\treadyJ := sorted[j].SelectedProvider != nil && sorted[j].SelectedProvider.Status == FullySatisfied\n\t\t\treturn readyI && !readyJ\n\t\t})\n\t\tfor _, p := range sorted {\n\t\t\tready := p.SelectedProvider != nil && p.SelectedProvider.Status == FullySatisfied\n\t\t\tvar lbl string\n\t\t\tif ready {\n\t\t\t\tlbl = p.SelectedProvider.ProviderComposite\n\t\t\t} else {\n\t\t\t\tlbl = \"missing\"\n\t\t\t}\n\t\t\tfmt.Printf(\"%s %s models → %s\\n\", mark(ready), p.Publisher, lbl)\n\t\t}\n\t}\n\n\tvar partialLines []string\n\tadded := map[string]bool{}\n\tfor _, pub := range res.Publishers {\n\t\tpartialProviders := pub.PartialProviders\n\t\tfor _, sp := range partialProviders {\n\t\t\t// already added this provider\n\t\t\tif added[sp.ProviderComposite] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// compute vars set vs missing\n\t\t\topt, ok := opts[sp.ProviderComposite]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treq := requiredVars(opt.Config)\n\t\t\tvar setVars []string\n\t\t\tfor _, v := range req {\n\t\t\t\tmissing := false\n\t\t\t\tfor _, mv := range sp.MissingVars {\n\t\t\t\t\tif mv == v {\n\t\t\t\t\t\tmissing = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !missing {\n\t\t\t\t\tsetVars = append(setVars, v)\n\t\t\t\t}\n\t\t\t}\n\t\t\tadded[sp.ProviderComposite] = true\n\t\t\tpartialLines = append(partialLines,\n\t\t\t\tfmt.Sprintf(\"%s\\n  set → %s\\n  missing → %s\",\n\t\t\t\t\tcolor.New(color.Bold).Sprint(sp.ProviderComposite),\n\t\t\t\t\tstrings.Join(setVars, \", \"),\n\t\t\t\t\tstrings.Join(sp.MissingVars, \", \"),\n\t\t\t\t))\n\t\t}\n\t}\n\tif len(partialLines) > 0 {\n\t\tsort.Strings(partialLines)\n\t\tfmt.Println()\n\t\tfmt.Println(color.New(term.ColorHiYellow, color.Bold).Sprint(\"⚠️  Providers with partial credentials\"))\n\t\tfor _, l := range partialLines {\n\t\t\tfmt.Println(l)\n\t\t}\n\t}\n\n\tbyPub := providersByPublisher(opts)\n\n\tbyPubWithoutOpenRouter := map[shared.ModelPublisher][]shared.ModelProvider{}\n\tfor pub, providers := range byPub {\n\t\tnonOrProviders := []shared.ModelProvider{}\n\t\tfor _, provider := range providers {\n\t\t\tif provider != shared.ModelProviderOpenRouter {\n\t\t\t\tnonOrProviders = append(nonOrProviders, provider)\n\t\t\t}\n\t\t}\n\t\tif len(nonOrProviders) > 0 {\n\t\t\tbyPubWithoutOpenRouter[pub] = nonOrProviders\n\t\t}\n\t}\n\n\tallPublishersHaveOpenRouter := allPublishersHaveProvider(byPub, shared.ModelProviderOpenRouter)\n\n\tif allPublishersHaveOpenRouter {\n\t\tfmt.Println()\n\t\tfmt.Println(color.New(term.ColorHiCyan, color.Bold).Sprint(\"🚀 Quick option → OpenRouter.ai\"))\n\t\tfmt.Println(\"OpenRouter allows you to use all models in the current model pack with a single account and API key. To get started:\")\n\t\tfmt.Println()\n\t\tstep := func(n int, txt string) { fmt.Printf(\"%d. %s\\n\", n, txt) }\n\t\tstep(1, \"Sign up at \"+color.New(color.Bold).Sprint(\"https://openrouter.ai/sign-up\"))\n\t\tstep(2, \"Buy some credits at \"+color.New(color.Bold).Sprint(\"https://openrouter.ai/settings/credits\"))\n\t\tstep(3, \"Generate an API key at \"+color.New(color.Bold).Sprint(\"https://openrouter.ai/settings/keys\"))\n\t\tif term.IsRepl {\n\t\t\tstep(4, \"Quit the REPL with \"+cyanChip.Sprint(\" \\\\quit \"))\n\t\t\tstep(5, \"Run \"+cyanChip.Sprint(\" export OPENROUTER_API_KEY=… \"))\n\t\t\tstep(6, \"Restart the REPL with \"+cyanChip.Sprint(\" plandex \"))\n\t\t} else {\n\t\t\tstep(4, \"Run \"+cyanChip.Sprint(\" export OPENROUTER_API_KEY=… \"))\n\t\t}\n\t}\n\n\tif len(byPubWithoutOpenRouter) > 0 {\n\t\tfmt.Println()\n\t\tfmt.Println(color.New(term.ColorHiCyan, color.Bold).Sprint(\"🔑 Other model providers\"))\n\t\tif allPublishersHaveOpenRouter {\n\t\t\tfmt.Println(\"You can also use the following providers for the current model pack:\")\n\t\t} else {\n\t\t\tfmt.Println(\"You can use the following providers for the current model pack:\")\n\t\t}\n\n\t\tfmt.Println()\n\t\tpubs := make([]string, 0, len(byPubWithoutOpenRouter))\n\t\tfor p := range byPubWithoutOpenRouter {\n\t\t\tpubs = append(pubs, string(p))\n\t\t}\n\t\tsort.Strings(pubs)\n\t\tfor _, p := range pubs {\n\t\t\tproviders := byPubWithoutOpenRouter[shared.ModelPublisher(p)]\n\t\t\tproviderNames := make([]string, 0, len(providers))\n\t\t\tfor _, provider := range providers {\n\t\t\t\tproviderNames = append(providerNames, string(provider))\n\t\t\t}\n\t\t\tfmt.Printf(\"%s → %s\\n\", color.New(color.Bold).Sprint(p+\" models\"), strings.Join(providerNames, \", \"))\n\t\t}\n\n\t\tfmt.Println(color.New(color.Bold, term.ColorHiCyan).Sprint(\"\\n📖 Per-provider instructions\"))\n\t\tfmt.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\"))\n\t}\n\n\tfmt.Println()\n}\n\n// return required env‑var names (API key + required extras)\nfunc requiredVars(cfg *shared.ModelProviderConfigSchema) []string {\n\tvar vars []string\n\tif cfg.ApiKeyEnvVar != \"\" {\n\t\tvars = append(vars, cfg.ApiKeyEnvVar)\n\t}\n\tfor _, ex := range cfg.ExtraAuthVars {\n\t\tif ex.Required {\n\t\t\tvars = append(vars, ex.Var)\n\t\t}\n\t}\n\treturn vars\n}\n\nfunc providersByPublisher(opts shared.ModelProviderOptions) map[shared.ModelPublisher][]shared.ModelProvider {\n\tbyPub := map[shared.ModelPublisher][]shared.ModelProvider{}\n\tsortedOpts := make([]shared.ModelProviderOption, 0, len(opts))\n\tfor _, opt := range opts {\n\t\tsortedOpts = append(sortedOpts, opt)\n\t}\n\tsort.Slice(sortedOpts, func(i, j int) bool {\n\t\treturn sortedOpts[i].Priority < sortedOpts[j].Priority\n\t})\n\tfor _, opt := range sortedOpts {\n\t\tfor pub := range opt.Publishers {\n\t\t\tbyPub[pub] = append(byPub[pub], opt.Config.Provider)\n\t\t}\n\t}\n\treturn byPub\n}\n\nfunc allPublishersHaveProvider(byPub map[shared.ModelPublisher][]shared.ModelProvider, p shared.ModelProvider) bool {\n\tif len(byPub) == 0 {\n\t\treturn false\n\t}\n\n\tfor _, providers := range byPub {\n\t\tfound := false\n\t\tfor _, provider := range providers {\n\t\t\tif provider == p {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// emoji for satisfied vs missing\nfunc mark(ok bool) string {\n\tif ok {\n\t\treturn \"✅\"\n\t}\n\treturn \"❌\"\n}\n\nvar cachedAccountCredentials *types.AccountCredentials\n\nfunc SetAccountCredentials(creds *types.AccountCredentials) error {\n\tif auth.Current == nil {\n\t\treturn fmt.Errorf(\"no authenticated user\")\n\t}\n\tdir := filepath.Join(fs.HomePlandexDir, auth.Current.UserId, auth.Current.OrgId)\n\terr := os.MkdirAll(dir, 0700)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating account credentials directory: %v\", err)\n\t}\n\tpath := filepath.Join(dir, \"creds.json\")\n\tbytes, err := json.MarshalIndent(creds, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling account credentials: %v\", err)\n\t}\n\terr = os.WriteFile(path, bytes, 0600)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing account credentials: %v\", err)\n\t}\n\n\tcachedAccountCredentials = creds\n\n\treturn nil\n}\n\nfunc GetAccountCredentials() (*types.AccountCredentials, error) {\n\tif cachedAccountCredentials != nil {\n\t\treturn cachedAccountCredentials, nil\n\t}\n\n\tif auth.Current == nil {\n\t\treturn nil, fmt.Errorf(\"no authenticated user\")\n\t}\n\tpath := filepath.Join(fs.HomePlandexDir, auth.Current.UserId, auth.Current.OrgId, \"creds.json\")\n\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tvar creds types.AccountCredentials\n\terr = json.Unmarshal(bytes, &creds)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcachedAccountCredentials = &creds\n\n\treturn &creds, nil\n}\n"
  },
  {
    "path": "app/cli/lib/model_settings.go",
    "content": "package lib\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/schema\"\n\t\"plandex-cli/term\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar DefaultModelSettingsPath string\n\nfunc init() {\n\tDefaultModelSettingsPath = filepath.Join(fs.HomePlandexDir, \"default-model-settings.json\")\n}\n\nfunc GetPlanModelSettingsPath(planId string) string {\n\treturn filepath.Join(fs.HomePlandexDir, planId, \"model-settings.json\")\n}\n\ntype ModelSettingsCheckLocalChangesResult struct {\n\tHasLocalChanges           bool\n\tLocalModelPackSchemaRoles *shared.ModelPackSchemaRoles\n}\n\nfunc ModelSettingsCheckLocalChanges(path string) (ModelSettingsCheckLocalChangesResult, error) {\n\thashPath := path + \".hash\"\n\n\texists, err := fs.FileExists(path)\n\tif err != nil {\n\t\treturn ModelSettingsCheckLocalChangesResult{}, fmt.Errorf(\"error checking model settings file: %v\", err)\n\t}\n\n\tif !exists {\n\t\treturn ModelSettingsCheckLocalChangesResult{}, nil\n\t}\n\n\tlastSavedHash, err := os.ReadFile(hashPath)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn ModelSettingsCheckLocalChangesResult{}, fmt.Errorf(\"error reading hash file: %v\", err)\n\t}\n\n\tlocalJsonData, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn ModelSettingsCheckLocalChangesResult{}, fmt.Errorf(\"error reading JSON file: %v\", err)\n\t}\n\n\tvar clientModelPackSchemaRoles *shared.ClientModelPackSchemaRoles\n\terr = json.Unmarshal(localJsonData, &clientModelPackSchemaRoles)\n\tif err != nil {\n\t\treturn ModelSettingsCheckLocalChangesResult{}, fmt.Errorf(\"error unmarshalling JSON file: %v\", err)\n\t}\n\n\tcurrentHash, err := clientModelPackSchemaRoles.ToModelPackSchemaRoles().Hash()\n\tif err != nil {\n\t\treturn ModelSettingsCheckLocalChangesResult{}, fmt.Errorf(\"error hashing model pack: %v\", err)\n\t}\n\n\tmodelPackSchemaRoles := clientModelPackSchemaRoles.ToModelPackSchemaRoles()\n\n\treturn ModelSettingsCheckLocalChangesResult{\n\t\tHasLocalChanges:           currentHash != string(lastSavedHash),\n\t\tLocalModelPackSchemaRoles: &modelPackSchemaRoles,\n\t}, nil\n}\n\nfunc WriteModelSettingsFile(path string, originalSettings *shared.PlanSettings) error {\n\terr := os.MkdirAll(filepath.Dir(path), 0755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating directory: %v\", err)\n\t}\n\n\tmodelPackSchemaRoles := originalSettings.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles\n\n\tclientModelPackRoles := modelPackSchemaRoles.ToClientModelPackSchemaRoles()\n\n\tbytes, err := json.MarshalIndent(clientModelPackRoles, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling model pack: %v\", err)\n\t}\n\n\terr = os.WriteFile(path, bytes, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing JSON file: %v\", err)\n\t}\n\n\terr = SaveModelPackRolesHash(path, &modelPackSchemaRoles)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error saving model pack roles hash: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc SaveModelPackRolesHash(basePath string, serverModelPack *shared.ModelPackSchemaRoles) error {\n\thashPath := basePath + \".hash\"\n\n\thash, err := serverModelPack.Hash()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error hashing model pack: %v\", err)\n\t}\n\n\terr = os.WriteFile(hashPath, []byte(hash), 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing hash file: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc ApplyModelSettings(path string, originalSettings *shared.PlanSettings) (*shared.PlanSettings, error) {\n\tjsonData, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading JSON file: %v\", err)\n\t}\n\n\tsettings, err := originalSettings.DeepCopy()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error copying settings: %v\", err)\n\t}\n\n\tclientModelPackRoles, err := schema.ValidateModelPackInlineJSON(jsonData)\n\tif err != nil {\n\t\tterm.StopSpinner()\n\t\tcolor.New(color.Bold, term.ColorHiRed).Println(\"🚨 Error validating JSON file\")\n\t\tfmt.Println(err.Error())\n\t\tos.Exit(1)\n\t}\n\tmodelPackRoles := clientModelPackRoles.ToModelPackSchemaRoles()\n\n\tmodelPackSchema := shared.ModelPackSchema{\n\t\tName:                 \"custom\",\n\t\tDescription:          \"Model pack with custom settings\",\n\t\tModelPackSchemaRoles: modelPackRoles,\n\t}\n\tmodelPack := modelPackSchema.ToModelPack()\n\tsettings.SetCustomModelPack(&modelPack)\n\n\terr = SaveModelPackRolesHash(path, &modelPackRoles)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error saving model settings hash: %v\", err)\n\t}\n\n\treturn settings, nil\n}\n\nfunc SaveLatestPlanModelSettingsIfNeeded() (bool, error) {\n\tpath := GetPlanModelSettingsPath(CurrentPlanId)\n\tjsonData, err := os.ReadFile(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, fmt.Errorf(\"error reading JSON file: %v\", err)\n\t}\n\n\tvar clientModelPackSchemaRoles *shared.ClientModelPackSchemaRoles\n\terr = json.Unmarshal(jsonData, &clientModelPackSchemaRoles)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error unmarshalling JSON file: %v\", err)\n\t}\n\n\tmodelPackSchemaRoles := clientModelPackSchemaRoles.ToModelPackSchemaRoles()\n\n\tlocalHash, err := modelPackSchemaRoles.Hash()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error hashing model pack: %v\", err)\n\t}\n\n\tsettings, apiErr := api.Client.GetSettings(CurrentPlanId, CurrentBranch)\n\tif apiErr != nil {\n\t\treturn false, fmt.Errorf(\"error getting settings: %v\", apiErr)\n\t}\n\n\tserverHash, err := settings.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles.Hash()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error hashing model pack: %v\", err)\n\t}\n\n\tif localHash == serverHash {\n\t\treturn false, nil\n\t}\n\n\terr = WriteModelSettingsFile(path, settings)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error writing model settings file: %v\", err)\n\t}\n\n\treturn true, nil\n}\n\n// save settings in file to server\nfunc SyncPlanModelSettings() error {\n\tsettings, err := api.Client.GetSettings(CurrentPlanId, CurrentBranch)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting settings: %v\", err)\n\t}\n\n\tupdatedSettings, apiErr := ApplyModelSettings(GetPlanModelSettingsPath(CurrentPlanId), settings)\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error applying model settings: %v\", err)\n\t}\n\n\tres, updateErr := api.Client.UpdateSettings(CurrentPlanId, CurrentBranch, shared.UpdateSettingsRequest{\n\t\tModelPackName: updatedSettings.ModelPackName,\n\t\tModelPack:     updatedSettings.ModelPack,\n\t})\n\n\tif updateErr != nil {\n\t\treturn fmt.Errorf(\"error updating settings: %v\", err)\n\t}\n\n\tif res == nil {\n\t\treturn nil\n\t}\n\n\tfmt.Println(res.Msg)\n\n\treturn nil\n}\n\nfunc SyncDefaultModelSettings() error {\n\tsettings, err := api.Client.GetOrgDefaultSettings()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting settings: %v\", err)\n\t}\n\n\tupdatedSettings, apiErr := ApplyModelSettings(DefaultModelSettingsPath, settings)\n\tif apiErr != nil {\n\t\treturn fmt.Errorf(\"error applying model settings: %v\", err)\n\t}\n\n\tres, updateErr := api.Client.UpdateOrgDefaultSettings(shared.UpdateSettingsRequest{\n\t\tModelPackName: updatedSettings.ModelPackName,\n\t\tModelPack:     updatedSettings.ModelPack,\n\t})\n\n\tif updateErr != nil {\n\t\treturn fmt.Errorf(\"error updating settings: %v\", err)\n\t}\n\n\tif res == nil {\n\t\treturn nil\n\t}\n\n\tfmt.Println(res.Msg)\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/lib/models_sync.go",
    "content": "package lib\n\nimport (\n\t\"fmt\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/term\"\n\n\t\"github.com/fatih/color\"\n)\n\nfunc PromptSyncModelsIfNeeded() error {\n\tvar changes []string\n\tvar onApprove []func() error\n\n\tuserId := auth.Current.UserId\n\tif userId == \"\" {\n\t\treturn fmt.Errorf(\"auth.Current.UserId is empty\")\n\t}\n\n\tcustomModelsPath := GetCustomModelsPath(userId)\n\n\tcustomModelsRes, err := CustomModelsCheckLocalChanges(customModelsPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking custom models: %v\", err)\n\t}\n\n\tif customModelsRes.HasLocalChanges {\n\t\tchanges = append(\n\t\t\tchanges,\n\t\t\tfmt.Sprintf(\"%s → %s\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"Custom models\"), customModelsPath))\n\n\t\tonApprove = append(onApprove, SyncCustomModels)\n\t}\n\n\tdefaultModelSettingsRes, err := ModelSettingsCheckLocalChanges(DefaultModelSettingsPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking default model settings: %v\", err)\n\t}\n\n\tif defaultModelSettingsRes.HasLocalChanges {\n\t\tchanges = append(\n\t\t\tchanges,\n\t\t\tfmt.Sprintf(\"%s → %s\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"Default model settings\"), DefaultModelSettingsPath))\n\n\t\tonApprove = append(onApprove, SyncDefaultModelSettings)\n\t}\n\n\tplanModelSettingsRes, err := ModelSettingsCheckLocalChanges(GetPlanModelSettingsPath(CurrentPlanId))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking plan model settings: %v\", err)\n\t}\n\n\tif planModelSettingsRes.HasLocalChanges {\n\t\tchanges = append(\n\t\t\tchanges,\n\t\t\tfmt.Sprintf(\"%s → %s\", color.New(term.ColorHiCyan, color.Bold).Sprint(\"Plan model settings\"), GetPlanModelSettingsPath(CurrentPlanId)))\n\n\t\tonApprove = append(onApprove, SyncPlanModelSettings)\n\t}\n\n\tif len(changes) == 0 {\n\t\treturn nil\n\t}\n\n\tterm.StopSpinner()\n\tcolor.New(color.Bold, term.ColorHiYellow).Println(\"⚠️  Model settings have local changes\")\n\n\tfmt.Println()\n\tfor _, change := range changes {\n\t\tfmt.Println(change)\n\t}\n\tfmt.Println()\n\n\tshouldSave, err := term.ConfirmYesNo(\"Save changes now?\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error confirming: %v\", err)\n\t}\n\n\tif !shouldSave {\n\t\treturn nil\n\t}\n\n\tfor _, fn := range onApprove {\n\t\terr := fn()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error syncing models: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/lib/org_user_config.go",
    "content": "package lib\n\nimport (\n\t\"plandex-cli/api\"\n\t\"plandex-cli/term\"\n\n\tshared \"plandex-shared\"\n)\n\nvar cachedOrgUserConfig *shared.OrgUserConfig\n\nfunc MustGetOrgUserConfig() *shared.OrgUserConfig {\n\tif cachedOrgUserConfig != nil {\n\t\treturn cachedOrgUserConfig\n\t}\n\n\torgUserConfig, err := api.Client.GetOrgUserConfig()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting org user config: %v\", err)\n\t}\n\tcachedOrgUserConfig = orgUserConfig\n\treturn orgUserConfig\n}\n\nfunc MustUpdateOrgUserConfig(orgUserConfig shared.OrgUserConfig) {\n\terr := api.Client.UpdateOrgUserConfig(orgUserConfig)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error updating org user config: %v\", err)\n\t}\n\tSetCachedOrgUserConfig(&orgUserConfig)\n}\n\nfunc SetCachedOrgUserConfig(orgUserConfig *shared.OrgUserConfig) {\n\tcachedOrgUserConfig = orgUserConfig\n}\n"
  },
  {
    "path": "app/cli/lib/plan_config.go",
    "content": "package lib\n\nimport (\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/term\"\n\t\"sort\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/olekukonko/tablewriter\"\n)\n\nvar cachedPlanConfig *shared.PlanConfig\n\nfunc MustGetCurrentPlanConfig() *shared.PlanConfig {\n\tif cachedPlanConfig != nil {\n\t\treturn cachedPlanConfig\n\t}\n\n\tplanConfig, apiErr := api.Client.GetPlanConfig(CurrentPlanId)\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting plan config: %v\", apiErr)\n\t}\n\tcachedPlanConfig = planConfig\n\treturn planConfig\n}\n\nfunc SetCachedPlanConfig(planConfig *shared.PlanConfig) {\n\tcachedPlanConfig = planConfig\n}\n\nfunc ShowPlanConfig(config *shared.PlanConfig, key string) {\n\ttable := tablewriter.NewWriter(os.Stdout)\n\ttable.SetAutoWrapText(true)\n\ttable.SetHeader([]string{\"Name\", \"Value\", \"Description\"})\n\n\tnumVisibleSettings := 0\n\tfor k, setting := range shared.ConfigSettingsByKey {\n\t\tif key != \"\" && k != key {\n\t\t\tcontinue\n\t\t}\n\n\t\tif setting.Visible == nil || setting.Visible(config) {\n\t\t\tnumVisibleSettings++\n\t\t}\n\t}\n\tnumOutput := 0\n\n\tsortedSettings := make([][]string, 0, len(shared.ConfigSettingsByKey))\n\n\tfor k, setting := range shared.ConfigSettingsByKey {\n\t\tif key != \"\" && k != key {\n\t\t\tcontinue\n\t\t}\n\n\t\tif setting.Visible == nil || setting.Visible(config) {\n\t\t\tvar sortKey string\n\t\t\tif setting.SortKey != \"\" {\n\t\t\t\tsortKey = setting.SortKey\n\t\t\t} else {\n\t\t\t\tsortKey = k\n\t\t\t}\n\t\t\tsortedSettings = append(sortedSettings, []string{sortKey, setting.Name, setting.Getter(config), setting.Desc})\n\t\t}\n\t}\n\n\tsort.Slice(sortedSettings, func(i, j int) bool {\n\t\treturn sortedSettings[i][0] < sortedSettings[j][0]\n\t})\n\n\tfor _, row := range sortedSettings {\n\t\ttable.Append(row[1:])\n\t\tnumOutput++\n\t\tif numOutput < numVisibleSettings {\n\t\t\ttable.Append([]string{\"\", \"\", \"\"})\n\t\t}\n\t}\n\n\ttable.Render()\n}\n"
  },
  {
    "path": "app/cli/lib/plans.go",
    "content": "package lib\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/types\"\n\t\"sync\"\n)\n\nfunc WriteCurrentPlan(id string) error {\n\tif fs.HomePlandexDir == \"\" {\n\t\treturn fmt.Errorf(\"HomePlandexDir not set\")\n\t}\n\n\tif CurrentProjectId == \"\" || HomeCurrentPlanPath == \"\" {\n\t\treturn fmt.Errorf(\"no current project\")\n\t}\n\n\tvar currentPlanSettingsByAccount *types.CurrentPlanSettingsByAccount\n\n\tbytes, err := os.ReadFile(HomeCurrentPlanPath)\n\tif err == nil {\n\t\terr = json.Unmarshal(bytes, &currentPlanSettingsByAccount)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error unmarshalling current-plans-v2.json: %v\", err)\n\t\t}\n\t} else if !os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"error checking if current-plans-v2.json exists: %v\", err)\n\t}\n\n\tif currentPlanSettingsByAccount == nil {\n\t\tcurrentPlanSettingsByAccount = &types.CurrentPlanSettingsByAccount{}\n\t}\n\n\tsettings := types.CurrentPlanSettings{\n\t\tId: id,\n\t}\n\n\t(*currentPlanSettingsByAccount)[auth.Current.UserId] = &settings\n\n\tbytes, err = json.Marshal(currentPlanSettingsByAccount)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling current plan: %v\", err)\n\t}\n\n\terr = os.WriteFile(HomeCurrentPlanPath, bytes, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing current plan: %v\", err)\n\t}\n\n\tCurrentPlanId = id\n\n\treturn nil\n}\n\nfunc ClearCurrentPlan() error {\n\tif fs.HomePlandexDir == \"\" {\n\t\treturn fmt.Errorf(\"HomePlandexDir not set\")\n\t}\n\n\tif CurrentProjectId == \"\" || HomeCurrentPlanPath == \"\" {\n\t\treturn fmt.Errorf(\"no current project\")\n\t}\n\n\tvar currentPlanSettingsByAccount *types.CurrentPlanSettingsByAccount\n\n\tbytes, err := os.ReadFile(HomeCurrentPlanPath)\n\tif err == nil {\n\t\terr = json.Unmarshal(bytes, &currentPlanSettingsByAccount)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error unmarshalling current-plans-v2.json: %v\", err)\n\t\t}\n\t} else if !os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"error checking if current-plans-v2.json exists: %v\", err)\n\t}\n\n\tif currentPlanSettingsByAccount != nil {\n\t\tdelete(*currentPlanSettingsByAccount, auth.Current.UserId)\n\t}\n\n\tbytes, err = json.Marshal(currentPlanSettingsByAccount)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling current plan: %v\", err)\n\t}\n\n\terr = os.WriteFile(HomeCurrentPlanPath, bytes, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing current plan: %v\", err)\n\t}\n\n\tCurrentPlanId = \"\"\n\n\treturn nil\n}\n\nfunc WriteCurrentBranch(branch string) error {\n\tif fs.HomePlandexDir == \"\" {\n\t\treturn fmt.Errorf(\"HomePlandexDir not set\")\n\t}\n\n\tif CurrentProjectId == \"\" || HomeCurrentPlanPath == \"\" {\n\t\treturn fmt.Errorf(\"no current project\")\n\t}\n\n\tif CurrentPlanId == \"\" {\n\t\treturn fmt.Errorf(\"no current plan\")\n\t}\n\n\tdir := filepath.Join(fs.HomePlandexDir, CurrentProjectId, CurrentPlanId)\n\n\terr := os.MkdirAll(dir, os.ModePerm)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating plan dir: %v\", err)\n\t}\n\n\tpath := filepath.Join(dir, \"settings-v2.json\")\n\n\tvar settingsByAccount *types.PlanSettingsByAccount\n\n\tbytes, err := os.ReadFile(path)\n\tif err == nil {\n\t\terr = json.Unmarshal(bytes, &settingsByAccount)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error unmarshalling settings-v2.json: %v\", err)\n\t\t}\n\t} else if !os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"error checking if settings-v2.json exists: %v\", err)\n\t}\n\n\tif settingsByAccount == nil {\n\t\tsettingsByAccount = &types.PlanSettingsByAccount{}\n\t}\n\n\texistingSettings := (*settingsByAccount)[auth.Current.UserId]\n\n\tif existingSettings == nil {\n\t\texistingSettings = &types.PlanSettings{}\n\t}\n\n\texistingSettings.Branch = branch\n\t(*settingsByAccount)[auth.Current.UserId] = existingSettings\n\n\tbytes, err = json.Marshal(settingsByAccount)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling current plan settings: %v\", err)\n\t}\n\n\terr = os.WriteFile(path, bytes, 0644)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing current plan settings: %v\", err)\n\t}\n\n\tCurrentBranch = branch\n\n\treturn nil\n}\n\nfunc GetCurrentBranchNamesByPlanId(planIds []string) (map[string]string, error) {\n\tif fs.HomePlandexDir == \"\" {\n\t\treturn nil, fmt.Errorf(\"HomePlandexDir not set\")\n\t}\n\n\tif CurrentProjectId == \"\" || HomeCurrentPlanPath == \"\" {\n\t\treturn nil, fmt.Errorf(\"no current project\")\n\t}\n\n\tvar mu sync.Mutex\n\tbranches := make(map[string]string)\n\terrCh := make(chan error, len(planIds))\n\tfor _, planId := range planIds {\n\t\tgo func(planId string) {\n\t\t\tbranch, err := getPlanCurrentBranch(planId)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan current branch: %v\", err)\n\t\t\t} else {\n\t\t\t\tmu.Lock()\n\t\t\t\tdefer mu.Unlock()\n\t\t\t\tbranches[planId] = branch\n\t\t\t\terrCh <- nil\n\t\t\t}\n\t\t}(planId)\n\t}\n\n\tfor i := 0; i < len(planIds); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn branches, nil\n}\n\nfunc getPlanCurrentBranch(planId string) (string, error) {\n\tif fs.HomePlandexDir == \"\" {\n\t\treturn \"\", fmt.Errorf(\"HomePlandexDir not set\")\n\t}\n\n\tif CurrentProjectId == \"\" || HomeCurrentPlanPath == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no current project\")\n\t}\n\n\tv2Path := filepath.Join(fs.HomePlandexDir, CurrentProjectId, planId, \"settings-v2.json\")\n\n\tvar settings *types.PlanSettings\n\n\t// check if settings-v2.json exists\n\t_, err := os.Stat(v2Path)\n\tif err == nil {\n\t\t// read settings-v2.json\n\t\tvar settingsByAccount types.PlanSettingsByAccount\n\t\tbytes, err := os.ReadFile(v2Path)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error reading settings-v2.json: %v\", err)\n\t\t}\n\t\terr = json.Unmarshal(bytes, &settingsByAccount)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error unmarshalling settings-v2.json: %v\", err)\n\t\t}\n\n\t\tsettings = settingsByAccount[auth.Current.UserId]\n\t} else if os.IsNotExist(err) {\n\t\treturn \"main\", nil\n\t} else {\n\t\treturn \"\", fmt.Errorf(\"error checking if settings-v2.json exists: %v\", err)\n\t}\n\n\tif settings == nil {\n\t\treturn \"main\", nil\n\t}\n\n\treturn settings.Branch, nil\n}\n"
  },
  {
    "path": "app/cli/lib/repl.go",
    "content": "package lib\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/term\"\n\t\"strconv\"\n\t\"syscall\"\n)\n\nvar ReplSettingsDir string\n\ntype ReplMode string\n\nconst (\n\tReplModeTell ReplMode = \"tell\"\n\tReplModeChat ReplMode = \"chat\"\n)\n\ntype ReplState struct {\n\tMode    ReplMode\n\tIsMulti bool\n}\n\nvar CurrentReplState = ReplState{\n\tMode:    ReplModeChat,\n\tIsMulti: false,\n}\n\ntype ReplSettings struct {\n\tState   ReplState\n\tHistory []string\n}\n\nvar ReplCmdAliases = map[string]string{\n\t\"chat\":  \"ch\",\n\t\"tell\":  \"t\",\n\t\"multi\": \"m\",\n\t\"quit\":  \"q\",\n\t\"help\":  \"h\",\n\t\"run\":   \"r\",\n\t\"send\":  \"s\",\n}\n\nfunc init() {\n\tReplSettingsDir = filepath.Join(fs.HomePlandexDir, \"repl_settings\")\n}\n\nfunc EnsureReplSettingsFile() {\n\tif err := os.MkdirAll(ReplSettingsDir, os.ModePerm); err != nil {\n\t\tterm.OutputErrorAndExit(\"Error creating repl history directory: %v\", err)\n\t}\n\n\tsettingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+\".json\")\n\tif _, err := os.Stat(settingsFile); os.IsNotExist(err) {\n\t\tfile, err := os.Create(settingsFile)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error creating history file: %v\", err)\n\t\t}\n\t\tdefer file.Close()\n\n\t\t// Write empty settings object\n\t\tvar settings ReplSettings\n\n\t\tdata, err := json.Marshal(settings)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error converting settings to JSON: %v\", err)\n\t\t}\n\n\t\tif _, err := file.Write(data); err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error writing to history file: %v\", err)\n\t\t}\n\t}\n}\n\nfunc writeSettings(settings *ReplSettings) {\n\tsettingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+\".json\")\n\tdata, err := json.Marshal(settings)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error converting settings to JSON: %v\", err)\n\t}\n\n\tif err := os.WriteFile(settingsFile, data, 0644); err != nil {\n\t\tterm.OutputErrorAndExit(\"Error writing settings file: %v\", err)\n\t}\n}\n\nfunc getSettings() *ReplSettings {\n\tEnsureReplSettingsFile()\n\n\tsettingsFile := filepath.Join(ReplSettingsDir, CurrentProjectId+\".json\")\n\n\t// Read existing settings\n\tdata, err := os.ReadFile(settingsFile)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error reading history file: %v\", err)\n\t}\n\n\t// Parse JSON\n\tvar settings ReplSettings\n\tif err := json.Unmarshal(data, &settings); err != nil {\n\t\tterm.OutputErrorAndExit(\"Error parsing history file: %v\", err)\n\t}\n\n\treturn &settings\n}\n\nfunc LoadState() {\n\tsettings := getSettings()\n\n\tif settings.State.Mode != \"\" {\n\t\tCurrentReplState = settings.State\n\t} else {\n\t\t// Write default state\n\t\tWriteState()\n\t}\n}\n\nfunc WriteState() {\n\tsettings := getSettings()\n\tsettings.State = CurrentReplState\n\twriteSettings(settings)\n}\n\nfunc WriteHistory(input string) {\n\tsettings := getSettings()\n\t// Add new input\n\tsettings.History = append(settings.History, input)\n\twriteSettings(settings)\n}\n\nfunc GetHistory() []string {\n\tsettings := getSettings()\n\treturn settings.History\n}\n\n// ExecPlandexCommand spawns the same binary, wiring std streams directly so you\n// don't have to capture output. Any os.Exit calls in the child won't kill your REPL.\nfunc ExecPlandexCommand(args []string) (string, error) {\n\treturn ExecPlandexCommandWithParams(args, ExecPlandexCommandParams{})\n}\n\ntype ExecPlandexCommandParams struct {\n\tDisableSuggestions bool\n\tSessionId          string\n}\n\nfunc ExecPlandexCommandWithParams(args []string, params ExecPlandexCommandParams) (string, error) {\n\t// Create temp file\n\ttmpFile, err := os.CreateTemp(\"\", \"plandex-output-*\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ttmpPath := tmpFile.Name()\n\ttmpFile.Close()\n\tdefer os.Remove(tmpPath)\n\n\tvar env []string = os.Environ()\n\tif os.Getenv(\"PLANDEX_REPL\") == \"\" {\n\t\tcolumns := term.GetTerminalWidth()\n\t\thasDarkBackground := term.HasDarkBackground()\n\t\tstreamForegroundColor := term.GetStreamForegroundColor()\n\n\t\tvar glamourStyle string\n\t\tif hasDarkBackground {\n\t\t\tglamourStyle = \"dark\"\n\t\t} else {\n\t\t\tglamourStyle = \"light\"\n\t\t}\n\n\t\t// Set env vars\n\t\tenv = append(env,\n\t\t\t\"PLANDEX_REPL=1\",\n\t\t\t\"PLANDEX_REPL_OUTPUT_FILE=\"+tmpPath,\n\t\t\t\"PLANDEX_COLUMNS=\"+strconv.Itoa(columns),\n\t\t\t\"PLANDEX_STREAM_FOREGROUND_COLOR=\"+streamForegroundColor.Sequence(false),\n\t\t\t\"GLAMOUR_STYLE=\"+glamourStyle,\n\t\t\t\"PLANDEX_SKIP_UPGRADE=1\",\n\t\t)\n\n\t\tif params.SessionId != \"\" {\n\t\t\tenv = append(env, \"PLANDEX_REPL_SESSION_ID=\"+params.SessionId)\n\t\t}\n\t}\n\n\tif params.DisableSuggestions {\n\t\tenv = append(env, \"PLANDEX_DISABLE_SUGGESTIONS=1\")\n\t}\n\n\t// Run command\n\tcmd := exec.Command(os.Args[0], args...)\n\tcmd.Env = env\n\n\t// Connect stdin directly\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsignal.Ignore(syscall.SIGINT)\n\tdefer signal.Reset(syscall.SIGINT)\n\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n\n\tgo func() {\n\t\tsig := <-sigChan\n\t\tsyscall.Kill(-cmd.Process.Pid, sig.(syscall.Signal))\n\t}()\n\n\terr = cmd.Wait()\n\tif err != nil {\n\t\tif _, ok := err.(*exec.ExitError); ok {\n\t\t\treturn \"\", nil\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\t// Read output from temp file\n\toutput, err := os.ReadFile(tmpPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(output), nil\n}\n"
  },
  {
    "path": "app/cli/lib/rewind.go",
    "content": "package lib\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/fs\"\n\tshared \"plandex-shared\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n)\n\n// GetUndonePlanApplies returns the list of PlanApplies that will be undone by rewinding to targetSHA.\n// An apply is considered \"undone\" if its timestamp is after OR equal to the target SHA's timestamp,\n// since we want to revert to the state before the target SHA.\nfunc GetUndonePlanApplies(currentState *shared.CurrentPlanState, timestamp time.Time) []*shared.PlanApply {\n\tif currentState == nil {\n\t\treturn nil\n\t}\n\n\tvar undoneApplies []*shared.PlanApply\n\tfor _, apply := range currentState.PlanApplies {\n\t\t// Include applies after OR equal to target time\n\t\tif !apply.CreatedAt.Before(timestamp) {\n\t\t\tundoneApplies = append(undoneApplies, apply)\n\t\t}\n\t}\n\n\t// Sort by creation time ascending to ensure proper order\n\tsort.Slice(undoneApplies, func(i, j int) bool {\n\t\treturn undoneApplies[i].CreatedAt.Before(undoneApplies[j].CreatedAt)\n\t})\n\n\treturn undoneApplies\n}\n\n// GetAffectedFilePaths extracts the set of file paths that were modified by the given PlanApplies.\n// It looks up each PlanFileResultId in the current state to get the actual file paths.\nfunc GetAffectedFilePaths(currentState *shared.CurrentPlanState, applies []*shared.PlanApply) map[string]bool {\n\tif currentState == nil || currentState.PlanResult == nil {\n\t\treturn nil\n\t}\n\n\t// First collect all file result IDs\n\tfileResultIds := make(map[string]bool)\n\tfor _, apply := range applies {\n\t\tif apply == nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, fileId := range apply.PlanFileResultIds {\n\t\t\tif fileId != \"\" {\n\t\t\t\tfileResultIds[fileId] = true\n\t\t\t}\n\t\t}\n\t}\n\n\t// Then get the actual file paths from the plan result\n\taffectedPaths := make(map[string]bool)\n\tfor _, result := range currentState.PlanResult.Results {\n\t\tif result == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip if this result wasn't part of an undone apply\n\t\tif !fileResultIds[result.Id] {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip if the result was rejected\n\t\tif result.RejectedAt != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip if the result was never applied\n\t\tif result.AppliedAt == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Validate path\n\t\tif result.Path == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check if path is in plan context\n\t\tif currentState.ContextsByPath[result.Path] == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\taffectedPaths[result.Path] = true\n\t}\n\n\treturn affectedPaths\n}\n\n// RewindAnalysis captures all the information about a potential rewind operation\ntype RewindAnalysis struct {\n\t// Files that need to be modified when rewinding from current plan state to target plan state\n\tRequiredChanges map[string]string\n\t// Files that have been modified on disk relative to current plan state (potential conflicts)\n\tConflicts map[string]bool\n}\n\n// AnalyzeRewind examines the three states (disk, current plan, target plan) to determine:\n// 1. What files need to be changed to reach target state\n// 2. Which of those changes would conflict with user modifications\nfunc AnalyzeRewind(targetState, currentState *shared.CurrentPlanState) (*RewindAnalysis, error) {\n\tif targetState == nil || currentState == nil {\n\t\treturn nil, fmt.Errorf(\"both target and current states must be provided\")\n\t}\n\n\t// First determine what files need to be changed between current and target plan states\n\trequiredChanges := make(map[string]string)\n\n\t// Track all paths we need to examine for either changes or conflicts\n\tallPaths := make(map[string]bool)\n\n\t// Add paths from both states\n\tfor path, context := range targetState.ContextsByPath {\n\t\tif context.ContextType != shared.ContextFileType {\n\t\t\tcontinue\n\t\t}\n\t\tallPaths[path] = true\n\t}\n\tfor path, context := range currentState.ContextsByPath {\n\t\tif context.ContextType != shared.ContextFileType {\n\t\t\tcontinue\n\t\t}\n\t\tallPaths[path] = true\n\t}\n\n\t// For each path, check if content differs between current and target states\n\tfor path := range allPaths {\n\t\ttargetContent := \"\"\n\t\tif ctx := targetState.ContextsByPath[path]; ctx != nil {\n\t\t\ttargetContent = ctx.Body\n\t\t}\n\n\t\tcurrentContent := \"\"\n\t\tif ctx := currentState.ContextsByPath[path]; ctx != nil {\n\t\t\tcurrentContent = ctx.Body\n\t\t}\n\n\t\t// If content differs between plan states, this is a required change\n\t\tif targetContent != currentContent {\n\t\t\tif targetContent == \"\" {\n\t\t\t\t// File should be removed\n\t\t\t\trequiredChanges[path] = \"\"\n\t\t\t} else {\n\t\t\t\t// File should be added or modified\n\t\t\t\trequiredChanges[path] = targetContent\n\t\t\t}\n\t\t}\n\t}\n\n\t// Now check for conflicts by comparing disk state with current plan state\n\t// A conflict exists if a file that needs to be changed has been modified on disk\n\tconflicts := make(map[string]bool)\n\n\tvar mu sync.Mutex\n\terrCh := make(chan error, len(requiredChanges))\n\n\tfor path := range requiredChanges {\n\t\tgo func(path string) {\n\t\t\tvar outErr error\n\t\t\tdefer func() { errCh <- outErr }()\n\n\t\t\t// Get the content from current plan state\n\t\t\tcurrentContent := \"\"\n\t\t\tif ctx := currentState.ContextsByPath[path]; ctx != nil {\n\t\t\t\tcurrentContent = ctx.Body\n\t\t\t}\n\n\t\t\t// Get the actual file content from disk\n\t\t\tdstPath := filepath.Join(fs.ProjectRoot, path)\n\t\t\tdiskContent, err := os.ReadFile(dstPath)\n\t\t\tif err != nil {\n\t\t\t\tif os.IsNotExist(err) {\n\t\t\t\t\t// If file doesn't exist on disk and current state has no content,\n\t\t\t\t\t// there's no conflict\n\t\t\t\t\tif currentContent == \"\" {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\t// Otherwise it's a conflict because file was deleted\n\t\t\t\t\tmu.Lock()\n\t\t\t\t\tconflicts[path] = true\n\t\t\t\t\tmu.Unlock()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\toutErr = fmt.Errorf(\"failed to read %s: %w\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// If disk content differs from current plan state, we have a conflict\n\t\t\tif string(diskContent) != currentContent {\n\t\t\t\tmu.Lock()\n\t\t\t\tconflicts[path] = true\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t}(path)\n\t}\n\n\t// Collect any errors from goroutines\n\tfor i := 0; i < len(requiredChanges); i++ {\n\t\tif err := <-errCh; err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &RewindAnalysis{\n\t\tRequiredChanges: requiredChanges,\n\t\tConflicts:       conflicts,\n\t}, nil\n}\n\n// RemoveEmptyDirs recursively removes empty directories starting from the given path\nfunc RemoveEmptyDirs(path string, baseDir string) error {\n\t// Check if the path is a directory\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\tif !info.IsDir() {\n\t\treturn nil\n\t}\n\n\t// List directory contents\n\tentries, err := os.ReadDir(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If directory has contents, leave it alone\n\tif len(entries) > 0 {\n\t\treturn nil\n\t}\n\n\t// Directory is empty, remove it if it's not the base dir\n\tif path != baseDir {\n\t\tif err := os.Remove(path); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Recursively check parent directory\n\tparent := filepath.Dir(path)\n\tif parent != baseDir && parent != path {\n\t\treturn RemoveEmptyDirs(parent, baseDir)\n\t}\n\n\treturn nil\n}\n\n// ApplyRewindChanges updates files on disk to match target state\nfunc ApplyRewindChanges(requiredChanges map[string]string) error {\n\tif len(requiredChanges) == 0 {\n\t\treturn nil\n\t}\n\n\t// Track directories that might need cleanup\n\tdirsToCheck := make(map[string]bool)\n\tvar mu sync.Mutex\n\n\terrCh := make(chan error, len(requiredChanges))\n\n\tfor path, content := range requiredChanges {\n\t\tgo func(path, content string) {\n\t\t\tdstPath := filepath.Join(fs.ProjectRoot, path)\n\n\t\t\tif content == \"\" {\n\t\t\t\t// Remove the file\n\t\t\t\terr := os.Remove(dstPath)\n\t\t\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\t\t\terrCh <- fmt.Errorf(\"failed to remove %s: %w\", path, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Mark parent directory for cleanup\n\t\t\t\tparentDir := filepath.Dir(dstPath)\n\t\t\t\tmu.Lock()\n\t\t\t\tdirsToCheck[parentDir] = true\n\t\t\t\tmu.Unlock()\n\t\t\t\terrCh <- nil\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Ensure directory exists\n\t\t\tif err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to create directory for %s: %w\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Write the file\n\t\t\tif err := os.WriteFile(dstPath, []byte(content), 0644); err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to write %s: %w\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}(path, content)\n\t}\n\n\t// Collect any errors\n\tfor i := 0; i < len(requiredChanges); i++ {\n\t\tif err := <-errCh; err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Clean up empty directories\n\tfor dir := range dirsToCheck {\n\t\tif err := RemoveEmptyDirs(dir, fs.ProjectRoot); err != nil {\n\t\t\t// Log but don't fail the operation for directory cleanup errors\n\t\t\tfmt.Printf(\"Warning: failed to clean up directory %s: %v\\n\", dir, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/cli/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/cmd\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/plan_exec\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"plandex-cli/ui\"\n\n\tshared \"plandex-shared\"\n\n\t\"gopkg.in/natefinch/lumberjack.v2\"\n)\n\nfunc init() {\n\t// inter-package dependency injections to avoid circular imports\n\tauth.SetApiClient(api.Client)\n\n\tauth.SetOpenUnauthenticatedCloudURLFn(ui.OpenUnauthenticatedCloudURL)\n\tauth.SetOpenAuthenticatedURLFn(ui.OpenAuthenticatedURL)\n\n\tterm.SetOpenAuthenticatedURLFn(ui.OpenAuthenticatedURL)\n\tterm.SetOpenUnauthenticatedCloudURLFn(ui.OpenUnauthenticatedCloudURL)\n\tterm.SetConvertTrialFn(auth.ConvertTrial)\n\n\tplan_exec.SetPromptSyncModelsIfNeeded(lib.PromptSyncModelsIfNeeded)\n\n\tlib.SetBuildPlanInlineFn(func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error) {\n\t\tauthVars := lib.MustVerifyAuthVars(auth.Current.IntegratedModelsMode)\n\t\treturn plan_exec.Build(plan_exec.ExecParams{\n\t\t\tCurrentPlanId: lib.CurrentPlanId,\n\t\t\tCurrentBranch: lib.CurrentBranch,\n\t\t\tAuthVars:      authVars,\n\t\t\tCheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {\n\t\t\t\treturn lib.CheckOutdatedContextWithOutput(true, autoConfirm, maybeContexts, projectPaths)\n\t\t\t},\n\t\t}, types.BuildFlags{})\n\t})\n\n\t// set up a rotating file logger\n\tlogger := &lumberjack.Logger{\n\t\tFilename:   filepath.Join(fs.HomePlandexDir, \"plandex.log\"),\n\t\tMaxSize:    10,   // megabytes before rotation\n\t\tMaxBackups: 3,    // number of backups to keep\n\t\tMaxAge:     28,   // days to keep old logs\n\t\tCompress:   true, // compress rotated files\n\t}\n\n\t// Set the output of the logger\n\tlog.SetOutput(logger)\n\tlog.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)\n\n\t// log.Println(\"Starting Plandex - logging initialized\")\n}\n\nfunc main() {\n\t// Manually check for help flags at the root level\n\tif len(os.Args) == 2 && (os.Args[1] == \"-h\" || os.Args[1] == \"--help\") {\n\t\t// Display your custom help here\n\t\tterm.PrintCustomHelp(true)\n\t\tos.Exit(0)\n\t}\n\n\tvar firstArg string\n\tif len(os.Args) > 1 {\n\t\tfirstArg = os.Args[1]\n\t}\n\n\tif firstArg != \"version\" && firstArg != \"browser\" && firstArg != \"help\" && firstArg != \"h\" {\n\t\tcheckForUpgrade()\n\t}\n\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "app/cli/nodemon.json",
    "content": "{\n  \"watch\": [\n    \".\",\n    \"../shared\"\n  ],\n  \"ext\": \"go,mod,sum\",\n  \"exec\": \"./dev.sh\"\n}"
  },
  {
    "path": "app/cli/plan.json",
    "content": "{\n  \"name\": \"draft\",\n  \"proposalId\": \"\",\n  \"rootId\": \"\",\n  \"createdAt\": \"2023-11-04T09:44:29.147Z\",\n  \"updatedAt\": \"2023-11-04T09:44:29.147Z\",\n  \"description\": null,\n  \"contextTokens\": 0,\n  \"convoTokens\": 0,\n  \"convoSummarizedTokens\": 0\n}"
  },
  {
    "path": "app/cli/plan_exec/action_menu.go",
    "content": "package plan_exec\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\tshared \"plandex-shared\"\n\t\"strings\"\n\n\t\"github.com/eiannone/keyboard\"\n\t\"github.com/fatih/color\"\n\t\"github.com/olekukonko/tablewriter\"\n)\n\ntype hotkeyOption struct {\n\tchar            string\n\tkey             keyboard.Key\n\tcommand         string\n\tdescription     string\n\treplOnly        bool\n\tterminalOnly    bool\n\tdropdownOnly    bool\n\tapplyScriptOnly bool\n}\n\nvar allHotkeyOptions = []hotkeyOption{\n\t{\n\t\tchar:         \"d\",\n\t\tcommand:      \"diff ui\",\n\t\tdescription:  \"Review diffs in browser UI\",\n\t\treplOnly:     false,\n\t\tterminalOnly: false,\n\t},\n\t{\n\t\tchar:         \"g\",\n\t\tcommand:      \"git diff format\",\n\t\tdescription:  \"Review diffs in git diff format\",\n\t\treplOnly:     false,\n\t\tterminalOnly: false,\n\t},\n\t{\n\t\tchar:         \"r\",\n\t\tcommand:      \"reject\",\n\t\tdescription:  \"Reject some or all pending changes\",\n\t\treplOnly:     false,\n\t\tterminalOnly: false,\n\t},\n\n\t{\n\t\tchar:         \"a\",\n\t\tcommand:      \"apply\",\n\t\tdescription:  \"Apply all pending changes\",\n\t\treplOnly:     false,\n\t\tterminalOnly: false,\n\t},\n\n\t{\n\t\tchar:            \"f\",\n\t\tcommand:         \"full auto\",\n\t\tdescription:     \"Apply and debug in full auto mode\",\n\t\treplOnly:        true,\n\t\tterminalOnly:    false,\n\t\tapplyScriptOnly: true,\n\t},\n\n\t{\n\t\tchar:         \"q\",\n\t\tkey:          keyboard.KeyEnter,\n\t\tcommand:      \"exit menu\",\n\t\tdescription:  \"Exit menu\",\n\t\tdropdownOnly: true,\n\t},\n}\n\nfunc showHotkeyMenu(diffs []string) {\n\thasApplyScript := false\n\tfor _, diff := range diffs {\n\t\tif diff == \"_apply.sh\" {\n\t\t\thasApplyScript = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tnumDiffs := len(diffs)\n\tif hasApplyScript {\n\t\tnumDiffs--\n\t}\n\n\tif numDiffs > 0 {\n\t\ts := \"files have\"\n\t\tif numDiffs == 1 {\n\t\t\ts = \"file has\"\n\t\t}\n\t\tcolor.New(color.Bold, term.ColorHiGreen).Printf(\"🧐 %d %s pending changes\\n\", numDiffs, s)\n\n\t\tfor _, diff := range diffs {\n\t\t\tif diff == \"_apply.sh\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfmt.Printf(\"• 📄 %s\\n\", diff)\n\t\t}\n\t\tfmt.Println()\n\t}\n\n\tif hasApplyScript {\n\t\tcolor.New(color.Bold, term.ColorHiYellow).Println(\"🚀 Commands pending\")\n\t\tfmt.Println()\n\t}\n\n\tvar b strings.Builder\n\ttable := tablewriter.NewWriter(&b)\n\ttable.SetAutoWrapText(false)\n\ttable.SetHeaderLine(false)\n\ttable.SetAlignment(tablewriter.ALIGN_LEFT)\n\n\tfor _, opt := range allHotkeyOptions {\n\t\tif (opt.terminalOnly && term.IsRepl) || (opt.replOnly && !term.IsRepl) || opt.dropdownOnly || (opt.applyScriptOnly && !hasApplyScript) {\n\t\t\tcontinue\n\t\t}\n\n\t\tc := color.New(term.ColorHiCyan, color.Bold)\n\t\tif opt.command == \"apply\" {\n\t\t\tc = color.New(term.ColorHiGreen, color.Bold)\n\t\t} else if opt.command == \"reject\" {\n\t\t\tc = color.New(term.ColorHiRed, color.Bold)\n\t\t} else if opt.command == \"full auto\" {\n\t\t\tc = color.New(term.ColorHiYellow, color.Bold)\n\t\t}\n\n\t\ttable.Append([]string{\n\t\t\tc.Sprintf(\"(%s)\", opt.char),\n\t\t\topt.command,\n\t\t\topt.description,\n\t\t})\n\t}\n\n\ttable.Render()\n\tfmt.Print(b.String())\n\n\tfmt.Printf(\"%s %s %s %s %s\",\n\t\tcolor.New(term.ColorHiMagenta, color.Bold).Sprint(\"Press a hotkey,\"),\n\t\tcolor.New(color.FgHiWhite, color.Bold).Sprintf(\"↓\"),\n\t\tcolor.New(term.ColorHiMagenta, color.Bold).Sprintf(\"to select, or\"),\n\t\tcolor.New(color.FgHiWhite, color.Bold).Sprintf(\"enter\"),\n\t\tcolor.New(term.ColorHiMagenta, color.Bold).Sprintf(\"to exit menu/keep iterating>\"),\n\t)\n}\n\nfunc handleHotkey(diffs []string, params ExecParams) {\n\tchar, key, err := term.GetUserKeyInput()\n\tif err != nil {\n\t\tfmt.Printf(\"\\nError getting key: %v\\n\", err)\n\t\tshowHotkeyMenu(diffs)\n\t\thandleHotkey(diffs, params)\n\t}\n\n\tif key == keyboard.KeyArrowDown {\n\t\toptions := []string{}\n\t\tfor _, opt := range allHotkeyOptions {\n\t\t\tif (opt.terminalOnly && term.IsRepl) || (opt.replOnly && !term.IsRepl) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\toptions = append(options, opt.description)\n\t\t}\n\n\t\tselected, err := term.SelectFromList(\n\t\t\t\"Select an action\",\n\t\t\toptions,\n\t\t)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\nError selecting action: %v\\n\", err)\n\t\t\tshowHotkeyMenu(diffs)\n\t\t\thandleHotkey(diffs, params)\n\t\t}\n\n\t\tif selected != \"\" {\n\t\t\tvar option hotkeyOption\n\t\t\tfor _, opt := range allHotkeyOptions {\n\t\t\t\tif opt.description == selected {\n\t\t\t\t\tif (opt.terminalOnly && term.IsRepl) || (opt.replOnly && !term.IsRepl) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\toption = opt\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\thandleHotkeyOption(option, diffs, params)\n\t\t}\n\t}\n\n\thandleHotkeyOption(hotkeyOption{char: string(char), key: key}, diffs, params)\n}\n\nfunc handleHotkeyOption(option hotkeyOption, diffs []string, params ExecParams) {\n\texitUnlessDiffs := func() {\n\t\tdiffs, apiErr := getDiffs(params)\n\t\tif apiErr != nil {\n\t\t\tfmt.Printf(\"\\nError getting plan diffs: %v\\n\", apiErr.Msg)\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\tif len(diffs) == 0 {\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\tshowHotkeyMenu(diffs)\n\t\thandleHotkey(diffs, params)\n\t}\n\n\tfmt.Println()\n\n\tif option.char == \"d\" {\n\t\tfmt.Println()\n\t\t_, err := lib.ExecPlandexCommandWithParams([]string{\"diffs\", \"--ui\", \"--from-tell-menu\"}, lib.ExecPlandexCommandParams{\n\t\t\tDisableSuggestions: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\nError showing diffs: %v\\n\", err)\n\t\t}\n\t\tfmt.Println()\n\t} else if option.char == \"g\" {\n\t\tfmt.Println()\n\t\t_, err := lib.ExecPlandexCommandWithParams([]string{\"diffs\", \"--git\"}, lib.ExecPlandexCommandParams{\n\t\t\tDisableSuggestions: true,\n\t\t})\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\nError showing diffs: %v\\n\", err)\n\t\t}\n\t\tfmt.Println()\n\t} else if option.char == \"a\" {\n\t\tfmt.Print(\"(a)\")\n\t\tfmt.Println()\n\t\t_, err := lib.ExecPlandexCommand([]string{\"apply\"})\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\nError applying changes: %v\\n\", err)\n\t\t}\n\t\tfmt.Println()\n\t\tos.Exit(0)\n\t} else if option.char == \"r\" {\n\t\tfmt.Println()\n\t\t_, err := lib.ExecPlandexCommand([]string{\"reject\"})\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\nError rejecting changes: %v\\n\", err)\n\t\t}\n\t\tfmt.Println()\n\t\texitUnlessDiffs()\n\t} else if option.char == \"f\" {\n\t\tfmt.Print(\"(f)\")\n\t\tfmt.Println()\n\n\t\tcolor.New(term.ColorHiYellow, color.Bold).Println(\"⚠️  Full auto mode allows automatic apply, execution, and multiple rounds of debugging without review.\")\n\t\tfmt.Println()\n\n\t\t_, err := lib.ExecPlandexCommand([]string{\"apply\", \"--full\"})\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\nError applying changes: %v\\n\", err)\n\t\t}\n\t\tfmt.Println()\n\t\tos.Exit(0)\n\t} else if option.char == \"q\" || option.key == keyboard.KeyEnter {\n\t\tos.Exit(0)\n\t} else {\n\t\tfmt.Println(\"\\nInvalid hotkey\")\n\t}\n\n\tshowHotkeyMenu(diffs)\n\thandleHotkey(diffs, params)\n}\n\nfunc getDiffs(params ExecParams) ([]string, *shared.ApiError) {\n\tcurrentPlan, apiErr := api.Client.GetCurrentPlanState(params.CurrentPlanId, params.CurrentBranch)\n\tif apiErr != nil {\n\t\treturn nil, apiErr\n\t}\n\n\treturn currentPlan.PlanResult.SortedPaths, nil\n}\n"
  },
  {
    "path": "app/cli/plan_exec/apply_exec.go",
    "content": "package plan_exec\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n)\n\nfunc GetOnApplyExecFail(applyFlags types.ApplyFlags, tellFlags types.TellFlags) types.OnApplyExecFailFn {\n\treturn getOnApplyExecFail(applyFlags, tellFlags, \"\")\n}\n\nfunc GetOnApplyExecFailWithCommand(applyFlags types.ApplyFlags, tellFlags types.TellFlags, execCommand string) types.OnApplyExecFailFn {\n\treturn getOnApplyExecFail(applyFlags, tellFlags, execCommand)\n}\n\nfunc getOnApplyExecFail(applyFlags types.ApplyFlags, tellFlags types.TellFlags, execCommand string) types.OnApplyExecFailFn {\n\tvar onExecFail types.OnApplyExecFailFn\n\tonExecFail = func(status int, output string, attempt int, toRollback *types.ApplyRollbackPlan, onErr types.OnErrFn, onSuccess func()) {\n\t\tvar proceed bool\n\t\tresetAttempts := false\n\n\t\tif applyFlags.AutoDebug > 0 {\n\t\t\tif attempt >= applyFlags.AutoDebug {\n\t\t\t\ttimesLbl := \"times\"\n\t\t\t\tif attempt == 1 {\n\t\t\t\t\ttimesLbl = \"time\"\n\t\t\t\t}\n\t\t\t\tcolor.New(term.ColorHiRed, color.Bold).Printf(\"Commands failed %d %s.\\n\", attempt, timesLbl)\n\t\t\t} else {\n\t\t\t\tproceed = true\n\t\t\t}\n\t\t}\n\n\t\tif !proceed {\n\t\t\tconst (\n\t\t\t\tDebugAndRetry          = \"Debug and retry once\"\n\t\t\t\tDebugInFullAutoMode    = \"Debug in full auto mode\"\n\t\t\t\tRollbackChangesAndExit = \"Rollback changes and exit\"\n\t\t\t\tApplyChangesAndExit    = \"Apply changes and exit\"\n\t\t\t)\n\t\t\topts := []string{\n\t\t\t\tDebugAndRetry,\n\t\t\t\tDebugInFullAutoMode,\n\t\t\t\tRollbackChangesAndExit,\n\t\t\t\tApplyChangesAndExit,\n\t\t\t}\n\n\t\t\tselection, err := term.SelectFromList(\"What do you want to do?\", opts)\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"failed to get confirmation user input: %s\", err)\n\t\t\t}\n\n\t\t\tswitch selection {\n\t\t\tcase DebugAndRetry:\n\t\t\t\tproceed = true\n\t\t\tcase DebugInFullAutoMode:\n\t\t\t\tproceed = true\n\t\t\t\tresetAttempts = true\n\n\t\t\t\tterm.StartSpinner(\"\")\n\t\t\t\tconfig, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)\n\n\t\t\t\tif apiErr != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"failed to get plan config: %s\", apiErr)\n\t\t\t\t}\n\n\t\t\t\tif config.AutoMode != shared.AutoModeFull {\n\t\t\t\t\tconfig.SetAutoMode(shared.AutoModeFull)\n\t\t\t\t\tapiErr = api.Client.UpdatePlanConfig(lib.CurrentPlanId, shared.UpdatePlanConfigRequest{\n\t\t\t\t\t\tConfig: config,\n\t\t\t\t\t})\n\n\t\t\t\t\tif apiErr != nil {\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"failed to update plan config: %s\", apiErr)\n\t\t\t\t\t}\n\n\t\t\t\t\tlib.SetCachedPlanConfig(config)\n\n\t\t\t\t\tapplyFlags.AutoCommit = true\n\t\t\t\t\tapplyFlags.AutoConfirm = true\n\t\t\t\t\tapplyFlags.AutoExec = true\n\t\t\t\t\tapplyFlags.AutoDebug = config.AutoDebugTries\n\n\t\t\t\t\ttellFlags.AutoApply = true\n\t\t\t\t\ttellFlags.AutoContext = true\n\t\t\t\t\ttellFlags.ExecEnabled = true\n\t\t\t\t\ttellFlags.SmartContext = true\n\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tfmt.Println(\"✅ Full auto mode enabled\")\n\t\t\t\t\tfmt.Println()\n\t\t\t\t} else {\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t}\n\n\t\t\tcase RollbackChangesAndExit:\n\t\t\t\tif toRollback != nil {\n\t\t\t\t\tlib.Rollback(toRollback, true)\n\t\t\t\t}\n\t\t\t\tos.Exit(1)\n\t\t\tcase ApplyChangesAndExit:\n\t\t\t\tonSuccess()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif proceed {\n\t\t\tif toRollback != nil && toRollback.HasChanges() {\n\t\t\t\tlib.Rollback(toRollback, true)\n\t\t\t}\n\n\t\t\tauthVars := lib.MustVerifyAuthVarsSilent(auth.Current.IntegratedModelsMode)\n\n\t\t\tprompt := fmt.Sprintf(\"Execution failed with exit status %d. Output:\\n\\n%s\\n\\n--\\n\\n\",\n\t\t\t\tstatus, output)\n\n\t\t\ttellFlags.IsUserContinue = false\n\n\t\t\tif execCommand != \"\" {\n\t\t\t\ttellFlags.IsApplyDebug = false\n\t\t\t\ttellFlags.ExecEnabled = false\n\t\t\t\ttellFlags.IsUserDebug = true\n\t\t\t} else {\n\t\t\t\ttellFlags.IsApplyDebug = true\n\t\t\t\ttellFlags.ExecEnabled = true\n\t\t\t\ttellFlags.IsUserDebug = false\n\t\t\t}\n\n\t\t\tlog.Printf(\"Calling TellPlan for next debug attempt\")\n\n\t\t\tTellPlan(ExecParams{\n\t\t\t\tCurrentPlanId: lib.CurrentPlanId,\n\t\t\t\tCurrentBranch: lib.CurrentBranch,\n\t\t\t\tAuthVars:      authVars,\n\t\t\t\tCheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {\n\t\t\t\t\treturn lib.CheckOutdatedContextWithOutput(true, true, maybeContexts, projectPaths)\n\t\t\t\t},\n\t\t\t}, prompt, tellFlags)\n\n\t\t\tlog.Printf(\"Applying plan after tell\")\n\n\t\t\tif resetAttempts {\n\t\t\t\tattempt = 0\n\t\t\t}\n\n\t\t\tlib.MustApplyPlanAttempt(lib.ApplyPlanParams{\n\t\t\t\tPlanId:      lib.CurrentPlanId,\n\t\t\t\tBranch:      lib.CurrentBranch,\n\t\t\t\tApplyFlags:  applyFlags,\n\t\t\t\tTellFlags:   tellFlags,\n\t\t\t\tOnExecFail:  onExecFail,\n\t\t\t\tExecCommand: execCommand,\n\t\t\t}, attempt+1)\n\t\t}\n\t}\n\n\treturn onExecFail\n}\n"
  },
  {
    "path": "app/cli/plan_exec/build.go",
    "content": "package plan_exec\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/stream\"\n\tstreamtui \"plandex-cli/stream_tui\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc Build(params ExecParams, flags types.BuildFlags) (bool, error) {\n\tbuildBg := flags.BuildBg\n\n\tterm.StartSpinner(\"\")\n\n\terr := PromptSyncModelsIfNeeded()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error syncing models: %v\", err)\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tcontexts, apiErr := api.Client.ListContext(params.CurrentPlanId, params.CurrentBranch)\n\n\tif apiErr != nil {\n\t\tterm.OutputErrorAndExit(\"Error getting context: %v\", apiErr)\n\t}\n\n\tpaths, err := fs.GetProjectPaths(fs.GetBaseDirForContexts(contexts))\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error getting project paths: %v\", err)\n\t}\n\n\tanyOutdated, didUpdate, err := params.CheckOutdatedContext(contexts, paths)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"error checking outdated context: %v\", err)\n\t}\n\n\tif anyOutdated && !didUpdate {\n\t\tterm.StopSpinner()\n\t\tlog.Println(\"Build canceled\")\n\t\treturn false, nil\n\t}\n\n\tapiErr = api.Client.BuildPlan(params.CurrentPlanId, params.CurrentBranch, shared.BuildPlanRequest{\n\t\tConnectStream: !buildBg,\n\t\tProjectPaths:  paths.ActivePaths,\n\t\tAuthVars:      params.AuthVars,\n\t}, stream.OnStreamPlan)\n\n\tterm.StopSpinner()\n\n\tif apiErr != nil {\n\t\tif apiErr.Msg == shared.NoBuildsErr {\n\t\t\tfmt.Println(\"🤷‍♂️ This plan has no pending changes to build\")\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, fmt.Errorf(\"error building plan: %v\", apiErr.Msg)\n\t}\n\n\tif !buildBg {\n\t\tch := make(chan error)\n\n\t\tgo func() {\n\t\t\terr := streamtui.StartStreamUI(\"\", true, !flags.AutoApply)\n\n\t\t\tif err != nil {\n\t\t\t\tch <- fmt.Errorf(\"error starting stream UI: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tch <- nil\n\t\t}()\n\n\t\t// Wait for the stream to finish\n\t\terr := <-ch\n\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\treturn true, nil\n}\n"
  },
  {
    "path": "app/cli/plan_exec/params.go",
    "content": "package plan_exec\n\nimport (\n\t\"plandex-cli/types\"\n\tshared \"plandex-shared\"\n)\n\ntype ExecParams struct {\n\tCurrentPlanId        string\n\tCurrentBranch        string\n\tAuthVars             map[string]string\n\tCheckOutdatedContext func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error)\n}\n\nvar PromptSyncModelsIfNeeded func() error\n\nfunc SetPromptSyncModelsIfNeeded(fn func() error) {\n\tPromptSyncModelsIfNeeded = fn\n}\n"
  },
  {
    "path": "app/cli/plan_exec/tell.go",
    "content": "package plan_exec\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/auth\"\n\t\"plandex-cli/fs\"\n\t\"plandex-cli/stream\"\n\tstreamtui \"plandex-cli/stream_tui\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"plandex-cli/ui\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/shopspring/decimal\"\n)\n\n// For cloud trials in Integrated Models mode, we warn after the stream finishes when the balance is less than $1\nconst CloudTrialBalanceWarningThreshold = 1\n\nfunc TellPlan(\n\tparams ExecParams,\n\tprompt string,\n\tflags types.TellFlags,\n) {\n\n\ttellBg := flags.TellBg\n\ttellStop := flags.TellStop\n\ttellNoBuild := flags.TellNoBuild\n\tisUserContinue := flags.IsUserContinue\n\tisDebugCmd := flags.IsUserDebug\n\tisChatOnly := flags.IsChatOnly\n\tautoContext := flags.AutoContext\n\tsmartContext := flags.SmartContext\n\texecEnabled := flags.ExecEnabled\n\tautoApply := flags.AutoApply\n\tisApplyDebug := flags.IsApplyDebug\n\tisImplementationOfChat := flags.IsImplementationOfChat\n\tskipChangesMenu := flags.SkipChangesMenu\n\tdone := make(chan struct{})\n\n\tif prompt == \"\" && isImplementationOfChat {\n\t\tprompt = \"Go ahead with the plan based on what we've discussed so far.\"\n\t}\n\n\toutputPromptIfTell := func() {\n\t\tif isUserContinue || prompt == \"\" {\n\t\t\treturn\n\t\t}\n\n\t\tterm.StopSpinner()\n\t\t// print prompt so it isn't lost\n\t\tcolor.New(term.ColorHiCyan, color.Bold).Println(\"\\nYour prompt 👇\")\n\t\tfmt.Println()\n\t\tfmt.Println(prompt)\n\t\tfmt.Println()\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\terr := PromptSyncModelsIfNeeded()\n\tif err != nil {\n\t\toutputPromptIfTell()\n\t\tterm.OutputErrorAndExit(\"Error syncing models: %v\", err)\n\t}\n\n\tterm.StartSpinner(\"\")\n\n\tcontexts, apiErr := api.Client.ListContext(params.CurrentPlanId, params.CurrentBranch)\n\n\tif apiErr != nil {\n\t\toutputPromptIfTell()\n\t\tterm.OutputErrorAndExit(\"Error getting context: %v\", apiErr)\n\t}\n\n\tpaths, err := fs.GetProjectPaths(fs.GetBaseDirForContexts(contexts))\n\n\tif err != nil {\n\t\toutputPromptIfTell()\n\t\tterm.OutputErrorAndExit(\"Error getting project paths: %v\", err)\n\t}\n\n\tanyOutdated, didUpdate, err := params.CheckOutdatedContext(contexts, paths)\n\n\tif err != nil {\n\t\toutputPromptIfTell()\n\t\tterm.OutputErrorAndExit(\"Error checking outdated context: %v\", err)\n\t}\n\n\tif anyOutdated && !didUpdate {\n\t\tterm.StopSpinner()\n\t\tif isUserContinue {\n\t\t\tlog.Println(\"Plan won't continue\")\n\t\t} else {\n\t\t\tlog.Println(\"Prompt not sent\")\n\t\t}\n\n\t\toutputPromptIfTell()\n\t\tcolor.New(term.ColorHiRed, color.Bold).Println(\"🛑 Plan won't continue due to outdated context\")\n\n\t\tos.Exit(0)\n\t}\n\n\tvar fn func() bool\n\tfn = func() bool {\n\n\t\tvar buildMode shared.BuildMode\n\t\tif tellNoBuild || isChatOnly {\n\t\t\tbuildMode = shared.BuildModeNone\n\t\t} else {\n\t\t\tbuildMode = shared.BuildModeAuto\n\t\t}\n\n\t\t// if isUserContinue {\n\t\t// \tterm.StartSpinner(\"⚡️ Continuing plan...\")\n\t\t// } else {\n\t\t// \tterm.StartSpinner(\"💬 Sending prompt...\")\n\t\t// }\n\n\t\tterm.StartSpinner(\"\")\n\n\t\tvar osDetails string\n\t\tif execEnabled {\n\t\t\tosDetails = term.GetOsDetails()\n\t\t}\n\n\t\tisGitRepo := fs.ProjectRootIsGitRepo()\n\n\t\tapiErr := api.Client.TellPlan(params.CurrentPlanId, params.CurrentBranch, shared.TellPlanRequest{\n\t\t\tPrompt:                 prompt,\n\t\t\tConnectStream:          !tellBg,\n\t\t\tAutoContinue:           !tellStop,\n\t\t\tProjectPaths:           paths.ActivePaths,\n\t\t\tBuildMode:              buildMode,\n\t\t\tIsUserContinue:         isUserContinue,\n\t\t\tIsUserDebug:            isDebugCmd,\n\t\t\tIsChatOnly:             isChatOnly,\n\t\t\tAutoContext:            autoContext,\n\t\t\tSmartContext:           smartContext,\n\t\t\tExecEnabled:            execEnabled,\n\t\t\tOsDetails:              osDetails,\n\t\t\tAuthVars:               params.AuthVars,\n\t\t\tIsImplementationOfChat: isImplementationOfChat,\n\t\t\tIsGitRepo:              isGitRepo,\n\t\t\tSessionId:              os.Getenv(\"PLANDEX_REPL_SESSION_ID\"),\n\t\t}, stream.OnStreamPlan)\n\n\t\tterm.StopSpinner()\n\n\t\tif apiErr != nil {\n\t\t\tif apiErr.Type == shared.ApiErrorTypeTrialMessagesExceeded {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\n🚨 You've reached the Plandex Cloud trial limit of %d messages per plan\\n\", apiErr.TrialMessagesExceededError.MaxReplies)\n\n\t\t\t\tres, err := term.ConfirmYesNo(\"Upgrade now?\")\n\n\t\t\t\tif err != nil {\n\t\t\t\t\toutputPromptIfTell()\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error prompting upgrade trial: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif res {\n\t\t\t\t\tauth.ConvertTrial()\n\t\t\t\t\t// retry action after converting trial\n\t\t\t\t\treturn fn()\n\t\t\t\t}\n\n\t\t\t\toutputPromptIfTell()\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\toutputPromptIfTell()\n\t\t\tterm.OutputErrorAndExit(\"Prompt error: %v\", apiErr.Msg)\n\t\t} else if apiErr != nil && isUserContinue && apiErr.Type == shared.ApiErrorTypeContinueNoMessages {\n\t\t\tfmt.Println(\"🤷‍♂️ There's no plan yet to continue\")\n\t\t\tfmt.Println()\n\t\t\tterm.PrintCmds(\"\", \"tell\")\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\tif !tellBg {\n\t\t\tgo func() {\n\t\t\t\terr := streamtui.StartStreamUI(\n\t\t\t\t\tprompt,\n\t\t\t\t\tfalse,\n\t\t\t\t\t!(autoApply || autoContext || isApplyDebug || isDebugCmd),\n\t\t\t\t)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\toutputPromptIfTell()\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error starting stream UI: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif auth.Current.IsCloud && auth.Current.IntegratedModelsMode && auth.Current.OrgIsTrial {\n\t\t\t\t\tterm.StartSpinner(\"\")\n\t\t\t\t\tbalance, apiErr := api.Client.GetBalance()\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\tif apiErr != nil {\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error getting balance: %v\", apiErr.Msg)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tif balance.LessThan(decimal.NewFromInt(CloudTrialBalanceWarningThreshold)) {\n\t\t\t\t\t\tcolor.New(term.ColorHiYellow, color.Bold).Printf(\"\\n⚠️  Your Plandex Cloud trial has $%s in credits remaining\\n\\n\", balance.StringFixed(2))\n\n\t\t\t\t\t\tconst continueOpt = \"Continue\"\n\t\t\t\t\t\tconst billingSettingsOpt = \"Go to billing settings (then continue)\"\n\n\t\t\t\t\t\topts := []string{continueOpt, billingSettingsOpt}\n\t\t\t\t\t\tchoice, err := term.SelectFromList(\"What do you want to do?\", opts)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error selecting option: %v\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif choice == billingSettingsOpt {\n\t\t\t\t\t\t\tui.OpenAuthenticatedURL(\"Opening billing settings in your browser.\", \"/settings/billing\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif isChatOnly {\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\tif !term.IsRepl {\n\t\t\t\t\t\tterm.PrintCmds(\"\", \"tell\", \"convo\", \"summary\", \"log\")\n\t\t\t\t\t}\n\t\t\t\t} else if autoApply || isDebugCmd || isApplyDebug {\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\t// do nothing, allow auto apply to run\n\t\t\t\t} else if skipChangesMenu {\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\t// script mode, don't show menu\n\t\t\t\t} else {\n\t\t\t\t\tterm.StartSpinner(\"\")\n\t\t\t\t\t// sleep a little to prevent lock contention on server\n\t\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t\t\tdiffs, apiErr := getDiffs(params)\n\t\t\t\t\tterm.StopSpinner()\n\t\t\t\t\tif apiErr != nil {\n\t\t\t\t\t\tterm.OutputErrorAndExit(\"Error getting plan diffs: %v\", apiErr.Msg)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tnumDiffs := len(diffs)\n\t\t\t\t\thasDiffs := numDiffs > 0\n\n\t\t\t\t\tfmt.Println()\n\n\t\t\t\t\tif tellStop && hasDiffs {\n\t\t\t\t\t\tif hasDiffs {\n\t\t\t\t\t\t\t// term.PrintCmds(\"\", \"continue\", \"diff\", \"diff --ui\", \"apply\", \"reject\", \"log\")\n\t\t\t\t\t\t\tshowHotkeyMenu(diffs)\n\t\t\t\t\t\t\thandleHotkey(diffs, params)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tterm.PrintCmds(\"\", \"continue\", \"log\")\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if hasDiffs {\n\t\t\t\t\t\t// term.PrintCmds(\"\", \"diff\", \"diff --ui\", \"apply\", \"reject\", \"log\")\n\t\t\t\t\t\tshowHotkeyMenu(diffs)\n\t\t\t\t\t\thandleHotkey(diffs, params)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tclose(done)\n\n\t\t\t}()\n\t\t}\n\n\t\treturn true\n\t}\n\n\tshouldContinue := fn()\n\tif !shouldContinue {\n\t\treturn\n\t}\n\n\tif tellBg {\n\t\toutputPromptIfTell()\n\t\tfmt.Println(\"✅ Plan is active in the background\")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"ps\", \"connect\", \"stop\")\n\t} else {\n\t\t<-done\n\t}\n}\n"
  },
  {
    "path": "app/cli/schema/json-schemas/definitions/auto-modes.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/definitions/auto-modes.schema.json\",\n  \"title\": \"Auto Mode Enum\",\n  \"description\": \"Reusable enum for auto modes\",\n  \"enum\": [\n    \"full\",\n    \"semi\",\n    \"plus\",\n    \"basic\",\n    \"none\",\n    \"custom\"\n  ]\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/definitions/local-providers.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/definitions/local-providers.schema.json\",\n  \"title\": \"Local Provider Enum\",\n  \"description\": \"Reusable enum for local providers\",\n  \"enum\": [\n    \"ollama\"\n  ]\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/definitions/model-providers.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/definitions/model-providers.schema.json\",\n  \"title\": \"Model Providers\",\n  \"description\": \"The built-in model providers that Plandex supports. Use 'custom' for a provider that is not built-in.\",\n  \"enum\": [\n    \"openrouter\",\n    \"openai\",\n    \"anthropic\",\n    \"google-ai-studio\",\n    \"google-vertex\",\n    \"azure-openai\",\n    \"deepseek\",\n    \"perplexity\",\n    \"custom\"\n  ]\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/model-config.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/model-config.schema.json\",\n  \"title\": \"Model Config\",\n  \"description\": \"Config for a model\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"modelId\": {\n      \"type\": \"string\",\n      \"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'.\"\n    },\n    \"publisher\": {\n      \"type\": \"string\",\n      \"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.\"\n    },\n    \"description\": {\n      \"type\": \"string\",\n      \"description\": \"A human-readable description of the model, e.g. 'OpenAI o3'.\"\n    },\n    \"defaultMaxConvoTokens\": {\n      \"type\": \"number\",\n      \"description\": \"The default maximum number of conversation tokens that are allowed before Plandex starts using gradual summarization to shorten the conversation.\"\n    },\n    \"maxTokens\": {\n      \"type\": \"number\",\n      \"description\": \"The maximum number of input tokens the model can be called with.\"\n    },\n    \"maxOutputTokens\": {\n      \"type\": \"number\",\n      \"description\": \"The maximum number of output tokens the model can produce.\"\n    },\n    \"reservedOutputTokens\": {\n      \"type\": \"number\",\n      \"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.\"\n    },\n    \"preferredOutputFormat\": {\n      \"type\": \"string\",\n      \"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.\",\n      \"enum\": [\n        \"xml\",\n        \"tool-call-json\"\n      ]\n    },\n    \"systemPromptDisabled\": {\n      \"type\": \"boolean\",\n      \"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.\"\n    },\n    \"roleParamsDisabled\": {\n      \"type\": \"boolean\",\n      \"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.\"\n    },\n    \"stopDisabled\": {\n      \"type\": \"boolean\",\n      \"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.\"\n    },\n    \"predictedOutputEnabled\": {\n      \"type\": \"boolean\",\n      \"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.\"\n    },\n    \"includeReasoning\": {\n      \"type\": \"boolean\",\n      \"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.\"\n    },\n    \"reasoningBudget\": {\n      \"type\": \"number\",\n      \"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).\"\n    },\n    \"hasImageSupport\": {\n      \"type\": \"boolean\",\n      \"description\": \"Whether the model is multi-modal and supports images in context.\"\n    },\n    \"reasoningEffortEnabled\": {\n      \"type\": \"boolean\",\n      \"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).\"\n    },\n    \"reasoningEffort\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"low\",\n        \"medium\",\n        \"high\"\n      ],\n      \"description\": \"For reasoning models that use 'reasoningEffort' to control reasoning output (e.g. OpenAI o3), this is the reasoning effort to use.\"\n    },\n    \"supportsCacheControl\": {\n      \"type\": \"boolean\",\n      \"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.\"\n    },\n    \"singleMessageNoSystemPrompt\": {\n      \"type\": \"boolean\",\n      \"description\": \"Whether the model rejects a single message that is a system prompt (e.g. Anthropic models).\"\n    },\n    \"tokenEstimatePaddingPct\": {\n      \"type\": \"number\",\n      \"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.\"\n    },\n    \"providers\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"provider\": {\n            \"description\": \"The model provider. Use 'custom' for a provider that is not built-in.\",\n            \"$ref\": \"./definitions/model-providers.schema.json\"\n          },\n          \"customProvider\": {\n            \"type\": \"string\",\n            \"description\": \"If the provider is 'custom', this is the name of the custom provider.\"\n          },\n          \"modelName\": {\n            \"type\": \"string\",\n            \"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.\"\n          }\n        },\n        \"required\": [\n          \"provider\",\n          \"modelName\"\n        ],\n        \"allOf\": [\n          {\n            \"if\": {\n              \"properties\": {\n                \"provider\": {\n                  \"const\": \"custom\"\n                }\n              }\n            },\n            \"then\": {\n              \"required\": [\n                \"customProvider\"\n              ]\n            },\n            \"else\": {\n              \"not\": {\n                \"required\": [\n                  \"customProvider\"\n                ]\n              }\n            }\n          }\n        ]\n      },\n      \"minItems\": 1\n    }\n  },\n  \"required\": [\n    \"modelId\",\n    \"defaultMaxConvoTokens\",\n    \"maxTokens\",\n    \"maxOutputTokens\",\n    \"reservedOutputTokens\",\n    \"preferredOutputFormat\",\n    \"providers\"\n  ],\n  \"additionalProperties\": false\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/model-pack-base-config.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/model-pack-base-config.schema.json\",\n  \"title\": \"Base Model Pack Config\",\n  \"description\": \"Base config for a model pack\",\n  \"type\": \"object\",\n  \"allOf\": [\n    {\n      \"$ref\": \"./model-pack-roles.schema.json\"\n    }\n  ],\n  \"properties\": {\n    \"$schema\": {\n      \"type\": \"string\"\n    },\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"The name of the model pack\"\n    },\n    \"description\": {\n      \"type\": \"string\",\n      \"description\": \"The human-friendly description of the model pack\"\n    }\n  },\n  \"required\": [\n    \"name\",\n    \"description\"\n  ]\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/model-pack-config.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/model-pack-config.schema.json\",\n  \"title\": \"Model Pack Config\",\n  \"allOf\": [\n    {\n      \"$ref\": \"./model-pack-base-config.schema.json\"\n    }\n  ],\n  \"properties\": {\n    \"$schema\": true,\n    \"name\": true,\n    \"description\": true,\n    \"localProvider\": true,\n    \"planner\": true,\n    \"coder\": true,\n    \"architect\": true,\n    \"summarizer\": true,\n    \"builder\": true,\n    \"wholeFileBuilder\": true,\n    \"names\": true,\n    \"commitMessages\": true,\n    \"autoContinue\": true\n  },\n  \"additionalProperties\": false\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/model-pack-inline.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/model-pack-inline.schema.json\",\n  \"title\": \"Inline Model Pack Config\",\n  \"description\": \"Inline model pack config for plan model settings or default model settings\",\n  \"allOf\": [\n    {\n      \"$ref\": \"./model-pack-roles.schema.json\"\n    }\n  ],\n  \"properties\": {\n    \"$schema\": true,\n    \"name\": true,\n    \"description\": true,\n    \"localProvider\": true,\n    \"planner\": true,\n    \"coder\": true,\n    \"architect\": true,\n    \"summarizer\": true,\n    \"builder\": true,\n    \"wholeFileBuilder\": true,\n    \"names\": true,\n    \"commitMessages\": true,\n    \"autoContinue\": true\n  },\n  \"additionalProperties\": false\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/model-pack-roles.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/model-pack-roles.schema.json\",\n  \"title\": \"Model Pack Roles\",\n  \"type\": \"object\",\n  \"description\": \"Config for a model pack's roles\",\n  \"definitions\": {\n    \"roleRef\": {\n      \"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'\",\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        {\n          \"$ref\": \"./model-role-config.schema.json\"\n        }\n      ]\n    }\n  },\n  \"properties\": {\n    \"$schema\": {\n      \"type\": \"string\"\n    },\n    \"localProvider\": {\n      \"description\": \"The local provider for the model pack\",\n      \"$ref\": \"./definitions/local-providers.schema.json\"\n    },\n    \"planner\": {\n      \"description\": \"This is the 'main' role that replies to prompts and makes plans.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"coder\": {\n      \"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.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"architect\": {\n      \"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.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"summarizer\": {\n      \"description\": \"Summarizes conversations to stay under the limit set in the model's 'defaultMaxConvoTokens'.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"builder\": {\n      \"description\": \"Builds the proposed changes described by the `planner` role into pending file updates.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"wholeFileBuilder\": {\n      \"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.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"names\": {\n      \"description\": \"Gives automatically-generated names to plans and context.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"commitMessages\": {\n      \"description\": \"Automatically generates commit messages for a set of pending updates.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"autoContinue\": {\n      \"description\": \"Determines whether a plan is finished or should automatically continue based on the previous response.\",\n      \"$ref\": \"#/definitions/roleRef\"\n    }\n  },\n  \"required\": [\n    \"planner\",\n    \"summarizer\",\n    \"builder\",\n    \"names\",\n    \"commitMessages\",\n    \"autoContinue\"\n  ]\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/model-provider-config.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/model-provider-config.schema.json\",\n  \"title\": \"Model Provider Config\",\n  \"description\": \"Config for a custom model provider\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"The name of the model provider. This is used to reference the model provider in a model's 'providers' array.\"\n    },\n    \"baseUrl\": {\n      \"type\": \"string\",\n      \"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.\"\n    },\n    \"skipAuth\": {\n      \"type\": \"boolean\",\n      \"default\": false,\n      \"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.).\"\n    },\n    \"apiKeyEnvVar\": {\n      \"type\": \"string\",\n      \"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.\"\n    },\n    \"extraAuthVars\": {\n      \"type\": \"array\",\n      \"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.).\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"var\": {\n            \"type\": \"string\",\n            \"description\": \"The name of the environment variable that contains the authentication variable. For example, 'OPENAI_ORG_ID' for OpenAI.\"\n          },\n          \"maybeJSONFilePath\": {\n            \"type\": \"boolean\",\n            \"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.\"\n          },\n          \"required\": {\n            \"type\": \"boolean\",\n            \"description\": \"Whether the variable is required. If set to true, the authentication variable is required. For example, 'true' for OpenAI.\"\n          },\n          \"default\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"var\"\n        ]\n      },\n      \"minItems\": 1\n    }\n  },\n  \"required\": [\n    \"name\",\n    \"baseUrl\"\n  ],\n  \"anyOf\": [\n    {\n      \"required\": [\n        \"apiKeyEnvVar\"\n      ]\n    },\n    {\n      \"required\": [\n        \"extraAuthVars\"\n      ]\n    },\n    {\n      \"required\": [\n        \"skipAuth\"\n      ],\n      \"properties\": {\n        \"skipAuth\": {\n          \"const\": true\n        }\n      }\n    }\n  ],\n  \"additionalProperties\": false\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/model-role-config.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/model-role-config.schema.json\",\n  \"title\": \"Model Role Config\",\n  \"description\": \"Config for a defined model role within a model pack\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"roleRef\": {\n      \"oneOf\": [\n        {\n          \"type\": \"string\",\n          \"minLength\": 1\n        },\n        {\n          \"$ref\": \"#\"\n        }\n      ]\n    }\n  },\n  \"properties\": {\n    \"modelId\": {\n      \"type\": \"string\",\n      \"description\": \"The 'modelId' of the built-in or custom model to use for this role.\"\n    },\n    \"temperature\": {\n      \"type\": \"number\",\n      \"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.\"\n    },\n    \"topP\": {\n      \"type\": \"number\",\n      \"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.\"\n    },\n    \"reservedOutputTokens\": {\n      \"type\": \"number\"\n    },\n    \"maxConvoTokens\": {\n      \"type\": \"number\"\n    },\n    \"largeContextFallback\": {\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"largeOutputFallback\": {\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"errorFallback\": {\n      \"$ref\": \"#/definitions/roleRef\"\n    },\n    \"strongModel\": {\n      \"$ref\": \"#/definitions/roleRef\"\n    }\n  },\n  \"required\": [\n    \"modelId\"\n  ],\n  \"additionalProperties\": false\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/models-input.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/models-input.schema.json\",\n  \"title\": \"Models Input\",\n  \"description\": \"Input schema for custom models, providers, and model packs\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"$schema\": {\n      \"type\": \"string\"\n    },\n    \"models\": {\n      \"type\": \"array\",\n      \"description\": \"Custom models to import into Plandex. They can be referenced by 'modelId' in model packs.\",\n      \"items\": {\n        \"$ref\": \"./model-config.schema.json\"\n      }\n    },\n    \"providers\": {\n      \"type\": \"array\",\n      \"description\": \"Custom model providers to import into Plandex. They can be referenced in a custom model's 'providers' array.\",\n      \"items\": {\n        \"$ref\": \"./model-provider-config.schema.json\"\n      }\n    },\n    \"modelPacks\": {\n      \"type\": \"array\",\n      \"description\": \"Model packs to import into Plandex. These define which models to use for each of Plandex's roles.\",\n      \"items\": {\n        \"$ref\": \"./model-pack-config.schema.json\"\n      }\n    }\n  },\n  \"additionalProperties\": false\n}"
  },
  {
    "path": "app/cli/schema/json-schemas/plan-config.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://plandex.ai/schemas/plan-config.schema.json\",\n  \"title\": \"Plan Config\",\n  \"description\": \"Config for a plan\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"$schema\": {\n      \"type\": \"string\"\n    },\n    \"autoMode\": {\n      \"$ref\": \"./definitions/auto-modes.schema.json\"\n    }\n  },\n  \"additionalProperties\": false\n}"
  },
  {
    "path": "app/cli/schema/schemas.go",
    "content": "package schema\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\tgojsonreference \"github.com/xeipuuv/gojsonreference\"\n\t\"github.com/xeipuuv/gojsonschema\"\n)\n\nconst scheme = \"embed://\"\n\ntype SchemaPath string\n\nconst (\n\tSchemaPathInputConfig     SchemaPath = \"json-schemas/models-input.schema.json\"\n\tSchemaPathPlanConfig      SchemaPath = \"json-schemas/plan-config.schema.json\"\n\tSchemaPathModelPackInline SchemaPath = \"json-schemas/model-pack-inline.schema.json\"\n)\n\n//go:embed json-schemas/*.schema.json json-schemas/definitions/*.schema.json\nvar schemaFS embed.FS\n\ntype embeddedSchemaLoader struct {\n\tsource string\n\tfs     embed.FS\n}\n\nfunc ValidateModelsInputJSON(jsonData []byte) (shared.ClientModelsInput, error) {\n\treturn validateJSON[shared.ClientModelsInput](jsonData, SchemaPathInputConfig)\n}\n\nfunc ValidateModelPackInlineJSON(jsonData []byte) (shared.ClientModelPackSchemaRoles, error) {\n\treturn validateJSON[shared.ClientModelPackSchemaRoles](jsonData, SchemaPathModelPackInline)\n}\n\nfunc validateJSON[T any](jsonData []byte, schemaPath SchemaPath) (T, error) {\n\tvar zero T\n\n\t// strip meta-keywords that break additionalProperties ──\n\tvar tmp interface{}\n\tif err := json.Unmarshal(jsonData, &tmp); err != nil {\n\t\treturn zero, fmt.Errorf(\"invalid json: %w\", err)\n\t}\n\tif obj, ok := tmp.(map[string]interface{}); ok {\n\t\tdelete(obj, \"$schema\") // ignore top-level $schema\n\t\tvar err error\n\t\tjsonData, err = json.Marshal(obj)\n\t\tif err != nil {\n\t\t\treturn zero, fmt.Errorf(\"error marshalling json: %w\", err)\n\t\t}\n\t}\n\n\tschemaLoader := newEmbeddedSchemaLoader(schemaPath)\n\tdocumentLoader := gojsonschema.NewBytesLoader(jsonData)\n\n\tresult, err := gojsonschema.Validate(schemaLoader, documentLoader)\n\tif err != nil {\n\t\treturn zero, err\n\t}\n\tif !result.Valid() {\n\t\tvar msgs []string\n\t\tfor _, d := range result.Errors() {\n\t\t\tmsgs = append(msgs, \"• \"+d.String())\n\t\t}\n\t\treturn zero, errors.New(strings.Join(msgs, \"\\n\"))\n\t}\n\n\tvar v T\n\tif err := json.Unmarshal(jsonData, &v); err != nil {\n\t\treturn zero, fmt.Errorf(\"unmarshal error: %w\", err)\n\t}\n\treturn v, nil\n}\n\nfunc newEmbeddedSchemaLoader(source SchemaPath) *embeddedSchemaLoader {\n\treturn &embeddedSchemaLoader{\n\t\tsource: string(source),\n\t\tfs:     schemaFS,\n\t}\n}\n\nfunc (l *embeddedSchemaLoader) JsonSource() interface{} {\n\treturn l.source\n}\n\nfunc (l *embeddedSchemaLoader) LoadJSON() (interface{}, error) {\n\t// remove both \"./\" and the scheme prefix\n\tsource := strings.TrimPrefix(l.source, \"./\")\n\tsource = strings.TrimPrefix(source, scheme)\n\n\t// convert absolute Plandex URLs to our embed path\n\tconst webPrefix = \"https://plandex.ai/schemas/\"\n\tif strings.HasPrefix(source, webPrefix) {\n\t\tsource = path.Join(\"json-schemas\", strings.TrimPrefix(source, webPrefix))\n\t}\n\n\tif strings.HasSuffix(source, \".schema.json\") {\n\t\tschemaPath := source\n\t\t// for schemas with relative path references, add the json-schemas prefix\n\t\tif !strings.HasPrefix(schemaPath, \"json-schemas/\") {\n\t\t\tschemaPath = path.Join(\"json-schemas\", schemaPath)\n\t\t}\n\t\tdata, err := l.fs.ReadFile(schemaPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading embedded schema %s: %v\", schemaPath, err)\n\t\t}\n\t\tvar v interface{}\n\t\tdec := json.NewDecoder(bytes.NewReader(data))\n\t\tdec.UseNumber() // use numbers instead of floats\n\t\tif err := dec.Decode(&v); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing embedded schema %s: %v\", source, err)\n\t\t}\n\t\treturn v, nil\n\t}\n\n\tvar v interface{}\n\tdec := json.NewDecoder(bytes.NewReader([]byte(source)))\n\tdec.UseNumber() // use numbers instead of floats\n\tif err := dec.Decode(&v); err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing schema JSON: %v\", err)\n\t}\n\treturn v, nil\n}\n\nfunc (l *embeddedSchemaLoader) JsonReference() (gojsonreference.JsonReference, error) {\n\treturn gojsonreference.NewJsonReference(scheme + l.source)\n}\n\ntype embeddedLoaderFactory struct{}\n\nfunc (embeddedLoaderFactory) New(source string) gojsonschema.JSONLoader {\n\tsource = strings.TrimPrefix(source, scheme)\n\treturn newEmbeddedSchemaLoader(SchemaPath(source))\n}\n\nfunc (l *embeddedSchemaLoader) LoaderFactory() gojsonschema.JSONLoaderFactory {\n\treturn embeddedLoaderFactory{}\n}\n"
  },
  {
    "path": "app/cli/stream/stream.go",
    "content": "package stream\n\nimport (\n\t\"log\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/lib\"\n\tstreamtui \"plandex-cli/stream_tui\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/types\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n)\n\nvar OnStreamPlan types.OnStreamPlan\n\nfunc init() {\n\tOnStreamPlan = func(params types.OnStreamPlanParams) {\n\t\tif params.Err != nil {\n\t\t\tif strings.Contains(params.Err.Error(), \"missing heartbeats\") || strings.Contains(strings.ToLower(params.Err.Error()), \"eof\") {\n\t\t\t\tlog.Println(\"Error in stream:\", params.Err)\n\t\t\t\tstreamtui.Send(shared.StreamMessage{\n\t\t\t\t\tType: shared.StreamMessageError,\n\t\t\t\t\tError: &shared.ApiError{\n\t\t\t\t\t\tMsg: \"Stream error: \" + params.Err.Error(),\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\t// try to reconnect\n\t\t\t\tterm.StartSpinner(\"Reconnecting...\")\n\t\t\t\tapiErr := api.Client.ConnectPlan(lib.CurrentPlanId, lib.CurrentBranch, OnStreamPlan)\n\t\t\t\tterm.StopSpinner()\n\n\t\t\t\tif apiErr != nil {\n\t\t\t\t\tlog.Println(\"Error reconnecting to stream:\", apiErr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif params.Msg.Type == shared.StreamMessageStart {\n\t\t\tlog.Println(\"Stream started\")\n\t\t\treturn\n\t\t}\n\n\t\t// log.Println(\"Stream message:\")\n\t\t// log.Println(spew.Sdump(*params.Msg))\n\n\t\tstreamtui.Send(*params.Msg)\n\t}\n}\n"
  },
  {
    "path": "app/cli/stream_tui/debouncer.go",
    "content": "package streamtui\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\n// UpdateDebouncer helps prevent visual glitches from rapid updates\ntype UpdateDebouncer struct {\n\tmu          sync.Mutex\n\tlastUpdate  time.Time\n\tminInterval time.Duration\n\tpending     bool\n}\n\nfunc NewUpdateDebouncer(minInterval time.Duration) *UpdateDebouncer {\n\treturn &UpdateDebouncer{\n\t\tminInterval: minInterval,\n\t}\n}\n\n// ShouldUpdate returns true if enough time has passed since the last update\nfunc (d *UpdateDebouncer) ShouldUpdate() bool {\n\td.mu.Lock()\n\tdefer d.mu.Unlock()\n\n\tnow := time.Now()\n\tif now.Sub(d.lastUpdate) < d.minInterval {\n\t\td.pending = true\n\t\treturn false\n\t}\n\n\td.lastUpdate = now\n\td.pending = false\n\treturn true\n}\n"
  },
  {
    "path": "app/cli/stream_tui/model.go",
    "content": "package streamtui\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\tbubbleKey \"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nconst (\n\tMissingFileLoadLabel      = \"Load the file into context\"\n\tMissingFileSkipLabel      = \"Skip generating this file\"\n\tMissingFileOverwriteLabel = \"Allow Plandex to overwrite this file\"\n)\n\nvar promptChoices = []shared.RespondMissingFileChoice{\n\tshared.RespondMissingFileChoiceLoad,\n\tshared.RespondMissingFileChoiceSkip,\n\tshared.RespondMissingFileChoiceOverwrite,\n}\n\nvar missingFileSelectOpts = []string{\n\tMissingFileLoadLabel,\n\tMissingFileSkipLabel,\n\tMissingFileOverwriteLabel,\n}\n\nvar stateMu sync.RWMutex\n\ntype streamUIModel struct {\n\tbuildOnly   bool\n\tcanSendToBg bool\n\tkeymap      keymap\n\n\treply       string\n\tmainDisplay string\n\n\tmainViewport viewport.Model\n\n\tprocessing   bool\n\tstarting     bool\n\tspinner      spinner.Model\n\tbuildSpinner spinner.Model\n\tsharedTicker *time.Ticker\n\n\tbuilding       bool\n\ttokensByPath   map[string]int\n\tfinishedByPath map[string]bool\n\tremovedByPath  map[string]bool\n\n\tready  bool\n\twidth  int\n\theight int\n\n\tatScrollBottom bool\n\n\tpromptingMissingFile   bool\n\tmissingFilePath        string\n\tmissingFileSelectedIdx int\n\tpromptedMissingFile    bool\n\tautoLoadedMissingFile  bool\n\tmissingFileContent     string\n\tmissingFileTokens      int\n\n\tprompt string\n\n\tstopped    bool\n\tbackground bool\n\tfinished   bool\n\n\terr    error\n\tapiErr *shared.ApiError\n\n\tupdateDebouncer *UpdateDebouncer\n\n\tautoLoadContextCancelFn context.CancelFunc\n\n\tbuildViewCollapsed bool\n\tuserToggledBuild   bool\n}\n\ntype keymap = struct {\n\tstop,\n\tscrollUp,\n\tscrollDown,\n\tpageUp,\n\tpageDown,\n\tstart,\n\tend,\n\tup,\n\tdown,\n\tquit,\n\tbackground,\n\tenter bubbleKey.Binding\n}\n\nfunc (m streamUIModel) Init() tea.Cmd {\n\tlog.Println(\"Model Init start\")\n\tm.mainViewport.MouseWheelEnabled = true\n\treturn tea.Batch(\n\t\tm.Tick(),\n\t\tm.pollBuildStatus(),\n\t)\n}\n\ntype buildStatusPollMsg time.Time\n\nfunc (m streamUIModel) pollBuildStatus() tea.Cmd {\n\treturn tea.Every(5*time.Second, func(t time.Time) tea.Msg {\n\t\treturn buildStatusPollMsg(t)\n\t})\n}\n\nfunc initialModel(prestartReply, prompt string, buildOnly bool, canSendToBg bool) *streamUIModel {\n\tsharedTicker := time.NewTicker(100 * time.Millisecond)\n\n\ts := spinner.New()\n\ts.Spinner = spinner.Points\n\ts.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(\"205\"))\n\n\tbuildSpinner := spinner.New()\n\tbuildSpinner.Spinner = spinner.MiniDot\n\n\tinitialState := streamUIModel{\n\t\tbuildOnly:          buildOnly,\n\t\tcanSendToBg:        canSendToBg,\n\t\tbuildViewCollapsed: false,\n\t\tprompt:             prompt,\n\t\treply:              prestartReply,\n\t\tkeymap: keymap{\n\t\t\tquit: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"ctrl+c\"),\n\t\t\t\tbubbleKey.WithHelp(\"ctrl+c\", \"quit\"),\n\t\t\t),\n\n\t\t\tbackground: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"b\"),\n\t\t\t\tbubbleKey.WithHelp(\"b\", \"background\"),\n\t\t\t),\n\n\t\t\tstop: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"s\"),\n\t\t\t\tbubbleKey.WithHelp(\"s\", \"stop\"),\n\t\t\t),\n\n\t\t\tscrollDown: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"j\"),\n\t\t\t\tbubbleKey.WithHelp(\"j\", \"scroll down\"),\n\t\t\t),\n\n\t\t\tscrollUp: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"k\"),\n\t\t\t\tbubbleKey.WithHelp(\"k\", \"scroll up\"),\n\t\t\t),\n\n\t\t\tpageDown: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"d\", \"pageDown\"),\n\t\t\t\tbubbleKey.WithHelp(\"d\", \"page down\"),\n\t\t\t),\n\n\t\t\tpageUp: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"u\", \"pageUp\"),\n\t\t\t\tbubbleKey.WithHelp(\"u\", \"page up\"),\n\t\t\t),\n\n\t\t\tup: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"up\"),\n\t\t\t\tbubbleKey.WithHelp(\"up\", \"prev\"),\n\t\t\t),\n\n\t\t\tdown: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"down\"),\n\t\t\t\tbubbleKey.WithHelp(\"down\", \"next\"),\n\t\t\t),\n\n\t\t\tenter: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"enter\"),\n\t\t\t\tbubbleKey.WithHelp(\"enter\", \"select\"),\n\t\t\t),\n\n\t\t\tstart: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"g\", \"home\"),\n\t\t\t\tbubbleKey.WithHelp(\"g\", \"start\"),\n\t\t\t),\n\n\t\t\tend: bubbleKey.NewBinding(\n\t\t\t\tbubbleKey.WithKeys(\"G\", \"end\"),\n\t\t\t\tbubbleKey.WithHelp(\"G\", \"end\"),\n\t\t\t),\n\t\t},\n\n\t\ttokensByPath:    make(map[string]int),\n\t\tfinishedByPath:  make(map[string]bool),\n\t\tremovedByPath:   make(map[string]bool),\n\t\tspinner:         s,\n\t\tbuildSpinner:    buildSpinner,\n\t\tsharedTicker:    sharedTicker,\n\t\tatScrollBottom:  true,\n\t\tstarting:        true,\n\t\tupdateDebouncer: NewUpdateDebouncer(8 * time.Millisecond),\n\t}\n\n\treturn &initialState\n}\n\nfunc (m streamUIModel) Tick() tea.Cmd {\n\treturn func() tea.Msg {\n\t\t<-m.sharedTicker.C\n\t\treturn spinner.TickMsg{}\n\t}\n}\n\nfunc (m *streamUIModel) cleanup() {\n\tlog.Println(\"Cleaning up stream UI model\")\n\tm.updateState(func() {\n\t\tm.sharedTicker.Stop()\n\t})\n}\n\nfunc (m *streamUIModel) readState() streamUIModel {\n\tstateMu.RLock()\n\tdefer stateMu.RUnlock()\n\treturn *m\n}\n\nfunc (m *streamUIModel) updateState(updateFn func()) {\n\tstateMu.Lock()\n\tdefer stateMu.Unlock()\n\tupdateFn()\n}\n"
  },
  {
    "path": "app/cli/stream_tui/run.go",
    "content": "package streamtui\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"plandex-cli/term\"\n\t\"sync\"\n\n\tshared \"plandex-shared\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/fatih/color\"\n)\n\nvar ui *tea.Program\nvar mu sync.Mutex\nvar wg sync.WaitGroup\n\nvar prestartReply string\nvar prestartErr *shared.ApiError\nvar prestartAbort bool\n\nfunc StartStreamUI(prompt string, buildOnly, canSendToBg bool) error {\n\tif prestartErr != nil {\n\t\tlog.Println(\"stream UI - prestart error: \", prestartErr)\n\t\tterm.HandleApiError(prestartErr)\n\t}\n\n\tif prestartAbort {\n\t\tfmt.Println(\"🛑 Stopped early\")\n\t\tos.Exit(0)\n\t}\n\n\tlog.Println(\"Starting stream UI\")\n\n\tinitial := initialModel(prestartReply, prompt, buildOnly, canSendToBg)\n\n\tmu.Lock()\n\tui = tea.NewProgram(initial, tea.WithAltScreen())\n\tmu.Unlock()\n\n\tlog.Println(\"Running bubbletea program\")\n\twg.Add(1)\n\tm, err := ui.Run()\n\tlog.Println(\"Bubbletea program finished\")\n\twg.Done()\n\n\tlog.Println(\"Stream UI finished\")\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error running stream UI: %v\", err)\n\t}\n\n\tvar mod *streamUIModel\n\tc, ok := m.(*streamUIModel)\n\tif ok {\n\t\tmod = c\n\t} else {\n\t\tc := m.(streamUIModel)\n\t\tmod = &c\n\t}\n\n\tfmt.Println()\n\n\tif !mod.buildOnly {\n\t\tfmt.Println(mod.mainDisplay)\n\t}\n\n\tif len(mod.finishedByPath) > 0 || len(mod.tokensByPath) > 0 {\n\t\tfmt.Println(mod.renderStaticBuild())\n\t}\n\n\tif mod.err != nil {\n\t\tlog.Println(\"stream UI - error: \", mod.err)\n\n\t\tfmt.Println()\n\t\tterm.OutputErrorAndExit(mod.err.Error())\n\t}\n\n\tif mod.apiErr != nil {\n\t\tlog.Println(\"stream UI - api error: \", mod.apiErr)\n\n\t\tfmt.Println()\n\t\tterm.HandleApiError(mod.apiErr)\n\t}\n\n\tif mod.stopped {\n\t\tfmt.Println()\n\t\tcolor.New(color.BgBlack, color.Bold, color.FgHiRed).Println(\" 🛑 Stopped early \")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"log\", \"rewind\", \"tell\")\n\t\tos.Exit(0)\n\t} else if mod.background {\n\t\tfmt.Println()\n\t\tcolor.New(color.BgBlack, color.Bold, color.FgHiGreen).Println(\" ✅ Plan is active in the background \")\n\t\tfmt.Println()\n\t\tterm.PrintCmds(\"\", \"ps\", \"connect\", \"stop\")\n\t\tos.Exit(0)\n\t}\n\n\tif os.Getenv(\"PLANDEX_REPL\") != \"\" && os.Getenv(\"PLANDEX_REPL_OUTPUT_FILE\") != \"\" {\n\t\t// write output to file\n\t\terr := os.WriteFile(os.Getenv(\"PLANDEX_REPL_OUTPUT_FILE\"), []byte(mod.reply), 0644)\n\t\tif err != nil {\n\t\t\tlog.Println(\"stream UI - error writing output to repl temp file: \", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc Quit() {\n\tif ui == nil {\n\t\tlog.Println(\"stream UI is nil, can't quit\")\n\t\treturn\n\t}\n\tmu.Lock()\n\tif ui != nil {\n\t\tui.Quit()\n\t}\n\tmu.Unlock()\n\n\twg.Wait() // Wait for the UI to fully terminate\n}\n\nfunc Send(msg shared.StreamMessage) {\n\tif ui == nil {\n\t\tlog.Println(\"stream ui is nil\")\n\n\t\tif msg.Type == shared.StreamMessageError {\n\t\t\tprestartErr = msg.Error\n\t\t} else if msg.Type == shared.StreamMessageAborted {\n\n\t\t} else if msg.Type == shared.StreamMessageReply {\n\t\t\tprestartReply += msg.ReplyChunk\n\t\t}\n\t\treturn\n\t}\n\tmu.Lock()\n\tdefer mu.Unlock()\n\tui.Send(msg)\n}\n\nfunc ToggleVisibility(hide bool) {\n\tif ui == nil {\n\t\treturn\n\t}\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\tif hide {\n\t\tui.Send(tea.ExitAltScreen())\n\t} else {\n\t\tui.Send(tea.EnterAltScreen())\n\t}\n}\n"
  },
  {
    "path": "app/cli/stream_tui/update.go",
    "content": "package streamtui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/lib\"\n\t\"plandex-cli/term\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\tbubbleKey \"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/fatih/color\"\n)\n\nfunc (m streamUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\t// log.Println(\"Stream TUI - Update received message:\", spew.Sdump(msg))\n\n\tswitch msg := msg.(type) {\n\n\tcase spinner.TickMsg:\n\t\tstate := m.readState()\n\n\t\tif state.processing || state.starting {\n\t\t\tm.updateState(func() {\n\t\t\t\tspinnerModel, _ := m.spinner.Update(msg)\n\t\t\t\tm.spinner = spinnerModel\n\t\t\t})\n\t\t}\n\t\tif state.building {\n\t\t\tm.updateState(func() {\n\t\t\t\tbuildSpinnerModel, _ := m.buildSpinner.Update(msg)\n\t\t\t\tm.buildSpinner = buildSpinnerModel\n\t\t\t})\n\t\t}\n\t\treturn m, m.Tick()\n\n\tcase tea.WindowSizeMsg:\n\t\tm.windowResized(msg.Width, msg.Height)\n\n\tcase shared.StreamMessage:\n\t\treturn m.streamUpdate(&msg, false)\n\n\tcase contextLoadDoneMsg:\n\t\tif msg.err != nil {\n\t\t\tlog.Println(\"failed to auto load context files:\", msg.err)\n\t\t\tm.updateState(func() {\n\t\t\t\tm.err = msg.err\n\t\t\t\tm.processing = false\n\t\t\t})\n\t\t\treturn m, tea.Quit\n\t\t}\n\n\t\t// We have the loaded content in msg.text\n\t\tm.updateState(func() {\n\t\t\tif msg.text != \"\" {\n\t\t\t\tm.reply += \"\\n\\n\" + msg.text + \"\\n\\n\"\n\t\t\t}\n\t\t\t// and keep processing\n\t\t\tm.processing = true\n\t\t})\n\t\tm.updateReplyDisplay()\n\t\treturn m, m.Tick()\n\n\tcase delayFileRestartMsg:\n\t\tm.updateState(func() {\n\t\t\tm.finishedByPath[msg.path] = false\n\t\t})\n\n\t// Scroll wheel doesn't seem to work--not sure why\n\t// case tea.MouseMsg:\n\t// \tif !m.promptingMissingFile {\n\t// \t\tif msg.Type == tea.MouseWheelUp {\n\t// \t\t\tm.mainViewport.LineUp(3)\n\t// \t\t} else if msg.Type == tea.MouseWheelDown {\n\t// \t\t\tm.mainViewport.LineDown(3)\n\t// \t\t}\n\t// \t}\n\n\tcase tea.KeyMsg:\n\t\tswitch {\n\t\tcase bubbleKey.Matches(msg, m.keymap.stop) || bubbleKey.Matches(msg, m.keymap.quit):\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\n\t\t\tdefer cancel()\n\t\t\tapiErr := api.Client.StopPlan(ctx, lib.CurrentPlanId, lib.CurrentBranch)\n\t\t\tif apiErr != nil {\n\t\t\t\tlog.Println(\"stop plan api error:\", apiErr)\n\t\t\t\tm.updateState(func() {\n\t\t\t\t\tm.apiErr = apiErr\n\t\t\t\t})\n\t\t\t}\n\t\t\tm.updateState(func() {\n\t\t\t\tm.stopped = true\n\t\t\t})\n\t\t\treturn m, tea.Quit\n\n\t\tcase bubbleKey.Matches(msg, m.keymap.background):\n\t\t\tstate := m.readState()\n\t\t\tif state.canSendToBg {\n\t\t\t\tm.updateState(func() {\n\t\t\t\t\tm.background = true\n\t\t\t\t})\n\t\t\t\treturn m, tea.Quit\n\t\t\t}\n\n\t\tcase bubbleKey.Matches(msg, m.keymap.scrollDown) && !m.promptingMissingFile:\n\t\t\tm.scrollDown()\n\t\tcase bubbleKey.Matches(msg, m.keymap.scrollUp) && !m.promptingMissingFile:\n\t\t\tm.scrollUp()\n\t\tcase bubbleKey.Matches(msg, m.keymap.pageDown) && !m.promptingMissingFile:\n\t\t\tm.pageDown()\n\t\tcase bubbleKey.Matches(msg, m.keymap.pageUp) && !m.promptingMissingFile:\n\t\t\tm.pageUp()\n\t\tcase bubbleKey.Matches(msg, m.keymap.up) && m.building:\n\t\t\tm.up()\n\t\tcase bubbleKey.Matches(msg, m.keymap.down) && m.building:\n\t\t\tm.down()\n\t\tcase bubbleKey.Matches(msg, m.keymap.start) && !m.promptingMissingFile:\n\t\t\tm.scrollStart()\n\t\tcase bubbleKey.Matches(msg, m.keymap.end) && !m.promptingMissingFile:\n\t\t\tm.scrollEnd()\n\t\tcase m.promptingMissingFile && bubbleKey.Matches(msg, m.keymap.enter):\n\t\t\treturn m.selectedMissingFileOpt()\n\n\t\tdefault:\n\t\t\tm.resolveEscapeSequence(msg.String())\n\t\t}\n\n\tcase buildStatusPollMsg:\n\t\tstate := m.readState()\n\n\t\tnumPaths := len(m.tokensByPath)\n\t\tnumFinished := 0\n\n\t\tfor _, isBuilt := range m.finishedByPath {\n\t\t\tif isBuilt {\n\t\t\t\tnumFinished++\n\t\t\t}\n\t\t}\n\n\t\tif !state.finished && !state.stopped && !state.background && numPaths > 0 && numPaths != numFinished {\n\t\t\tstatus, apiErr := api.Client.GetBuildStatus(lib.CurrentPlanId, lib.CurrentBranch)\n\t\t\tif apiErr != nil {\n\t\t\t\treturn m, m.pollBuildStatus()\n\t\t\t}\n\n\t\t\tm.updateState(func() {\n\t\t\t\tfor path, isBuilt := range status.BuiltFiles {\n\t\t\t\t\tisBuilding := status.IsBuildingByPath[path]\n\t\t\t\t\tif isBuilt && !isBuilding {\n\t\t\t\t\t\tm.finishedByPath[path] = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t\treturn m, m.pollBuildStatus()\n\t}\n\n\treturn m, nil\n}\n\nfunc (m *streamUIModel) windowResized(w, h int) {\n\tm.updateState(func() {\n\t\tm.width = w\n\t\tm.height = h\n\t})\n\n\tstate := m.readState()\n\n\t_, viewportHeight := state.getViewportDimensions()\n\n\tif state.ready {\n\t\tm.updateViewportDimensions()\n\t} else {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport = viewport.New(w, viewportHeight)\n\t\t\tm.mainViewport.Style = lipgloss.NewStyle().Padding(0, 1, 0, 1)\n\t\t})\n\n\t\tm.updateReplyDisplay()\n\n\t\tm.updateState(func() {\n\t\t\tm.ready = true\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) updateReplyDisplay() {\n\tstate := m.readState()\n\n\tif state.buildOnly {\n\t\treturn\n\t}\n\n\ts := \"\"\n\n\tif state.prompt != \"\" {\n\t\tpromptTxt := term.GetPlain(state.prompt)\n\n\t\ts += color.New(color.BgGreen, color.Bold, color.FgHiWhite).Sprintf(\" 💬 User prompt 👇 \")\n\t\ts += \"\\n\\n\" + strings.TrimSpace(promptTxt) + \"\\n\"\n\t}\n\n\tif state.reply != \"\" {\n\t\treplyMd, _ := term.GetMarkdown(state.reply)\n\t\ts += \"\\n\" + color.New(color.BgBlue, color.Bold, color.FgHiWhite).Sprintf(\" 🤖 Plandex reply 👇 \")\n\t\ts += \"\\n\\n\" + strings.TrimSpace(replyMd)\n\t} else {\n\t\ts += \"\\n\"\n\t}\n\n\tm.updateState(func() {\n\t\tm.mainDisplay = s\n\t\tm.mainViewport.SetContent(s)\n\t})\n\n\tm.updateViewportDimensions()\n\n\tif state.atScrollBottom {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport.GotoBottom()\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) updateViewportDimensions() {\n\tstate := m.readState()\n\tw, h := state.getViewportDimensions()\n\n\tm.updateState(func() {\n\t\tm.mainViewport.Width = w\n\t\tm.mainViewport.Height = h\n\t})\n}\n\nfunc (m *streamUIModel) getViewportDimensions() (int, int) {\n\tw := m.width\n\th := m.height\n\n\thelpHeight := lipgloss.Height(m.renderHelp())\n\n\tvar buildHeight int\n\tif m.building {\n\t\tif m.buildViewCollapsed {\n\t\t\tbuildHeight = 3\n\t\t} else {\n\t\t\tbuildHeight = len(m.getRows(false))\n\t\t}\n\t}\n\n\tvar processingHeight int\n\tif m.starting || m.processing {\n\t\tprocessingHeight = lipgloss.Height(m.renderProcessing())\n\t}\n\n\tmaxViewportHeight := h - (helpHeight + processingHeight + buildHeight)\n\tif maxViewportHeight < 0 {\n\t\tmaxViewportHeight = 0\n\t}\n\tviewportHeight := min(maxViewportHeight, lipgloss.Height(m.mainDisplay))\n\tviewportWidth := w\n\n\treturn viewportWidth, viewportHeight\n}\n\nfunc (m streamUIModel) replyScrollable() bool {\n\treturn m.mainViewport.TotalLineCount() > m.mainViewport.VisibleLineCount()\n}\n\nfunc (m *streamUIModel) scrollDown() {\n\tstate := m.readState()\n\n\tif state.replyScrollable() {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport.LineDown(1)\n\t\t})\n\t}\n\n\tstate = m.readState()\n\n\tm.updateState(func() {\n\t\tm.atScrollBottom = !state.replyScrollable() || state.mainViewport.AtBottom()\n\t})\n}\n\nfunc (m *streamUIModel) scrollUp() {\n\tstate := m.readState()\n\n\tif state.replyScrollable() {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport.LineUp(1)\n\t\t\tm.atScrollBottom = false\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) pageDown() {\n\tstate := m.readState()\n\n\tif state.replyScrollable() {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport.ViewDown()\n\t\t})\n\t}\n\n\tstate = m.readState()\n\n\tm.updateState(func() {\n\t\tm.atScrollBottom = !state.replyScrollable() || state.mainViewport.AtBottom()\n\t})\n}\n\nfunc (m *streamUIModel) pageUp() {\n\tstate := m.readState()\n\n\tif state.replyScrollable() {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport.ViewUp()\n\t\t\tm.atScrollBottom = false\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) scrollStart() {\n\tstate := m.readState()\n\n\tif state.replyScrollable() {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport.GotoTop()\n\t\t\tm.atScrollBottom = false\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) scrollEnd() {\n\tstate := m.readState()\n\n\tif state.replyScrollable() {\n\t\tm.updateState(func() {\n\t\t\tm.mainViewport.GotoBottom()\n\t\t\tm.atScrollBottom = true\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) streamUpdate(msg *shared.StreamMessage, deferUIUpdate bool) (tea.Model, tea.Cmd) {\n\tswitch msg.Type {\n\n\tcase shared.StreamMessageMulti:\n\t\tcmds := []tea.Cmd{}\n\t\tfor _, subMsg := range msg.StreamMessages {\n\t\t\tteaModel, cmd := m.streamUpdate(&subMsg, true)\n\t\t\tm = teaModel.(*streamUIModel)\n\t\t\tif cmd != nil {\n\t\t\t\tcmds = append(cmds, cmd)\n\t\t\t}\n\t\t}\n\n\t\tm.updateReplyDisplay()\n\t\tm.updateViewportDimensions()\n\n\t\treturn m, tea.Batch(cmds...)\n\n\tcase shared.StreamMessageConnectActive:\n\t\tif msg.InitPrompt != \"\" {\n\t\t\tm.updateState(func() {\n\t\t\t\tm.prompt = msg.InitPrompt\n\t\t\t})\n\t\t}\n\t\tif msg.InitBuildOnly {\n\t\t\tm.updateState(func() {\n\t\t\t\tm.buildOnly = true\n\t\t\t})\n\t\t}\n\t\tif len(msg.InitReplies) > 0 {\n\t\t\tm.updateState(func() {\n\t\t\t\tm.reply = strings.Join(msg.InitReplies, \"\\n\\n👇\\n\\n\")\n\t\t\t})\n\t\t}\n\t\tm.updateReplyDisplay()\n\t\treturn m.checkMissingFile(msg)\n\n\tcase shared.StreamMessagePromptMissingFile:\n\t\treturn m.checkMissingFile(msg)\n\n\tcase shared.StreamMessageReply:\n\t\t// ignore empty reply messages\n\t\tif msg.ReplyChunk == \"\" {\n\t\t\treturn m, nil\n\t\t}\n\n\t\tstate := m.readState()\n\n\t\tif state.starting {\n\t\t\tm.updateState(func() {\n\t\t\t\tm.starting = false\n\t\t\t})\n\t\t}\n\n\t\tif state.processing {\n\t\t\tlog.Println(\"Non-empty message reply, setting processing to false\")\n\t\t\tm.updateState(func() {\n\t\t\t\tm.processing = false\n\t\t\t\tif state.promptedMissingFile || state.autoLoadedMissingFile {\n\t\t\t\t\tlog.Println(\"Prompted missing file or auto loaded missing file, resetting (and skipping 👇 marker)\")\n\t\t\t\t\tm.promptedMissingFile = false\n\t\t\t\t\tm.autoLoadedMissingFile = false\n\t\t\t\t} else {\n\t\t\t\t\tlog.Println(\"Not prompted missing file or auto loaded missing file, adding 👇 marker\")\n\t\t\t\t\tm.reply += \"\\n\\n👇\\n\\n\"\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\tm.updateState(func() {\n\t\t\tm.reply += msg.ReplyChunk\n\t\t})\n\n\t\tif !deferUIUpdate {\n\t\t\tm.updateReplyDisplay()\n\t\t}\n\n\tcase shared.StreamMessageBuildInfo:\n\t\tstate := m.readState()\n\n\t\tif state.starting {\n\t\t\tm.updateState(func() {\n\t\t\t\tm.starting = false\n\t\t\t})\n\t\t}\n\n\t\tm.updateState(func() {\n\t\t\tm.building = true\n\t\t})\n\t\twasFinished := state.finishedByPath[msg.BuildInfo.Path]\n\t\tnowFinished := msg.BuildInfo.Finished\n\n\t\tm.updateState(func() {\n\t\t\tif msg.BuildInfo.Removed {\n\t\t\t\tm.removedByPath[msg.BuildInfo.Path] = true\n\t\t\t} else {\n\t\t\t\tm.removedByPath[msg.BuildInfo.Path] = false\n\t\t\t}\n\t\t})\n\n\t\tif msg.BuildInfo.Finished {\n\t\t\tm.updateState(func() {\n\t\t\t\tm.tokensByPath[msg.BuildInfo.Path] = 0\n\t\t\t\tm.finishedByPath[msg.BuildInfo.Path] = true\n\t\t\t})\n\t\t} else {\n\t\t\tif wasFinished && !nowFinished {\n\t\t\t\t// delay for a second before marking not finished again (so check flashes green prior to restarting build)\n\t\t\t\tlog.Println(\"Stream message build info - delaying for 1 second before marking not finished again\")\n\t\t\t\treturn m, startDelay(msg.BuildInfo.Path, time.Second*1)\n\t\t\t} else {\n\t\t\t\tm.updateState(func() {\n\t\t\t\t\tm.finishedByPath[msg.BuildInfo.Path] = false\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tm.updateState(func() {\n\t\t\t\tm.tokensByPath[msg.BuildInfo.Path] += msg.BuildInfo.NumTokens\n\t\t\t})\n\t\t}\n\n\t\t// Auto-collapse if build info takes up too much space\n\t\tstate = m.readState()\n\t\tif !state.userToggledBuild && state.building {\n\t\t\trows := len(m.getRows(false))\n\t\t\tm.updateState(func() {\n\t\t\t\tm.buildViewCollapsed = rows > 3\n\t\t\t})\n\t\t}\n\n\t\tif !deferUIUpdate {\n\t\t\tm.updateViewportDimensions()\n\t\t}\n\n\t\treturn m, m.Tick()\n\n\tcase shared.StreamMessageDescribing:\n\t\tlog.Println(\"Message describing, setting processing to true\")\n\t\tm.updateState(func() {\n\t\t\tm.processing = true\n\t\t})\n\t\treturn m, m.Tick()\n\n\t\t// Instead of blocking here, we'll spawn a command\n\tcase shared.StreamMessageLoadContext:\n\t\tm.updateState(func() {\n\t\t\tm.processing = true\n\t\t})\n\t\treturn m, tea.Batch(\n\t\t\tloadContextCmd(msg.LoadContextFiles),\n\t\t\ttea.Tick(time.Second/10, func(t time.Time) tea.Msg {\n\t\t\t\treturn spinner.TickMsg{}\n\t\t\t}),\n\t\t)\n\n\tcase shared.StreamMessageError:\n\t\tlog.Println(\"Stream message error:\", spew.Sdump(msg))\n\n\t\tstate := m.readState()\n\t\tif state.autoLoadContextCancelFn != nil {\n\t\t\tstate.autoLoadContextCancelFn()\n\t\t}\n\n\t\tm.updateState(func() {\n\t\t\tm.apiErr = msg.Error\n\t\t})\n\t\treturn m, tea.Quit\n\n\tcase shared.StreamMessageFinished:\n\t\tm.updateState(func() {\n\t\t\tm.finished = true\n\t\t})\n\t\treturn m, tea.Quit\n\n\tcase shared.StreamMessageAborted:\n\t\tm.updateState(func() {\n\t\t\tm.stopped = true\n\t\t})\n\t\treturn m, tea.Quit\n\n\tcase shared.StreamMessageRepliesFinished:\n\t\tlog.Println(\"Replies finished, setting processing to false\")\n\t\tstate := m.readState()\n\n\t\tm.updateState(func() {\n\t\t\tm.processing = false\n\t\t})\n\n\t\tif state.building {\n\t\t\treturn m, m.Tick()\n\t\t}\n\t}\n\n\treturn m, nil\n}\n\ntype delayFileRestartMsg struct {\n\tpath string\n}\n\nfunc startDelay(path string, delay time.Duration) tea.Cmd {\n\treturn func() tea.Msg {\n\t\ttime.Sleep(delay)\n\t\treturn delayFileRestartMsg{path: path}\n\t}\n}\n\nvar escReceivedAt time.Time\nvar escSeq string\n\nfunc (m *streamUIModel) resolveEscapeSequence(val string) {\n\tif val == \"esc\" || val == \"alt+[\" {\n\t\tescReceivedAt = time.Now()\n\t\tgo func() {\n\t\t\ttime.Sleep(51 * time.Millisecond)\n\t\t\tescReceivedAt = time.Time{}\n\t\t\tescSeq = \"\"\n\t\t}()\n\t}\n\n\tif !escReceivedAt.IsZero() {\n\t\telapsed := time.Since(escReceivedAt)\n\n\t\tif elapsed < 50*time.Millisecond {\n\t\t\tescSeq += val\n\n\t\t\tif escSeq == \"esc[A\" || escSeq == \"alt+[A\" {\n\t\t\t\tm.up()\n\t\t\t\tescReceivedAt = time.Time{}\n\t\t\t\tescSeq = \"\"\n\t\t\t} else if escSeq == \"esc[B\" || escSeq == \"alt+[B\" {\n\t\t\t\tm.down()\n\t\t\t\tescReceivedAt = time.Time{}\n\t\t\t\tescSeq = \"\"\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (m *streamUIModel) up() {\n\tstate := m.readState()\n\tif state.promptingMissingFile {\n\t\tm.updateState(func() {\n\t\t\tm.missingFileSelectedIdx = max(m.missingFileSelectedIdx-1, 0)\n\t\t})\n\t} else {\n\t\tm.updateState(func() {\n\t\t\tm.buildViewCollapsed = false\n\t\t\tm.userToggledBuild = true\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) down() {\n\tstate := m.readState()\n\tif state.promptingMissingFile {\n\t\tm.updateState(func() {\n\t\t\tm.missingFileSelectedIdx = min(m.missingFileSelectedIdx+1, len(missingFileSelectOpts)-1)\n\t\t})\n\t} else {\n\t\tm.updateState(func() {\n\t\t\tm.buildViewCollapsed = true\n\t\t\tm.userToggledBuild = true\n\t\t})\n\t}\n}\n\nfunc (m *streamUIModel) selectedMissingFileOpt() (tea.Model, tea.Cmd) {\n\tstate := m.readState()\n\tchoice := promptChoices[state.missingFileSelectedIdx]\n\n\tif choice == \"\" {\n\t\treturn m, nil\n\t}\n\n\tapiErr := api.Client.RespondMissingFile(lib.CurrentPlanId, lib.CurrentBranch, shared.RespondMissingFileRequest{\n\t\tChoice:   choice,\n\t\tFilePath: m.missingFilePath,\n\t\tBody:     m.missingFileContent,\n\t})\n\n\tif apiErr != nil {\n\t\tlog.Println(\"missing file prompt api error:\", apiErr)\n\t\tm.updateState(func() {\n\t\t\tm.apiErr = apiErr\n\t\t})\n\t\treturn m, nil\n\t}\n\n\tif choice == shared.RespondMissingFileChoiceSkip {\n\t\treplyLines := strings.Split(state.reply, \"\\n\")\n\t\tm.updateState(func() {\n\t\t\tm.reply = strings.Join(replyLines[:len(replyLines)-3], \"\\n\")\n\t\t})\n\t\tm.updateReplyDisplay()\n\t}\n\n\tm.updateState(func() {\n\t\tm.promptingMissingFile = false\n\t\tm.missingFilePath = \"\"\n\t\tm.missingFileSelectedIdx = 0\n\t\tm.missingFileContent = \"\"\n\t\tm.missingFileTokens = 0\n\t\tm.promptedMissingFile = true\n\t\tm.processing = true\n\t})\n\n\treturn m, func() tea.Msg {\n\t\t<-m.sharedTicker.C\n\t\treturn spinner.TickMsg{}\n\t}\n}\n\nfunc (m *streamUIModel) checkMissingFile(msg *shared.StreamMessage) (tea.Model, tea.Cmd) {\n\tif msg.MissingFilePath != \"\" {\n\t\tlog.Println(\"checkMissingFile - received missing file message | path:\", msg.MissingFilePath)\n\n\t\tif msg.MissingFileAutoContext {\n\t\t\tlog.Println(\"checkMissingFile - received missing file message | auto context\")\n\t\t\tm.updateState(func() {\n\t\t\t\tm.processing = true\n\t\t\t\tm.autoLoadedMissingFile = true\n\t\t\t})\n\n\t\t\treturn m, tea.Batch(\n\t\t\t\tfunc() tea.Msg {\n\t\t\t\t\t<-m.sharedTicker.C\n\t\t\t\t\treturn spinner.TickMsg{}\n\t\t\t\t},\n\t\t\t\tfunc() tea.Msg {\n\t\t\t\t\tbytes, err := os.ReadFile(msg.MissingFilePath)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Println(\"failed to read file:\", err)\n\t\t\t\t\t\tm.err = fmt.Errorf(\"failed to read file: %w\", err)\n\t\t\t\t\t\treturn tea.Quit\n\t\t\t\t\t}\n\t\t\t\t\tcontent := string(shared.NormalizeEOL(bytes))\n\n\t\t\t\t\tlog.Println(\"checkMissingFile - calling RespondMissingFile\")\n\t\t\t\t\tapiErr := api.Client.RespondMissingFile(lib.CurrentPlanId, lib.CurrentBranch, shared.RespondMissingFileRequest{\n\t\t\t\t\t\tChoice:   shared.RespondMissingFileChoiceLoad,\n\t\t\t\t\t\tFilePath: msg.MissingFilePath,\n\t\t\t\t\t\tBody:     content,\n\t\t\t\t\t})\n\n\t\t\t\t\tif apiErr != nil {\n\t\t\t\t\t\tlog.Println(\"missing file prompt api error:\", apiErr)\n\t\t\t\t\t\tm.updateState(func() {\n\t\t\t\t\t\t\tm.apiErr = apiErr\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn tea.Quit\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.Println(\"checkMissingFile - RespondMissingFile success\")\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\tm.updateState(func() {\n\t\t\tm.promptingMissingFile = true\n\t\t\tm.missingFilePath = msg.MissingFilePath\n\t\t})\n\n\t\tbytes, err := os.ReadFile(m.missingFilePath)\n\t\tif err != nil {\n\t\t\tlog.Println(\"failed to read file:\", err)\n\t\t\tm.updateState(func() {\n\t\t\t\tm.err = fmt.Errorf(\"failed to read file: %w\", err)\n\t\t\t})\n\t\t\treturn m, nil\n\t\t}\n\n\t\tmissingFileContent := string(bytes)\n\t\tm.updateState(func() {\n\t\t\tm.missingFileContent = missingFileContent\n\t\t})\n\n\t\tnumTokens := shared.GetNumTokensEstimate(missingFileContent)\n\t\tm.updateState(func() {\n\t\t\tm.missingFileTokens = numTokens\n\t\t})\n\t}\n\n\treturn m, nil\n}\n\n// contextLoadDoneMsg is sent when the long-running AutoLoadContextFiles completes\ntype contextLoadDoneMsg struct {\n\ttext string\n\terr  error\n}\n\nfunc loadContextCmd(loadContextFiles []string) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tdefer cancel()\n\n\t\t// Run the long operation directly\n\t\ttext, err := lib.AutoLoadContextFiles(ctx, loadContextFiles)\n\n\t\t// Return the result as a message\n\t\treturn contextLoadDoneMsg{\n\t\t\ttext: text,\n\t\t\terr:  err,\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/cli/stream_tui/view.go",
    "content": "package streamtui\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"plandex-cli/term\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/fatih/color\"\n)\n\nvar borderColor = lipgloss.Color(\"#444\")\nvar helpTextColor = lipgloss.Color(\"#ddd\")\n\nfunc (m streamUIModel) View() string {\n\n\tif m.promptingMissingFile {\n\t\treturn m.renderMissingFilePrompt()\n\t}\n\n\tviews := []string{}\n\tif !m.buildOnly {\n\t\tviews = append(views, m.renderMainView())\n\t}\n\tif m.processing || m.starting {\n\t\tviews = append(views, m.renderProcessing())\n\t}\n\tif m.building {\n\t\tviews = append(views, m.renderBuild())\n\t}\n\tviews = append(views, m.renderHelp())\n\n\treturn lipgloss.JoinVertical(lipgloss.Left, views...)\n}\n\nfunc (m streamUIModel) renderMainView() string {\n\treturn m.mainViewport.View()\n}\n\nfunc (m streamUIModel) renderHelp() string {\n\tstyle := lipgloss.NewStyle().Width(m.width).Foreground(lipgloss.Color(helpTextColor)).BorderStyle(lipgloss.NormalBorder()).BorderTop(true).BorderForeground(lipgloss.Color(borderColor))\n\n\tif m.buildOnly {\n\t\ts := \" (s)top\"\n\t\tif m.canSendToBg {\n\t\t\ts += \" • (b)ackground\"\n\t\t}\n\t\treturn style.Render(s)\n\t} else {\n\t\ts := \" (s)top\"\n\t\tif m.canSendToBg {\n\t\t\ts += \" • (b)ackground\"\n\t\t}\n\t\ts += \" • (j/k) scroll • (d/u) page • (g/G) start/end\"\n\t\treturn style.Render(s)\n\t}\n}\n\nfunc (m streamUIModel) renderProcessing() string {\n\tif m.starting || m.processing {\n\t\treturn \"\\n \" + m.spinner.View()\n\t} else {\n\t\treturn \"\"\n\t}\n}\n\nfunc (m streamUIModel) renderBuild() string {\n\treturn m.doRenderBuild(false)\n}\n\nfunc (m streamUIModel) renderStaticBuild() string {\n\treturn m.doRenderBuild(true)\n}\n\nfunc (m streamUIModel) doRenderBuild(outputStatic bool) string {\n\tif !m.building && !outputStatic {\n\t\treturn \"\"\n\t}\n\n\tif outputStatic && len(m.finishedByPath) == 0 && len(m.tokensByPath) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar style lipgloss.Style\n\tif m.buildOnly {\n\t\tstyle = lipgloss.NewStyle().Width(m.width)\n\t} else {\n\t\tstyle = lipgloss.NewStyle().Width(m.width).BorderStyle(lipgloss.NormalBorder()).BorderTop(true).BorderForeground(lipgloss.Color(borderColor))\n\t}\n\n\tif !outputStatic && m.buildViewCollapsed {\n\t\t// Render collapsed view\n\t\tinProgress := 0\n\t\ttotal := len(m.tokensByPath)\n\t\tfor path := range m.tokensByPath {\n\t\t\tif path == \"_apply.sh\" {\n\t\t\t\ttotal--\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !m.finishedByPath[path] {\n\t\t\t\tinProgress++\n\t\t\t}\n\t\t}\n\n\t\t_, hasApplyScript := m.tokensByPath[\"_apply.sh\"]\n\t\tapplyScriptFinished := m.finishedByPath[\"_apply.sh\"]\n\n\t\tlbl := \"file\"\n\t\tif total > 1 {\n\t\t\tlbl = \"files\"\n\t\t}\n\n\t\tvar summary string\n\t\tif total > 0 {\n\t\t\tsummary = fmt.Sprintf(\" 📄 %d %s\", total, lbl)\n\t\t}\n\t\tif inProgress > 0 {\n\t\t\tsummary += fmt.Sprintf(\" • 📝 editing %d %s\", inProgress, m.buildSpinner.View())\n\t\t}\n\t\tif hasApplyScript {\n\t\t\tif total > 0 {\n\t\t\t\tsummary += \" •\"\n\t\t\t}\n\t\t\tif applyScriptFinished {\n\t\t\t\tsummary += \" 🚀 wrote commands\"\n\t\t\t} else {\n\t\t\t\tsummary += fmt.Sprintf(\" 🚀 editing commands %s\", m.buildSpinner.View())\n\t\t\t}\n\t\t}\n\t\thead := m.getBuildHeader(outputStatic)\n\t\treturn style.Render(lipgloss.JoinVertical(lipgloss.Left, head, summary))\n\t}\n\n\tresRows := m.getRows(outputStatic)\n\n\tres := style.Render(strings.Join(resRows, \"\\n\"))\n\n\treturn res\n}\n\nfunc (m streamUIModel) didBuild() bool {\n\treturn !(m.stopped || m.err != nil || m.apiErr != nil)\n}\n\nfunc (m streamUIModel) getBuildHeader(static bool) string {\n\tlbl := \"Building plan \"\n\tbgColor := color.BgGreen\n\tif static {\n\t\tif !m.didBuild() {\n\t\t\tlbl = \"Build incomplete \"\n\t\t\tbgColor = color.BgRed\n\t\t} else {\n\t\t\tlbl = \"Built plan \"\n\t\t}\n\t}\n\n\thead := color.New(bgColor, color.FgHiWhite, color.Bold).Sprint(\" 🏗  \") + color.New(bgColor, color.FgHiWhite).Sprint(lbl)\n\n\t// Add collapse/expand hint\n\tvar hint string\n\tif !static {\n\t\thint = \"(↓) collapse\"\n\t\tif m.buildViewCollapsed {\n\t\t\thint = \"(↑) expand\"\n\t\t}\n\t}\n\tpadding := m.width - lipgloss.Width(head) - lipgloss.Width(hint) - 1 // 1 for space\n\tif padding > 0 {\n\t\thead += strings.Repeat(\" \", padding) + hint\n\t}\n\n\treturn head\n}\n\nfunc (m streamUIModel) getRows(static bool) []string {\n\tbuilt := m.didBuild() && static\n\thead := m.getBuildHeader(static)\n\n\t// Gather file paths, _apply.sh last\n\tfilePaths := make([]string, 0, len(m.tokensByPath))\n\tfor filePath := range m.tokensByPath {\n\t\tif filePath == \"_apply.sh\" {\n\t\t\tcontinue\n\t\t}\n\t\tfilePaths = append(filePaths, filePath)\n\t}\n\tsort.Strings(filePaths)\n\tif _, ok := m.tokensByPath[\"_apply.sh\"]; ok {\n\t\tfilePaths = append(filePaths, \"_apply.sh\")\n\t}\n\n\tvar rows [][]string\n\tlineWidth := 0\n\tlineNum := -1\n\trowIdx := 0\n\n\tfor _, filePath := range filePaths {\n\t\ttokens := m.tokensByPath[filePath]\n\t\tfinished := m.finished || m.finishedByPath[filePath] || built\n\t\tremoved := m.removedByPath[filePath]\n\n\t\t// Basic block label\n\t\ticon := \"📄\"\n\t\tlabel := filePath\n\t\tif filePath == \"_apply.sh\" {\n\t\t\ticon = \"🚀\"\n\t\t\tlabel = \"commands\"\n\t\t}\n\t\tblock := fmt.Sprintf(\"%s %s\", icon, label)\n\n\t\t// Mark removed/finished/tokens\n\t\tswitch {\n\t\tcase removed:\n\t\t\tblock += \" ❌\"\n\t\tcase finished:\n\t\t\tblock += \" ✅\"\n\t\tcase tokens > 0:\n\t\t\tblock += fmt.Sprintf(\" %d 🪙\", tokens)\n\t\tdefault:\n\t\t\tblock += \" \" + m.buildSpinner.View()\n\t\t}\n\n\t\t// Truncate if needed\n\t\tblockWidth := lipgloss.Width(block)\n\t\tif blockWidth > m.width {\n\t\t\tmaxWidth := m.width - lipgloss.Width(\"⋯\")\n\t\t\tif maxWidth < 4 {\n\t\t\t\tblock = string([]rune(block)[0:1]) + \"⋯\"\n\t\t\t} else {\n\t\t\t\thalf := maxWidth / 2\n\t\t\t\trunes := []rune(block)\n\t\t\t\tblock = string(runes[:half]) + \"⋯\" + string(runes[len(runes)-half:])\n\t\t\t}\n\t\t}\n\n\t\t// Build the \"prefix + block\" text tentatively:\n\t\tprefix := \"\"\n\t\tif rowIdx > 0 {\n\t\t\tprefix = \" | \"\n\t\t}\n\t\tcandidate := prefix + block\n\t\tcandidateWidth := lipgloss.Width(candidate)\n\n\t\t// Check if we have no row or it won't fit with the prefix\n\t\tif lineNum == -1 || lineWidth+candidateWidth > m.width {\n\t\t\t// Start a new row\n\t\t\trows = append(rows, []string{})\n\t\t\tlineNum++\n\t\t\trowIdx = 0\n\t\t\tlineWidth = 0\n\n\t\t\t// In a new row, there's no prefix\n\t\t\tcandidate = block\n\t\t\tcandidateWidth = lipgloss.Width(candidate)\n\t\t}\n\n\t\trows[lineNum] = append(rows[lineNum], candidate)\n\t\tlineWidth += candidateWidth\n\t\trowIdx++\n\t}\n\n\t// If empty row left at the end, strip it\n\tif len(rows) > 0 && len(rows[len(rows)-1]) == 0 {\n\t\trows = rows[:len(rows)-1]\n\t}\n\n\t// Final output lines\n\tresRows := make([]string, len(rows)+1)\n\tresRows[0] = head\n\tfor i, row := range rows {\n\t\tresRows[i+1] = lipgloss.JoinHorizontal(lipgloss.Left, row...)\n\t}\n\n\treturn resRows\n}\n\nfunc (m streamUIModel) renderMissingFilePrompt() string {\n\tstyle := lipgloss.NewStyle().Padding(1).BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(borderColor)).Width(m.width - 2).Height(m.height - 2)\n\n\tprompt := \"📄 \" + color.New(color.Bold, term.ColorHiYellow).Sprint(m.missingFilePath) + \" isn't in context.\"\n\n\tprompt += \"\\n\\n\"\n\n\tdesc := \"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.\"\n\n\twords := strings.Split(desc, \" \")\n\tfor i, word := range words {\n\t\twords[i] = color.New(color.FgWhite).Sprint(word)\n\t}\n\n\tprompt += strings.Join(words, \" \")\n\n\tprompt += \"\\n\\n\" + color.New(term.ColorHiMagenta, color.Bold).Sprintln(\"🧐 What do you want to do?\")\n\n\tfor i, opt := range missingFileSelectOpts {\n\t\tif i == m.missingFileSelectedIdx {\n\t\t\tprompt += color.New(term.ColorHiCyan, color.Bold).Sprint(\" > \" + opt)\n\t\t} else {\n\t\t\tprompt += \"   \" + opt\n\t\t}\n\n\t\tif opt == MissingFileLoadLabel {\n\t\t\tprompt += fmt.Sprintf(\" | %d 🪙\", m.missingFileTokens)\n\t\t}\n\n\t\tprompt += \"\\n\"\n\t}\n\n\treturn style.Render(prompt)\n}\n"
  },
  {
    "path": "app/cli/term/color.go",
    "content": "package term\n\nimport (\n\t\"github.com/fatih/color\"\n\t\"github.com/muesli/termenv\"\n)\n\nvar IsDarkBg = termenv.HasDarkBackground()\n\nvar ColorHiGreen color.Attribute\nvar ColorHiMagenta color.Attribute\nvar ColorHiRed color.Attribute\nvar ColorHiYellow color.Attribute\nvar ColorHiCyan color.Attribute\nvar ColorHiBlue color.Attribute\n\nfunc init() {\n\n\tif IsDarkBg {\n\t\tColorHiGreen = color.FgHiGreen\n\t\tColorHiMagenta = color.FgHiMagenta\n\t\tColorHiRed = color.FgHiRed\n\t\tColorHiYellow = color.FgHiYellow\n\t\tColorHiCyan = color.FgHiCyan\n\t\tColorHiBlue = color.FgHiBlue\n\t} else {\n\t\tColorHiGreen = color.FgGreen\n\t\tColorHiMagenta = color.FgMagenta\n\t\tColorHiRed = color.FgRed\n\t\tColorHiYellow = color.FgYellow\n\t\tColorHiCyan = color.FgCyan\n\t\tColorHiBlue = color.FgBlue\n\t}\n}\n"
  },
  {
    "path": "app/cli/term/errors.go",
    "content": "package term\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar openUnauthenticatedCloudURL func(msg, path string)\nvar openAuthenticatedURL func(msg, path string)\nvar convertTrial func()\n\nfunc SetOpenUnauthenticatedCloudURLFn(fn func(msg, path string)) {\n\topenUnauthenticatedCloudURL = fn\n}\nfunc SetOpenAuthenticatedURLFn(fn func(msg, path string)) {\n\topenAuthenticatedURL = fn\n}\nfunc SetConvertTrialFn(fn func()) {\n\tconvertTrial = fn\n}\n\nfunc OutputSimpleError(msg string, args ...interface{}) {\n\tmsg = fmt.Sprintf(msg, args...)\n\tfmt.Fprintln(os.Stderr, color.New(ColorHiRed, color.Bold).Sprint(\"🚨 \"+shared.Capitalize(msg)))\n}\n\nfunc OutputErrorAndExit(msg string, args ...interface{}) {\n\tStopSpinner()\n\n\tmsg = fmt.Sprintf(msg, args...)\n\n\tmsg = strings.ReplaceAll(msg, \"status code:\", \"status code\")\n\tmsg = strings.ReplaceAll(msg, \", body:\", \":\")\n\n\tdisplayMsg := \"\"\n\terrorParts := strings.Split(msg, \": \")\n\n\taddedErrors := map[string]bool{}\n\n\tif len(errorParts) > 1 {\n\t\tvar lastPart string\n\t\ti := 0\n\t\tfor idx, part := range errorParts {\n\t\t\t// don't repeat the same error message\n\t\t\tif _, ok := addedErrors[strings.ToLower(part)]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttail := strings.Join(errorParts[idx:], \": \")\n\t\t\tif maybeJSON(tail) {\n\t\t\t\tprettyJSON := prettyJSON(tail)\n\t\t\t\tindent := strings.Repeat(\"  \", i)\n\n\t\t\t\t// prepend indent to **each** line in the pretty JSON\n\t\t\t\tindentedJSON := strings.ReplaceAll(prettyJSON, \"\\n\", \"\\n\"+indent+\"  \")\n\n\t\t\t\t// now write the block\n\t\t\t\tdisplayMsg += \"\\n\" + indent + \"→ \" + indentedJSON\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif len(lastPart) < 10 && i > 0 {\n\t\t\t\tlastPart = lastPart + \": \" + part\n\t\t\t\tdisplayMsg += \": \" + part\n\t\t\t\taddedErrors[strings.ToLower(lastPart)] = true\n\t\t\t\taddedErrors[strings.ToLower(part)] = true\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif i != 0 {\n\t\t\t\tdisplayMsg += \"\\n\"\n\t\t\t}\n\n\t\t\t// indent the error message\n\t\t\tfor n := 0; n < i; n++ {\n\t\t\t\tdisplayMsg += \"  \"\n\t\t\t}\n\t\t\tif i > 0 {\n\t\t\t\tdisplayMsg += \"→ \"\n\t\t\t}\n\n\t\t\ts := shared.Capitalize(part)\n\t\t\tif i == 0 {\n\t\t\t\ts = color.New(ColorHiRed, color.Bold).Sprint(\"🚨 \" + s)\n\t\t\t}\n\n\t\t\tdisplayMsg += s\n\n\t\t\taddedErrors[strings.ToLower(part)] = true\n\t\t\tlastPart = part\n\t\t\ti++\n\t\t}\n\t} else {\n\t\tdisplayMsg = color.New(ColorHiRed, color.Bold).Sprint(\"🚨 \" + msg)\n\t}\n\n\tfmt.Fprintln(os.Stderr, color.New(ColorHiRed, color.Bold).Sprint(displayMsg))\n\tos.Exit(1)\n}\n\nfunc OutputUnformattedErrorAndExit(msg string) {\n\tStopSpinner()\n\tfmt.Fprintln(os.Stderr, msg)\n\tos.Exit(1)\n}\n\nfunc OutputNoCurrentPlanErrorAndExit() {\n\tfmt.Println(\"🤷‍♂️ No current plan\")\n\tfmt.Println()\n\tPrintCmds(\"\", \"new\", \"cd\")\n\tos.Exit(1)\n}\n\nfunc HandleApiError(apiError *shared.ApiError) {\n\tif apiError.Type == shared.ApiErrorTypeCloudSubscriptionPaused {\n\t\tif apiError.BillingError.HasBillingPermission {\n\t\t\tStopSpinner()\n\t\t\tfmt.Println(\"Your org's Plandex Cloud subscription is paused.\")\n\t\t\tres, err := ConfirmYesNo(\"Go to billing settings?\")\n\t\t\tif err != nil {\n\t\t\t\tOutputErrorAndExit(\"error getting confirmation\")\n\t\t\t}\n\t\t\tif res {\n\t\t\t\topenAuthenticatedURL(\"Opening billing settings in your browser.\", \"/settings/billing\")\n\t\t\t\tos.Exit(0)\n\t\t\t} else {\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t} else {\n\t\t\tOutputErrorAndExit(\"Your org's subscription is paused. Please contact an org owner to continue.\")\n\t\t}\n\t}\n\n\tif apiError.Type == shared.ApiErrorTypeCloudSubscriptionOverdue {\n\t\tif apiError.BillingError.HasBillingPermission {\n\t\t\tStopSpinner()\n\t\t\tOutputSimpleError(\"Your org's Plandex Cloud subscription is overdue.\")\n\t\t\tres, err := ConfirmYesNo(\"Go to billing settings?\")\n\t\t\tif err != nil {\n\t\t\t\tOutputErrorAndExit(\"error getting confirmation\")\n\t\t\t}\n\t\t\tif res {\n\t\t\t\topenAuthenticatedURL(\"Opening billing settings in your browser.\", \"/settings/billing\")\n\t\t\t\tos.Exit(0)\n\t\t\t} else {\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t} else {\n\t\t\tOutputErrorAndExit(\"Your org's subscription is overdue. Please contact an org owner to continue.\")\n\t\t}\n\t}\n\n\tif apiError.Type == shared.ApiErrorTypeCloudMonthlyMaxReached {\n\t\tif apiError.BillingError.HasBillingPermission {\n\t\t\tStopSpinner()\n\t\t\tOutputSimpleError(\"Your org has reached its monthly limit for Plandex Cloud.\")\n\t\t\tres, err := ConfirmYesNo(\"Go to billing settings?\")\n\t\t\tif err != nil {\n\t\t\t\tOutputErrorAndExit(\"error getting confirmation\")\n\t\t\t}\n\t\t\tif res {\n\t\t\t\topenAuthenticatedURL(\"Opening billing settings in your browser.\", \"/settings/billing\")\n\t\t\t\tos.Exit(0)\n\t\t\t} else {\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t} else {\n\t\t\tOutputErrorAndExit(\"Your org has reached its monthly limit for Plandex Cloud.\")\n\t\t}\n\t}\n\n\tif apiError.Type == shared.ApiErrorTypeCloudInsufficientCredits {\n\t\tif apiError.BillingError.HasBillingPermission {\n\t\t\tStopSpinner()\n\t\t\tOutputSimpleError(\"Insufficient credits\")\n\t\t\tres, err := ConfirmYesNo(\"Go to billing settings?\")\n\t\t\tif err != nil {\n\t\t\t\tOutputErrorAndExit(\"error getting confirmation\")\n\t\t\t}\n\t\t\tif res {\n\t\t\t\topenAuthenticatedURL(\"Opening billing settings in your browser.\", \"/settings/billing\")\n\t\t\t\tos.Exit(0)\n\t\t\t} else {\n\t\t\t\tos.Exit(0)\n\t\t\t}\n\t\t} else {\n\t\t\tOutputErrorAndExit(\"Insufficient credits\")\n\t\t}\n\t}\n\n\tif apiError.Type == shared.ApiErrorTypeTrialMessagesExceeded {\n\t\tStopSpinner()\n\t\tfmt.Fprintf(os.Stderr, \"\\n🚨 You've reached the Plandex Cloud trial limit of %d messages per plan\\n\", apiError.TrialMessagesExceededError.MaxReplies)\n\n\t\tres, err := ConfirmYesNo(\"Upgrade now?\")\n\n\t\tif err != nil {\n\t\t\tOutputErrorAndExit(\"Error prompting upgrade trial: %v\", err)\n\t\t}\n\n\t\tif res {\n\t\t\tconvertTrial()\n\t\t\tPrintCmds(\"\", \"continue\")\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\tStopSpinner()\n\tOutputErrorAndExit(apiError.Msg)\n}\n\nfunc maybeJSON(s string) bool {\n\ts = strings.TrimSpace(s)\n\tif strings.HasPrefix(s, \"{\") && strings.HasSuffix(s, \"}\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"[\") && strings.HasSuffix(s, \"]\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc prettyJSON(s string) string {\n\tvar v any\n\tif err := json.Unmarshal([]byte(s), &v); err != nil {\n\t\treturn s // not JSON\n\t}\n\tout, _ := json.MarshalIndent(v, \"\", \"  \")\n\treturn string(out)\n}\n"
  },
  {
    "path": "app/cli/term/format.go",
    "content": "package term\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/charmbracelet/glow/utils\"\n\t\"github.com/muesli/reflow/wordwrap\"\n\t\"github.com/muesli/termenv\"\n)\n\nfunc init() {\n\t// pre-cache the glamour renderer\n\tgetGlamourRenderer()\n}\n\nvar (\n\tcachedGlamourRenderer      *glamour.TermRenderer\n\tcachedGlamourRendererWidth int\n)\n\nfunc getGlamourRenderer() (*glamour.TermRenderer, error) {\n\twidth := GetTerminalWidth()\n\n\tif cachedGlamourRenderer != nil && cachedGlamourRendererWidth == width {\n\t\treturn cachedGlamourRenderer, nil\n\t}\n\n\t// Build the renderer options.\n\tvar opts []glamour.TermRendererOption\n\n\t// Check for a GLAMOUR_STYLE env variable.\n\tif style, ok := os.LookupEnv(\"GLAMOUR_STYLE\"); ok && style != \"\" {\n\t\topts = append(opts, glamour.WithStandardStyle(style))\n\t} else {\n\t\t// Fallback to auto style detection.\n\t\topts = append(opts, glamour.WithAutoStyle())\n\t}\n\n\t// Always set word wrap and preserved newlines.\n\topts = append(opts,\n\t\tglamour.WithWordWrap(min(width, 80)),\n\t\tglamour.WithPreservedNewLines(),\n\t)\n\n\tr, err := glamour.NewTermRenderer(opts...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create glamour renderer: %w\", err)\n\t}\n\n\tcachedGlamourRenderer = r\n\tcachedGlamourRendererWidth = width\n\treturn r, nil\n}\n\nfunc GetMarkdown(input string) (string, error) {\n\tinputBytes := utils.RemoveFrontmatter([]byte(input))\n\n\tr, err := getGlamourRenderer()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tout, err := r.RenderBytes(inputBytes)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(out), nil\n}\n\nfunc GetPlain(input string) string {\n\twidth := GetTerminalWidth()\n\n\ts := wordwrap.String(input, min(width-2, 80))\n\t// add padding\n\tlines := strings.Split(s, \"\\n\")\n\t// for i := range lines {\n\t// \tlines[i] = \"  \" + lines[i]\n\t// }\n\ts = strings.Join(lines, \"\\n\")\n\n\ts = termenv.String(s).Foreground(GetStreamForegroundColor()).String()\n\n\treturn s\n}\n"
  },
  {
    "path": "app/cli/term/help.go",
    "content": "package term\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n)\n\ntype CmdConfig struct {\n\tCmd   string\n\tAlias string\n\tDesc  string\n\tRepl  bool\n}\n\nvar CliCommands = []CmdConfig{\n\t{\"\", \"\", \"start the Plandex REPL\", false},\n\n\t// {\"--full\", \"\", fmt.Sprintf(\"start the Plandex REPL with auto-mode %s\", \"'full'\"), false},\n\t// {\"--semi\", \"\", fmt.Sprintf(\"start the Plandex REPL with auto-mode %s\", \"'semi'\"), false},\n\t// {\"--plus\", \"\", fmt.Sprintf(\"start the Plandex REPL with auto-mode %s\", \"'plus'\"), false},\n\t// {\"--basic\", \"\", fmt.Sprintf(\"start the Plandex REPL with auto-mode %s\", \"'basic'\"), false},\n\t// {\"--none\", \"\", fmt.Sprintf(\"start the Plandex REPL with auto-mode %s\", \"'none'\"), false},\n\n\t// {\"--daily\", \"\", fmt.Sprintf(\"start the Plandex REPL with %s model pack\", \"'daily-driver'\"), false},\n\t// {\"--strong\", \"\", fmt.Sprintf(\"start the Plandex REPL with %s model pack\", \"'strong'\"), false},\n\t// {\"--cheap\", \"\", fmt.Sprintf(\"start the Plandex REPL with %s model pack\", \"'cheap'\"), false},\n\t// {\"--oss\", \"\", fmt.Sprintf(\"start the Plandex REPL with %s model pack\", \"'oss'\"), false},\n\n\t{\"new\", \"\", \"start a new plan\", true},\n\n\t{\"new --full\", \"\", fmt.Sprintf(\"start a new plan with auto-mode %s\", \"'full'\"), true},\n\t{\"new --semi\", \"\", fmt.Sprintf(\"start a new plan with auto-mode %s\", \"'semi'\"), true},\n\t{\"new --plus\", \"\", fmt.Sprintf(\"start a new plan with auto-mode %s\", \"'plus'\"), true},\n\t{\"new --basic\", \"\", fmt.Sprintf(\"start a new plan with auto-mode %s\", \"'basic'\"), true},\n\t{\"new --none\", \"\", fmt.Sprintf(\"start a new plan with auto-mode %s\", \"'none'\"), true},\n\n\t{\"new --daily\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'daily-driver'\"), true},\n\t{\"new --reasoning\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'reasoning'\"), true},\n\t{\"new --strong\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'strong'\"), true},\n\t{\"new --cheap\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'cheap'\"), true},\n\t{\"new --oss\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'oss'\"), true},\n\t{\"new --gemini-planner\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'gemini-planner'\"), true},\n\t{\"new --o3-planner\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'o3-planner'\"), true},\n\t{\"new --r1-planner\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'r1-planner'\"), true},\n\t{\"new --perplexity-planner\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'perplexity-planner'\"), true},\n\t{\"new --opus-planner\", \"\", fmt.Sprintf(\"start a new plan with %s model pack\", \"'opus-planner'\"), true},\n\n\t{\"plans\", \"pl\", \"list plans\", true},\n\t{\"cd\", \"\", \"set current plan by name or index\", true},\n\t{\"current\", \"cu\", \"show current plan\", true},\n\t{\"rename\", \"\", \"rename the current plan\", true},\n\t{\"delete-plan\", \"dp\", \"delete plan by name or index\", true},\n\n\t{\"config\", \"\", \"show current plan config\", true},\n\t{\"set-config\", \"\", \"update current plan config\", true},\n\t{\"config default\", \"\", \"show the default config for new plans\", true},\n\t{\"set-config default\", \"\", \"update the default config for new plans\", true},\n\n\t{\"set-auto\", \"\", \"update auto-mode (autonomy level) for current plan\", true},\n\t{\"set-auto none\", \"\", fmt.Sprintf(\"set auto-mode to %s\", \"'none'\"), true},\n\t{\"set-auto basic\", \"\", fmt.Sprintf(\"set auto-mode to %s\", \"'basic'\"), true},\n\t{\"set-auto plus\", \"\", fmt.Sprintf(\"set auto-mode to %s\", \"'plus'\"), true},\n\t{\"set-auto semi\", \"\", fmt.Sprintf(\"set auto-mode to %s\", \"'semi'\"), true},\n\t{\"set-auto full\", \"\", fmt.Sprintf(\"set auto-mode to %s\", \"'full'\"), true},\n\n\t{\"set-auto default\", \"\", \"set the default auto-mode for new plans\", true},\n\n\t{\"tell\", \"t\", \"describe a task to complete\", false},\n\t{\"chat\", \"ch\", \"ask a question or chat\", false},\n\n\t{\"load\", \"l\", \"load files/dirs/urls/notes/images or pipe data into context\", true},\n\t{\"ls\", \"\", \"list everything in context\", true},\n\t{\"rm\", \"\", \"remove context by index, range, name, or glob\", true},\n\t{\"clear\", \"\", \"remove all context\", true},\n\t{\"update\", \"u\", \"update outdated context\", true},\n\t{\"show\", \"\", \"show current context by name or index\", true},\n\n\t{\"diff --ui\", \"\", \"review pending changes in a browser UI\", true},\n\t{\"diff\", \"\", \"review pending changes in 'git diff' format\", true},\n\t{\"diff --plain\", \"\", \"review pending changes in 'git diff' format with no color formatting\", false},\n\t{\"summary\", \"\", \"show the latest summary of the current plan\", true},\n\n\t{\"apply\", \"ap\", \"apply pending changes to project files\", true},\n\t{\"reject\", \"rj\", \"reject pending changes to one or more project files\", true},\n\n\t{\"log\", \"\", \"show log of plan updates\", true},\n\t{\"rewind\", \"rw\", \"rewind to a previous state\", true},\n\n\t{\"continue\", \"c\", \"continue the plan\", true},\n\t{\"debug\", \"db\", \"repeatedly run a command and auto-apply fixes until it succeeds\", true},\n\t{\"build\", \"b\", \"build any pending changes\", true},\n\n\t{\"convo\", \"\", \"show plan conversation\", true},\n\t{\"convo 1\", \"\", \"show a specific message in the conversation\", false},\n\t{\"convo 2-5\", \"\", \"show a range of messages in the conversation\", false},\n\t{\"convo --plain\", \"\", \"show conversation in plain text\", false},\n\n\t{\"branches\", \"br\", \"list plan branches\", true},\n\t{\"checkout\", \"co\", \"checkout or create a branch\", true},\n\t{\"delete-branch\", \"dlb\", \"delete a branch by name or index\", true},\n\n\t{\"plans --archived\", \"\", \"list archived plans\", true},\n\t{\"archive\", \"arc\", \"archive a plan\", true},\n\t{\"unarchive\", \"unarc\", \"unarchive a plan\", true},\n\n\t{\"models\", \"\", \"show current plan model settings\", true},\n\t{\"models default\", \"\", \"show the default model settings for new plans\", true},\n\n\t{\"models available\", \"\", \"show all available models\", true},\n\t{\"models available --custom\", \"\", \"show available custom models only\", true},\n\n\t{\"models custom\", \"\", \"manage custom models, providers, and model packs\", true},\n\n\t{\"providers\", \"\", \"show all available model providers\", true},\n\t{\"providers --custom\", \"\", \"show available custom model providers only\", true},\n\n\t{\"model-packs\", \"\", \"show all available model packs\", true},\n\n\t{\"model-packs --custom\", \"\", \"show custom model packs only\", true},\n\t{\"model-packs show\", \"\", \"show a built-in or custom model pack's settings\", true},\n\n\t{\"set-model\", \"\", \"update current plan model settings\", true},\n\t{\"set-model default\", \"\", \"update the default model settings for new plans\", true},\n\n\t{\"set-model daily\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'daily-driver'\"), true},\n\t{\"set-model reasoning\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'reasoning'\"), true},\n\t{\"set-model strong\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'strong'\"), true},\n\t{\"set-model cheap\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'cheap'\"), true},\n\t{\"set-model oss\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'oss'\"), true},\n\t{\"set-model gemini-planner\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'gemini-planner'\"), true},\n\t{\"set-model o3-planner\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'o3-planner'\"), true},\n\t{\"set-model r1-planner\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'r1-planner'\"), true},\n\t{\"set-model perplexity-planner\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'perplexity-planner'\"), true},\n\t{\"set-model opus-planner\", \"\", fmt.Sprintf(\"Use %s model pack\", \"'opus-planner'\"), true},\n\n\t{\"ps\", \"\", \"list active and recently finished plan streams\", true},\n\t{\"stop\", \"\", \"stop an active plan stream\", true},\n\t{\"connect\", \"conn\", \"connect to an active plan stream\", true},\n\n\t{\"sign-in\", \"\", \"sign in, accept an invite, or create an account\", true},\n\t{\"invite\", \"\", \"invite a user to join your org\", true},\n\t{\"revoke\", \"\", \"revoke an invite or remove a user from your org\", true},\n\t{\"users\", \"\", \"list users and pending invites in your org\", true},\n\n\t{\"connect-claude\", \"\", \"connect your Claude Pro or Max subscription\", true},\n\t{\"disconnect-claude\", \"\", \"disconnect your Claude Pro or Max subscription\", true},\n\t{\"claude-status\", \"\", \"status of your Claude Pro or Max subscription connection\", true},\n\n\t{\"usage\", \"\", \"show Plandex Cloud current balance and usage report\", true},\n\t{\"usage --today\", \"\", \"show Plandex Cloud usage for the day so far\", true},\n\t{\"usage --month\", \"\", \"show Plandex Cloud usage for the current billing month\", true},\n\t{\"usage --plan\", \"\", \"show Plandex Cloud usage for the current plan\", true},\n\n\t{\"usage --log\", \"\", \"show Plandex Cloud transaction log\", true},\n\n\t{\"billing\", \"\", \"show Plandex Cloud billing settings\", true},\n}\n\nvar CmdDesc = map[string]CmdConfig{}\n\nfunc init() {\n\tfor _, cmd := range CliCommands {\n\t\tCmdDesc[cmd.Cmd] = cmd\n\t}\n}\n\nfunc PrintCmds(prefix string, cmds ...string) {\n\tprintCmds(os.Stderr, prefix, []color.Attribute{color.Bold, color.FgHiWhite, color.BgCyan, color.FgHiWhite}, cmds...)\n}\n\nfunc PrintCmdsWithColors(prefix string, colors []color.Attribute, cmds ...string) {\n\tprintCmds(os.Stderr, prefix, colors, cmds...)\n}\n\nfunc printCmds(w io.Writer, prefix string, colors []color.Attribute, cmds ...string) {\n\tif os.Getenv(\"PLANDEX_DISABLE_SUGGESTIONS\") != \"\" {\n\t\treturn\n\t}\n\n\tfor _, cmd := range cmds {\n\t\tconfig, ok := CmdDesc[cmd]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif IsRepl && !config.Repl {\n\t\t\tcontinue\n\t\t}\n\n\t\talias := config.Alias\n\t\tdesc := config.Desc\n\n\t\tif alias != \"\" {\n\t\t\tif IsRepl {\n\t\t\t\tcmd = fmt.Sprintf(\"%s (\\\\%s)\", cmd, alias)\n\t\t\t} else {\n\t\t\t\tcontainsFull := strings.Contains(cmd, alias)\n\n\t\t\t\tif containsFull {\n\t\t\t\t\tcmd = strings.Replace(cmd, alias, fmt.Sprintf(\"(%s)\", alias), 1)\n\t\t\t\t} else {\n\t\t\t\t\tcmd = fmt.Sprintf(\"%s (%s)\", cmd, alias)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// desc += color.New(color.FgWhite).Sprintf(\" • alias → %s\", fmt.Sprint(a'lias'))\n\t\t}\n\n\t\tvar styled string\n\t\tif IsRepl {\n\t\t\tstyled = color.New(colors...).Sprintf(\" \\\\%s \", cmd)\n\t\t} else if cmd == \"\" { // special case for the repl\n\t\t\tstyled = color.New(colors...).Sprintf(\" plandex \")\n\t\t} else {\n\t\t\tstyled = color.New(colors...).Sprintf(\" plandex %s \", cmd)\n\t\t}\n\n\t\tfmt.Fprintf(w, \"%s%s 👉 %s\\n\", prefix, styled, desc)\n\t}\n\n}\n\nfunc PrintCustomCmd(prefix, cmd, alias, desc string) {\n\tcmd = strings.Replace(cmd, alias, fmt.Sprintf(\"(%s)\", alias), 1)\n\t// desc += color.New(color.FgWhite).Sprintf(\" • alias → %s\", fmt.Sprint(a'lias'))\n\tstyled := color.New(color.Bold, color.FgHiWhite, color.BgCyan, color.FgHiWhite).Sprintf(\" plandex %s \", cmd)\n\tfmt.Printf(\"%s%s 👉 %s\\n\", prefix, styled, desc)\n}\n\n// PrintCustomHelp prints the custom help output for the Plandex CLI\nfunc PrintCustomHelp(all bool) {\n\tbuilder := &strings.Builder{}\n\n\tcolor.New(color.Bold, color.BgGreen, color.FgHiWhite).Fprintln(builder, \" Usage \")\n\tcolor.New(color.Bold).Fprintln(builder, \"  plandex [command] [flags]\")\n\tcolor.New(color.Bold).Fprintln(builder, \"  pdx [command] [flags]\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgGreen, color.FgHiWhite).Fprintln(builder, \" Help \")\n\tcolor.New(color.Bold).Fprintln(builder, \"  plandex help # show basic usage\")\n\tcolor.New(color.Bold).Fprintln(builder, \"  plandex help --all # show all commands\")\n\tcolor.New(color.Bold).Fprintln(builder, \"  plandex [command] --help\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgMagenta, color.FgHiWhite).Fprintln(builder, \" Getting Started \")\n\tfmt.Fprintln(builder)\n\tfmt.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 \"))\n\tfmt.Fprintln(builder)\n\tfmt.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] \"))\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgMagenta, color.FgHiWhite).Fprintln(builder, \" REPL Options \")\n\tfmt.Fprintln(builder)\n\t// Add REPL startup flags\n\tfmt.Fprintln(builder, color.New(color.Bold, color.FgHiBlue).Sprint(\"  Mode \"))\n\tfmt.Fprintln(builder, \"    --chat, -c     Start in chat mode (for conversation without making changes)\")\n\tfmt.Fprintln(builder, \"    --tell, -t     Start in tell mode (for implementation)\")\n\tfmt.Fprintln(builder)\n\n\tfmt.Fprintln(builder, color.New(color.Bold, color.FgHiBlue).Sprint(\"  Autonomy \"))\n\tfmt.Fprintln(builder, \"    --no-auto      None → step-by-step, no automation\")\n\tfmt.Fprintln(builder, \"    --basic        Basic → auto-continue plans\")\n\tfmt.Fprintln(builder, \"    --plus         Plus → auto-update context, smart context, auto-commit changes\")\n\tfmt.Fprintln(builder, \"    --semi         Semi-Auto → auto-load context\")\n\tfmt.Fprintln(builder, \"    --full         Full-Auto → auto-apply, auto-exec, auto-debug\")\n\tfmt.Fprintln(builder)\n\n\tfmt.Fprintln(builder, color.New(color.Bold, color.FgHiBlue).Sprint(\"  Models \"))\n\tfmt.Fprintln(builder, \"    --daily        Daily driver pack\")\n\tfmt.Fprintln(builder, \"    --reasoning    Reasoning pack\")\n\tfmt.Fprintln(builder, \"    --strong       Strong pack\")\n\tfmt.Fprintln(builder, \"    --cheap        Cheap pack\")\n\tfmt.Fprintln(builder, \"    --oss          Open source pack\")\n\n\tfmt.Fprintln(builder)\n\n\tif all {\n\t\tfmt.Print(builder.String())\n\t\tPrintHelpAllCommands()\n\t} else {\n\t\tfmt.Print(builder.String())\n\t\t// in the same style as 'getting started' section, output See All Commands\n\n\t\tcolor.New(color.Bold, color.BgHiBlue, color.FgHiWhite).Fprintln(builder, \" Use 'plandex help --all' or 'plandex help -a' for a list of all commands \")\n\t\tfmt.Fprintln(builder)\n\n\t\tfmt.Print(builder.String())\n\t}\n\n}\n\nfunc PrintHelpAllCommands() {\n\tbuilder := &strings.Builder{}\n\n\tcolor.New(color.Bold, color.BgMagenta, color.FgHiWhite).Fprintln(builder, \" Key Commands \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, color.FgHiMagenta}, \"new\", \"load\", \"tell\", \"diff\", \"diff --ui\", \"apply\", \"reject\", \"debug\", \"chat\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Plans \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"new\", \"plans\", \"cd\", \"current\", \"delete-plan\", \"rename\", \"archive\", \"plans --archived\", \"unarchive\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Changes \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"diff\", \"diff --ui\", \"diff --plain\", \"apply\", \"reject\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Context \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"load\", \"ls\", \"rm\", \"update\", \"clear\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Branches \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"branches\", \"checkout\", \"delete-branch\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" History \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"log\", \"rewind\", \"convo\", \"convo 1\", \"convo 2-5\", \"convo --plain\", \"summary\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Control \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"tell\", \"continue\", \"build\", \"debug\", \"chat\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Streams \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"ps\", \"connect\", \"stop\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Config \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"config\", \"set-config\", \"config default\", \"set-config default\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Autonomy \")\n\tprintCmds(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\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" AI Models \")\n\tprintCmds(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\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Custom Models \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan},\n\t\t\"models custom\", \"models available\", \"models available --custom\", \"providers\", \"providers --custom\", \"model-packs\", \"model-packs --custom\", \"model-packs show\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Accounts \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"sign-in\", \"invite\", \"revoke\", \"users\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Integrations \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"connect-claude\", \"disconnect-claude\", \"claude-status\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" Cloud \")\n\tprintCmds(builder, \" \", []color.Attribute{color.Bold, ColorHiCyan}, \"usage\", \"usage --today\", \"usage --month\", \"usage --plan\", \"usage --log\", \"billing\")\n\tfmt.Fprintln(builder)\n\n\tcolor.New(color.Bold, color.BgCyan, color.FgHiWhite).Fprintln(builder, \" New Plan Shortcuts \")\n\tprintCmds(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\")\n\tfmt.Fprintln(builder)\n\n\tfmt.Print(builder.String())\n}\n\nfunc ShowCmd(cmd string) string {\n\tif IsRepl {\n\t\tcmd = fmt.Sprintf(\"\\\\%s\", cmd)\n\t} else {\n\t\tcmd = fmt.Sprintf(\"plandex %s\", cmd)\n\t}\n\n\treturn color.New(color.Bold, color.BgCyan, color.FgHiWhite).Sprintf(\" %s \", cmd)\n}\n"
  },
  {
    "path": "app/cli/term/os.go",
    "content": "package term\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n)\n\nfunc GetOsDetails() string {\n\treturn fmt.Sprintf(\n\t\t\"OS: %s\\nArchitecture: %s\\nCPUs: %d\\nShell: %s\",\n\t\truntime.GOOS,\n\t\truntime.GOARCH,\n\t\truntime.NumCPU(),\n\t\tos.Getenv(\"SHELL\"),\n\t)\n}\n"
  },
  {
    "path": "app/cli/term/prompt.go",
    "content": "package term\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/cqroot/prompt\"\n\t\"github.com/cqroot/prompt/input\"\n\t\"github.com/eiannone/keyboard\"\n\t\"github.com/fatih/color\"\n)\n\nfunc GetRequiredUserStringInput(msg string) (string, error) {\n\tres, err := GetUserStringInput(msg)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get user input: %s\", err)\n\t}\n\n\tif res == \"\" {\n\t\tcolor.New(color.Bold, ColorHiRed).Println(\"🚨 This input is required\")\n\t\treturn GetRequiredUserStringInput(msg)\n\t}\n\n\treturn res, nil\n}\n\nfunc GetUserStringInput(msg string) (string, error) {\n\treturn GetUserStringInputWithDefault(msg, \"\")\n}\n\nfunc GetUserStringInputWithDefault(msg, def string) (string, error) {\n\tdisableBracketedPaste()\n\tdefer enableBracketedPaste()\n\n\tres, err := prompt.New().Ask(msg).Input(def)\n\n\tif err != nil && err.Error() == \"user quit prompt\" {\n\t\tos.Exit(0)\n\t}\n\n\treturn res, err\n}\n\nfunc GetRequiredUserStringInputWithDefault(msg, def string) (string, error) {\n\tres, err := GetUserStringInputWithDefault(msg, def)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get user input: %s\", err)\n\t}\n\n\tif res == \"\" {\n\t\tcolor.New(color.Bold, ColorHiRed).Println(\"🚨 This input is required\")\n\t\treturn GetRequiredUserStringInputWithDefault(msg, def)\n\t}\n\n\treturn res, nil\n}\n\nfunc GetUserPasswordInput(msg string) (string, error) {\n\tdisableBracketedPaste()\n\tdefer enableBracketedPaste()\n\n\tres, err := prompt.New().Ask(msg).Input(\"\", input.WithEchoMode(input.EchoPassword))\n\n\tif err != nil && err.Error() == \"user quit prompt\" {\n\t\tos.Exit(0)\n\t}\n\n\treturn res, err\n}\n\nfunc GetUserKeyInput() (rune, keyboard.Key, error) {\n\tif err := keyboard.Open(); err != nil {\n\t\treturn 0, 0, fmt.Errorf(\"failed to open keyboard: %s\", err)\n\t}\n\tdefer func() {\n\t\t_ = keyboard.Close()\n\t}()\n\n\tchar, key, err := keyboard.GetKey()\n\tif err != nil {\n\t\treturn 0, 0, fmt.Errorf(\"failed to read keypress: %s\", err)\n\t}\n\n\treturn char, key, nil\n}\n\nfunc ConfirmYesNo(fmtStr string, fmtArgs ...interface{}) (bool, error) {\n\tcolor.New(ColorHiMagenta, color.Bold).Printf(fmtStr+\" (y)es | (n)o\", fmtArgs...)\n\tcolor.New(ColorHiMagenta, color.Bold).Print(\"> \")\n\n\tchar, key, err := GetUserKeyInput()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to get user input: %s\", err)\n\t}\n\n\t// ctrl+c == no\n\tif key == keyboard.KeyCtrlC {\n\t\treturn false, nil\n\t}\n\n\tfmt.Println(string(char))\n\tif char == 'y' || char == 'Y' {\n\t\treturn true, nil\n\t} else if char == 'n' || char == 'N' {\n\t\treturn false, nil\n\t} else {\n\t\tfmt.Println()\n\t\tcolor.New(ColorHiRed, color.Bold).Print(\"Invalid input.\\nEnter 'y' for yes or 'n' for no.\\n\\n\")\n\t\treturn ConfirmYesNo(fmtStr, fmtArgs...)\n\t}\n}\n\nfunc ConfirmYesNoCancel(fmtStr string, fmtArgs ...interface{}) (bool, bool, error) {\n\tcolor.New(ColorHiMagenta, color.Bold).Printf(fmtStr+\" (y)es | (n)o | (c)ancel\", fmtArgs...)\n\tcolor.New(ColorHiMagenta, color.Bold).Print(\"> \")\n\n\tchar, key, err := GetUserKeyInput()\n\tif err != nil {\n\t\treturn false, false, fmt.Errorf(\"failed to get user input: %s\", err)\n\t}\n\n\t// ctrl+c == cancel\n\tif key == keyboard.KeyCtrlC {\n\t\treturn false, true, nil\n\t}\n\n\tfmt.Println(string(char))\n\tif char == 'y' || char == 'Y' {\n\t\treturn true, false, nil\n\t} else if char == 'n' || char == 'N' {\n\t\treturn false, false, nil\n\t} else if char == 'c' || char == 'C' {\n\t\treturn false, true, nil\n\t} else {\n\t\tfmt.Println()\n\t\tcolor.New(ColorHiRed, color.Bold).Print(\"Invalid input.\\nEnter 'y' for yes, 'n' for no, or 'c' for cancel.\\n\\n\")\n\t\treturn ConfirmYesNoCancel(fmtStr, fmtArgs...)\n\t}\n}\n\nfunc disableBracketedPaste() {\n\tfmt.Print(\"\\033[?2004l\")\n}\n\nfunc enableBracketedPaste() {\n\tfmt.Print(\"\\033[?2004h\")\n}\n"
  },
  {
    "path": "app/cli/term/repl.go",
    "content": "package term\n\nimport \"os\"\n\nvar IsRepl = os.Getenv(\"PLANDEX_REPL\") != \"\"\n\nfunc SetIsRepl(value bool) {\n\tIsRepl = value\n}\n"
  },
  {
    "path": "app/cli/term/select.go",
    "content": "package term\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/plandex-ai/survey/v2\"\n)\n\nfunc SelectFromList(msg string, options []string) (string, error) {\n\tvar selected string\n\tprompt := &survey.Select{\n\t\tMessage:       color.New(ColorHiMagenta, color.Bold).Sprint(msg),\n\t\tOptions:       convertToStringSlice(options),\n\t\tFilterMessage: \"\",\n\t}\n\terr := survey.AskOne(prompt, &selected)\n\tif err != nil {\n\t\tif err.Error() == \"interrupt\" {\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\treturn \"\", err\n\t}\n\n\treturn selected, nil\n}\n\nfunc convertToStringSlice[T any](input []T) []string {\n\tvar result []string\n\tfor _, v := range input {\n\t\tresult = append(result, fmt.Sprint(v))\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "app/cli/term/spinner.go",
    "content": "package term\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/briandowns/spinner\"\n)\n\nconst withMessageMinDuration = 700 * time.Millisecond\nconst withoutMessageMinDuration = 350 * time.Millisecond\n\nvar s = spinner.New(spinner.CharSets[33], 100*time.Millisecond)\nvar startedAt time.Time\n\nvar lastMessage string\nvar active bool\nvar currentWarningLoop int32\n\nfunc StartSpinner(msg string) {\n\tif active {\n\t\tif msg == lastMessage {\n\t\t\treturn\n\t\t}\n\n\t\ts.Stop()\n\t}\n\n\tstartedAt = time.Now()\n\ts.Prefix = msg + \" \"\n\tlastMessage = msg\n\ts.Start()\n\tactive = true\n}\n\nfunc StopSpinner() {\n\telapsed := time.Since(startedAt)\n\n\tif lastMessage != \"\" && elapsed < withMessageMinDuration {\n\t\ttime.Sleep(withMessageMinDuration - elapsed)\n\t} else if elapsed < withoutMessageMinDuration {\n\t\ttime.Sleep(withoutMessageMinDuration - elapsed)\n\t}\n\n\ts.Stop()\n\tClearCurrentLine()\n\n\tactive = false\n}\n\nfunc ResumeSpinner() {\n\tif !active {\n\t\tStartSpinner(lastMessage)\n\t}\n}\n\nfunc LongSpinnerWithWarning(msg, warning string) {\n\tatomic.AddInt32(&currentWarningLoop, 1)\n\tcurrentLoop := currentWarningLoop\n\n\tStartSpinner(msg)\n\n\tvar flashWarning func()\n\tflashWarning = func() {\n\t\tgo func() {\n\t\t\ttime.Sleep(3 * time.Second)\n\t\t\tif !active || atomic.LoadInt32(&currentWarningLoop) != currentLoop {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tStartSpinner(warning)\n\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t\tif !active || atomic.LoadInt32(&currentWarningLoop) != currentLoop {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tStartSpinner(msg)\n\t\t\tflashWarning()\n\t\t}()\n\t}\n\tflashWarning()\n}\n"
  },
  {
    "path": "app/cli/term/utils.go",
    "content": "package term\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/muesli/termenv\"\n\t\"golang.org/x/term\"\n)\n\nfunc init() {\n\t// pre-cache terminal settings\n\tIsTerminal()\n\tGetTerminalWidth()\n\tGetStreamForegroundColor()\n\tHasDarkBackground()\n}\n\nfunc AlternateScreen() {\n\t// Switch to alternate screen and hide the cursor\n\tfmt.Print(\"\\x1b[?1049h\\x1b[?25l\")\n}\n\nfunc ClearScreen() {\n\tfmt.Print(\"\\x1b[2J\")\n}\n\nfunc MoveCursorToTopLeft() {\n\tfmt.Print(\"\\x1b[H\")\n}\n\nfunc ClearCurrentLine() {\n\tfmt.Print(\"\\033[2K\")\n}\n\nfunc MoveUpLines(numLines int) {\n\tfmt.Printf(\"\\033[%dA\", numLines)\n}\n\nfunc BackToMain() {\n\t// Switch back to main screen and show the cursor on exit\n\tfmt.Print(\"\\x1b[?1049l\\x1b[?25h\")\n}\n\nfunc PageOutput(output string) {\n\tcmd := exec.Command(\"less\", \"-R\")\n\tcmd.Env = append(os.Environ(), \"LESS=FRX\", \"LESSCHARSET=utf-8\")\n\tcmd.Stdin = strings.NewReader(output)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\tOutputErrorAndExit(\"Failed to page output: %v\", err)\n\t}\n}\n\nfunc PageOutputReverse(output string) {\n\tcmd := exec.Command(\"less\", \"-RX\", \"+G\")\n\tcmd.Stdin = strings.NewReader(output)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\t// Set the environment variables specifically for the less command\n\tcmd.Env = append(os.Environ(), \"LESS=FRX\", \"LESSCHARSET=utf-8\")\n\n\tif err := cmd.Run(); err != nil {\n\t\tOutputErrorAndExit(\"Failed to page output: %v\", err)\n\t}\n}\n\nfunc GetDivisionLine() string {\n\t// Get the terminal width\n\tterminalWidth := GetTerminalWidth()\n\treturn strings.Repeat(\"─\", terminalWidth)\n}\n\nvar envReplCols int\nvar envDefaultCols int\n\nfunc GetTerminalWidth() int {\n\tif envReplCols != 0 {\n\t\treturn envReplCols\n\t}\n\n\tif os.Getenv(\"PLANDEX_COLUMNS\") != \"\" {\n\t\tw, err := strconv.Atoi(os.Getenv(\"PLANDEX_COLUMNS\"))\n\t\tif err == nil {\n\t\t\tenvReplCols = w\n\t\t\treturn w\n\t\t}\n\t}\n\n\tif IsTerminal() {\n\t\t// Try to get terminal size\n\t\tif w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {\n\t\t\treturn w\n\t\t}\n\t}\n\n\tif envDefaultCols != 0 {\n\t\treturn envDefaultCols\n\t}\n\n\t// Not running in a TTY or GetSize failed; use a default.\n\t// Try to get width from environment variable\n\tif w, err := strconv.Atoi(os.Getenv(\"COLUMNS\")); err == nil {\n\t\tenvDefaultCols = w\n\t\treturn w\n\t}\n\n\t// Fallback to default width\n\treturn 80\n}\n\nvar envStreamForegroundColor termenv.Color\n\nfunc GetStreamForegroundColor() termenv.Color {\n\tif envStreamForegroundColor != nil {\n\t\treturn envStreamForegroundColor\n\t}\n\n\tif os.Getenv(\"PLANDEX_STREAM_FOREGROUND_COLOR\") != \"\" {\n\t\tenvStreamForegroundColor = termenv.ANSI256.Color(os.Getenv(\"PLANDEX_STREAM_FOREGROUND_COLOR\"))\n\t\treturn envStreamForegroundColor\n\t}\n\n\tc := \"234\"\n\tif HasDarkBackground() {\n\t\tc = \"251\"\n\t}\n\tenvStreamForegroundColor = termenv.ANSI256.Color(c)\n\treturn envStreamForegroundColor\n}\n\nvar envHasDarkBackground bool\nvar cachedHasDarkBackground bool\n\nfunc HasDarkBackground() bool {\n\tif cachedHasDarkBackground {\n\t\treturn envHasDarkBackground\n\t}\n\tenvHasDarkBackground = termenv.HasDarkBackground()\n\tcachedHasDarkBackground = true\n\treturn envHasDarkBackground\n}\n\nvar envIsTerminal bool\nvar cachedIsTerminal bool\n\nfunc IsTerminal() bool {\n\tif cachedIsTerminal {\n\t\treturn envIsTerminal\n\t}\n\tenvIsTerminal = term.IsTerminal(int(os.Stdout.Fd()))\n\tcachedIsTerminal = true\n\treturn envIsTerminal\n}\n"
  },
  {
    "path": "app/cli/types/api.go",
    "content": "package types\n\nimport (\n\t\"context\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/shopspring/decimal\"\n)\n\ntype OnStreamPlanParams struct {\n\tMsg *shared.StreamMessage\n\tErr error\n}\n\ntype OnStreamPlan func(params OnStreamPlanParams)\n\ntype ApiClient interface {\n\tCreateCliTrialSession() (string, *shared.ApiError)\n\tGetCliTrialSession(token string) (*shared.SessionResponse, *shared.ApiError)\n\n\tCreateEmailVerification(email, customHost, userId string) (*shared.CreateEmailVerificationResponse, *shared.ApiError)\n\n\tCreateSignInCode() (string, *shared.ApiError)\n\n\tCreateAccount(req shared.CreateAccountRequest, customHost string) (*shared.SessionResponse, *shared.ApiError)\n\tSignIn(req shared.SignInRequest, customHost string) (*shared.SessionResponse, *shared.ApiError)\n\n\tSignOut() *shared.ApiError\n\n\tGetOrgSession() (*shared.Org, *shared.ApiError)\n\tListOrgs() ([]*shared.Org, *shared.ApiError)\n\tCreateOrg(req shared.CreateOrgRequest) (*shared.CreateOrgResponse, *shared.ApiError)\n\n\tGetOrgUserConfig() (*shared.OrgUserConfig, *shared.ApiError)\n\tUpdateOrgUserConfig(req shared.OrgUserConfig) *shared.ApiError\n\n\tListUsers() (*shared.ListUsersResponse, *shared.ApiError)\n\tDeleteUser(userId string) *shared.ApiError\n\n\tListOrgRoles() ([]*shared.OrgRole, *shared.ApiError)\n\n\tInviteUser(req shared.InviteRequest) *shared.ApiError\n\tListPendingInvites() ([]*shared.Invite, *shared.ApiError)\n\tListAcceptedInvites() ([]*shared.Invite, *shared.ApiError)\n\tListAllInvites() ([]*shared.Invite, *shared.ApiError)\n\tDeleteInvite(inviteId string) *shared.ApiError\n\n\tCreateProject(req shared.CreateProjectRequest) (*shared.CreateProjectResponse, *shared.ApiError)\n\tListProjects() ([]*shared.Project, *shared.ApiError)\n\tSetProjectPlan(projectId string, req shared.SetProjectPlanRequest) *shared.ApiError\n\tRenameProject(projectId string, req shared.RenameProjectRequest) *shared.ApiError\n\n\tListPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError)\n\tListArchivedPlans(projectIds []string) ([]*shared.Plan, *shared.ApiError)\n\tListPlansRunning(projectIds []string, includeRecent bool) (*shared.ListPlansRunningResponse, *shared.ApiError)\n\n\tGetCurrentBranchByPlanId(projectId string, req shared.GetCurrentBranchByPlanIdRequest) (map[string]*shared.Branch, *shared.ApiError)\n\n\tGetPlan(planId string) (*shared.Plan, *shared.ApiError)\n\tCreatePlan(projectId string, req shared.CreatePlanRequest) (*shared.CreatePlanResponse, *shared.ApiError)\n\n\tTellPlan(planId, branch string, req shared.TellPlanRequest, onStreamPlan OnStreamPlan) *shared.ApiError\n\tBuildPlan(planId, branch string, req shared.BuildPlanRequest, onStreamPlan OnStreamPlan) *shared.ApiError\n\tRespondMissingFile(planId, branch string, req shared.RespondMissingFileRequest) *shared.ApiError\n\n\tDeletePlan(planId string) *shared.ApiError\n\tDeleteAllPlans(projectId string) *shared.ApiError\n\tConnectPlan(planId, branch string, onStreamPlan OnStreamPlan) *shared.ApiError\n\tStopPlan(ctx context.Context, planId, branch string) *shared.ApiError\n\n\tArchivePlan(planId string) *shared.ApiError\n\tUnarchivePlan(planId string) *shared.ApiError\n\tRenamePlan(planId string, name string) *shared.ApiError\n\n\tGetCurrentPlanState(planId, branch string) (*shared.CurrentPlanState, *shared.ApiError)\n\tGetCurrentPlanStateAtSha(planId, sha string) (*shared.CurrentPlanState, *shared.ApiError)\n\tApplyPlan(planId, branch string, req shared.ApplyPlanRequest) (string, *shared.ApiError)\n\tRejectAllChanges(planId, branch string) *shared.ApiError\n\tRejectFile(planId, branch, filePath string) *shared.ApiError\n\tRejectFiles(planId, branch string, paths []string) *shared.ApiError\n\tGetPlanDiffs(planId, branch string, plain bool) (string, *shared.ApiError)\n\n\tLoadContext(planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError)\n\tUpdateContext(planId, branch string, req shared.UpdateContextRequest) (*shared.UpdateContextResponse, *shared.ApiError)\n\tDeleteContext(planId, branch string, req shared.DeleteContextRequest) (*shared.DeleteContextResponse, *shared.ApiError)\n\tListContext(planId, branch string) ([]*shared.Context, *shared.ApiError)\n\tLoadCachedFileMap(planId, branch string, req shared.LoadCachedFileMapRequest) (*shared.LoadCachedFileMapResponse, *shared.ApiError)\n\n\tListConvo(planId, branch string) ([]*shared.ConvoMessage, *shared.ApiError)\n\tGetPlanStatus(planId, branch string) (string, *shared.ApiError)\n\tListLogs(planId, branch string) (*shared.LogResponse, *shared.ApiError)\n\tRewindPlan(planId, branch string, req shared.RewindPlanRequest) (*shared.RewindPlanResponse, *shared.ApiError)\n\n\tListBranches(planId string) ([]*shared.Branch, *shared.ApiError)\n\tDeleteBranch(planId, branch string) *shared.ApiError\n\tCreateBranch(planId, branch string, req shared.CreateBranchRequest) *shared.ApiError\n\n\tGetSettings(planId, branch string) (*shared.PlanSettings, *shared.ApiError)\n\tUpdateSettings(planId, branch string, req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError)\n\n\tGetOrgDefaultSettings() (*shared.PlanSettings, *shared.ApiError)\n\tUpdateOrgDefaultSettings(req shared.UpdateSettingsRequest) (*shared.UpdateSettingsResponse, *shared.ApiError)\n\n\tGetPlanConfig(planId string) (*shared.PlanConfig, *shared.ApiError)\n\tUpdatePlanConfig(planId string, req shared.UpdatePlanConfigRequest) *shared.ApiError\n\tGetDefaultPlanConfig() (*shared.PlanConfig, *shared.ApiError)\n\tUpdateDefaultPlanConfig(req shared.UpdateDefaultPlanConfigRequest) *shared.ApiError\n\n\tCreateCustomModels(input *shared.ModelsInput) *shared.ApiError\n\tListCustomModels() ([]*shared.CustomModel, *shared.ApiError)\n\n\tListCustomProviders() ([]*shared.CustomProvider, *shared.ApiError)\n\n\tListModelPacks() ([]*shared.ModelPack, *shared.ApiError)\n\n\tGetCreditsTransactions(pageSize, pageNum int, req shared.CreditsLogRequest) (*shared.CreditsLogResponse, *shared.ApiError)\n\tGetCreditsSummary(req shared.CreditsLogRequest) (*shared.CreditsSummaryResponse, *shared.ApiError)\n\tGetBalance() (decimal.Decimal, *shared.ApiError)\n\n\tGetFileMap(req shared.GetFileMapRequest) (*shared.GetFileMapResponse, *shared.ApiError)\n\tGetContextBody(planId, branch, contextId string) (*shared.GetContextBodyResponse, *shared.ApiError)\n\tAutoLoadContext(ctx context.Context, planId, branch string, req shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError)\n\tGetBuildStatus(planId, branch string) (*shared.GetBuildStatusResponse, *shared.ApiError)\n}\n"
  },
  {
    "path": "app/cli/types/apply.go",
    "content": "package types\n\nimport (\n\t\"os\"\n)\n\ntype ApplyFlags struct {\n\tAutoConfirm bool\n\tAutoCommit  bool\n\tNoCommit    bool\n\tAutoExec    bool\n\tNoExec      bool\n\tAutoDebug   int\n}\n\ntype ApplyRollbackOption string\n\nconst (\n\tApplyRollbackOptionKeep     ApplyRollbackOption = \"Apply file changes\"\n\tApplyRollbackOptionRollback ApplyRollbackOption = \"Roll back file changes\"\n)\n\ntype OnApplyExecFailFn func(status int, output string, attempt int, toRollback *ApplyRollbackPlan, onErr OnErrFn, onSuccess func())\n\ntype ApplyReversion struct {\n\tContent string\n\tMode    os.FileMode\n}\n\ntype ApplyRollbackPlan struct {\n\tToRevert             map[string]ApplyReversion\n\tToRemove             []string\n\tPreviousProjectPaths *ProjectPaths\n}\n\nfunc (r *ApplyRollbackPlan) HasChanges() bool {\n\treturn len(r.ToRevert) > 0 || len(r.ToRemove) > 0\n}\n"
  },
  {
    "path": "app/cli/types/exec.go",
    "content": "package types\n\ntype TellFlags struct {\n\tTellBg                 bool\n\tTellStop               bool\n\tTellNoBuild            bool\n\tIsUserContinue         bool\n\tIsUserDebug            bool\n\tIsApplyDebug           bool\n\tIsChatOnly             bool\n\tAutoContext            bool\n\tSmartContext           bool\n\tContinuedAfterAction   bool\n\tExecEnabled            bool\n\tAutoApply              bool\n\tIsImplementationOfChat bool\n\tSkipChangesMenu        bool\n}\ntype BuildFlags struct {\n\tBuildBg   bool\n\tAutoApply bool\n}\n"
  },
  {
    "path": "app/cli/types/fs.go",
    "content": "package types\n\nimport ignore \"github.com/sabhiram/go-gitignore\"\n\ntype ProjectPaths struct {\n\tActivePaths    map[string]bool\n\tAllPaths       map[string]bool\n\tActiveDirs     map[string]bool\n\tAllDirs        map[string]bool\n\tPlandexIgnored *ignore.GitIgnore\n\tIgnoredPaths   map[string]string\n\tGitIgnoredDirs map[string]bool\n}\n"
  },
  {
    "path": "app/cli/types/types.go",
    "content": "package types\n\nimport (\n\tshared \"plandex-shared\"\n\t\"time\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype LoadContextParams struct {\n\tNote              string\n\tRecursive         bool\n\tNamesOnly         bool\n\tForceSkipIgnore   bool\n\tImageDetail       openai.ImageURLDetail\n\tDefsOnly          bool\n\tSkipIgnoreWarning bool\n\tAutoLoaded        bool\n\tSessionId         string\n}\n\ntype ContextOutdatedResult struct {\n\tMsg             string\n\tUpdatedContexts []*shared.Context\n\tRemovedContexts []*shared.Context\n\tTokenDiffsById  map[string]int\n\tNumFiles        int\n\tNumUrls         int\n\tNumTrees        int\n\tNumMaps         int\n\tNumFilesRemoved int\n\tNumTreesRemoved int\n\tReqFn           func() (map[string]*shared.UpdateContextParams, error)\n}\n\nconst (\n\tPlanOutdatedStrategyOverwrite        string = \"Clear the modifications and then apply\"\n\tPlanOutdatedStrategyApplyUnmodified  string = \"Apply only new and unmodified files\"\n\tPlanOutdatedStrategyApplyNoConflicts string = \"Apply anyway since there are no conflicts\"\n\tPlanOutdatedStrategyRebuild          string = \"Rebuild the plan with updated context\"\n\tPlanOutdatedStrategyCancel           string = \"Cancel\"\n)\n\ntype CurrentPlanSettings struct {\n\tId string `json:\"id\"`\n}\n\ntype PlanSettings struct {\n\tBranch string `json:\"branch\"`\n}\n\ntype CurrentProjectSettings struct {\n\tId string `json:\"id\"`\n}\n\ntype CurrentPlanSettingsByAccount map[string]*CurrentPlanSettings\ntype PlanSettingsByAccount map[string]*PlanSettings\ntype CurrentProjectSettingsByAccount map[string]*CurrentProjectSettings\n\ntype ChangesUIScrollReplacement struct {\n\tOldContent        string\n\tNewContent        string\n\tNumLinesPrepended int\n}\n\ntype ChangesUIViewportsUpdate struct {\n\tScrollReplacement *ChangesUIScrollReplacement\n}\n\ntype OnErrFn func(errMsg string, errArgs ...interface{})\n\ntype OauthResponse struct {\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tExpiresIn    int    `json:\"expires_in\"`\n}\n\ntype OauthCreds struct {\n\tOauthResponse\n\tExpiresAt time.Time `json:\"expires_at\"`\n}\n\ntype AccountCredentials struct {\n\tClaudeMax *OauthCreds `json:\"claudeMax\"`\n}\n"
  },
  {
    "path": "app/cli/ui/ui.go",
    "content": "package ui\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-cli/api\"\n\t\"plandex-cli/term\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/pkg/browser\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc OpenAuthenticatedURL(msg, path string) {\n\tsignInCode, apiErr := api.Client.CreateSignInCode()\n\tif apiErr != nil {\n\t\tlog.Fatalf(\"Error creating sign in code: %v\", apiErr)\n\t}\n\n\tapiHost := api.GetApiHost()\n\tappHost := strings.Replace(apiHost, \"api-v2.\", \"app.\", 1)\n\tappHost = strings.Replace(appHost, \"api.\", \"app.\", 1)\n\n\ttoken := shared.UiSignInToken{\n\t\tPin:        signInCode,\n\t\tRedirectTo: path,\n\t}\n\n\tjsonToken, err := json.Marshal(token)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error marshalling token: %v\", err)\n\t}\n\n\tencodedToken := base64.URLEncoding.EncodeToString(jsonToken)\n\n\turl := fmt.Sprintf(\"%s/auth/%s\", appHost, encodedToken)\n\n\tOpenURL(msg, url)\n}\n\nfunc OpenUnauthenticatedCloudURL(msg, path string) {\n\tapiHost := api.GetApiHost()\n\tappHost := strings.Replace(apiHost, \"api-v2.\", \"app.\", 1)\n\n\turl := fmt.Sprintf(\"%s%s\", appHost, path)\n\n\tOpenURL(msg, url)\n}\n\nfunc OpenURL(msg, url string) {\n\n\tfmt.Printf(\n\t\t\"%s\\n\\nIf it doesn't open automatically, use this URL:\\n%s\\n\",\n\t\tcolor.New(term.ColorHiGreen).Sprintf(msg),\n\t\turl,\n\t)\n\n\terr := browser.OpenURL(url)\n\tif err != nil {\n\t\tfmt.Printf(\"Failed to open URL automatically: %v\\n\", err)\n\t\tfmt.Println(\"Please open the URL manually in your browser.\")\n\t}\n}\n"
  },
  {
    "path": "app/cli/upgrade.go",
    "content": "package main\n\nimport (\n\t\"archive/tar\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"plandex-cli/term\"\n\t\"plandex-cli/version\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Masterminds/semver\"\n\t\"github.com/fatih/color\"\n\t\"github.com/inconshreveable/go-update\"\n)\n\nfunc checkForUpgrade() {\n\tif os.Getenv(\"PLANDEX_SKIP_UPGRADE\") != \"\" {\n\t\treturn\n\t}\n\n\tif version.Version == \"development\" {\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tdefer term.StopSpinner()\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\tlatestVersionURL := \"https://plandex.ai/v2/cli-version.txt\"\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, latestVersionURL, nil)\n\tif err != nil {\n\t\tlog.Println(\"Error creating request:\", err)\n\t\treturn\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tlog.Println(\"Error checking latest version:\", err)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Println(\"Error reading response body:\", err)\n\t\treturn\n\t}\n\n\tversionStr := string(body)\n\tversionStr = strings.TrimSpace(versionStr)\n\n\tlatestVersion, err := semver.NewVersion(versionStr)\n\tif err != nil {\n\t\tlog.Println(\"Error parsing latest version:\", err)\n\t\treturn\n\t}\n\n\tcurrentVersion, err := semver.NewVersion(version.Version)\n\tif err != nil {\n\t\tlog.Println(\"Error parsing current version:\", err)\n\t\treturn\n\t}\n\n\tif latestVersion.GreaterThan(currentVersion) {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"A new version of Plandex is available:\", color.New(color.Bold, term.ColorHiGreen).Sprint(versionStr))\n\t\tfmt.Printf(\"Current version: %s\\n\", color.New(color.Bold, term.ColorHiCyan).Sprint(version.Version))\n\t\tconfirmed, err := term.ConfirmYesNo(\"Upgrade to the latest version?\")\n\t\tif err != nil {\n\t\t\tlog.Println(\"Error reading input:\", err)\n\t\t\treturn\n\t\t}\n\n\t\tif confirmed {\n\t\t\tterm.ResumeSpinner()\n\t\t\terr := doUpgrade(latestVersion.String())\n\t\t\tif err != nil {\n\t\t\t\tterm.OutputErrorAndExit(\"Failed to upgrade: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tterm.StopSpinner()\n\t\t\trestartPlandex()\n\t\t} else {\n\t\t\tfmt.Println(\"Note: set PLANDEX_SKIP_UPGRADE=1 to stop upgrade prompts\")\n\t\t}\n\t}\n}\n\nfunc doUpgrade(version string) error {\n\ttag := fmt.Sprintf(\"cli/v%s\", version)\n\tescapedTag := url.QueryEscape(tag)\n\n\tdownloadURL := fmt.Sprintf(\"https://github.com/plandex-ai/plandex/releases/download/%s/plandex_%s_%s_%s.tar.gz\", escapedTag, version, runtime.GOOS, runtime.GOARCH)\n\tresp, err := http.Get(downloadURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to download the update: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Create a temporary file to save the downloaded archive\n\ttempFile, err := os.CreateTemp(\"\", \"*.tar.gz\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create temporary file: %w\", err)\n\t}\n\tdefer os.Remove(tempFile.Name()) // Clean up file afterwards\n\n\t// Copy the response body to the temporary file\n\t_, err = io.Copy(tempFile, resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to save the downloaded archive: %w\", err)\n\t}\n\n\t_, err = tempFile.Seek(0, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to seek in temporary file: %w\", err)\n\t}\n\n\t// Now, extract the binary from the tempFile\n\tgzr, err := gzip.NewReader(tempFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create gzip reader: %w\", err)\n\t}\n\tdefer gzr.Close()\n\n\ttarReader := tar.NewReader(gzr)\n\tfor {\n\t\theader, err := tarReader.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak // End of archive\n\t\t}\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read tar header: %w\", err)\n\t\t}\n\n\t\t// Check if the current file is the binary\n\t\tif header.Typeflag == tar.TypeReg && (header.Name == \"plandex\" || header.Name == \"plandex.exe\") {\n\t\t\terr = update.Apply(tarReader, update.Options{})\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, fs.ErrPermission) {\n\t\t\t\t\treturn fmt.Errorf(\"failed to apply update due to permission error; please try running your command again with 'sudo': %w\", err)\n\t\t\t\t}\n\t\t\t\treturn fmt.Errorf(\"failed to apply update: %w\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc restartPlandex() {\n\texe, err := os.Executable()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to determine executable path: %v\", err)\n\t}\n\n\tcmd := exec.Command(exe, os.Args[1:]...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\terr = cmd.Start()\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to restart: %v\", err)\n\t}\n\n\terr = cmd.Wait()\n\n\t// If the process exited with an error, exit with the same error code\n\tif exitErr, ok := err.(*exec.ExitError); ok {\n\t\tos.Exit(exitErr.ExitCode())\n\t} else if err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to restart: %v\", err)\n\t}\n\n\tos.Exit(0)\n}\n"
  },
  {
    "path": "app/cli/url/url.go",
    "content": "package url\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"plandex-cli/term\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n)\n\nconst (\n\t// Constants for fetchURLContent function\n\tmaxRedirections    = 10\n\thttpTimeout        = 30 * time.Second\n\tmaxContentSizeInMB = 10\n)\n\nfunc FetchURLContent(url string) (string, error) {\n\tclient := &http.Client{\n\t\tTimeout: httpTimeout,\n\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {\n\t\t\tif len(via) >= maxRedirections {\n\t\t\t\treturn errors.New(\"stopped after too many redirects\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\n\tresp, err := client.Get(url)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\treturn \"\", errors.New(\"non-2xx HTTP response status: \" + resp.Status)\n\t}\n\n\t// Limit the response reader to a maximum amount\n\tlimitedReader := io.LimitReader(resp.Body, maxContentSizeInMB*1024*1024)\n\n\tcontent, err := io.ReadAll(limitedReader)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcontentType := resp.Header.Get(\"Content-Type\")\n\tif strings.Contains(contentType, \"text/html\") {\n\t\treturn ExtractTextualContent(string(content)), nil\n\t} else {\n\t\treturn string(content), nil\n\t}\n}\n\nfunc ExtractTextualContent(htmlContent string) string {\n\tr := strings.NewReader(htmlContent)\n\tdoc, err := goquery.NewDocumentFromReader(r)\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Failed to parse HTML: %v\", err)\n\t}\n\n\treturn doc.Text()\n}\n\nfunc SanitizeURL(url string) string {\n\t// remove protocol portion with a regex\n\tre := regexp.MustCompile(`^.*?://`)\n\turl = re.ReplaceAllString(url, \"\")\n\n\t// Replace common invalid filename characters. You can extend this list as needed.\n\tsanitized := strings.ReplaceAll(url, \":\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \"/\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \"?\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \"&\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \"=\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \"#\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \"%\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \"*\", \"_\")\n\tsanitized = strings.ReplaceAll(sanitized, \" \", \"_\")\n\treturn sanitized\n}\n\nfunc IsValidURL(str string) bool {\n\tu, err := url.Parse(str)\n\treturn err == nil && u.Scheme != \"\" && u.Host != \"\"\n}\n"
  },
  {
    "path": "app/cli/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"time\"\n)\n\nfunc EnsureMinDuration(start time.Time, minDuration time.Duration) {\n\telapsed := time.Since(start)\n\tif elapsed < minDuration {\n\t\ttime.Sleep(minDuration - elapsed)\n\t}\n}\n"
  },
  {
    "path": "app/cli/version/version.go",
    "content": "package version\n\n// Version will be set at build time using -ldflags\nvar Version = \"development\"\n"
  },
  {
    "path": "app/cli/version.txt",
    "content": "2.2.1\n"
  },
  {
    "path": "app/docker-compose.yml",
    "content": "services:\n  plandex-postgres:\n    image: postgres:latest\n    restart: always\n    environment:\n      POSTGRES_PASSWORD: plandex\n      POSTGRES_USER: plandex\n      POSTGRES_DB: plandex\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - plandex-db:/var/lib/postgresql/data\n    networks:\n      - plandex-network\n  plandex-server:\n    image: plandexai/plandex-server:latest\n    # build:\n    #   context: .\n    #   dockerfile: server/Dockerfile\n    volumes:\n      - plandex-files:/plandex-server\n    ports:\n      - \"8099:8099\"\n      - \"4000:4000\"\n    environment:\n      DATABASE_URL: \"postgres://plandex:plandex@plandex-postgres:5432/plandex?sslmode=disable\"\n      GOENV: development\n      LOCAL_MODE: 1\n      PLANDEX_BASE_DIR: /plandex-server\n      OLLAMA_BASE_URL: http://host.docker.internal:11434\n      \n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\n    networks:\n      - plandex-network\n    depends_on:\n      - plandex-postgres\n    command: [ \"/bin/sh\", \"-c\", \"/scripts/wait-for-it.sh plandex-postgres:5432 -- ./plandex-server\" ]\n\nnetworks:\n  plandex-network:\n    driver: bridge\n\nvolumes:\n  plandex-db:\n  plandex-files:"
  },
  {
    "path": "app/plans/credits-cmd.txt",
    "content": "\nAdd 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.\n\nIf IntegratedModelsMode isn't true, output that the org isn't using integrated models mode and nothing else.\n\nDisplay the current balance nicely in a table.\n"
  },
  {
    "path": "app/plans/credits-log-cmd.txt",
    "content": "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.\n\nYou 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.\n\nWe 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\n\nThink 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. \n"
  },
  {
    "path": "app/plans/json-prompts-to-xml.md",
    "content": "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`.\n\nHere are the calls I want to update:\n\n- commit message ('genPlanDescription' call)\n- exec status ('execStatusShouldContinue' call) \n- naming functions - 'GenPlanName', 'GenPipedDataName', and 'GenNoteName'\n\nLook at 'build_exec.go' for an example of how to extract XML from a response.\n\nFor each of the LLM calls:\n\n- 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.\n\n- 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.\n\n- 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.\n\n- Update the post LLM call handling to extract the appropriate data using xml tags instead of json.\n\n- Apart from the updated prompts, do not change other parameters in the LLM calls (like model, temperature, etc.)\n\n- 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."
  },
  {
    "path": "app/plans/plan-config.md",
    "content": "I want to add a plan config feature. It should be loosely modeled  \non the plan model settings (often referred to also as just plan settings),  \nand the 'model packs' feature. I want to store a 'plan_config' JSON field on\nthe 'plans' table. I also want a 'settings' and a 'set' CLI command.        \n  \n'settings' shows the current config (like 'models' cmd)  and 'set' allows \nupdating the config just like 'set-model'. the server-side updates can also\nwork similarly to models and model sets. it shouldn't be added to the file  \nsystem 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.\n\nFor prompting and user input in the 'set' command, use the same approach as the 'set-model' command. Don't introduce any new dependencies.                                                                              \nit should have these properties to start:                                   \n                                                                              \nAutoApply bool AutoCommit bool AutoContext bool NoExec bool AutoDebug bool AutoDebugTries int         \n\nApart from the plan config, I also want a default user-level config with the\nsame properties. similar to how there's 'set-model' and 'set-model default'-\nthis should also have a 'set default' command. again use model settings as a\nguide.\n\nOn 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.\n\nOn the client side, update the API interface and implementation for the new api calls.\n\nAlso 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).\n\nServer-side, it needs api handlers, db handlers, and db up/down migration.\n\nAlso add request/response types.\n\nFor 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).\n\nUpdate 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."
  },
  {
    "path": "app/reset_local.sh",
    "content": "#!/usr/bin/env bash\n\n# Get the absolute path to the script's directory, regardless of where it's run from\nSCRIPT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\"\n\n# Change to the app directory if we're not already there\ncd \"$SCRIPT_DIR\"\n\necho \"Clearing local mode...\"\n\n./clear_local.sh\n\necho \"Starting local mode...\"\n\n./start_local.sh"
  },
  {
    "path": "app/scripts/cmd/gen/gen.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"text/template\"\n)\n\nfunc main() {\n\tif len(os.Args) < 2 {\n\t\tlog.Fatalf(\"Usage: %s <path/to/directory>\", os.Args[0])\n\t}\n\n\tdirPath := os.Args[1]\n\tdirName := filepath.Base(dirPath)\n\n\t// Create the main directory\n\tif err := os.MkdirAll(dirPath, 0755); err != nil {\n\t\tlog.Fatalf(\"Error creating directory: %s\", err)\n\t}\n\n\tf, err := os.Create(fmt.Sprintf(\"%s/%s\", dirPath, \"promptfooconfig.yaml\"))\n\tif err != nil {\n\t\tlog.Fatalf(\"Error creating file: %s\", err)\n\t}\n\tf.Close()\n\n\t// Create files inside the directory\n\tfiles := []string{\n\t\t\"parameters.json\",\n\t\t\"config.properties\",\n\t\t\"prompt.txt\",\n\t}\n\n\tfor _, file := range files {\n\t\tf, err := os.Create(fmt.Sprintf(\"%s/%s.%s\", dirPath, dirName, file))\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error creating file: %s\", err)\n\t\t}\n\t\tf.Close()\n\t}\n\n\t// Create assets and tests directories\n\tsubDirs := []string{\"assets\", \"tests\"}\n\n\tfor _, subDir := range subDirs {\n\t\tif err := os.Mkdir(fmt.Sprintf(\"%s/%s\", dirPath, subDir), 0755); err != nil {\n\t\t\tlog.Fatalf(\"Error creating subdirectory: %s\", err)\n\t\t}\n\t}\n\n\t// Template for promptfooconfig.yaml\n\tymlTemplate := `description: \"{{ .Name }}\"\n\nprompts:\n  - file://{{ .Name }}.prompt.txt\n\nproviders:\n  - file://{{ .Name }}.provider.yml\n\ntests: tests/*.tests.yml\n`\n\n\t// Populate promptfooconfig.yaml\n\tpromptFooConfigTmpl, err := template.New(\"yml\").Parse(ymlTemplate)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error creating template: %s\", err)\n\t}\n\n\t// Template for config.properties\n\tpropertiesTemplate := `provider_id=openai:gpt-4o\ntemperature=\nmax_tokens=\ntop_p=\nresponse_format=\nfunction_name=\ntool_type=function\nfunction_param_type=object\ntool_choice_type=function\ntool_choice_function_name=\nnested_parameters_json={{ .Name }}.parameters.json\n`\n\n\t// Populate config.properties\n\tconfigPropertiesTmpl, err := template.New(\"properties\").Parse(propertiesTemplate)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error creating template: %s\", err)\n\t}\n\n\tconfigFile, err := os.Create(fmt.Sprintf(\"%s/%s.%s\", dirPath, dirName, \"config.properties\"))\n\tif err != nil {\n\t\tlog.Fatalf(\"Error creating config.properties: %s\", err)\n\t}\n\tdefer configFile.Close()\n\n\tfile, err := os.Create(fmt.Sprintf(\"%s/promptfooconfig.yaml\", dirPath))\n\tif err != nil {\n\t\tlog.Fatalf(\"Error creating promptfooconfig.yaml: %s\", err)\n\t}\n\tdefer file.Close()\n\n\tdata := struct {\n\t\tName string\n\t}{\n\t\tName: dirName,\n\t}\n\n\tif err := promptFooConfigTmpl.Execute(file, data); err != nil {\n\t\tlog.Fatalf(\"Error executing template: %s\", err)\n\t}\n\n\tif err := configPropertiesTmpl.Execute(configFile, data); err != nil {\n\t\tlog.Fatalf(\"Error executing template: %s\", err)\n\t}\n\n\tfmt.Println(\"Directory created successfully!\")\n\tfmt.Println(\"Please check the contents of the directory and proceed with the implementation.\")\n}\n"
  },
  {
    "path": "app/scripts/cmd/provider/gen_provider.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"text/template\"\n)\n\nvar testDir = \"test/evals/promptfoo-poc\"\nvar templFile = testDir + \"/templates/\" + \"/provider.template.yml\"\n\nfunc main() {\n\n\ttestAbsPath, _ := filepath.Abs(testDir)\n\ttemplAbsPath, _ := filepath.Abs(templFile)\n\n\t// Function to walk through directories and find required values\n\terr := filepath.Walk(testAbsPath, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !info.IsDir() && filepath.Ext(path) == \".properties\" {\n\t\t\tdirName := filepath.Base(filepath.Dir(path))\n\t\t\toutputFileName := filepath.Join(filepath.Dir(path), dirName+\".provider.yml\")\n\n\t\t\t// Read the template file\n\t\t\ttemplateContent, err := os.ReadFile(templAbsPath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Error reading template file: %v\", err)\n\t\t\t}\n\n\t\t\t// Prepare variables (this assumes properties file is a simple key=value format)\n\t\t\tvariables := map[string]interface{}{}\n\t\t\tproperties, err := os.ReadFile(path)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Error reading properties file: %v\", err)\n\t\t\t}\n\t\t\tfor _, line := range strings.Split(string(properties), \"\\n\") {\n\t\t\t\tif len(line) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tparts := strings.SplitN(line, \"=\", 2)\n\n\t\t\t\tif len(parts) > 2 {\n\t\t\t\t\tlog.Fatalf(\"Invalid line in properties file: %s\", line)\n\t\t\t\t}\n\n\t\t\t\tif len(parts) < 2 {\n\t\t\t\t\tlog.Fatalf(\"Invalid line in properties file: %s\", line)\n\t\t\t\t}\n\n\t\t\t\tkey := strings.TrimSpace(parts[0])\n\t\t\t\tvalue := strings.TrimSpace(parts[1])\n\n\t\t\t\tif key != \"nested_parameters_json\" {\n\t\t\t\t\tvariables[key] = value\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Read the file path from the nested_parameters_json key\n\t\t\t\tparametersJsonFile := filepath.Join(filepath.Dir(path), value)\n\t\t\t\tjsonParameters, err := os.ReadFile(parametersJsonFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"Error reading nested parameters JSON file: %v\", err)\n\t\t\t\t}\n\t\t\t\t// Parse the JSON string\n\t\t\t\tvar nestedParameters map[string]interface{}\n\n\t\t\t\t// We marshal and unmarshal the JSON to ensure that the nested properties are properly formatted \n\t\t\t\t// for the template, and to ensure that the data is correct json\n\n\t\t\t\terr = json.Unmarshal(jsonParameters, &nestedParameters)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"Error un-marshalling nested parameters JSON: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tparameters, err := json.Marshal(nestedParameters)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Fatalf(\"Error marshalling nested parameters JSON: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Add the nested properties to the variables\n\t\t\t\tvariables[\"parameters\"] = string(parameters)\n\t\t\t}\n\n\t\t\t// Parse and execute the template\n\t\t\ttmpl, err := template.New(\"yamlTemplate\").Parse(string(templateContent))\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Error parsing template: %v\", err)\n\t\t\t}\n\t\t\toutputFile, err := os.Create(outputFileName)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Error creating output file: %v\", err)\n\t\t\t}\n\t\t\tdefer outputFile.Close()\n\n\t\t\terr = tmpl.Execute(outputFile, variables)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Error executing template: %v\", err)\n\t\t\t}\n\t\t\tlog.Printf(\"Template rendered and saved to '%s'\", outputFileName)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tlog.Fatalf(\"Error walking the path: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "app/scripts/dev.sh",
    "content": "#!/usr/bin/env bash\n\n# Detect zsh and trigger it if its the shell\nif [ -n \"$ZSH_VERSION\" ]; then\n  # shell is zsh\n  echo \"Detected zsh\"\n  zsh -c \"source ~/.zshrc && $*\"\nfi\n\n# Get the directory of the script\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# Change to the script directory\ncd \"$SCRIPT_DIR\" || exit 1\n\n# Install Python deps\n\"$SCRIPT_DIR/litellm_deps.sh\"\n\n# Update PATH for python venv\nexport PATH=\"$SCRIPT_DIR/../litellm-venv/bin:$PATH\"\n\n# Detect if reflex is installed and install it if not\nif ! [ -x \"$(command -v reflex)\" ]; then\n\n  # Check if the $GOPATH is empty\n  if [ -z \"$GOPATH\" ]; then\n    echo \"Error: GOPATH is not set. Please set it to continue...\" >&2\n    exit 1\n  fi\n\n  echo 'Error: reflex is not installed. Installing it now...' >&2\n  go install github.com/cespare/reflex@latest\nfi\n\nterminate() {\n  pkill -f 'plandex-server' # Assuming plandex-server is the name of your process\n  kill -TERM \"$pid1\" 2>/dev/null\n  kill -TERM \"$pid2\" 2>/dev/null\n}\n\ntrap terminate SIGTERM SIGINT\n\n(cd .. && cd cli && ./dev.sh)\n\ncd ../\n\nexport DATABASE_URL=postgres://ds:@localhost/plandex_local?sslmode=disable\nexport GOENV=development\nexport LOCAL_MODE=1\n\nreflex -r '^(cli|shared)/.*\\.(go|mod|sum)$' -- sh -c 'cd cli && ./dev.sh' &\npid1=$!\n\nreflex -r '^(server|shared)/.*\\.(go|mod|sum|py)$' -s -- sh -c 'cd server && go build && ./plandex-server' &\npid2=$!\n\nwait $pid1\nwait $pid2\n"
  },
  {
    "path": "app/scripts/litellm_deps.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nVENV_DIR=\"$SCRIPT_DIR/../litellm-venv\"\nREQUIRED_PYTHON=\"python3\"\nREQUIRED_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\")\n\nif ! command -v \"$REQUIRED_PYTHON\" &>/dev/null; then\n  echo \"Python3 not found. Please install it and run this script again.\"\n  exit 1\nfi\n\nif [ ! -d \"$VENV_DIR\" ]; then\n  echo \"Creating Python virtual environment at $VENV_DIR...\"\n  \"$REQUIRED_PYTHON\" -m venv \"$VENV_DIR\"\nfi\n\nsource \"$VENV_DIR/bin/activate\"\n\nis_installed() {\n  python -c \"import pkg_resources; pkg_resources.require('$1')\" &>/dev/null\n}\n\nfor package in \"${REQUIRED_PACKAGES[@]}\"; do\n  if ! is_installed \"$package\"; then\n    echo \"Installing Python package: $package\"\n    pip install \"$package\"\n  else\n    echo \"Python package $package already installed\"\n  fi\ndone\n\ndeactivate\n"
  },
  {
    "path": "app/scripts/wait-for-it.sh",
    "content": "#!/usr/bin/env bash\n# Use this script to test if a given TCP host/port are available\n\nWAITFORIT_cmdname=${0##*/}\n\nechoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo \"$@\" 1>&2; fi }\n\nusage()\n{\n    cat << USAGE >&2\nUsage:\n    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]\n    -h HOST | --host=HOST       Host or IP under test\n    -p PORT | --port=PORT       TCP port under test\n                                Alternatively, you specify the host and port as host:port\n    -s | --strict               Only execute subcommand if the test succeeds\n    -q | --quiet                Don't output any status messages\n    -t TIMEOUT | --timeout=TIMEOUT\n                                Timeout in seconds, zero for no timeout\n    -- COMMAND ARGS             Execute command with args after the test finishes\nUSAGE\n    exit 1\n}\n\nwait_for()\n{\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    else\n        echoerr \"$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout\"\n    fi\n    WAITFORIT_start_ts=$(date +%s)\n    while :\n    do\n        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then\n            nc -z $WAITFORIT_HOST $WAITFORIT_PORT\n            WAITFORIT_result=$?\n        else\n            (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1\n            WAITFORIT_result=$?\n        fi\n        if [[ $WAITFORIT_result -eq 0 ]]; then\n            WAITFORIT_end_ts=$(date +%s)\n            echoerr \"$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds\"\n            break\n        fi\n        sleep 1\n    done\n    return $WAITFORIT_result\n}\n\nwait_for_wrapper()\n{\n    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692\n    if [[ $WAITFORIT_QUIET -eq 1 ]]; then\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    else\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    fi\n    WAITFORIT_PID=$!\n    trap \"kill -INT -$WAITFORIT_PID\" INT\n    wait $WAITFORIT_PID\n    WAITFORIT_RESULT=$?\n    if [[ $WAITFORIT_RESULT -ne 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    fi\n    return $WAITFORIT_RESULT\n}\n\n# process arguments\nwhile [[ $# -gt 0 ]]\ndo\n    case \"$1\" in\n        *:* )\n        WAITFORIT_hostport=(${1//:/ })\n        WAITFORIT_HOST=${WAITFORIT_hostport[0]}\n        WAITFORIT_PORT=${WAITFORIT_hostport[1]}\n        shift 1\n        ;;\n        --child)\n        WAITFORIT_CHILD=1\n        shift 1\n        ;;\n        -q | --quiet)\n        WAITFORIT_QUIET=1\n        shift 1\n        ;;\n        -s | --strict)\n        WAITFORIT_STRICT=1\n        shift 1\n        ;;\n        -h)\n        WAITFORIT_HOST=\"$2\"\n        if [[ $WAITFORIT_HOST == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --host=*)\n        WAITFORIT_HOST=\"${1#*=}\"\n        shift 1\n        ;;\n        -p)\n        WAITFORIT_PORT=\"$2\"\n        if [[ $WAITFORIT_PORT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --port=*)\n        WAITFORIT_PORT=\"${1#*=}\"\n        shift 1\n        ;;\n        -t)\n        WAITFORIT_TIMEOUT=\"$2\"\n        if [[ $WAITFORIT_TIMEOUT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --timeout=*)\n        WAITFORIT_TIMEOUT=\"${1#*=}\"\n        shift 1\n        ;;\n        --)\n        shift\n        WAITFORIT_CLI=(\"$@\")\n        break\n        ;;\n        --help)\n        usage\n        ;;\n        *)\n        echoerr \"Unknown argument: $1\"\n        usage\n        ;;\n    esac\ndone\n\nif [[ \"$WAITFORIT_HOST\" == \"\" || \"$WAITFORIT_PORT\" == \"\" ]]; then\n    echoerr \"Error: you need to provide a host and port to test.\"\n    usage\nfi\n\nWAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}\nWAITFORIT_STRICT=${WAITFORIT_STRICT:-0}\nWAITFORIT_CHILD=${WAITFORIT_CHILD:-0}\nWAITFORIT_QUIET=${WAITFORIT_QUIET:-0}\n\n# Check to see if timeout is from busybox?\nWAITFORIT_TIMEOUT_PATH=$(type -p timeout)\nWAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)\n\nWAITFORIT_BUSYTIMEFLAG=\"\"\nif [[ $WAITFORIT_TIMEOUT_PATH =~ \"busybox\" ]]; then\n    WAITFORIT_ISBUSY=1\n    # Check if busybox timeout uses -t flag\n    # (recent Alpine versions don't support -t anymore)\n    if timeout &>/dev/stdout | grep -q -e '-t '; then\n        WAITFORIT_BUSYTIMEFLAG=\"-t\"\n    fi\nelse\n    WAITFORIT_ISBUSY=0\nfi\n\nif [[ $WAITFORIT_CHILD -gt 0 ]]; then\n    wait_for\n    WAITFORIT_RESULT=$?\n    exit $WAITFORIT_RESULT\nelse\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        wait_for_wrapper\n        WAITFORIT_RESULT=$?\n    else\n        wait_for\n        WAITFORIT_RESULT=$?\n    fi\nfi\n\nif [[ $WAITFORIT_CLI != \"\" ]]; then\n    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then\n        echoerr \"$WAITFORIT_cmdname: strict mode, refusing to execute subprocess\"\n        exit $WAITFORIT_RESULT\n    fi\n    exec \"${WAITFORIT_CLI[@]}\"\nelse\n    exit $WAITFORIT_RESULT\nfi\n"
  },
  {
    "path": "app/server/.gitignore",
    "content": "cloud/"
  },
  {
    "path": "app/server/Dockerfile",
    "content": "FROM golang:1.23.3\n\n# Update and install necessary packages including build tools for Tree-sitter\nRUN apt-get update && \\\n  apt-get install -y git gcc g++ make python3 python3-venv\n\n# Install Python and create a virtual environment for litellm passthrough\nRUN python3 -m venv /opt/venv\n    \n# Activate the virtual environment for all following RUN commands\nENV PATH=\"/opt/venv/bin:$PATH\"\n\n# Now install litellm passthrough dependencies in the virtual environment\nRUN 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\"\n\nWORKDIR /app\n\n# Copy go.mod and go.sum for shared and server, and install dependencies\nCOPY ./shared/go.mod ./shared/go.sum ./shared/\nRUN cd shared && go mod download\n\nCOPY ./server/go.mod ./server/go.sum ./server/\nRUN cd server && go mod download\n\n# Copy the actual source code\nCOPY ./server ./server\nCOPY ./shared ./shared\nCOPY ./scripts /scripts\n\n# Set working directory to server\nWORKDIR /app/server\n\n# Build the application\nRUN rm -f plandex-server && go build -o plandex-server .\n\n# Set the port and expose it\nENV PORT=8099\nEXPOSE 8099\n\n# Command to run the executable\nCMD [\"./plandex-server\"]\n"
  },
  {
    "path": "app/server/db/account_helpers.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\ntype CreateAccountResult struct {\n\tUser  *User\n\tOrgId string\n\tToken string\n}\n\nfunc CreateAccount(name, email, emailVerificationId string, tx *sqlx.Tx) (*CreateAccountResult, error) {\n\tisLocalMode := (os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\")\n\t// create user\n\tuser, err := CreateUser(name, email, tx)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating user: %v\", err)\n\t}\n\n\tuserId := user.Id\n\tdomain := user.Domain\n\n\t// create auth token\n\ttoken, authTokenId, err := CreateAuthToken(userId, tx)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating auth token: %v\", err)\n\t}\n\n\t// skipping email verification in local mode\n\tif !isLocalMode {\n\t\t// update email verification with user and auth token ids\n\t\t_, err = tx.Exec(\"UPDATE email_verifications SET user_id = $1, auth_token_id = $2 WHERE id = $3\", userId, authTokenId, emailVerificationId)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error updating email verification: %v\", err)\n\t\t}\n\t}\n\n\t// add to org matching domain if one exists and auto add domain users is true for that org\n\torgId, err := AddToOrgForDomain(domain, userId, tx)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error adding user to org for domain: %v\", err)\n\t}\n\n\treturn &CreateAccountResult{\n\t\tUser:  user,\n\t\tOrgId: orgId,\n\t\tToken: token,\n\t}, nil\n}\n"
  },
  {
    "path": "app/server/db/ai_model_helpers.go",
    "content": "package db\n"
  },
  {
    "path": "app/server/db/auth_helpers.go",
    "content": "package db\n\nimport (\n\t\"crypto/sha256\"\n\t\"database/sql\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/pkg/errors\"\n)\n\nconst tokenExpirationDays = 90 // (trial tokens don't expire)\n\nfunc CreateAuthToken(userId string, tx *sqlx.Tx) (token, id string, err error) {\n\tuid := uuid.New()\n\tbytes := uid[:]\n\thashBytes := sha256.Sum256(bytes)\n\thash := hex.EncodeToString(hashBytes[:])\n\n\terr = tx.QueryRow(\"INSERT INTO auth_tokens (user_id, token_hash) VALUES ($1, $2) RETURNING id\", userId, hash).Scan(&id)\n\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"error creating auth token: %v\", err)\n\t}\n\n\treturn uid.String(), id, nil\n}\n\nfunc ValidateAuthToken(token string) (*AuthToken, error) {\n\tuid, err := uuid.Parse(token)\n\n\tif err != nil {\n\t\tlog.Println(\"error parsing token\", err)\n\t\treturn nil, errors.New(\"invalid token\")\n\t}\n\n\tbytes := uid[:]\n\thashBytes := sha256.Sum256(bytes)\n\ttokenHash := hex.EncodeToString(hashBytes[:])\n\n\tvar authToken AuthToken\n\terr = 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))\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\tlog.Println(\"auth token error - no rows found\")\n\t\t\treturn nil, errors.New(\"invalid token\")\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error validating token: %v\", err)\n\t}\n\n\treturn &authToken, nil\n}\n\nfunc CreateEmailVerification(email string, userId, pinHash string) error {\n\tvar err error\n\tif userId == \"\" {\n\t\t_, err = Conn.Exec(\"INSERT INTO email_verifications (email, pin_hash) VALUES ($1, $2)\", email, pinHash)\n\t} else {\n\t\t_, err = Conn.Exec(\"INSERT INTO email_verifications (email, pin_hash, user_id) VALUES ($1, $2, $3)\", email, pinHash, userId)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating email verification: %v\", err)\n\t}\n\n\treturn nil\n}\n\n// email verifications expire in 5 minutes\nconst emailVerificationExpirationMinutes = 5\n\nconst InvalidOrExpiredPinError = \"invalid or expired pin\"\n\nfunc ValidateEmailVerification(email, pin string) (id string, err error) {\n\treturn validateEmailVerification(email, pin, true, true)\n}\n\nfunc ValidateEmailPreviouslyVerified(email, pin string) (id string, err error) {\n\treturn validateEmailVerification(email, pin, false, false)\n}\n\nfunc validateEmailVerification(email, pin string, enforceExpiration bool, errOnAlreadyVerified bool) (id string, err error) {\n\tpinHashBytes := sha256.Sum256([]byte(pin))\n\tpinHash := hex.EncodeToString(pinHashBytes[:])\n\n\tvar authTokenId *string\n\n\tquery := `SELECT id, auth_token_id \n              FROM email_verifications\n              WHERE pin_hash = $1 \n\t\t\t\t\t\t\tAND email = $2`\n\n\tif enforceExpiration {\n\t\tquery += ` AND created_at > $3`\n\t\terr = Conn.QueryRow(query, pinHash, email, time.Now().Add(-emailVerificationExpirationMinutes*time.Minute)).Scan(&id, &authTokenId)\n\t} else {\n\t\terr = Conn.QueryRow(query, pinHash, email).Scan(&id, &authTokenId)\n\t}\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn \"\", errors.New(InvalidOrExpiredPinError)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"error validating email verification: %v\", err)\n\t}\n\n\tif authTokenId != nil && errOnAlreadyVerified {\n\t\treturn \"\", errors.New(\"pin already verified\")\n\t} else if authTokenId == nil && !errOnAlreadyVerified {\n\t\treturn \"\", errors.New(\"pin not previously verified\")\n\t}\n\n\treturn id, nil\n}\n\nfunc CreateSignInCode(userId, orgId, pinHash string) error {\n\t_, err := Conn.Exec(\"INSERT INTO sign_in_codes (user_id, org_id, pin_hash) VALUES ($1, $2, $3)\", userId, orgId, pinHash)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating sign in code: %v\", err)\n\t}\n\n\treturn nil\n}\n\nconst signInCodeExpirationMinutes = 5\n\ntype ValidateSignInCodeRes struct {\n\tId     string\n\tOrgId  string\n\tUserId string\n}\n\nfunc ValidateSignInCode(pin string) (*ValidateSignInCodeRes, error) {\n\tpinHashBytes := sha256.Sum256([]byte(pin))\n\tpinHash := hex.EncodeToString(pinHashBytes[:])\n\n\tres := &ValidateSignInCodeRes{}\n\tvar authTokenId *string\n\tquery := `SELECT id, org_id, user_id, auth_token_id FROM sign_in_codes WHERE pin_hash = $1 AND created_at > $2`\n\terr := Conn.QueryRow(query, pinHash, time.Now().Add(-signInCodeExpirationMinutes*time.Minute)).Scan(&res.Id, &res.OrgId, &res.UserId, &authTokenId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error validating sign in code: %v\", err)\n\t}\n\n\tif authTokenId != nil {\n\t\treturn nil, errors.New(\"sign in code already used\")\n\t}\n\n\treturn res, nil\n}\n\nfunc GetUserPermissions(userId, orgId string) ([]string, error) {\n\tvar permissions []string\n\n\tquery := `\n    SELECT p.name, p.resource_id \n    FROM permissions p\n    JOIN org_roles_permissions orp ON p.id = orp.permission_id\n    JOIN orgs_users ou ON orp.org_role_id = ou.org_role_id\n    WHERE ou.user_id = $1 AND ou.org_id = $2\n    `\n\n\trows, err := Conn.Query(query, userId, orgId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar permission string\n\t\tvar resourceId sql.NullString\n\t\tif err := rows.Scan(&permission, &resourceId); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttoAdd := permission\n\t\tif resourceId.Valid {\n\t\t\ttoAdd = toAdd + \"|\" + resourceId.String\n\t\t}\n\n\t\tpermissions = append(permissions, toAdd)\n\t}\n\n\t// Check for errors from iterating over rows.\n\tif err = rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn permissions, nil\n}\n"
  },
  {
    "path": "app/server/db/branch_helpers.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/lib/pq\"\n)\n\nfunc CreateBranch(repo *GitRepo, plan *Plan, parentBranch *Branch, name string, tx *sqlx.Tx) (*Branch, error) {\n\n\tquery := `INSERT INTO branches (org_id, owner_id, plan_id, parent_branch_id, name, status, context_tokens, convo_tokens) \n\tVALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n\tRETURNING id, created_at, updated_at`\n\n\tvar (\n\t\tcontextTokens  int\n\t\tconvoTokens    int\n\t\tparentBranchId *string\n\t)\n\n\tif parentBranch != nil {\n\t\tparentBranchId = &parentBranch.Id\n\n\t\tcontextTokens = parentBranch.ContextTokens\n\t\tconvoTokens = parentBranch.ConvoTokens\n\t}\n\n\tbranch := &Branch{\n\t\tOrgId:          plan.OrgId,\n\t\tOwnerId:        plan.OwnerId,\n\t\tPlanId:         plan.Id,\n\t\tParentBranchId: parentBranchId,\n\t\tName:           name,\n\t\tStatus:         shared.PlanStatusDraft,\n\t}\n\n\tvar err error\n\n\tif tx == nil {\n\t\terr = Conn.QueryRow(\n\t\t\tquery,\n\t\t\tbranch.OrgId,\n\t\t\tbranch.OwnerId,\n\t\t\tbranch.PlanId,\n\t\t\tbranch.ParentBranchId,\n\t\t\tbranch.Name,\n\t\t\tbranch.Status,\n\t\t\tcontextTokens,\n\t\t\tconvoTokens,\n\t\t).Scan(\n\t\t\t&branch.Id,\n\t\t\t&branch.CreatedAt,\n\t\t\t&branch.UpdatedAt,\n\t\t)\n\t} else {\n\t\terr = tx.QueryRow(\n\t\t\tquery,\n\t\t\tbranch.OrgId,\n\t\t\tbranch.OwnerId,\n\t\t\tbranch.PlanId,\n\t\t\tbranch.ParentBranchId,\n\t\t\tbranch.Name,\n\t\t\tbranch.Status,\n\t\t\tcontextTokens,\n\t\t\tconvoTokens,\n\t\t).Scan(\n\t\t\t&branch.Id,\n\t\t\t&branch.CreatedAt,\n\t\t\t&branch.UpdatedAt,\n\t\t)\n\t}\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating branch: %v\", err)\n\t}\n\n\t// Create the git branch (except for main, which is created by default on repo init)\n\tif name != \"main\" {\n\t\t// parentBranchName := \"main\"\n\t\t// if parentBranch != nil {\n\t\t// \tparentBranchName = parentBranch.Name\n\t\t// }\n\n\t\terr = repo.GitCreateBranch(name)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error creating git branch: %v\", err)\n\t\t}\n\t}\n\n\terr = IncActiveBranches(plan.Id, 1, tx)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error incrementing active branches: %v\", err)\n\t}\n\n\treturn branch, nil\n}\n\nfunc GetDbBranch(planId, name string) (*Branch, error) {\n\tvar branch Branch\n\terr := Conn.Get(&branch, \"SELECT * FROM branches WHERE plan_id = $1 AND name = $2\", planId, name)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting branch: %v\", err)\n\t}\n\n\treturn &branch, nil\n}\n\nfunc ListPlanBranches(repo *GitRepo, planId string) ([]*Branch, error) {\n\tvar branches []*Branch\n\terr := Conn.Select(&branches, \"SELECT * FROM branches WHERE plan_id = $1 ORDER BY created_at\", planId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing branches: %v\", err)\n\t}\n\n\t// log.Println(\"branches: \", spew.Sdump(branches))\n\n\tgitBranches, err := repo.GitListBranches()\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing git branches: %v\", err)\n\t}\n\n\t// log.Println(\"gitBranches: \", spew.Sdump(gitBranches))\n\n\tvar nameSet = make(map[string]bool)\n\tfor _, name := range gitBranches {\n\t\tnameSet[name] = true\n\t}\n\n\tvar res []*Branch\n\tfor _, branch := range branches {\n\t\tif nameSet[branch.Name] {\n\t\t\tres = append(res, branch)\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\nfunc ListBranchesForPlans(orgId string, planIds []string) ([]*Branch, error) {\n\tvar branches []*Branch\n\terr := Conn.Select(&branches, \"SELECT * FROM branches WHERE plan_id = ANY($1) ORDER BY created_at\", pq.Array(planIds))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing branches: %v\", err)\n\t}\n\n\treturn branches, nil\n}\n\nfunc DeleteBranch(ctx context.Context, repo *GitRepo, planId, branch string) error {\n\treturn WithTx(ctx, \"delete branch\", func(tx *sqlx.Tx) error {\n\n\t\t_, err := tx.Exec(\"DELETE FROM branches WHERE plan_id = $1 AND name = $2\", planId, branch)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting branch: %v\", err)\n\t\t}\n\n\t\terr = IncActiveBranches(planId, -1, tx)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error decrementing active branches: %v\", err)\n\t\t}\n\n\t\terr = repo.GitDeleteBranch(branch)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting branch dir: %v\", err)\n\t\t}\n\n\t\treturn err\n\t})\n}\n"
  },
  {
    "path": "app/server/db/build_helpers.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc StorePlanBuild(build *PlanBuild) error {\n\n\tquery := `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`\n\n\targs := map[string]interface{}{\n\t\t\"org_id\":           build.OrgId,\n\t\t\"plan_id\":          build.PlanId,\n\t\t\"convo_message_id\": build.ConvoMessageId,\n\t\t\"file_path\":        build.FilePath,\n\t}\n\n\trow, err := Conn.NamedQuery(query, args)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error storing plan build: %v\", err)\n\t}\n\tdefer row.Close()\n\n\tif row.Next() {\n\t\tvar createdAt, updatedAt time.Time\n\t\tvar id string\n\t\tif err := row.Scan(&id, &createdAt, &updatedAt); err != nil {\n\t\t\treturn fmt.Errorf(\"error storing plan build: %v\", err)\n\t\t}\n\n\t\tbuild.Id = id\n\t\tbuild.CreatedAt = createdAt\n\t\tbuild.UpdatedAt = updatedAt\n\t}\n\n\treturn nil\n}\n\nfunc SetBuildError(build *PlanBuild) error {\n\t_, err := Conn.Exec(\"UPDATE plan_builds SET error = $1 WHERE id = $2\", build.Error, build.Id)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting build error: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/context_helpers_conflicts.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\tshared \"plandex-shared\"\n\t\"runtime\"\n\t\"runtime/debug\"\n)\n\ntype invalidateConflictedResultsParams struct {\n\torgId         string\n\tplanId        string\n\tfilesToUpdate map[string]string\n\tdescriptions  []*ConvoMessageDescription\n\tcurrentPlan   *shared.CurrentPlanState\n}\n\nfunc invalidateConflictedResults(params invalidateConflictedResultsParams) error {\n\torgId := params.orgId\n\tplanId := params.planId\n\tfilesToUpdate := params.filesToUpdate\n\n\tvar descriptions []*ConvoMessageDescription\n\n\tif params.descriptions == nil {\n\t\tvar err error\n\t\tdescriptions, err = GetConvoMessageDescriptions(orgId, planId)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting pending build descriptions: %v\", err)\n\t\t}\n\t} else {\n\t\tdescriptions = params.descriptions\n\t}\n\n\tvar currentPlan *shared.CurrentPlanState\n\n\tif params.currentPlan == nil {\n\t\tvar err error\n\t\tcurrentPlan, err = GetCurrentPlanState(CurrentPlanStateParams{\n\t\t\tOrgId:                    orgId,\n\t\t\tPlanId:                   planId,\n\t\t\tConvoMessageDescriptions: descriptions,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting current plan state: %v\", err)\n\t\t}\n\t} else {\n\t\tcurrentPlan = params.currentPlan\n\t}\n\n\tconflictPaths := currentPlan.PlanResult.FileResultsByPath.ConflictedPaths(filesToUpdate)\n\n\t// log.Println(\"invalidateConflictedResults - Conflicted paths:\", conflictPaths)\n\n\tif len(conflictPaths) > 0 {\n\t\ttoUpdateDescs := []*ConvoMessageDescription{}\n\n\t\tfor _, desc := range descriptions {\n\t\t\tif !desc.DidBuild || desc.AppliedAt != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, op := range desc.Operations {\n\t\t\t\tif _, found := conflictPaths[op.Path]; found {\n\t\t\t\t\tif desc.BuildPathsInvalidated == nil {\n\t\t\t\t\t\tdesc.BuildPathsInvalidated = make(map[string]bool)\n\t\t\t\t\t}\n\t\t\t\t\tdesc.BuildPathsInvalidated[op.Path] = true\n\t\t\t\t\ttoUpdateDescs = append(toUpdateDescs, desc)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tnumRoutines := len(toUpdateDescs) + 1\n\t\terrCh := make(chan error, numRoutines)\n\n\t\tfor _, desc := range toUpdateDescs {\n\t\t\tgo func(desc *ConvoMessageDescription) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"panic in StoreDescription: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"panic in StoreDescription: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\terr := StoreDescription(desc)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error storing description: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\terrCh <- nil\n\t\t\t}(desc)\n\t\t}\n\n\t\tgo func() {\n\t\t\terr := DeletePendingResultsForPaths(orgId, planId, conflictPaths)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error deleting pending results: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tfor i := 0; i < numRoutines; i++ {\n\t\t\terr := <-errCh\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error storing description: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/context_helpers_get.go",
    "content": "package db\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n)\n\nfunc GetPlanContexts(orgId, planId string, includeBody, includeMapParts bool) ([]*Context, error) {\n\tvar contexts []*Context\n\tcontextDir := getPlanContextDir(orgId, planId)\n\n\t// get all context files\n\tfiles, err := os.ReadDir(contextDir)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn contexts, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading context dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\tvar mu sync.Mutex\n\n\t// read each context file\n\tfor _, file := range files {\n\t\tif strings.HasSuffix(file.Name(), \".meta\") {\n\t\t\tgo func(file os.DirEntry) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"panic in GetPlanContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tcontext, err := GetContext(orgId, planId, strings.TrimSuffix(file.Name(), \".meta\"), includeBody, includeMapParts)\n\n\t\t\t\tmu.Lock()\n\t\t\t\tdefer mu.Unlock()\n\t\t\t\tcontexts = append(contexts, context)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error reading context file: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\terrCh <- nil\n\t\t\t}(file)\n\t\t} else {\n\t\t\t// only processing meta files here, so just send nil for accurate count\n\t\t\terrCh <- nil\n\t\t}\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading context files: %v\", err)\n\t\t}\n\t}\n\n\t// sort contexts by CreatedAt\n\tsort.Slice(contexts, func(i, j int) bool {\n\t\treturn contexts[i].CreatedAt.Before(contexts[j].CreatedAt)\n\t})\n\n\treturn contexts, nil\n}\n\nfunc GetContext(orgId, planId, contextId string, includeBody, includeMapParts bool) (*Context, error) {\n\tcontextDir := getPlanContextDir(orgId, planId)\n\n\t// read the meta file\n\tmetaPath := filepath.Join(contextDir, contextId+\".meta\")\n\n\tmetaBytes, err := os.ReadFile(metaPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading context meta file: %v\", err)\n\t}\n\n\tvar context Context\n\terr = json.Unmarshal(metaBytes, &context)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling context meta file: %v\", err)\n\t}\n\n\tif includeBody {\n\t\t// read the body file\n\t\tbodyPath := filepath.Join(contextDir, strings.TrimSuffix(contextId, \".meta\")+\".body\")\n\t\tbodyBytes, err := os.ReadFile(bodyPath)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading context body file: %v\", err)\n\t\t}\n\n\t\tcontext.Body = string(bodyBytes)\n\t}\n\n\tif includeMapParts {\n\t\t// read the map parts file\n\t\tmapPartsPath := filepath.Join(contextDir, strings.TrimSuffix(contextId, \".meta\")+\".map-parts\")\n\t\tmapPartsBytes, err := os.ReadFile(mapPartsPath)\n\t\tif !os.IsNotExist(err) {\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error reading context map parts file: %v\", err)\n\t\t\t}\n\n\t\t\terr = json.Unmarshal(mapPartsBytes, &context.MapParts)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error unmarshalling context map parts file: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &context, nil\n}\n"
  },
  {
    "path": "app/server/db/context_helpers_load.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log\"\n\tshared \"plandex-shared\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Ctx is a context.Context - to avoid confusion with Plandex contexts\ntype Ctx context.Context\n\ntype LoadContextsParams struct {\n\tReq                      *shared.LoadContextRequest\n\tOrgId                    string\n\tPlan                     *Plan\n\tBranchName               string\n\tUserId                   string\n\tSkipConflictInvalidation bool\n\tCachedMapsByPath         map[string]*CachedMap\n\tAutoLoaded               bool\n}\n\nfunc LoadContexts(ctx Ctx, params LoadContextsParams) (*shared.LoadContextResponse, []*Context, error) {\n\t// startTime := time.Now()\n\t// showElapsed := func(msg string) {\n\t// \telapsed := time.Since(startTime)\n\t// \tlog.Println(\"LoadContexts\", msg, \"elapsed: %s\\n\", elapsed)\n\t// }\n\n\t// log.Println(\"LoadContexts - params\", spew.Sdump(params))\n\n\treq := params.Req\n\torgId := params.OrgId\n\tplan := params.Plan\n\tplanId := plan.Id\n\tbranchName := params.BranchName\n\tuserId := params.UserId\n\tautoLoaded := params.AutoLoaded\n\n\tfilesToLoad := map[string]string{}\n\tfor _, context := range *req {\n\t\tif context.ContextType == shared.ContextFileType {\n\t\t\tfilesToLoad[context.FilePath] = context.Body\n\t\t}\n\t}\n\n\tif !params.SkipConflictInvalidation {\n\t\terr := invalidateConflictedResults(invalidateConflictedResultsParams{\n\t\t\torgId:         orgId,\n\t\t\tplanId:        planId,\n\t\t\tfilesToUpdate: filesToLoad,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"error invalidating conflicted results: %v\", err)\n\t\t}\n\t}\n\n\ttokensAdded := 0\n\tbasicTokensAdded := 0\n\n\tparamsByTempId := make(map[string]*shared.LoadContextParams)\n\tnumTokensByTempId := make(map[string]int)\n\n\tbranch, err := GetDbBranch(planId, branchName)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error getting branch: %v\", err)\n\t}\n\ttotalTokens := branch.ContextTokens\n\ttotalPlannerTokens := totalTokens\n\ttotalBasicPlannerTokens := 0\n\ttotalMapTokens := 0\n\n\tsettings, err := GetPlanSettings(plan)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error getting settings: %v\", err)\n\t}\n\n\tplanConfig, err := GetPlanConfig(planId)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error getting plan config: %v\", err)\n\t}\n\n\tplannerMaxTokens := settings.GetPlannerEffectiveMaxTokens()\n\tcontextLoaderMaxTokens := settings.GetArchitectEffectiveMaxTokens()\n\n\tmapContextsByFilePath := make(map[string]Context)\n\n\texistingContexts, err := GetPlanContexts(orgId, planId, false, false)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error getting existing contexts: %v\", err)\n\t}\n\n\t// check overall context limits - these should be getting enforced by the client, so just error out if exceeded\n\tnumExistingContexts := len(existingContexts)\n\tif numExistingContexts+len(*req) > shared.MaxContextCount {\n\t\treturn nil, nil, fmt.Errorf(\"too many contexts: %d\", numExistingContexts+len(*req))\n\t}\n\n\tvar totalContextSize int64\n\tfor _, context := range existingContexts {\n\t\ttotalContextSize += context.BodySize\n\t}\n\tfor _, context := range *req {\n\t\tsize := int64(len(context.Body))\n\t\ttotalContextSize += size\n\t\tif size > shared.MaxContextBodySize {\n\t\t\treturn nil, nil, fmt.Errorf(\"context body is too large: %d\", size)\n\t\t}\n\t}\n\n\tif totalContextSize > shared.MaxTotalContextSize {\n\t\treturn nil, nil, fmt.Errorf(\"total context size is too large: %d\", totalContextSize)\n\t}\n\n\texistingContextsByName := make(map[string]bool)\n\tfor _, context := range existingContexts {\n\t\tcomposite := strings.Join([]string{context.Name, string(context.ContextType)}, \"|\")\n\t\texistingContextsByName[composite] = true\n\n\t\tif planConfig.AutoLoadContext && context.ContextType == shared.ContextMapType {\n\t\t\ttotalMapTokens += context.NumTokens\n\t\t\ttotalPlannerTokens -= context.NumTokens\n\t\t}\n\n\t\tif !context.AutoLoaded && context.ContextType != shared.ContextMapType {\n\t\t\ttotalBasicPlannerTokens += context.NumTokens\n\t\t}\n\t}\n\n\tvar filteredReq []*shared.LoadContextParams\n\tfor _, context := range *req {\n\t\tcomposite := strings.Join([]string{context.Name, string(context.ContextType)}, \"|\")\n\t\tif !existingContextsByName[composite] {\n\t\t\tfilteredReq = append(filteredReq, context)\n\t\t}\n\t}\n\n\t*req = filteredReq\n\n\tfor _, contextParams := range *req {\n\t\ttempId := uuid.New().String()\n\n\t\tvar numTokens int\n\t\tvar err error\n\n\t\tvar isMap bool\n\n\t\tif contextParams.ContextType == shared.ContextMapType && (len(contextParams.MapBodies) > 0 || params.CachedMapsByPath != nil) {\n\t\t\tisMap = true\n\t\t\tvar mappedFiles shared.FileMapBodies\n\t\t\tif params.CachedMapsByPath != nil && params.CachedMapsByPath[contextParams.FilePath] != nil {\n\t\t\t\tlog.Println(\"Using cached map for\", contextParams.FilePath)\n\t\t\t\tmappedFiles = params.CachedMapsByPath[contextParams.FilePath].MapParts\n\t\t\t} else {\n\t\t\t\tlog.Println(\"Using map bodies for\", contextParams.FilePath)\n\t\t\t\tmappedFiles = contextParams.MapBodies\n\n\t\t\t\t// check size and num path limits - these should be getting enforced by the client, so just error out if exceeded\n\t\t\t\tif len(mappedFiles) > shared.MaxContextMapPaths {\n\t\t\t\t\treturn nil, nil, fmt.Errorf(\"map has too many paths: %d\", len(mappedFiles))\n\t\t\t\t}\n\n\t\t\t\ttotalMapSize := 0\n\t\t\t\tfor _, body := range mappedFiles {\n\t\t\t\t\tnumBytes := len(body)\n\t\t\t\t\ttotalMapSize += numBytes\n\t\t\t\t\tif numBytes > shared.MaxContextMapSingleInputSize {\n\t\t\t\t\t\treturn nil, nil, fmt.Errorf(\"map input %s is too large: %d\", contextParams.FilePath, numBytes)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif totalMapSize > shared.MaxContextBodySize {\n\t\t\t\t\treturn nil, nil, fmt.Errorf(\"map is too large: %d\", totalMapSize)\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\tvar mapShas map[string]string\n\t\t\tvar mapTokens map[string]int\n\t\t\tvar mapSizes map[string]int64\n\n\t\t\tif params.CachedMapsByPath != nil && params.CachedMapsByPath[contextParams.FilePath] != nil {\n\t\t\t\tmapShas = params.CachedMapsByPath[contextParams.FilePath].MapShas\n\t\t\t\tmapTokens = params.CachedMapsByPath[contextParams.FilePath].MapTokens\n\t\t\t\tmapSizes = params.CachedMapsByPath[contextParams.FilePath].MapSizes\n\t\t\t} else {\n\t\t\t\tmapShas = contextParams.InputShas\n\t\t\t\tmapTokens = contextParams.InputTokens\n\t\t\t\tmapSizes = contextParams.InputSizes\n\t\t\t}\n\n\t\t\tcombinedBody := mappedFiles.CombinedMap(mapTokens)\n\t\t\tnumTokens = shared.GetNumTokensEstimate(combinedBody)\n\n\t\t\tautoLoaded = autoLoaded || contextParams.AutoLoaded\n\n\t\t\tlog.Println(\"LoadContexts - map - autoLoaded\", autoLoaded)\n\n\t\t\tnewContext := Context{\n\t\t\t\t// Id generated by db layer\n\t\t\t\tOrgId:       orgId,\n\t\t\t\tOwnerId:     userId,\n\t\t\t\tPlanId:      planId,\n\t\t\t\tProjectId:   plan.ProjectId,\n\t\t\t\tContextType: shared.ContextMapType,\n\t\t\t\tName:        contextParams.Name,\n\t\t\t\tUrl:         contextParams.Url,\n\t\t\t\tFilePath:    contextParams.FilePath,\n\t\t\t\tNumTokens:   numTokens,\n\t\t\t\tBody:        combinedBody,\n\t\t\t\tMapParts:    mappedFiles,\n\t\t\t\tMapShas:     mapShas,\n\t\t\t\tMapTokens:   mapTokens,\n\t\t\t\tMapSizes:    mapSizes,\n\t\t\t\tAutoLoaded:  autoLoaded || contextParams.AutoLoaded,\n\t\t\t}\n\n\t\t\tmapContextsByFilePath[contextParams.FilePath] = newContext\n\n\t\t} else if contextParams.ContextType == shared.ContextImageType {\n\t\t\tnumTokens, err = shared.GetImageTokens(contextParams.Body, contextParams.ImageDetail)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"error getting image num tokens: %v\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tnumTokens = shared.GetNumTokensEstimate(contextParams.Body)\n\t\t}\n\n\t\tparamsByTempId[tempId] = contextParams\n\t\tnumTokensByTempId[tempId] = numTokens\n\t\ttotalTokens += numTokens\n\n\t\t// maps don't count toward the token limit if auto-loading\n\t\tif planConfig.AutoLoadContext && isMap {\n\t\t\ttokensAdded += numTokens\n\t\t\ttotalMapTokens += numTokens\n\t\t} else if autoLoaded {\n\t\t\ttokensAdded += numTokens\n\t\t\ttotalPlannerTokens += numTokens\n\t\t} else {\n\t\t\ttokensAdded += numTokens\n\t\t\ttotalPlannerTokens += numTokens\n\t\t\ttotalBasicPlannerTokens += numTokens\n\t\t\tbasicTokensAdded += numTokens\n\t\t}\n\t}\n\n\t// showElapsed(\"Loaded reqs\")\n\tif planConfig.AutoLoadContext {\n\t\tif totalMapTokens > contextLoaderMaxTokens {\n\t\t\treturn &shared.LoadContextResponse{\n\t\t\t\tTokensAdded:       tokensAdded,\n\t\t\t\tTotalTokens:       totalMapTokens,\n\t\t\t\tMaxTokens:         contextLoaderMaxTokens,\n\t\t\t\tMaxTokensExceeded: true,\n\t\t\t}, nil, nil\n\t\t}\n\n\t\tif totalBasicPlannerTokens > plannerMaxTokens {\n\t\t\treturn &shared.LoadContextResponse{\n\t\t\t\tTokensAdded:       basicTokensAdded,\n\t\t\t\tTotalTokens:       totalBasicPlannerTokens,\n\t\t\t\tMaxTokens:         plannerMaxTokens,\n\t\t\t\tMaxTokensExceeded: true,\n\t\t\t}, nil, nil\n\t\t}\n\t} else {\n\t\tif totalTokens > plannerMaxTokens {\n\t\t\treturn &shared.LoadContextResponse{\n\t\t\t\tTokensAdded:       tokensAdded,\n\t\t\t\tTotalTokens:       totalTokens,\n\t\t\t\tMaxTokens:         plannerMaxTokens,\n\t\t\t\tMaxTokensExceeded: true,\n\t\t\t}, nil, nil\n\t\t}\n\t}\n\n\tvar dbContexts []*Context\n\tvar apiContexts []*shared.Context\n\tvar mu sync.Mutex\n\n\terrCh := make(chan error, len(paramsByTempId))\n\tfor tempId, loadParams := range paramsByTempId {\n\n\t\tgo func(tempId string, loadParams *shared.LoadContextParams) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in LoadContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in LoadContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\thash := sha256.Sum256([]byte(loadParams.Body))\n\t\t\tsha := hex.EncodeToString(hash[:])\n\n\t\t\tvar context Context\n\t\t\tif mapContext, ok := mapContextsByFilePath[loadParams.FilePath]; ok {\n\t\t\t\tcontext = mapContext\n\t\t\t} else {\n\t\t\t\t// log.Println(\"tempId\", tempId, \"params.FilePath\", params.FilePath, \"sha\", sha)\n\t\t\t\t// log.Println(\"params.Body\", params.Body)\n\n\t\t\t\tcontext = Context{\n\t\t\t\t\t// Id generated by db layer\n\t\t\t\t\tOrgId:           orgId,\n\t\t\t\t\tOwnerId:         userId,\n\t\t\t\t\tPlanId:          planId,\n\t\t\t\t\tProjectId:       plan.ProjectId,\n\t\t\t\t\tContextType:     loadParams.ContextType,\n\t\t\t\t\tName:            loadParams.Name,\n\t\t\t\t\tUrl:             loadParams.Url,\n\t\t\t\t\tFilePath:        loadParams.FilePath,\n\t\t\t\t\tNumTokens:       numTokensByTempId[tempId],\n\t\t\t\t\tSha:             sha,\n\t\t\t\t\tBody:            loadParams.Body,\n\t\t\t\t\tForceSkipIgnore: loadParams.ForceSkipIgnore,\n\t\t\t\t\tImageDetail:     loadParams.ImageDetail,\n\t\t\t\t\tAutoLoaded:      autoLoaded || loadParams.AutoLoaded,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr := StoreContext(&context, params.CachedMapsByPath != nil)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error storing context: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tdbContexts = append(dbContexts, &context)\n\t\t\tapiContext := context.ToApi()\n\t\t\tapiContext.Body = \"\"\n\t\t\tapiContexts = append(apiContexts, apiContext)\n\t\t\tmu.Unlock()\n\n\t\t\terrCh <- nil\n\t\t}(tempId, loadParams)\n\t}\n\n\tfor i := 0; i < len(paramsByTempId); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"error storing context: %v\", err)\n\t\t}\n\t}\n\n\terr = AddPlanContextTokens(planId, branchName, tokensAdded)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error adding plan context tokens: %v\", err)\n\t}\n\n\tcommitMsg := shared.SummaryForLoadContext(apiContexts, tokensAdded, totalTokens)\n\n\tif len(apiContexts) > 0 {\n\t\tcommitMsg += \"\\n\\n\" + shared.TableForLoadContext(apiContexts, false)\n\t}\n\n\treturn &shared.LoadContextResponse{\n\t\tTokensAdded: tokensAdded,\n\t\tTotalTokens: totalTokens,\n\t\tMsg:         commitMsg,\n\t}, dbContexts, nil\n}\n"
  },
  {
    "path": "app/server/db/context_helpers_map.go",
    "content": "package db\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\tshared \"plandex-shared\"\n)\n\nfunc GetCachedMap(orgId, projectId, filePath string) (*Context, error) {\n\tmapCacheDir := getProjectMapCacheDir(orgId, projectId)\n\n\tfilePathHash := md5.Sum([]byte(filePath))\n\tfilePathHashStr := hex.EncodeToString(filePathHash[:])\n\n\tmapCachePath := filepath.Join(mapCacheDir, filePathHashStr+\".json\")\n\n\tlog.Println(\"GetCachedMap - mapCachePath\", mapCachePath)\n\n\tmapCacheBytes, err := os.ReadFile(mapCachePath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading cached map: %v\", err)\n\t}\n\n\tvar context Context\n\terr = json.Unmarshal(mapCacheBytes, &context)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling cached map: %v\", err)\n\t}\n\n\treturn &context, nil\n}\n\ntype CachedMap struct {\n\tMapParts  shared.FileMapBodies\n\tMapShas   map[string]string\n\tMapTokens map[string]int\n\tMapSizes  map[string]int64\n}\n"
  },
  {
    "path": "app/server/db/context_helpers_remove.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\tshared \"plandex-shared\"\n\t\"runtime\"\n\t\"runtime/debug\"\n)\n\nfunc ContextRemove(orgId, planId string, contexts []*Context) error {\n\treturn contextRemove(contextRemoveParams{\n\t\torgId:    orgId,\n\t\tplanId:   planId,\n\t\tcontexts: contexts,\n\t})\n}\n\ntype contextRemoveParams struct {\n\torgId        string\n\tplanId       string\n\tcontexts     []*Context\n\tdescriptions []*ConvoMessageDescription\n\tcurrentPlan  *shared.CurrentPlanState\n}\n\nfunc contextRemove(params contextRemoveParams) error {\n\torgId := params.orgId\n\tplanId := params.planId\n\tcontexts := params.contexts\n\n\t// remove files\n\tnumFiles := 0\n\n\tfilesToUpdate := make(map[string]string)\n\n\terrCh := make(chan error, numFiles)\n\tfor _, context := range contexts {\n\t\tfilesToUpdate[context.FilePath] = \"\"\n\t\tcontextDir := getPlanContextDir(orgId, planId)\n\t\tfor _, ext := range []string{\".meta\", \".body\", \".map-parts\"} {\n\t\t\tnumFiles++\n\t\t\tgo func(context *Context, dir, ext string) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"panic in contextRemove: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"panic in contextRemove: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\terrCh <- os.Remove(filepath.Join(dir, context.Id+ext))\n\t\t\t}(context, contextDir, ext)\n\t\t}\n\t}\n\n\tfor i := 0; i < numFiles; i++ {\n\t\terr := <-errCh\n\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\treturn fmt.Errorf(\"error removing context file: %v\", err)\n\t\t}\n\t}\n\n\terr := invalidateConflictedResults(invalidateConflictedResultsParams{\n\t\torgId:         orgId,\n\t\tplanId:        planId,\n\t\tfilesToUpdate: filesToUpdate,\n\t\tdescriptions:  params.descriptions,\n\t\tcurrentPlan:   params.currentPlan,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error invalidating conflicted results: %v\", err)\n\t}\n\n\treturn nil\n}\n\ntype ClearContextParams struct {\n\tOrgId       string\n\tPlanId      string\n\tSkipMaps    bool\n\tSkipPending bool\n}\n\nfunc ClearContext(params ClearContextParams) error {\n\torgId := params.OrgId\n\tplanId := params.PlanId\n\tskipMaps := params.SkipMaps\n\tskipPending := params.SkipPending\n\n\tcontexts, err := GetPlanContexts(orgId, planId, false, false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting plan contexts: %v\", err)\n\t}\n\n\tvar descriptions []*ConvoMessageDescription\n\tvar currentPlan *shared.CurrentPlanState\n\n\tif skipPending {\n\t\tvar err error\n\t\tdescriptions, err = GetConvoMessageDescriptions(orgId, planId)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting pending build descriptions: %v\", err)\n\t\t}\n\n\t\tcurrentPlan, err = GetCurrentPlanState(CurrentPlanStateParams{\n\t\t\tOrgId:                    orgId,\n\t\t\tPlanId:                   planId,\n\t\t\tConvoMessageDescriptions: descriptions,\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting current plan state: %v\", err)\n\t\t}\n\t}\n\n\ttoRemove := []*Context{}\n\n\tfor _, context := range contexts {\n\t\tshouldSkip := false\n\n\t\tif !(skipMaps && context.ContextType == shared.ContextMapType) {\n\t\t\tshouldSkip = true\n\t\t}\n\n\t\tif skipPending && currentPlan.CurrentPlanFiles.Files[context.FilePath] != \"\" {\n\t\t\tshouldSkip = true\n\t\t}\n\n\t\tif !shouldSkip {\n\t\t\ttoRemove = append(toRemove, context)\n\t\t}\n\t}\n\n\tif len(toRemove) > 0 {\n\t\terr := contextRemove(contextRemoveParams{\n\t\t\torgId:        orgId,\n\t\t\tplanId:       planId,\n\t\t\tcontexts:     toRemove,\n\t\t\tdescriptions: descriptions,\n\t\t\tcurrentPlan:  currentPlan,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error removing non-map contexts: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/context_helpers_store.go",
    "content": "package db\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/google/uuid\"\n)\n\nfunc StoreContext(context *Context, skipMapCache bool) error {\n\t// log.Println(\"StoreContext - Storing context\", context.Id, context.Name, context.ContextType)\n\t// log.Println(\"StoreContext - Num tokens\", context.NumTokens)\n\n\tcontextDir := getPlanContextDir(context.OrgId, context.PlanId)\n\n\terr := os.MkdirAll(contextDir, os.ModePerm)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating context dir: %v\", err)\n\t}\n\n\tts := time.Now().UTC()\n\tif context.Id == \"\" {\n\t\tcontext.Id = uuid.New().String()\n\t\tcontext.CreatedAt = ts\n\t}\n\tcontext.UpdatedAt = ts\n\tcontext.BodySize = int64(len(context.Body))\n\n\tmetaFilename := context.Id + \".meta\"\n\tmetaPath := filepath.Join(contextDir, metaFilename)\n\n\toriginalBody := context.Body\n\toriginalBody = strings.ReplaceAll(originalBody, \"\\\\`\\\\`\\\\`\", \"\\\\\\\\`\\\\\\\\`\\\\\\\\`\")\n\toriginalBody = strings.ReplaceAll(originalBody, \"```\", \"\\\\`\\\\`\\\\`\")\n\n\tbodyFilename := context.Id + \".body\"\n\tbodyPath := filepath.Join(contextDir, bodyFilename)\n\tbody := []byte(originalBody)\n\tcontext.Body = \"\"\n\n\toriginalMapParts := context.MapParts\n\tvar mapPath string\n\tvar mapBytes []byte\n\tif len(context.MapParts) > 0 {\n\t\tmapFilename := context.Id + \".map-parts\"\n\t\tmapPath = filepath.Join(contextDir, mapFilename)\n\t\tmapBytes, err = json.MarshalIndent(context.MapParts, \"\", \"  \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal map parts: %v\", err)\n\t\t}\n\t\tcontext.MapParts = nil\n\t}\n\n\t// Convert the ModelContextPart to JSON\n\tdata, err := json.MarshalIndent(context, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal context context: %v\", err)\n\t}\n\n\t// Write the body to the file\n\tif err = os.WriteFile(bodyPath, body, 0644); err != nil {\n\t\treturn fmt.Errorf(\"failed to write context body to file %s: %v\", bodyPath, err)\n\t}\n\n\t// Write the meta data to the file\n\tif err = os.WriteFile(metaPath, data, 0644); err != nil {\n\t\treturn fmt.Errorf(\"failed to write context meta to file %s: %v\", metaPath, err)\n\t}\n\n\tif mapPath != \"\" {\n\t\tif err = os.WriteFile(mapPath, mapBytes, 0644); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write context map to file %s: %v\", mapPath, err)\n\t\t}\n\t}\n\n\tcontext.Body = originalBody\n\tcontext.MapParts = originalMapParts\n\n\tif mapPath != \"\" && !skipMapCache {\n\t\tlog.Println(\"StoreContext - context.MapParts length\", len(context.MapParts))\n\n\t\tmapCacheDir := getProjectMapCacheDir(context.OrgId, context.ProjectId)\n\n\t\t// ensure map cache dir exists\n\t\terr = os.MkdirAll(mapCacheDir, os.ModePerm)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating map cache dir: %v\", err)\n\t\t}\n\n\t\tfilePathHash := md5.Sum([]byte(context.FilePath))\n\t\tfilePathHashStr := hex.EncodeToString(filePathHash[:])\n\n\t\tmapCachePath := filepath.Join(mapCacheDir, filePathHashStr+\".json\")\n\n\t\tlog.Println(\"StoreContext - mapCachePath\", mapCachePath)\n\n\t\tcachedContext := Context{\n\t\t\tContextType: shared.ContextMapType,\n\t\t\tFilePath:    context.FilePath,\n\t\t\tName:        context.Name,\n\t\t\tBody:        context.Body,\n\t\t\tNumTokens:   context.NumTokens,\n\t\t\tMapParts:    context.MapParts,\n\t\t\tMapShas:     context.MapShas,\n\t\t\tMapTokens:   context.MapTokens,\n\t\t\tMapSizes:    context.MapSizes,\n\t\t\tUpdatedAt:   context.UpdatedAt,\n\t\t}\n\n\t\tcachedContextBytes, err := json.MarshalIndent(cachedContext, \"\", \"  \")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal cached context: %v\", err)\n\t\t}\n\n\t\terr = os.WriteFile(mapCachePath, cachedContextBytes, 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write context map to file %s: %v\", mapCachePath, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/context_helpers_update.go",
    "content": "package db\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log\"\n\tshared \"plandex-shared\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sync\"\n)\n\ntype UpdateContextsParams struct {\n\tReq                      *shared.UpdateContextRequest\n\tOrgId                    string\n\tPlan                     *Plan\n\tBranchName               string\n\tContextsById             map[string]*Context\n\tSkipConflictInvalidation bool\n}\n\nfunc UpdateContexts(params UpdateContextsParams) (*shared.UpdateContextResponse, error) {\n\treq := params.Req\n\torgId := params.OrgId\n\tplan := params.Plan\n\tplanId := plan.Id\n\tbranchName := params.BranchName\n\n\tbranch, err := GetDbBranch(planId, branchName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting branch: %v\", err)\n\t}\n\n\tif branch == nil {\n\t\treturn nil, fmt.Errorf(\"branch not found\")\n\t}\n\n\ttotalTokens := branch.ContextTokens\n\ttotalPlannerTokens := totalTokens\n\n\ttotalMapTokens := 0\n\n\ttotalBasicPlannerTokens := 0\n\tfor _, context := range params.ContextsById {\n\t\tif context.ContextType != shared.ContextMapType && !context.AutoLoaded {\n\t\t\ttotalBasicPlannerTokens += context.NumTokens\n\t\t}\n\t}\n\n\tsettings, err := GetPlanSettings(plan)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting settings: %v\", err)\n\t}\n\n\tplanConfig, err := GetPlanConfig(planId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan config: %v\", err)\n\t}\n\n\tmodelPacks, err := ListModelPacks(orgId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting model packs: %v\", err)\n\t}\n\n\tapiModelPacks := make([]*shared.ModelPack, len(modelPacks))\n\tfor i, modelPack := range modelPacks {\n\t\tapiModelPacks[i] = modelPack.ToApi()\n\t}\n\n\tplannerMaxTokens := settings.GetPlannerEffectiveMaxTokens()\n\tcontextLoaderMaxTokens := settings.GetArchitectEffectiveMaxTokens()\n\n\tif planConfig.AutoLoadContext {\n\t\texistingContexts, err := GetPlanContexts(orgId, planId, false, false)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting existing contexts: %v\", err)\n\t\t}\n\n\t\tfor _, context := range existingContexts {\n\t\t\tif context.ContextType == shared.ContextMapType {\n\t\t\t\ttotalMapTokens += context.NumTokens\n\t\t\t\ttotalPlannerTokens -= context.NumTokens\n\t\t\t}\n\t\t}\n\t}\n\n\taggregateTokensDiff := 0\n\taggregateBasicTokensDiff := 0\n\ttokenDiffsById := make(map[string]int)\n\n\tvar contextsById map[string]*Context\n\tif params.ContextsById == nil {\n\t\tcontextsById = make(map[string]*Context)\n\t} else {\n\t\tcontextsById = params.ContextsById\n\t}\n\n\tvar totalContextCount int\n\tvar totalBodySize int64\n\n\tfor _, context := range contextsById {\n\t\ttotalContextCount++\n\t\ttotalBodySize += context.BodySize\n\t}\n\n\tfor id, params := range *req {\n\t\tsize := int64(len(params.Body))\n\n\t\tif size > shared.MaxContextBodySize {\n\t\t\treturn nil, fmt.Errorf(\"context body is too large: %d\", size)\n\t\t}\n\n\t\tif context, ok := contextsById[id]; ok {\n\t\t\ttotalBodySize += size - context.BodySize\n\t\t} else {\n\t\t\ttotalContextCount++\n\t\t\ttotalBodySize += size\n\t\t}\n\t}\n\n\tif totalContextCount > shared.MaxContextCount {\n\t\treturn nil, fmt.Errorf(\"too many contexts to update (found %d, limit is %d)\", totalContextCount, shared.MaxContextCount)\n\t}\n\n\tif totalBodySize > shared.MaxContextBodySize {\n\t\treturn nil, fmt.Errorf(\"total context body size exceeds limit (size %.2f MB, limit %d MB)\", float64(totalBodySize)/1024/1024, int(shared.MaxContextBodySize)/1024/1024)\n\t}\n\n\tvar updatedContexts []*shared.Context\n\n\tnumFiles := 0\n\tnumUrls := 0\n\tnumTrees := 0\n\tnumMaps := 0\n\n\tvar mu sync.Mutex\n\terrCh := make(chan error, len(*req))\n\n\tfor id, params := range *req {\n\t\tgo func(id string, params *shared.UpdateContextParams) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in UpdateContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in UpdateContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tvar context *Context\n\t\t\tif _, ok := contextsById[id]; ok {\n\t\t\t\tcontext = contextsById[id]\n\t\t\t} else {\n\t\t\t\tvar err error\n\t\t\t\tcontext, err = GetContext(orgId, planId, id, true, true)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting context: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// log.Println(\"Got context\", context.Id, \"numTokens\", context.NumTokens)\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\n\t\t\tcontextsById[id] = context\n\t\t\tupdatedContexts = append(updatedContexts, context.ToApi())\n\n\t\t\tif context.ContextType != shared.ContextMapType {\n\t\t\t\tvar updateNumTokens int\n\t\t\t\tvar err error\n\n\t\t\t\tif context.ContextType == shared.ContextImageType {\n\t\t\t\t\tupdateNumTokens, err = shared.GetImageTokens(params.Body, context.ImageDetail)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"error getting num tokens: %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tupdateNumTokens = shared.GetNumTokensEstimate(params.Body)\n\t\t\t\t\t// log.Println(\"len(params.Body)\", len(params.Body))\n\t\t\t\t}\n\n\t\t\t\t// log.Println(\"Updating context\", id, \"updateNumTokens\", updateNumTokens)\n\n\t\t\t\ttokenDiff := updateNumTokens - context.NumTokens\n\t\t\t\ttokenDiffsById[id] = tokenDiff\n\t\t\t\taggregateTokensDiff += tokenDiff\n\t\t\t\ttotalTokens += tokenDiff\n\t\t\t\ttotalPlannerTokens += tokenDiff\n\t\t\t\tif !context.AutoLoaded {\n\t\t\t\t\ttotalBasicPlannerTokens += tokenDiff\n\t\t\t\t\taggregateBasicTokensDiff += tokenDiff\n\t\t\t\t}\n\t\t\t\tcontext.NumTokens = updateNumTokens\n\t\t\t}\n\n\t\t\tswitch context.ContextType {\n\t\t\tcase shared.ContextFileType:\n\t\t\t\tnumFiles++\n\t\t\tcase shared.ContextURLType:\n\t\t\t\tnumUrls++\n\t\t\tcase shared.ContextDirectoryTreeType:\n\t\t\t\tnumTrees++\n\t\t\tcase shared.ContextMapType:\n\t\t\t\tnumMaps++\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}(id, params)\n\t}\n\n\tfor i := 0; i < len(*req); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting context: %v\", err)\n\t\t}\n\t}\n\n\tif planConfig.AutoLoadContext {\n\t\tif totalBasicPlannerTokens > plannerMaxTokens {\n\t\t\treturn &shared.UpdateContextResponse{\n\t\t\t\tTokensAdded:       aggregateTokensDiff,\n\t\t\t\tTotalTokens:       totalBasicPlannerTokens,\n\t\t\t\tMaxTokens:         plannerMaxTokens,\n\t\t\t\tMaxTokensExceeded: true,\n\t\t\t}, nil\n\t\t}\n\t}\n\tfilesToLoad := map[string]string{}\n\tfor _, context := range updatedContexts {\n\t\tif context.ContextType == shared.ContextFileType {\n\t\t\tfilesToLoad[context.FilePath] = (*req)[context.Id].Body\n\t\t}\n\t}\n\n\tif !params.SkipConflictInvalidation {\n\t\terr = invalidateConflictedResults(invalidateConflictedResultsParams{\n\t\t\torgId:         orgId,\n\t\t\tplanId:        planId,\n\t\t\tfilesToUpdate: filesToLoad,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error invalidating conflicted results: %v\", err)\n\t\t}\n\t}\n\n\terrCh = make(chan error, len(*req))\n\n\tfor id, params := range *req {\n\t\tgo func(id string, params *shared.UpdateContextParams) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in UpdateContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in UpdateContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tcontext := contextsById[id]\n\n\t\t\tif context.ContextType == shared.ContextMapType {\n\t\t\t\toldNumTokens := context.NumTokens\n\n\t\t\t\tfor path, part := range params.MapBodies {\n\t\t\t\t\tif len(part) > shared.MaxContextMapSingleInputSize {\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"map input %s is too large: %d\", path, len(part))\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tif context.MapParts == nil {\n\t\t\t\t\t\tcontext.MapParts = make(shared.FileMapBodies)\n\t\t\t\t\t}\n\t\t\t\t\tif context.MapShas == nil {\n\t\t\t\t\t\tcontext.MapShas = make(map[string]string)\n\t\t\t\t\t}\n\t\t\t\t\tif context.MapTokens == nil {\n\t\t\t\t\t\tcontext.MapTokens = make(map[string]int)\n\t\t\t\t\t}\n\t\t\t\t\tif context.MapSizes == nil {\n\t\t\t\t\t\tcontext.MapSizes = make(map[string]int64)\n\t\t\t\t\t}\n\n\t\t\t\t\t// prevNumTokens := context.MapTokens[path]\n\n\t\t\t\t\tcontext.MapParts[path] = part\n\t\t\t\t\tcontext.MapShas[path] = params.InputShas[path]\n\t\t\t\t\tcontext.MapTokens[path] = params.InputTokens[path]\n\t\t\t\t\tcontext.MapSizes[path] = params.InputSizes[path]\n\t\t\t\t}\n\n\t\t\t\tfor _, path := range params.RemovedMapPaths {\n\t\t\t\t\tdelete(context.MapParts, path)\n\t\t\t\t\tdelete(context.MapShas, path)\n\t\t\t\t\tdelete(context.MapTokens, path)\n\t\t\t\t\tdelete(context.MapSizes, path)\n\t\t\t\t}\n\n\t\t\t\tif len(context.MapParts) > shared.MaxContextMapPaths {\n\t\t\t\t\terrCh <- fmt.Errorf(\"map has too many paths: %d\", len(context.MapParts))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\ttotalMapSize := 0\n\t\t\t\tfor _, part := range context.MapParts {\n\t\t\t\t\ttotalMapSize += len(part)\n\t\t\t\t}\n\t\t\t\tif totalMapSize > shared.MaxContextBodySize {\n\t\t\t\t\terrCh <- fmt.Errorf(\"map total size is too large: %d\", totalMapSize)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tcontext.Body = context.MapParts.CombinedMap(context.MapTokens)\n\t\t\t\tnewNumTokens := shared.GetNumTokensEstimate(context.Body)\n\t\t\t\ttokenDiff := newNumTokens - oldNumTokens\n\n\t\t\t\tmu.Lock()\n\t\t\t\ttokenDiffsById[id] = tokenDiff\n\t\t\t\taggregateTokensDiff += tokenDiff\n\t\t\t\ttotalTokens += tokenDiff\n\t\t\t\tif planConfig.AutoLoadContext {\n\t\t\t\t\ttotalMapTokens += tokenDiff\n\t\t\t\t} else {\n\t\t\t\t\ttotalPlannerTokens += tokenDiff\n\t\t\t\t}\n\t\t\t\tmu.Unlock()\n\n\t\t\t\tcontext.NumTokens = newNumTokens\n\t\t\t} else {\n\t\t\t\tcontext.Body = params.Body\n\t\t\t\thash := sha256.Sum256([]byte(context.Body))\n\t\t\t\tcontext.Sha = hex.EncodeToString(hash[:])\n\t\t\t}\n\n\t\t\t// log.Println(\"storing context\", id)\n\t\t\t// log.Printf(\"context name: %s, sha: %s\\n\", context.Name, context.Sha)\n\n\t\t\terr := StoreContext(context, false)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error storing context: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// log.Println(\"stored context\", id)\n\t\t\t// log.Println()\n\n\t\t\terrCh <- nil\n\t\t}(id, params)\n\t}\n\n\tfor i := 0; i < len(*req); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error storing context: %v\", err)\n\t\t}\n\t}\n\n\tif planConfig.AutoLoadContext {\n\t\tif totalMapTokens > contextLoaderMaxTokens {\n\t\t\treturn &shared.UpdateContextResponse{\n\t\t\t\tTokensAdded:       aggregateTokensDiff,\n\t\t\t\tTotalTokens:       totalTokens,\n\t\t\t\tMaxTokens:         contextLoaderMaxTokens,\n\t\t\t\tMaxTokensExceeded: true,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\tupdateRes := &shared.ContextUpdateResult{\n\t\tUpdatedContexts: updatedContexts,\n\t\tTokenDiffsById:  tokenDiffsById,\n\t\tTokensDiff:      aggregateTokensDiff,\n\t\tTotalTokens:     totalTokens,\n\t\tNumFiles:        numFiles,\n\t\tNumUrls:         numUrls,\n\t\tNumTrees:        numTrees,\n\t\tNumMaps:         numMaps,\n\t\tMaxTokens:       plannerMaxTokens,\n\t}\n\n\terr = AddPlanContextTokens(planId, branchName, aggregateTokensDiff)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error adding plan context tokens: %v\", err)\n\t}\n\n\tcommitMsg := shared.SummaryForUpdateContext(shared.SummaryForUpdateContextParams{\n\t\tNumFiles:    numFiles,\n\t\tNumTrees:    numTrees,\n\t\tNumUrls:     numUrls,\n\t\tNumMaps:     numMaps,\n\t\tTokensDiff:  aggregateTokensDiff,\n\t\tTotalTokens: totalTokens,\n\t}) + \"\\n\\n\" + shared.TableForContextUpdate(updateRes)\n\treturn &shared.LoadContextResponse{\n\t\tTokensAdded: aggregateTokensDiff,\n\t\tTotalTokens: totalTokens,\n\t\tMsg:         commitMsg,\n\t}, nil\n}\n"
  },
  {
    "path": "app/server/db/convo_helpers.go",
    "content": "package db\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/google/uuid\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc GetPlanConvo(orgId, planId string) ([]*ConvoMessage, error) {\n\tvar convo []*ConvoMessage\n\tconvoDir := getPlanConversationDir(orgId, planId)\n\n\tfiles, err := os.ReadDir(convoDir)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn convo, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading convo dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\tconvoCh := make(chan *ConvoMessage, len(files))\n\n\tfor _, file := range files {\n\t\tgo func(file os.DirEntry) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanConvo: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanConvo: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tbytes, err := os.ReadFile(filepath.Join(convoDir, file.Name()))\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading convo file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar convoMessage ConvoMessage\n\t\t\terr = json.Unmarshal(bytes, &convoMessage)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error unmarshalling convo file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconvoCh <- &convoMessage\n\n\t\t}(file)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\treturn nil, fmt.Errorf(\"error reading convo files: %v\", err)\n\t\tcase convoMessage := <-convoCh:\n\t\t\tconvo = append(convo, convoMessage)\n\t\t}\n\t}\n\n\tsort.Slice(convo, func(i, j int) bool {\n\t\treturn convo[i].CreatedAt.Before(convo[j].CreatedAt)\n\t})\n\n\treturn convo, nil\n}\n\nfunc GetConvoMessage(orgId, planId, messageId string) (*ConvoMessage, error) {\n\tconvoDir := getPlanConversationDir(orgId, planId)\n\n\tfilePath := filepath.Join(convoDir, messageId+\".json\")\n\n\tbytes, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading convo message: %v\", err)\n\t}\n\n\tvar convoMessage ConvoMessage\n\terr = json.Unmarshal(bytes, &convoMessage)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling convo message: %v\", err)\n\t}\n\n\treturn &convoMessage, nil\n}\n\nfunc StoreConvoMessage(repo *GitRepo, message *ConvoMessage, currentUserId, branch string, commit bool) (string, error) {\n\tconvoDir := getPlanConversationDir(message.OrgId, message.PlanId)\n\n\tts := time.Now().UTC()\n\n\tif message.Id == \"\" {\n\t\tmessage.Id = uuid.New().String()\n\t}\n\n\tmessage.CreatedAt = ts\n\n\tbytes, err := json.Marshal(message)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error marshalling convo message: %v\", err)\n\t}\n\n\terr = os.MkdirAll(convoDir, os.ModePerm)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error creating convo dir: %v\", err)\n\t}\n\n\terr = os.WriteFile(filepath.Join(convoDir, message.Id+\".json\"), bytes, os.ModePerm)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error writing convo message: %v\", err)\n\t}\n\n\terr = AddPlanConvoMessage(message, branch)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error adding convo tokens: %v\", err)\n\t}\n\n\tvar desc string\n\tif message.Role == openai.ChatMessageRoleUser {\n\t\tdesc = \"💬 User prompt\"\n\t\t// TODO: add user name\n\t} else {\n\t\tdesc = \"🤖 Plandex reply\"\n\t\tif message.Stopped {\n\t\t\tdesc += \" | 🛑 \" + color.New(color.FgHiRed).Sprint(\"stopped\")\n\t\t}\n\t}\n\n\treplyTags := message.Flags.GetReplyTags()\n\n\tvar msg string\n\tif len(replyTags) > 0 {\n\t\tmsg = fmt.Sprintf(\"Message #%d | %s | %s | %d 🪙\", message.Num, desc, strings.Join(replyTags, \" | \"), message.Tokens)\n\t} else {\n\t\tmsg = fmt.Sprintf(\"Message #%d | %s | %d 🪙\", message.Num, desc, message.Tokens)\n\t}\n\n\tif len(message.AddedSubtasks) > 0 {\n\t\tmsg += \"\\n\\n\"\n\t\tfor _, subtask := range message.AddedSubtasks {\n\t\t\tmsg += \"\\n• \" + subtask.Title\n\t\t}\n\t}\n\n\tif len(message.RemovedSubtasks) > 0 {\n\t\tmsg += \"\\n\\n\"\n\t\tmsg += \"Removed Tasks\"\n\t\tfor _, subtask := range message.RemovedSubtasks {\n\t\t\tmsg += \"\\n• \" + subtask\n\t\t}\n\t}\n\n\tlog.Println(\"StoreConvoMessage - message.Flags.CurrentStage.TellStage:\", message.Flags.CurrentStage.TellStage)\n\tlog.Println(\"StoreConvoMessage - message.Subtask:\", message.Subtask)\n\n\tif message.Flags.CurrentStage.TellStage == shared.TellStageImplementation && message.Subtask != nil {\n\t\tmsg += \"\\n\\n\" + \"📋 \" + message.Subtask.Title\n\t\tif len(message.Subtask.UsesFiles) > 0 {\n\t\t\tfor _, file := range message.Subtask.UsesFiles {\n\t\t\t\tmsg += \"\\n • 📄 \" + file\n\t\t\t}\n\t\t}\n\t}\n\n\tif message.Flags.DidCompletePlan {\n\t\tmsg += \"\\n\\n\" + \"🏁 Completed Plan\"\n\t}\n\n\t// Cleaner without the cut off message - maybe need a separate command to show both the log and full messages?\n\t// cutoff := 140\n\t// if len(message.Message) > cutoff {\n\t// \tmsg += \"\\n\\n\" + message.Message[:cutoff] + \"...\"\n\t// } else {\n\t// \tmsg += \"\\n\\n\" + message.Message\n\t// }\n\n\tif commit {\n\t\tlog.Printf(\"[Git] StoreConvoMessage - committing convo message: %s, branch: %s\", msg, branch)\n\t\terr = repo.GitAddAndCommit(branch, msg)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error committing convo message: %v\", err)\n\t\t}\n\t}\n\n\treturn msg, nil\n}\n"
  },
  {
    "path": "app/server/db/data_models.go",
    "content": "package db\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\n// The models below should only be used server-side.\n// Many of them have corresponding models in shared/api for client-side use.\n// This adds some duplication, but helps ensure that server-only data doesn't leak to the client.\n// Models used client-side have a ToApi() method to convert it to the corresponding client-side model.\n\ntype AuthToken struct {\n\tId        string     `db:\"id\"`\n\tUserId    string     `db:\"user_id\"`\n\tTokenHash string     `db:\"token_hash\"`\n\tCreatedAt time.Time  `db:\"created_at\"`\n\tDeletedAt *time.Time `db:\"deleted_at\"`\n}\n\ntype Org struct {\n\tId                 string  `db:\"id\"`\n\tName               string  `db:\"name\"`\n\tDomain             *string `db:\"domain\"`\n\tAutoAddDomainUsers bool    `db:\"auto_add_domain_users\"`\n\tOwnerId            string  `db:\"owner_id\"`\n\tIsTrial            bool    `db:\"is_trial\"`\n\n\tCreatedAt time.Time `db:\"created_at\"`\n\tUpdatedAt time.Time `db:\"updated_at\"`\n}\n\nfunc (org *Org) ToApi() *shared.Org {\n\treturn &shared.Org{\n\t\tId:                 org.Id,\n\t\tName:               org.Name,\n\t\tAutoAddDomainUsers: org.AutoAddDomainUsers,\n\t\tIsTrial:            org.IsTrial,\n\t}\n}\n\ntype User struct {\n\tId                string             `db:\"id\"`\n\tName              string             `db:\"name\"`\n\tEmail             string             `db:\"email\"`\n\tDomain            string             `db:\"domain\"`\n\tNumNonDraftPlans  int                `db:\"num_non_draft_plans\"`\n\tDefaultPlanConfig *shared.PlanConfig `db:\"default_plan_config\"`\n\tCreatedAt         time.Time          `db:\"created_at\"`\n\tUpdatedAt         time.Time          `db:\"updated_at\"`\n}\n\nfunc (user *User) ToApi() *shared.User {\n\treturn &shared.User{\n\t\tId:                user.Id,\n\t\tName:              user.Name,\n\t\tEmail:             user.Email,\n\t\tNumNonDraftPlans:  user.NumNonDraftPlans,\n\t\tIsTrial:           false, // legacy field\n\t\tDefaultPlanConfig: user.DefaultPlanConfig,\n\t}\n}\n\ntype Invite struct {\n\tId         string     `db:\"id\"`\n\tOrgId      string     `db:\"org_id\"`\n\tEmail      string     `db:\"email\"`\n\tName       string     `db:\"name\"`\n\tInviterId  string     `db:\"inviter_id\"`\n\tInviteeId  *string    `db:\"invitee_id\"`\n\tOrgRoleId  string     `db:\"org_role_id\"`\n\tAcceptedAt *time.Time `db:\"accepted_at\"`\n\tCreatedAt  time.Time  `db:\"created_at\"`\n\tUpdatedAt  time.Time  `db:\"updated_at\"`\n}\n\nfunc (invite *Invite) ToApi() *shared.Invite {\n\treturn &shared.Invite{\n\t\tId:         invite.Id,\n\t\tOrgId:      invite.OrgId,\n\t\tEmail:      invite.Email,\n\t\tName:       invite.Name,\n\t\tInviterId:  invite.InviterId,\n\t\tInviteeId:  invite.InviteeId,\n\t\tOrgRoleId:  invite.OrgRoleId,\n\t\tAcceptedAt: invite.AcceptedAt,\n\t\tCreatedAt:  invite.CreatedAt,\n\t}\n}\n\ntype OrgUser struct {\n\tId        string                `db:\"id\"`\n\tOrgId     string                `db:\"org_id\"`\n\tOrgRoleId string                `db:\"org_role_id\"`\n\tUserId    string                `db:\"user_id\"`\n\tConfig    *shared.OrgUserConfig `db:\"config\"`\n\tCreatedAt time.Time             `db:\"created_at\"`\n\tUpdatedAt time.Time             `db:\"updated_at\"`\n}\n\nfunc (orgUser *OrgUser) ToApi() *shared.OrgUser {\n\treturn &shared.OrgUser{\n\t\tOrgId:     orgUser.OrgId,\n\t\tOrgRoleId: orgUser.OrgRoleId,\n\t\tUserId:    orgUser.UserId,\n\t\tConfig:    orgUser.Config,\n\t}\n}\n\ntype Project struct {\n\tId        string    `db:\"id\"`\n\tOrgId     string    `db:\"org_id\"`\n\tName      string    `db:\"name\"`\n\tCreatedAt time.Time `db:\"created_at\"`\n\tUpdatedAt time.Time `db:\"updated_at\"`\n}\n\nfunc (project *Project) ToApi() *shared.Project {\n\treturn &shared.Project{\n\t\tId:   project.Id,\n\t\tName: project.Name,\n\t}\n}\n\ntype Plan struct {\n\tId              string             `db:\"id\"`\n\tOrgId           string             `db:\"org_id\"`\n\tOwnerId         string             `db:\"owner_id\"`\n\tProjectId       string             `db:\"project_id\"`\n\tName            string             `db:\"name\"`\n\tSharedWithOrgAt *time.Time         `db:\"shared_with_org_at,omitempty\"`\n\tTotalReplies    int                `db:\"total_replies\"`\n\tActiveBranches  int                `db:\"active_branches\"`\n\tPlanConfig      *shared.PlanConfig `db:\"plan_config\"`\n\tArchivedAt      *time.Time         `db:\"archived_at,omitempty\"`\n\tCreatedAt       time.Time          `db:\"created_at\"`\n\tUpdatedAt       time.Time          `db:\"updated_at\"`\n}\n\nfunc (plan *Plan) ToApi() *shared.Plan {\n\treturn &shared.Plan{\n\t\tId:              plan.Id,\n\t\tOwnerId:         plan.OwnerId,\n\t\tProjectId:       plan.ProjectId,\n\t\tName:            plan.Name,\n\t\tSharedWithOrgAt: plan.SharedWithOrgAt,\n\t\tTotalReplies:    plan.TotalReplies,\n\t\tActiveBranches:  plan.ActiveBranches,\n\t\tPlanConfig:      plan.PlanConfig,\n\t\tArchivedAt:      plan.ArchivedAt,\n\t\tCreatedAt:       plan.CreatedAt,\n\t\tUpdatedAt:       plan.UpdatedAt,\n\t}\n}\n\ntype Branch struct {\n\tId              string            `db:\"id\"`\n\tOrgId           string            `db:\"org_id\"`\n\tOwnerId         string            `db:\"owner_id\"`\n\tPlanId          string            `db:\"plan_id\"`\n\tParentBranchId  *string           `db:\"parent_branch_id\"`\n\tName            string            `db:\"name\"`\n\tStatus          shared.PlanStatus `db:\"status\"`\n\tError           *string           `db:\"error\"`\n\tContextTokens   int               `db:\"context_tokens\"`\n\tConvoTokens     int               `db:\"convo_tokens\"`\n\tSharedWithOrgAt *time.Time        `db:\"shared_with_org_at,omitempty\"`\n\tArchivedAt      *time.Time        `db:\"archived_at,omitempty\"`\n\tCreatedAt       time.Time         `db:\"created_at\"`\n\tUpdatedAt       time.Time         `db:\"updated_at\"`\n\tDeletedAt       *time.Time        `db:\"deleted_at\"`\n}\n\nfunc (branch *Branch) ToApi() *shared.Branch {\n\treturn &shared.Branch{\n\t\tId:              branch.Id,\n\t\tPlanId:          branch.PlanId,\n\t\tOwnerId:         branch.OwnerId,\n\t\tParentBranchId:  branch.ParentBranchId,\n\t\tName:            branch.Name,\n\t\tStatus:          branch.Status,\n\t\tContextTokens:   branch.ContextTokens,\n\t\tConvoTokens:     branch.ConvoTokens,\n\t\tSharedWithOrgAt: branch.SharedWithOrgAt,\n\t\tArchivedAt:      branch.ArchivedAt,\n\t\tCreatedAt:       branch.CreatedAt,\n\t\tUpdatedAt:       branch.UpdatedAt,\n\t}\n}\n\ntype ConvoSummary struct {\n\tId                          string    `db:\"id\"`\n\tOrgId                       string    `db:\"org_id\"`\n\tPlanId                      string    `db:\"plan_id\"`\n\tLatestConvoMessageId        string    `db:\"latest_convo_message_id\"`\n\tLatestConvoMessageCreatedAt time.Time `db:\"latest_convo_message_created_at\"`\n\tSummary                     string    `db:\"summary\"`\n\tTokens                      int       `db:\"tokens\"`\n\tNumMessages                 int       `db:\"num_messages\"`\n\tCreatedAt                   time.Time `db:\"created_at\"`\n}\n\nfunc (summary *ConvoSummary) ToApi() *shared.ConvoSummary {\n\treturn &shared.ConvoSummary{\n\t\tId:                          summary.Id,\n\t\tLatestConvoMessageId:        summary.LatestConvoMessageId,\n\t\tLatestConvoMessageCreatedAt: summary.LatestConvoMessageCreatedAt,\n\t\tSummary:                     summary.Summary,\n\t\tTokens:                      summary.Tokens,\n\t\tNumMessages:                 summary.NumMessages,\n\t\tCreatedAt:                   summary.CreatedAt,\n\t}\n}\n\ntype PlanBuild struct {\n\tId             string    `db:\"id\"`\n\tOrgId          string    `db:\"org_id\"`\n\tPlanId         string    `db:\"plan_id\"`\n\tConvoMessageId string    `db:\"convo_message_id\"`\n\tFilePath       string    `db:\"file_path\"`\n\tError          string    `db:\"error\"`\n\tCreatedAt      time.Time `db:\"created_at\"`\n\tUpdatedAt      time.Time `db:\"updated_at\"`\n}\n\nfunc (build *PlanBuild) ToApi() *shared.PlanBuild {\n\treturn &shared.PlanBuild{\n\t\tId:             build.Id,\n\t\tConvoMessageId: build.ConvoMessageId,\n\t\tError:          build.Error,\n\t\tFilePath:       build.FilePath,\n\t\tCreatedAt:      build.CreatedAt,\n\t\tUpdatedAt:      build.UpdatedAt,\n\t}\n}\n\ntype OrgRole struct {\n\tId          string    `db:\"id\"`\n\tOrgId       *string   `db:\"org_id\"`\n\tName        string    `db:\"name\"`\n\tLabel       string    `db:\"label\"`\n\tDescription string    `db:\"description\"`\n\tCreatedAt   time.Time `db:\"created_at\"`\n\tUpdatedAt   time.Time `db:\"updated_at\"`\n}\n\nfunc (role *OrgRole) ToApi() *shared.OrgRole {\n\treturn &shared.OrgRole{\n\t\tId:          role.Id,\n\t\tIsDefault:   role.OrgId == nil,\n\t\tLabel:       role.Label,\n\t\tDescription: role.Description,\n\t}\n}\n\ntype ModelStream struct {\n\tId              string     `db:\"id\"`\n\tOrgId           string     `db:\"org_id\"`\n\tPlanId          string     `db:\"plan_id\"`\n\tInternalIp      string     `db:\"internal_ip\"`\n\tBranch          string     `db:\"branch\"`\n\tLastHeartbeatAt time.Time  `db:\"last_heartbeat_at\"`\n\tCreatedAt       time.Time  `db:\"created_at\"`\n\tFinishedAt      *time.Time `db:\"finished_at\"`\n}\n\n// type ModelStreamSubscription struct {\n// \tId            string     `db:\"id\"`\n// \tOrgId         string     `db:\"org_id\"`\n// \tPlanId        string     `db:\"plan_id\"`\n// \tUserId        string     `db:\"user_id\"`\n// \tModelStreamId string     `db:\"model_stream_id\"`\n// \tUserIp        string     `db:\"user_ip\"`\n// \tCreatedAt     time.Time  `db:\"created_at\"`\n// \tFinishedAt    *time.Time `db:\"finished_at\"`\n// }\n\ntype LockScope string\n\nconst (\n\tLockScopeRead  LockScope = \"r\"\n\tLockScopeWrite LockScope = \"w\"\n)\n\ntype repoLock struct {\n\tId              string    `db:\"id\"`\n\tOrgId           string    `db:\"org_id\"`\n\tUserId          *string   `db:\"user_id\"`\n\tPlanId          string    `db:\"plan_id\"`\n\tScope           LockScope `db:\"scope\"`\n\tBranch          *string   `db:\"branch\"`\n\tPlanBuildId     *string   `db:\"plan_build_id\"`\n\tLastHeartbeatAt time.Time `db:\"last_heartbeat_at\"`\n\tCreatedAt       time.Time `db:\"created_at\"`\n}\n\ntype ModelPack struct {\n\tId               string                   `db:\"id\"`\n\tOrgId            string                   `db:\"org_id\"`\n\tName             string                   `db:\"name\"`\n\tDescription      string                   `db:\"description\"`\n\tPlanner          shared.PlannerRoleConfig `db:\"planner\"`\n\tCoder            *shared.ModelRoleConfig  `db:\"coder\"`\n\tPlanSummary      shared.ModelRoleConfig   `db:\"plan_summary\"`\n\tBuilder          shared.ModelRoleConfig   `db:\"builder\"`\n\tWholeFileBuilder *shared.ModelRoleConfig  `db:\"whole_file_builder\"`\n\tNamer            shared.ModelRoleConfig   `db:\"namer\"`\n\tCommitMsg        shared.ModelRoleConfig   `db:\"commit_msg\"`\n\tExecStatus       shared.ModelRoleConfig   `db:\"exec_status\"`\n\tArchitect        *shared.ModelRoleConfig  `db:\"context_loader\"`\n\tCreatedAt        time.Time                `db:\"created_at\"`\n\tUpdatedAt        time.Time                `db:\"updated_at\"`\n}\n\nfunc ModelPackFromApi(apiModelPack *shared.ModelPack) *ModelPack {\n\treturn &ModelPack{\n\t\tName:             apiModelPack.Name,\n\t\tDescription:      apiModelPack.Description,\n\t\tPlanner:          apiModelPack.Planner,\n\t\tArchitect:        apiModelPack.Architect,\n\t\tCoder:            apiModelPack.Coder,\n\t\tPlanSummary:      apiModelPack.PlanSummary,\n\t\tBuilder:          apiModelPack.Builder,\n\t\tWholeFileBuilder: apiModelPack.WholeFileBuilder,\n\t\tNamer:            apiModelPack.Namer,\n\t\tCommitMsg:        apiModelPack.CommitMsg,\n\t\tExecStatus:       apiModelPack.ExecStatus,\n\t}\n}\n\nfunc (modelPack *ModelPack) ToApi() *shared.ModelPack {\n\treturn &shared.ModelPack{\n\t\tId:               modelPack.Id,\n\t\tName:             modelPack.Name,\n\t\tDescription:      modelPack.Description,\n\t\tPlanner:          modelPack.Planner,\n\t\tArchitect:        modelPack.Architect,\n\t\tCoder:            modelPack.Coder,\n\t\tPlanSummary:      modelPack.PlanSummary,\n\t\tBuilder:          modelPack.Builder,\n\t\tWholeFileBuilder: modelPack.WholeFileBuilder,\n\t\tNamer:            modelPack.Namer,\n\t\tCommitMsg:        modelPack.CommitMsg,\n\t\tExecStatus:       modelPack.ExecStatus,\n\t}\n}\n\ntype CustomModel struct {\n\tId                    string                   `db:\"id\"`\n\tOrgId                 string                   `db:\"org_id\"`\n\tModelId               shared.ModelId           `db:\"model_id\"`\n\tPublisher             shared.ModelPublisher    `db:\"publisher\"`\n\tDescription           string                   `db:\"description\"`\n\tMaxTokens             int                      `db:\"max_tokens\"`\n\tDefaultMaxConvoTokens int                      `db:\"default_max_convo_tokens\"`\n\tMaxOutputTokens       int                      `db:\"max_output_tokens\"`\n\tReservedOutputTokens  int                      `db:\"reserved_output_tokens\"`\n\tHasImageSupport       bool                     `db:\"has_image_support\"`\n\tPreferredOutputFormat shared.ModelOutputFormat `db:\"preferred_output_format\"`\n\n\tSystemPromptDisabled   bool                   `db:\"system_prompt_disabled\"`\n\tRoleParamsDisabled     bool                   `db:\"role_params_disabled\"`\n\tStopDisabled           bool                   `db:\"stop_disabled\"`\n\tPredictedOutputEnabled bool                   `db:\"predicted_output_enabled\"`\n\tReasoningEffortEnabled bool                   `db:\"reasoning_effort_enabled\"`\n\tReasoningEffort        shared.ReasoningEffort `db:\"reasoning_effort\"`\n\tIncludeReasoning       bool                   `db:\"include_reasoning\"`\n\tReasoningBudget        int                    `db:\"reasoning_budget\"`\n\tSupportsCacheControl   bool                   `db:\"supports_cache_control\"`\n\t// for anthropic, single message system prompt needs to be flipped to 'user'\n\tSingleMessageNoSystemPrompt bool `db:\"single_message_no_system_prompt\"`\n\n\t// for anthropic, token estimate padding percentage\n\tTokenEstimatePaddingPct float64 `db:\"token_estimate_padding_pct\"`\n\n\tProviders CustomModelProviders `db:\"providers\"`\n\n\tCreatedAt time.Time `db:\"created_at\"`\n\tUpdatedAt time.Time `db:\"updated_at\"`\n}\n\nfunc CustomModelFromApi(apiModel *shared.CustomModel) *CustomModel {\n\tproviders := make(CustomModelProviders, len(apiModel.Providers))\n\tfor i, provider := range apiModel.Providers {\n\t\tproviders[i] = CustomModelUsesProvider{\n\t\t\tProvider:       provider.Provider,\n\t\t\tCustomProvider: provider.CustomProvider,\n\t\t\tModelName:      provider.ModelName,\n\t\t}\n\t}\n\tdbModel := CustomModel{\n\t\tId:                          apiModel.Id,\n\t\tModelId:                     apiModel.ModelId,\n\t\tPublisher:                   apiModel.Publisher,\n\t\tDescription:                 apiModel.Description,\n\t\tMaxTokens:                   apiModel.MaxTokens,\n\t\tHasImageSupport:             apiModel.ModelCompatibility.HasImageSupport,\n\t\tDefaultMaxConvoTokens:       apiModel.DefaultMaxConvoTokens,\n\t\tMaxOutputTokens:             apiModel.MaxOutputTokens,\n\t\tReservedOutputTokens:        apiModel.ReservedOutputTokens,\n\t\tPreferredOutputFormat:       apiModel.PreferredOutputFormat,\n\t\tSystemPromptDisabled:        apiModel.SystemPromptDisabled,\n\t\tRoleParamsDisabled:          apiModel.RoleParamsDisabled,\n\t\tStopDisabled:                apiModel.StopDisabled,\n\t\tPredictedOutputEnabled:      apiModel.PredictedOutputEnabled,\n\t\tIncludeReasoning:            apiModel.IncludeReasoning,\n\t\tReasoningEffortEnabled:      apiModel.ReasoningEffortEnabled,\n\t\tReasoningEffort:             apiModel.ReasoningEffort,\n\t\tReasoningBudget:             apiModel.ReasoningBudget,\n\t\tSupportsCacheControl:        apiModel.SupportsCacheControl,\n\t\tSingleMessageNoSystemPrompt: apiModel.SingleMessageNoSystemPrompt,\n\t\tTokenEstimatePaddingPct:     apiModel.TokenEstimatePaddingPct,\n\t\tProviders:                   providers,\n\t}\n\n\treturn &dbModel\n}\n\nfunc (model *CustomModel) ToApi() *shared.CustomModel {\n\tproviders := make([]shared.BaseModelUsesProvider, len(model.Providers))\n\tfor i, provider := range model.Providers {\n\t\tproviders[i] = *provider.ToApi()\n\t}\n\treturn &shared.CustomModel{\n\t\tId:          model.Id,\n\t\tModelId:     model.ModelId,\n\t\tPublisher:   model.Publisher,\n\t\tDescription: model.Description,\n\t\tBaseModelShared: shared.BaseModelShared{\n\t\t\tDefaultMaxConvoTokens:       model.DefaultMaxConvoTokens,\n\t\t\tMaxTokens:                   model.MaxTokens,\n\t\t\tMaxOutputTokens:             model.MaxOutputTokens,\n\t\t\tReservedOutputTokens:        model.ReservedOutputTokens,\n\t\t\tPreferredOutputFormat:       model.PreferredOutputFormat,\n\t\t\tSystemPromptDisabled:        model.SystemPromptDisabled,\n\t\t\tRoleParamsDisabled:          model.RoleParamsDisabled,\n\t\t\tStopDisabled:                model.StopDisabled,\n\t\t\tPredictedOutputEnabled:      model.PredictedOutputEnabled,\n\t\t\tIncludeReasoning:            model.IncludeReasoning,\n\t\t\tReasoningEffortEnabled:      model.ReasoningEffortEnabled,\n\t\t\tReasoningEffort:             model.ReasoningEffort,\n\t\t\tReasoningBudget:             model.ReasoningBudget,\n\t\t\tSupportsCacheControl:        model.SupportsCacheControl,\n\t\t\tSingleMessageNoSystemPrompt: model.SingleMessageNoSystemPrompt,\n\t\t\tTokenEstimatePaddingPct:     model.TokenEstimatePaddingPct,\n\n\t\t\tModelCompatibility: shared.ModelCompatibility{\n\t\t\t\tHasImageSupport: model.HasImageSupport,\n\t\t\t},\n\t\t},\n\t\tProviders: providers,\n\t\tCreatedAt: &model.CreatedAt,\n\t\tUpdatedAt: &model.UpdatedAt,\n\t}\n}\n\ntype ExtraAuthVars []shared.ModelProviderExtraAuthVars\n\nfunc (e *ExtraAuthVars) Scan(src interface{}) error {\n\tif src == nil {\n\t\treturn nil\n\t}\n\n\tswitch s := src.(type) {\n\tcase []byte:\n\t\treturn json.Unmarshal(s, e)\n\tcase string:\n\t\treturn json.Unmarshal([]byte(s), e)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported data type: %T\", src)\n\t}\n}\n\nfunc (e ExtraAuthVars) Value() (driver.Value, error) {\n\treturn json.Marshal(e)\n}\n\ntype CustomProvider struct {\n\tId            string        `db:\"id\"`\n\tOrgId         string        `db:\"org_id\"`\n\tName          string        `db:\"name\"`\n\tBaseUrl       string        `db:\"base_url\"`\n\tSkipAuth      bool          `db:\"skip_auth\"`\n\tApiKeyEnvVar  string        `db:\"api_key_env_var\"`\n\tExtraAuthVars ExtraAuthVars `db:\"extra_auth_vars\"`\n\tCreatedAt     time.Time     `db:\"created_at\"`\n\tUpdatedAt     time.Time     `db:\"updated_at\"`\n}\n\nfunc CustomProviderFromApi(apiProvider *shared.CustomProvider) *CustomProvider {\n\treturn &CustomProvider{\n\t\tId:            apiProvider.Id,\n\t\tName:          apiProvider.Name,\n\t\tBaseUrl:       apiProvider.BaseUrl,\n\t\tSkipAuth:      apiProvider.SkipAuth,\n\t\tApiKeyEnvVar:  apiProvider.ApiKeyEnvVar,\n\t\tExtraAuthVars: apiProvider.ExtraAuthVars,\n\t}\n}\n\nfunc (provider *CustomProvider) ToApi() *shared.CustomProvider {\n\treturn &shared.CustomProvider{\n\t\tId:            provider.Id,\n\t\tName:          provider.Name,\n\t\tBaseUrl:       provider.BaseUrl,\n\t\tSkipAuth:      provider.SkipAuth,\n\t\tApiKeyEnvVar:  provider.ApiKeyEnvVar,\n\t\tExtraAuthVars: provider.ExtraAuthVars,\n\t}\n}\n\ntype CustomModelUsesProvider struct {\n\tProvider       shared.ModelProvider `db:\"provider\"`\n\tCustomProvider *string              `db:\"custom_provider\"`\n\tModelName      shared.ModelName     `db:\"model_name\"`\n}\n\nfunc (usesProvider *CustomModelUsesProvider) ToApi() *shared.BaseModelUsesProvider {\n\treturn &shared.BaseModelUsesProvider{\n\t\tProvider:       usesProvider.Provider,\n\t\tModelName:      usesProvider.ModelName,\n\t\tCustomProvider: usesProvider.CustomProvider,\n\t}\n}\n\ntype CustomModelProviders []CustomModelUsesProvider\n\nfunc (providers *CustomModelProviders) Scan(src interface{}) error {\n\tif src == nil {\n\t\treturn nil\n\t}\n\n\tswitch s := src.(type) {\n\tcase []byte:\n\t\treturn json.Unmarshal(s, providers)\n\tcase string:\n\t\treturn json.Unmarshal([]byte(s), providers)\n\t}\n\n\treturn fmt.Errorf(\"unsupported data type: %T\", src)\n}\n\nfunc (providers CustomModelProviders) Value() (driver.Value, error) {\n\treturn json.Marshal(providers)\n}\n\ntype DefaultPlanSettings struct {\n\tId           string              `db:\"id\"`\n\tOrgId        string              `db:\"org_id\"`\n\tPlanSettings shared.PlanSettings `db:\"plan_settings\"`\n\tCreatedAt    time.Time           `db:\"created_at\"`\n\tUpdatedAt    time.Time           `db:\"updated_at\"`\n}\n\n// Models below are stored in files, not in the database.\n// This allows us to store them in a git repo and use git to manage history.\n\ntype Context struct {\n\tId              string                `json:\"id\"`\n\tOrgId           string                `json:\"orgId\"`\n\tOwnerId         string                `json:\"ownerId\"`\n\tProjectId       string                `json:\"projectId\"`\n\tPlanId          string                `json:\"planId\"`\n\tContextType     shared.ContextType    `json:\"contextType\"`\n\tName            string                `json:\"name\"`\n\tUrl             string                `json:\"url\"`\n\tFilePath        string                `json:\"filePath\"`\n\tSha             string                `json:\"sha\"`\n\tNumTokens       int                   `json:\"numTokens\"`\n\tBody            string                `json:\"body,omitempty\"`\n\tBodySize        int64                 `json:\"bodySize,omitempty\"`\n\tForceSkipIgnore bool                  `json:\"forceSkipIgnore\"`\n\tImageDetail     openai.ImageURLDetail `json:\"imageDetail,omitempty\"`\n\tMapParts        shared.FileMapBodies  `json:\"mapParts,omitempty\"`\n\tMapShas         map[string]string     `json:\"mapShas,omitempty\"`\n\tMapTokens       map[string]int        `json:\"mapTokens,omitempty\"`\n\tMapSizes        map[string]int64      `json:\"mapSizes,omitempty\"`\n\tAutoLoaded      bool                  `json:\"autoLoaded\"`\n\tCreatedAt       time.Time             `json:\"createdAt\"`\n\tUpdatedAt       time.Time             `json:\"updatedAt\"`\n}\n\nfunc (context *Context) ToMeta() *Context {\n\t// everything except body and mapParts\n\treturn &Context{\n\t\tId:              context.Id,\n\t\tOrgId:           context.OrgId,\n\t\tOwnerId:         context.OwnerId,\n\t\tProjectId:       context.ProjectId,\n\t\tPlanId:          context.PlanId,\n\t\tContextType:     context.ContextType,\n\t\tName:            context.Name,\n\t\tUrl:             context.Url,\n\t\tFilePath:        context.FilePath,\n\t\tSha:             context.Sha,\n\t\tNumTokens:       context.NumTokens,\n\t\tBodySize:        context.BodySize,\n\t\tForceSkipIgnore: context.ForceSkipIgnore,\n\t\tAutoLoaded:      context.AutoLoaded,\n\t\tImageDetail:     context.ImageDetail,\n\t\tMapShas:         context.MapShas,\n\t\tMapTokens:       context.MapTokens,\n\t\tMapSizes:        context.MapSizes,\n\t\tCreatedAt:       context.CreatedAt,\n\t\tUpdatedAt:       context.UpdatedAt,\n\t}\n}\n\nfunc (context *Context) ToApi() *shared.Context {\n\treturn &shared.Context{\n\t\tId:              context.Id,\n\t\tOwnerId:         context.OwnerId,\n\t\tContextType:     context.ContextType,\n\t\tName:            context.Name,\n\t\tUrl:             context.Url,\n\t\tFilePath:        context.FilePath,\n\t\tSha:             context.Sha,\n\t\tNumTokens:       context.NumTokens,\n\t\tBody:            context.Body,\n\t\tBodySize:        context.BodySize,\n\t\tForceSkipIgnore: context.ForceSkipIgnore,\n\t\tAutoLoaded:      context.AutoLoaded,\n\t\tImageDetail:     context.ImageDetail,\n\t\tMapParts:        context.MapParts,\n\t\tMapShas:         context.MapShas,\n\t\tMapTokens:       context.MapTokens,\n\t\tMapSizes:        context.MapSizes,\n\t\tCreatedAt:       context.CreatedAt,\n\t\tUpdatedAt:       context.UpdatedAt,\n\t}\n}\n\ntype ConvoMessage struct {\n\tId                    string                   `json:\"id\"`\n\tOrgId                 string                   `json:\"orgId\"`\n\tPlanId                string                   `json:\"planId\"`\n\tUserId                string                   `json:\"userId\"`\n\tRole                  string                   `json:\"role\"`\n\tTokens                int                      `json:\"tokens\"`\n\tNum                   int                      `json:\"num\"`\n\tMessage               string                   `json:\"message\"`\n\tStopped               bool                     `json:\"stopped\"`\n\tSubtask               *Subtask                 `json:\"subtask,omitempty\"`\n\tAddedSubtasks         []*Subtask               `json:\"addedSubtasks,omitempty\"`\n\tRemovedSubtasks       []string                 `json:\"removedSubtasks,omitempty\"`\n\tFlags                 shared.ConvoMessageFlags `json:\"flags\"`\n\tActivatedPaths        map[string]bool          `json:\"activatePaths,omitempty\"`\n\tActivatedPathsOrdered []string                 `json:\"activatePathsOrdered,omitempty\"`\n\tCreatedAt             time.Time                `json:\"createdAt\"`\n}\n\nfunc (msg *ConvoMessage) ToApi() *shared.ConvoMessage {\n\taddedSubtasks := make([]*shared.Subtask, len(msg.AddedSubtasks))\n\tfor i, subtask := range msg.AddedSubtasks {\n\t\taddedSubtasks[i] = subtask.ToApi()\n\t}\n\treturn &shared.ConvoMessage{\n\t\tId:              msg.Id,\n\t\tUserId:          msg.UserId,\n\t\tRole:            msg.Role,\n\t\tTokens:          msg.Tokens,\n\t\tNum:             msg.Num,\n\t\tMessage:         msg.Message,\n\t\tStopped:         msg.Stopped,\n\t\tFlags:           msg.Flags,\n\t\tSubtask:         msg.Subtask.ToApi(),\n\t\tAddedSubtasks:   addedSubtasks,\n\t\tRemovedSubtasks: msg.RemovedSubtasks,\n\t\tCreatedAt:       msg.CreatedAt,\n\t}\n}\n\ntype ConvoMessageDescription struct {\n\tId                    string `json:\"id\"`\n\tOrgId                 string `json:\"orgId\"`\n\tPlanId                string `json:\"planId\"`\n\tConvoMessageId        string `json:\"convoMessageId\"`\n\tSummarizedToMessageId string `json:\"summarizedToMessageId\"`\n\tWroteFiles            bool   `json:\"wroteFiles\"`\n\tCommitMsg             string `json:\"commitMsg\"`\n\t// Files                 []string        `json:\"files\"`\n\tOperations            []*shared.Operation `json:\"operations\"`\n\tError                 string              `json:\"error\"`\n\tDidBuild              bool                `json:\"didBuild\"`\n\tBuildPathsInvalidated map[string]bool     `json:\"buildPathsInvalidated\"`\n\tAppliedAt             *time.Time          `json:\"appliedAt,omitempty\"`\n\tCreatedAt             time.Time           `json:\"createdAt\"`\n\tUpdatedAt             time.Time           `json:\"updatedAt\"`\n}\n\nfunc (desc *ConvoMessageDescription) ToApi() *shared.ConvoMessageDescription {\n\treturn &shared.ConvoMessageDescription{\n\t\tId:                    desc.Id,\n\t\tConvoMessageId:        desc.ConvoMessageId,\n\t\tSummarizedToMessageId: desc.SummarizedToMessageId,\n\t\tWroteFiles:            desc.WroteFiles,\n\t\tCommitMsg:             desc.CommitMsg,\n\t\t// Files:                 desc.Files,\n\t\tOperations:            desc.Operations,\n\t\tDidBuild:              desc.DidBuild,\n\t\tBuildPathsInvalidated: desc.BuildPathsInvalidated,\n\t\tAppliedAt:             desc.AppliedAt,\n\t\tError:                 desc.Error,\n\t\tCreatedAt:             desc.CreatedAt,\n\t\tUpdatedAt:             desc.UpdatedAt,\n\t}\n}\n\ntype PlanFileResult struct {\n\tId                  string `json:\"id\"`\n\tTypeVersion         int    `json:\"typeVersion\"`\n\tReplaceWithLineNums bool   `json:\"replaceWithLineNums\"`\n\tOrgId               string `json:\"orgId\"`\n\tPlanId              string `json:\"planId\"`\n\tConvoMessageId      string `json:\"convoMessageId\"`\n\tPlanBuildId         string `json:\"planBuildId\"`\n\tPath                string `json:\"path\"`\n\tContent             string `json:\"content,omitempty\"`\n\n\tReplacements []*shared.Replacement `json:\"replacements\"`\n\n\tRemovedFile bool `json:\"removedFile\"`\n\n\tAnyFailed bool   `json:\"anyFailed\"`\n\tError     string `json:\"error\"`\n\n\tSyntaxErrors []string `json:\"syntaxErrors\"`\n\n\tAppliedAt  *time.Time `json:\"appliedAt,omitempty\"`\n\tRejectedAt *time.Time `json:\"rejectedAt,omitempty\"`\n\tCreatedAt  time.Time  `json:\"createdAt\"`\n\tUpdatedAt  time.Time  `json:\"updatedAt\"`\n}\n\nfunc (res *PlanFileResult) ToApi() *shared.PlanFileResult {\n\treturn &shared.PlanFileResult{\n\t\tId:                  res.Id,\n\t\tTypeVersion:         res.TypeVersion,\n\t\tReplaceWithLineNums: res.ReplaceWithLineNums,\n\t\tPlanBuildId:         res.PlanBuildId,\n\t\tConvoMessageId:      res.ConvoMessageId,\n\t\tPath:                res.Path,\n\t\tContent:             res.Content,\n\t\tAnyFailed:           res.AnyFailed,\n\t\tAppliedAt:           res.AppliedAt,\n\t\tRejectedAt:          res.RejectedAt,\n\t\tReplacements:        res.Replacements,\n\t\tRemovedFile:         res.RemovedFile,\n\t\tCreatedAt:           res.CreatedAt,\n\t\tUpdatedAt:           res.UpdatedAt,\n\t}\n}\n\ntype PlanApply struct {\n\tId                         string    `json:\"id\"`\n\tOrgId                      string    `json:\"orgId\"`\n\tPlanId                     string    `json:\"planId\"`\n\tUserId                     string    `json:\"userId\"`\n\tConvoMessageIds            []string  `json:\"convoMessageIds\"`\n\tConvoMessageDescriptionIds []string  `json:\"convoMessageDescriptionIds\"`\n\tPlanFileResultIds          []string  `json:\"planFileResultIds\"`\n\tCommitMsg                  string    `json:\"commitMsg\"`\n\tCreatedAt                  time.Time `json:\"createdAt\"`\n}\n\nfunc (apply *PlanApply) ToApi() *shared.PlanApply {\n\treturn &shared.PlanApply{\n\t\tId:                         apply.Id,\n\t\tUserId:                     apply.UserId,\n\t\tConvoMessageIds:            apply.ConvoMessageIds,\n\t\tConvoMessageDescriptionIds: apply.ConvoMessageDescriptionIds,\n\t\tPlanFileResultIds:          apply.PlanFileResultIds,\n\t\tCommitMsg:                  apply.CommitMsg,\n\t\tCreatedAt:                  apply.CreatedAt,\n\t}\n}\n\ntype Subtask struct {\n\tTitle       string   `json:\"title\"`\n\tDescription string   `json:\"description\"`\n\tUsesFiles   []string `json:\"usesFiles\"`\n\tIsFinished  bool     `json:\"isFinished\"`\n\tNumTries    int      `json:\"numTries\"`\n}\n\nfunc (subtask *Subtask) ToApi() *shared.Subtask {\n\tif subtask == nil {\n\t\treturn nil\n\t}\n\treturn &shared.Subtask{\n\t\tTitle:       subtask.Title,\n\t\tDescription: subtask.Description,\n\t\tUsesFiles:   subtask.UsesFiles,\n\t\tIsFinished:  subtask.IsFinished,\n\t}\n}\n"
  },
  {
    "path": "app/server/db/db.go",
    "content": "package db\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/golang-migrate/migrate/v4\"\n\t\"github.com/golang-migrate/migrate/v4/database/postgres\"\n\t_ \"github.com/golang-migrate/migrate/v4/source/file\"\n\t\"github.com/jmoiron/sqlx\"\n\t_ \"github.com/lib/pq\"\n)\n\nvar Conn *sqlx.DB\n\nconst LockTimeout = 4000\nconst IdleInTransactionSessionTimeout = 90000\nconst StatementTimeout = 30000\n\nfunc Connect() error {\n\tvar err error\n\n\tdbUrl := os.Getenv(\"DATABASE_URL\")\n\tif dbUrl == \"\" {\n\t\tif os.Getenv(\"DB_HOST\") != \"\" &&\n\t\t\tos.Getenv(\"DB_PORT\") != \"\" &&\n\t\t\tos.Getenv(\"DB_USER\") != \"\" &&\n\t\t\tos.Getenv(\"DB_PASSWORD\") != \"\" &&\n\t\t\tos.Getenv(\"DB_NAME\") != \"\" {\n\t\t\tencodedPassword := url.QueryEscape(os.Getenv(\"DB_PASSWORD\"))\n\n\t\t\tdbUrl = \"postgres://\" + os.Getenv(\"DB_USER\") + \":\" + encodedPassword + \"@\" + os.Getenv(\"DB_HOST\") + \":\" + os.Getenv(\"DB_PORT\") + \"/\" + os.Getenv(\"DB_NAME\")\n\t\t}\n\n\t\tif dbUrl == \"\" {\n\t\t\treturn errors.New(\"DATABASE_URL or DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, and DB_NAME environment variables must be set\")\n\t\t}\n\t}\n\n\tif strings.Contains(dbUrl, \"?\") {\n\t\tdbUrl += fmt.Sprintf(\"&statement_timeout=%d&lock_timeout=%d&timezone=UTC&idle_in_transaction_session_timeout=%d\", StatementTimeout, LockTimeout, IdleInTransactionSessionTimeout)\n\t} else {\n\t\tdbUrl += fmt.Sprintf(\"?statement_timeout=%d&lock_timeout=%d&timezone=UTC&idle_in_transaction_session_timeout=%d\", StatementTimeout, LockTimeout, IdleInTransactionSessionTimeout)\n\t}\n\n\tConn, err = sqlx.Connect(\"postgres\", dbUrl)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Println(\"connected to database\")\n\n\tif os.Getenv(\"GOENV\") == \"production\" {\n\t\tConn.SetMaxOpenConns(50)\n\t\tConn.SetMaxIdleConns(20)\n\t} else {\n\t\tConn.SetMaxOpenConns(10)\n\t\tConn.SetMaxIdleConns(5)\n\t}\n\n\t// Verify settings\n\ttype setting struct {\n\t\tName    string  `db:\"name\"`\n\t\tSetting string  `db:\"setting\"`\n\t\tUnit    *string `db:\"unit\"`\n\t\tContext string  `db:\"context\"`\n\t}\n\n\tvar settings []setting\n\terr = Conn.Select(&settings, `\n\t\tSELECT name, setting, unit, context \n\t\tFROM pg_settings \n\t\tWHERE name IN ('statement_timeout', 'lock_timeout', 'TimeZone', 'idle_in_transaction_session_timeout')\n`)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking settings: %v\", err)\n\t}\n\n\ts := \"\"\n\tfor _, setting := range settings {\n\t\tunitStr := \"\"\n\t\tif setting.Unit != nil {\n\t\t\tunitStr = \" \" + *setting.Unit // Add a leading space only if there's a unit\n\t\t}\n\t\ts += fmt.Sprintf(\"- %s = %s%s (context: %s)\\n\", setting.Name, setting.Setting, unitStr, setting.Context)\n\t}\n\tlog.Printf(\"\\n\\nDatabase settings:\\n%s\\n\", s)\n\n\treturn nil\n}\n\nfunc MigrationsUp() error {\n\tmigrationsDir := \"migrations\"\n\tif os.Getenv(\"MIGRATIONS_DIR\") != \"\" {\n\t\tmigrationsDir = os.Getenv(\"MIGRATIONS_DIR\")\n\t}\n\n\treturn migrationsUp(migrationsDir)\n}\n\nfunc MigrationsUpWithDir(dir string) error {\n\treturn migrationsUp(dir)\n}\n\nfunc migrationsUp(dir string) error {\n\tif Conn == nil {\n\t\treturn errors.New(\"db not initialized\")\n\t}\n\n\tdriver, err := postgres.WithInstance(Conn.DB, &postgres.Config{})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating postgres driver: %v\", err)\n\t}\n\n\tm, err := migrate.NewWithDatabaseInstance(\n\t\t\"file://\"+dir,\n\t\t\"postgres\", driver)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating migration instance: %v\", err)\n\t}\n\n\t// Uncomment below (and update migration version) to reset migration state to a specific version after a failure\n\t// if os.Getenv(\"GOENV\") == \"development\" {\n\t// \tmigrateVersion := 2025052900\n\t// \tif err := m.Force(migrateVersion); err != nil {\n\t// \t\treturn fmt.Errorf(\"error forcing migration version: %v\", err)\n\t// \t}\n\t// }\n\n\t// Uncomment below to run down migrations (RESETS DATABASE!!)\n\t// if os.Getenv(\"GOENV\") == \"development\" {\n\t// \terr = m.Down()\n\t// \tif err != nil {\n\t// \t\tif err == migrate.ErrNoChange {\n\t// \t\t\tlog.Println(\"no migrations to run down\")\n\t// \t\t} else {\n\t// \t\t\treturn fmt.Errorf(\"error running down migrations: %v\", err)\n\t// \t\t}\n\t// \t}\n\t// \tlog.Println(\"ran down migrations - database was reset\")\n\t// }\n\n\t// Uncomment below and edit 'stepsBack' to go back a specific number of migrations\n\t// if os.Getenv(\"GOENV\") == \"development\" {\n\t// \tstepsBack := 1\n\t// \terr = m.Steps(-stepsBack)\n\t// \tif err != nil {\n\t// \t\treturn fmt.Errorf(\"error running down migrations: %v\", err)\n\t// \t}\n\t// \tlog.Printf(\"went down %d migration\\n\", stepsBack)\n\t// }\n\n\terr = m.Up()\n\n\tif err != nil {\n\t\tif err == migrate.ErrNoChange {\n\t\t\tlog.Println(\"migration state is up to date\")\n\t\t} else {\n\n\t\t\treturn fmt.Errorf(\"error running migrations: %v\", err)\n\t\t}\n\t}\n\n\tif err == nil {\n\t\tlog.Println(\"ran migrations successfully\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/diff_helpers.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc GetPlanDiffs(orgId, planId string, plain bool) (string, error) {\n\tplanState, err := GetCurrentPlanState(CurrentPlanStateParams{\n\t\tOrgId:  orgId,\n\t\tPlanId: planId,\n\t})\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting current plan state: %v\", err)\n\t}\n\n\t// create temp directory\n\ttempDirPath, err := os.MkdirTemp(getOrgDir(orgId), \"tmp-diffs-*\")\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error creating temp dir: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tgo os.RemoveAll(tempDirPath)\n\t}()\n\n\t// init a git repo in the temp dir\n\terr = initGitRepo(tempDirPath)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error initializing git repo: %v\", err)\n\t}\n\n\tfiles := planState.CurrentPlanFiles.Files\n\tremoved := planState.CurrentPlanFiles.Removed\n\n\t// write the original files to the temp dir\n\terrCh := make(chan error, len(planState.ContextsByPath))\n\thasAnyOriginal := false\n\n\tfor path, context := range planState.ContextsByPath {\n\t\tgo func(path string, context *shared.Context) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanDiffs: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanDiffs: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\t_, hasPath := files[path]\n\t\t\t_, hasRemoved := removed[path]\n\t\t\tif hasPath || hasRemoved {\n\t\t\t\thasAnyOriginal = true\n\t\t\t\t// ensure file directory exists\n\t\t\t\terr = os.MkdirAll(filepath.Dir(filepath.Join(tempDirPath, path)), 0755)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error creating directory: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\terr = os.WriteFile(filepath.Join(tempDirPath, path), []byte(context.Body), 0644)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error writing file: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path, context)\n\t}\n\n\tfor range planState.ContextsByPath {\n\t\terr = <-errCh\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error writing original files to temp dir: %v\", err)\n\t\t}\n\t}\n\n\tif hasAnyOriginal {\n\t\t// add and commit the files in the temp dir\n\t\terr := gitAdd(tempDirPath, \".\")\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v\", tempDirPath, err)\n\t\t}\n\n\t\terr = gitCommit(tempDirPath, \"original files\")\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v\", tempDirPath, err)\n\t\t}\n\t}\n\n\t// write the current files to the temp dir\n\terrCh = make(chan error, len(files))\n\n\tfor path, file := range files {\n\t\tgo func(path, file string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanDiffs: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanDiffs: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\t// ensure file directory exists\n\t\t\terr = os.MkdirAll(filepath.Dir(filepath.Join(tempDirPath, path)), 0755)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error creating directory: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terr = os.WriteFile(filepath.Join(tempDirPath, path), []byte(file), 0644)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error writing file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path, file)\n\t}\n\n\tfor path, shouldRemove := range removed {\n\t\tgo func(path string, shouldRemove bool) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanDiffs: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanDiffs: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tif shouldRemove {\n\t\t\t\terr = os.RemoveAll(filepath.Join(tempDirPath, path))\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error removing file: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path, shouldRemove)\n\t}\n\n\tfor i := 0; i < len(files)+len(removed); i++ {\n\t\terr = <-errCh\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error applying changes to temp dir: %v\", err)\n\t\t}\n\t}\n\n\terr = gitAdd(tempDirPath, \".\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v\", tempDirPath, err)\n\t}\n\n\tcolorArg := \"--color=always\"\n\tif plain {\n\t\tcolorArg = \"--no-color\"\n\t}\n\tres, err := exec.Command(\"git\", \"-C\", tempDirPath, \"diff\", \"--cached\", colorArg).CombinedOutput()\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting diffs: %v\", err)\n\t}\n\n\treturn string(res), nil\n}\n"
  },
  {
    "path": "app/server/db/fs.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nvar BaseDir string\n\nfunc init() {\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"error getting user home dir: %v\", err))\n\t}\n\n\tlog.Println(\"Plandex server home dir:\", home)\n\tlog.Println(\"os.Getenv(PLANDEX_BASE_DIR):\", os.Getenv(\"PLANDEX_BASE_DIR\"))\n\tlog.Println(\"GOENV:\", os.Getenv(\"GOENV\"))\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\tlog.Println(\"Local mode enabled\")\n\t}\n\n\tBaseDir = os.Getenv(\"PLANDEX_BASE_DIR\")\n\n\tif BaseDir == \"\" {\n\t\tif os.Getenv(\"GOENV\") == \"development\" {\n\t\t\tBaseDir = filepath.Join(home, \"plandex-server\")\n\t\t} else {\n\t\t\tBaseDir = \"/plandex-server\"\n\t\t}\n\t}\n\n\tlog.Printf(\"File system dir: %v\\n\", BaseDir)\n}\n\nfunc InitPlan(orgId, planId string) error {\n\tdir := getPlanDir(orgId, planId)\n\terr := os.MkdirAll(dir, os.ModePerm)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating plan dir: %v\", err)\n\t}\n\n\tfor _, subdirFn := range [](func(orgId, planId string) string){\n\t\tgetPlanContextDir,\n\t\tgetPlanConversationDir,\n\t\tgetPlanResultsDir,\n\t\tgetPlanDescriptionsDir} {\n\t\terr = os.MkdirAll(subdirFn(orgId, planId), os.ModePerm)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating plan subdir: %v\", err)\n\t\t}\n\t}\n\n\terr = InitGitRepo(orgId, planId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error initializing git repo: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc DeletePlanDir(orgId, planId string) error {\n\tdir := getPlanDir(orgId, planId)\n\terr := os.RemoveAll(dir)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting plan dir: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc getOrgDir(orgId string) string {\n\treturn filepath.Join(BaseDir, \"orgs\", orgId)\n}\n\nfunc getProjectDir(orgId, projectId string) string {\n\treturn filepath.Join(getOrgDir(orgId), \"projects\", projectId)\n}\n\nfunc getProjectMapCacheDir(orgId, projectId string) string {\n\treturn filepath.Join(getProjectDir(orgId, projectId), \"map_cache\")\n}\n\nfunc getPlanDir(orgId, planId string) string {\n\treturn filepath.Join(getOrgDir(orgId), \"plans\", planId)\n}\n\nfunc getPlanContextDir(orgId, planId string) string {\n\treturn filepath.Join(getPlanDir(orgId, planId), \"context\")\n}\n\nfunc getPlanConversationDir(orgId, planId string) string {\n\treturn filepath.Join(getPlanDir(orgId, planId), \"conversation\")\n}\n\nfunc getPlanResultsDir(orgId, planId string) string {\n\treturn filepath.Join(getPlanDir(orgId, planId), \"results\")\n}\n\nfunc getPlanAppliesDir(orgId, planId string) string {\n\treturn filepath.Join(getPlanDir(orgId, planId), \"applies\")\n}\n\nfunc getPlanDescriptionsDir(orgId, planId string) string {\n\treturn filepath.Join(getPlanDir(orgId, planId), \"descriptions\")\n}\n"
  },
  {
    "path": "app/server/db/git.go",
    "content": "package db\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n)\n\nconst (\n\tmaxGitRetries     = 5\n\tbaseGitRetryDelay = 100 * time.Millisecond\n)\n\nfunc init() {\n\t// ensure git is available\n\tcmd := exec.Command(\"git\", \"--version\")\n\tif err := cmd.Run(); err != nil {\n\t\tpanic(fmt.Errorf(\"error running git --version: %v\", err))\n\t}\n}\n\ntype GitRepo struct {\n\torgId  string\n\tplanId string\n}\n\nfunc InitGitRepo(orgId, planId string) error {\n\tdir := getPlanDir(orgId, planId)\n\treturn initGitRepo(dir)\n}\n\nfunc initGitRepo(dir string) error {\n\t// Set the default branch name to 'main' for the new repository\n\tres, err := exec.Command(\"git\", \"-C\", dir, \"init\", \"-b\", \"main\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error initializing git repository with 'main' as default branch for dir: %s, err: %v, output: %s\", dir, err, string(res))\n\t}\n\n\t// Configure user name and email for the repository\n\tif err := setGitConfig(dir, \"user.email\", \"server@plandex.ai\"); err != nil {\n\t\treturn err\n\t}\n\tif err := setGitConfig(dir, \"user.name\", \"Plandex\"); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc getGitRepo(orgId, planId string) *GitRepo {\n\treturn &GitRepo{\n\t\torgId:  orgId,\n\t\tplanId: planId,\n\t}\n}\n\nfunc (repo *GitRepo) GitAddAndCommit(branch, message string) error {\n\tlog.Printf(\"[Git] GitAddAndCommit - orgId: %s, planId: %s, branch: %s, message: %s\", repo.orgId, repo.planId, branch, message)\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitWriteOperation(func() error {\n\t\treturn gitAdd(dir, \".\")\n\t}, dir, fmt.Sprintf(\"GitAddAndCommit > gitAdd: plan=%s branch=%s\", planId, branch))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\terr = gitWriteOperation(func() error {\n\t\treturn gitCommit(dir, message)\n\t}, dir, fmt.Sprintf(\"GitAddAndCommit > gitCommit: plan=%s branch=%s\", planId, branch))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\t// log.Println(\"[Git] GitAddAndCommit - finished, logging repo state\")\n\n\t// repo.LogGitRepoState()\n\n\treturn nil\n}\n\nfunc (repo *GitRepo) GitRewindToSha(branch, sha string) error {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitWriteOperation(func() error {\n\t\treturn gitRewindToSha(dir, sha)\n\t}, dir, fmt.Sprintf(\"GitRewindToSha > gitRewindToSha: plan=%s branch=%s\", planId, branch))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error rewinding git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn nil\n}\n\nfunc (repo *GitRepo) GetCurrentCommitSha() (sha string, err error) {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\tcmd := exec.Command(\"git\", \"-C\", dir, \"rev-parse\", \"HEAD\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting current commit SHA for dir: %s, err: %v\", dir, err)\n\t}\n\n\tsha = strings.TrimSpace(string(output))\n\treturn sha, nil\n}\n\nfunc (repo *GitRepo) GetCommitTime(branch, ref string) (time.Time, error) {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\t// Use git show to get the commit timestamp\n\tcmd := exec.Command(\"git\", \"-C\", dir, \"show\", \"-s\", \"--format=%ct\", ref)\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"error getting commit time for ref %s: %v\", ref, err)\n\t}\n\n\t// Parse the Unix timestamp\n\ttimestamp, err := strconv.ParseInt(strings.TrimSpace(string(output)), 10, 64)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"error parsing commit timestamp for ref %s: %v\", ref, err)\n\t}\n\n\t// Convert Unix timestamp to time.Time\n\tcommitTime := time.Unix(timestamp, 0)\n\treturn commitTime, nil\n}\n\nfunc (repo *GitRepo) GitResetToSha(sha string) error {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitWriteOperation(func() error {\n\t\tcmd := exec.Command(\"git\", \"-C\", dir, \"reset\", \"--hard\", sha)\n\t\t_, err := cmd.Output()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error resetting git repository to SHA for dir: %s, sha: %s, err: %v\", dir, sha, err)\n\t\t}\n\n\t\treturn nil\n\t}, dir, fmt.Sprintf(\"GitResetToSha > gitReset: plan=%s sha=%s\", planId, sha))\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error resetting git repository to SHA for dir: %s, sha: %s, err: %v\", dir, sha, err)\n\t}\n\n\treturn nil\n}\n\nfunc (repo *GitRepo) GitCheckoutSha(sha string) error {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitWriteOperation(func() error {\n\t\tcmd := exec.Command(\"git\", \"-C\", dir, \"checkout\", sha)\n\t\t_, err := cmd.Output()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error checking out git repository at SHA for dir: %s, sha: %s, err: %v\", dir, sha, err)\n\t\t}\n\n\t\treturn nil\n\t}, dir, fmt.Sprintf(\"GitCheckoutSha > gitCheckout: plan=%s sha=%s\", planId, sha))\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking out git repository at SHA for dir: %s, sha: %s, err: %v\", dir, sha, err)\n\t}\n\n\treturn nil\n}\n\nfunc (repo *GitRepo) GetGitCommitHistory(branch string) (body string, shas []string, err error) {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\tbody, shas, err = getGitCommitHistory(dir)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"error getting git history for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn body, shas, nil\n}\n\nfunc (repo *GitRepo) GetLatestCommit(branch string) (sha, body string, err error) {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\tsha, body, err = getLatestCommit(dir)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"error getting latest commit for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn sha, body, nil\n}\n\nfunc (repo *GitRepo) GetLatestCommitShaBeforeTime(branch string, before time.Time) (sha string, err error) {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\tlog.Printf(\"ADMIN - GetLatestCommitShaBeforeTime - dir: %s, before: %s\", dir, before.Format(\"2006-01-02T15:04:05Z\"))\n\n\t// Round up to the next second\n\t// roundedTime := before.Add(time.Second).Truncate(time.Second)\n\n\tgitFormattedTime := before.Format(\"2006-01-02 15:04:05+0000\")\n\n\t// log.Printf(\"ADMIN - Git formatted time: %s\", gitFormattedTime)\n\n\tcmd := exec.Command(\"git\", \"-C\", dir, \"log\", \"-n\", \"1\",\n\t\t\"--before=\"+gitFormattedTime,\n\t\t\"--pretty=%h@@|@@%B@>>>@\")\n\tlog.Printf(\"ADMIN - Executing command: %s\", cmd.String())\n\tres, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting latest commit before time for dir: %s, err: %v, output: %s\", dir, err, string(res))\n\t}\n\n\t// log.Printf(\"ADMIN - git log res: %s\", string(res))\n\n\toutput := strings.TrimSpace(string(res))\n\n\t// history := processGitHistoryOutput(strings.TrimSpace(string(res)))\n\n\t// log.Printf(\"ADMIN - History: %v\", history)\n\n\tif output == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no commits found before time: %s\", before.Format(\"2006-01-02T15:04:05Z\"))\n\t}\n\n\tsha = strings.Split(output, \"@@|@@\")[0]\n\treturn sha, nil\n}\n\nfunc (repo *GitRepo) GitListBranches() ([]string, error) {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"branch\", \"--format=%(refname:short)\")\n\tcmd.Dir = dir\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting git branches for dir: %s, err: %v\", dir, err)\n\t}\n\n\tbranches := strings.Split(strings.TrimSpace(out.String()), \"\\n\")\n\n\tif len(branches) == 0 || (len(branches) == 1 && branches[0] == \"\") {\n\t\treturn []string{\"main\"}, nil\n\t}\n\n\treturn branches, nil\n}\n\nfunc (repo *GitRepo) GitCreateBranch(newBranch string) error {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitWriteOperation(func() error {\n\t\tres, err := exec.Command(\"git\", \"-C\", dir, \"checkout\", \"-b\", newBranch).CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating git branch for dir: %s, err: %v, output: %s\", dir, err, string(res))\n\t\t}\n\n\t\treturn nil\n\t}, dir, fmt.Sprintf(\"GitCreateBranch > gitCheckout: plan=%s branch=%s\", planId, newBranch))\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (repo *GitRepo) GitDeleteBranch(branchName string) error {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitWriteOperation(func() error {\n\t\tres, err := exec.Command(\"git\", \"-C\", dir, \"branch\", \"-D\", branchName).CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting git branch for dir: %s, err: %v, output: %s\", dir, err, string(res))\n\t\t}\n\n\t\treturn nil\n\t}, dir, fmt.Sprintf(\"GitDeleteBranch > gitBranch: plan=%s branch=%s\", planId, branchName))\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (repo *GitRepo) GitClearUncommittedChanges(branch string) error {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tlog.Printf(\"[Git] GitClearUncommittedChanges - orgId: %s, planId: %s, branch: %s\", orgId, planId, branch)\n\n\tdir := getPlanDir(orgId, planId)\n\n\t// first do a lightweight git status to check if there are any uncommitted changes\n\t// prevents heavier operations below if there are no changes (the usual case)\n\tres, err := exec.Command(\"git\", \"status\", \"--porcelain\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking for uncommitted changes: %v, output: %s\", err, string(res))\n\t}\n\n\t// If there's output, there are uncommitted changes\n\thasChanges := strings.TrimSpace(string(res)) != \"\"\n\n\tif !hasChanges {\n\t\tlog.Printf(\"[Git] GitClearUncommittedChanges - no changes to clear for plan %s\", planId)\n\t\treturn nil\n\t}\n\n\terr = gitWriteOperation(func() error {\n\t\t// Reset staged changes\n\t\tlog.Printf(\"[Git] GitClearUncommittedChanges - resetting staged changes for plan %s\", planId)\n\t\tres, err := exec.Command(\"git\", \"-C\", dir, \"reset\", \"--hard\").CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error resetting staged changes | err: %v, output: %s\", err, string(res))\n\t\t}\n\t\tlog.Printf(\"[Git] GitClearUncommittedChanges - reset staged changes finished for plan %s\", planId)\n\t\treturn nil\n\t}, dir, fmt.Sprintf(\"GitClearUncommittedChanges > gitReset: plan=%s\", planId))\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = gitWriteOperation(func() error {\n\t\t// Clean untracked files\n\t\tlog.Printf(\"[Git] GitClearUncommittedChanges - cleaning untracked files for plan %s\", planId)\n\t\tres, err := exec.Command(\"git\", \"-C\", dir, \"clean\", \"-d\", \"-f\").CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error cleaning untracked files | err: %v, output: %s\", err, string(res))\n\t\t}\n\t\tlog.Printf(\"[Git] GitClearUncommittedChanges - clean untracked files finished for plan %s\", planId)\n\t\treturn nil\n\t}, dir, fmt.Sprintf(\"GitClearUncommittedChanges > gitClean: plan=%s\", planId))\n\n\treturn err\n}\n\nfunc (repo *GitRepo) GitCheckoutBranch(branch string) error {\n\torgId := repo.orgId\n\tplanId := repo.planId\n\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitWriteOperation(func() error {\n\t\treturn gitCheckoutBranch(dir, branch)\n\t}, dir, fmt.Sprintf(\"GitCheckoutBranch > gitCheckout: plan=%s branch=%s\", planId, branch))\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc gitAdd(repoDir, path string) error {\n\n\tif err := gitRemoveIndexLockFileIfExists(repoDir); err != nil {\n\t\treturn fmt.Errorf(\"error removing lock file before add: %v\", err)\n\t}\n\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"add\", path).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\treturn nil\n}\n\nfunc gitCommit(repoDir, commitMsg string) error {\n\tif err := gitRemoveIndexLockFileIfExists(repoDir); err != nil {\n\t\treturn fmt.Errorf(\"error removing lock file before commit: %v\", err)\n\t}\n\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"commit\", \"-m\", commitMsg).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\treturn nil\n\n}\n\nfunc gitCheckoutBranch(repoDir, branch string) error {\n\tlog.Printf(\"[Git] gitCheckoutBranch - repoDir: %s, branch: %s\", repoDir, branch)\n\tif err := gitRemoveIndexLockFileIfExists(repoDir); err != nil {\n\t\treturn fmt.Errorf(\"error removing lock file before checkout: %v\", err)\n\t}\n\n\t// get current branch and only checkout if it's not the same\n\t// trying to check out the same branch will result in an error\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"-C\", repoDir, \"branch\", \"--show-current\")\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting current git branch for dir: %s, err: %v\", repoDir, err)\n\t}\n\n\tcurrentBranch := strings.TrimSpace(out.String())\n\tlog.Printf(\"[Git] gitCheckoutBranch - currentBranch: %s\", currentBranch)\n\n\tif currentBranch == branch {\n\t\tlog.Printf(\"[Git] gitCheckoutBranch - already on branch %s, skipping\", branch)\n\t\treturn nil\n\t}\n\n\tlog.Println(\"[Git] gitCheckoutBranch - checking out branch:\", branch)\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"checkout\", branch).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking out git branch for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\treturn nil\n}\n\nfunc gitRewindToSha(repoDir, sha string) error {\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"reset\", \"--hard\", sha).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error executing git reset for dir: %s, sha: %s, err: %v, output: %s\", repoDir, sha, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc getLatestCommit(dir string) (sha, body string, err error) {\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"log\", \"--pretty=%h@@|@@%at@@|@@%B@>>>@\")\n\tcmd.Dir = dir\n\tcmd.Stdout = &out\n\terr = cmd.Run()\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"error getting git history for dir: %s, err: %v\", dir, err)\n\t}\n\n\t// Process the log output to get it in the desired format.\n\thistory := processGitHistoryOutput(strings.TrimSpace(out.String()))\n\n\tfirst := history[0]\n\n\tsha = first[0]\n\tbody = first[1]\n\n\treturn sha, body, nil\n}\n\nfunc getGitCommitHistory(dir string) (body string, shas []string, err error) {\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"log\", \"--pretty=%h@@|@@%at@@|@@%B@>>>@\")\n\tcmd.Dir = dir\n\tcmd.Stdout = &out\n\terr = cmd.Run()\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"error getting git history for dir: %s, err: %v\", dir, err)\n\t}\n\n\t// Process the log output to get it in the desired format.\n\thistory := processGitHistoryOutput(strings.TrimSpace(out.String()))\n\n\tvar output []string\n\tfor _, el := range history {\n\t\tshas = append(shas, el[0])\n\t\toutput = append(output, el[1])\n\t}\n\n\treturn strings.Join(output, \"\\n\\n\"), shas, nil\n}\n\n// processGitHistoryOutput processes the raw output from the git log command and returns a formatted string.\nfunc processGitHistoryOutput(raw string) [][2]string {\n\tvar history [][2]string\n\tentries := strings.Split(raw, \"@>>>@\") // Split entries using the custom separator.\n\n\tfor _, entry := range entries {\n\t\t// First clean up any leading/trailing whitespace or newlines from each entry.\n\t\tentry = strings.TrimSpace(entry)\n\n\t\t// Now split the cleaned entry into its parts.\n\t\tparts := strings.Split(entry, \"@@|@@\")\n\t\tif len(parts) == 3 {\n\t\t\tsha := parts[0]\n\t\t\ttimestampStr := parts[1]\n\t\t\tmessage := strings.TrimSpace(parts[2]) // Trim whitespace from message as well.\n\n\t\t\t// Extract and format timestamp.\n\t\t\ttimestamp, err := strconv.ParseInt(timestampStr, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tcontinue // Skip entries with invalid timestamps.\n\t\t\t}\n\n\t\t\tdt := time.Unix(timestamp, 0).UTC()\n\t\t\tformattedTs := dt.Format(\"Mon Jan 2, 2006 | 3:04:05pm MST\")\n\n\t\t\t// Prepare the header with colors.\n\t\t\theaderColor := color.New(color.FgCyan, color.Bold)\n\t\t\tdateColor := color.New(color.FgCyan)\n\n\t\t\t// Combine sha, formatted timestamp, and message header into one string.\n\t\t\theader := fmt.Sprintf(\"%s | %s\", headerColor.Sprintf(\"📝 Update %s\", sha), dateColor.Sprintf(\"%s\", formattedTs))\n\n\t\t\t// Combine header and message with a newline only if the message is not empty.\n\t\t\tfullEntry := header\n\t\t\tif message != \"\" {\n\t\t\t\tfullEntry += \"\\n\" + message\n\t\t\t}\n\n\t\t\thistory = append(history, [2]string{sha, fullEntry})\n\t\t}\n\t}\n\n\treturn history\n}\n\nfunc removeLockFile(lockFilePath string) error {\n\t_, err := os.Stat(lockFilePath)\n\texists := err == nil\n\t// log.Println(\"index.lock file exists:\", exists)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"error checking lock file: %v\", err)\n\t}\n\n\tattempts := 0\n\tfor exists {\n\t\tif attempts > 10 {\n\t\t\treturn fmt.Errorf(\"error removing index.lock file: %v after %d attempts\", err, attempts)\n\t\t}\n\n\t\tlog.Printf(\"[Git] removeLockFile - removing index.lock file: %s, attempt: %d\", lockFilePath, attempts)\n\n\t\tif err := os.Remove(lockFilePath); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tlog.Printf(\"[Git] removeLockFile - %s file not found, skipping removal\", lockFilePath)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"error removing lock file: %v\", err)\n\t\t}\n\n\t\t_, err = os.Stat(lockFilePath)\n\t\texists = err == nil\n\n\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\treturn fmt.Errorf(\"error checking lock file: %v\", err)\n\t\t}\n\n\t\tlog.Printf(\"[Git] removeLockFile - after removal, %s file exists: %t\", lockFilePath, exists)\n\t\tif exists {\n\t\t\tlog.Printf(\"[Git] removeLockFile - %s file still exists, retrying after delay\", lockFilePath)\n\t\t} else {\n\t\t\tlog.Printf(\"[Git] removeLockFile - %s file removed successfully\", lockFilePath)\n\t\t\treturn nil\n\t\t}\n\n\t\tattempts++\n\t\ttime.Sleep(20 * time.Millisecond)\n\t}\n\n\treturn nil\n}\n\nfunc gitRemoveIndexLockFileIfExists(repoDir string) error {\n\tlog.Printf(\"[Git] gitRemoveIndexLockFileIfExists - repoDir: %s\", repoDir)\n\n\tpaths := []string{\n\t\tfilepath.Join(repoDir, \".git\", \"index.lock\"),\n\t\tfilepath.Join(repoDir, \".git\", \"refs\", \"heads\", \"HEAD.lock\"),\n\t\tfilepath.Join(repoDir, \".git\", \"HEAD.lock\"),\n\t}\n\n\terrCh := make(chan error, len(paths))\n\n\tfor _, path := range paths {\n\t\tgo func(path string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in gitRemoveIndexLockFileIfExists: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in gitRemoveIndexLockFileIfExists: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tif err := removeLockFile(path); err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path)\n\t}\n\n\terrs := []error{}\n\tfor i := 0; i < len(paths); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn fmt.Errorf(\"error removing lock files: %v\", errs)\n\t}\n\n\treturn nil\n}\n\nfunc setGitConfig(repoDir, key, value string) error {\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"config\", key, value).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting git config %s to %s for dir: %s, err: %v, output: %s\", key, value, repoDir, err, string(res))\n\t}\n\treturn nil\n}\n\nfunc gitWriteOperation(operation func() error, repoDir, label string) error {\n\tlog.Printf(\"[Git] gitWriteOperation - label: %s\", label)\n\tvar err error\n\tfor attempt := 0; attempt < maxGitRetries; attempt++ {\n\t\tif attempt > 0 {\n\t\t\tdelay := time.Duration(1<<uint(attempt-1)) * baseGitRetryDelay // Exponential backoff\n\t\t\ttime.Sleep(delay)\n\t\t\tlog.Printf(\"Retry attempt %d for git operation %s (delay: %v)\\n\", attempt+1, label, delay)\n\t\t}\n\n\t\terr = operation()\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Check if error is retryable\n\t\tif strings.Contains(err.Error(), \"index.lock\") || strings.Contains(err.Error(), \"cannot lock ref\") {\n\t\t\tlog.Printf(\"Git lock file error detected for %s, will retry: %v\\n\", label, err)\n\t\t\terr = gitRemoveIndexLockFileIfExists(repoDir)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"error removing lock files: %v\", err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Non-retryable error\n\t\treturn err\n\t}\n\treturn fmt.Errorf(\"operation %s failed after %d attempts: %v\", label, maxGitRetries, err)\n}\n\n// LogGitRepoState prints out useful debug info about the current git repository:\n//   - The currently checked-out branch\n//   - The last few commits\n//   - The status (untracked changes, etc.)\n//   - A directory listing of refs/heads\n//   - A directory listing of .git/ (to spot any leftover lock files or HEAD files)\nfunc (repo *GitRepo) LogGitRepoState() {\n\trepoDir := getPlanDir(repo.orgId, repo.planId)\n\n\tlog.Println(\"[DEBUG] --- Git Repo State ---\")\n\n\t// 1. Current branch\n\tout, err := exec.Command(\"git\", \"-C\", repoDir, \"branch\", \"--show-current\").CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] error running `git branch --show-current`: %v, output: %s\", err, string(out))\n\t} else {\n\t\tlog.Printf(\"[DEBUG] Current branch: %s\", string(out))\n\t}\n\n\t// 2. Recent commits\n\tout, err = exec.Command(\"git\", \"-C\", repoDir, \"log\", \"--oneline\", \"-5\").CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] error running `git log --oneline -5`: %v, output: %s\", err, string(out))\n\t} else {\n\t\tlog.Printf(\"[DEBUG] Recent commits:\\n%s\", string(out))\n\t}\n\n\t// 3. Git status\n\tout, err = exec.Command(\"git\", \"-C\", repoDir, \"status\", \"--short\", \"--branch\").CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] error running `git status`: %v, output: %s\", err, string(out))\n\t} else {\n\t\tlog.Printf(\"[DEBUG] Git status:\\n%s\", string(out))\n\t}\n\n\t// 4. Show all refs (to see if `.git/refs/heads/HEAD` exists)\n\tout, err = exec.Command(\"git\", \"-C\", repoDir, \"show-ref\").CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] error running `git show-ref`: %v, output: %s\", err, string(out))\n\t} else {\n\t\tlog.Printf(\"[DEBUG] All refs:\\n%s\", string(out))\n\t}\n\n\t// 5. Directory listing of .git/refs/heads\n\theadsDir := filepath.Join(repoDir, \".git\", \"refs\", \"heads\")\n\tout, err = exec.Command(\"ls\", \"-l\", headsDir).CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] error listing heads dir: %s, err: %v, output: %s\", headsDir, err, string(out))\n\t} else {\n\t\tlog.Printf(\"[DEBUG] .git/refs/heads contents:\\n%s\", string(out))\n\t}\n\n\t// 5a. If there's actually a HEAD file in `.git/refs/heads`, cat it.\n\theadRefPath := filepath.Join(headsDir, \"HEAD\")\n\tif _, err := os.Stat(headRefPath); err == nil {\n\t\t// The file `.git/refs/heads/HEAD` exists, which is unusual\n\t\tlog.Printf(\"[DEBUG] Found .git/refs/heads/HEAD. Dumping contents:\")\n\t\tcatOut, _ := exec.Command(\"cat\", headRefPath).CombinedOutput()\n\t\tlog.Printf(\"[DEBUG] .git/refs/heads/HEAD contents:\\n%s\", string(catOut))\n\t} else if !os.IsNotExist(err) {\n\t\tlog.Printf(\"[DEBUG] error checking for .git/refs/heads/HEAD: %v\", err)\n\t}\n\n\t// 6. Directory listing of .git/ in case there's HEAD.lock or index.lock\n\tgitDir := filepath.Join(repoDir, \".git\")\n\tout, err = exec.Command(\"ls\", \"-l\", gitDir).CombinedOutput()\n\tif err != nil {\n\t\tlog.Printf(\"[DEBUG] error listing .git dir: %s, err: %v, output: %s\", gitDir, err, string(out))\n\t} else {\n\t\tlog.Printf(\"[DEBUG] .git/ contents:\\n%s\", string(out))\n\t}\n\n\t// 6a. If there's a .git/HEAD file, cat it\n\theadFilePath := filepath.Join(gitDir, \"HEAD\")\n\tif _, err := os.Stat(headFilePath); err == nil {\n\t\tlog.Printf(\"[DEBUG] .git/HEAD file exists. Dumping contents:\")\n\t\tcatOut, _ := exec.Command(\"cat\", headFilePath).CombinedOutput()\n\t\tlog.Printf(\"[DEBUG] .git/HEAD contents:\\n%s\", string(catOut))\n\t} else if !os.IsNotExist(err) {\n\t\tlog.Printf(\"[DEBUG] error checking for .git/HEAD: %v\", err)\n\t}\n\n\t// 6b. Check for HEAD.lock or index.lock specifically\n\theadLockPath := filepath.Join(gitDir, \"HEAD.lock\")\n\tif _, err := os.Stat(headLockPath); err == nil {\n\t\tlog.Printf(\"[DEBUG] HEAD.lock file exists at: %s\", headLockPath)\n\t} else if !os.IsNotExist(err) {\n\t\tlog.Printf(\"[DEBUG] error checking for HEAD.lock: %v\", err)\n\t}\n\n\tindexLockPath := filepath.Join(gitDir, \"index.lock\")\n\tif _, err := os.Stat(indexLockPath); err == nil {\n\t\tlog.Printf(\"[DEBUG] index.lock file exists at: %s\", indexLockPath)\n\t} else if !os.IsNotExist(err) {\n\t\tlog.Printf(\"[DEBUG] error checking for index.lock: %v\", err)\n\t}\n\n\tlog.Println(\"[DEBUG] --- End Git Repo State ---\")\n}\n"
  },
  {
    "path": "app/server/db/invite_helpers.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc CreateInvite(invite *Invite, tx *sqlx.Tx) error {\n\terr := tx.QueryRow(\"INSERT INTO invites (org_id, email, name, inviter_id, org_role_id) VALUES ($1, $2, $3, $4, $5) RETURNING id\", invite.OrgId, invite.Email, invite.Name, invite.InviterId, invite.OrgRoleId).Scan(&invite.Id)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating invite: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetInvite(id string) (*Invite, error) {\n\tvar invite Invite\n\terr := Conn.Get(&invite, \"SELECT * FROM invites WHERE id = $1\", id)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting invite: %v\", err)\n\t}\n\n\treturn &invite, nil\n}\n\nfunc GetActiveInviteByEmail(orgId, email string) (*Invite, error) {\n\tvar invite Invite\n\terr := Conn.Get(&invite, \"SELECT * FROM invites WHERE org_id = $1 AND email = $2 AND accepted_at IS NULL\", orgId, email)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting invite: %v\", err)\n\t}\n\n\treturn &invite, nil\n}\n\nfunc ListPendingInvites(orgId string) ([]*Invite, error) {\n\tvar invites []*Invite\n\terr := Conn.Select(&invites, \"SELECT * FROM invites WHERE org_id = $1 AND accepted_at IS NULL\", orgId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting pending invites for org: %v\", err)\n\t}\n\n\treturn invites, nil\n}\n\nfunc ListAllInvites(orgId string) ([]*Invite, error) {\n\tvar invites []*Invite\n\terr := Conn.Select(&invites, \"SELECT * FROM invites WHERE org_id = $1\", orgId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting all invites for org: %v\", err)\n\t}\n\n\treturn invites, nil\n}\n\nfunc ListAcceptedInvites(orgId string) ([]*Invite, error) {\n\tvar invites []*Invite\n\terr := Conn.Select(&invites, \"SELECT * FROM invites WHERE org_id = $1 AND accepted_at IS NOT NULL\", orgId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting accepted invites for org: %v\", err)\n\t}\n\n\treturn invites, nil\n}\n\nfunc GetPendingInvitesForEmail(email string) ([]*Invite, error) {\n\temail = strings.ToLower(email)\n\tvar invites []*Invite\n\terr := Conn.Select(&invites, \"SELECT * FROM invites WHERE email = $1 AND accepted_at IS NULL\", email)\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting invites and org names for email: %v\", err)\n\t}\n\n\treturn invites, nil\n}\n\nfunc DeleteInvite(id string, tx *sqlx.Tx) error {\n\tquery := \"DELETE FROM invites WHERE id = $1\"\n\tvar err error\n\n\tif tx == nil {\n\t\t_, err = Conn.Exec(query, id)\n\t} else {\n\t\t_, err = tx.Exec(query, id)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting invite: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc AcceptInvite(ctx context.Context, invite *Invite, inviteeId string) error {\n\terr := WithTx(ctx, \"accept invite\", func(tx *sqlx.Tx) error {\n\n\t\t_, err := tx.Exec(`UPDATE invites SET accepted_at = NOW(), invitee_id = $1 WHERE id = $2`, inviteeId, invite.Id)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error accepting invite: %v\", err)\n\t\t}\n\n\t\t// create org user\n\t\terr = CreateOrgUser(invite.OrgId, inviteeId, invite.OrgRoleId, tx)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating org user: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error accepting invite: %v\", err)\n\t}\n\n\tinvite.InviteeId = &inviteeId\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/locks.go",
    "content": "package db\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"math\"\n\t\"math/rand\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/shutdown\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/lib/pq\"\n\t\"github.com/pkg/errors\"\n)\n\nconst locksVerboseLogging = false\n\nconst lockHeartbeatInterval = 3 * time.Second\nconst lockHeartbeatTimeout = 60 * time.Second\nconst maxLockRetries = 6\nconst initialLockRetryDelay = 300 * time.Millisecond\nconst backoffFactor = 2    // Exponential base\nconst jitterFraction = 0.3 // e.g. ±30% of the backoff\n\n// We want deletes to win quickly vs. lock reads, so we retry them more aggressively with no backoff\nconst maxDeleteRetries = 60\nconst deleteRetryDelay = 50 * time.Millisecond\n\nvar activeLockIds = make(map[string]bool)\nvar activeLockIdsMu sync.Mutex\n\n// LockRepoParams holds the data needed for your lock calls\ntype LockRepoParams struct {\n\tOrgId       string\n\tUserId      string\n\tPlanId      string\n\tBranch      string\n\tScope       LockScope\n\tPlanBuildId string\n\tCtx         context.Context\n\tCancelFn    context.CancelFunc\n\tReason      string\n}\n\nfunc lockRepoDB(params LockRepoParams, numRetry int) (string, error) {\n\tstart := time.Now()\n\tgoroutineID := getGoroutineID()\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Lock][%d] START lock attempt for plan %s scope %s (retry %d) at %v | reason: %s\",\n\t\t\tgoroutineID, params.PlanId, params.Scope, numRetry, start, params.Reason)\n\t}\n\n\tdefer func() {\n\t\tif locksVerboseLogging {\n\t\t\telapsed := time.Since(start)\n\t\t\tlog.Printf(\"[Lock][%d] END lock attempt took %v | reason: %s\", goroutineID, elapsed, params.Reason)\n\t\t}\n\t}()\n\n\t// ensure context did not cancel\n\tif params.Ctx.Err() != nil {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Lock][%d] Context canceled, returning error: %v | reason: %s\", goroutineID, params.Ctx.Err(), params.Reason)\n\t\t}\n\t\treturn \"\", params.Ctx.Err()\n\t}\n\n\tinitialJitter := time.Duration(rand.Int63n(int64(5000 * time.Microsecond)))\n\n\tselect {\n\tcase <-params.Ctx.Done():\n\t\treturn \"\", params.Ctx.Err()\n\tcase <-time.After(initialJitter):\n\t}\n\n\torgId := params.OrgId\n\tuserId := params.UserId\n\tplanId := params.PlanId\n\tbranch := params.Branch\n\tscope := params.Scope\n\tplanBuildId := params.PlanBuildId\n\tctx := params.Ctx\n\tcancelFn := params.CancelFn\n\n\tif orgId == \"\" {\n\t\treturn \"\", fmt.Errorf(\"orgId is required\")\n\t}\n\tif planId == \"\" {\n\t\treturn \"\", fmt.Errorf(\"planId is required\")\n\t}\n\tif scope != LockScopeRead && scope != LockScopeWrite {\n\t\treturn \"\", fmt.Errorf(\"invalid lock scope: %s\", scope)\n\t}\n\n\ttx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})\n\tif err != nil {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Lock][%d] Error starting transaction %v | reason: %s\",\n\t\t\t\tgoroutineID, err, params.Reason)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"error starting transaction: %v\", err)\n\t}\n\n\tvar committed bool\n\n\t// Ensure that rollback is attempted in case of failure\n\tdefer func() {\n\t\tif committed {\n\t\t\treturn\n\t\t}\n\n\t\tpanicErr := recover()\n\t\tif panicErr != nil {\n\t\t\tlog.Printf(\"panic in lock repo: %v\", panicErr)\n\t\t}\n\n\t\tif rbErr := tx.Rollback(); rbErr != nil {\n\t\t\tif rbErr == sql.ErrTxDone {\n\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t// log.Println(\"attempted to roll back transaction, but it was already committed\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"transaction rollback error: %v\\n\", rbErr)\n\t\t\t}\n\t\t} else {\n\t\t\tif locksVerboseLogging {\n\t\t\t\tlog.Println(\"transaction rolled back\")\n\t\t\t}\n\t\t}\n\t}()\n\n\tforUpdate := params.Scope == LockScopeWrite\n\n\tselectStart := time.Now()\n\tif locksVerboseLogging {\n\t\tif forUpdate {\n\t\t\tlog.Printf(\"[Lock][%d] Starting SELECT FOR UPDATE at %v | reason: %s\", goroutineID, selectStart, params.Reason)\n\t\t} else {\n\t\t\tlog.Printf(\"[Lock][%d] Starting SELECT FOR SHARE at %v | reason: %s\", goroutineID, selectStart, params.Reason)\n\t\t}\n\t}\n\n\tlockablePlanIdQuery := \"SELECT * FROM lockable_plan_ids WHERE plan_id = $1\"\n\tif forUpdate {\n\t\tlockablePlanIdQuery += \" FOR UPDATE\"\n\t} else {\n\t\tlockablePlanIdQuery += \" FOR SHARE\"\n\t}\n\n\t_, err = tx.Exec(lockablePlanIdQuery, planId)\n\tif err != nil {\n\t\tlog.Printf(\"[Lock][%d] getting lockable plan id for %s: %v | reason: %s\", goroutineID, planId, err, params.Reason)\n\t\treturn retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {\n\t\t\treturn lockRepoDB(params, nextAttempt)\n\t\t})\n\t}\n\n\tlog.Printf(\"[Lock][%d] got lockable plan id for %s | reason: %s\", goroutineID, planId, params.Reason)\n\n\tquery := \"SELECT id, org_id, user_id, plan_id, plan_build_id, scope, branch, last_heartbeat_at, created_at FROM repo_locks WHERE plan_id = $1\"\n\tif forUpdate {\n\t\tquery += \" FOR UPDATE\"\n\t} else {\n\t\tquery += \" FOR SHARE\"\n\t}\n\tqueryArgs := []interface{}{planId}\n\n\tvar locks []*repoLock\n\tif locksVerboseLogging {\n\t\tlog.Println(\"obtaining repo lock with query\")\n\t}\n\trepoLockRows, err := tx.Query(query, queryArgs...)\n\tif err != nil {\n\t\tlog.Printf(\"[Lock][%d] error obtaining repo lock with query: %v | reason: %s\", goroutineID, err, params.Reason)\n\t\treturn retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {\n\t\t\treturn lockRepoDB(params, nextAttempt)\n\t\t})\n\t}\n\tif locksVerboseLogging {\n\t\tlog.Println(\"repo lock query executed\")\n\t}\n\n\tif locksVerboseLogging {\n\t\tif forUpdate {\n\t\t\tlog.Printf(\"[Lock][%d] SELECT FOR UPDATE took %v | reason: %s\",\n\t\t\t\tgoroutineID, time.Since(selectStart), params.Reason)\n\t\t} else {\n\t\t\tlog.Printf(\"[Lock][%d] SELECT FOR SHARE took %v | reason: %s\",\n\t\t\t\tgoroutineID, time.Since(selectStart), params.Reason)\n\t\t}\n\t}\n\n\tdefer repoLockRows.Close()\n\n\tvar expiredLockIds []string\n\texpiredLockIdsSet := make(map[string]bool)\n\n\tnow := time.Now()\n\tfor repoLockRows.Next() {\n\t\tvar lock repoLock\n\t\tif err := repoLockRows.Scan(&lock.Id, &lock.OrgId, &lock.UserId, &lock.PlanId, &lock.PlanBuildId, &lock.Scope, &lock.Branch, &lock.LastHeartbeatAt, &lock.CreatedAt); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error scanning repo lock: %v\", err)\n\t\t}\n\n\t\t// ensure heartbeat hasn't timed out\n\t\tif now.Sub(lock.LastHeartbeatAt) < lockHeartbeatTimeout {\n\t\t\tlocks = append(locks, &lock)\n\t\t} else {\n\t\t\texpiredLockIds = append(expiredLockIds, lock.Id)\n\t\t\texpiredLockIdsSet[lock.Id] = true\n\t\t}\n\t}\n\n\tif err := repoLockRows.Err(); err != nil {\n\t\tlog.Printf(\"[Lock][%d] error iterating over repo locks: %v | reason: %s\", goroutineID, err, params.Reason)\n\t\treturn \"\", fmt.Errorf(\"error iterating over repo locks: %v\", err)\n\t}\n\n\tlog.Printf(\"[Lock][%d] %d locks found, %d expired | reason: %s\", goroutineID, len(locks), len(expiredLockIds), params.Reason)\n\n\tif len(expiredLockIds) > 0 {\n\t\tlog.Printf(\"[Lock][%d] %d expired locks found, deleting | reason: %s\", goroutineID, len(expiredLockIds), params.Reason)\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"deleting expired locks: %v\", expiredLockIds)\n\t\t}\n\n\t\tquery := \"DELETE FROM repo_locks WHERE id = ANY($1)\"\n\t\t_, err := tx.Exec(query, pq.Array(expiredLockIds))\n\t\tif err != nil {\n\t\t\tif isDeadlockError(err) {\n\t\t\t\tlog.Println(\"deadlock clearing expired locks, won't do anything\")\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"[Lock][%d] error removing expired locks: %v | reason: %s\", goroutineID, err, params.Reason)\n\t\t\t\treturn \"\", fmt.Errorf(\"error removing expired locks: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tcanAcquire := true\n\n\tfor _, lock := range locks {\n\t\tlockBranch := \"\"\n\t\tif lock.Branch != nil {\n\t\t\tlockBranch = *lock.Branch\n\t\t}\n\n\t\tif scope == LockScopeRead {\n\t\t\t// if we're trying to acquire a read lock, we can do so unless there's a conflicting lock\n\t\t\t// a write lock always conflicts with a read lock (regardless of branch)\n\t\t\t// 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)\n\t\t\tif lock.Scope == LockScopeWrite {\n\t\t\t\tcanAcquire = false\n\t\t\t\tbreak\n\t\t\t} else if lock.Scope == LockScopeRead {\n\t\t\t\tif lockBranch != branch {\n\t\t\t\t\tcanAcquire = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else if scope == LockScopeWrite {\n\t\t\t// if we're trying to acquire a write lock, we can only do so if there's no other lock (read or write)\n\t\t\tcanAcquire = false\n\t\t\tbreak\n\t\t} else {\n\t\t\terr = fmt.Errorf(\"invalid lock scope: %v\", scope)\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif !canAcquire {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Println(\"can't acquire lock.\", \"numRetry:\", numRetry)\n\t\t}\n\t\tconflictErr := errors.New(\"lock conflict: cannot acquire read/write lock\")\n\t\tlog.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))\n\n\t\treturn retryWithExponentialBackoff(params.Ctx, conflictErr, numRetry, func(nextAttempt int) (string, error) {\n\t\t\treturn lockRepoDB(params, nextAttempt)\n\t\t})\n\t}\n\n\tif locksVerboseLogging {\n\t\tlog.Println(\"can acquire lock - inserting new lock\")\n\t}\n\n\tinsertStart := time.Now()\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Lock][%d] Starting INSERT at %v | reason: %s\", goroutineID, insertStart, params.Reason)\n\t}\n\n\t// Insert the new lock\n\tvar lockPlanBuildId *string\n\tif planBuildId != \"\" {\n\t\tlockPlanBuildId = &planBuildId\n\t}\n\n\tvar lockBranch *string\n\tif branch != \"\" {\n\t\tlockBranch = &branch\n\t}\n\n\tnewLock := &repoLock{\n\t\tPlanId:      planId,\n\t\tOrgId:       orgId,\n\t\tPlanBuildId: lockPlanBuildId,\n\t\tScope:       scope,\n\t\tBranch:      lockBranch,\n\t}\n\n\tif userId != \"\" {\n\t\tnewLock.UserId = &userId\n\t}\n\n\tvar insertedId sql.NullString\n\n\tinsertQuery := \"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\"\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"Insert query: %s\", insertQuery)\n\t}\n\n\terr = tx.QueryRow(\n\t\tinsertQuery,\n\t\tnewLock.OrgId,\n\t\tnewLock.UserId,\n\t\tnewLock.PlanId,\n\t\tnewLock.PlanBuildId,\n\t\tnewLock.Scope,\n\t\tnewLock.Branch,\n\t).Scan(&insertedId)\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\t// Means ON CONFLICT DO NOTHING prevented insertion\n\t\t\t// => concurrency conflict => backoff & retry\n\t\t\treturn retryWithExponentialBackoff(params.Ctx,\n\t\t\t\terrors.New(\"lock conflict: row not inserted\"),\n\t\t\t\tnumRetry,\n\t\t\t\tfunc(nextAttempt int) (string, error) {\n\t\t\t\t\treturn lockRepoDB(params, nextAttempt)\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\n\t\tlog.Printf(\"[Lock][%d] error inserting new lock: %v | reason: %s\", goroutineID, err, params.Reason)\n\t\treturn \"\", fmt.Errorf(\"error inserting new lock: %v\", err)\n\t}\n\n\tif insertedId.Valid {\n\t\tnewLock.Id = insertedId.String\n\t} else {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"no rows returned from insert query, means there was a conflict\")\n\t\t}\n\t\treturn retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {\n\t\t\treturn lockRepoDB(params, nextAttempt)\n\t\t})\n\t}\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Lock][%d] INSERT took %v | reason: %s\",\n\t\t\tgoroutineID, time.Since(insertStart), params.Reason)\n\t}\n\n\t// Commit the transaction\n\tif err = tx.Commit(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error committing transaction: %v\", err)\n\t}\n\n\tcommitted = true\n\n\tactiveLockIdsMu.Lock()\n\tactiveLockIds[newLock.Id] = true\n\tactiveLockIdsMu.Unlock()\n\n\tlog.Printf(\"Lock acquired: %s for plan %s with scope %s | reason: %s\", newLock.Id, planId, scope, params.Reason)\n\n\t// Start a goroutine to keep the lock alive\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in heartbeat goroutine: %v\\n%s\", r, debug.Stack())\n\t\t\t\tcancelFn()\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic in lock heartbeat goroutine: %v\\n%s\", r, debug.Stack()))\n\t\t\t}\n\t\t}()\n\n\t\tonCancel := func() {\n\t\t\tlog.Printf(\"[Lock][Heartbeat] Timeout or context canceled during heartbeat loop for lock %s for plan %s | reason: %s\", newLock.Id, planId, params.Reason)\n\t\t}\n\n\t\tnumErrors := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tonCancel()\n\t\t\t\treturn\n\n\t\t\tdefault:\n\t\t\t\tjitter := time.Duration(rand.Int63n(int64(float64(lockHeartbeatInterval)*0.1)) * int64(numErrors+1))\n\n\t\t\t\tlog.Printf(\"[Lock][Heartbeat] Will update heartbeat for %s | %s | Heartbeat interval: %s, jitter: %s\", planId, params.Reason, lockHeartbeatInterval, jitter)\n\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tonCancel()\n\t\t\t\t\treturn\n\t\t\t\tcase <-time.After(lockHeartbeatInterval + jitter):\n\t\t\t\t}\n\n\t\t\t\tlog.Printf(\"[Lock][Heartbeat] %s | %s | Updating repo lock last heartbeat\\n\", planId, params.Reason)\n\n\t\t\t\tres, err := Conn.Exec(\"UPDATE repo_locks SET last_heartbeat_at = NOW() WHERE id = $1\", newLock.Id)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"[Lock][Heartbeat] %s | %s | Error updating repo lock last heartbeat: %v\\n\", planId, params.Reason, err)\n\n\t\t\t\t\tif isDeadlockError(err) {\n\t\t\t\t\t\tlog.Printf(\"[Lock][Heartbeat] %s | %s | Heartbeat deadlock error, keep retrying\\n\", planId, params.Reason)\n\t\t\t\t\t}\n\n\t\t\t\t\tnumErrors++\n\n\t\t\t\t\tif numErrors > 5 {\n\t\t\t\t\t\tlog.Printf(\"[Lock][Heartbeat] %s | %s | Too many errors updating repo lock last heartbeat: %v\\n\", planId, params.Reason, err)\n\t\t\t\t\t\tcancelFn()\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// check if 0 rows were updated\n\t\t\t\t\trowsAffected, err := res.RowsAffected()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"Error getting rows affected: %v\\n\", err)\n\t\t\t\t\t\tcancelFn()\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tif rowsAffected == 0 {\n\t\t\t\t\t\tlog.Printf(\"[Lock][Heartbeat] %s | %s | Lock not found: %s | stopping heartbeat loop\\n\", planId, params.Reason, newLock.Id)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.Printf(\"[Lock][Heartbeat] %s | %s | Lock found: %s | continuing heartbeat loop\\n\", planId, params.Reason, newLock.Id)\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t}()\n\n\t// check if git lock file exists\n\t// remove it if so\n\terr = gitRemoveIndexLockFileIfExists(getPlanDir(orgId, planId))\n\tif err != nil {\n\t\tlog.Printf(\"[Lock] %s | %s | Error removing lock file: %v\", planId, params.Reason, err)\n\t\treturn newLock.Id, fmt.Errorf(\"error removing lock file: %v\", err)\n\t}\n\n\tif branch != \"\" {\n\t\t// checkout the branch\n\t\terr = gitCheckoutBranch(getPlanDir(orgId, planId), branch)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[Lock] %s | %s | Error checking out branch: %v\", planId, params.Reason, err)\n\t\t\treturn newLock.Id, fmt.Errorf(\"error checking out branch: %v\", err)\n\t\t}\n\t\tlog.Printf(\"[Lock] %s | %s | Checked out branch\", planId, params.Reason)\n\t}\n\n\treturn newLock.Id, nil\n}\n\nfunc deleteRepoLockDB(id, planId, reason string, numRetry int) error {\n\tstart := time.Now()\n\tgoroutineID := getGoroutineID()\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Lock][Delete][%d] START delete lock %s at %v | reason: %s\", goroutineID, id, start, reason)\n\n\t\tdefer func() {\n\t\t\tlog.Printf(\"[Lock][Delete][%d] END delete lock took %v | reason: %s\", goroutineID, time.Since(start), reason)\n\t\t}()\n\t}\n\n\tresult, err := Conn.Exec(\"DELETE FROM repo_locks WHERE id = $1\", id)\n\tif err != nil {\n\t\tlog.Printf(\"[Lock][Delete][%d] Error deleting lock: %v | reason: %s\", goroutineID, err, reason)\n\n\t\terr := retryDeleteLock(shutdown.ShutdownCtx, err, numRetry, func(nextAttempt int) error {\n\t\t\treturn deleteRepoLockDB(id, planId, reason, nextAttempt)\n\t\t})\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[Lock][Delete][%d] Error deleting lock after retries: %v | %s | %s | %s\", goroutineID, err, id, planId, reason)\n\t\t\treturn err\n\t\t}\n\n\t\t// retries succeeded, stop\n\t\treturn nil\n\t}\n\n\trowsAffected, _ := result.RowsAffected()\n\tif rowsAffected > 0 {\n\t\tlog.Printf(\"[Lock][Delete][%d] Lock released: %s for plan %s | %s\", goroutineID, id, planId, reason)\n\t} else {\n\t\tlog.Printf(\"[Lock][Delete][%d] Lock not found: %s | %s | %s\", goroutineID, id, planId, reason)\n\t}\n\n\tactiveLockIdsMu.Lock()\n\tdelete(activeLockIds, id)\n\tactiveLockIdsMu.Unlock()\n\n\treturn nil\n}\n\nfunc formatStackTrace(stack []byte) string {\n\tnumLines := 10\n\tif !locksVerboseLogging {\n\t\tnumLines = 5\n\t}\n\treturn formatStackTraceWithNumLines(stack, numLines)\n}\n\nfunc formatStackTraceLong(stack []byte) string {\n\treturn formatStackTraceWithNumLines(stack, 20)\n}\n\nfunc formatStackTraceWithNumLines(stack []byte, numLines int) string {\n\tlines := strings.Split(string(stack), \"\\n\")\n\t// Take first 10 meaningful lines of stack trace\n\t// Skip runtime frames (first 7 lines) and limit to next 10 lines\n\trelevantLines := lines[7:min(len(lines), 7+numLines)]\n\treturn strings.Join(relevantLines, \"\\n\")\n}\n\nfunc getGoroutineID() uint64 {\n\tb := make([]byte, 64)\n\tb = b[:runtime.Stack(b, false)]\n\tb = bytes.TrimPrefix(b, []byte(\"goroutine \"))\n\tb = b[:bytes.IndexByte(b, ' ')]\n\tn, _ := strconv.ParseUint(string(b), 10, 64)\n\treturn n\n}\n\nfunc isDeadlockError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\n\tif pqErr, ok := err.(*pq.Error); ok && (pqErr.Code == \"40001\" || pqErr.Code == \"40P01\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc retryWithExponentialBackoff(\n\tctx context.Context,\n\tcause error,\n\tattempt int,\n\tnextCall func(int) (string, error),\n) (string, error) {\n\t// If we have retried enough times, bail out.\n\tif attempt >= maxLockRetries {\n\t\tlog.Printf(\"[Lock][Retry][%d] Failed to acquire lock after %d attempts: %v\", getGoroutineID(), attempt, cause)\n\t\treturn \"\", fmt.Errorf(\"failed to acquire lock after %d attempts: %w\", attempt, cause)\n\t}\n\n\t// Exponential delay: initialRetryDelay * 2^(attempt)\n\tbackoff := time.Duration(float64(initialLockRetryDelay) * math.Pow(backoffFactor, float64(attempt)))\n\t// Add jitter: ± jitterFraction\n\tjitterRange := time.Duration(float64(backoff) * jitterFraction)\n\tjitter := time.Duration(rand.Int63n(int64(jitterRange)*2)) - jitterRange\n\n\twait := backoff + jitter\n\tif wait < 0 {\n\t\twait = 0\n\t}\n\n\tlog.Printf(\"[Lock][Retry][%d] Lock/transaction conflict (attempt #%d). Retrying in %s... (cause: %v)\", getGoroutineID(), attempt, wait, cause)\n\n\tselect {\n\tcase <-ctx.Done():\n\t\tlog.Printf(\"[Lock][Retry][%d] Context canceled while waiting to retry: %v\", getGoroutineID(), ctx.Err())\n\t\treturn \"\", fmt.Errorf(\"context canceled while waiting to retry: %w\", ctx.Err())\n\tcase <-time.After(wait):\n\t\t// Proceed with the next attempt.\n\t}\n\n\treturn nextCall(attempt + 1)\n}\n\nfunc retryDeleteLock(ctx context.Context, cause error, attempt int, nextCall func(int) error) error {\n\tif attempt >= maxDeleteRetries {\n\t\treturn fmt.Errorf(\"delete lock failed after 10 attempts: %w\", cause)\n\t}\n\t// retry 10 times, no backoff or maybe a tiny 50ms\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-time.After(deleteRetryDelay):\n\t}\n\treturn nextCall(attempt + 1)\n}\n\nfunc CleanupActiveLocks(ctx context.Context) error {\n\tlog.Println(\"Cleaning up any active repo locks...\")\n\n\t// Start a transaction with repeatable read isolation level\n\ttx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error starting transaction: %v\", err)\n\t}\n\n\t// Ensure rollback is attempted in case of failure\n\tdefer func() {\n\t\tpanicErr := recover()\n\t\tif panicErr != nil {\n\t\t\tlog.Printf(\"panic in cleanup all locks: %v\", panicErr)\n\t\t}\n\n\t\tif rbErr := tx.Rollback(); rbErr != nil {\n\t\t\tif rbErr == sql.ErrTxDone {\n\t\t\t\t// log.Println(\"attempted to roll back transaction, but it was already committed\")\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"transaction rollback error: %v\\n\", rbErr)\n\t\t\t}\n\t\t} else {\n\t\t\tif locksVerboseLogging {\n\t\t\t\tlog.Println(\"transaction rolled back\")\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Delete all active locks\n\tquery := \"DELETE FROM repo_locks WHERE id = ANY($1)\"\n\tids := make([]string, 0, len(activeLockIds))\n\tfor id := range activeLockIds {\n\t\tids = append(ids, id)\n\t}\n\t_, err = tx.Exec(query, pq.Array(ids))\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\tlog.Println(\"No active locks to cleanup\")\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"error removing all locks: %v\", err)\n\t\t}\n\t}\n\n\t// Commit the transaction\n\tif err = tx.Commit(); err != nil {\n\t\treturn fmt.Errorf(\"error committing transaction: %v\", err)\n\t}\n\n\tactiveLockIdsMu.Lock()\n\tactiveLockIds = make(map[string]bool)\n\tactiveLockIdsMu.Unlock()\n\n\tlog.Println(\"Successfully cleaned up all repo locks\")\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/models.go",
    "content": "package db\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/lib/pq\"\n)\n\nfunc UpsertCustomModel(tx *sqlx.Tx, model *CustomModel) error {\n\tif tx == nil {\n\t\treturn fmt.Errorf(\"tx is nil\")\n\t}\n\tquery := `\nINSERT INTO custom_models (\n    org_id, model_id,\n    publisher, description,\n    max_tokens, default_max_convo_tokens, max_output_tokens, reserved_output_tokens,\n    has_image_support, preferred_output_format,\n    system_prompt_disabled, role_params_disabled, stop_disabled,\n    predicted_output_enabled, reasoning_effort_enabled, reasoning_effort,\n    include_reasoning, reasoning_budget, supports_cache_control,\n    single_message_no_system_prompt, token_estimate_padding_pct,\n    providers\n)\nVALUES (\n    $1,$2,\n    $3,$4,\n    $5,$6,$7,$8,\n    $9,$10,\n    $11,$12,$13,\n    $14,$15,$16,\n    $17,$18,$19,\n    $20,$21,\n    $22\n)\nON CONFLICT (org_id, model_id)\nDO UPDATE SET\n    publisher                     = EXCLUDED.publisher,\n    description                   = EXCLUDED.description,\n    max_tokens                    = EXCLUDED.max_tokens,\n    default_max_convo_tokens      = EXCLUDED.default_max_convo_tokens,\n    max_output_tokens             = EXCLUDED.max_output_tokens,\n    reserved_output_tokens        = EXCLUDED.reserved_output_tokens,\n    has_image_support             = EXCLUDED.has_image_support,\n    preferred_output_format       = EXCLUDED.preferred_output_format,\n    system_prompt_disabled        = EXCLUDED.system_prompt_disabled,\n    role_params_disabled          = EXCLUDED.role_params_disabled,\n    stop_disabled                 = EXCLUDED.stop_disabled,\n    predicted_output_enabled      = EXCLUDED.predicted_output_enabled,\n    reasoning_effort_enabled      = EXCLUDED.reasoning_effort_enabled,\n    reasoning_effort              = EXCLUDED.reasoning_effort,\n    include_reasoning             = EXCLUDED.include_reasoning,\n    reasoning_budget              = EXCLUDED.reasoning_budget,\n    supports_cache_control        = EXCLUDED.supports_cache_control,\n    single_message_no_system_prompt = EXCLUDED.single_message_no_system_prompt,\n    token_estimate_padding_pct    = EXCLUDED.token_estimate_padding_pct,\n    providers                     = EXCLUDED.providers\nRETURNING id, created_at, updated_at;\n`\n\n\treturn tx.QueryRow(\n\t\tquery,\n\t\tmodel.OrgId,\n\t\tmodel.ModelId,\n\t\tmodel.Publisher,\n\t\tmodel.Description,\n\t\tmodel.MaxTokens,\n\t\tmodel.DefaultMaxConvoTokens,\n\t\tmodel.MaxOutputTokens,\n\t\tmodel.ReservedOutputTokens,\n\t\tmodel.HasImageSupport,\n\t\tmodel.PreferredOutputFormat,\n\t\tmodel.SystemPromptDisabled,\n\t\tmodel.RoleParamsDisabled,\n\t\tmodel.StopDisabled,\n\t\tmodel.PredictedOutputEnabled,\n\t\tmodel.ReasoningEffortEnabled,\n\t\tmodel.ReasoningEffort,\n\t\tmodel.IncludeReasoning,\n\t\tmodel.ReasoningBudget,\n\t\tmodel.SupportsCacheControl,\n\t\tmodel.SingleMessageNoSystemPrompt,\n\t\tmodel.TokenEstimatePaddingPct,\n\t\tmodel.Providers,\n\t).Scan(&model.Id, &model.CreatedAt, &model.UpdatedAt)\n}\n\nfunc ListCustomModels(orgId string) ([]*CustomModel, error) {\n\tvar models []*CustomModel\n\terr := Conn.Select(&models, `SELECT * FROM custom_models WHERE org_id = $1 ORDER BY created_at`, orgId)\n\treturn models, err\n}\n\nfunc ListCustomModelsForModelIds(orgId string, modelIds []string) ([]*CustomModel, error) {\n\tvar models []*CustomModel\n\tquery := `SELECT * FROM custom_models WHERE org_id = $1 AND model_id = ANY($2) ORDER BY created_at`\n\terr := Conn.Select(&models, query, orgId, pq.Array(modelIds))\n\treturn models, err\n}\n\nfunc GetCustomModel(orgId, id string) (*CustomModel, error) {\n\tvar model CustomModel\n\terr := Conn.Get(&model, `SELECT * FROM custom_models WHERE org_id = $1 AND id = $2`, orgId, id)\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &model, nil\n}\n\nfunc DeleteCustomModels(tx *sqlx.Tx, orgId string, ids []string) error {\n\tif tx == nil {\n\t\treturn fmt.Errorf(\"tx is nil\")\n\t}\n\t_, err := tx.Exec(`DELETE FROM custom_models WHERE org_id = $1 AND id = ANY($2)`, orgId, pq.Array(ids))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting custom models: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc UpsertCustomProvider(tx *sqlx.Tx, p *CustomProvider) error {\n\tif tx == nil {\n\t\treturn fmt.Errorf(\"tx is nil\")\n\t}\n\tconst q = `\nINSERT INTO custom_providers (\n\t  org_id, name, base_url,\n\t  skip_auth, api_key_env_var, extra_auth_vars\n)\nVALUES (\n\t  $1,$2,$3,\n\t  $4,$5,$6\n)\nON CONFLICT (org_id, name)\nDO UPDATE SET\n\t  base_url        = EXCLUDED.base_url,\n\t  skip_auth       = EXCLUDED.skip_auth,\n\t  api_key_env_var = EXCLUDED.api_key_env_var,\n\t  extra_auth_vars = EXCLUDED.extra_auth_vars\nRETURNING id, created_at, updated_at;\n`\n\treturn tx.QueryRow(\n\t\tq,\n\t\tp.OrgId,\n\t\tp.Name,\n\t\tp.BaseUrl,\n\t\tp.SkipAuth,\n\t\tp.ApiKeyEnvVar,\n\t\tp.ExtraAuthVars,\n\t).Scan(&p.Id, &p.CreatedAt, &p.UpdatedAt)\n}\n\nfunc GetCustomProvider(orgId, id string) (*CustomProvider, error) {\n\tvar provider CustomProvider\n\terr := Conn.Get(&provider, `SELECT * FROM custom_providers WHERE org_id = $1 AND id = $2`, orgId, id)\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &provider, nil\n}\n\nfunc ListCustomProviders(orgId string) ([]*CustomProvider, error) {\n\tvar providers []*CustomProvider\n\terr := Conn.Select(&providers, `SELECT * FROM custom_providers WHERE org_id = $1 ORDER BY name`, orgId)\n\treturn providers, err\n}\n\nfunc ListCustomProvidersForNames(orgId string, names []string) ([]*CustomProvider, error) {\n\tvar providers []*CustomProvider\n\tquery := `SELECT * FROM custom_providers WHERE org_id = $1 AND name = ANY($2) ORDER BY name`\n\terr := Conn.Select(&providers, query, orgId, pq.Array(names))\n\treturn providers, err\n}\n\nfunc DeleteCustomProviders(tx *sqlx.Tx, orgId string, ids []string) error {\n\tif tx == nil {\n\t\treturn fmt.Errorf(\"tx is nil\")\n\t}\n\t_, err := tx.Exec(`DELETE FROM custom_providers WHERE org_id = $1 AND id = ANY($2)`, orgId, pq.Array(ids))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting custom providers: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc UpsertModelPack(tx *sqlx.Tx, mp *ModelPack) error {\n\tif tx == nil {\n\t\treturn fmt.Errorf(\"tx is nil\")\n\t}\n\tconst q = `\nINSERT INTO model_sets (\n\t  org_id, name, description,\n\t  planner, coder, plan_summary,\n\t  builder, whole_file_builder, namer,\n\t  commit_msg, exec_status, context_loader\n)\nVALUES (\n\t  $1,$2,$3,\n\t  $4,$5,$6,\n\t  $7,$8,$9,\n\t  $10,$11,$12\n)\nON CONFLICT (org_id, name)\nDO UPDATE SET\n\t  description        = EXCLUDED.description,\n\t  planner            = EXCLUDED.planner,\n\t  coder              = EXCLUDED.coder,\n\t  plan_summary       = EXCLUDED.plan_summary,\n\t  builder            = EXCLUDED.builder,\n\t  whole_file_builder = EXCLUDED.whole_file_builder,\n\t  namer              = EXCLUDED.namer,\n\t  commit_msg         = EXCLUDED.commit_msg,\n\t  exec_status        = EXCLUDED.exec_status,\n\t  context_loader     = EXCLUDED.context_loader\nRETURNING id, created_at;\n`\n\treturn tx.QueryRow(\n\t\tq,\n\t\tmp.OrgId,\n\t\tmp.Name,\n\t\tmp.Description,\n\t\tmp.Planner,\n\t\tmp.Coder,\n\t\tmp.PlanSummary,\n\t\tmp.Builder,\n\t\tmp.WholeFileBuilder,\n\t\tmp.Namer,\n\t\tmp.CommitMsg,\n\t\tmp.ExecStatus,\n\t\tmp.Architect,\n\t).Scan(&mp.Id, &mp.CreatedAt)\n}\n\nfunc ListModelPacks(orgId string) ([]*ModelPack, error) {\n\tvar modelPacks []*ModelPack\n\n\tquery := `SELECT * FROM model_sets WHERE org_id = $1`\n\terr := Conn.Select(&modelPacks, query, orgId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error fetching model packs: %v\", err)\n\t}\n\n\treturn modelPacks, nil\n}\n\nfunc ListModelPacksForNames(orgId string, names []string) ([]*ModelPack, error) {\n\tvar modelPacks []*ModelPack\n\tquery := `SELECT * FROM model_sets WHERE org_id = $1 AND name = ANY($2)`\n\terr := Conn.Select(&modelPacks, query, orgId, names)\n\treturn modelPacks, err\n}\n\nfunc DeleteModelPacks(tx *sqlx.Tx, orgId string, ids []string) error {\n\tif tx == nil {\n\t\treturn fmt.Errorf(\"tx is nil\")\n\t}\n\t_, err := tx.Exec(`DELETE FROM model_sets WHERE org_id = $1 AND id = ANY($2)`, orgId, pq.Array(ids))\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting model pack: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/org_helpers.go",
    "content": "package db\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/lib/pq\"\n)\n\nconst orgFields = \"id, name, domain, auto_add_domain_users, owner_id, is_trial, created_at, updated_at\"\n\nfunc GetAccessibleOrgsForUser(user *User) ([]*Org, error) {\n\t// direct access\n\tvar orgUsers []*OrgUser\n\tvar orgs []*Org\n\n\terr := Conn.Select(&orgUsers, \"SELECT * FROM orgs_users WHERE user_id = $1\", user.Id)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting orgs for user: %v\", err)\n\t}\n\n\torgRoleIdByOrgId := map[string]string{}\n\torgIds := []string{}\n\tfor _, ou := range orgUsers {\n\t\torgIds = append(orgIds, ou.OrgId)\n\t\torgRoleIdByOrgId[ou.OrgId] = ou.OrgRoleId\n\t}\n\n\tif len(orgIds) > 0 {\n\t\tquery := fmt.Sprintf(\"SELECT %s FROM orgs WHERE id = ANY($1)\", orgFields)\n\t\terr = Conn.Select(&orgs, query, pq.Array(orgIds))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting orgs for user: %v\", err)\n\t\t}\n\t} else {\n\t\tlog.Println(\"No orgs found for user\")\n\t\treturn orgs, nil\n\t}\n\n\t// access via invitation\n\tinvites, err := GetPendingInvitesForEmail(user.Email)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting invites for user: %v\", err)\n\t}\n\n\torgIds = []string{}\n\tfor _, invite := range invites {\n\t\torgIds = append(orgIds, invite.OrgId)\n\t\torgRoleIdByOrgId[invite.OrgId] = invite.OrgRoleId\n\t}\n\n\tif len(orgIds) > 0 {\n\t\tvar orgsFromInvites []*Org\n\t\tquery := fmt.Sprintf(\"SELECT %s FROM orgs WHERE id = ANY($1)\", orgFields)\n\t\terr = Conn.Select(&orgsFromInvites, query, pq.Array(orgIds))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting orgs from invites: %v\", err)\n\t\t}\n\t\torgs = append(orgs, orgsFromInvites...)\n\t}\n\n\treturn orgs, nil\n}\nfunc GetOrg(orgId string) (*Org, error) {\n\tvar org Org\n\tquery := fmt.Sprintf(\"SELECT %s FROM orgs WHERE id = $1\", orgFields)\n\terr := Conn.Get(&org, query, orgId)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, fmt.Errorf(\"org not found\")\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting org: %v\", err)\n\t}\n\n\treturn &org, nil\n}\n\nfunc ValidateOrgMembership(userId string, orgId string) (bool, error) {\n\tvar count int\n\terr := Conn.QueryRow(\"SELECT COUNT(*) FROM orgs_users WHERE user_id = $1 AND org_id = $2\", userId, orgId).Scan(&count)\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error validating org membership: %v\", err)\n\t}\n\n\treturn count > 0, nil\n}\n\nfunc CreateOrg(req *shared.CreateOrgRequest, userId string, domain *string, tx *sqlx.Tx) (*Org, error) {\n\torg := &Org{\n\t\tName:               req.Name,\n\t\tDomain:             domain,\n\t\tAutoAddDomainUsers: req.AutoAddDomainUsers,\n\t\tOwnerId:            userId,\n\t}\n\n\terr := 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)\n\n\tif err != nil {\n\t\tif IsNonUniqueErr(err) {\n\t\t\t// Handle the uniqueness constraint violation\n\t\t\treturn nil, fmt.Errorf(\"an org with domain %s already exists\", *domain)\n\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error creating org: %v\", err)\n\t}\n\n\torgOwnerRoleId, err := GetOrgOwnerRoleId()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting org owner role id: %v\", err)\n\t}\n\n\t_, err = tx.Exec(\"INSERT INTO orgs_users (org_id, user_id, org_role_id) VALUES ($1, $2, $3)\", org.Id, userId, orgOwnerRoleId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error adding org membership: %v\", err)\n\t}\n\n\treturn org, nil\n}\n\nfunc GetOrgForDomain(domain string) (*Org, error) {\n\tvar org Org\n\tquery := fmt.Sprintf(\"SELECT %s FROM orgs WHERE domain = $1\", orgFields)\n\terr := Conn.Get(&org, query, domain)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting org for domain: %v\", err)\n\t}\n\n\treturn &org, nil\n}\n\nfunc AddOrgDomainUsers(orgId, domain string, tx *sqlx.Tx) error {\n\tusersForDomain, err := GetUsersForDomain(domain)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting users for domain: %v\", err)\n\t}\n\n\torgMemberRoleId, err := GetOrgMemberRoleId()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting org member role id: %v\", err)\n\t}\n\n\tif len(usersForDomain) > 0 {\n\t\t// create org users for each user\n\t\tvar valueStrings []string\n\t\tvar valueArgs []interface{}\n\t\tfor i, user := range usersForDomain {\n\t\t\tnum := i * 3\n\t\t\tvalueStrings = append(valueStrings, fmt.Sprintf(\"($%d, $%d, $%d)\", num+1, num+2, num+3))\n\t\t\tvalueArgs = append(valueArgs, orgId, user.Id, orgMemberRoleId)\n\t\t}\n\n\t\t// Join all value strings and execute a single query\n\t\tstmt := 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, \",\"))\n\t\t_, err = tx.Exec(stmt, valueArgs...)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error adding org users: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc DeleteOrgUser(orgId, userId string, tx *sqlx.Tx) error {\n\tlog.Printf(\"Deleting org user, org: %s | user: %s\\n\", orgId, userId)\n\n\t_, err := tx.Exec(\"DELETE FROM orgs_users WHERE org_id = $1 AND user_id = $2\", orgId, userId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting org member: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc CreateOrgUser(orgId, userId, orgRoleId string, tx *sqlx.Tx) error {\n\tquery := \"INSERT INTO orgs_users (org_id, user_id, org_role_id) VALUES ($1, $2, $3)\"\n\tvar err error\n\tif tx == nil {\n\t\t_, err = Conn.Exec(query, orgId, userId, orgRoleId)\n\t} else {\n\t\t_, err = tx.Exec(query, orgId, userId, orgRoleId)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error adding org member: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc ListOrgRoles(orgId string) ([]*OrgRole, error) {\n\tvar orgRoles []*OrgRole\n\terr := Conn.Select(&orgRoles, \"SELECT * FROM org_roles WHERE org_id IS NULL OR org_id = $1\", orgId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing org roles: %v\", err)\n\t}\n\n\treturn orgRoles, nil\n}\n\nfunc AddToOrgForDomain(userId, domain string, tx *sqlx.Tx) (string, error) {\n\torg, err := GetOrgForDomain(domain)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting org for domain: %v\", err)\n\t}\n\n\torgOwnerRoleId, err := GetOrgOwnerRoleId()\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting org owner role id: %v\", err)\n\t}\n\n\tif org != nil && org.AutoAddDomainUsers {\n\t\terr = CreateOrgUser(org.Id, userId, orgOwnerRoleId, tx)\n\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error adding org user: %v\", err)\n\t\t}\n\t}\n\n\tvar orgId string\n\tif org != nil {\n\t\torgId = org.Id\n\t}\n\n\treturn orgId, nil\n}\n"
  },
  {
    "path": "app/server/db/plan_config_helpers.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc GetPlanConfig(planId string) (*shared.PlanConfig, error) {\n\tquery := \"SELECT plan_config FROM plans WHERE id = $1\"\n\n\tvar config shared.PlanConfig\n\terr := Conn.Get(&config, query, planId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan config: %v\", err)\n\t}\n\n\treturn &config, nil\n}\n\nfunc StorePlanConfig(planId string, config *shared.PlanConfig) error {\n\tquery := `\n\t\tUPDATE plans \n\t\tSET plan_config = $1\n\t\tWHERE id = $2\n\t`\n\n\t_, err := Conn.Exec(query, config, planId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error storing plan config: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetDefaultPlanConfig(userId string) (*shared.PlanConfig, error) {\n\tquery := \"SELECT default_plan_config FROM users WHERE id = $1\"\n\n\tvar config shared.PlanConfig\n\terr := Conn.Get(&config, query, userId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting default plan config: %v\", err)\n\t}\n\n\treturn &config, nil\n}\n\nfunc StoreDefaultPlanConfig(userId string, config *shared.PlanConfig, tx *sqlx.Tx) error {\n\tquery := `\n\t\tUPDATE users SET default_plan_config = $1 WHERE id = $2\n\t`\n\n\t_, err := tx.Exec(query, config, userId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error storing default plan config: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/plan_helpers.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/lib/pq\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc CreatePlan(ctx context.Context, orgId, projectId, userId, name string) (*Plan, error) {\n\tvar plan *Plan\n\terr := WithTx(ctx, \"create plan\", func(tx *sqlx.Tx) error {\n\n\t\tplanConfig, err := GetDefaultPlanConfig(userId)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting default plan config: %v\", err)\n\t\t}\n\n\t\tquery := `INSERT INTO plans (org_id, owner_id, project_id, name, plan_config) \n\tVALUES ($1, $2, $3, $4, $5)\n\tRETURNING id, created_at, updated_at`\n\n\t\tplan = &Plan{\n\t\t\tOrgId:      orgId,\n\t\t\tOwnerId:    userId,\n\t\t\tProjectId:  projectId,\n\t\t\tName:       name,\n\t\t\tPlanConfig: planConfig,\n\t\t}\n\n\t\terr = tx.QueryRow(\n\t\t\tquery,\n\t\t\torgId,\n\t\t\tuserId,\n\t\t\tprojectId,\n\t\t\tname,\n\t\t\tplanConfig,\n\t\t).Scan(\n\t\t\t&plan.Id,\n\t\t\t&plan.CreatedAt,\n\t\t\t&plan.UpdatedAt,\n\t\t)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating plan: %v\", err)\n\t\t}\n\n\t\t_, err = tx.Exec(\"INSERT INTO lockable_plan_ids (plan_id) VALUES ($1)\", plan.Id)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error inserting lockable plan id: %v\", err)\n\t\t}\n\n\t\t// the one place where we do this to skip the locking queue\n\t\t// ok to cheat this once since we're creating a new plan\n\t\trepo := getGitRepo(orgId, plan.Id)\n\t\t_, err = CreateBranch(repo, plan, nil, \"main\", tx)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating main branch: %v\", err)\n\t\t}\n\n\t\tlog.Println(\"Created branch main\")\n\n\t\terr = InitPlan(orgId, plan.Id)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error initializing plan dir: %v\", err)\n\t\t}\n\n\t\tlog.Println(\"Initialized plan dir\")\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn plan, nil\n}\n\nfunc ListOwnedPlans(projectIds []string, userId string, archived bool) ([]*Plan, error) {\n\tqs := \"SELECT * FROM plans WHERE project_id = ANY($1) AND owner_id = $2\"\n\tqargs := []interface{}{pq.Array(projectIds), userId}\n\n\tif archived {\n\t\tqs += \" AND archived_at IS NOT NULL\"\n\t} else {\n\t\tqs += \" AND archived_at IS NULL\"\n\t}\n\n\tqs += \" ORDER BY updated_at DESC\"\n\n\tvar plans []*Plan\n\terr := Conn.Select(&plans, qs, qargs...)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing plans: %v\", err)\n\t}\n\n\treturn plans, nil\n}\n\nfunc GetPlanNamesById(planIds []string) (map[string]string, error) {\n\tvar plans []*Plan\n\terr := Conn.Select(&plans, \"SELECT id, name FROM plans WHERE id = ANY($1)\", pq.Array(planIds))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan names: %v\", err)\n\t}\n\n\tnamesMap := make(map[string]string)\n\tfor _, plan := range plans {\n\t\tnamesMap[plan.Id] = plan.Name\n\t}\n\n\treturn namesMap, nil\n}\n\nfunc AddPlanContextTokens(planId, branch string, addTokens int) error {\n\t_, err := Conn.Exec(\"UPDATE branches SET context_tokens = context_tokens + $1 WHERE plan_id = $2 AND name = $3\", addTokens, planId, branch)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error updating plan tokens: %v\", err)\n\t}\n\treturn nil\n}\n\nfunc AddPlanConvoMessage(msg *ConvoMessage, branch string) error {\n\terrCh := make(chan error, 2)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in AddPlanConvoMessage: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in AddPlanConvoMessage: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\n\t\t_, err := Conn.Exec(\"UPDATE branches SET convo_tokens = convo_tokens + $1 WHERE plan_id = $2 AND name = $3\", msg.Tokens, msg.PlanId, branch)\n\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error updating plan tokens: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in AddPlanConvoMessage: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in AddPlanConvoMessage: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\n\t\tif msg.Role != openai.ChatMessageRoleAssistant {\n\t\t\terrCh <- nil\n\t\t\treturn\n\t\t}\n\t\t_, err := Conn.Exec(\"UPDATE plans SET total_replies = total_replies + 1 WHERE id = $1\", msg.PlanId)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error updating plan total replies: %v\", err)\n\t\t}\n\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error updating plan tokens: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc SyncPlanTokens(orgId, planId, branch string) error {\n\tvar contexts []*Context\n\tvar convos []*ConvoMessage\n\terrCh := make(chan error, 2)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in SyncPlanTokens: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in SyncPlanTokens: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tvar err error\n\t\tcontexts, err = GetPlanContexts(orgId, planId, false, false)\n\t\terrCh <- err\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in SyncPlanTokens: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in SyncPlanTokens: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tvar err error\n\t\tconvos, err = GetPlanConvo(orgId, planId)\n\t\terrCh <- err\n\t}()\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting contexts or convo: %v\", err)\n\t\t}\n\t}\n\n\tcontextTokens := 0\n\tfor _, context := range contexts {\n\t\tcontextTokens += context.NumTokens\n\t}\n\n\tconvoTokens := 0\n\tfor _, msg := range convos {\n\t\tconvoTokens += msg.Tokens\n\t}\n\n\t_, err := Conn.Exec(\"UPDATE branches SET context_tokens = $1, convo_tokens = $2 WHERE plan_id = $3 AND name = $4\", contextTokens, convoTokens, planId, branch)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error updating plan tokens: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetPlan(planId string) (*Plan, error) {\n\tvar plan Plan\n\n\terr := Conn.Get(&plan, \"SELECT * FROM plans WHERE id = $1\", planId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan: %v\", err)\n\t}\n\n\treturn &plan, nil\n}\n\nfunc SetPlanStatus(planId, branch string, status shared.PlanStatus, errStr string) error {\n\t_, err := Conn.Exec(\"UPDATE branches SET status = $1, error = $2 WHERE plan_id = $3 AND name = $4\", status, errStr, planId, branch)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting plan status: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc RenamePlan(planId string, name string, tx *sqlx.Tx) error {\n\tvar err error\n\tif tx == nil {\n\t\t_, err = Conn.Exec(\"UPDATE plans SET name = $1 WHERE id = $2\", name, planId)\n\t} else {\n\t\t_, err = tx.Exec(\"UPDATE plans SET name = $1 WHERE id = $2\", name, planId)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error renaming plan: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc IncActiveBranches(planId string, inc int, tx *sqlx.Tx) error {\n\t_, err := tx.Exec(\"UPDATE plans SET active_branches = active_branches + $1 WHERE id = $2\", inc, planId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error updating plan active branches: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc IncNumNonDraftPlans(userId string, tx *sqlx.Tx) error {\n\t_, err := tx.Exec(\"UPDATE users SET num_non_draft_plans = num_non_draft_plans + 1 WHERE id = $1\", userId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error updating user num_non_draft_plans: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc StoreDescription(description *ConvoMessageDescription) error {\n\tdescriptionsDir := getPlanDescriptionsDir(description.OrgId, description.PlanId)\n\n\terr := os.MkdirAll(descriptionsDir, os.ModePerm)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating convo message descriptions dir: %v\", err)\n\t}\n\n\tfor _, op := range description.Operations {\n\t\tif op.Content != \"\" {\n\t\t\tquoted := strconv.Quote(op.Content)\n\t\t\top.Content = quoted[1 : len(quoted)-1]\n\t\t}\n\t\tif op.Description != \"\" {\n\t\t\tquoted := strconv.Quote(op.Description)\n\t\t\top.Description = quoted[1 : len(quoted)-1]\n\t\t}\n\t}\n\n\tnow := time.Now()\n\n\tif description.Id == \"\" {\n\t\tdescription.Id = uuid.New().String()\n\t\tdescription.CreatedAt = now\n\t}\n\tdescription.UpdatedAt = now\n\n\tbytes, err := json.Marshal(description)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling convo message description: %v\", err)\n\t}\n\n\terr = os.WriteFile(filepath.Join(descriptionsDir, description.Id+\".json\"), bytes, os.ModePerm)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing convo message description: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc DeleteDraftPlans(orgId, projectId, userId string) error {\n\tres, err := Conn.Query(\"DELETE FROM plans WHERE project_id = $1 AND owner_id = $2 AND name = 'draft' RETURNING id;\", projectId, userId)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting draft plans: %v\", err)\n\t}\n\n\tdefer res.Close()\n\n\t// get ids\n\tvar ids []string\n\n\tfor res.Next() {\n\t\tvar id string\n\t\terr := res.Scan(&id)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error scanning deleted draft plan id: %v\", err)\n\t\t}\n\t\tids = append(ids, id)\n\t}\n\n\terrCh := make(chan error, len(ids))\n\tfor _, planId := range ids {\n\t\tgo func(planId string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in DeleteDraftPlans: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in DeleteDraftPlans: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\terrCh <- DeletePlanDir(orgId, planId)\n\t\t}(planId)\n\t}\n\n\tfor i := 0; i < len(ids); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting draft plan dir: %v\", err)\n\t\t}\n\t}\n\n\tif len(ids) > 0 {\n\t\tlog.Println(\"Deleted\", len(ids), \"draft plans\")\n\t}\n\n\treturn nil\n}\n\nfunc DeleteOwnerPlans(orgId, projectId, userId string) error {\n\tres, err := Conn.Query(\"DELETE FROM plans WHERE project_id = $1 AND owner_id = $2 RETURNING id;\", projectId, userId)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting plans: %v\", err)\n\t}\n\n\tdefer res.Close()\n\n\t// get ids\n\tvar ids []string\n\n\tfor res.Next() {\n\t\tvar id string\n\t\terr := res.Scan(&id)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error scanning deleted draft plan id: %v\", err)\n\t\t}\n\t\tids = append(ids, id)\n\t}\n\n\terrCh := make(chan error, len(ids))\n\tfor _, planId := range ids {\n\t\tgo func(planId string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in DeleteOwnerPlans: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in DeleteOwnerPlans: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\terrCh <- DeletePlanDir(orgId, planId)\n\t\t}(planId)\n\t}\n\n\tfor i := 0; i < len(ids); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting plan dir: %v\", err)\n\t\t}\n\t}\n\n\tif len(ids) > 0 {\n\t\tlog.Println(\"Deleted\", len(ids), \"plans\")\n\t}\n\n\treturn nil\n}\n\nfunc ValidatePlanAccess(planId, userId, orgId string) (*Plan, error) {\n\t// get plan\n\tplan, err := GetPlan(planId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan: %v\", err)\n\t}\n\n\tif plan == nil {\n\t\treturn nil, nil\n\t}\n\n\tif plan.OrgId != orgId {\n\t\treturn nil, nil\n\t}\n\n\thasProjectAccess, err := ProjectExists(orgId, plan.ProjectId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error validating project membership: %v\", err)\n\t}\n\n\tif !hasProjectAccess {\n\t\treturn nil, nil\n\t}\n\n\t// owner has access\n\tif plan.OwnerId == userId {\n\t\treturn plan, nil\n\t}\n\n\t// plan is shared with org\n\tif plan.SharedWithOrgAt != nil {\n\t\treturn plan, nil\n\t}\n\n\treturn nil, nil\n}\n\nfunc BumpPlanUpdatedAt(planId string, t time.Time) error {\n\t_, err := Conn.Exec(\"UPDATE plans SET updated_at = $1 WHERE id = $2\", t, planId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error updating plan updated at: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetPlanIdsForProject(projectId string) ([]string, error) {\n\tvar ids []string\n\terr := Conn.Select(&ids, \"SELECT id FROM plans WHERE project_id = $1\", projectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan ids for project: %v\", err)\n\t}\n\treturn ids, nil\n}\n"
  },
  {
    "path": "app/server/db/project_helpers.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc ProjectExists(orgId, projectId string) (bool, error) {\n\tvar count int\n\terr := Conn.QueryRow(\"SELECT COUNT(*) FROM projects WHERE org_id = $1 AND id = $2\", orgId, projectId).Scan(&count)\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"error checking if project exists: %v\", err)\n\t}\n\n\treturn count > 0, nil\n}\n\nfunc CreateProject(orgId, name string, tx *sqlx.Tx) (string, error) {\n\tvar projectId string\n\terr := tx.QueryRow(\"INSERT INTO projects (org_id, name) VALUES ($1, $2) RETURNING id\", orgId, name).Scan(&projectId)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error creating project: %v\", err)\n\t}\n\n\treturn projectId, nil\n}\n"
  },
  {
    "path": "app/server/db/queue.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"runtime/debug\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n)\n\ntype repoOpFn func(repo *GitRepo) error\n\ntype repoOperation struct {\n\torgId          string\n\tuserId         string\n\tplanId         string\n\tbranch         string\n\tscope          LockScope\n\tplanBuildId    string\n\tid             string\n\treason         string\n\top             repoOpFn\n\tctx            context.Context\n\tcancelFn       context.CancelFunc\n\tdone           chan error\n\tclearRepoOnErr bool\n}\n\ntype repoQueue struct {\n\tops          []*repoOperation\n\tmu           sync.Mutex\n\tisProcessing bool\n}\n\ntype repoQueueMap map[string]*repoQueue\n\nvar queuesMu sync.Mutex\nvar repoQueues = make(repoQueueMap)\n\nfunc (m repoQueueMap) getQueue(planId string) *repoQueue {\n\tqueuesMu.Lock()\n\tdefer queuesMu.Unlock()\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Queue] Getting queue for plan %s\", planId)\n\t}\n\n\tq, ok := m[planId]\n\tif !ok {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Queue] Creating new queue for plan %s\", planId)\n\t\t}\n\t\tq = &repoQueue{}\n\t\tm[planId] = q\n\t}\n\treturn q\n}\n\nfunc (m repoQueueMap) add(op *repoOperation) int {\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Queue] Adding operation %s (%s) to queue for plan %s\", op.id, op.reason, op.planId)\n\t}\n\tq := m.getQueue(op.planId)\n\treturn q.add(op)\n}\n\n// Add enqueues an operation, and then kicks off processing if needed.\nfunc (q *repoQueue) add(op *repoOperation) int {\n\tvar numOps int\n\tq.mu.Lock()\n\tq.ops = append(q.ops, op)\n\tnumOps = len(q.ops)\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Queue] Operation %s (%s) enqueued, queue length now %d\", op.id, op.reason, numOps)\n\t}\n\n\t// If nobody else is processing, we'll start\n\tif !q.isProcessing {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Queue] Starting queue processing for operation %s (%s)\", op.id, op.reason)\n\t\t}\n\t\tq.isProcessing = true\n\t\tgo q.runQueue() // run in the background\n\t} else if locksVerboseLogging {\n\t\tlog.Printf(\"[Queue] Queue already processing, operation %s (%s) will wait\", op.id, op.reason)\n\t}\n\tq.mu.Unlock()\n\n\treturn numOps\n}\n\nfunc (q *repoQueue) nextBatch() []*repoOperation {\n\tq.mu.Lock()\n\tdefer q.mu.Unlock()\n\n\tif len(q.ops) == 0 {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Queue] No operations in queue\")\n\t\t}\n\t\treturn nil\n\t}\n\n\tfirstOp := q.ops[0]\n\tres := []*repoOperation{firstOp}\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Queue] Processing first operation %s (%s) with scope %s, branch %s\",\n\t\t\tfirstOp.id, firstOp.reason, firstOp.scope, firstOp.branch)\n\t}\n\n\tq.ops = q.ops[1:]\n\n\t// writes always go one at a time, blocking everything else, as do read locks on the root plan (no branch)\n\tif firstOp.scope == LockScopeWrite || firstOp.branch == \"\" {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Queue] Operation %s is write or root branch read, processing alone\", firstOp.id)\n\t\t}\n\t\treturn res\n\t}\n\n\t// reads go in parallel as long as they are on the same branch\n\tfor len(q.ops) > 0 {\n\t\top := q.ops[0]\n\t\tif op.scope == LockScopeRead && op.branch == firstOp.branch {\n\t\t\tif locksVerboseLogging {\n\t\t\t\tlog.Printf(\"[Queue] Batching compatible read operation %s (%s) with same branch %s\",\n\t\t\t\t\top.id, op.reason, op.branch)\n\t\t\t}\n\t\t\tres = append(res, op)\n\t\t\tq.ops = q.ops[1:]\n\t\t} else {\n\t\t\tif locksVerboseLogging {\n\t\t\t\tlog.Printf(\"[Queue] Operation %s (%s) with scope %s, branch %s not compatible with batch, stopping\",\n\t\t\t\t\top.id, op.reason, op.scope, op.branch)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Queue] Created batch of %d operations\", len(res))\n\t}\n\n\treturn res\n}\n\nfunc (q *repoQueue) runQueue() {\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Queue] Starting queue processing\")\n\t}\n\n\tfor {\n\t\t// get the next batch\n\t\tops := q.nextBatch()\n\t\tif len(ops) == 0 {\n\t\t\t// Nothing left in the queue, so mark not processing and return\n\t\t\tif locksVerboseLogging {\n\t\t\t\tlog.Printf(\"[Queue] Queue empty, stopping processing\")\n\t\t\t}\n\t\t\tq.mu.Lock()\n\t\t\tq.isProcessing = false\n\t\t\tq.mu.Unlock()\n\t\t\treturn\n\t\t}\n\n\t\tfirstOp := ops[0]\n\n\t\tfunc() {\n\n\t\t\tif locksVerboseLogging {\n\t\t\t\tlog.Printf(\"[Queue] Attempting to acquire DB lock for plan %s, branch %s, scope %s\",\n\t\t\t\t\tfirstOp.planId, firstOp.branch, firstOp.scope)\n\t\t\t}\n\n\t\t\tlockId, err := lockRepoDB(LockRepoParams{\n\t\t\t\tOrgId:       firstOp.orgId,\n\t\t\t\tUserId:      firstOp.userId,\n\t\t\t\tPlanId:      firstOp.planId,\n\t\t\t\tBranch:      firstOp.branch,\n\t\t\t\tScope:       firstOp.scope,\n\t\t\t\tPlanBuildId: firstOp.planBuildId,\n\t\t\t\tReason:      firstOp.reason,\n\t\t\t\tCtx:         firstOp.ctx,\n\t\t\t\tCancelFn:    firstOp.cancelFn,\n\t\t\t}, 0)\n\n\t\t\tif lockId != \"\" {\n\t\t\t\tlog.Printf(\"[Queue] Acquired DB lock %s\", lockId)\n\n\t\t\t\tdefer func() {\n\t\t\t\t\tlog.Printf(\"[Queue] Releasing DB lock %s for plan %s\", lockId, firstOp.planId)\n\t\t\t\t\treleaseErr := deleteRepoLockDB(lockId, firstOp.planId, firstOp.reason, 0)\n\t\t\t\t\tif releaseErr != nil {\n\t\t\t\t\t\tlog.Printf(\"[Queue] Failed to release DB lock: %v\", releaseErr)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Printf(\"[Queue] DB lock %s released successfully\", lockId)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"[Queue] Failed to get DB lock: %v\", err)\n\t\t\t\tfor _, op := range ops {\n\t\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t\tlog.Printf(\"[Queue] Notifying operation %s (%s) of lock failure\", op.id, op.reason)\n\t\t\t\t\t}\n\t\t\t\t\top.done <- fmt.Errorf(\"failed to get DB lock: %w\", err)\n\t\t\t\t}\n\t\t\t\t// we still need to process the rest of the queue\n\t\t\t\t// if the error is critical, caller will handle it\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif locksVerboseLogging {\n\t\t\t\tlog.Printf(\"[Queue] Acquired DB lock %s, processing batch of %d operations\", lockId, len(ops))\n\t\t\t}\n\n\t\t\trepo := getGitRepo(firstOp.orgId, firstOp.planId)\n\t\t\tvar needsRollback bool\n\n\t\t\t// Process the batch\n\t\t\t// If it's a writer => single op\n\t\t\t// If multiple same‐branch readers => do them in parallel\n\t\t\tvar wg sync.WaitGroup\n\t\t\tfor _, op := range ops {\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func(op *repoOperation) {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tselect {\n\t\t\t\t\tcase <-op.ctx.Done():\n\t\t\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t\t\tlog.Printf(\"[Queue] Operation %s (%s) context canceled\", op.id, op.reason)\n\t\t\t\t\t\t}\n\t\t\t\t\t\top.done <- op.ctx.Err()\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t\t\tlog.Printf(\"[Queue] Starting operation %s (%s)\", op.id, op.reason)\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// actually do the operation\n\n\t\t\t\t\t\tvar opErr error\n\n\t\t\t\t\t\tfunc() {\n\t\t\t\t\t\t\tdefer func() {\n\t\t\t\t\t\t\t\tpanicErr := recover()\n\t\t\t\t\t\t\t\tif panicErr != nil {\n\t\t\t\t\t\t\t\t\tlog.Printf(\"[Queue] Panic in operation %s (%s): %v\", op.id, op.reason, panicErr)\n\t\t\t\t\t\t\t\t\tlog.Printf(\"[Queue] Stack trace: %s\", string(debug.Stack()))\n\t\t\t\t\t\t\t\t\topErr = fmt.Errorf(\"panic in operation: %v\\n%s\", panicErr, string(debug.Stack()))\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif opErr != nil && op.scope == LockScopeWrite && op.clearRepoOnErr {\n\t\t\t\t\t\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t\t\t\t\t\tlog.Printf(\"[Queue] Operation %s (%s) failed with error, marking for rollback: %v\",\n\t\t\t\t\t\t\t\t\t\t\top.id, op.reason, opErr)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tneedsRollback = true\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}()\n\n\t\t\t\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t\t\t\tlog.Printf(\"[Queue] Executing operation %s (%s)\", op.id, op.reason)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\topErr = op.op(repo)\n\t\t\t\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t\t\t\tif opErr != nil {\n\t\t\t\t\t\t\t\t\tlog.Printf(\"[Queue] Operation %s (%s) failed with error: %v\", op.id, op.reason, opErr)\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlog.Printf(\"[Queue] Operation %s (%s) completed successfully\", op.id, op.reason)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}()\n\n\t\t\t\t\t\t// signal to the caller via op.done\n\t\t\t\t\t\tif locksVerboseLogging {\n\t\t\t\t\t\t\tlog.Printf(\"[Queue] Notifying caller of operation %s (%s) completion\", op.id, op.reason)\n\t\t\t\t\t\t}\n\t\t\t\t\t\top.done <- opErr\n\t\t\t\t\t}\n\t\t\t\t}(op)\n\t\t\t}\n\t\t\twg.Wait()\n\n\t\t\tif needsRollback {\n\t\t\t\tlog.Printf(\"[Queue] Performing rollback for plan %s branch %s\", firstOp.planId, firstOp.branch)\n\t\t\t\trollbackErr := repo.GitClearUncommittedChanges(firstOp.branch)\n\t\t\t\tif rollbackErr != nil {\n\t\t\t\t\tlog.Printf(\"[Queue] Failed to rollback: %v\", rollbackErr)\n\t\t\t\t} else if locksVerboseLogging {\n\t\t\t\t\tlog.Printf(\"[Queue] Rollback completed successfully\")\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n}\n\ntype ExecRepoOperationParams struct {\n\tOrgId          string\n\tUserId         string\n\tPlanId         string\n\tBranch         string\n\tScope          LockScope\n\tPlanBuildId    string\n\tReason         string\n\tCtx            context.Context\n\tCancelFn       context.CancelFunc\n\tClearRepoOnErr bool\n}\n\nfunc ExecRepoOperation(\n\tparams ExecRepoOperationParams,\n\top repoOpFn,\n) error {\n\tid := uuid.New().String()\n\n\tlog.Printf(\"[Queue] ExecRepoOperation called for plan %s, branch %s, scope %s, reason %s\",\n\t\tparams.PlanId, params.Branch, params.Scope, params.Reason)\n\n\tdone := make(chan error, 1)\n\tnumOps := repoQueues.add(&repoOperation{\n\t\tid:             id,\n\t\torgId:          params.OrgId,\n\t\tplanId:         params.PlanId,\n\t\tbranch:         params.Branch,\n\t\tscope:          params.Scope,\n\t\treason:         params.Reason,\n\t\tplanBuildId:    params.PlanBuildId,\n\t\top:             op,\n\t\tdone:           done,\n\t\tctx:            params.Ctx,\n\t\tcancelFn:       params.CancelFn,\n\t\tclearRepoOnErr: params.ClearRepoOnErr,\n\t})\n\n\tif numOps > 1 {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Queue] Operation %s (%s) queued behind %d operations\", id, params.Reason, numOps-1)\n\t\t\tfor i, op := range repoQueues.getQueue(params.PlanId).ops {\n\t\t\t\tlog.Printf(\"[Queue] Operation %d: %s - %s\\n\", i, op.id, op.reason)\n\t\t\t}\n\t\t}\n\t}\n\n\tselect {\n\tcase err := <-done:\n\t\tif locksVerboseLogging {\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"[Queue] Operation %s (%s) completed with error: %v\", id, params.Reason, err)\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"[Queue] Operation %s (%s) completed successfully\", id, params.Reason)\n\t\t\t}\n\t\t}\n\t\treturn err\n\tcase <-params.Ctx.Done():\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"[Queue] Operation %s (%s) context canceled while waiting\", id, params.Reason)\n\t\t}\n\t\treturn params.Ctx.Err()\n\t}\n}\n"
  },
  {
    "path": "app/server/db/rbac_helpers.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"log\"\n)\n\nvar orgOwnerRoleId string\nvar orgMemberRoleId string\n\nfunc GetOrgOwnerRoleId() (string, error) {\n\tif orgOwnerRoleId == \"\" {\n\t\terr := cacheOrgOwnerRoleId()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error getting org owner role id: %v\", err)\n\t\t}\n\t}\n\n\tif orgOwnerRoleId == \"\" {\n\t\treturn \"\", fmt.Errorf(\"org owner role id is empty\")\n\t}\n\n\treturn orgOwnerRoleId, nil\n}\n\nfunc GetOrgMemberRoleId() (string, error) {\n\tif orgMemberRoleId == \"\" {\n\t\terr := cacheOrgMemberRoleId()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error getting org member role id: %v\", err)\n\t\t}\n\t}\n\n\tif orgMemberRoleId == \"\" {\n\t\treturn \"\", fmt.Errorf(\"org member role id is empty\")\n\t}\n\n\treturn orgMemberRoleId, nil\n}\n\nfunc GetOrgOwners(orgId string) ([]*User, error) {\n\tvar users []*User\n\terr := 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)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting org owners: %v\", err)\n\t}\n\n\treturn users, nil\n}\n\nfunc CacheOrgRoleIds() error {\n\terr := cacheOrgOwnerRoleId()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting org owner role id: %v\", err)\n\n\t}\n\n\tif orgOwnerRoleId == \"\" {\n\t\tlog.Println(\"org owner role id is empty at startup\")\n\t}\n\n\terr = cacheOrgMemberRoleId()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting org member role id: %v\", err)\n\t}\n\n\tif orgMemberRoleId == \"\" {\n\t\tlog.Println(\"org member role id is empty at startup\")\n\t}\n\n\treturn nil\n}\n\nfunc cacheOrgOwnerRoleId() error {\n\tvar roleId string\n\terr := Conn.Get(&roleId, \"SELECT id FROM org_roles WHERE name = 'owner'\")\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting owner role id: %v\", err)\n\t}\n\n\torgOwnerRoleId = roleId\n\n\treturn nil\n}\n\nfunc cacheOrgMemberRoleId() error {\n\tvar roleId string\n\terr := Conn.Get(&roleId, \"SELECT id FROM org_roles WHERE name = 'member'\")\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting member role id: %v\", err)\n\t}\n\n\torgMemberRoleId = roleId\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/result_helpers.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/google/uuid\"\n)\n\nfunc StorePlanResult(result *PlanFileResult) error {\n\tnow := time.Now()\n\tif result.Id == \"\" {\n\t\tresult.Id = uuid.New().String()\n\t\tresult.CreatedAt = now\n\t}\n\tresult.UpdatedAt = now\n\n\tbytes, err := json.MarshalIndent(result, \"\", \"  \")\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling result: %v\", err)\n\t}\n\n\tresultsDir := getPlanResultsDir(result.OrgId, result.PlanId)\n\n\terr = os.MkdirAll(resultsDir, 0755)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating results dir: %v\", err)\n\t}\n\n\tlog.Printf(\"Storing plan result: %s - %s\", result.Path, result.Id)\n\n\terr = os.WriteFile(filepath.Join(resultsDir, result.Id+\".json\"), bytes, 0644)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing result file: %v\", err)\n\t}\n\n\treturn nil\n\n}\n\ntype CurrentPlanStateParams struct {\n\tOrgId                    string\n\tPlanId                   string\n\tPlanFileResults          []*PlanFileResult\n\tConvoMessageDescriptions []*ConvoMessageDescription\n\tContexts                 []*Context\n}\n\nfunc GetFullCurrentPlanStateParams(orgId, planId string) (CurrentPlanStateParams, error) {\n\terrCh := make(chan error, 3)\n\n\tvar results []*PlanFileResult\n\tvar convoMessageDescriptions []*ConvoMessageDescription\n\tvar contexts []*Context\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in GetFullCurrentPlanStateParams: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in GetFullCurrentPlanStateParams: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\n\t\tres, err := GetPlanFileResults(orgId, planId)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting plan file results: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tresults = res\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in GetFullCurrentPlanStateParams: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in GetFullCurrentPlanStateParams: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tres, err := GetConvoMessageDescriptions(orgId, planId)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting latest plan build description: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tconvoMessageDescriptions = res\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in GetFullCurrentPlanStateParams: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in GetFullCurrentPlanStateParams: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tres, err := GetPlanContexts(orgId, planId, true, false)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting contexts: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tcontexts = res\n\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 3; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn CurrentPlanStateParams{}, err\n\t\t}\n\t}\n\n\treturn CurrentPlanStateParams{\n\t\tOrgId:                    orgId,\n\t\tPlanId:                   planId,\n\t\tPlanFileResults:          results,\n\t\tConvoMessageDescriptions: convoMessageDescriptions,\n\t\tContexts:                 contexts,\n\t}, nil\n}\n\nfunc GetCurrentPlanState(params CurrentPlanStateParams) (*shared.CurrentPlanState, error) {\n\torgId := params.OrgId\n\tplanId := params.PlanId\n\n\tvar dbPlanFileResults []*PlanFileResult\n\tvar convoMessageDescriptions []*shared.ConvoMessageDescription\n\tcontextsByPath := map[string]*Context{}\n\tplanApplies := []*shared.PlanApply{}\n\terrCh := make(chan error, 4)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tif params.PlanFileResults == nil {\n\t\t\tres, err := GetPlanFileResults(orgId, planId)\n\t\t\tdbPlanFileResults = res\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan file results: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tdbPlanFileResults = params.PlanFileResults\n\t\t}\n\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tif params.ConvoMessageDescriptions == nil {\n\t\t\tres, err := GetConvoMessageDescriptions(orgId, planId)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting latest plan build description: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, desc := range res {\n\t\t\t\tconvoMessageDescriptions = append(convoMessageDescriptions, desc.ToApi())\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, desc := range params.ConvoMessageDescriptions {\n\t\t\t\tconvoMessageDescriptions = append(convoMessageDescriptions, desc.ToApi())\n\t\t\t}\n\t\t}\n\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tvar contexts []*Context\n\t\tif params.Contexts == nil {\n\t\t\tres, err := GetPlanContexts(orgId, planId, true, false)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting contexts: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcontexts = res\n\n\t\t\tlog.Println(\"Got contexts:\", len(contexts))\n\t\t} else {\n\t\t\tcontexts = params.Contexts\n\t\t}\n\n\t\tfor _, context := range contexts {\n\t\t\tif context.FilePath != \"\" {\n\t\t\t\tcontextsByPath[context.FilePath] = context\n\t\t\t}\n\t\t}\n\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in GetCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tres, err := GetPlanApplies(orgId, planId)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting plan applies: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfor _, apply := range res {\n\t\t\tplanApplies = append(planApplies, apply.ToApi())\n\t\t}\n\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 4; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tvar apiPlanFileResults []*shared.PlanFileResult\n\tpendingResultPaths := map[string]bool{}\n\n\tfor _, dbPlanFileResult := range dbPlanFileResults {\n\t\t// log.Printf(\"Plan file result: %s\", dbPlanFileResult.Id)\n\n\t\tapiResult := dbPlanFileResult.ToApi()\n\t\tapiPlanFileResults = append(apiPlanFileResults, apiResult)\n\n\t\tif apiResult.IsPending() {\n\t\t\t// log.Printf(\"Pending result: %s\", dbPlanFileResult.Id)\n\n\t\t\tpendingResultPaths[apiResult.Path] = true\n\t\t} else {\n\t\t\t// log.Printf(\"Not pending result: %s\", apiResult.Id)\n\t\t\t// log.Printf(\"Applied at: %v\", apiResult.AppliedAt)\n\t\t\t// log.Printf(\"Rejected at: %v\", apiResult.RejectedAt)\n\t\t\t// log.Printf(\"Content: %v\", apiResult.Content != \"\")\n\t\t\t// log.Printf(\"Num Replacement: %d\", len(apiResult.Replacements))\n\t\t\t// log.Printf(\"Num Pending Replacements: %v\", apiResult.NumPendingReplacements())\n\t\t}\n\t}\n\tplanResult := GetPlanResult(apiPlanFileResults)\n\n\tpendingContextsByPath := map[string]*shared.Context{}\n\tfor path, context := range contextsByPath {\n\t\tpendingContextsByPath[path] = context.ToApi()\n\t}\n\n\t// log.Println(\"Pending contexts by path:\", len(pendingContextsByPath))\n\n\tplanState := &shared.CurrentPlanState{\n\t\tPlanResult:               planResult,\n\t\tConvoMessageDescriptions: convoMessageDescriptions,\n\t\tContextsByPath:           pendingContextsByPath,\n\t\tPlanApplies:              planApplies,\n\t}\n\n\tcurrentPlanFiles, err := planState.GetFiles()\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting current plan files: %v\", err)\n\t}\n\n\tplanState.CurrentPlanFiles = currentPlanFiles\n\n\treturn planState, nil\n}\n\nfunc GetConvoMessageDescriptions(orgId, planId string) ([]*ConvoMessageDescription, error) {\n\tvar descriptions []*ConvoMessageDescription\n\tdescriptionsDir := getPlanDescriptionsDir(orgId, planId)\n\tfiles, err := os.ReadDir(descriptionsDir)\n\n\tif err != nil {\n\n\t\tif os.IsNotExist(err) {\n\t\t\treturn descriptions, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading descriptions dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\tdescCh := make(chan *ConvoMessageDescription, len(files))\n\n\tfor _, file := range files {\n\t\tgo func(file os.DirEntry) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetConvoMessageDescriptions: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetConvoMessageDescriptions: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tpath := filepath.Join(descriptionsDir, file.Name())\n\n\t\t\tbytes, err := os.ReadFile(path)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading description file %s: %v\", file.Name(), err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar description ConvoMessageDescription\n\t\t\terr = json.Unmarshal(bytes, &description)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Error unmarshalling description file:\", path)\n\t\t\t\tlog.Println(\"bytes:\")\n\t\t\t\tlog.Println(string(bytes))\n\n\t\t\t\terrCh <- fmt.Errorf(\"error unmarshalling description file %s: %v\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tdescCh <- &description\n\t\t}(file)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\treturn nil, fmt.Errorf(\"error reading description files: %v\", err)\n\t\tcase description := <-descCh:\n\t\t\tif description.WroteFiles && description.AppliedAt == nil {\n\t\t\t\tdescriptions = append(descriptions, description)\n\t\t\t}\n\t\t}\n\t}\n\n\tsort.Slice(descriptions, func(i, j int) bool {\n\t\treturn descriptions[i].CreatedAt.Before(descriptions[j].CreatedAt)\n\t})\n\n\treturn descriptions, nil\n}\n\nfunc GetPlanFileResults(orgId, planId string) ([]*PlanFileResult, error) {\n\tvar results []*PlanFileResult\n\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\n\tfiles, err := os.ReadDir(resultsDir)\n\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn results, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading results dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\tresultCh := make(chan *PlanFileResult, len(files))\n\n\tfor _, file := range files {\n\t\t// log.Printf(\"Result file: %s\", file.Name())\n\n\t\tgo func(file os.DirEntry) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanFileResults: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanFileResults: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tbytes, err := os.ReadFile(filepath.Join(resultsDir, file.Name()))\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading result file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar result PlanFileResult\n\t\t\terr = json.Unmarshal(bytes, &result)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error unmarshalling result file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresultCh <- &result\n\t\t}(file)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\treturn nil, fmt.Errorf(\"error reading result files: %v\", err)\n\t\tcase result := <-resultCh:\n\t\t\tresults = append(results, result)\n\t\t}\n\t}\n\n\tsort.Slice(results, func(i, j int) bool {\n\t\treturn results[i].CreatedAt.Before(results[j].CreatedAt)\n\t})\n\n\treturn results, nil\n}\n\nfunc GetPlanFileResultById(orgId, planId, resultId string) (*PlanFileResult, error) {\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\n\tbytes, err := os.ReadFile(filepath.Join(resultsDir, resultId+\".json\"))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading result file: %v\", err)\n\t}\n\n\tvar result PlanFileResult\n\terr = json.Unmarshal(bytes, &result)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling result file: %v\", err)\n\t}\n\n\treturn &result, nil\n}\n\nfunc GetPlanResult(planFileResults []*shared.PlanFileResult) *shared.PlanResult {\n\tresByPath := make(shared.PlanFileResultsByPath)\n\treplacementsByPath := make(map[string][]*shared.Replacement)\n\tvar paths []string\n\n\tfor _, planFileRes := range planFileResults {\n\t\tif planFileRes.IsPending() {\n\t\t\t_, hasPath := resByPath[planFileRes.Path]\n\n\t\t\tresByPath[planFileRes.Path] = append(resByPath[planFileRes.Path], planFileRes)\n\n\t\t\tif !hasPath {\n\t\t\t\t// log.Printf(\"Adding res path: %s\", planFileRes.Path)\n\t\t\t\tpaths = append(paths, planFileRes.Path)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, results := range resByPath {\n\t\tfor _, planRes := range results {\n\t\t\treplacementsByPath[planRes.Path] = append(replacementsByPath[planRes.Path], planRes.Replacements...)\n\t\t}\n\t}\n\n\t// sort paths ascending\n\tsort.Slice(paths, func(i, j int) bool {\n\t\treturn paths[i] < paths[j]\n\t})\n\n\treturn &shared.PlanResult{\n\t\tFileResultsByPath:  resByPath,\n\t\tSortedPaths:        paths,\n\t\tReplacementsByPath: replacementsByPath,\n\t\tResults:            planFileResults,\n\t}\n}\n\ntype ApplyPlanParams struct {\n\tOrgId                  string\n\tUserId                 string\n\tBranchName             string\n\tPlan                   *Plan\n\tCurrentPlanState       *shared.CurrentPlanState\n\tCurrentPlanStateParams *CurrentPlanStateParams\n\tCommitMsg              string\n}\n\nfunc ApplyPlan(repo *GitRepo, ctx context.Context, params ApplyPlanParams) error {\n\torgId := params.OrgId\n\tuserId := params.UserId\n\tbranchName := params.BranchName\n\tplan := params.Plan\n\tcurrentPlanState := params.CurrentPlanState\n\tcurrentPlanParams := params.CurrentPlanStateParams\n\tplanId := plan.Id\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\n\tvar pendingDbResults []*PlanFileResult\n\n\tplanFileResults := currentPlanParams.PlanFileResults\n\tconvoMessageDescriptions := currentPlanParams.ConvoMessageDescriptions\n\tcontexts := currentPlanParams.Contexts\n\n\tcontextsByPath := make(map[string]*Context)\n\tfor _, context := range contexts {\n\t\tif context.FilePath != \"\" {\n\t\t\tcontextsByPath[context.FilePath] = context\n\t\t}\n\t}\n\n\tfor _, result := range planFileResults {\n\t\tapiResult := result.ToApi()\n\t\tif apiResult.IsPending() {\n\t\t\tpendingDbResults = append(pendingDbResults, result)\n\t\t}\n\t}\n\n\tlog.Printf(\"Pending db results: %d\", len(pendingDbResults))\n\n\tpendingNewFilesSet := make(map[string]bool)\n\tpendingUpdatedFilesSet := make(map[string]bool)\n\tfor _, result := range pendingDbResults {\n\t\tif result.Path == \"_apply.sh\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(result.Replacements) == 0 && result.Content != \"\" {\n\t\t\tpendingNewFilesSet[result.Path] = true\n\t\t} else if !pendingNewFilesSet[result.Path] {\n\t\t\tpendingUpdatedFilesSet[result.Path] = true\n\t\t}\n\t}\n\n\tvar loadContextRes *shared.LoadContextResponse\n\tvar updateContextRes *shared.UpdateContextResponse\n\n\tnumRoutines := len(pendingDbResults) +\n\t\tlen(convoMessageDescriptions)\n\n\tif len(pendingNewFilesSet) > 0 {\n\t\tnumRoutines++\n\t}\n\tif len(pendingUpdatedFilesSet) > 0 {\n\t\tnumRoutines++\n\t}\n\n\terrCh := make(chan error, numRoutines)\n\tnow := time.Now()\n\n\tfor _, result := range pendingDbResults {\n\t\tgo func(result *PlanFileResult) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tresult.AppliedAt = &now\n\n\t\t\tbytes, err := json.MarshalIndent(result, \"\", \"  \")\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error marshalling result: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terr = os.WriteFile(filepath.Join(resultsDir, result.Id+\".json\"), bytes, 0644)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error writing result file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- nil\n\n\t\t}(result)\n\t}\n\n\tfor _, description := range convoMessageDescriptions {\n\t\tgo func(description *ConvoMessageDescription) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tdescription.AppliedAt = &now\n\n\t\t\terr := StoreDescription(description)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error storing convo message description: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}(description)\n\t}\n\n\tif len(pendingNewFilesSet) > 0 {\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tloadReq := shared.LoadContextRequest{}\n\t\t\tfor path := range pendingNewFilesSet {\n\t\t\t\tloadReq = append(loadReq, &shared.LoadContextParams{\n\t\t\t\t\tContextType: shared.ContextFileType,\n\t\t\t\t\tName:        path,\n\t\t\t\t\tFilePath:    path,\n\t\t\t\t\tBody:        currentPlanState.CurrentPlanFiles.Files[path],\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif len(loadReq) > 0 {\n\t\t\t\tres, _, err := LoadContexts(\n\t\t\t\t\tctx,\n\t\t\t\t\tLoadContextsParams{\n\t\t\t\t\t\tOrgId:                    orgId,\n\t\t\t\t\t\tUserId:                   userId,\n\t\t\t\t\t\tPlan:                     plan,\n\t\t\t\t\t\tBranchName:               branchName,\n\t\t\t\t\t\tReq:                      &loadReq,\n\t\t\t\t\t\tSkipConflictInvalidation: true, // no need to invalidate conflicts when applying plan--and fixes race condition since invalidation check loads description\n\t\t\t\t\t\tAutoLoaded:               true,\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error loading context: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tloadContextRes = res\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\t}\n\n\tif len(pendingUpdatedFilesSet) > 0 {\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in ApplyPlan: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tupdateReq := shared.UpdateContextRequest{}\n\t\t\tfor path := range pendingUpdatedFilesSet {\n\t\t\t\tcontext := contextsByPath[path]\n\t\t\t\tupdateReq[context.Id] = &shared.UpdateContextParams{\n\t\t\t\t\tBody: currentPlanState.CurrentPlanFiles.Files[path],\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(updateReq) > 0 {\n\t\t\t\tres, err := UpdateContexts(\n\t\t\t\t\tUpdateContextsParams{\n\t\t\t\t\t\tOrgId:                    orgId,\n\t\t\t\t\t\tPlan:                     plan,\n\t\t\t\t\t\tBranchName:               branchName,\n\t\t\t\t\t\tReq:                      &updateReq,\n\t\t\t\t\t\tSkipConflictInvalidation: true, // no need to invalidate conflicts when applying plan--and fixes race condition since invalidation check loads description\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error updating context: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tupdateContextRes = res\n\t\t\t}\n\t\t\terrCh <- nil\n\n\t\t}()\n\n\t}\n\n\tfor i := 0; i < numRoutines; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error applying plan: %v\", err)\n\t\t}\n\t}\n\n\t// Store the PlanApply record\n\tplanApply := &PlanApply{\n\t\tId:        uuid.New().String(),\n\t\tOrgId:     orgId,\n\t\tPlanId:    planId,\n\t\tUserId:    userId,\n\t\tCommitMsg: params.CommitMsg,\n\t\tCreatedAt: now,\n\t}\n\n\t// Collect the IDs from the pending results and descriptions\n\tvar resultIds []string\n\tvar descriptionIds []string\n\tvar messageIds []string\n\n\tfor _, result := range pendingDbResults {\n\t\tresultIds = append(resultIds, result.Id)\n\t}\n\tfor _, desc := range convoMessageDescriptions {\n\t\tdescriptionIds = append(descriptionIds, desc.Id)\n\t\tmessageIds = append(messageIds, desc.ConvoMessageId)\n\t}\n\n\tplanApply.PlanFileResultIds = resultIds\n\tplanApply.ConvoMessageDescriptionIds = descriptionIds\n\tplanApply.ConvoMessageIds = messageIds\n\n\t// Store the PlanApply object\n\tbytes, err := json.MarshalIndent(planApply, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling plan apply: %v\", err)\n\t}\n\n\tappliesDir := getPlanAppliesDir(orgId, planId)\n\terr = os.MkdirAll(appliesDir, 0755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating applies dir: %v\", err)\n\t}\n\n\terr = os.WriteFile(filepath.Join(appliesDir, planApply.Id+\".json\"), bytes, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing plan apply file: %v\", err)\n\t}\n\n\tmsg := \"✅ Marked pending results as applied\"\n\n\tcurrentFiles := currentPlanState.CurrentPlanFiles.Files\n\tvar sortedFiles []string\n\tfor path := range currentFiles {\n\t\tsortedFiles = append(sortedFiles, path)\n\t}\n\tsort.Strings(sortedFiles)\n\tfor _, path := range sortedFiles {\n\t\tmsg += fmt.Sprintf(\"\\n • 📄 %s\", path)\n\t}\n\tmsg += \"\\n\" + \"✏️  \" + params.CommitMsg\n\n\tif loadContextRes != nil && !loadContextRes.MaxTokensExceeded {\n\t\tmsg += \"\\n\\n\" + loadContextRes.Msg\n\t}\n\n\tif updateContextRes != nil && !updateContextRes.MaxTokensExceeded {\n\t\tmsg += \"\\n\\n\" + updateContextRes.Msg\n\t}\n\n\terr = repo.GitAddAndCommit(branchName, msg)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing plan: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc RejectAllResults(orgId, planId string) error {\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\n\tfiles, err := os.ReadDir(resultsDir)\n\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"error reading results dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\tnow := time.Now()\n\n\tfor _, file := range files {\n\t\tresultId := strings.TrimSuffix(file.Name(), \".json\")\n\n\t\tgo func(resultId string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in RejectAllResults: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in RejectAllResults: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\terr := RejectPlanFile(orgId, planId, resultId, now)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error rejecting result: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}(resultId)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error rejecting plan: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc DeletePendingResultsForPaths(orgId, planId string, paths map[string]bool) error {\n\t// log.Println(\"Deleting pending results for paths\")\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\tfiles, err := os.ReadDir(resultsDir)\n\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"error reading results dir: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(files))\n\n\tfor _, file := range files {\n\t\tresultId := strings.TrimSuffix(file.Name(), \".json\")\n\n\t\tgo func(resultId string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in DeletePendingResultsForPaths: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in DeletePendingResultsForPaths: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tbytes, err := os.ReadFile(filepath.Join(resultsDir, resultId+\".json\"))\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading result file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar result PlanFileResult\n\t\t\terr = json.Unmarshal(bytes, &result)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error unmarshalling result file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// log.Printf(\"Checking pending result: %s\", resultId)\n\n\t\t\tif result.ToApi().IsPending() && paths[result.Path] {\n\t\t\t\tlog.Printf(\"Deleting pending result: %s\", resultId)\n\n\t\t\t\terr = os.Remove(filepath.Join(resultsDir, resultId+\".json\"))\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error deleting result file: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}(resultId)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting pending results: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc RejectPlanFiles(orgId, planId string, files []string, now time.Time) error {\n\terrCh := make(chan error, len(files))\n\n\tfor _, file := range files {\n\t\tgo func(file string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in RejectPlanFiles: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in RejectPlanFiles: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\terr := RejectPlanFile(orgId, planId, file, now)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}(file)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error rejecting plan files: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc RejectPlanFile(orgId, planId, filePathOrResultId string, now time.Time) error {\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\tresults, err := GetPlanFileResults(orgId, planId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting plan file results: %v\", err)\n\t}\n\n\terrCh := make(chan error, len(results))\n\n\tfor _, result := range results {\n\t\tgo func(result *PlanFileResult) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in RejectPlanFile: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in RejectPlanFile: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tif (result.Path == filePathOrResultId || result.Id == filePathOrResultId) && result.AppliedAt == nil && result.RejectedAt == nil {\n\t\t\t\tresult.RejectedAt = &now\n\t\t\t} else {\n\t\t\t\terrCh <- nil\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbytes, err := json.MarshalIndent(result, \"\", \"  \")\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error marshalling result: %v\", err)\n\t\t\t}\n\n\t\t\terr = os.WriteFile(filepath.Join(resultsDir, result.Id+\".json\"), bytes, 0644)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error writing result file: %v\", err)\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}(result)\n\t}\n\n\tfor i := 0; i < len(results); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error rejecting plan: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc RejectReplacement(orgId, planId, resultId, replacementId string) error {\n\tresultsDir := getPlanResultsDir(orgId, planId)\n\n\tbytes, err := os.ReadFile(filepath.Join(resultsDir, resultId+\".json\"))\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading result file: %v\", err)\n\t}\n\n\tvar result PlanFileResult\n\terr = json.Unmarshal(bytes, &result)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error unmarshalling result file: %v\", err)\n\t}\n\n\tif result.RejectedAt != nil {\n\t\treturn nil\n\t}\n\n\tnow := time.Now()\n\n\tfoundReplacement := false\n\tfor _, replacement := range result.Replacements {\n\t\tif replacement.Id == replacementId {\n\t\t\treplacement.RejectedAt = &now\n\t\t\tfoundReplacement = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !foundReplacement {\n\t\treturn fmt.Errorf(\"replacement not found: %s\", replacementId)\n\t}\n\n\treturn nil\n}\n\nfunc GetPlanApplies(orgId, planId string) ([]*PlanApply, error) {\n\tappliesDir := getPlanAppliesDir(orgId, planId)\n\tfiles, err := os.ReadDir(appliesDir)\n\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading applies dir: %v\", err)\n\t}\n\n\tplanApplies := []*PlanApply{}\n\tvar mu sync.Mutex\n\n\terrCh := make(chan error, len(files))\n\n\tfor _, file := range files {\n\t\tgo func(file os.DirEntry) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in GetPlanApplies: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GetPlanApplies: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tbytes, err := os.ReadFile(filepath.Join(appliesDir, file.Name()))\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading apply file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar apply PlanApply\n\t\t\terr = json.Unmarshal(bytes, &apply)\n\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error unmarshalling apply file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tplanApplies = append(planApplies, &apply)\n\t\t\tmu.Unlock()\n\n\t\t\terrCh <- nil\n\t\t}(file)\n\t}\n\n\tfor i := 0; i < len(files); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting plan applies: %v\", err)\n\t\t}\n\t}\n\n\treturn planApplies, nil\n}\n"
  },
  {
    "path": "app/server/db/settings_helpers.go",
    "content": "package db\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc GetPlanSettings(plan *Plan) (settings *shared.PlanSettings, err error) {\n\tplanDir := getPlanDir(plan.OrgId, plan.Id)\n\tsettingsPath := filepath.Join(planDir, \"settings.json\")\n\n\tresult, err := GetApiCustomModels(plan.OrgId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting custom models: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tif settings != nil {\n\t\t\tsettings.Configure(result.CustomModelPacks, result.CustomModels, result.CustomProviders, os.Getenv(\"PLANDEX_CLOUD\") != \"\")\n\t\t}\n\t}()\n\n\tbytes, err := os.ReadFile(settingsPath)\n\n\tif os.IsNotExist(err) || len(bytes) == 0 {\n\t\tlog.Printf(\"GetPlanSettings - no settings file found for plan %s - checking org defaults\", plan.Id)\n\t\t// see if org has default settings\n\t\tdefaultSettings, err := GetOrgDefaultSettings(plan.OrgId)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting org default settings: %v\", err)\n\t\t}\n\n\t\tif defaultSettings != nil {\n\t\t\tlog.Printf(\"GetPlanSettings - found org default settings for plan %s\", plan.Id)\n\t\t\treturn defaultSettings, nil\n\t\t} else {\n\t\t\tlog.Printf(\"GetPlanSettings - no org default settings found for plan %s\", plan.Id)\n\t\t}\n\n\t\tlog.Println(\"GetPlanSettings - no default settings found, returning default settings object\")\n\t\t// if it doesn't exist, return default settings object\n\t\tsettings = &shared.PlanSettings{\n\t\t\tUpdatedAt:     plan.CreatedAt,\n\t\t\tModelPackName: shared.DefaultModelPack.Name,\n\t\t}\n\t\treturn settings, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading settings file: %v\", err)\n\t}\n\n\tlog.Printf(\"GetPlanSettings - settings found in file\")\n\n\terr = json.Unmarshal(bytes, &settings)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling settings: %v\", err)\n\t}\n\n\treturn settings, nil\n}\n\nfunc StorePlanSettings(plan *Plan, settings shared.PlanSettings) error {\n\tplanDir := getPlanDir(plan.OrgId, plan.Id)\n\tsettingsPath := filepath.Join(planDir, \"settings.json\")\n\n\tsettings.UpdatedAt = time.Now()\n\tsettings.CustomModelPacks = nil\n\tsettings.CustomModels = nil\n\tsettings.CustomModelsById = nil\n\tsettings.CustomProviders = nil\n\tsettings.UsesCustomProviderByModelId = nil\n\tsettings.IsCloud = false\n\tsettings.Configured = false\n\n\tbytes, err := json.Marshal(settings)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling settings: %v\", err)\n\t}\n\n\terr = os.WriteFile(settingsPath, bytes, 0644)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing settings file: %v\", err)\n\t}\n\n\terr = BumpPlanUpdatedAt(plan.Id, settings.UpdatedAt)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error bumping plan updated at: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc GetOrgDefaultSettings(orgId string) (settings *shared.PlanSettings, err error) {\n\tresult, err := GetApiCustomModels(orgId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting custom models: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tif settings != nil {\n\t\t\tsettings.Configure(result.CustomModelPacks, result.CustomModels, result.CustomProviders, os.Getenv(\"PLANDEX_CLOUD\") != \"\")\n\t\t}\n\t}()\n\n\tquery := \"SELECT * FROM default_plan_settings WHERE org_id = $1\"\n\n\tvar defaults DefaultPlanSettings\n\n\terr = Conn.Get(&defaults, query, orgId)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\tlog.Println(\"GetOrgDefaultSettings - no rows - returning default settings\")\n\t\t\t// if it doesn't exist, return default settings object\n\t\t\tsettings := &shared.PlanSettings{\n\t\t\t\tUpdatedAt:     time.Time{},\n\t\t\t\tModelPackName: shared.DefaultModelPack.Name,\n\t\t\t}\n\t\t\treturn settings, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"error getting default plan settings: %v\", err)\n\t}\n\n\treturn &defaults.PlanSettings, nil\n}\n\nfunc GetOrgDefaultSettingsForUpdate(orgId string, tx *sqlx.Tx) (settings *shared.PlanSettings, err error) {\n\tresult, err := GetApiCustomModels(orgId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting custom models: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tif settings != nil {\n\t\t\tsettings.Configure(result.CustomModelPacks, result.CustomModels, result.CustomProviders, os.Getenv(\"PLANDEX_CLOUD\") != \"\")\n\t\t}\n\t}()\n\n\tquery := \"SELECT * FROM default_plan_settings WHERE org_id = $1 FOR UPDATE\"\n\n\tvar defaults DefaultPlanSettings\n\n\terr = tx.Get(&defaults, query, orgId)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\t// if it doesn't exist, return default settings object\n\t\t\tsettings := &shared.PlanSettings{\n\t\t\t\tUpdatedAt:     time.Time{},\n\t\t\t\tModelPackName: shared.DefaultModelPack.Name,\n\t\t\t}\n\t\t\treturn settings, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"error getting default plan settings: %v\", err)\n\t}\n\n\treturn &defaults.PlanSettings, nil\n}\n\nfunc StoreOrgDefaultSettings(orgId string, settings *shared.PlanSettings, tx *sqlx.Tx) error {\n\tsettings.UpdatedAt = time.Now()\n\n\tquery := `INSERT INTO default_plan_settings (org_id, plan_settings) \n\tVALUES ($1, $2) \n\tON CONFLICT (org_id) DO UPDATE SET plan_settings = excluded.plan_settings\n\t`\n\n\t_, err := tx.Exec(query, orgId, settings)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error storing default plan settings: %v\", err)\n\t}\n\n\treturn nil\n}\n\ntype GetCustomModelsResult struct {\n\tCustomModels     []*shared.CustomModel\n\tCustomProviders  []*shared.CustomProvider\n\tCustomModelPacks []*shared.ModelPack\n}\n\nfunc GetApiCustomModels(orgId string) (result *GetCustomModelsResult, err error) {\n\tvar customModels []*CustomModel\n\tvar customProviders []*CustomProvider\n\tvar customModelPacks []*ModelPack\n\n\terrCh := make(chan error, 3)\n\n\tgo func() {\n\t\tres, err := ListModelPacks(orgId)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting custom model packs: %v\", err)\n\t\t}\n\t\tcustomModelPacks = res\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tres, err := ListCustomModels(orgId)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting custom models: %v\", err)\n\t\t}\n\t\tcustomModels = res\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tres, err := ListCustomProviders(orgId)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting custom providers: %v\", err)\n\t\t}\n\t\tcustomProviders = res\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 3; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tapiModelPacks := make([]*shared.ModelPack, len(customModelPacks))\n\tfor i, modelPack := range customModelPacks {\n\t\tapiModelPacks[i] = modelPack.ToApi()\n\t}\n\n\tapiCustomModels := make([]*shared.CustomModel, len(customModels))\n\tfor i, model := range customModels {\n\t\tapiCustomModels[i] = model.ToApi()\n\t}\n\n\tapiCustomProviders := make([]*shared.CustomProvider, len(customProviders))\n\tfor i, provider := range customProviders {\n\t\tapiCustomProviders[i] = provider.ToApi()\n\t}\n\n\tresult = &GetCustomModelsResult{\n\t\tCustomModels:     apiCustomModels,\n\t\tCustomProviders:  apiCustomProviders,\n\t\tCustomModelPacks: apiModelPacks,\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "app/server/db/stream_helpers.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/notify\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/lib/pq\"\n)\n\nconst modelStreamHeartbeatInterval = 1 * time.Second\nconst modelStreamHeartbeatTimeout = 5 * time.Second\n\nfunc StoreModelStream(stream *ModelStream, ctx context.Context, cancelFn context.CancelFunc) error {\n\tquery := `INSERT INTO model_streams (org_id, plan_id, internal_ip, branch) VALUES (:org_id, :plan_id, :internal_ip, :branch) RETURNING id, created_at`\n\n\trow, err := Conn.NamedQuery(query, stream)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error storing model stream: %v\", err)\n\t}\n\n\tdefer row.Close()\n\n\tif row.Next() {\n\t\tvar createdAt time.Time\n\t\tvar id string\n\t\tif err := row.Scan(&id, &createdAt); err != nil {\n\t\t\treturn fmt.Errorf(\"error storing model stream: %v\", err)\n\t\t}\n\n\t\tstream.Id = id\n\t\tstream.CreatedAt = createdAt\n\t}\n\n\t// Start a goroutine to keep the lock alive\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in StoreModelStream: %v\\n%s\", r, debug.Stack())\n\t\t\t\tcancelFn()\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic in StoreModelStream: %v\\n%s\", r, debug.Stack()))\n\t\t\t}\n\t\t}()\n\n\t\tnumErrors := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\terr := SetModelStreamFinished(stream.Id)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error setting model stream %s finished: %v\\n\", stream.Id, err)\n\t\t\t\t}\n\t\t\t\treturn\n\n\t\t\tdefault:\n\t\t\t\t_, err := Conn.Exec(\"UPDATE model_streams SET last_heartbeat_at = NOW() WHERE id = $1\", stream.Id)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error updating model stream last heartbeat: %v\\n\", err)\n\t\t\t\t\tnumErrors++\n\n\t\t\t\t\tif numErrors > 5 {\n\t\t\t\t\t\tlog.Printf(\"Too many errors updating model stream last heartbeat: %v\\n\", err)\n\t\t\t\t\t\tcancelFn()\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(modelStreamHeartbeatInterval)\n\t\t\t}\n\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc SetModelStreamFinished(id string) error {\n\tlog.Println(\"Setting model stream finished:\", id)\n\n\t_, err := Conn.Exec(\"UPDATE model_streams SET finished_at = NOW() WHERE id = $1\", id)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting model stream finished: %v\", err)\n\t}\n\n\tlog.Println(\"Set model stream finished successfully:\", id)\n\n\treturn nil\n}\n\nfunc GetActiveModelStream(planId, branch string) (*ModelStream, error) {\n\tvar stream ModelStream\n\terr := Conn.Get(&stream, \"SELECT * FROM model_streams WHERE plan_id = $1 AND branch = $2 AND finished_at IS NULL\", planId, branch)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting active model stream: %v\", err)\n\t}\n\n\tif time.Now().Add(-modelStreamHeartbeatTimeout).After(stream.LastHeartbeatAt) {\n\t\tlog.Printf(\"Model stream %s has not sent a heartbeat in %s\\n\", stream.Id, modelStreamHeartbeatTimeout)\n\n\t\terr := SetModelStreamFinished(stream.Id)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error setting model stream finished: %v\", err)\n\t\t}\n\n\t\terr = SetPlanStatus(planId, branch, shared.PlanStatusError, \"Model stream has not sent a heartbeat in 5 seconds\")\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error setting plan status to error: %v\", err)\n\t\t}\n\n\t\treturn nil, nil\n\t} else {\n\t\tlog.Printf(\"Model stream %s sent heartbeat %d seconds ago\\n\", stream.Id, int(time.Since(stream.LastHeartbeatAt).Seconds()))\n\t}\n\n\treturn &stream, nil\n}\n\nfunc GetActiveOrRecentModelStreams(planIds []string) ([]*ModelStream, error) {\n\tvar streams []*ModelStream\n\terr := 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))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting active or recent model streams: %v\", err)\n\t}\n\n\treturn streams, nil\n}\n\nfunc GetActiveModelStreams(planIds []string) ([]*ModelStream, error) {\n\tvar streams []*ModelStream\n\terr := Conn.Select(&streams, \"SELECT * FROM model_streams WHERE plan_id = ANY($1) AND finished_at IS NULL ORDER BY created_at\", pq.Array(planIds))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting active  model streams: %v\", err)\n\t}\n\n\treturn streams, nil\n}\n\n// func StoreModelStreamSubscription(subscription *ModelStreamSubscription) error {\n// \tquery := `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`\n\n// \trow, err := Conn.NamedQuery(query, subscription)\n\n// \tif err != nil {\n// \t\treturn fmt.Errorf(\"error storing model stream subscription: %v\", err)\n// \t}\n\n// \tdefer row.Close()\n\n// \tif row.Next() {\n// \t\tvar createdAt time.Time\n// \t\tvar id string\n// \t\tif err := row.Scan(&id, &createdAt); err != nil {\n// \t\t\treturn fmt.Errorf(\"error storing model stream subscription: %v\", err)\n// \t\t}\n\n// \t\tsubscription.Id = id\n// \t\tsubscription.CreatedAt = createdAt\n// \t}\n\n// \treturn nil\n// }\n\n// func SetModelStreamSubscriptionFinished(id string) error {\n// \t_, err := Conn.Exec(\"UPDATE model_stream_subscriptions SET finished_at = NOW() WHERE id = $1\", id)\n\n// \tif err != nil {\n// \t\treturn fmt.Errorf(\"error setting model stream subscription finished: %v\", err)\n// \t}\n\n// \treturn nil\n// }\n"
  },
  {
    "path": "app/server/db/subtask_helpers.go",
    "content": "package db\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc GetPlanSubtasks(orgId, planId string) ([]*Subtask, error) {\n\tplanDir := getPlanDir(orgId, planId)\n\tsubtasksPath := filepath.Join(planDir, \"subtasks.json\")\n\n\tbytes, err := os.ReadFile(subtasksPath)\n\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error reading subtasks: %v\", err)\n\t}\n\n\tvar subtasks []*Subtask\n\terr = json.Unmarshal(bytes, &subtasks)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling subtasks: %v\", err)\n\t}\n\n\treturn subtasks, nil\n}\n\nfunc StorePlanSubtasks(orgId, planId string, subtasks []*Subtask) error {\n\tplanDir := getPlanDir(orgId, planId)\n\n\tbytes, err := json.Marshal(subtasks)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling subtasks: %v\", err)\n\t}\n\n\terr = os.WriteFile(filepath.Join(planDir, \"subtasks.json\"), bytes, os.ModePerm)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing subtasks: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/summary_helpers.go",
    "content": "package db\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/lib/pq\"\n)\n\nfunc GetPlanSummaries(planId string, convoMessageIds []string) ([]*ConvoSummary, error) {\n\tvar summaries []*ConvoSummary\n\n\terr := 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))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan summaries: %v\", err)\n\t}\n\treturn summaries, nil\n}\n\nfunc StoreSummary(summary *ConvoSummary) error {\n\tquery := \"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\"\n\n\trow, err := Conn.NamedQuery(query, summary)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error storing summary: %v\", err)\n\t}\n\n\tdefer row.Close()\n\n\tif row.Next() {\n\t\tvar createdAt time.Time\n\t\tvar id string\n\t\tif err := row.Scan(&id, &createdAt); err != nil {\n\t\t\treturn fmt.Errorf(\"error storing summary: %v\", err)\n\t\t}\n\n\t\tsummary.Id = id\n\t\tsummary.CreatedAt = createdAt\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/transactions.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"runtime/debug\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc WithTx(ctx context.Context, reason string, fn func(tx *sqlx.Tx) error) error {\n\treturn withTx(ctx, nil, reason, fn)\n}\n\nfunc WithTxOpts(ctx context.Context, opts *sql.TxOptions, reason string, fn func(tx *sqlx.Tx) error) error {\n\treturn withTx(ctx, opts, reason, fn)\n}\n\nfunc withTx(ctx context.Context, opts *sql.TxOptions, reason string, fn func(tx *sqlx.Tx) error) error {\n\tlog.Printf(\"starting transaction: (%s)\", reason)\n\n\ttx, err := Conn.BeginTxx(ctx, opts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error starting transaction: %v\", err)\n\t}\n\n\tvar committed bool\n\n\t// Ensure that rollback is attempted in case of failure\n\tdefer func() {\n\t\tpanicErr := recover()\n\t\tif panicErr != nil {\n\t\t\tlog.Printf(\"panic in WithTx (%s): %v\\n%s\", reason, panicErr, debug.Stack())\n\t\t\tlog.Printf(\"stack trace (panic - %s):\\n%s\", reason, debug.Stack())\n\t\t}\n\n\t\tif committed {\n\t\t\treturn\n\t\t}\n\n\t\tif rbErr := tx.Rollback(); rbErr != nil {\n\t\t\tif rbErr == sql.ErrTxDone {\n\t\t\t\tlog.Printf(\"attempted to roll back transaction, but it was already committed: (%s)\", reason)\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"transaction rollback error: (%s) %v\\n\", reason, rbErr)\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Printf(\"transaction rolled back: (%s)\", reason)\n\t\t}\n\t}()\n\n\terr = fn(tx)\n\n\tif err != nil {\n\t\tlog.Printf(\"error in WithTx (%s): %v\", reason, err)\n\t\treturn err\n\t}\n\n\terr = tx.Commit()\n\tif err != nil {\n\t\tlog.Printf(\"error committing transaction: (%s) %v\", reason, err)\n\t\treturn fmt.Errorf(\"error committing transaction: %v\", err)\n\t}\n\n\tcommitted = true\n\n\tlog.Printf(\"committed transaction: (%s)\", reason)\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/db/user_helpers.go",
    "content": "package db\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\tshared \"plandex-shared\"\n\t\"strings\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/lib/pq\"\n)\n\nfunc GetUser(userId string) (*User, error) {\n\tvar user User\n\terr := Conn.Get(&user, \"SELECT * FROM users WHERE id = $1\", userId)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting user: %v\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc GetUserByEmail(email string) (*User, error) {\n\tvar user User\n\terr := Conn.Get(&user, \"SELECT * FROM users WHERE email = $1\", email)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting user: %v\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc GetUsersForDomain(domain string) ([]*User, error) {\n\tvar users []*User\n\terr := Conn.Select(&users, \"SELECT * FROM users WHERE domain = $1\", domain)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting users for domain: %v\", err)\n\t}\n\n\treturn users, nil\n}\n\nfunc GetOrgUser(userId, orgId string) (*OrgUser, error) {\n\tvar orgUser OrgUser\n\terr := Conn.Get(&orgUser, \"SELECT * FROM orgs_users WHERE user_id = $1 AND org_id = $2\", userId, orgId)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting org user: %v\", err)\n\t}\n\n\treturn &orgUser, nil\n}\n\nfunc GetOrgUserConfig(userId, orgId string) (*shared.OrgUserConfig, error) {\n\tvar orgUserConfig shared.OrgUserConfig\n\terr := Conn.Get(&orgUserConfig, \"SELECT config FROM orgs_users WHERE user_id = $1 AND org_id = $2\", userId, orgId)\n\n\tif err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"error getting org user config: %v\", err)\n\t}\n\n\treturn &orgUserConfig, nil\n}\n\nfunc UpdateOrgUserConfig(userId, orgId string, config *shared.OrgUserConfig) error {\n\t_, err := Conn.Exec(\"UPDATE orgs_users SET config = $1 WHERE user_id = $2 AND org_id = $3\", config, userId, orgId)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error updating org user config: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc ListOrgUsers(orgId string) ([]*OrgUser, error) {\n\tvar orgUsers []*OrgUser\n\terr := Conn.Select(&orgUsers, \"SELECT * FROM orgs_users WHERE org_id = $1\", orgId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing org users: %v\", err)\n\t}\n\n\treturn orgUsers, nil\n}\n\nfunc ListUsers(orgId string) ([]*User, error) {\n\tvar users []*User\n\n\torgUsers, err := ListOrgUsers(orgId)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing users: %v\", err)\n\t}\n\n\tuserIds := make([]string, len(orgUsers))\n\tfor i, ou := range orgUsers {\n\t\tuserIds[i] = ou.UserId\n\t}\n\n\terr = Conn.Select(&users, \"SELECT * FROM users WHERE id = ANY($1)\", pq.Array(userIds))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing users: %v\", err)\n\t}\n\n\treturn users, nil\n}\n\nfunc CreateUser(name, email string, tx *sqlx.Tx) (*User, error) {\n\temailSplit := strings.Split(email, \"@\")\n\tif len(emailSplit) != 2 {\n\t\treturn nil, fmt.Errorf(\"invalid email: %v\", email)\n\t}\n\tdomain := emailSplit[1]\n\n\tuser := User{\n\t\tName:   name,\n\t\tEmail:  email,\n\t\tDomain: domain,\n\t}\n\n\terr := tx.QueryRow(\"INSERT INTO users (name, email, domain) VALUES ($1, $2, $3) RETURNING id\", user.Name, user.Email, user.Domain).Scan(&user.Id)\n\n\tif err != nil {\n\t\tif IsNonUniqueErr(err) {\n\t\t\treturn nil, fmt.Errorf(\"user already exists for email: %v\", email)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"error creating user: %v\", err)\n\t}\n\n\treturn &user, nil\n}\n\nfunc NumUsersWithRole(orgId, roleId string) (int, error) {\n\tvar count int\n\terr := Conn.Get(&count, \"SELECT COUNT(*) FROM orgs_users WHERE org_id = $1 AND org_role_id = $2\", orgId, roleId)\n\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"error counting users with role: %v\", err)\n\t}\n\n\treturn count, nil\n}\n"
  },
  {
    "path": "app/server/db/utils.go",
    "content": "package db\n\nimport (\n\t\"github.com/lib/pq\"\n)\n\nfunc IsNonUniqueErr(err error) bool {\n\tif err, ok := err.(*pq.Error); ok {\n\t\tif err.Code == \"23505\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "app/server/diff/diff.go",
    "content": "package diff\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/google/uuid\"\n)\n\nfunc GetDiffs(original, updated string) (string, error) {\n\t// create temp directory\n\ttempDirPath, err := os.MkdirTemp(\"\", \"tmp-diffs-*\")\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error creating temp dir: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tgo os.RemoveAll(tempDirPath)\n\t}()\n\n\t// write the original file to the temp dir\n\terr = os.WriteFile(filepath.Join(tempDirPath, \"original\"), []byte(original), 0644)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error writing original file: %v\", err)\n\t}\n\n\t// write the updated file to the temp dir\n\terr = os.WriteFile(filepath.Join(tempDirPath, \"updated\"), []byte(updated), 0644)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error writing updated file: %v\", err)\n\t}\n\n\tcmd := exec.Command(\"git\", \"-C\", tempDirPath, \"diff\", \"--no-color\", \"--no-index\", \"original\", \"updated\")\n\n\tres, err := cmd.CombinedOutput()\n\n\tif err != nil {\n\t\texitError, ok := err.(*exec.ExitError)\n\t\tif ok && exitError.ExitCode() == 1 {\n\t\t\t// Exit status 1 means diffs were found, which is expected\n\t\t} else {\n\t\t\tlog.Printf(\"Error getting diffs: %v\\n\", err)\n\t\t\tlog.Printf(\"Diff output: %s\\n\", res)\n\t\t\treturn \"\", fmt.Errorf(\"error getting diffs: %v\", err)\n\t\t}\n\t}\n\n\treturn string(res), nil\n}\n\ntype change struct {\n\tOld    string\n\tNew    string\n\tLine   int\n\tLength int\n}\n\nfunc GetDiffReplacements(original, updated string) ([]*shared.Replacement, error) {\n\tdiff, err := GetDiffs(original, updated)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting git diffs: %v\", err)\n\t}\n\n\tvar changes []*change\n\tscanner := bufio.NewScanner(strings.NewReader(diff))\n\n\tvar currentHunk *change\n\tvar oldLines, newLines []string\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\t// Parse hunk header\n\t\tif strings.HasPrefix(line, \"@@\") {\n\t\t\t// If we have a previous hunk, process it\n\t\t\tif currentHunk != nil {\n\t\t\t\tchange := processHunk(oldLines, newLines, currentHunk.Line)\n\t\t\t\tif change != nil {\n\t\t\t\t\tchanges = append(changes, change)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Parse the new hunk header\n\t\t\tlineInfo := strings.Split(line, \" \")[1:] // Skip @@ part\n\t\t\toldInfo := strings.Split(lineInfo[0], \",\")\n\t\t\tstartLine, _ := strconv.Atoi(strings.TrimPrefix(oldInfo[0], \"-\"))\n\n\t\t\tcurrentHunk = &change{\n\t\t\t\tLine: startLine,\n\t\t\t}\n\t\t\toldLines = []string{}\n\t\t\tnewLines = []string{}\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentHunk == nil {\n\t\t\tcontinue // Skip until we find a hunk\n\t\t}\n\n\t\t// Process the lines within a hunk\n\t\tswitch {\n\t\tcase strings.HasPrefix(line, \"-\"):\n\t\t\toldLines = append(oldLines, strings.TrimPrefix(line, \"-\"))\n\t\tcase strings.HasPrefix(line, \"+\"):\n\t\t\tnewLines = append(newLines, strings.TrimPrefix(line, \"+\"))\n\t\tcase strings.HasPrefix(line, \" \"):\n\t\t\t// Context lines - add to both\n\t\t\tline = strings.TrimPrefix(line, \" \")\n\t\t\toldLines = append(oldLines, line)\n\t\t\tnewLines = append(newLines, line)\n\t\t}\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"error scanning diff: %v\", err)\n\t}\n\n\t// Process the last hunk if exists\n\tif currentHunk != nil {\n\t\tchange := processHunk(oldLines, newLines, currentHunk.Line)\n\t\tif change != nil {\n\t\t\tchanges = append(changes, change)\n\t\t}\n\t}\n\n\treplacements := make([]*shared.Replacement, len(changes))\n\tfor i, change := range changes {\n\t\treplacements[i] = &shared.Replacement{\n\t\t\tId:  uuid.New().String(),\n\t\t\tOld: change.Old,\n\t\t\tNew: change.New,\n\t\t}\n\t}\n\n\treturn replacements, nil\n}\n\nfunc processHunk(oldLines, newLines []string, startLine int) *change {\n\tif len(oldLines) == 0 && len(newLines) == 0 {\n\t\treturn nil\n\t}\n\n\treturn &change{\n\t\tOld:    strings.Join(oldLines, \"\\n\"),\n\t\tNew:    strings.Join(newLines, \"\\n\"),\n\t\tLine:   startLine,\n\t\tLength: len(oldLines),\n\t}\n}\n"
  },
  {
    "path": "app/server/email/email.go",
    "content": "package email\n\nimport (\n\t\"fmt\"\n\t\"net/smtp\"\n\t\"os\"\n\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/ses\"\n)\n\n// SendEmailViaSES sends an email using AWS SES\nfunc SendEmailViaSES(recipient, subject, htmlBody, textBody string) error {\n\tsess, err := session.NewSession()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating AWS session: %v\", err)\n\t}\n\n\t// Create an SES session.\n\tsvc := ses.New(sess)\n\n\t// Assemble the email.\n\tinput := &ses.SendEmailInput{\n\t\tDestination: &ses.Destination{\n\t\t\tToAddresses: []*string{\n\t\t\t\taws.String(recipient),\n\t\t\t},\n\t\t},\n\t\tMessage: &ses.Message{\n\t\t\tBody: &ses.Body{\n\t\t\t\tHtml: &ses.Content{\n\t\t\t\t\tCharset: aws.String(\"UTF-8\"),\n\t\t\t\t\tData:    aws.String(htmlBody),\n\t\t\t\t},\n\t\t\t\tText: &ses.Content{\n\t\t\t\t\tCharset: aws.String(\"UTF-8\"),\n\t\t\t\t\tData:    aws.String(textBody),\n\t\t\t\t},\n\t\t\t},\n\t\t\tSubject: &ses.Content{\n\t\t\t\tCharset: aws.String(\"UTF-8\"),\n\t\t\t\tData:    aws.String(subject),\n\t\t\t},\n\t\t},\n\t\tSource: aws.String(\"Plandex <support@plandex.ai>\"),\n\t}\n\n\t// Attempt to send the email.\n\t_, err = svc.SendEmail(input)\n\n\treturn err\n}\n\nfunc sendEmailViaSMTP(recipient, subject, htmlBody, textBody string) error {\n\tsmtpHost := os.Getenv(\"SMTP_HOST\")\n\tsmtpPort := os.Getenv(\"SMTP_PORT\")\n\tsmtpUser := os.Getenv(\"SMTP_USER\")\n\tsmtpPassword := os.Getenv(\"SMTP_PASSWORD\")\n\tsmtpFrom := os.Getenv(\"SMTP_FROM\")\n\n\tif smtpHost == \"\" || smtpPort == \"\" || smtpUser == \"\" || smtpPassword == \"\" {\n\t\treturn fmt.Errorf(\"SMTP settings not found in environment variables\")\n\t}\n\n\tif smtpFrom == \"\" {\n\t\tsmtpFrom = smtpUser\n\t}\n\n\tsmtpAddress := fmt.Sprintf(\"%s:%s\", smtpHost, smtpPort)\n\n\tauth := smtp.PlainAuth(\"\", smtpUser, smtpPassword, smtpHost)\n\n\t// Generate a MIME boundary\n\tboundary := \"BOUNDARY1234567890\"\n\theader := 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)\n\n\t// Prepare the text body part\n\ttextPart := fmt.Sprintf(\"--%s\\r\\nContent-Type: text/plain; charset=\\\"UTF-8\\\"\\r\\n\\r\\n%s\\r\\n\", boundary, textBody)\n\n\t// Prepare the HTML body part\n\thtmlPart := fmt.Sprintf(\"--%s\\r\\nContent-Type: text/html; charset=\\\"UTF-8\\\"\\r\\n\\r\\n%s\\r\\n\", boundary, htmlBody)\n\n\t// End marker for the boundary\n\tendBoundary := fmt.Sprintf(\"--%s--\", boundary)\n\n\t// Combine the parts to form the full email message\n\tmessage := []byte(header + textPart + htmlPart + endBoundary)\n\n\terr := smtp.SendMail(smtpAddress, auth, smtpFrom, []string{recipient}, message)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error sending email via SMTP: %v\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/email/invite.go",
    "content": "package email\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gen2brain/beeep\"\n)\n\nfunc SendInviteEmail(email, inviteeFirstName, inviterName, orgName string) error {\n\t// Check if the environment is production\n\tif os.Getenv(\"GOENV\") == \"production\" {\n\t\t// Production environment - send email using AWS SES\n\t\tsubject := fmt.Sprintf(\"%s, you've been invited to join %s on Plandex\", inviteeFirstName, orgName)\n\n\t\thtmlBody := fmt.Sprintf(`<p>Hi %s,</p><p>%s has invited you to join the org <strong>%s</strong> on <a href=\"https://plandex.ai\">Plandex.</a></p><p>Plandex is a terminal-based AI programming engine for complex tasks.</p><p>To accept the invite, first <a href=\"https://docs.plandex.ai/install/\">install Plandex</a>, then open a terminal and run 'plandex sign-in'. Enter '%s' when asked for your email and follow the prompts from there.</p><p>If you have questions, feedback, or run into a problem, you can reply directly to this email, <a href=\"https://github.com/plandex-ai/plandex/discussions\">start a discussion</a>, or <a href=\"https://github.com/plandex-ai/plandex/issues\">open an issue.</a></p>`, inviteeFirstName, inviterName, orgName, email)\n\n\t\ttextBody := 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)\n\n\t\tif os.Getenv(\"IS_CLOUD\") == \"\" {\n\t\t\treturn sendEmailViaSMTP(email, subject, htmlBody, textBody)\n\t\t} else {\n\t\t\treturn SendEmailViaSES(email, subject, htmlBody, textBody)\n\t\t}\n\t} else {\n\t\t// Send notification\n\t\terr := beeep.Notify(\"Invite Sent\", fmt.Sprintf(\"Invite sent to %s (email not sent in development)\", email), \"\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error sending notification in dev: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/email/verification.go",
    "content": "package email\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/atotto/clipboard\"\n\t\"github.com/gen2brain/beeep\"\n)\n\nfunc SendVerificationEmail(email string, pin string) error {\n\t// Check if the environment is production\n\tif os.Getenv(\"GOENV\") == \"production\" {\n\t\t// Production environment - send email using AWS SES\n\t\tsubject := \"Your Plandex Pin\"\n\t\thtmlBody := fmt.Sprintf(`<p>Hi there,</p>\n<p>Welcome to Plandex! Your pin is:<br><br>\n<strong style=\"font-size: 18px;\">%s</strong></p>\n<p>It will be valid for the next 5 minutes.</p>\n<p style=\"color: #666; font-size: 12px; margin-top: 20px;\">If you didn't request this, you can safely ignore the email.</p>`, pin)\n\t\ttextBody := 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)\n\n\t\tif os.Getenv(\"IS_CLOUD\") == \"\" {\n\t\t\treturn sendEmailViaSMTP(email, subject, htmlBody, textBody)\n\t\t} else {\n\t\t\treturn SendEmailViaSES(email, subject, htmlBody, textBody)\n\t\t}\n\t}\n\n\tif os.Getenv(\"GOENV\") == \"development\" {\n\t\t// Development environment\n\t\tlog.Printf(\"Development mode: Verification pin is %s for email %s\", pin, email)\n\n\t\t// Copy pin to clipboard\n\t\tclipboard.WriteAll(pin) // ignore error\n\n\t\t// Send notification\n\t\tbeeep.Notify(\"Verification Pin\", fmt.Sprintf(\"Verification pin %s copied to clipboard %s\", pin, email), \"\") // ignore error\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/go.mod",
    "content": "module plandex-server\n\ngo 1.23.3\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/mux v1.8.1\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/sashabaranov/go-openai v1.40.0\n\tplandex-shared v0.0.0-00010101000000-000000000000\n)\n\nrequire (\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect\n\tgithub.com/godbus/dbus/v5 v5.1.0 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/jinzhu/copier v0.4.0 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect\n\tgithub.com/olekukonko/tablewriter v0.0.5 // indirect\n\tgithub.com/pkoukk/tiktoken-go v0.1.7 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/image v0.27.0 // indirect\n\tgolang.org/x/sys v0.33.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nrequire (\n\tgithub.com/atotto/clipboard v0.1.4\n\tgithub.com/aws/aws-sdk-go v1.55.7\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4\n\tgithub.com/golang-migrate/migrate/v4 v4.18.3\n\tgithub.com/jmoiron/sqlx v1.4.0\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82\n\tgithub.com/stretchr/testify v1.10.0\n\tgolang.org/x/mod v0.21.0\n\tgolang.org/x/net v0.40.0\n)\n\nreplace plandex-shared => ../shared\n"
  },
  {
    "path": "app/server/go.sum",
    "content": "filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=\ngithub.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=\ngithub.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM=\ngithub.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4=\ngithub.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=\ngithub.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 h1:ygs9POGDQpQGLJPlq4+0LBUmMBNox1N4JSpw+OETcvI=\ngithub.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=\ngithub.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=\ngithub.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=\ngithub.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=\ngithub.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=\ngithub.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=\ngithub.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/sashabaranov/go-openai v1.40.0 h1:Peg9Iag5mUJtPW00aYatlsn97YML0iNULiLNe74iPrU=\ngithub.com/sashabaranov/go-openai v1.40.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4=\ngithub.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=\ngithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=\ngo.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=\ngo.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=\ngo.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=\ngo.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=\ngo.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=\ngo.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngolang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=\ngolang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=\ngolang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=\ngolang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=\ngolang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=\ngolang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=\ngolang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "app/server/handlers/accounts.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/types\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc CreateAccountHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateAccountHandler\")\n\n\tif os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\tlog.Println(\"Creating accounts is not supported in cloud mode\")\n\t\thttp.Error(w, \"Creating accounts is not supported in cloud mode\", http.StatusNotImplemented)\n\t\treturn\n\t}\n\n\tisLocalMode := (os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\")\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar req shared.CreateAccountRequest\n\terr = json.Unmarshal(body, &req)\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling request: %v\\n\", err)\n\t\thttp.Error(w, \"Error unmarshalling request: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\treq.Email = strings.ToLower(req.Email)\n\n\tvar emailVerificationId string\n\n\t// skipping email verification in dev/local mode\n\tif !isLocalMode {\n\t\temailVerificationId, err = db.ValidateEmailVerification(req.Email, req.Pin)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error validating email verification: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error validating email verification: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar apiErr *shared.ApiError\n\tvar user *db.User\n\tvar userId string\n\tvar token string\n\tvar orgId string\n\n\terr = db.WithTx(r.Context(), \"create account\", func(tx *sqlx.Tx) error {\n\t\tres, err := db.CreateAccount(req.UserName, req.Email, emailVerificationId, tx)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error creating account: %v\", err)\n\t\t}\n\n\t\tuser = res.User\n\t\tuserId = user.Id\n\t\ttoken = res.Token\n\t\torgId = res.OrgId\n\n\t\t_, apiErr = hooks.ExecHook(hooks.CreateAccount, hooks.HookParams{\n\t\t\tAuth: &types.ServerAuth{\n\t\t\t\tUser:  user,\n\t\t\t\tOrgId: orgId,\n\t\t\t},\n\t\t})\n\n\t\treturn nil\n\t})\n\n\tif apiErr != nil {\n\t\twriteApiError(w, *apiErr)\n\t\treturn\n\t}\n\n\tif err != nil {\n\t\tlog.Printf(\"Error creating account: %v\\n\", err)\n\t\thttp.Error(w, \"Error creating account: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// get orgs\n\torgs, err := db.GetAccessibleOrgsForUser(user)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting orgs for user: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting orgs for user: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tapiOrgs, apiErr := toApiOrgs(orgs)\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"Error converting orgs to API orgs: %v\\n\", apiErr)\n\t\twriteApiError(w, *apiErr)\n\t\treturn\n\t}\n\n\tresp := shared.SessionResponse{\n\t\tUserId:      userId,\n\t\tToken:       token,\n\t\tEmail:       req.Email,\n\t\tUserName:    req.UserName,\n\t\tOrgs:        apiOrgs,\n\t\tIsLocalMode: os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\",\n\t}\n\n\tbytes, err := json.Marshal(resp)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully created account\")\n\n\tw.Write(bytes)\n}\n"
  },
  {
    "path": "app/server/handlers/auth_helpers.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/types\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"golang.org/x/mod/semver\"\n)\n\nfunc Authenticate(w http.ResponseWriter, r *http.Request, requireOrg bool) *types.ServerAuth {\n\treturn execAuthenticate(w, r, requireOrg, true)\n}\n\nfunc AuthenticateOptional(w http.ResponseWriter, r *http.Request, requireOrg bool) *types.ServerAuth {\n\treturn execAuthenticate(w, r, requireOrg, false)\n}\n\nfunc GetAuthHeader(r *http.Request) (*shared.AuthHeader, error) {\n\tauthHeader := r.Header.Get(\"Authorization\")\n\n\t// check for a cookie as well for ui requests\n\tif authHeader == \"\" {\n\t\tlog.Println(\"no auth header - checking for cookie\")\n\n\t\t// Try to get auth token from a cookie as a fallback\n\t\tcookie, err := r.Cookie(\"authToken\")\n\t\tif err != nil {\n\t\t\tif err == http.ErrNoCookie {\n\t\t\t\tlog.Println(\"no auth cookie\")\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"error retrieving auth cookie: %v\", err)\n\t\t}\n\t\t// Use the token from the cookie as the fallback authorization header\n\t\tauthHeader = cookie.Value\n\t\tlog.Println(\"got auth header from cookie\")\n\t}\n\n\tif authHeader == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tif !strings.HasPrefix(authHeader, \"Bearer \") {\n\t\treturn nil, fmt.Errorf(\"invalid auth header\")\n\t}\n\n\t// strip off the \"Bearer \" prefix\n\tencoded := strings.TrimPrefix(authHeader, \"Bearer \")\n\n\t// decode the base64-encoded credentials\n\tbytes, err := base64.URLEncoding.DecodeString(encoded)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decoding auth token: %v\", err)\n\t}\n\n\t// parse the credentials\n\tvar parsed shared.AuthHeader\n\terr = json.Unmarshal(bytes, &parsed)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing auth token: %v\", err)\n\t}\n\n\treturn &parsed, nil\n}\n\nfunc ClearAuthCookieIfBrowser(w http.ResponseWriter, r *http.Request) error {\n\tacceptHeader := r.Header.Get(\"Accept\")\n\tif acceptHeader == \"\" {\n\t\t// no accept header, not a browser request\n\t\treturn nil\n\t}\n\n\t// Check for existing auth cookie\n\t_, err := r.Cookie(\"authToken\")\n\tif err == http.ErrNoCookie {\n\t\t// No auth cookie, nothing to clear\n\t\treturn nil\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error retrieving auth cookie: %v\", err)\n\t}\n\n\tvar domain string\n\tif os.Getenv(\"GOENV\") == \"production\" {\n\t\tdomain = os.Getenv(\"APP_SUBDOMAIN\") + \".plandex.ai\"\n\t}\n\n\t// Clear the authToken cookie\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     \"authToken\",\n\t\tPath:     \"/\",\n\t\tValue:    \"\",\n\t\tMaxAge:   -1,\n\t\tSecure:   os.Getenv(\"GOENV\") != \"development\",\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t\tDomain:   domain,\n\t})\n\n\tlog.Println(\"cleared auth cookie\")\n\n\treturn nil\n}\n\nfunc ClearAccountFromCookies(w http.ResponseWriter, r *http.Request, userId string) error {\n\t// Get stored accounts\n\tstoredAccounts, err := GetAccountsFromCookie(r)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting accounts from cookie: %v\", err)\n\t}\n\n\t// Remove the account with the given userId\n\tfor i, account := range storedAccounts {\n\t\tif account.UserId == userId {\n\t\t\tstoredAccounts = append(storedAccounts[:i], storedAccounts[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Marshal the updated accounts\n\tupdatedAccountsBytes, err := json.Marshal(storedAccounts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling updated accounts: %v\", err)\n\t}\n\n\t// Encode to base64\n\tencodedAccounts := base64.URLEncoding.EncodeToString(updatedAccountsBytes)\n\n\t// Set the updated accounts cookie\n\tvar domain string\n\tif os.Getenv(\"GOENV\") == \"production\" {\n\t\tdomain = os.Getenv(\"APP_SUBDOMAIN\") + \".plandex.ai\"\n\t}\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     \"accounts\",\n\t\tPath:     \"/\",\n\t\tValue:    encodedAccounts,\n\t\tSecure:   os.Getenv(\"GOENV\") != \"development\",\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t\tDomain:   domain,\n\t})\n\n\treturn nil\n}\n\nfunc SetAuthCookieIfBrowser(w http.ResponseWriter, r *http.Request, user *db.User, token, orgId string) error {\n\tlog.Println(\"setting auth cookie if browser\")\n\n\tacceptHeader := r.Header.Get(\"Accept\")\n\tif acceptHeader == \"\" {\n\t\t// no accept header, not a browser request\n\t\tlog.Println(\"not a browser request\")\n\t\treturn nil\n\t}\n\n\tlog.Println(\"is browser - setting auth cookie\")\n\n\tif token == \"\" {\n\t\tauthHeader, err := GetAuthHeader(r)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting auth header: %v\", err)\n\t\t}\n\t\ttoken = authHeader.Token\n\t}\n\n\tif token == \"\" {\n\t\treturn fmt.Errorf(\"no token\")\n\t}\n\n\t// set authToken cookie\n\tauthHeader := shared.AuthHeader{\n\t\tToken: token,\n\t\tOrgId: orgId,\n\t}\n\n\tbytes, err := json.Marshal(authHeader)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling auth header: %v\", err)\n\t}\n\n\t// base64 encode\n\ttoken = base64.URLEncoding.EncodeToString(bytes)\n\n\tvar domain string\n\tif os.Getenv(\"GOENV\") == \"production\" {\n\t\tdomain = os.Getenv(\"APP_SUBDOMAIN\") + \".plandex.ai\"\n\t}\n\n\tcookie := &http.Cookie{\n\t\tName:     \"authToken\",\n\t\tPath:     \"/\",\n\t\tValue:    \"Bearer \" + token,\n\t\tSecure:   os.Getenv(\"GOENV\") != \"development\",\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t\tDomain:   domain,\n\t\tExpires:  time.Now().Add(time.Hour * 24 * 90),\n\t}\n\n\tlog.Println(\"setting auth cookie\", cookie)\n\n\thttp.SetCookie(w, cookie)\n\n\tstoredAccounts, err := GetAccountsFromCookie(r)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting accounts from cookie: %v\", err)\n\t}\n\n\tfound := false\n\tfor _, account := range storedAccounts {\n\t\tif account.UserId == user.Id {\n\t\t\tfound = true\n\n\t\t\taccount.Token = token\n\t\t\taccount.Email = user.Email\n\t\t\taccount.UserName = user.Name\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\tstoredAccounts = append(storedAccounts, &shared.ClientAccount{\n\t\t\tEmail:    user.Email,\n\t\t\tUserName: user.Name,\n\t\t\tUserId:   user.Id,\n\t\t\tToken:    token,\n\t\t})\n\t}\n\n\tbytes, err = json.Marshal(storedAccounts)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling accounts: %v\", err)\n\t}\n\n\t// base64 encode\n\taccounts := base64.URLEncoding.EncodeToString(bytes)\n\n\thttp.SetCookie(w, &http.Cookie{\n\t\tName:     \"accounts\",\n\t\tPath:     \"/\",\n\t\tValue:    accounts,\n\t\tSecure:   os.Getenv(\"GOENV\") != \"development\",\n\t\tHttpOnly: true,\n\t\tSameSite: http.SameSiteLaxMode,\n\t\tDomain:   domain,\n\t\tExpires:  time.Now().Add(time.Hour * 24 * 90),\n\t})\n\n\treturn nil\n}\n\nfunc GetAccountsFromCookie(r *http.Request) ([]*shared.ClientAccount, error) {\n\taccountsCookie, err := r.Cookie(\"accounts\")\n\n\tif err == http.ErrNoCookie {\n\t\treturn []*shared.ClientAccount{}, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting accounts cookie: %v\", err)\n\t}\n\n\tbytes, err := base64.URLEncoding.DecodeString(accountsCookie.Value)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decoding accounts cookie: %v\", err)\n\t}\n\n\tvar accounts []*shared.ClientAccount\n\terr = json.Unmarshal(bytes, &accounts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling accounts cookie: %v\", err)\n\t}\n\n\treturn accounts, nil\n}\n\nfunc ValidateAndSignIn(w http.ResponseWriter, r *http.Request, req shared.SignInRequest) (*shared.SessionResponse, error) {\n\tvar user *db.User\n\tvar emailVerificationId string\n\tvar signInCodeId string\n\tvar signInCodeOrgId string\n\tvar err error\n\n\tisLocalMode := (os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\")\n\n\tif req.IsSignInCode {\n\t\tres, err := db.ValidateSignInCode(req.Pin)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error validating sign in code: %v\\n\", err)\n\t\t\treturn nil, fmt.Errorf(\"error validating sign in code: %v\", err)\n\t\t}\n\n\t\tuser, err = db.GetUser(res.UserId)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting user: %v\\n\", err)\n\t\t\treturn nil, fmt.Errorf(\"error getting user: %v\", err)\n\t\t}\n\n\t\tif user == nil {\n\t\t\tlog.Printf(\"User not found for id: %v\\n\", res.UserId)\n\t\t\treturn nil, fmt.Errorf(\"user not found\")\n\t\t}\n\n\t\tsignInCodeId = res.Id\n\t\tsignInCodeOrgId = res.OrgId\n\t} else {\n\t\treq.Email = strings.ToLower(req.Email)\n\t\tuser, err = db.GetUserByEmail(req.Email)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting user: %v\\n\", err)\n\t\t\treturn nil, fmt.Errorf(\"error getting user: %v\", err)\n\t\t}\n\n\t\tif user == nil {\n\t\t\tlog.Printf(\"User not found for email: %v\\n\", req.Email)\n\t\t\treturn nil, fmt.Errorf(\"not found\")\n\t\t}\n\n\t\t// only validate email in non-local mode\n\t\tif !isLocalMode {\n\t\t\temailVerificationId, err = db.ValidateEmailVerification(req.Email, req.Pin)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error validating email verification: %v\\n\", err)\n\t\t\t\treturn nil, fmt.Errorf(\"error validating email verification: %v\", err)\n\t\t\t}\n\n\t\t\tlog.Println(\"Email verification successful\")\n\t\t}\n\t}\n\n\tvar token string\n\tvar authTokenId string\n\n\terr = db.WithTx(r.Context(), \"validate and sign in\", func(tx *sqlx.Tx) error {\n\t\tvar err error\n\t\t// create auth token\n\t\ttoken, authTokenId, err = db.CreateAuthToken(user.Id, tx)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error creating auth token: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error creating auth token: %v\", err)\n\t\t}\n\n\t\tif req.IsSignInCode {\n\t\t\t// update sign in code with auth token id\n\t\t\t_, err = tx.Exec(\"UPDATE sign_in_codes SET auth_token_id = $1 WHERE id = $2\", authTokenId, signInCodeId)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error updating sign in code: %v\\n\", err)\n\t\t\t\treturn fmt.Errorf(\"error updating sign in code: %v\", err)\n\t\t\t}\n\t\t} else if !isLocalMode { // only update email verification in non-local mode\n\t\t\t// update email verification with user and auth token ids\n\t\t\t_, err = tx.Exec(\"UPDATE email_verifications SET user_id = $1, auth_token_id = $2 WHERE id = $3\", user.Id, authTokenId, emailVerificationId)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error updating email verification: %v\\n\", err)\n\t\t\t\treturn fmt.Errorf(\"error updating email verification: %v\", err)\n\t\t\t}\n\n\t\t\tlog.Println(\"Email verification updated\")\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error validating and signing in: %v\", err)\n\t}\n\n\t// get orgs\n\torgs, err := db.GetAccessibleOrgsForUser(user)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting orgs for user: %v\\n\", err)\n\t\treturn nil, fmt.Errorf(\"error getting orgs for user: %v\", err)\n\t}\n\n\tif req.IsSignInCode {\n\t\tfilteredOrgs := []*db.Org{}\n\t\tfor _, org := range orgs {\n\t\t\tif org.Id == signInCodeOrgId {\n\t\t\t\tfilteredOrgs = append(filteredOrgs, org)\n\t\t\t}\n\t\t}\n\t\torgs = filteredOrgs\n\t}\n\n\t// with a single org, set the orgId in the cookie\n\t// otherwise, the user will be prompted to select an org\n\tvar orgId string\n\tif len(orgs) == 1 {\n\t\torgId = orgs[0].Id\n\t}\n\n\tlog.Println(\"Setting auth cookie if browser\")\n\terr = SetAuthCookieIfBrowser(w, r, user, token, orgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error setting auth cookie: %v\\n\", err)\n\t\treturn nil, fmt.Errorf(\"error setting auth cookie: %v\", err)\n\t}\n\n\tapiOrgs, apiErr := toApiOrgs(orgs)\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"Error converting orgs to api orgs: %v\\n\", apiErr)\n\t\treturn nil, fmt.Errorf(\"error converting orgs to api orgs: %v\", apiErr)\n\t}\n\n\tresp := shared.SessionResponse{\n\t\tUserId:      user.Id,\n\t\tToken:       token,\n\t\tEmail:       user.Email,\n\t\tUserName:    user.Name,\n\t\tOrgs:        apiOrgs,\n\t\tIsLocalMode: os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\",\n\t}\n\n\treturn &resp, nil\n}\n\nfunc requireMinClientVersion(w http.ResponseWriter, r *http.Request, minVersion string) bool {\n\tmsg := fmt.Sprintf(\"Client version is too old for this endpoint. Please upgrade to version %s or later.\", minVersion)\n\n\tversion := r.Header.Get(\"X-Client-Version\")\n\tif version == \"\" {\n\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\treturn false\n\t}\n\n\tif version == \"development\" {\n\t\treturn true\n\t}\n\n\tif semver.Compare(version, minVersion) < 0 {\n\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc execAuthenticate(w http.ResponseWriter, r *http.Request, requireOrg bool, raiseErr bool) *types.ServerAuth {\n\tlog.Println(\"authenticating request\")\n\n\tparsed, err := GetAuthHeader(r)\n\n\tif err != nil {\n\t\tlog.Printf(\"error getting auth header: %v\\n\", err)\n\t\tif raiseErr {\n\t\t\thttp.Error(w, \"error getting auth header\", http.StatusInternalServerError)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif parsed == nil {\n\t\tlog.Println(\"no auth header\")\n\t\tif raiseErr {\n\t\t\thttp.Error(w, \"no auth header\", http.StatusUnauthorized)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// validate the token\n\tauthToken, err := db.ValidateAuthToken(parsed.Token)\n\n\tif err != nil {\n\t\tlog.Printf(\"error validating auth token: %v\\n\", err)\n\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeInvalidToken,\n\t\t\tStatus: http.StatusUnauthorized,\n\t\t\tMsg:    \"Invalid auth token\",\n\t\t})\n\t\treturn nil\n\t}\n\n\tuser, err := db.GetUser(authToken.UserId)\n\n\tif err != nil {\n\t\tlog.Printf(\"error getting user: %v\\n\", err)\n\t\tif raiseErr {\n\t\t\thttp.Error(w, \"error getting user\", http.StatusInternalServerError)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif !requireOrg {\n\t\treturn &types.ServerAuth{\n\t\t\tAuthToken: authToken,\n\t\t\tUser:      user,\n\t\t}\n\t}\n\n\tif parsed.OrgId == \"\" {\n\t\tlog.Println(\"no org id\")\n\t\tif raiseErr {\n\t\t\thttp.Error(w, \"no org id\", http.StatusUnauthorized)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// validate the org membership\n\tisMember, err := db.ValidateOrgMembership(authToken.UserId, parsed.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"error validating org membership: %v\\n\", err)\n\t\tif raiseErr {\n\t\t\thttp.Error(w, \"error validating org membership\", http.StatusInternalServerError)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif !isMember {\n\t\t// check if there's an invite for this user and accept it if so (adds the user to the org)\n\t\tinvite, err := db.GetActiveInviteByEmail(parsed.OrgId, user.Email)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"error getting invite for org user: %v\\n\", err)\n\t\t\tif raiseErr {\n\t\t\t\thttp.Error(w, \"error getting invite for org user\", http.StatusInternalServerError)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tif invite != nil {\n\t\t\tlog.Println(\"accepting invite\")\n\n\t\t\terr := db.AcceptInvite(r.Context(), invite, authToken.UserId)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"error accepting invite: %v\\n\", err)\n\t\t\t\tif raiseErr {\n\t\t\t\t\thttp.Error(w, \"error accepting invite\", http.StatusInternalServerError)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t} else {\n\t\t\tlog.Println(\"user is not a member of the org\")\n\t\t\tif raiseErr {\n\t\t\t\thttp.Error(w, \"not a member of org\", http.StatusUnauthorized)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// get user permissions\n\tpermissions, err := db.GetUserPermissions(authToken.UserId, parsed.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"error getting user permissions: %v\\n\", err)\n\t\tif raiseErr {\n\t\t\thttp.Error(w, \"error getting user permissions\", http.StatusInternalServerError)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// build the permissions map\n\tpermissionsMap := make(shared.Permissions)\n\tfor _, permission := range permissions {\n\t\tpermissionsMap[permission] = true\n\t}\n\n\tauth := &types.ServerAuth{\n\t\tAuthToken:   authToken,\n\t\tUser:        user,\n\t\tOrgId:       parsed.OrgId,\n\t\tPermissions: permissionsMap,\n\t}\n\n\t// don't send hash for org-session requests\n\tvar hash string\n\tif r.URL.Path != \"/orgs/session\" {\n\t\thash = parsed.Hash\n\t}\n\n\t_, apiErr := hooks.ExecHook(hooks.Authenticate, hooks.HookParams{\n\t\tAuth: auth,\n\t\tAuthenticateHookRequestParams: &hooks.AuthenticateHookRequestParams{\n\t\t\tPath: r.URL.Path,\n\t\t\tHash: hash,\n\t\t},\n\t})\n\n\tif apiErr != nil {\n\t\twriteApiError(w, *apiErr)\n\t\treturn nil\n\t}\n\n\tlog.Printf(\"UserId: %s, Email: %s, OrgId: %s\\n\", authToken.UserId, user.Email, parsed.OrgId)\n\n\treturn auth\n\n}\n\nfunc authorizeProject(w http.ResponseWriter, projectId string, auth *types.ServerAuth) bool {\n\treturn authorizeProjectOptional(w, projectId, auth, true)\n}\n\nfunc authorizeProjectOptional(w http.ResponseWriter, projectId string, auth *types.ServerAuth, shouldErr bool) bool {\n\tlog.Println(\"authorizing project\")\n\n\tprojectExists, err := db.ProjectExists(auth.OrgId, projectId)\n\n\tif err != nil {\n\t\tlog.Printf(\"error validating project: %v\\n\", err)\n\t\thttp.Error(w, \"error validating project\", http.StatusInternalServerError)\n\t\treturn false\n\t}\n\n\tif !projectExists && shouldErr {\n\t\tlog.Println(\"project does not exist in org\")\n\t\thttp.Error(w, \"project does not exist in org\", http.StatusNotFound)\n\t\treturn false\n\t}\n\n\treturn projectExists\n}\n\nfunc authorizeProjectRename(w http.ResponseWriter, projectId string, auth *types.ServerAuth) bool {\n\tif !authorizeProject(w, projectId, auth) {\n\t\treturn false\n\t}\n\n\tif !auth.HasPermission(shared.PermissionRenameAnyProject) {\n\t\tlog.Println(\"User does not have permission to rename project\")\n\t\thttp.Error(w, \"User does not have permission to rename project\", http.StatusForbidden)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc authorizeProjectDelete(w http.ResponseWriter, projectId string, auth *types.ServerAuth) bool {\n\tif !authorizeProject(w, projectId, auth) {\n\t\treturn false\n\t}\n\n\tif !auth.HasPermission(shared.PermissionDeleteAnyProject) {\n\t\tlog.Println(\"User does not have permission to delete project\")\n\t\thttp.Error(w, \"User does not have permission to delete project\", http.StatusForbidden)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc authorizePlan(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {\n\tlog.Println(\"authorizing plan\")\n\n\tplan, err := db.ValidatePlanAccess(planId, auth.User.Id, auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"error validating plan membership: %v\\n\", err)\n\t\thttp.Error(w, \"error validating plan membership\", http.StatusInternalServerError)\n\t\treturn nil\n\t}\n\n\tif plan == nil {\n\t\tlog.Println(\"user doesn't have access the plan\")\n\t\thttp.Error(w, \"no access to plan\", http.StatusUnauthorized)\n\t\treturn nil\n\t}\n\n\treturn plan\n}\n\nfunc authorizePlanUpdate(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn nil\n\t}\n\n\tif plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionUpdateAnyPlan) {\n\t\tlog.Println(\"User does not have permission to update plan\")\n\t\thttp.Error(w, \"User does not have permission to update plan\", http.StatusForbidden)\n\t\treturn nil\n\t}\n\n\treturn plan\n}\n\nfunc authorizePlanDelete(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn nil\n\t}\n\n\tif plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionDeleteAnyPlan) {\n\t\tlog.Println(\"User does not have permission to delete plan\")\n\t\thttp.Error(w, \"User does not have permission to delete plan\", http.StatusForbidden)\n\t\treturn nil\n\t}\n\n\treturn plan\n}\n\nfunc authorizePlanRename(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn nil\n\t}\n\n\tif plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionRenameAnyPlan) {\n\t\tlog.Println(\"User does not have permission to rename plan\")\n\t\thttp.Error(w, \"User does not have permission to rename plan\", http.StatusForbidden)\n\t\treturn nil\n\t}\n\n\treturn plan\n}\n\nfunc authorizePlanArchive(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn nil\n\t}\n\n\tif plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionArchiveAnyPlan) {\n\t\tlog.Println(\"User does not have permission to archive plan\")\n\t\thttp.Error(w, \"User does not have permission to archive plan\", http.StatusForbidden)\n\t\treturn nil\n\t}\n\n\treturn plan\n}\n"
  },
  {
    "path": "app/server/handlers/branches.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc ListBranchesHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListBranchesHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tvar err error\n\n\tctx, cancel := context.WithCancel(r.Context())\n\tvar branches []*db.Branch\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   \"main\",\n\t\tReason:   \"list branches\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tres, err := db.ListPlanBranches(repo, planId)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tbranches = res\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting branches: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting branches: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tjsonBytes, err := json.Marshal(branches)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling branches: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling branches: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully retrieved branches\")\n\n\tw.Write(jsonBytes)\n}\n\nfunc CreateBranchHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateBranchHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer func() {\n\t\tlog.Println(\"Closing request body\")\n\t\tr.Body.Close()\n\t}()\n\n\tvar req shared.CreateBranchRequest\n\tif err := json.Unmarshal(body, &req); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body \", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tparentBranch, err := db.GetDbBranch(planId, branch)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting parent branch: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting parent branch: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   \"main\",\n\t\tReason:   \"create branch\",\n\t\tScope:    db.LockScopeWrite,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\n\t\terr := db.WithTx(ctx, \"create branch\", func(tx *sqlx.Tx) error {\n\t\t\t_, err = db.CreateBranch(repo, plan, parentBranch, req.Name, tx)\n\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error creating branch: %v\", err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\t\treturn err\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error creating branch: %v\\n\", err)\n\t\thttp.Error(w, \"Error creating branch: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully created branch\")\n}\n\nfunc DeleteBranchHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for DeleteBranchHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tif branch == \"main\" {\n\t\tlog.Println(\"Cannot delete main branch\")\n\t\thttp.Error(w, \"Cannot delete main branch\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   \"main\",\n\t\tReason:   \"delete branch\",\n\t\tScope:    db.LockScopeWrite,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\terr := repo.GitDeleteBranch(branch)\n\t\treturn err\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error deleting branch: %v\\n\", err)\n\t\thttp.Error(w, \"Error deleting branch: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully deleted branch\")\n}\n"
  },
  {
    "path": "app/server/handlers/client_helper.go",
    "content": "package handlers\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/model\"\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n)\n\ntype initClientsParams struct {\n\tw    http.ResponseWriter\n\tauth *types.ServerAuth\n\n\tapiKeys     map[string]string // deprecated\n\topenAIOrgId string            // deprecated\n\n\tauthVars map[string]string\n\n\tplan          *db.Plan\n\tsettings      *shared.PlanSettings\n\torgUserConfig *shared.OrgUserConfig\n}\n\ntype initClientsResult struct {\n\tclients  map[string]model.ClientInfo\n\tauthVars map[string]string\n}\n\nfunc initClients(params initClientsParams) initClientsResult {\n\tw := params.w\n\tsettings := params.settings\n\torgUserConfig := params.orgUserConfig\n\n\tauthVars := map[string]string{}\n\tif params.authVars != nil {\n\t\tauthVars = params.authVars\n\t} else if params.apiKeys != nil {\n\t\tauthVars = map[string]string{}\n\t\tfor envVar, apiKey := range params.apiKeys {\n\t\t\tauthVars[envVar] = apiKey\n\t\t}\n\t\tif params.openAIOrgId != \"\" {\n\t\t\tauthVars[\"OPENAI_ORG_ID\"] = params.openAIOrgId\n\t\t}\n\t}\n\n\thookResult, apiErr := hooks.ExecHook(hooks.GetIntegratedModels, hooks.HookParams{\n\t\tAuth: params.auth,\n\t\tPlan: params.plan,\n\t})\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"Error getting integrated models: %v\\n\", apiErr)\n\t\thttp.Error(w, \"Error getting integrated models\", http.StatusInternalServerError)\n\t\treturn initClientsResult{}\n\t}\n\n\tif hookResult.GetIntegratedModelsResult != nil && hookResult.GetIntegratedModelsResult.IntegratedModelsMode {\n\t\tmerged := map[string]string{}\n\t\tfor k, v := range hookResult.GetIntegratedModelsResult.AuthVars {\n\t\t\tmerged[k] = v\n\t\t}\n\t\tif authVars[shared.AnthropicClaudeMaxTokenEnvVar] != \"\" {\n\t\t\tmerged[shared.AnthropicClaudeMaxTokenEnvVar] = authVars[shared.AnthropicClaudeMaxTokenEnvVar]\n\t\t}\n\t\tauthVars = merged\n\t}\n\tif len(authVars) == 0 && os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\tlog.Println(\"No api keys/credentials provided for models\")\n\t\thttp.Error(w, \"No api keys/credentials provided for models\", http.StatusBadRequest)\n\t\treturn initClientsResult{}\n\t}\n\n\tclients := model.InitClients(authVars, settings, orgUserConfig)\n\n\treturn initClientsResult{\n\t\tclients:  clients,\n\t\tauthVars: authVars,\n\t}\n}\n"
  },
  {
    "path": "app/server/handlers/context_helper.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model\"\n\t\"plandex-server/types\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\n\tshared \"plandex-shared\"\n)\n\ntype loadContextsParams struct {\n\tw                http.ResponseWriter\n\tr                *http.Request\n\tauth             *types.ServerAuth\n\tloadReq          *shared.LoadContextRequest\n\tplan             *db.Plan\n\tbranchName       string\n\tcachedMapsByPath map[string]*db.CachedMap\n\tautoLoaded       bool\n}\n\nfunc loadContexts(\n\tparams loadContextsParams,\n) (*shared.LoadContextResponse, []*db.Context) {\n\tw := params.w\n\tr := params.r\n\tauth := params.auth\n\tloadReq := params.loadReq\n\tplan := params.plan\n\tbranchName := params.branchName\n\tcachedMapsByPath := params.cachedMapsByPath\n\tautoLoaded := params.autoLoaded\n\n\tlog.Printf(\"[ContextHelper] Starting loadContexts with %d contexts, cachedMapsByPath: %v, autoLoaded: %v\", len(*loadReq), cachedMapsByPath != nil, autoLoaded)\n\n\t// check file count and size limits\n\t// this is all a sanity check - we should have already checked these limits in the client\n\ttotalFiles := 0\n\tmapFilesCount := 0\n\tfor _, context := range *loadReq {\n\t\ttotalFiles++\n\t\tif context.ContextType == shared.ContextMapType {\n\t\t\tmapFilesCount++\n\t\t\tlog.Printf(\"[ContextHelper] Found map file: %s with %d map bodies\", context.FilePath, len(context.MapBodies))\n\n\t\t\tif len(context.MapBodies) > shared.MaxContextMapPaths {\n\t\t\t\tlog.Printf(\"Error: Too many map files to load (found %d, limit is %d)\\n\", len(context.MapBodies), shared.MaxContextMapPaths)\n\t\t\t\thttp.Error(w, fmt.Sprintf(\"Too many map files to load (found %d, limit is %d)\", len(context.MapBodies), shared.MaxContextMapPaths), http.StatusBadRequest)\n\t\t\t\treturn nil, nil\n\t\t\t}\n\n\t\t\t// these are already mapped, so they shouldn't be anywhere close to the input limit, but we'll use it for the sanity check\n\t\t\tfor _, body := range context.MapBodies {\n\t\t\t\tif len(body) > shared.MaxContextMapSingleInputSize {\n\t\t\t\t\tlog.Printf(\"Error: Map file %s exceeds size limit (size %d, limit %d)\\n\", context.FilePath, len(body), shared.MaxContextMapSingleInputSize)\n\t\t\t\t\thttp.Error(w, fmt.Sprintf(\"Map file %s exceeds size limit (size %d, limit %d)\", context.FilePath, len(body), shared.MaxContextMapSingleInputSize), http.StatusBadRequest)\n\t\t\t\t\treturn nil, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif totalFiles > shared.MaxContextCount {\n\t\t\tlog.Printf(\"Error: Too many contexts to load (found %d, limit is %d)\\n\", totalFiles, shared.MaxContextCount)\n\t\t\thttp.Error(w, fmt.Sprintf(\"Too many contexts to load (found %d, limit is %d)\", totalFiles, shared.MaxContextCount), http.StatusBadRequest)\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tfileSize := int64(len(context.Body))\n\t\tif fileSize > shared.MaxContextBodySize {\n\t\t\tlog.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)\n\t\t\thttp.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)\n\t\t\treturn nil, nil\n\t\t}\n\n\t}\n\n\tif mapFilesCount > 0 {\n\t\tlog.Printf(\"[ContextHelper] Processing %d map files out of %d total contexts\", mapFilesCount, totalFiles)\n\t}\n\n\tvar err error\n\n\tvar settings *shared.PlanSettings\n\tvar clients map[string]model.ClientInfo\n\tvar authVars map[string]string\n\tvar orgUserConfig *shared.OrgUserConfig\n\n\tfor _, context := range *loadReq {\n\t\tif context.ContextType == shared.ContextPipedDataType || context.ContextType == shared.ContextNoteType || context.ContextType == shared.ContextImageType {\n\n\t\t\tsettings, err = db.GetPlanSettings(plan)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan settings: %v\\n\", err)\n\t\t\t\thttp.Error(w, \"Error getting plan settings: \"+err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn nil, nil\n\t\t\t}\n\n\t\t\torgUserConfig, err = db.GetOrgUserConfig(auth.User.Id, auth.OrgId)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting org user config: %v\\n\", err)\n\t\t\t\thttp.Error(w, \"Error getting org user config: \"+err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn nil, nil\n\t\t\t}\n\n\t\t\tres := initClients(\n\t\t\t\tinitClientsParams{\n\t\t\t\t\tw:           w,\n\t\t\t\t\tauth:        auth,\n\t\t\t\t\tapiKeys:     context.ApiKeys,\n\t\t\t\t\topenAIOrgId: context.OpenAIOrgId,\n\t\t\t\t\tauthVars:    context.AuthVars,\n\t\t\t\t\tplan:        plan,\n\t\t\t\t\tsettings:    settings,\n\t\t\t\t},\n\t\t\t)\n\n\t\t\tclients = res.clients\n\t\t\tauthVars = res.authVars\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// ensure image compatibility if we're loading an image\n\tfor _, context := range *loadReq {\n\t\tif context.ContextType == shared.ContextImageType {\n\t\t\tif !settings.GetModelPack().Planner.GetSharedBaseConfig(settings).HasImageSupport {\n\t\t\t\tlog.Printf(\"Error loading context: %s does not support images in context\\n\", settings.GetModelPack().Planner.ModelId)\n\t\t\t\thttp.Error(w, fmt.Sprintf(\"Error loading context: %s does not support images in context\", settings.GetModelPack().Planner.ModelId), http.StatusBadRequest)\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// get name for piped data or notes if present\n\tnum := 0\n\terrCh := make(chan error, len(*loadReq))\n\tfor _, context := range *loadReq {\n\t\tif context.ContextType == shared.ContextPipedDataType {\n\t\t\tnum++\n\n\t\t\tgo func(context *shared.LoadContextParams) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"panic in GenPipedDataName: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GenPipedDataName: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tname, err := model.GenPipedDataName(model.GenPipedDataNameParams{\n\t\t\t\t\tCtx:           r.Context(),\n\t\t\t\t\tAuth:          auth,\n\t\t\t\t\tPlan:          plan,\n\t\t\t\t\tSettings:      settings,\n\t\t\t\t\tAuthVars:      authVars,\n\t\t\t\t\tSessionId:     context.SessionId,\n\t\t\t\t\tClients:       clients,\n\t\t\t\t\tPipedContent:  context.Body,\n\t\t\t\t\tOrgUserConfig: orgUserConfig,\n\t\t\t\t})\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error generating name for piped data: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tcontext.Name = name\n\t\t\t\terrCh <- nil\n\t\t\t}(context)\n\t\t} else if context.ContextType == shared.ContextNoteType {\n\t\t\tnum++\n\n\t\t\tgo func(context *shared.LoadContextParams) {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"panic in GenNoteName: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\terrCh <- fmt.Errorf(\"panic in GenNoteName: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tname, err := model.GenNoteName(r.Context(), auth, plan, settings, orgUserConfig, clients, authVars, context.Body, context.SessionId)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"error generating name for note: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tcontext.Name = name\n\t\t\t\terrCh <- nil\n\t\t\t}(context)\n\t\t}\n\t}\n\tif num > 0 {\n\t\tfor i := 0; i < num; i++ {\n\t\t\terr := <-errCh\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error: %v\\n\", err)\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn nil, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar loadRes *shared.LoadContextResponse\n\tvar dbContexts []*db.Context\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:          auth.OrgId,\n\t\tUserId:         auth.User.Id,\n\t\tPlanId:         plan.Id,\n\t\tBranch:         branchName,\n\t\tReason:         \"load contexts\",\n\t\tScope:          db.LockScopeWrite,\n\t\tCtx:            ctx,\n\t\tCancelFn:       cancel,\n\t\tClearRepoOnErr: true,\n\t}, func(repo *db.GitRepo) error {\n\t\tlog.Printf(\"[ContextHelper] Calling db.LoadContexts with %d contexts, %d cached maps\", len(*loadReq), len(cachedMapsByPath))\n\t\tfor path := range cachedMapsByPath {\n\t\t\tlog.Printf(\"[ContextHelper] Using cached map for path: %s\", path)\n\t\t}\n\n\t\tres, dbContextsRes, err := db.LoadContexts(ctx, db.LoadContextsParams{\n\t\t\tOrgId:            auth.OrgId,\n\t\t\tPlan:             plan,\n\t\t\tBranchName:       branchName,\n\t\t\tReq:              loadReq,\n\t\t\tUserId:           auth.User.Id,\n\t\t\tCachedMapsByPath: cachedMapsByPath,\n\t\t\tAutoLoaded:       autoLoaded,\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tloadRes = res\n\t\tdbContexts = dbContextsRes\n\n\t\tlog.Printf(\"[ContextHelper] db.LoadContexts completed successfully, loaded %d contexts\", len(dbContexts))\n\n\t\t// Log information about loaded map contexts\n\t\tmapContextsCount := 0\n\t\tfor _, context := range dbContexts {\n\t\t\tif context.ContextType == shared.ContextMapType {\n\t\t\t\tmapContextsCount++\n\t\t\t\tlog.Printf(\"[ContextHelper] Loaded map context: %s, path: %s, tokens: %d\", context.Name, context.FilePath, context.NumTokens)\n\t\t\t}\n\t\t}\n\t\tif mapContextsCount > 0 {\n\t\t\tlog.Printf(\"[ContextHelper] Successfully loaded %d map contexts out of %d total contexts\", mapContextsCount, len(dbContexts))\n\t\t}\n\n\t\tif loadRes.MaxTokensExceeded {\n\t\t\treturn nil\n\t\t}\n\n\t\tlog.Printf(\"[ContextHelper] Committing changes to branch %s\", branchName)\n\t\terr = repo.GitAddAndCommit(branchName, res.Msg)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error committing changes: %v\", err)\n\t\t}\n\n\t\tlog.Printf(\"[ContextHelper] Committing changes to branch %s completed successfully\", branchName)\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error loading contexts: %v\\n\", err)\n\t\thttp.Error(w, \"Error loading contexts: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn nil, nil\n\t}\n\n\tif loadRes.MaxTokensExceeded {\n\t\tlog.Printf(\"The total number of tokens (%d) exceeds the maximum allowed (%d)\", loadRes.TotalTokens, loadRes.MaxTokens)\n\t\tbytes, err := json.Marshal(loadRes)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tw.Write(bytes)\n\t\treturn nil, nil\n\t}\n\n\treturn loadRes, dbContexts\n}\n"
  },
  {
    "path": "app/server/handlers/err_helper.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc writeApiError(w http.ResponseWriter, apiErr shared.ApiError) {\n\tbytes, err := json.Marshal(apiErr)\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\t// If marshalling fails, fall back to a simpler error message\n\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Printf(\"API Error: %v\\n\", apiErr.Msg)\n\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(apiErr.Status)\n\n\t_, writeErr := w.Write(bytes)\n\tif writeErr != nil {\n\t\tlog.Printf(\"Error writing response: %v\\n\", writeErr)\n\t}\n}\n"
  },
  {
    "path": "app/server/handlers/file_maps.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sync\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc GetFileMapHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetFileMapHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\tlog.Println(\"GetFileMapHandler: auth failed\")\n\t\treturn\n\t}\n\n\tvar req shared.GetFileMapRequest\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Error decoding request: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tlog.Println(\"GetFileMapHandler: checking limits\")\n\n\tif len(req.MapInputs) > shared.MaxContextMapPaths {\n\t\thttp.Error(w, fmt.Sprintf(\"Too many files to map: %d (max %d)\", len(req.MapInputs), shared.MaxContextMapPaths), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\ttotalSize := 0\n\tfor path, input := range req.MapInputs {\n\t\t// the client should be truncating inputs to the max size, but we'll check here too\n\t\tif len(input) > shared.MaxContextMapSingleInputSize {\n\t\t\thttp.Error(w, fmt.Sprintf(\"File %s is too large: %d (max %d)\", path, len(input), shared.MaxContextMapSingleInputSize), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\ttotalSize += len(input)\n\t}\n\n\t// On the client, once the total size limit is exceeded, we send empty file maps for remaining files\n\tif totalSize > shared.MaxContextMapTotalInputSize+10000 {\n\t\thttp.Error(w, fmt.Sprintf(\"Max map size exceeded: %d (max %d)\", totalSize, shared.MaxContextMapTotalInputSize), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// Check batch size limits\n\tif len(req.MapInputs) > shared.ContextMapMaxBatchSize {\n\t\thttp.Error(w, fmt.Sprintf(\"Batch contains too many files: %d (max %d)\", len(req.MapInputs), shared.ContextMapMaxBatchSize), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif int64(totalSize) > shared.ContextMapMaxBatchBytes {\n\t\thttp.Error(w, fmt.Sprintf(\"Batch size too large: %d bytes (max %d bytes)\", totalSize, shared.ContextMapMaxBatchBytes), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tresults := make(chan shared.FileMapBodies, 1)\n\n\terr := queueProjectMapJob(projectMapJob{\n\t\tinputs:  req.MapInputs,\n\t\tctx:     r.Context(),\n\t\tresults: results,\n\t})\n\tif err != nil {\n\t\tlog.Println(\"GetFileMapHandler: map queue is full\")\n\t\thttp.Error(w, \"Too many project map jobs, please try again later\", http.StatusTooManyRequests)\n\t\treturn\n\t}\n\n\tselect {\n\tcase <-r.Context().Done():\n\t\thttp.Error(w, \"Request was cancelled\", http.StatusRequestTimeout)\n\t\treturn\n\tcase maps := <-results:\n\t\tif maps == nil {\n\t\t\thttp.Error(w, \"Mapping timed out\", http.StatusRequestTimeout)\n\t\t\treturn\n\t\t}\n\n\t\tresp := shared.GetFileMapResponse{\n\t\t\tMapBodies: maps,\n\t\t}\n\t\trespBytes, err := json.Marshal(resp)\n\t\tif err != nil {\n\t\t\thttp.Error(w, fmt.Sprintf(\"Error marshalling response: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write(respBytes)\n\n\t\tlog.Printf(\"GetFileMapHandler success - writing response bytes: %d\", len(respBytes))\n\t}\n}\n\nfunc LoadCachedFileMapHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for LoadCachedFileMapHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranchName := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId, \"branchName: \", branchName)\n\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tvar req shared.LoadCachedFileMapRequest\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Error decoding request: %v\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tcachedMetaByPath := map[string]*shared.Context{}\n\tcachedMapsByPath := map[string]*db.CachedMap{}\n\tvar mu sync.Mutex\n\terrCh := make(chan error, len(req.FilePaths))\n\n\tfor _, path := range req.FilePaths {\n\t\tgo func(path string) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in LoadCachedFileMapHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic in LoadCachedFileMapHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tcachedContext, err := db.GetCachedMap(plan.OrgId, plan.ProjectId, path)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error getting cached map: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif cachedContext != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\tcachedMetaByPath[path] = cachedContext.ToMeta().ToApi()\n\t\t\t\tcachedMapsByPath[path] = &db.CachedMap{\n\t\t\t\t\tMapParts:  cachedContext.MapParts,\n\t\t\t\t\tMapShas:   cachedContext.MapShas,\n\t\t\t\t\tMapTokens: cachedContext.MapTokens,\n\t\t\t\t\tMapSizes:  cachedContext.MapSizes,\n\t\t\t\t}\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t\terrCh <- nil\n\t\t}(path)\n\t}\n\n\tfor range req.FilePaths {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting cached map: %v\", err)\n\t\t\thttp.Error(w, fmt.Sprintf(\"Error getting cached map: %v\", err), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n\n\tresp := shared.LoadCachedFileMapResponse{}\n\n\tvar loadRes *shared.LoadContextResponse\n\tif len(cachedMetaByPath) == 0 {\n\t\tlog.Println(\"no cached maps found\")\n\t} else {\n\t\tlog.Println(\"cached map found\")\n\n\t\tcachedByPath := map[string]bool{}\n\t\tfor _, cachedContext := range cachedMetaByPath {\n\t\t\tcachedByPath[cachedContext.FilePath] = true\n\t\t}\n\t\tresp.CachedByPath = cachedByPath\n\n\t\tvar loadReq shared.LoadContextRequest\n\t\tfor _, cachedContext := range cachedMetaByPath {\n\t\t\tloadReq = append(loadReq, &shared.LoadContextParams{\n\t\t\t\tContextType: shared.ContextMapType,\n\t\t\t\tName:        cachedContext.Name,\n\t\t\t\tFilePath:    cachedContext.FilePath,\n\t\t\t\tBody:        cachedContext.Body,\n\t\t\t})\n\t\t}\n\n\t\tloadRes, _ = loadContexts(loadContextsParams{\n\t\t\tw:                w,\n\t\t\tr:                r,\n\t\t\tauth:             auth,\n\t\t\tloadReq:          &loadReq,\n\t\t\tplan:             plan,\n\t\t\tbranchName:       branchName,\n\t\t\tcachedMapsByPath: cachedMapsByPath,\n\t\t})\n\n\t\tif loadRes == nil {\n\t\t\tlog.Println(\"LoadCachedFileMapHandler - loadRes is nil\")\n\t\t\treturn\n\t\t}\n\n\t\tresp.LoadRes = loadRes\n\t}\n\n\tbytes, err := json.Marshal(resp)\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\", err)\n\t\thttp.Error(w, fmt.Sprintf(\"Error marshalling response: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n}\n"
  },
  {
    "path": "app/server/handlers/file_maps_queue.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\t\"math\"\n\t\"plandex-server/syntax/file_map\"\n\tshared \"plandex-shared\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n)\n\n// simple in-memory per-instance queue for file map jobs\n// ensures mapping doesn't take over all available CPUs\n\nconst fileMapMaxQueueSize = 20 // caller errors out if this is exceeded\nvar fileMapMaxConcurrency = 3  // set to 3/4 of available CPUs below\nconst mapJobTimeout = 60 * time.Second\n\ntype projectMapJob struct {\n\tinputs  shared.FileMapInputs\n\tctx     context.Context\n\tresults chan shared.FileMapBodies\n}\n\nvar projectMapQueue = make(chan projectMapJob, fileMapMaxQueueSize)\n\nvar mapCPUSem chan struct{}\n\nfunc init() {\n\t// Use 3/4 of available CPUs for mapping workers\n\tcpus := runtime.NumCPU()\n\tfileMapMaxConcurrency = int(math.Ceil(float64(cpus) * 0.75))\n\tif fileMapMaxConcurrency < 1 {\n\t\tfileMapMaxConcurrency = 1\n\t}\n\n\tlog.Printf(\"fileMapMaxConcurrency: %d\", fileMapMaxConcurrency)\n\n\tmapCPUSem = make(chan struct{}, fileMapMaxConcurrency)\n\n\t// start workers, one per CPU\n\tfor i := 0; i < fileMapMaxConcurrency; i++ {\n\t\tgo processProjectMapQueue()\n\t}\n}\n\nfunc processProjectMapQueue() {\n\tfor job := range projectMapQueue {\n\t\tif job.ctx.Err() != nil {\n\t\t\tif job.ctx.Err() == context.DeadlineExceeded {\n\t\t\t\tlog.Printf(\"processProjectMapQueue: job context deadline exceeded: %v\", job.ctx.Err())\n\t\t\t\tsafeSend(job.results, nil)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Printf(\"processProjectMapQueue: job context cancelled: %v\", job.ctx.Err())\n\t\t\tsafeSend(job.results, nil)\n\t\t\tcontinue\n\t\t}\n\t\tctxWithTimeout, cancel := context.WithTimeout(job.ctx, mapJobTimeout)\n\t\tmapWorker(projectMapJob{\n\t\t\tinputs:  job.inputs,\n\t\t\tctx:     ctxWithTimeout,\n\t\t\tresults: job.results,\n\t\t})\n\t\tcancel()\n\t}\n}\n\nfunc queueProjectMapJob(job projectMapJob) error {\n\tlog.Printf(\"queueProjectMapJob: len(projectMapQueue): %d\", len(projectMapQueue))\n\tselect {\n\tcase projectMapQueue <- job:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"queue is full\")\n\t}\n}\n\nfunc mapWorker(job projectMapJob) {\n\tmaps := make(shared.FileMapBodies)\n\twg := sync.WaitGroup{}\n\tvar mu sync.Mutex\n\n\tlog.Printf(\"mapWorker: len(job.inputs): %d\", len(job.inputs))\n\n\tfor path, input := range job.inputs {\n\t\tif !shared.HasFileMapSupport(path) {\n\t\t\tmu.Lock()\n\t\t\tmaps[path] = \"[NO MAP]\"\n\t\t\tmu.Unlock()\n\t\t\tcontinue\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo func(path string, input string) {\n\t\t\tif job.ctx.Err() != nil {\n\t\t\t\twg.Done()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmapCPUSem <- struct{}{}\n\t\t\tdefer func() { <-mapCPUSem }()\n\t\t\tdefer wg.Done()\n\n\t\t\tfileMap, err := file_map.MapFile(job.ctx, path, []byte(input))\n\t\t\tif err != nil {\n\t\t\t\t// Skip files that can't be parsed, just log the error\n\t\t\t\tlog.Printf(\"Error mapping file %s: %v\", path, err)\n\t\t\t\tmu.Lock()\n\t\t\t\tmaps[path] = \"[NO MAP]\"\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmu.Lock()\n\t\t\tmaps[path] = fileMap.String()\n\t\t\tmu.Unlock()\n\t\t}(path, input)\n\t}\n\n\twg.Wait()\n\n\tif job.ctx.Err() != nil {\n\t\tsafeSend(job.results, nil)\n\t\treturn\n\t}\n\n\tsafeSend(job.results, maps)\n}\n\nfunc safeSend(ch chan shared.FileMapBodies, v shared.FileMapBodies) {\n\t// never block, never panic\n\tselect {\n\tcase ch <- v:\n\tdefault: // buffer already full – receiver must have gone away\n\t}\n}\n"
  },
  {
    "path": "app/server/handlers/invites.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/email\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc InviteUserHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for InviteUserHandler\")\n\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Local mode is not supported for invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't invite other users\",\n\t\t})\n\n\t\treturn\n\t}\n\n\tcurrentUserId := auth.User.Id\n\n\tvar req shared.InviteRequest\n\terr = json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling request: %v\\n\", err)\n\t\thttp.Error(w, \"Error unmarshalling request: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\treq.Email = strings.ToLower(req.Email)\n\n\t// ensure current user can invite target user\n\tpermission := shared.Permission(strings.Join([]string{string(shared.PermissionInviteUser), req.OrgRoleId}, \"|\"))\n\n\tif !auth.HasPermission(permission) {\n\t\tlog.Printf(\"User does not have permission to invite user with role: %v\\n\", req.OrgRoleId)\n\t\thttp.Error(w, \"User does not have permission to invite user with role: \"+req.OrgRoleId, http.StatusForbidden)\n\t\treturn\n\t}\n\n\t// ensure user doesn't already have access to org via domain\n\tsplit := strings.Split(req.Email, \"@\")\n\tif len(split) != 2 {\n\t\tlog.Printf(\"Invalid email: %v\\n\", req.Email)\n\t\thttp.Error(w, \"Invalid email: \"+req.Email, http.StatusBadRequest)\n\t\treturn\n\t}\n\tdomain := &split[1]\n\n\tif org.AutoAddDomainUsers && org.Domain == domain {\n\t\tlog.Printf(\"User already has access to org via domain: %v\\n\", domain)\n\t\thttp.Error(w, \"User already has access to org via domain: \"+*domain, http.StatusBadRequest)\n\t}\n\n\t// ensure user with this email isn't already in the org\n\tuser, err := db.GetUserByEmail(req.Email)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting user: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting user: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif user != nil {\n\t\tisMember, err := db.ValidateOrgMembership(user.Id, auth.OrgId)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error validating org membership: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error validating org membership: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif isMember {\n\t\t\tlog.Println(\"User is already a member of org\")\n\t\t\thttp.Error(w, \"User is already a member of org\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// ensure invite isn't already active\n\tinvite, err := db.GetActiveInviteByEmail(auth.OrgId, req.Email)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting invite: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting invite: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif invite != nil {\n\t\tlog.Println(\"Invite already exists\")\n\t\thttp.Error(w, \"Invite already exists\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr = db.WithTx(r.Context(), \"invite user\", func(tx *sqlx.Tx) error {\n\n\t\terr = db.CreateInvite(&db.Invite{\n\t\t\tOrgId:     auth.OrgId,\n\t\t\tOrgRoleId: req.OrgRoleId,\n\t\t\tEmail:     req.Email,\n\t\t\tName:      req.Name,\n\t\t\tInviterId: currentUserId,\n\t\t}, tx)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error creating invite: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error creating invite: %v\", err)\n\t\t}\n\n\t\terr = email.SendInviteEmail(req.Email, req.Name, auth.User.Name, org.Name)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error sending invite email: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error sending invite email: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error inviting user: %v\\n\", err)\n\t\thttp.Error(w, \"Error inviting user: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully created invite\")\n}\n\nfunc ListPendingInvitesHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for ListInvitesHandler\")\n\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Local mode is not supported for invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't list invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tinvites, err := db.ListPendingInvites(auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing invites: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing invites: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiInvites []*shared.Invite\n\tfor _, invite := range invites {\n\t\tapiInvites = append(apiInvites, invite.ToApi())\n\t}\n\n\tbytes, err := json.Marshal(apiInvites)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling invites: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling invites: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\tlog.Println(\"Successfully processed request for ListPendingInvitesHandler\")\n}\n\nfunc ListAcceptedInvitesHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for ListAcceptedInvitesHandler\")\n\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Local mode is not supported for invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't list invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tinvites, err := db.ListAcceptedInvites(auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing invites: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing invites: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiInvites []*shared.Invite\n\tfor _, invite := range invites {\n\t\tapiInvites = append(apiInvites, invite.ToApi())\n\t}\n\n\tbytes, err := json.Marshal(apiInvites)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling invites: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling invites: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\tlog.Println(\"Successfully processed request for ListAcceptedInvitesHandler\")\n}\n\nfunc ListAllInvitesHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for ListAllInvitesHandler\")\n\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Local mode is not supported for invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't list invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tinvites, err := db.ListAllInvites(auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing invites: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing invites: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiInvites []*shared.Invite\n\tfor _, invite := range invites {\n\t\tapiInvites = append(apiInvites, invite.ToApi())\n\t}\n\n\tbytes, err := json.Marshal(apiInvites)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling invites: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling invites: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\tlog.Println(\"Successfully processed request for ListAllInvitesHandler\")\n}\n\nfunc DeleteInviteHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for DeleteInviteHandler\")\n\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Local mode is not supported for invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't delete invites\",\n\t\t})\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tinviteId := vars[\"inviteId\"]\n\n\tinvite, err := db.GetInvite(inviteId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting invite: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting invite: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif invite == nil || invite.OrgId != auth.OrgId {\n\t\tlog.Printf(\"Invite not found: %v\\n\", inviteId)\n\t\thttp.Error(w, \"Invite not found: \"+inviteId, http.StatusNotFound)\n\t\treturn\n\t}\n\n\t// ensure current user can remove target invite\n\tremovePermission := shared.Permission(strings.Join([]string{string(shared.PermissionRemoveUser), invite.OrgRoleId}, \"|\"))\n\n\tinvitePermission := shared.Permission(strings.Join([]string{string(shared.PermissionInviteUser), invite.OrgRoleId}, \"|\"))\n\n\tif !(auth.HasPermission(removePermission) ||\n\t\t(auth.User.Id == invite.InviterId && auth.HasPermission(invitePermission))) {\n\t\tlog.Printf(\"User does not have permission to remove invite with role: %v\\n\", invite.OrgRoleId)\n\t\thttp.Error(w, \"User does not have permission to remove invite with role: \"+invite.OrgRoleId, http.StatusForbidden)\n\t\treturn\n\t}\n\n\terr = db.DeleteInvite(inviteId, nil)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error deleting invite: %v\\n\", err)\n\t\thttp.Error(w, \"Error deleting invite: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully deleted invite\")\n}\n"
  },
  {
    "path": "app/server/handlers/models.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nconst CustomModelsMinClientVersion = \"2.2.0\"\n\nfunc UpsertCustomModelsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateCustomModelHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {\n\t\treturn\n\t}\n\n\tvar modelsInput shared.ModelsInput\n\tif err := json.NewDecoder(r.Body).Decode(&modelsInput); err != nil {\n\t\tlog.Printf(\"Error decoding request body: %v\\n\", err)\n\t\thttp.Error(w, \"Invalid request body: \"+err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif len(modelsInput.CustomProviders) > 0 {\n\t\tif os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\t\thttp.Error(w, \"Custom model providers are not supported on Plandex Cloud\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif len(modelsInput.CustomModels) > 0 {\n\t\tif os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\t\tapiOrg, err := getApiOrg(auth.OrgId)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error fetching org: %v\\n\", err)\n\t\t\t\thttp.Error(w, \"Failed to create custom model: \"+err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif apiOrg.IntegratedModelsMode {\n\t\t\t\thttp.Error(w, \"Custom models are not supported on Plandex Cloud in Integrated Models mode\", http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\thasDuplicates, errMsg := modelsInput.CheckNoDuplicates()\n\tif !hasDuplicates {\n\t\thttp.Error(w, \"Has duplicates: \"+errMsg, http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tfor _, provider := range modelsInput.CustomProviders {\n\t\tif provider.Name == \"\" {\n\t\t\tmsg := \"Provider name is required\"\n\t\t\tlog.Println(msg)\n\t\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\n\tfor _, model := range modelsInput.CustomModels {\n\t\tif model.ModelId == \"\" {\n\t\t\tmsg := \"Model id is required\"\n\t\t\tlog.Println(msg)\n\t\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tif shared.BuiltInBaseModelsById[model.ModelId] != nil {\n\t\t\tmsg := fmt.Sprintf(\"%s is a built-in base model id, so it can't be used for a custom model\", model.ModelId)\n\t\t\tlog.Println(msg)\n\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\treturn\n\t\t}\n\t}\n\n\tfor _, modelPack := range modelsInput.CustomModelPacks {\n\t\tif modelPack.Name == \"\" {\n\t\t\tmsg := \"Model pack name is required\"\n\t\t\tlog.Println(msg)\n\t\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tif shared.BuiltInModelPacksByName[modelPack.Name] != nil {\n\t\t\tmsg := fmt.Sprintf(\"%s is a built-in model pack name, so it can't be used for a custom model pack\", modelPack.Name)\n\t\t\tlog.Println(msg)\n\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\treturn\n\t\t}\n\t}\n\n\tvar existingCustomModelIds = make(map[shared.ModelId]bool)\n\tvar existingCustomProviderNames = make(map[string]bool)\n\n\tcustomModels, err := db.ListCustomModels(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error fetching custom models: %v\\n\", err)\n\t\thttp.Error(w, \"Failed to create custom model: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tcustomModelPacks, err := db.ListModelPacks(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error fetching custom model packs: %v\\n\", err)\n\t\thttp.Error(w, \"Failed to create custom model: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar customProviders []*db.CustomProvider\n\n\tif os.Getenv(\"IS_CLOUD\") == \"\" {\n\t\tcustomProviders, err = db.ListCustomProviders(auth.OrgId)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error fetching custom providers: %v\\n\", err)\n\t\t\thttp.Error(w, \"Failed to create custom model: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n\n\tapiCustomModels := make([]*shared.CustomModel, len(customModels))\n\tfor i, model := range customModels {\n\t\tapiCustomModels[i] = model.ToApi()\n\t}\n\n\tapiCustomProviders := make([]*shared.CustomProvider, len(customProviders))\n\tfor i, provider := range customProviders {\n\t\tapiCustomProviders[i] = provider.ToApi()\n\t}\n\n\tapiCustomModelPacks := make([]*shared.ModelPackSchema, len(customModelPacks))\n\tfor i, modelPack := range customModelPacks {\n\t\tapiCustomModelPacks[i] = modelPack.ToApi().ToModelPackSchema()\n\t}\n\n\tupdatedModelsInput := modelsInput.FilterUnchanged(&shared.ModelsInput{\n\t\tCustomModels:     apiCustomModels,\n\t\tCustomProviders:  apiCustomProviders,\n\t\tCustomModelPacks: apiCustomModelPacks,\n\t})\n\n\tfor _, model := range customModels {\n\t\texistingCustomModelIds[model.ModelId] = true\n\t}\n\n\tfor _, provider := range customProviders {\n\t\texistingCustomProviderNames[provider.Name] = true\n\t}\n\n\tinputModelIds := make(map[string]bool)\n\tinputProviderNames := make(map[string]bool)\n\tinputModelPackNames := make(map[string]bool)\n\n\tfor _, model := range modelsInput.CustomModels {\n\t\tinputModelIds[string(model.ModelId)] = true\n\t}\n\n\tfor _, provider := range modelsInput.CustomProviders {\n\t\tinputProviderNames[provider.Name] = true\n\t}\n\n\tfor _, modelPack := range modelsInput.CustomModelPacks {\n\t\tinputModelPackNames[modelPack.Name] = true\n\t}\n\n\tvar toUpsertCustomModels []*db.CustomModel\n\tvar toUpsertCustomProviders []*db.CustomProvider\n\tvar toUpsertModelPacks []*db.ModelPack\n\n\tfor _, provider := range updatedModelsInput.CustomProviders {\n\t\tdbProvider := db.CustomProviderFromApi(provider)\n\t\tdbProvider.Id = provider.Id\n\t\tdbProvider.OrgId = auth.OrgId\n\n\t\ttoUpsertCustomProviders = append(toUpsertCustomProviders, dbProvider)\n\t}\n\n\tfor _, model := range updatedModelsInput.CustomModels {\n\t\t// ensure that providers to upsert are either built-in, being imported, or already exist\n\t\tfor _, provider := range model.Providers {\n\t\t\tif provider.Provider == shared.ModelProviderCustom {\n\t\t\t\t_, exists := existingCustomProviderNames[*provider.CustomProvider]\n\t\t\t\t_, creating := inputProviderNames[*provider.CustomProvider]\n\t\t\t\tif !exists && !creating {\n\t\t\t\t\tmsg := fmt.Sprintf(\"'%s' is not a custom model provider that exists or is being imported\", *provider.CustomProvider)\n\t\t\t\t\tlog.Println(msg)\n\t\t\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tpc, builtIn := shared.BuiltInModelProviderConfigs[provider.Provider]\n\t\t\t\tif !builtIn {\n\t\t\t\t\tmsg := fmt.Sprintf(\"'%s' is not a built-in model provider\", provider.Provider)\n\t\t\t\t\tlog.Println(msg)\n\t\t\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif os.Getenv(\"IS_CLOUD\") != \"\" && pc.LocalOnly {\n\t\t\t\t\tmsg := fmt.Sprintf(\"'%s' is a local-only model provider, so it can't be used on Plandex Cloud\", provider.Provider)\n\t\t\t\t\tlog.Println(msg)\n\t\t\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tdbModel := db.CustomModelFromApi(model)\n\t\tdbModel.Id = model.Id\n\t\tdbModel.OrgId = auth.OrgId\n\n\t\ttoUpsertCustomModels = append(toUpsertCustomModels, dbModel)\n\t}\n\n\tfor _, modelPack := range updatedModelsInput.CustomModelPacks {\n\t\t// ensure that all models are either built-in, being imported, or already exist\n\t\tallModelIds := modelPack.AllModelIds()\n\n\t\tfor _, modelId := range allModelIds {\n\t\t\t_, exists := existingCustomModelIds[modelId]\n\t\t\t_, creating := inputModelIds[string(modelId)]\n\t\t\tbm, builtIn := shared.BuiltInBaseModelsById[modelId]\n\n\t\t\tif !exists && !creating && !builtIn {\n\t\t\t\tmsg := fmt.Sprintf(\"'%s' is not built-in, not being imported, and not an existing custom model\", modelId)\n\t\t\t\tlog.Println(msg)\n\t\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif builtIn && os.Getenv(\"IS_CLOUD\") != \"\" && bm.IsLocalOnly() {\n\t\t\t\tmsg := fmt.Sprintf(\"'%s' is a local-only built-in model, so it can't be used on Plandex Cloud\", modelId)\n\t\t\t\tlog.Println(msg)\n\t\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tmp := modelPack.ToModelPack()\n\t\tdbMp := db.ModelPackFromApi(&mp)\n\t\tdbMp.OrgId = auth.OrgId\n\t\tdbMp.Id = mp.Id\n\n\t\ttoUpsertModelPacks = append(toUpsertModelPacks, dbMp)\n\t}\n\n\ttoDeleteCustomModelIds := []string{}\n\ttoDeleteCustomProviderIds := []string{}\n\ttoDeleteModelPackIds := []string{}\n\n\tfor _, model := range customModels {\n\t\tif _, exists := inputModelIds[string(model.ModelId)]; !exists {\n\t\t\ttoDeleteCustomModelIds = append(toDeleteCustomModelIds, model.Id)\n\t\t}\n\t}\n\n\tfor _, provider := range customProviders {\n\t\tif _, exists := inputProviderNames[provider.Name]; !exists {\n\t\t\ttoDeleteCustomProviderIds = append(toDeleteCustomProviderIds, provider.Id)\n\t\t}\n\t}\n\n\tfor _, modelPack := range customModelPacks {\n\t\tif _, exists := inputModelPackNames[modelPack.Name]; !exists {\n\t\t\ttoDeleteModelPackIds = append(toDeleteModelPackIds, modelPack.Id)\n\t\t}\n\t}\n\n\tnumChanges := len(toUpsertCustomModels) + len(toUpsertCustomProviders) + len(toUpsertModelPacks) + len(toDeleteCustomModelIds) + len(toDeleteCustomProviderIds) + len(toDeleteModelPackIds)\n\tif numChanges == 0 {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tlog.Println(\"No changes to custom models/providers/model packs\")\n\t\treturn\n\t}\n\n\terr = db.WithTx(r.Context(), \"create custom models/providers/model packs\", func(tx *sqlx.Tx) error {\n\t\tfor _, model := range toUpsertCustomModels {\n\t\t\tif err := db.UpsertCustomModel(tx, model); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error creating custom model: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tfor _, provider := range toUpsertCustomProviders {\n\t\t\tif err := db.UpsertCustomProvider(tx, provider); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error creating custom provider: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tfor _, modelPack := range toUpsertModelPacks {\n\t\t\tif err := db.UpsertModelPack(tx, modelPack); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error creating model pack: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif len(toDeleteCustomModelIds) > 0 {\n\t\t\tif err := db.DeleteCustomModels(tx, auth.OrgId, toDeleteCustomModelIds); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error deleting custom models: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif len(toDeleteCustomProviderIds) > 0 {\n\t\t\tif err := db.DeleteCustomProviders(tx, auth.OrgId, toDeleteCustomProviderIds); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error deleting custom providers: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif len(toDeleteModelPackIds) > 0 {\n\t\t\tif err := db.DeleteModelPacks(tx, auth.OrgId, toDeleteModelPackIds); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error deleting model packs: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error: %v\\n\", err)\n\t\thttp.Error(w, \"Failed to import custom models/providers/model packs: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.WriteHeader(http.StatusOK)\n\n\tlog.Println(\"Successfully imported custom models/providers/model packs\")\n}\n\nfunc GetCustomModelHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetCustomModelHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tid := mux.Vars(r)[\"modelId\"]\n\n\tres, err := db.GetCustomModel(auth.OrgId, id)\n\tif err != nil {\n\t\tlog.Printf(\"Error fetching custom model: %v\\n\", err)\n\t\thttp.Error(w, \"Failed to fetch custom model: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif res == nil {\n\t\thttp.Error(w, \"Custom model not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\terr = json.NewEncoder(w).Encode(res.ToApi())\n\tif err != nil {\n\t\tlog.Printf(\"Error encoding custom model: %v\\n\", err)\n\t\thttp.Error(w, fmt.Sprintf(\"Error encoding custom model: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully fetched custom model\")\n}\n\nfunc ListCustomModelsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListCustomModelsHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {\n\t\treturn\n\t}\n\n\tmodels, err := db.ListCustomModels(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error fetching custom models: %v\\n\", err)\n\t\thttp.Error(w, \"Failed to fetch custom models: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiList []*shared.CustomModel\n\tfor _, m := range models {\n\t\tapiList = append(apiList, m.ToApi())\n\t}\n\n\terr = json.NewEncoder(w).Encode(apiList)\n\tif err != nil {\n\t\tlog.Printf(\"Error encoding custom models: %v\\n\", err)\n\t\thttp.Error(w, fmt.Sprintf(\"Error encoding custom models: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully fetched custom models\")\n}\n\nfunc GetCustomProviderHandler(w http.ResponseWriter, r *http.Request) {\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tid := mux.Vars(r)[\"providerId\"]\n\n\tres, err := db.GetCustomProvider(auth.OrgId, id)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = json.NewEncoder(w).Encode(res.ToApi())\n\tif err != nil {\n\t\tlog.Printf(\"Error encoding custom provider: %v\\n\", err)\n\t\thttp.Error(w, fmt.Sprintf(\"Error encoding custom provider: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully fetched custom provider\")\n}\n\nfunc ListCustomProvidersHandler(w http.ResponseWriter, r *http.Request) {\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\thttp.Error(w, \"Custom model providers are not supported on Plandex Cloud\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tlist, err := db.ListCustomProviders(auth.OrgId)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiList []*shared.CustomProvider\n\tfor _, p := range list {\n\t\tapiList = append(apiList, p.ToApi())\n\t}\n\n\terr = json.NewEncoder(w).Encode(apiList)\n\tif err != nil {\n\t\tlog.Printf(\"Error encoding custom providers: %v\\n\", err)\n\t\thttp.Error(w, fmt.Sprintf(\"Error encoding custom providers: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully fetched custom providers\")\n}\n\nfunc CreateModelPackHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateModelPackHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {\n\t\treturn\n\t}\n\n\thttp.Error(w, \"Use POST /custom_models instead to create model packs\", http.StatusBadRequest)\n}\n\nfunc UpdateModelPackHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdateModelPackHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {\n\t\treturn\n\t}\n\n\thttp.Error(w, \"Use POST /custom_models instead to update model packs\", http.StatusBadRequest)\n}\n\nfunc ListModelPacksHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListModelPacksHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif !requireMinClientVersion(w, r, CustomModelsMinClientVersion) {\n\t\treturn\n\t}\n\n\tsets, err := db.ListModelPacks(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error fetching model packs: %v\\n\", err)\n\t\thttp.Error(w, \"Failed to fetch model packs: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiPacks []*shared.ModelPack\n\n\tfor _, mp := range sets {\n\t\tapiPacks = append(apiPacks, mp.ToApi())\n\t}\n\n\tjson.NewEncoder(w).Encode(apiPacks)\n\n\tlog.Println(\"Successfully fetched model packs\")\n}\n"
  },
  {
    "path": "app/server/handlers/org_helpers.go",
    "content": "package handlers\n\nimport (\n\t\"log\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc toApiOrgs(orgs []*db.Org) ([]*shared.Org, *shared.ApiError) {\n\tvar orgIds []string\n\tfor _, org := range orgs {\n\t\torgIds = append(orgIds, org.Id)\n\t}\n\n\thookRes, apiErr := hooks.ExecHook(hooks.GetApiOrgs, hooks.HookParams{\n\t\tGetApiOrgIds: orgIds,\n\t})\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"Error getting integrated models mode by org id: %v\\n\", apiErr)\n\t\treturn nil, apiErr\n\t}\n\n\tvar apiOrgs []*shared.Org\n\tfor _, org := range orgs {\n\t\tif hookRes.ApiOrgsById != nil {\n\t\t\thookApiOrg := hookRes.ApiOrgsById[org.Id]\n\t\t\tapiOrgs = append(apiOrgs, hookApiOrg)\n\t\t} else {\n\t\t\tapiOrgs = append(apiOrgs, org.ToApi())\n\t\t}\n\t}\n\n\treturn apiOrgs, nil\n}\n\nfunc getApiOrg(orgId string) (*shared.Org, *shared.ApiError) {\n\torg, err := db.GetOrg(orgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\treturn nil, &shared.ApiError{\n\t\t\tType: shared.ApiErrorTypeOther,\n\t\t\tMsg:  \"Error getting org\",\n\t\t}\n\t}\n\n\thookRes, apiErr := hooks.ExecHook(hooks.GetApiOrgs, hooks.HookParams{\n\t\tGetApiOrgIds: []string{org.Id},\n\t})\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"Error getting integrated models mode by org id: %v\\n\", apiErr)\n\t\treturn nil, apiErr\n\t}\n\n\tif hookRes.ApiOrgsById != nil {\n\t\treturn hookRes.ApiOrgsById[org.Id], nil\n\t}\n\n\treturn org.ToApi(), nil\n}\n"
  },
  {
    "path": "app/server/handlers/orgs.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc ListOrgsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListOrgsHandler\")\n\n\tauth := Authenticate(w, r, false)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torgs, err := db.GetAccessibleOrgsForUser(auth.User)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing orgs: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing orgs: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tapiOrgs, apiErr := toApiOrgs(orgs)\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"Error converting orgs to api: %v\\n\", apiErr)\n\t\twriteApiError(w, *apiErr)\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(apiOrgs)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully listed orgs\")\n\n\tw.Write(bytes)\n}\n\nfunc CreateOrgHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateOrgHandler\")\n\n\tif os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Plandex Cloud orgs can only be created by starting a trial\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, false)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar req shared.CreateOrgRequest\n\terr = json.Unmarshal(body, &req)\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling request: %v\\n\", err)\n\t\thttp.Error(w, \"Error unmarshalling request: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiErr *shared.ApiError\n\tvar org *db.Org\n\terr = db.WithTx(r.Context(), \"create org\", func(tx *sqlx.Tx) error {\n\t\tvar err error\n\t\tvar domain *string\n\t\tif req.AutoAddDomainUsers {\n\t\t\tif shared.IsEmailServiceDomain(auth.User.Domain) {\n\t\t\t\tlog.Printf(\"Invalid domain: %v\\n\", auth.User.Domain)\n\t\t\t\treturn fmt.Errorf(\"invalid domain: %v\", auth.User.Domain)\n\t\t\t}\n\n\t\t\tdomain = &auth.User.Domain\n\t\t}\n\n\t\t// create a new org\n\t\torg, err = db.CreateOrg(&req, auth.AuthToken.UserId, domain, tx)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error creating org: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error creating org: %v\", err)\n\t\t}\n\n\t\tif org.AutoAddDomainUsers && org.Domain != nil {\n\t\t\terr = db.AddOrgDomainUsers(org.Id, *org.Domain, tx)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error adding org domain users: %v\\n\", err)\n\t\t\t\treturn fmt.Errorf(\"error adding org domain users: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\t_, apiErr = hooks.ExecHook(hooks.CreateOrg, hooks.HookParams{\n\t\t\tAuth: auth,\n\t\t\tTx:   tx,\n\n\t\t\tCreateOrgHookRequestParams: &hooks.CreateOrgHookRequestParams{\n\t\t\t\tOrg: org,\n\t\t\t},\n\t\t})\n\n\t\treturn nil\n\t})\n\n\tif apiErr != nil {\n\t\twriteApiError(w, *apiErr)\n\t\treturn\n\t}\n\n\tif err != nil {\n\t\tlog.Printf(\"Error creating org: %v\\n\", err)\n\t\thttp.Error(w, \"Error creating org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresp := shared.CreateOrgResponse{\n\t\tId: org.Id,\n\t}\n\n\tbytes, err := json.Marshal(resp)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = SetAuthCookieIfBrowser(w, r, auth.User, \"\", org.Id)\n\tif err != nil {\n\t\tlog.Printf(\"Error setting auth cookie: %v\\n\", err)\n\t\thttp.Error(w, \"Error setting auth cookie: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully created org\")\n\n\tw.Write(bytes)\n}\n\nfunc GetOrgSessionHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetOrgSessionHandler\")\n\n\tauth := Authenticate(w, r, true)\n\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, apiErr := getApiOrg(auth.OrgId)\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"Error converting org to api: %v\\n\", apiErr)\n\t\twriteApiError(w, *apiErr)\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(org)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = SetAuthCookieIfBrowser(w, r, auth.User, \"\", org.Id)\n\tif err != nil {\n\t\tlog.Printf(\"Error setting auth cookie: %v\\n\", err)\n\t\thttp.Error(w, \"Error setting auth cookie: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"Successfully got org session\")\n}\n\nfunc ListOrgRolesHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListOrgRolesHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't list org roles\",\n\t\t})\n\t\treturn\n\t}\n\n\tif !auth.HasPermission(shared.PermissionListOrgRoles) {\n\t\tlog.Println(\"User cannot list org roles\")\n\t\thttp.Error(w, \"User cannot list org roles\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\troles, err := db.ListOrgRoles(auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing org roles: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing org roles: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiRoles []*shared.OrgRole\n\tfor _, role := range roles {\n\t\tapiRoles = append(apiRoles, role.ToApi())\n\t}\n\n\tbytes, err := json.Marshal(apiRoles)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully listed org roles\")\n\n\tw.Write(bytes)\n}\n"
  },
  {
    "path": "app/server/handlers/plan_config.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc GetPlanConfigHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetPlanConfigHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tconfig, err := db.GetPlanConfig(planId)\n\tif err != nil {\n\t\tlog.Println(\"Error getting plan config: \", err)\n\t\thttp.Error(w, \"Error getting plan config\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := shared.GetPlanConfigResponse{\n\t\tConfig: config,\n\t}\n\n\tbytes, err := json.Marshal(res)\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling response: \", err)\n\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\tlog.Println(\"GetPlanConfigHandler processed successfully\")\n}\n\nfunc UpdatePlanConfigHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdatePlanConfigHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tvar req shared.UpdatePlanConfigRequest\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\tlog.Println(\"Error decoding request body: \", err)\n\t\thttp.Error(w, \"Error decoding request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr = db.StorePlanConfig(planId, req.Config)\n\tif err != nil {\n\t\tlog.Println(\"Error storing plan config: \", err)\n\t\thttp.Error(w, \"Error storing plan config\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"UpdatePlanConfigHandler processed successfully\")\n}\n\nfunc GetDefaultPlanConfigHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetDefaultPlanConfigHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tconfig, err := db.GetDefaultPlanConfig(auth.User.Id)\n\tif err != nil {\n\t\tlog.Println(\"Error getting default plan config: \", err)\n\t\thttp.Error(w, \"Error getting default plan config\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := shared.GetDefaultPlanConfigResponse{\n\t\tConfig: config,\n\t}\n\n\tbytes, err := json.Marshal(res)\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling response: \", err)\n\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\tlog.Println(\"GetDefaultPlanConfigHandler processed successfully\")\n}\n\nfunc UpdateDefaultPlanConfigHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdateDefaultPlanConfigHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvar req shared.UpdateDefaultPlanConfigRequest\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\tlog.Println(\"Error decoding request body: \", err)\n\t\thttp.Error(w, \"Error decoding request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr = db.WithTx(r.Context(), \"update default plan config\", func(tx *sqlx.Tx) error {\n\n\t\terr := db.StoreDefaultPlanConfig(auth.User.Id, req.Config, tx)\n\t\tif err != nil {\n\t\t\tlog.Println(\"Error storing default plan config: \", err)\n\t\t\treturn fmt.Errorf(\"error storing default plan config: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error updating default plan config: \", err)\n\t\thttp.Error(w, \"Error updating default plan config\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"UpdateDefaultPlanConfigHandler processed successfully\")\n}\n"
  },
  {
    "path": "app/server/handlers/plans_changes.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\tmodelPlan \"plandex-server/model/plan\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc CurrentPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CurrentPlanHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tsha := vars[\"sha\"]\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch, \"sha: \", sha)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\t// Just in case this was sent immediately after a stream finished, wait a little before locking to allow for cleanup\n\ttime.Sleep(100 * time.Millisecond)\n\n\tctx, cancel := context.WithCancel(r.Context())\n\tscope := db.LockScopeRead\n\tif sha != \"\" {\n\t\tscope = db.LockScopeWrite\n\t}\n\tlog.Printf(\"locking with scope: %s\", scope)\n\n\tvar planState *shared.CurrentPlanState\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tScope:    scope,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t\tReason:   \"get current plan state\",\n\t}, func(repo *db.GitRepo) error {\n\t\tvar err error\n\t\tif sha != \"\" {\n\t\t\terr = repo.GitCheckoutSha(sha)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error checking out sha: %v\", err)\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tcheckoutErr := repo.GitCheckoutBranch(branch)\n\t\t\t\tif checkoutErr != nil {\n\t\t\t\t\tlog.Printf(\"Error checking out branch: %v\\n\", checkoutErr)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\tplanState, err = db.GetCurrentPlanState(db.CurrentPlanStateParams{\n\t\t\tOrgId:  auth.OrgId,\n\t\t\tPlanId: planId,\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting current plan state: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting current plan state: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting current plan state: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tjsonBytes, err := json.Marshal(planState)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling plan state: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling plan state: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully retrieved current plan state\")\n\n\tw.Write(jsonBytes)\n}\n\nfunc ApplyPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ApplyPlanHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tvar err error\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.ApplyPlanRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// Just in case this was sent immediately after a stream finished, wait a little before locking to allow for cleanup\n\ttime.Sleep(100 * time.Millisecond)\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar settings *shared.PlanSettings\n\tvar currentPlanParams db.CurrentPlanStateParams\n\tvar currentPlan *shared.CurrentPlanState\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tvar err error\n\t\tsettings, err = db.GetPlanSettings(plan)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting plan settings: %v\", err)\n\t\t}\n\n\t\tcurrentPlanParams, err = db.GetFullCurrentPlanStateParams(auth.OrgId, planId)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting current plan state params: %v\", err)\n\t\t}\n\n\t\tcurrentPlan, err = db.GetCurrentPlanState(currentPlanParams)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting current plan state: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting current plan state: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting current plan state: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"ApplyPlanHandler: Got current plan state:\", currentPlan != nil)\n\n\tres := initClients(\n\t\tinitClientsParams{\n\t\t\tw:           w,\n\t\t\tauth:        auth,\n\t\t\tapiKeys:     requestBody.ApiKeys,\n\t\t\topenAIOrgId: requestBody.OpenAIOrgId,\n\t\t\tauthVars:    requestBody.AuthVars,\n\t\t\tplan:        plan,\n\t\t\tsettings:    settings,\n\t\t},\n\t)\n\n\tclients := res.clients\n\tauthVars := res.authVars\n\n\tcommitMsg, err := modelPlan.GenCommitMsgForPendingResults(modelPlan.GenCommitMsgForPendingResultsParams{\n\t\tAuth:      auth,\n\t\tPlan:      plan,\n\t\tClients:   clients,\n\t\tSettings:  settings,\n\t\tCurrent:   currentPlan,\n\t\tAuthVars:  authVars,\n\t\tSessionId: requestBody.SessionId,\n\t\tCtx:       r.Context(),\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error generating commit message: %v\\n\", err)\n\t\thttp.Error(w, \"Error generating commit message: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:          auth.OrgId,\n\t\tUserId:         auth.User.Id,\n\t\tPlanId:         planId,\n\t\tBranch:         branch,\n\t\tScope:          db.LockScopeWrite,\n\t\tCtx:            ctx,\n\t\tCancelFn:       cancel,\n\t\tClearRepoOnErr: true,\n\t}, func(repo *db.GitRepo) error {\n\t\treturn db.ApplyPlan(repo, ctx, db.ApplyPlanParams{\n\t\t\tOrgId:                  auth.OrgId,\n\t\t\tUserId:                 auth.User.Id,\n\t\t\tBranchName:             branch,\n\t\t\tPlan:                   plan,\n\t\t\tCurrentPlanState:       currentPlan,\n\t\t\tCurrentPlanStateParams: &currentPlanParams,\n\t\t\tCommitMsg:              commitMsg,\n\t\t})\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error applying plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error applying plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write([]byte(commitMsg))\n\n\tlog.Println(\"Successfully applied plan\", planId)\n}\n\nfunc RejectAllChangesHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for RejectAllChangesHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:          auth.OrgId,\n\t\tUserId:         auth.User.Id,\n\t\tPlanId:         planId,\n\t\tBranch:         branch,\n\t\tScope:          db.LockScopeWrite,\n\t\tCtx:            ctx,\n\t\tCancelFn:       cancel,\n\t\tClearRepoOnErr: true,\n\t}, func(repo *db.GitRepo) error {\n\t\terr := db.RejectAllResults(auth.OrgId, planId)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\terr = repo.GitAddAndCommit(branch, \"🚫 Rejected all pending changes\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error committing rejected changes: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error rejecting all changes: %v\\n\", err)\n\t\thttp.Error(w, \"Error rejecting all changes: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully rejected all changes for plan\", planId)\n}\n\nfunc RejectFileHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for RejectFileHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tvar req shared.RejectFileRequest\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\tlog.Printf(\"Error decoding request: %v\\n\", err)\n\t\thttp.Error(w, \"Error decoding request: \"+err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:          auth.OrgId,\n\t\tUserId:         auth.User.Id,\n\t\tPlanId:         planId,\n\t\tBranch:         branch,\n\t\tScope:          db.LockScopeWrite,\n\t\tCtx:            ctx,\n\t\tCancelFn:       cancel,\n\t\tClearRepoOnErr: true,\n\t}, func(repo *db.GitRepo) error {\n\t\terr = db.RejectPlanFile(auth.OrgId, planId, req.FilePath, time.Now())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\terr = repo.GitAddAndCommit(branch, fmt.Sprintf(\"🚫 Rejected pending changes to file: %s\", req.FilePath))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error committing rejected changes: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error rejecting result: %v\\n\", err)\n\t\thttp.Error(w, \"Error rejecting result: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully rejected plan file\", req.FilePath)\n}\n\nfunc RejectFilesHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for RejectFilesHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tvar req shared.RejectFilesRequest\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\tif err != nil {\n\t\tlog.Printf(\"Error decoding request: %v\\n\", err)\n\t\thttp.Error(w, \"Error decoding request: \"+err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:          auth.OrgId,\n\t\tUserId:         auth.User.Id,\n\t\tPlanId:         planId,\n\t\tBranch:         branch,\n\t\tScope:          db.LockScopeWrite,\n\t\tCtx:            ctx,\n\t\tCancelFn:       cancel,\n\t\tClearRepoOnErr: true,\n\t}, func(repo *db.GitRepo) error {\n\t\terr = db.RejectPlanFiles(auth.OrgId, planId, req.Paths, time.Now())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tmsg := \"🚫 Rejected pending changes to file\"\n\t\tif len(req.Paths) > 1 {\n\t\t\tmsg += \"s\"\n\t\t}\n\t\tmsg += \":\"\n\n\t\tfor _, path := range req.Paths {\n\t\t\tmsg += fmt.Sprintf(\"\\n • %s\", path)\n\t\t}\n\n\t\terr = repo.GitAddAndCommit(branch, msg)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error committing rejected changes: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error rejecting result: %v\\n\", err)\n\t\thttp.Error(w, \"Error rejecting result: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully rejected plan files\", req.Paths)\n}\n\nfunc ArchivePlanHandler(w http.ResponseWriter, r *http.Request) {\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tlog.Println(\"Received request for ArchivePlanHandler\")\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlanArchive(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tif plan.ArchivedAt != nil {\n\t\tlog.Println(\"Plan already archived\")\n\t\thttp.Error(w, \"Plan already archived\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tres, err := db.Conn.Exec(\"UPDATE plans SET archived_at = NOW() WHERE id = $1\", planId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error archiving plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error archiving plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\trowsAffected, err := res.RowsAffected()\n\tif err != nil {\n\t\tlog.Printf(\"Error getting rows affected: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting rows affected: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif rowsAffected == 0 {\n\t\tlog.Println(\"Plan not found\")\n\t\thttp.Error(w, \"Not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully archived plan\", planId)\n}\n\nfunc UnarchivePlanHandler(w http.ResponseWriter, r *http.Request) {\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tlog.Println(\"Received request for UnarchivePlanHandler\")\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlanArchive(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tif plan.ArchivedAt == nil {\n\t\tlog.Println(\"Plan isn't archived\")\n\t\thttp.Error(w, \"Plan isn't archived\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tres, err := db.Conn.Exec(\"UPDATE plans SET archived_at = NULL WHERE id = $1\", planId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error archiving plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error archiving plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\trowsAffected, err := res.RowsAffected()\n\tif err != nil {\n\t\tlog.Printf(\"Error getting rows affected: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting rows affected: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif rowsAffected == 0 {\n\t\tlog.Println(\"Plan not found\")\n\t\thttp.Error(w, \"Not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully unarchived plan\", planId)\n}\n\nfunc GetPlanDiffsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetPlanDiffs\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tplain := r.URL.Query().Get(\"plain\") == \"true\"\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\tvar diffs string\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tvar err error\n\t\tdiffs, err = db.GetPlanDiffs(auth.OrgId, planId, plain)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting plan diffs: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting plan diffs: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Printf(\"diffs: %s\", diffs)\n\n\tw.Write([]byte(diffs))\n\n\tlog.Println(\"Successfully retrieved plan diffs\")\n}\n"
  },
  {
    "path": "app/server/handlers/plans_context.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc ListContextHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListContextHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\tvar dbContexts []*db.Context\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"list contexts\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tres, err := db.GetPlanContexts(auth.OrgId, planId, false, false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdbContexts = res\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting contexts: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting contexts: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar apiContexts []*shared.Context\n\n\tfor _, dbContext := range dbContexts {\n\t\tapiContexts = append(apiContexts, dbContext.ToApi())\n\t}\n\n\tbytes, err := json.Marshal(apiContexts)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling contexts: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling contexts: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n}\n\nfunc GetContextBodyHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetContextBodyHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tcontextId := vars[\"contextId\"]\n\tlog.Println(\"planId:\", planId, \"branch:\", branch, \"contextId:\", contextId)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar dbContexts []*db.Context\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"get context body\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tres, err := db.GetPlanContexts(auth.OrgId, planId, true, false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdbContexts = res\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting contexts: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting contexts: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar targetContext *db.Context\n\tfor _, dbContext := range dbContexts {\n\t\tif dbContext.Id == contextId {\n\t\t\ttargetContext = dbContext\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif targetContext == nil {\n\t\thttp.Error(w, \"Context not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tresponse := shared.GetContextBodyResponse{\n\t\tBody: targetContext.Body,\n\t}\n\n\tbytes, err := json.Marshal(response)\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n}\n\nfunc LoadContextHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for LoadContextHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranchName := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.LoadContextRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tres, _ := loadContexts(loadContextsParams{\n\t\tw:          w,\n\t\tr:          r,\n\t\tauth:       auth,\n\t\tloadReq:    &requestBody,\n\t\tplan:       plan,\n\t\tbranchName: branchName,\n\t})\n\n\tif res == nil {\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully processed LoadContextHandler request\")\n\n\tw.Write(bytes)\n}\n\nfunc UpdateContextHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdateContextHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranchName := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.UpdateContextRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar updateRes *shared.UpdateContextResponse\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:          auth.OrgId,\n\t\tUserId:         auth.User.Id,\n\t\tPlanId:         planId,\n\t\tBranch:         branchName,\n\t\tReason:         \"update contexts\",\n\t\tScope:          db.LockScopeWrite,\n\t\tCtx:            ctx,\n\t\tCancelFn:       cancel,\n\t\tClearRepoOnErr: true,\n\t}, func(repo *db.GitRepo) error {\n\t\tvar err error\n\t\tupdateRes, err = db.UpdateContexts(db.UpdateContextsParams{\n\t\t\tReq:        &requestBody,\n\t\t\tOrgId:      auth.OrgId,\n\t\t\tPlan:       plan,\n\t\t\tBranchName: branchName,\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif updateRes.MaxTokensExceeded {\n\t\t\treturn nil\n\t\t}\n\n\t\terr = repo.GitAddAndCommit(branchName, updateRes.Msg)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error committing changes: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error error updating contexts: %v\\n\", err)\n\t\thttp.Error(w, \"Error error updating contexts: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif updateRes.MaxTokensExceeded {\n\t\tlog.Printf(\"The total number of tokens (%d) exceeds the maximum allowed (%d)\", updateRes.TotalTokens, updateRes.MaxTokens)\n\t\tbytes, err := json.Marshal(updateRes)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tw.Write(bytes)\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(updateRes)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully processed UpdateContextHandler request\")\n\n\tw.Write(bytes)\n}\n\nfunc DeleteContextHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for DeleteContextHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranchName := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tbranch, err := db.GetDbBranch(planId, branchName)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting branch: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting branch: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.DeleteContextRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar dbContexts []*db.Context\n\tvar toRemove []*db.Context\n\tvar commitMsg string\n\tremoveTokens := 0\n\tvar toRemoveApiContexts []*shared.Context\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:          auth.OrgId,\n\t\tUserId:         auth.User.Id,\n\t\tPlanId:         planId,\n\t\tBranch:         branchName,\n\t\tReason:         \"delete contexts\",\n\t\tScope:          db.LockScopeWrite,\n\t\tCtx:            ctx,\n\t\tCancelFn:       cancel,\n\t\tClearRepoOnErr: true,\n\t}, func(repo *db.GitRepo) error {\n\t\tvar err error\n\t\tdbContexts, err = db.GetPlanContexts(auth.OrgId, planId, false, false)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting contexts: %v\", err)\n\t\t}\n\n\t\tfor _, dbContext := range dbContexts {\n\t\t\tif _, ok := requestBody.Ids[dbContext.Id]; ok {\n\t\t\t\ttoRemove = append(toRemove, dbContext)\n\t\t\t}\n\t\t}\n\n\t\terr = db.ContextRemove(auth.OrgId, planId, toRemove)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error removing contexts: %v\", err)\n\t\t}\n\n\t\tfor _, dbContext := range toRemove {\n\t\t\ttoRemoveApiContexts = append(toRemoveApiContexts, dbContext.ToApi())\n\t\t\tremoveTokens += dbContext.NumTokens\n\t\t}\n\n\t\tcommitMsg = shared.SummaryForRemoveContext(toRemoveApiContexts, branch.ContextTokens) + \"\\n\\n\" + shared.TableForRemoveContext(toRemoveApiContexts)\n\n\t\terr = repo.GitAddAndCommit(branchName, commitMsg)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error committing changes: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error deleting contexts: %v\\n\", err)\n\t\thttp.Error(w, \"Error deleting contexts: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = db.AddPlanContextTokens(planId, branchName, -removeTokens)\n\tif err != nil {\n\t\tlog.Printf(\"Error updating plan tokens: %v\\n\", err)\n\t\thttp.Error(w, \"Error updating plan tokens: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := shared.DeleteContextResponse{\n\t\tTokensRemoved: removeTokens,\n\t\tTotalTokens:   branch.ContextTokens - removeTokens,\n\t\tMsg:           commitMsg,\n\t}\n\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully deleted contexts\")\n\n\tw.Write(bytes)\n}\n"
  },
  {
    "path": "app/server/handlers/plans_convo.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc ListConvoHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for ListConvoHandler\")\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tvar err error\n\tvar convoMessages []*db.ConvoMessage\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"list convo\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tres, err := db.GetPlanConvo(auth.OrgId, planId)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tconvoMessages = res\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error getting plan convo: \", err)\n\t\thttp.Error(w, \"Error getting plan convo: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tapiConvoMessages := make([]*shared.ConvoMessage, len(convoMessages))\n\tfor i, convoMessage := range convoMessages {\n\t\tapiConvoMessages[i] = convoMessage.ToApi()\n\t}\n\n\tbytes, err := json.Marshal(apiConvoMessages)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling plan convo: \", err)\n\t\thttp.Error(w, \"Error marshalling plan convo: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully processed request for ListConvoHandler\")\n\tw.Write(bytes)\n\n}\n\nfunc GetPlanStatusHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for GetPlanStatusHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar convoMessages []*db.ConvoMessage\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"get plan status\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tres, err := db.GetPlanConvo(auth.OrgId, planId)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tconvoMessages = res\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error getting plan convo: \", err)\n\t\thttp.Error(w, \"Error getting plan convo: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif len(convoMessages) == 0 {\n\t\tlog.Println(\"No messages found for plan\")\n\t\treturn\n\t}\n\n\tconvoMessageIds := make([]string, len(convoMessages))\n\tfor i, convoMessage := range convoMessages {\n\t\tconvoMessageIds[i] = convoMessage.Id\n\t}\n\n\tsummmaries, err := db.GetPlanSummaries(planId, convoMessageIds)\n\n\tif err != nil {\n\t\tlog.Println(\"Error getting plan summaries: \", err)\n\t\thttp.Error(w, \"Error getting plan summaries: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif len(summmaries) == 0 {\n\t\tlog.Println(\"No summaries found for plan\")\n\t\treturn\n\t}\n\n\tlatestSummary := summmaries[len(summmaries)-1]\n\n\tbytes := []byte(latestSummary.Summary)\n\n\tw.Write(bytes)\n\n\tlog.Println(\"Successfully processed request for GetPlanStatusHandler\")\n}\n"
  },
  {
    "path": "app/server/handlers/plans_crud.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc CreatePlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreatePlanHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif !auth.HasPermission(shared.PermissionCreatePlan) {\n\t\tlog.Println(\"User does not have permission to create a plan\")\n\t\thttp.Error(w, \"User does not have permission to create a plan\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tprojectId := vars[\"projectId\"]\n\n\tlog.Println(\"projectId: \", projectId)\n\n\tif !authorizeProject(w, projectId, auth) {\n\t\treturn\n\t}\n\n\t_, apiErr := hooks.ExecHook(hooks.WillCreatePlan, hooks.HookParams{Auth: auth})\n\tif apiErr != nil {\n\t\twriteApiError(w, *apiErr)\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.CreatePlanRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tname := requestBody.Name\n\tif name == \"\" {\n\t\tname = \"draft\"\n\t}\n\n\tif name == \"draft\" {\n\t\t// delete any existing draft plans\n\t\terr = db.DeleteDraftPlans(auth.OrgId, projectId, auth.User.Id)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error deleting draft plans: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error deleting draft plans: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\ti := 2\n\t\toriginalName := name\n\t\tfor {\n\t\t\tvar count int\n\t\t\terr := db.Conn.Get(&count, \"SELECT COUNT(*) FROM plans WHERE project_id = $1 AND owner_id = $2 AND name = $3\", projectId, auth.User.Id, name)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error checking if plan exists: %v\\n\", err)\n\t\t\t\thttp.Error(w, \"Error checking if plan exists: \"+err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif count == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tname = originalName + \".\" + fmt.Sprint(i)\n\t\t\ti++\n\t\t}\n\t}\n\n\tplan, err := db.CreatePlan(r.Context(), auth.OrgId, projectId, auth.User.Id, name)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error creating plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error creating plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresp := shared.CreatePlanResponse{\n\t\tId:   plan.Id,\n\t\tName: plan.Name,\n\t}\n\n\tbytes, err := json.Marshal(resp)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Printf(\"Successfully created plan: %v\\n\", plan)\n}\n\nfunc GetPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetPlanHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(plan)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n}\n\nfunc RenamePlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for RenamePlanHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tvar requestBody shared.RenamePlanRequest\n\tif err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif plan.OwnerId != auth.User.Id {\n\t\tlog.Println(\"Only the plan owner can rename a plan\")\n\t\thttp.Error(w, \"Only the plan owner can rename a plan\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\tif requestBody.Name == \"\" {\n\t\tlog.Println(\"Name cannot be empty\")\n\t\thttp.Error(w, \"Name cannot be empty\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\terr := db.RenamePlan(planId, requestBody.Name, nil)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error renaming plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error renaming plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully renamed plan\")\n}\n\nfunc DeletePlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for DeletePlanHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlanDelete(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tif plan.OwnerId != auth.User.Id {\n\t\tlog.Println(\"Only the plan owner can delete a plan\")\n\t\thttp.Error(w, \"Only the plan owner can delete a plan\", http.StatusForbidden)\n\t\treturn\n\t}\n\n\tres, err := db.Conn.Exec(\"DELETE FROM plans WHERE id = $1\", planId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error deleting plan: %v\\n\", err)\n\t\thttp.Error(w, \"Error deleting plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\trowsAffected, err := res.RowsAffected()\n\tif err != nil {\n\t\tlog.Printf(\"Error getting rows affected: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting rows affected: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif rowsAffected == 0 {\n\t\tlog.Println(\"Plan not found\")\n\t\thttp.Error(w, \"Not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\terr = db.DeletePlanDir(auth.OrgId, planId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error deleting plan dir: %v\\n\", err)\n\t\thttp.Error(w, \"Error deleting plan dir: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully deleted plan\", planId)\n}\n\nfunc DeleteAllPlansHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for DeleteAllPlansHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tprojectId := vars[\"projectId\"]\n\n\tlog.Println(\"projectId: \", projectId)\n\n\tif !authorizeProject(w, projectId, auth) {\n\t\treturn\n\t}\n\n\terr := db.DeleteOwnerPlans(auth.OrgId, projectId, auth.User.Id)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error deleting plans: %v\\n\", err)\n\t\thttp.Error(w, \"Error deleting plans: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully deleted all plans\")\n}\n\nfunc ListPlansHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListPlans\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tprojectIds := r.URL.Query()[\"projectId\"]\n\n\tlog.Println(\"projectIds: \", projectIds)\n\n\tvar apiPlans []*shared.Plan\n\n\twritePlans := func() {\n\t\tjsonBytes, err := json.Marshal(apiPlans)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error marshalling plans: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error marshalling plans: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tw.Write(jsonBytes)\n\t}\n\n\tif len(projectIds) == 0 {\n\t\twritePlans()\n\t\treturn\n\t}\n\n\tauthorizedProjectIds := []string{}\n\tfor _, projectId := range projectIds {\n\t\tif authorizeProjectOptional(w, projectId, auth, false) {\n\t\t\tauthorizedProjectIds = append(authorizedProjectIds, projectId)\n\t\t}\n\t}\n\n\tif len(authorizedProjectIds) == 0 {\n\t\twritePlans()\n\t\treturn\n\t}\n\n\tplans, err := db.ListOwnedPlans(authorizedProjectIds, auth.User.Id, false)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing plans: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing plans: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfor _, plan := range plans {\n\t\tapiPlans = append(apiPlans, plan.ToApi())\n\t}\n\n\twritePlans()\n}\n\nfunc ListArchivedPlansHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListArchivedPlansHandler\")\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tprojectIds := r.URL.Query()[\"projectId\"]\n\n\tlog.Println(\"projectIds: \", projectIds)\n\n\tvar apiPlans []*shared.Plan\n\n\twritePlans := func() {\n\t\tjsonBytes, err := json.Marshal(apiPlans)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error marshalling plans: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error marshalling plans: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tlog.Println(\"Successfully processed ListArchivedPlansHandler request\")\n\n\t\tw.Write(jsonBytes)\n\t}\n\n\tif len(projectIds) == 0 {\n\t\twritePlans()\n\t\treturn\n\t}\n\n\tauthorizedProjectIds := []string{}\n\tfor _, projectId := range projectIds {\n\t\tif authorizeProjectOptional(w, projectId, auth, false) {\n\t\t\tauthorizedProjectIds = append(authorizedProjectIds, projectId)\n\t\t}\n\t}\n\n\tplans, err := db.ListOwnedPlans(authorizedProjectIds, auth.User.Id, true)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing plans: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing plans: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tfor _, plan := range plans {\n\t\tapiPlans = append(apiPlans, plan.ToApi())\n\t}\n\n\twritePlans()\n}\n\nfunc ListPlansRunningHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListPlansRunningHandler\")\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tprojectIds := r.URL.Query()[\"projectId\"]\n\tincludeRecent := r.URL.Query().Get(\"recent\") == \"true\"\n\n\tlog.Println(\"projectIds: \", projectIds)\n\n\tif len(projectIds) == 0 {\n\t\tlog.Println(\"No project ids provided\")\n\t\thttp.Error(w, \"No project ids provided\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tfor _, projectId := range projectIds {\n\t\tif !authorizeProject(w, projectId, auth) {\n\t\t\treturn\n\t\t}\n\t}\n\n\tplans, err := db.ListOwnedPlans(projectIds, auth.User.Id, false)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing plans: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing plans: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar planIds []string\n\tfor _, plan := range plans {\n\t\tplanIds = append(planIds, plan.Id)\n\t}\n\n\terrCh := make(chan error, 2)\n\tvar streams []*db.ModelStream\n\tvar branches []*db.Branch\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\n\t\tvar err error\n\t\tif includeRecent {\n\t\t\tstreams, err = db.GetActiveOrRecentModelStreams(planIds)\n\t\t} else {\n\t\t\tstreams, err = db.GetActiveModelStreams(planIds)\n\t\t}\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting recent model streams: %v\", err)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- fmt.Errorf(\"panic in ListPlansRunningHandler: %v\\n%s\", r, debug.Stack())\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\t\tvar err error\n\t\tbranches, err = db.ListBranchesForPlans(auth.OrgId, planIds)\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\"error getting branches: %v\", err)\n\t\t\treturn\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tlog.Println(err)\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n\n\tres := shared.ListPlansRunningResponse{\n\t\tBranches:                   []*shared.Branch{},\n\t\tStreamStartedAtByBranchId:  map[string]time.Time{},\n\t\tStreamFinishedAtByBranchId: map[string]time.Time{},\n\t\tPlansById:                  map[string]*shared.Plan{},\n\t\tStreamIdByBranchId:         map[string]string{},\n\t}\n\n\tvar apiPlansById = make(map[string]*shared.Plan)\n\tfor _, plan := range plans {\n\t\tapiPlan := plan.ToApi()\n\t\tapiPlansById[plan.Id] = apiPlan\n\t}\n\n\tvar apiBranchesByComposite = make(map[string]*shared.Branch)\n\tfor _, branch := range branches {\n\t\tapiBranch := branch.ToApi()\n\t\tapiBranchesByComposite[branch.PlanId+\"|\"+branch.Name] = apiBranch\n\t}\n\n\taddedBranches := make(map[string]bool)\n\tfor _, stream := range streams {\n\t\tbranchComposite := stream.PlanId + \"|\" + stream.Branch\n\t\tapiBranch, ok := apiBranchesByComposite[branchComposite]\n\t\tif !ok {\n\t\t\tlog.Printf(\"Stream %s has no branch\\n\", stream.Id)\n\t\t\thttp.Error(w, \"Stream has no branch\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tapiPlan, ok := apiPlansById[stream.PlanId]\n\t\tif !ok {\n\t\t\tlog.Printf(\"Stream %s has no plan\\n\", stream.Id)\n\t\t\thttp.Error(w, \"Stream has no plan\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif !addedBranches[branchComposite] {\n\t\t\tres.Branches = append(res.Branches, apiBranch)\n\t\t\taddedBranches[branchComposite] = true\n\t\t}\n\n\t\tres.StreamStartedAtByBranchId[apiBranch.Id] = stream.CreatedAt\n\t\tif stream.FinishedAt != nil {\n\t\t\tres.StreamFinishedAtByBranchId[apiBranch.Id] = *stream.FinishedAt\n\t\t}\n\t\tres.StreamIdByBranchId[apiBranch.Id] = stream.Id\n\n\t\tres.PlansById[stream.PlanId] = apiPlan\n\t}\n\n\tsort.Slice(res.Branches, func(i, j int) bool {\n\t\tiComposite := res.Branches[i].PlanId + \"|\" + res.Branches[i].Name\n\t\tjComposite := res.Branches[j].PlanId + \"|\" + res.Branches[j].Name\n\t\tiFinishedAt, iOk := res.StreamFinishedAtByBranchId[iComposite]\n\t\tjFinishedAt, jOk := res.StreamFinishedAtByBranchId[jComposite]\n\t\tiCreatedAt := res.StreamStartedAtByBranchId[iComposite]\n\t\tjCreatedAt := res.StreamStartedAtByBranchId[jComposite]\n\n\t\tif iOk && jOk {\n\t\t\treturn iFinishedAt.Before(jFinishedAt) // Sort finished streams by finishedAt in ascending order.\n\t\t}\n\t\tif iOk {\n\t\t\treturn false // Place i after j if i is finished and j is not.\n\t\t}\n\t\tif jOk {\n\t\t\treturn true // Place i before j if i is not finished and j is.\n\t\t}\n\t\treturn iCreatedAt.Before(jCreatedAt) // Sort by createdAt in ascending order if both are unfinished.\n\t})\n\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully processed ListPlansRunningHandler request\")\n\n\tw.Write(bytes)\n}\n\nfunc GetCurrentBranchByPlanIdHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CurrentBranchByPlanIdHandler\")\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tprojectId := vars[\"projectId\"]\n\n\tlog.Println(\"projectId: \", projectId)\n\n\tif !authorizeProject(w, projectId, auth) {\n\t\treturn\n\t}\n\n\tvar req shared.GetCurrentBranchByPlanIdRequest\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tplans, err := db.ListOwnedPlans([]string{projectId}, auth.User.Id, false)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing plans: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing plans: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif len(plans) == 0 {\n\t\tlog.Println(\"No plans found\")\n\t\thttp.Error(w, \"No plans found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tquery := \"SELECT * FROM branches WHERE \"\n\n\tvar orConditions []string\n\tvar queryArgs []interface{}\n\tcurrentArg := 1\n\tfor _, plan := range plans {\n\t\tbranchName, ok := req.CurrentBranchByPlanId[plan.Id]\n\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\torConditions = append(orConditions, fmt.Sprintf(\"(plan_id = $%d AND name = $%d)\", currentArg, currentArg+1))\n\t\tqueryArgs = append(queryArgs, plan.Id, branchName)\n\n\t\tcurrentArg += 2\n\t}\n\n\tquery += \"(\" + strings.Join(orConditions, \" OR \") + \") AND archived_at IS NULL AND deleted_at IS NULL\"\n\n\tvar branches []db.Branch\n\terr = db.Conn.Select(&branches, query, queryArgs...)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting branches: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting branches: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := map[string]*shared.Branch{}\n\tfor _, branch := range branches {\n\t\tres[branch.PlanId] = branch.ToApi()\n\t}\n\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling branches: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling branches: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully processed GetCurrentBranchByPlanIdHandler request\")\n\n\tw.Write(bytes)\n}\n"
  },
  {
    "path": "app/server/handlers/plans_exec.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/host\"\n\tmodelPlan \"plandex-server/model/plan\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc TellPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for TellPlanHandler\", \"ip:\", host.Ip)\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tplan := authorizePlanExecUpdate(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tsettings, err := db.GetPlanSettings(plan)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting plan settings: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting plan settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error reading request body: %v\", err))\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer func() {\n\t\tlog.Println(\"Closing request body\")\n\t\tr.Body.Close()\n\t}()\n\n\tvar requestBody shared.TellPlanRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error parsing request body: %v\", err))\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t_, apiErr := hooks.ExecHook(hooks.WillTellPlan, hooks.HookParams{\n\t\tAuth: auth,\n\t\tPlan: plan,\n\t})\n\tif apiErr != nil {\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error executing will tell plan hook: %v\", apiErr))\n\t\twriteApiError(w, *apiErr)\n\t\treturn\n\t}\n\n\torgUserConfig, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org user config: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org user config\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := initClients(\n\t\tinitClientsParams{\n\t\t\tw:             w,\n\t\t\tauth:          auth,\n\t\t\tapiKeys:       requestBody.ApiKeys,\n\t\t\topenAIOrgId:   requestBody.OpenAIOrgId,\n\t\t\tauthVars:      requestBody.AuthVars,\n\t\t\tplan:          plan,\n\t\t\tsettings:      settings,\n\t\t\torgUserConfig: orgUserConfig,\n\t\t},\n\t)\n\terr = modelPlan.Tell(modelPlan.TellParams{\n\t\tClients:  res.clients,\n\t\tPlan:     plan,\n\t\tBranch:   branch,\n\t\tAuth:     auth,\n\t\tReq:      &requestBody,\n\t\tAuthVars: res.authVars,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error telling plan: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error telling plan: %v\", err))\n\t\thttp.Error(w, \"Error telling plan: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif requestBody.ConnectStream {\n\t\tstartResponseStream(r.Context(), w, auth, planId, branch, false)\n\t}\n\n\tlog.Println(\"Successfully processed request for TellPlanHandler\")\n}\n\nfunc BuildPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for BuildPlanHandler\", \"ip:\", host.Ip)\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId)\n\tplan := authorizePlanExecUpdate(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tsettings, err := db.GetPlanSettings(plan)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting plan settings: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting plan settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error reading request body: %v\", err))\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer func() {\n\t\tlog.Println(\"Closing request body\")\n\t\tr.Body.Close()\n\t}()\n\n\tvar requestBody shared.BuildPlanRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error parsing request body: %v\", err))\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\torgUserConfig, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org user config: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org user config\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := initClients(\n\t\tinitClientsParams{\n\t\t\tw:             w,\n\t\t\tauth:          auth,\n\t\t\tapiKeys:       requestBody.ApiKeys,\n\t\t\topenAIOrgId:   requestBody.OpenAIOrgId,\n\t\t\tauthVars:      requestBody.AuthVars,\n\t\t\tplan:          plan,\n\t\t\tsettings:      settings,\n\t\t\torgUserConfig: orgUserConfig,\n\t\t},\n\t)\n\tnumBuilds, err := modelPlan.Build(modelPlan.BuildParams{\n\t\tClients:       res.clients,\n\t\tAuthVars:      res.authVars,\n\t\tPlan:          plan,\n\t\tBranch:        branch,\n\t\tAuth:          auth,\n\t\tSessionId:     requestBody.SessionId,\n\t\tOrgUserConfig: orgUserConfig,\n\t\tSettings:      settings,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error building plan: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error building plan: %v\", err))\n\t\thttp.Error(w, \"Error building plan\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif numBuilds == 0 {\n\t\tlog.Println(\"No builds were executed\")\n\t\tgo notify.NotifyErr(notify.SeverityInfo, fmt.Errorf(\"no builds were executed\"))\n\t\thttp.Error(w, shared.NoBuildsErr, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tif requestBody.ConnectStream {\n\t\tstartResponseStream(r.Context(), w, auth, planId, branch, false)\n\t}\n\n\tlog.Println(\"Successfully processed request for BuildPlanHandler\")\n}\n\nfunc ConnectPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ConnectPlanHandler\", \"ip:\", host.Ip)\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId)\n\tlog.Println(\"branch: \", branch)\n\tactive := modelPlan.GetActivePlan(planId, branch)\n\tisProxy := r.URL.Query().Get(\"proxy\") == \"true\"\n\n\tif active == nil {\n\t\tif isProxy {\n\t\t\tlog.Println(\"No active plan on proxied request\")\n\t\t\tgo notify.NotifyErr(notify.SeverityInfo, fmt.Errorf(\"no active plan on proxied request\"))\n\t\t\thttp.Error(w, \"No active plan\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tlog.Println(\"No active plan -- proxying request\")\n\n\t\tproxyActivePlanMethod(w, r, planId, branch, \"connect\")\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\tlog.Println(\"No auth\")\n\t\treturn\n\t}\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\tlog.Println(\"No plan\")\n\t\treturn\n\t}\n\n\tstartResponseStream(r.Context(), w, auth, planId, branch, true)\n\n\tlog.Println(\"Successfully processed request for ConnectPlanHandler\")\n}\n\nfunc StopPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for StopPlanHandler\", \"ip:\", host.Ip)\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId)\n\tlog.Println(\"branch: \", branch)\n\tactive := modelPlan.GetActivePlan(planId, branch)\n\tisProxy := r.URL.Query().Get(\"proxy\") == \"true\"\n\n\tif active == nil {\n\t\tif isProxy {\n\t\t\tlog.Println(\"No active plan on proxied request\")\n\t\t\thttp.Error(w, \"No active plan\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t\tproxyActivePlanMethod(w, r, planId, branch, \"stop\")\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tlog.Println(\"Sending stream aborted message to client\")\n\n\tactive.Stream(shared.StreamMessage{\n\t\tType: shared.StreamMessageAborted,\n\t})\n\n\t// give some time for stream message to be processed before canceling\n\tlog.Println(\"Sleeping for 100ms before canceling\")\n\ttime.Sleep(100 * time.Millisecond)\n\n\tvar err error\n\tctx, cancel := context.WithCancel(r.Context())\n\n\t// this is here to ensure that the plan is stopped even if the db operation fails\n\tdefer func() {\n\t\terr = modelPlan.Stop(planId, branch, auth.User.Id, auth.OrgId)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error stopping plan: %v\\n\", err)\n\t\t}\n\n\t\tlog.Println(\"Successfully processed request for StopPlanHandler\")\n\t}()\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"stop plan\",\n\t\tScope:    db.LockScopeWrite,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tlog.Println(\"Stopping plan - storing partial reply\")\n\t\terr = modelPlan.StorePartialReply(repo, planId, branch, auth.User.Id, auth.OrgId)\n\t\treturn err\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error storing partial reply: %v\\n\", err)\n\t\thttp.Error(w, \"Error storing partial reply\", http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc RespondMissingFileHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for RespondMissingFileHandler\", \"ip:\", host.Ip)\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId)\n\tlog.Println(\"branch: \", branch)\n\tisProxy := r.URL.Query().Get(\"proxy\") == \"true\"\n\n\tactive := modelPlan.GetActivePlan(planId, branch)\n\tif active == nil {\n\t\tif isProxy {\n\t\t\tlog.Println(\"No active plan on proxied request\")\n\t\t\thttp.Error(w, \"No active plan\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tproxyActivePlanMethod(w, r, planId, branch, \"respond_missing_file\")\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.RespondMissingFileRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tlog.Println(\"missing file choice:\", requestBody.Choice)\n\n\tif requestBody.Choice == shared.RespondMissingFileChoiceLoad {\n\t\tlog.Println(\"loading missing file\")\n\t\tres, dbContexts := loadContexts(loadContextsParams{\n\t\t\tw:    w,\n\t\t\tr:    r,\n\t\t\tauth: auth,\n\t\t\tloadReq: &shared.LoadContextRequest{\n\t\t\t\t&shared.LoadContextParams{\n\t\t\t\t\tContextType: shared.ContextFileType,\n\t\t\t\t\tName:        requestBody.FilePath,\n\t\t\t\t\tFilePath:    requestBody.FilePath,\n\t\t\t\t\tBody:        requestBody.Body,\n\t\t\t\t},\n\t\t\t},\n\t\t\tplan:       plan,\n\t\t\tbranchName: branch,\n\t\t\tautoLoaded: true,\n\t\t})\n\t\tif res == nil {\n\t\t\treturn\n\t\t}\n\n\t\tdbContext := dbContexts[0]\n\n\t\tlog.Println(\"loaded missing file:\", dbContext.FilePath)\n\n\t\tmodelPlan.UpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {\n\t\t\tif activePlan == nil {\n\t\t\t\tlog.Println(\"Active plan is nil\")\n\t\t\t\thttp.Error(w, \"Active plan is nil\", http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tactivePlan.Contexts = append(activePlan.Contexts, dbContext)\n\t\t\tactivePlan.ContextsByPath[dbContext.FilePath] = dbContext\n\t\t})\n\t}\n\n\t// This will resume model stream\n\tlog.Println(\"Resuming model stream\")\n\tactive.MissingFileResponseCh <- requestBody.Choice\n\n\tlog.Println(\"Successfully processed request for RespondMissingFileHandler\")\n}\n\nfunc AutoLoadContextHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for AutoLoadContextHandler\", \"ip:\", host.Ip)\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\tlog.Println(\"planId: \", planId)\n\tlog.Println(\"branch: \", branch)\n\n\tisProxy := r.URL.Query().Get(\"proxy\") == \"true\"\n\n\tactive := modelPlan.GetActivePlan(planId, branch)\n\tif active == nil {\n\t\tif isProxy {\n\t\t\tlog.Println(\"No active plan on proxied request\")\n\t\t\thttp.Error(w, \"No active plan\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tproxyActivePlanMethod(w, r, planId, branch, \"auto_load_context\")\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tvar err error\n\tdefer func() {\n\t\tif err == nil {\n\t\t\tactive.AutoLoadContextCh <- struct{}{}\n\t\t} else {\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"Error in AutoLoadContextHandler: \" + err.Error(),\n\t\t\t}\n\t\t}\n\t}()\n\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.LoadContextRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tlog.Println(\"AutoLoadContextHandler - loading contexts\")\n\n\tvar res *shared.LoadContextResponse\n\tvar dbContexts []*db.Context\n\tif len(requestBody) > 0 {\n\t\tres, dbContexts = loadContexts(loadContextsParams{\n\t\t\tw:          w,\n\t\t\tr:          r,\n\t\t\tauth:       auth,\n\t\t\tloadReq:    &requestBody,\n\t\t\tplan:       plan,\n\t\t\tbranchName: branch,\n\t\t\tautoLoaded: true,\n\t\t})\n\t}\n\n\tif res == nil {\n\t\t// the client will treat this as a no-op\n\t\tmarkdownRes := shared.LoadContextResponse{\n\t\t\tTokensAdded:       0,\n\t\t\tTotalTokens:       0,\n\t\t\tMaxTokensExceeded: false,\n\t\t\tMaxTokens:         0,\n\t\t\tMsg:               \"\",\n\t\t}\n\n\t\tbytes, err := json.Marshal(markdownRes)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tw.Write(bytes)\n\t\treturn\n\t}\n\n\tlog.Println(\"AutoLoadContextHandler - updating active plan\")\n\n\tmodelPlan.UpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {\n\t\tif activePlan == nil {\n\t\t\tlog.Println(\"Active plan is nil\")\n\t\t\thttp.Error(w, \"Active plan is nil\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tactivePlan.Contexts = append(activePlan.Contexts, dbContexts...)\n\t\tfor _, dbContext := range dbContexts {\n\t\t\tactivePlan.ContextsByPath[dbContext.FilePath] = dbContext\n\t\t}\n\t})\n\n\tlog.Println(\"AutoLoadContextHandler - updated active plan\")\n\n\tvar apiContexts []*shared.Context\n\tfor _, dbContext := range dbContexts {\n\t\tapiContexts = append(apiContexts, dbContext.ToApi())\n\t}\n\n\tmsg := shared.SummaryForLoadContext(apiContexts, res.TokensAdded, res.TotalTokens)\n\tmsg += \"\\n\\n\" + shared.TableForLoadContext(apiContexts, true)\n\n\tmarkdownRes := shared.LoadContextResponse{\n\t\tTokensAdded:       res.TokensAdded,\n\t\tTotalTokens:       res.TotalTokens,\n\t\tMaxTokensExceeded: res.MaxTokensExceeded,\n\t\tMaxTokens:         res.MaxTokens,\n\t\tMsg:               msg,\n\t}\n\n\tbytes, err := json.Marshal(markdownRes)\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"Successfully processed request for AutoLoadContextHandler\")\n}\n\nfunc GetBuildStatusHandler(w http.ResponseWriter, r *http.Request) {\n\t// logs are too chatty on this function, uncomment if you need to debug\n\t// log.Println(\"Received request for GetBuildStatusHandler\", \"ip:\", host.Ip)\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tisProxy := r.URL.Query().Get(\"proxy\") == \"true\"\n\n\tactive := modelPlan.GetActivePlan(planId, branch)\n\tif active == nil {\n\t\tif isProxy {\n\t\t\tlog.Println(\"No active plan on proxied request\")\n\t\t\thttp.Error(w, \"No active plan\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tproxyActivePlanMethod(w, r, planId, branch, \"auto_load_context\")\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tresponse := shared.GetBuildStatusResponse{\n\t\tBuiltFiles:       active.BuiltFiles,\n\t\tIsBuildingByPath: active.IsBuildingByPath,\n\t}\n\n\tbytes, err := json.Marshal(response)\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\t// log.Println(\"Successfully processed request for GetBuildStatusHandler\")\n}\n\nfunc authorizePlanExecUpdate(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn nil\n\t}\n\n\tif plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionUpdateAnyPlan) {\n\t\tlog.Println(\"User does not have permission to update plan\")\n\t\thttp.Error(w, \"User does not have permission to update plan\", http.StatusForbidden)\n\t\treturn nil\n\t}\n\n\treturn plan\n}\n"
  },
  {
    "path": "app/server/handlers/plans_versions.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc ListLogsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListLogsHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar body string\n\tvar shas []string\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"list logs\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tvar err error\n\t\tbody, shas, err = repo.GetGitCommitHistory(branch)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error getting logs: \", err)\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := shared.LogResponse{\n\t\tBody: body,\n\t\tShas: shas,\n\t}\n\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling logs: \", err)\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"Successfully processed request for ListLogsHandler\")\n}\n\nfunc RewindPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for RewindPlanHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId)\n\n\tif authorizePlan(w, planId, auth) == nil {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.RewindPlanRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"rewind plan\",\n\t\tScope:    db.LockScopeWrite,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\treturn repo.GitRewindToSha(branch, requestBody.Sha)\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error rewinding plan: \", err)\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = db.SyncPlanTokens(auth.OrgId, planId, branch)\n\n\tif err != nil {\n\t\tlog.Println(\"Error syncing plan tokens: \", err)\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar sha string\n\tvar latest string\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"get latest commit\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tsha, latest, err = repo.GetLatestCommit(branch)\n\t\treturn err\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error getting latest commit: \", err)\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := shared.RewindPlanResponse{\n\t\tLatestSha:    sha,\n\t\tLatestCommit: latest,\n\t}\n\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling response: \", err)\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"Successfully processed request for RewindPlanHandler\")\n}\n"
  },
  {
    "path": "app/server/handlers/projects.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc CreateProjectHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateProjectHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.CreateProjectRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif requestBody.Name == \"\" {\n\t\tlog.Println(\"Received empty name field\")\n\t\thttp.Error(w, \"name field is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar projectId string\n\terr = db.WithTx(r.Context(), \"create project\", func(tx *sqlx.Tx) error {\n\t\tvar err error\n\n\t\tprojectId, err = db.CreateProject(auth.OrgId, requestBody.Name, tx)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error creating project: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error creating project: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error creating project: %v\\n\", err)\n\t\thttp.Error(w, \"Error creating project: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tresp := shared.CreateProjectResponse{\n\t\tId: projectId,\n\t}\n\n\tbytes, err := json.Marshal(resp)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"Successfully created project\", projectId)\n}\n\nfunc ListProjectsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for ListProjectsHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\trows, err := db.Conn.Query(\"SELECT id, name FROM projects WHERE org_id = $1\", auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error listing projects: %v\\n\", err)\n\t\thttp.Error(w, \"Error listing projects: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar projects []shared.Project\n\n\tfor rows.Next() {\n\t\tvar project shared.Project\n\t\terr := rows.Scan(&project.Id, &project.Name)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error scanning project: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error scanning project: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tprojects = append(projects, project)\n\t}\n\n\tbytes, err := json.Marshal(projects)\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling projects: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling projects: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n}\n\nfunc ProjectSetPlanHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdateProjectSetPlanHandler\")\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tprojectId := vars[\"projectId\"]\n\n\tlog.Println(\"projectId: \", projectId)\n\n\tif !authorizeProject(w, projectId, auth) {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.SetProjectPlanRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif requestBody.PlanId == \"\" {\n\t\tlog.Println(\"Received empty planId field\")\n\t\thttp.Error(w, \"planId field is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// update statement here -- need auth / current user id\n\n\tlog.Println(\"Successfully set project plan\", projectId)\n}\n\nfunc RenameProjectHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for RenameProjectHandler\")\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tprojectId := vars[\"projectId\"]\n\n\tlog.Println(\"projectId: \", projectId)\n\n\tif !authorizeProjectRename(w, projectId, auth) {\n\t\treturn\n\t}\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer r.Body.Close()\n\n\tvar requestBody shared.RenameProjectRequest\n\tif err := json.Unmarshal(body, &requestBody); err != nil {\n\t\tlog.Printf(\"Error parsing request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error parsing request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif requestBody.Name == \"\" {\n\t\tlog.Println(\"Received empty name field\")\n\t\thttp.Error(w, \"name field is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tres, err := db.Conn.Exec(\"UPDATE projects SET name = $1 WHERE id = $2\", requestBody.Name, projectId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error updating project: %v\\n\", err)\n\t\thttp.Error(w, \"Error updating project: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\trowsAffected, err := res.RowsAffected()\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting rows affected: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting rows affected: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif rowsAffected == 0 {\n\t\tlog.Printf(\"Project not found: %v\\n\", projectId)\n\t\thttp.Error(w, \"Project not found: \"+projectId, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully renamed project\", projectId)\n\n}\n"
  },
  {
    "path": "app/server/handlers/proxy_helper.go",
    "content": "package handlers\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/host\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc proxyActivePlanMethod(w http.ResponseWriter, r *http.Request, planId, branch, method string) {\n\tmodelStream, err := db.GetActiveModelStream(planId, branch)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting active model stream: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting active model stream\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif modelStream == nil {\n\t\tlog.Printf(\"No active model stream for plan %s\\n\", planId)\n\t\thttp.Error(w, \"No active model stream for plan\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tif modelStream.InternalIp == host.Ip {\n\t\t// No active plan for this plan or else we wouldn't be calling proxyActivePlanMethod -- set the model stream to finished because something went wrong\n\t\terr := db.SetModelStreamFinished(modelStream.Id)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error setting model stream %s to finished: %v\\n\", modelStream.Id, err)\n\t\t}\n\n\t\terr = db.SetPlanStatus(planId, branch, shared.PlanStatusError, \"No active stream for plan\")\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error setting plan %s status to error: %v\\n\", planId, err)\n\t\t}\n\n\t\tlog.Printf(\"No active plan for plan %s\\n\", planId)\n\t\thttp.Error(w, \"No active plan for plan\", http.StatusNotFound)\n\t\treturn\n\t} else {\n\t\tlog.Printf(\"Forwarding request to %s\\n\", modelStream.InternalIp)\n\t\tproxyUrl := fmt.Sprintf(\"http://%s:%s/plans/%s/%s/%s\", modelStream.InternalIp, os.Getenv(\"PORT\"), planId, branch, method)\n\t\tproxyUrl += \"?proxy=true\"\n\n\t\tlog.Printf(\"Proxy url: %s\\n\", proxyUrl)\n\t\tproxyRequest(w, r, proxyUrl)\n\t\treturn\n\t}\n}\n\nfunc proxyRequest(w http.ResponseWriter, originalRequest *http.Request, url string) {\n\tclient := &http.Client{\n\t\tTimeout: time.Second * 10,\n\t}\n\n\t// Create a new request based on the original request\n\treq, err := http.NewRequestWithContext(originalRequest.Context(), originalRequest.Method, url, originalRequest.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error creating request for proxy: %v\\n\", err)\n\t\thttp.Error(w, \"Error creating request for proxy\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// Copy the headers from the original request to the new request\n\tfor name, headers := range originalRequest.Header {\n\t\tfor _, h := range headers {\n\t\t\treq.Header.Add(name, h)\n\t\t}\n\t}\n\n\t// Copy the body from the original request to the new request if it's a POST or PUT\n\tif originalRequest.Method == http.MethodPost || originalRequest.Method == http.MethodPut {\n\t\treq.Body = originalRequest.Body\n\t}\n\n\t// Make the request\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Printf(\"Error forwarding request: %v\\n\", err)\n\t\thttp.Error(w, \"Error forwarding request\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// Copy the response headers and status code\n\tfor name, headers := range resp.Header {\n\t\tfor _, h := range headers {\n\t\t\tw.Header().Add(name, h)\n\t\t}\n\t}\n\tw.WriteHeader(resp.StatusCode)\n\n\tlog.Printf(\"Proxy forwarded successfully with status code: %d\\n\", resp.StatusCode)\n\n\t// Copy the response body\n\tif _, err := io.Copy(w, resp.Body); err != nil {\n\t\tlog.Printf(\"Error copying response body: %v\\n\", err)\n\t\thttp.Error(w, \"Error copying response body\", http.StatusInternalServerError)\n\t}\n}\n"
  },
  {
    "path": "app/server/handlers/sessions.go",
    "content": "package handlers\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/email\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc CreateEmailVerificationHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateEmailVerificationHandler\")\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar req shared.CreateEmailVerificationRequest\n\terr = json.Unmarshal(body, &req)\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling request: %v\\n\", err)\n\t\thttp.Error(w, \"Error unmarshalling request: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\treq.Email = strings.ToLower(req.Email)\n\n\tvar hasAccount bool\n\tif req.UserId == \"\" {\n\t\tuser, err := db.GetUserByEmail(req.Email)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting user: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error getting user: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\thasAccount = user != nil\n\t} else {\n\t\thasAccount = true\n\n\t\tuser, err := db.GetUser(req.UserId)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting user: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error getting user: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif user == nil {\n\t\t\tlog.Printf(\"User not found for id: %v\\n\", req.UserId)\n\t\t\thttp.Error(w, \"User not found\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tif user.Email != req.Email {\n\t\t\tlog.Printf(\"User email does not match for id: %v\\n\", req.UserId)\n\t\t\thttp.Error(w, \"User email does not match\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t}\n\n\tif req.RequireUser && !hasAccount {\n\t\tlog.Printf(\"User not found for email: %v\\n\", req.Email)\n\t\thttp.Error(w, \"User not found\", http.StatusNotFound)\n\t\treturn\n\t} else if req.RequireNoUser && hasAccount {\n\t\tlog.Printf(\"User already exists for email: %v\\n\", req.Email)\n\t\thttp.Error(w, \"User already exists\", http.StatusConflict)\n\t\treturn\n\t}\n\n\tvar res shared.CreateEmailVerificationResponse\n\n\tif !(os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\") {\n\t\t// create pin - 6 alphanumeric characters\n\t\tpinBytes, err := shared.GetRandomAlphanumeric(6)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error generating random pin: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error generating random pin: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// get sha256 hash of pin\n\t\thashBytes := sha256.Sum256(pinBytes)\n\t\tpinHash := hex.EncodeToString(hashBytes[:])\n\n\t\t// create verification\n\t\terr = db.CreateEmailVerification(req.Email, req.UserId, pinHash)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error creating email verification: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error creating email verification: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\terr = email.SendVerificationEmail(req.Email, string(pinBytes))\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error sending verification email: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error sending verification email: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tres = shared.CreateEmailVerificationResponse{\n\t\t\tHasAccount: hasAccount,\n\t\t}\n\t} else {\n\t\tres = shared.CreateEmailVerificationResponse{\n\t\t\tHasAccount:  hasAccount,\n\t\t\tIsLocalMode: true,\n\t\t}\n\t}\n\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully created email verification\")\n\n\tw.Write(bytes)\n}\n\nfunc CheckEmailPinHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for VerifyEmailPinHandler\")\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar req shared.VerifyEmailPinRequest\n\terr = json.Unmarshal(body, &req)\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling request: %v\\n\", err)\n\t\thttp.Error(w, \"Error unmarshalling request: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\treq.Email = strings.ToLower(req.Email)\n\n\t_, err = db.ValidateEmailVerification(req.Email, req.Pin)\n\n\tif err != nil {\n\t\tif err.Error() == db.InvalidOrExpiredPinError {\n\t\t\thttp.Error(w, \"Invalid or expired pin\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tlog.Printf(\"Error validating email verification: %v\\n\", err)\n\t\thttp.Error(w, \"Error validating email verification: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully verified email pin\")\n}\n\n// sign in codes allow users to authenticate between different clients\n// like UI to CLI or vice versa\nfunc CreateSignInCodeHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for CreateSignInCodeHandler\")\n\n\tauth := Authenticate(w, r, true)\n\n\tif auth == nil {\n\t\treturn\n\t}\n\n\t// create pin - 6 alphanumeric characters\n\tpinBytes, err := shared.GetRandomAlphanumeric(6)\n\tif err != nil {\n\t\tlog.Printf(\"Error generating random pin: %v\\n\", err)\n\t\thttp.Error(w, \"Error generating random pin: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// get sha256 hash of pin\n\thashBytes := sha256.Sum256(pinBytes)\n\tpinHash := hex.EncodeToString(hashBytes[:])\n\n\terr = db.CreateSignInCode(auth.User.Id, auth.OrgId, pinHash)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error creating sign in code: %v\\n\", err)\n\t\thttp.Error(w, \"Error creating sign in code: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully created sign in code\")\n\n\t// return the pin as a response\n\tw.Write(pinBytes)\n}\n\nfunc SignInHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for SignInHandler\")\n\n\t// read the request body\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar req shared.SignInRequest\n\terr = json.Unmarshal(body, &req)\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling request: %v\\n\", err)\n\t\thttp.Error(w, \"Error unmarshalling request: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Validating and signing in\")\n\tresp, err := ValidateAndSignIn(w, r, req)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error signing in: %v\\n\", err)\n\t\thttp.Error(w, \"Error signing in: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(resp)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully signed in\")\n\n\tw.Write(bytes)\n}\n\nfunc SignOutHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for SignOutHandler\")\n\n\tauth := Authenticate(w, r, false)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\t_, err := db.Conn.Exec(\"UPDATE auth_tokens SET deleted_at = NOW() WHERE token_hash = $1\", auth.AuthToken.TokenHash)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error deleting auth token: %v\\n\", err)\n\t\thttp.Error(w, \"Error deleting auth token: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = ClearAuthCookieIfBrowser(w, r)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error clearing auth cookie: %v\\n\", err)\n\t\thttp.Error(w, \"Error clearing auth cookie: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = ClearAccountFromCookies(w, r, auth.User.Id)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error clearing account from cookies: %v\\n\", err)\n\t\thttp.Error(w, \"Error clearing account from cookies: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully signed out\")\n}\n\nfunc GetOrgUserConfigHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetOrgUserConfigHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torgUserConfig, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org user config: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org user config: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(orgUserConfig)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling response: %v\\n\", err)\n\t\thttp.Error(w, \"Error marshalling response: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n}\n\nfunc UpdateOrgUserConfigHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdateOrgUserConfigHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Error reading request body: %v\\n\", err)\n\t\thttp.Error(w, \"Error reading request body: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar req shared.OrgUserConfig\n\terr = json.Unmarshal(body, &req)\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling request: %v\\n\", err)\n\t\thttp.Error(w, \"Error unmarshalling request: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\terr = db.UpdateOrgUserConfig(auth.User.Id, auth.OrgId, &req)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error updating org user config: %v\\n\", err)\n\t\thttp.Error(w, \"Error updating org user config: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully updated org user config\")\n}\n"
  },
  {
    "path": "app/server/handlers/settings.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"reflect\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc GetSettingsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetSettingsHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tplan := authorizePlan(w, planId, auth)\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tvar settings *shared.PlanSettings\n\tctx, cancel := context.WithCancel(r.Context())\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"get settings\",\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\tres, err := db.GetPlanSettings(plan)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsettings = res\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error getting settings: \", err)\n\t\thttp.Error(w, \"Error getting settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(settings)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling settings: \", err)\n\t\thttp.Error(w, \"Error marshalling settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"GetSettingsHandler processed successfully\")\n\n\tw.Write(bytes)\n}\n\nfunc UpdateSettingsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdateSettingsHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tplanId := vars[\"planId\"]\n\tbranch := vars[\"branch\"]\n\n\tlog.Println(\"planId: \", planId, \"branch: \", branch)\n\n\tplan := authorizePlan(w, planId, auth)\n\n\tif plan == nil {\n\t\treturn\n\t}\n\n\tvar req shared.UpdateSettingsRequest\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\n\tif err != nil {\n\t\tlog.Println(\"Error decoding request body: \", err)\n\t\thttp.Error(w, \"Error decoding request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif req.ModelPackName == \"\" && req.ModelPack == nil {\n\t\tlog.Println(\"No model pack name or model pack provided\")\n\t\thttp.Error(w, \"No model pack name or model pack provided\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif req.ModelPackName != \"\" {\n\t\tif mp, builtIn := shared.BuiltInModelPacksByName[req.ModelPackName]; builtIn {\n\t\t\tif os.Getenv(\"IS_CLOUD\") != \"\" && mp.LocalProvider != \"\" {\n\t\t\t\tmsg := fmt.Sprintf(\"Built-in local model pack %s can't be used on Plandex Cloud\", req.ModelPackName)\n\t\t\t\tlog.Println(msg)\n\t\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\tif req.ModelPack != nil {\n\t\tif req.ModelPack.LocalProvider != \"\" {\n\t\t\tmsg := fmt.Sprintf(\"Local model pack %s can't be used on Plandex Cloud\", req.ModelPack.Name)\n\t\t\tlog.Println(msg)\n\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\treturn\n\t\t}\n\n\t\tids := req.ModelPack.ToModelPackSchema().AllModelIds()\n\t\tfor _, id := range ids {\n\t\t\tbm, builtIn := shared.BuiltInBaseModelsById[id]\n\t\t\tif builtIn && os.Getenv(\"IS_CLOUD\") != \"\" && bm.IsLocalOnly() {\n\t\t\t\tmsg := fmt.Sprintf(\"Built-in local model %s can't be used on Plandex Cloud\", id)\n\t\t\t\tlog.Println(msg)\n\t\t\t\thttp.Error(w, msg, http.StatusUnprocessableEntity)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\tctx, cancel := context.WithCancel(r.Context())\n\n\tvar commitMsg string\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tReason:   \"update settings\",\n\t\tScope:    db.LockScopeWrite,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancel,\n\t}, func(repo *db.GitRepo) error {\n\t\toriginalSettings, err := db.GetPlanSettings(plan)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting settings: %v\", err)\n\t\t}\n\n\t\tsettings, err := originalSettings.DeepCopy()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error copying settings: %v\", err)\n\t\t}\n\n\t\tif req.ModelPackName != \"\" {\n\t\t\tsettings.SetModelPackByName(req.ModelPackName)\n\t\t} else if req.ModelPack != nil {\n\t\t\tsettings.SetCustomModelPack(req.ModelPack)\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"no model pack name or model pack provided\")\n\t\t}\n\n\t\t// log.Println(\"Original settings:\")\n\t\t// spew.Dump(originalSettings)\n\n\t\t// log.Println(\"req.Settings:\")\n\t\t// spew.Dump(req.Settings)\n\n\t\terr = db.StorePlanSettings(plan, *settings)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error storing settings: %v\", err)\n\t\t}\n\n\t\tcommitMsg = getUpdateCommitMsg(settings, originalSettings, false)\n\n\t\terr = repo.GitAddAndCommit(branch, commitMsg)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error committing settings: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error updating settings: \", err)\n\t\thttp.Error(w, \"Error updating settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tres := shared.UpdateSettingsResponse{\n\t\tMsg: commitMsg,\n\t}\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling response: \", err)\n\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"UpdateSettingsHandler processed successfully\")\n\n}\n\nfunc GetDefaultSettingsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for GetDefaultSettingsHandler\")\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tsettings, err := db.GetOrgDefaultSettings(auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Println(\"Error getting default settings: \", err)\n\t\thttp.Error(w, \"Error getting default settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tbytes, err := json.Marshal(settings)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling default settings: \", err)\n\t\thttp.Error(w, \"Error marshalling default settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"GetDefaultSettingsHandler processed successfully\")\n}\n\nfunc UpdateDefaultSettingsHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received request for UpdateDefaultSettingsHandler\")\n\n\tauth := Authenticate(w, r, true)\n\n\tif auth == nil {\n\t\treturn\n\t}\n\n\tvar req shared.UpdateSettingsRequest\n\terr := json.NewDecoder(r.Body).Decode(&req)\n\n\tif err != nil {\n\t\tlog.Println(\"Error decoding request body: \", err)\n\t\thttp.Error(w, \"Error decoding request body\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif req.ModelPackName == \"\" && req.ModelPack == nil {\n\t\tlog.Println(\"No model pack name or model pack provided\")\n\t\thttp.Error(w, \"No model pack name or model pack provided\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar originalSettings *shared.PlanSettings\n\tvar settings *shared.PlanSettings\n\n\terr = db.WithTx(r.Context(), \"update default settings\", func(tx *sqlx.Tx) error {\n\t\tvar err error\n\n\t\toriginalSettings, err = db.GetOrgDefaultSettingsForUpdate(auth.OrgId, tx)\n\n\t\tif err != nil {\n\t\t\tlog.Println(\"Error getting default settings: \", err)\n\t\t\treturn fmt.Errorf(\"error getting default settings: %v\", err)\n\t\t}\n\n\t\tsettings, err = originalSettings.DeepCopy()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error copying settings: %v\", err)\n\t\t}\n\n\t\tif req.ModelPackName != \"\" {\n\t\t\tsettings.SetModelPackByName(req.ModelPackName)\n\t\t} else if req.ModelPack != nil {\n\t\t\tsettings.SetCustomModelPack(req.ModelPack)\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"no model pack name or model pack provided\")\n\t\t}\n\n\t\t// log.Println(\"Original settings:\")\n\t\t// spew.Dump(originalSettings)\n\n\t\t// log.Println(\"req.Settings:\")\n\t\t// spew.Dump(req.Settings)\n\n\t\terr = db.StoreOrgDefaultSettings(auth.OrgId, settings, tx)\n\n\t\tif err != nil {\n\t\t\tlog.Println(\"Error storing default settings: \", err)\n\t\t\treturn fmt.Errorf(\"error storing default settings: %v\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error updating default settings: \", err)\n\t\thttp.Error(w, \"Error updating default settings\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tcommitMsg := getUpdateCommitMsg(settings, originalSettings, true)\n\n\tres := shared.UpdateSettingsResponse{\n\t\tMsg: commitMsg,\n\t}\n\tbytes, err := json.Marshal(res)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling response: \", err)\n\t\thttp.Error(w, \"Error marshalling response\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Write(bytes)\n\n\tlog.Println(\"UpdateDefaultSettingsHandler processed successfully\")\n}\n\nfunc getUpdateCommitMsg(settings *shared.PlanSettings, originalSettings *shared.PlanSettings, isOrgDefault bool) string {\n\t// log.Println(\"Comparing settings\")\n\t// log.Println(\"Original:\")\n\t// spew.Dump(originalSettings)\n\t// log.Println(\"New:\")\n\t// spew.Dump(settings)\n\n\t// log.Println(\"Changes to settings:\", strings.Join(changes, \"\\n\"))\n\tvar s string\n\tif isOrgDefault {\n\t\ts = \"⚙️  Updated org-wide default settings:\"\n\t} else {\n\t\ts = \"⚙️  Updated model settings:\"\n\t}\n\n\tvar changes []string\n\tchanges = compareSettings(originalSettings, settings, changes)\n\n\tif len(changes) == 0 {\n\t\treturn \"No changes to settings\"\n\t}\n\n\tfor _, change := range changes {\n\t\ts += \"\\n\" + \"  • \" + change\n\t}\n\n\treturn s\n}\n\nfunc compareSettings(original, updated *shared.PlanSettings, changes []string) []string {\n\tif updated.ModelPackName != \"\" {\n\t\toriginalName := \"custom\"\n\t\tif original.ModelPackName != \"\" {\n\t\t\toriginalName = original.ModelPackName\n\t\t}\n\t\tchanges = append(changes, fmt.Sprintf(\"model-pack | %v → %v\", originalName, updated.ModelPackName))\n\t} else if updated.ModelPack != nil {\n\t\tif original.ModelPack == nil {\n\t\t\tchanges = append(changes, fmt.Sprintf(\"model-pack | %v → %v\", original.ModelPackName, \"custom\"))\n\t\t}\n\n\t\tchanges = compareAny(original.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles, updated.GetModelPack().ToModelPackSchema().ModelPackSchemaRoles, \"\", changes)\n\t}\n\n\treturn changes\n}\n\nfunc compareAny(a, b interface{}, path string, changes []string) []string {\n\taVal, bVal := reflect.ValueOf(a), reflect.ValueOf(b)\n\n\tif !aVal.IsValid() && !bVal.IsValid() {\n\t\treturn changes\n\t}\n\n\t// Pointer / nil handling – BEFORE dereferencing\n\tif aVal.Kind() == reflect.Ptr || bVal.Kind() == reflect.Ptr {\n\t\t// both nil → nothing\n\t\tif (aVal.Kind() == reflect.Ptr && aVal.IsNil()) &&\n\t\t\t(bVal.Kind() == reflect.Ptr && bVal.IsNil()) {\n\t\t\treturn changes\n\t\t}\n\n\t\t// one nil, one non-nil → record diff\n\t\tif (aVal.Kind() == reflect.Ptr && aVal.IsNil()) ||\n\t\t\t(bVal.Kind() == reflect.Ptr && bVal.IsNil()) {\n\t\t\taStr := \"none\"\n\t\t\tbStr := \"none\"\n\t\t\tif aVal.Kind() != reflect.Ptr || !aVal.IsNil() {\n\t\t\t\taStr = short(aVal)\n\t\t\t}\n\t\t\tif bVal.Kind() != reflect.Ptr || !bVal.IsNil() {\n\t\t\t\tbStr = short(bVal)\n\t\t\t}\n\t\t\tchanges = append(changes, fmt.Sprintf(\"%s | %s → %s\", path, aStr, bStr))\n\t\t\treturn changes\n\t\t}\n\n\t\t// both non-nil pointers → safe to dereference\n\t\tif aVal.Kind() == reflect.Ptr {\n\t\t\taVal = aVal.Elem()\n\t\t}\n\t\tif bVal.Kind() == reflect.Ptr {\n\t\t\tbVal = bVal.Elem()\n\t\t}\n\t}\n\n\t// Check again after dereferencing\n\tif !aVal.IsValid() && !bVal.IsValid() {\n\t\treturn changes\n\t}\n\n\t// One side nil → record diff and stop\n\tif !aVal.IsValid() || !bVal.IsValid() {\n\t\tvar aStr, bStr string\n\t\tif aVal.IsValid() {\n\t\t\taStr = short(aVal)\n\t\t} else {\n\t\t\taStr = \"none\"\n\t\t}\n\t\tif bVal.IsValid() {\n\t\t\tbStr = short(bVal)\n\t\t} else {\n\t\t\tbStr = \"none\"\n\t\t}\n\t\tchanges = append(changes, fmt.Sprintf(\"%s | %s → %s\", path, aStr, bStr))\n\t\treturn changes\n\t}\n\n\t// Ensure we can safely call Interface()\n\tif !aVal.CanInterface() || !bVal.CanInterface() {\n\t\treturn changes\n\t}\n\n\tif reflect.DeepEqual(aVal.Interface(), bVal.Interface()) {\n\t\treturn changes // No difference found\n\t}\n\n\tswitch aVal.Kind() {\n\tcase reflect.Struct:\n\t\tfor i := 0; i < aVal.NumField(); i++ {\n\t\t\tfield := aVal.Type().Field(i)\n\t\t\tif !field.IsExported() {\n\t\t\t\tcontinue // Skip unexported fields\n\t\t\t}\n\t\t\tfieldName := field.Name\n\t\t\tdasherizedName := shared.Dasherize(fieldName)\n\n\t\t\tupdatedPath := path\n\t\t\tif !(dasherizedName == \"model-set\" ||\n\t\t\t\tdasherizedName == \"model-role-config\" ||\n\t\t\t\tdasherizedName == \"base-model-config\" ||\n\t\t\t\tdasherizedName == \"planner-model-config\" ||\n\t\t\t\tdasherizedName == \"task-model-config\") {\n\t\t\t\tif updatedPath != \"\" {\n\t\t\t\t\tupdatedPath = updatedPath + \".\" + dasherizedName\n\t\t\t\t} else {\n\t\t\t\t\tif dasherizedName == \"model-overrides\" {\n\t\t\t\t\t\tdasherizedName = \"overrides\"\n\t\t\t\t\t}\n\t\t\t\t\tupdatedPath = dasherizedName\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchanges = compareAny(aVal.Field(i).Interface(), bVal.Field(i).Interface(), updatedPath, changes)\n\t\t}\n\tdefault:\n\t\tvar aStr, bStr string\n\t\tif aVal.IsValid() {\n\t\t\taStr = short(aVal)\n\t\t} else {\n\t\t\taStr = \"no override\"\n\t\t}\n\n\t\tif bVal.IsValid() {\n\t\t\tbStr = short(bVal)\n\t\t} else {\n\t\t\tbStr = \"no override\"\n\t\t}\n\n\t\tchange := fmt.Sprintf(\"%s | %v → %v\", path, aStr, bStr)\n\t\tchanges = append(changes, change)\n\t}\n\n\treturn changes\n}\n\nfunc short(v reflect.Value) string {\n\tif !v.IsValid() {\n\t\treturn \"none\"\n\t}\n\n\t// If it’s a pointer, follow it once\n\tif v.Kind() == reflect.Ptr {\n\t\tif v.IsNil() {\n\t\t\treturn \"none\"\n\t\t}\n\t\tv = v.Elem()\n\t}\n\n\tswitch v.Kind() {\n\tcase reflect.String:\n\t\treturn v.String()\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn fmt.Sprintf(\"%d\", v.Int())\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:\n\t\treturn fmt.Sprintf(\"%d\", v.Uint())\n\tcase reflect.Float32, reflect.Float64:\n\t\treturn fmt.Sprintf(\"%g\", v.Float())\n\tcase reflect.Struct:\n\t\t// Special-case ModelRoleConfigSchema: show the ModelId only\n\t\tif f := v.FieldByName(\"ModelId\"); f.IsValid() && f.Kind() == reflect.String {\n\t\t\treturn f.String()\n\t\t}\n\t\treturn fmt.Sprintf(\"%T\", v.Interface()) // fall-back: just the type name\n\tdefault:\n\t\treturn fmt.Sprintf(\"%v\", v.Interface())\n\t}\n}\n"
  },
  {
    "path": "app/server/handlers/stream_helper.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\tmodelPlan \"plandex-server/model/plan\"\n\t\"plandex-server/types\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\nconst HeartbeatInterval = 5 * time.Second\n\nfunc startResponseStream(reqCtx context.Context, w http.ResponseWriter, auth *types.ServerAuth, planId, branch string, isConnect bool) {\n\tlog.Println(\"Response stream manager: starting plan stream\")\n\n\tactive := modelPlan.GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\"Response stream manager: active plan not found for plan ID %s on branch %s\\n\", planId, branch)\n\t\thttp.Error(w, \"Active plan not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Transfer-Encoding\", \"chunked\")\n\tw.Header().Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\n\t// send initial message to client\n\tmsg := shared.StreamMessage{\n\t\tType: shared.StreamMessageStart,\n\t}\n\n\tbytes, err := json.Marshal(msg)\n\n\tif err != nil {\n\t\tlog.Printf(\"Response stream manager: error marshalling message: %v\\n\", err)\n\t\treturn\n\t}\n\n\tlog.Println(\"Response stream manager: sending initial message\")\n\terr = sendStreamMessage(w, string(bytes))\n\tif err != nil {\n\t\tlog.Println(\"Response stream manager: error sending initial message:\", err)\n\t\treturn\n\t}\n\n\tif isConnect {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\terr = initConnectActive(auth, planId, branch, w)\n\n\t\tif err != nil {\n\t\t\tlog.Println(\"Response stream manager: error initializing connection to active plan:\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tsubscriptionId, ch := modelPlan.SubscribePlan(reqCtx, planId, branch)\n\tdefer func() {\n\t\tlog.Println(\"Response stream manager: client stream closed\")\n\t\tmodelPlan.UnsubscribePlan(planId, branch, subscriptionId)\n\t}()\n\n\tif isConnect {\n\t\ttime.Sleep(50 * time.Millisecond)\n\t} else {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\n\tchHeartbeat := make(chan string)\n\n\t// send heartbeats while the stream is active\n\tgo func() {\n\t\tticker := time.NewTicker(HeartbeatInterval)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\tchHeartbeat <- string(shared.StreamMessageHeartbeat)\n\t\t\tcase <-reqCtx.Done():\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase <-reqCtx.Done():\n\t\t\tlog.Println(\"Response stream manager: request context done\")\n\t\t\treturn\n\t\tcase msg := <-chHeartbeat:\n\t\t\terr = sendStreamMessage(w, msg)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase msg := <-ch:\n\t\t\t// log.Println(\"Response stream manager: sending message:\", msg)\n\t\t\terr = sendStreamMessage(w, msg)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n}\n\nfunc sendStreamMessage(w http.ResponseWriter, msg string) error {\n\tbytes := []byte(msg + shared.STREAM_MESSAGE_SEPARATOR)\n\n\t// log.Printf(\"Response stream manager: writing message to client: %s\\n\", msg)\n\n\t_, err := w.Write(bytes)\n\tif err != nil {\n\t\tlog.Printf(\"Response stream manager: error writing to client: %v\\n\", err)\n\t\treturn err\n\t} else if flusher, ok := w.(http.Flusher); ok {\n\t\tflusher.Flush()\n\t}\n\treturn nil\n}\n\nfunc initConnectActive(auth *types.ServerAuth, planId, branch string, w http.ResponseWriter) error {\n\tlog.Println(\"Response stream manager: initializing connection to active plan\")\n\n\tactive := modelPlan.GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\treturn fmt.Errorf(\"active plan not found for plan ID %s on branch %s\", planId, branch)\n\t}\n\n\tmsg := shared.StreamMessage{\n\t\tType: shared.StreamMessageConnectActive,\n\t}\n\n\tif active.Prompt != \"\" && !active.BuildOnly {\n\t\tmsg.InitPrompt = active.Prompt\n\t}\n\n\tif active.BuildOnly {\n\t\tmsg.InitBuildOnly = true\n\t}\n\n\tif len(active.StoredReplyIds) > 0 {\n\t\tconvo, err := db.GetPlanConvo(auth.OrgId, active.Id)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting plan convo: %v\", err)\n\t\t}\n\n\t\tconvoMsgById := map[string]*db.ConvoMessage{}\n\t\tfor _, convoMsg := range convo {\n\t\t\tconvoMsgById[convoMsg.Id] = convoMsg\n\t\t}\n\n\t\tfor _, replyId := range active.StoredReplyIds {\n\t\t\tif convoMsg, ok := convoMsgById[replyId]; ok {\n\t\t\t\tmsg.InitReplies = append(msg.InitReplies, convoMsg.Message)\n\t\t\t}\n\t\t}\n\t}\n\n\tif active.CurrentReplyContent != \"\" {\n\t\tmsg.InitReplies = append(msg.InitReplies, active.CurrentReplyContent)\n\t}\n\n\tif active.MissingFilePath != \"\" {\n\t\tmsg.MissingFilePath = active.MissingFilePath\n\t}\n\n\tbytes, err := json.Marshal(msg)\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling message: %v\", err)\n\t}\n\n\tlog.Println(\"Response stream manager: sending connect message\")\n\terr = sendStreamMessage(w, string(bytes))\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error sending connect message: %v\", err)\n\t}\n\n\tbuildQueuesByPath := modelPlan.GetActivePlan(planId, branch).BuildQueuesByPath\n\n\t// if we're connecting to an active stream and there are active builds, send initial build info\n\tif len(buildQueuesByPath) > 0 {\n\n\t\tfor path, queue := range buildQueuesByPath {\n\t\t\tbuildInfo := shared.BuildInfo{Path: path}\n\n\t\t\tfor _, build := range queue {\n\t\t\t\tif build.BuildFinished() {\n\t\t\t\t\tbuildInfo.NumTokens = 0\n\t\t\t\t\tbuildInfo.Finished = true\n\t\t\t\t} else {\n\t\t\t\t\t// no longer showing token counts in build info - leaving commented out for now for reference\n\t\t\t\t\t// tokens := build.WithLineNumsBufferTokens\n\t\t\t\t\tbuildInfo.Finished = false\n\t\t\t\t\t// buildInfo.NumTokens += tokens\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmsg := shared.StreamMessage{\n\t\t\t\tType:      shared.StreamMessageBuildInfo,\n\t\t\t\tBuildInfo: &buildInfo,\n\t\t\t}\n\t\t\tbytes, err := json.Marshal(msg)\n\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error marshalling message: %v\", err)\n\t\t\t}\n\n\t\t\terr = sendStreamMessage(w, string(bytes))\n\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error sending message: %v\", err)\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/handlers/users.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\nfunc ListUsersHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for ListUsersHandler\")\n\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Local mode is not supported for user management\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't list users\",\n\t\t})\n\t\treturn\n\t}\n\n\tusers, err := db.ListUsers(auth.OrgId)\n\tif err != nil {\n\t\tlog.Println(\"Error listing users: \", err)\n\t\thttp.Error(w, \"Error listing users: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tapiUsers := make([]*shared.User, 0, len(users))\n\tfor _, user := range users {\n\t\tapiUsers = append(apiUsers, user.ToApi())\n\t}\n\n\torgUsers, err := db.ListOrgUsers(auth.OrgId)\n\tif err != nil {\n\t\tlog.Println(\"Error listing org users: \", err)\n\t\thttp.Error(w, \"Error listing org users: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\torgUsersByUserId := make(map[string]*shared.OrgUser)\n\tfor _, orgUser := range orgUsers {\n\t\torgUsersByUserId[orgUser.UserId] = orgUser.ToApi()\n\t}\n\n\tresp := shared.ListUsersResponse{\n\t\tUsers:            apiUsers,\n\t\tOrgUsersByUserId: orgUsersByUserId,\n\t}\n\n\tbytes, err := json.Marshal(resp)\n\n\tif err != nil {\n\t\tlog.Println(\"Error marshalling users: \", err)\n\t\thttp.Error(w, \"Error marshalling users: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully processed request for ListUsersHandler\")\n\n\tw.Write(bytes)\n}\n\nfunc DeleteOrgUserHandler(w http.ResponseWriter, r *http.Request) {\n\tlog.Println(\"Received a request for DeleteOrgUserHandler\")\n\n\tif os.Getenv(\"GOENV\") == \"development\" && os.Getenv(\"LOCAL_MODE\") == \"1\" {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Local mode is not supported for user management\",\n\t\t})\n\t\treturn\n\t}\n\n\tauth := Authenticate(w, r, true)\n\tif auth == nil {\n\t\treturn\n\t}\n\n\torg, err := db.GetOrg(auth.OrgId)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif org.IsTrial {\n\t\twriteApiError(w, shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeTrialActionNotAllowed,\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    \"Trial user can't delete users\",\n\t\t})\n\t\treturn\n\t}\n\n\tvars := mux.Vars(r)\n\tuserId := vars[\"userId\"]\n\n\tlog.Println(\"userId: \", userId)\n\n\torgUser, err := db.GetOrgUser(userId, auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org user: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org user: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// ensure current user can remove target user\n\tremovePermission := shared.Permission(strings.Join([]string{string(shared.PermissionRemoveUser), orgUser.OrgRoleId}, \"|\"))\n\n\tif !auth.HasPermission(removePermission) {\n\t\tlog.Printf(\"User does not have permission to remove user with role: %v\\n\", orgUser.OrgRoleId)\n\t\thttp.Error(w, \"User does not have permission to remove user with role: \"+orgUser.OrgRoleId, http.StatusForbidden)\n\t\treturn\n\t}\n\n\t// verify user is org member\n\tisMember, err := db.ValidateOrgMembership(userId, auth.OrgId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error validating org membership: %v\\n\", err)\n\t\thttp.Error(w, \"Error validating org membership: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif !isMember {\n\t\tlog.Printf(\"User %s is not a member of org %s\\n\", userId, auth.OrgId)\n\t\thttp.Error(w, \"User \"+userId+\" is not a member of org \"+auth.OrgId, http.StatusForbidden)\n\t\treturn\n\t}\n\n\torgOwnerRoleId, err := db.GetOrgOwnerRoleId()\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting org owner role id: %v\\n\", err)\n\t\thttp.Error(w, \"Error getting org owner role id: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// verify user isn't the only org owner\n\tif orgUser.OrgRoleId == orgOwnerRoleId {\n\t\tnumOwners, err := db.NumUsersWithRole(auth.OrgId, orgOwnerRoleId)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting number of org owners: %v\\n\", err)\n\t\t\thttp.Error(w, \"Error getting number of org owners: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif numOwners == 1 {\n\t\t\tlog.Println(\"Cannot delete the only org owner\")\n\t\t\thttp.Error(w, \"Cannot delete the only org owner\", http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t}\n\n\terr = db.WithTx(r.Context(), \"delete org user\", func(tx *sqlx.Tx) error {\n\n\t\terr = db.DeleteOrgUser(auth.OrgId, userId, tx)\n\n\t\tif err != nil {\n\t\t\tlog.Println(\"Error deleting org user: \", err)\n\t\t\treturn fmt.Errorf(\"error deleting org user: %v\", err)\n\t\t}\n\n\t\tinvite, err := db.GetActiveInviteByEmail(auth.OrgId, auth.User.Email)\n\n\t\tif err != nil {\n\t\t\tlog.Println(\"Error getting invite for org user: \", err)\n\t\t\treturn fmt.Errorf(\"error getting invite for org user: %v\", err)\n\t\t}\n\n\t\tif invite != nil {\n\t\t\terr = db.DeleteInvite(invite.Id, tx)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Error deleting invite: \", err)\n\t\t\t\treturn fmt.Errorf(\"error deleting invite: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Println(\"Error deleting org user: \", err)\n\t\thttp.Error(w, \"Error deleting org user: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Println(\"Successfully processed request for DeleteOrgUserHandler\")\n}\n"
  },
  {
    "path": "app/server/hooks/hooks.go",
    "content": "package hooks\n\nimport (\n\t\"context\"\n\t\"plandex-server/db\"\n\t\"plandex-server/types\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nconst (\n\tHealthCheck = \"health_check\"\n\n\tCreateAccount        = \"create_account\"\n\tWillCreatePlan       = \"will_create_plan\"\n\tWillTellPlan         = \"will_tell_plan\"\n\tWillExecPlan         = \"will_exec_plan\"\n\tWillSendModelRequest = \"will_send_model_request\"\n\tDidSendModelRequest  = \"did_send_model_request\"\n\tDidFinishBuilderRun  = \"did_finish_builder_run\"\n\tCreateOrg            = \"create_org\"\n\tAuthenticate         = \"authenticate\"\n\tGetIntegratedModels  = \"get_integrated_models\"\n\tGetApiOrgs           = \"get_api_orgs\"\n\tCallFastApply        = \"call_fast_apply\"\n)\n\ntype WillSendModelRequestParams struct {\n\tInputTokens  int\n\tOutputTokens int\n\tModelName    shared.ModelName\n\tIsUserPrompt bool\n\tModelTag     shared.ModelTag\n\tModelId      shared.ModelId\n}\n\ntype DidSendModelRequestParams struct {\n\tInputTokens     int\n\tOutputTokens    int\n\tCachedTokens    int\n\tModelId         shared.ModelId\n\tModelTag        shared.ModelTag\n\tModelName       shared.ModelName\n\tModelProvider   shared.ModelProvider\n\tModelRole       shared.ModelRole\n\tModelPackName   string\n\tPurpose         string\n\tGenerationId    string\n\tPlanId          string\n\tModelStreamId   string\n\tConvoMessageId  string\n\tBuildId         string\n\tStoppedEarly    bool\n\tUserCancelled   bool\n\tHadError        bool\n\tNoReportedUsage bool\n\tSessionId       string\n\n\tRequestStartedAt time.Time\n\tStreaming        bool\n\tStreamResult     string\n\tFirstTokenAt     time.Time\n\tReq              *types.ExtendedChatCompletionRequest\n\tRes              *openai.ChatCompletionResponse\n\tModelConfig      *shared.ModelRoleConfig\n}\n\ntype DidFinishBuilderRunParams struct {\n\tPlanId        string\n\tFilePath      string\n\tFileExt       string\n\tLang          string\n\tGenerationIds []string\n\n\tValidateModelConfig  *shared.ModelRoleConfig\n\tFastApplyModelConfig *shared.ModelRoleConfig\n\tWholeFileModelConfig *shared.ModelRoleConfig\n\n\tAutoApplySuccess                   bool\n\tAutoApplyValidationReasons         []string\n\tAutoApplyValidationSyntaxErrors    []string\n\tAutoApplyValidationPassed          bool\n\tAutoApplyValidationFailureResponse string\n\tAutoApplyValidationStartedAt       time.Time\n\tAutoApplyValidationFinishedAt      time.Time\n\n\tDidReplacement             bool\n\tReplacementSuccess         bool\n\tReplacementSyntaxErrors    []string\n\tReplacementFailureResponse string\n\tReplacementStartedAt       time.Time\n\tReplacementFinishedAt      time.Time\n\n\tDidRewriteProposed             bool\n\tRewriteProposedSuccess         bool\n\tRewriteProposedSyntaxErrors    []string\n\tRewriteProposedFailureResponse string\n\tRewriteProposedStartedAt       time.Time\n\tRewriteProposedFinishedAt      time.Time\n\n\tDidFastApply             bool\n\tFastApplySuccess         bool\n\tFastApplySyntaxErrors    []string\n\tFastApplyFailureResponse string\n\tFastApplyStartedAt       time.Time\n\tFastApplyFinishedAt      time.Time\n\n\tBuiltWholeFile           bool\n\tBuildWholeFileStartedAt  time.Time\n\tBuildWholeFileFinishedAt time.Time\n\n\tStartedAt  time.Time\n\tFinishedAt time.Time\n}\n\ntype CreateOrgHookRequestParams struct {\n\tOrg *db.Org\n}\n\ntype AuthenticateHookRequestParams struct {\n\tPath string\n\tHash string\n}\n\ntype FastApplyParams struct {\n\tInitialCode string `json:\"initialCode\"`\n\tEditSnippet string `json:\"editSnippet\"`\n\n\tInitialCodeTokens int\n\tEditSnippetTokens int\n\n\tLanguage shared.Language\n\n\tCtx context.Context\n}\n\ntype HookParams struct {\n\tAuth *types.ServerAuth\n\tPlan *db.Plan\n\tTx   *sqlx.Tx\n\n\tWillSendModelRequestParams    *WillSendModelRequestParams\n\tDidSendModelRequestParams     *DidSendModelRequestParams\n\tCreateOrgHookRequestParams    *CreateOrgHookRequestParams\n\tGetApiOrgIds                  []string\n\tAuthenticateHookRequestParams *AuthenticateHookRequestParams\n\tDidFinishBuilderRunParams     *DidFinishBuilderRunParams\n\tFastApplyParams               *FastApplyParams\n}\n\ntype GetIntegratedModelsResult struct {\n\tIntegratedModelsMode bool\n\tAuthVars             map[string]string\n}\n\ntype FastApplyResult struct {\n\tMergedCode string\n}\n\ntype HookResult struct {\n\tGetIntegratedModelsResult *GetIntegratedModelsResult\n\tApiOrgsById               map[string]*shared.Org\n\tFastApplyResult           *FastApplyResult\n}\n\ntype Hook func(params HookParams) (HookResult, *shared.ApiError)\n\nvar hooks = make(map[string]Hook)\n\nfunc RegisterHook(name string, hook Hook) {\n\thooks[name] = hook\n}\n\nfunc ExecHook(name string, params HookParams) (HookResult, *shared.ApiError) {\n\thook, ok := hooks[name]\n\tif !ok {\n\t\treturn HookResult{}, nil\n\t}\n\treturn hook(params)\n}\n\nfunc TestUpdate() {\n\n}\n"
  },
  {
    "path": "app/server/host/ip.go",
    "content": "package host\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n)\n\nvar Ip string\n\nfunc LoadIp() error {\n\tif os.Getenv(\"GOENV\") == \"development\" {\n\t\tIp = \"localhost\"\n\t\treturn nil\n\t}\n\n\tif os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\tvar err error\n\t\tIp, err = getAwsIp()\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting AWS ECS IP: %v\", err)\n\t\t}\n\n\t\tlog.Println(\"Got AWS ECS IP: \", Ip)\n\n\t} else if os.Getenv(\"IP\") != \"\" {\n\t\tIp = os.Getenv(\"IP\")\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n\ntype ecsMetadata struct {\n\tNetworks []struct {\n\t\tIPv4Addresses []string `json:\"IPv4Addresses\"`\n\t} `json:\"Networks\"`\n}\n\nvar awsIp string\n\nfunc getAwsIp() (string, error) {\n\tecsMetadataURL := os.Getenv(\"ECS_CONTAINER_METADATA_URI\")\n\n\tlog.Printf(\"Getting ECS metadata from %s\\n\", ecsMetadataURL)\n\n\tresp, err := http.Get(ecsMetadataURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar metadata ecsMetadata\n\terr = json.Unmarshal(body, &metadata)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(metadata.Networks) == 0 || len(metadata.Networks[0].IPv4Addresses) == 0 {\n\t\treturn \"\", errors.New(\"no IP address found in ECS metadata\")\n\t}\n\n\tawsIp = metadata.Networks[0].IPv4Addresses[0]\n\n\treturn awsIp, nil\n}\n"
  },
  {
    "path": "app/server/litellm_proxy.py",
    "content": "from litellm.llms.anthropic.common_utils import AnthropicModelInfo\nfrom typing import List, Optional\n\n_orig_get_hdrs = AnthropicModelInfo.get_anthropic_headers\n\ndef _oauth_get_hdrs(\n  self,\n  api_key: str,\n  anthropic_version: Optional[str] = None,\n  computer_tool_used: bool = False,\n  prompt_caching_set: bool = False,\n  pdf_used: bool = False,\n  file_id_used: bool = False,\n  mcp_server_used: bool = False,\n  is_vertex_request: bool = False,\n  user_anthropic_beta_headers: Optional[List[str]] = None,\n):\n  # call the original builder first\n  hdrs = _orig_get_hdrs(\n    self,\n    api_key=api_key,\n    anthropic_version=anthropic_version,\n    computer_tool_used=computer_tool_used,\n    prompt_caching_set=prompt_caching_set,\n    pdf_used=pdf_used,\n    file_id_used=file_id_used,\n    mcp_server_used=mcp_server_used,\n    is_vertex_request=is_vertex_request,\n    user_anthropic_beta_headers=user_anthropic_beta_headers,\n  )\n\n  # remove x-api-key when we detect an OAuth access-token\n  print(f\"api_key: {api_key}\")\n  if api_key and api_key.startswith((\"sk-ant-oat\", \"sk-ant-oau\")):\n    hdrs[\"anthropic-beta\"] = \"oauth-2025-04-20\"\n    hdrs[\"anthropic-product\"] = \"claude-code\"\n    hdrs.pop(\"x-api-key\", None)\n\n  print(f\"Anthropic headers: {hdrs}\")\n\n  return hdrs\n\n# monkey-patch AnthropicModelInfo.get_anthropic_headers to handle OAuth headers\nAnthropicModelInfo.get_anthropic_headers = _oauth_get_hdrs\n\nfrom fastapi import FastAPI, Request\nfrom fastapi.responses import StreamingResponse, JSONResponse\nfrom litellm import completion, _turn_on_debug\nimport json\nimport re\n\n# _turn_on_debug()\n\nLOGGING_ENABLED = False\n\nprint(\"Litellm proxy: starting proxy server on port 4000...\")\n\napp = FastAPI()\n\n@app.get(\"/health\")\nasync def health():\n  return {\"status\": \"ok\"}\n\n@app.post(\"/v1/chat/completions\")\nasync def passthrough(request: Request):\n  payload = await request.json()\n\n  if LOGGING_ENABLED:\n    # Log the request data for debugging\n    try:\n      # Get headers (excluding authorization to avoid logging credentials)\n      headers = dict(request.headers)\n      if \"Authorization\" in headers:\n        headers[\"Authorization\"] = \"Bearer [REDACTED]\"\n      if \"api-key\" in headers:\n        headers[\"api-key\"] = \"[REDACTED]\"\n      \n      # Create a log-friendly representation\n      request_data = {\n        \"method\": request.method,\n        \"url\": str(request.url),\n        \"headers\": headers,\n        \"body\": payload\n      }\n    \n      # Log the request data\n      print(\"Incoming request to /v1/chat/completions:\")\n      print(json.dumps(request_data, indent=2))\n    except Exception as e:\n      print(f\"Error logging request: {str(e)}\")\n\n  model = payload.get(\"model\", None)\n  print(f\"Litellm proxy: calling model: {model}\")\n\n  api_key = payload.pop(\"api_key\", None)\n\n  if not api_key:\n    api_key = request.headers.get(\"Authorization\")\n\n  if not api_key:\n    api_key = request.headers.get(\"api-key\")\n\n  if api_key and api_key.startswith(\"Bearer \"):\n    api_key = api_key.replace(\"Bearer \", \"\")\n\n  # api key optional for local/ollama models, so no need to error if not provided\n\n  # clean up for ollama if needed\n  payload = normalise_for_ollama(payload)\n\n  try:\n    if payload.get(\"stream\"):\n      \n      try:\n        response_stream = completion(api_key=api_key, **payload)\n      except Exception as e:\n        return error_response(e)\n      def stream_generator():\n        try:  \n          for chunk in response_stream:\n            yield f\"data: {json.dumps(chunk.to_dict())}\\n\\n\"\n          yield \"data: [DONE]\\n\\n\"\n        except Exception as e:\n          # surface the problem to the client _inside_ the SSE stream\n          yield f\"data: {json.dumps({'error': str(e)})}\\n\\n\"\n          return\n\n        finally:\n          try:\n            response_stream.close()\n          except AttributeError:\n            pass\n\n      print(f\"Litellm proxy: Initiating streaming response for model: {payload.get('model', 'unknown')}\")\n      return StreamingResponse(stream_generator(), media_type=\"text/event-stream\")\n\n    else:\n      print(f\"Litellm proxy: Non-streaming response requested for model: {payload.get('model', 'unknown')}\")\n      try:\n        result = completion(api_key=api_key, **payload)\n      except Exception as e:\n        return error_response(e)\n      return JSONResponse(content=result)\n\n  except Exception as e:\n    err_msg = str(e)\n    print(f\"Litellm proxy: Error: {err_msg}\")\n    status_match = re.search(r\"status code: (\\d+)\", err_msg)\n    if status_match:\n      status_code = int(status_match.group(1))\n    else:\n      status_code = 500\n    return JSONResponse(\n      status_code=status_code,\n      content={\"error\": err_msg}\n    )\n\ndef error_response(exc: Exception) -> JSONResponse:\n  status = getattr(exc, \"status_code\", 500)\n  retry_after = (\n    getattr(getattr(exc, \"response\", None), \"headers\", {})\n    .get(\"Retry-After\")\n  )\n  hdrs = {\"Retry-After\": retry_after} if retry_after else {}\n  return JSONResponse(status_code=status, content={\"error\": str(exc)}, headers=hdrs)\n\ndef normalise_for_ollama(p):\n  if not p.get(\"model\", \"\").startswith(\"ollama\"):\n    return p\n\n  # flatten content parts\n  for m in p.get(\"messages\", []):\n    if isinstance(m[\"content\"], list):  # [{type:\"text\", text:\"…\"}]\n        m[\"content\"] = \"\".join(part.get(\"text\", \"\")\n                                for part in m[\"content\"]\n                                if part.get(\"type\") == \"text\")\n\n  # drop params Ollama ignores\n  for k in (\"top_p\", \"temperature\", \"presence_penalty\",\n            \"tool_choice\", \"tools\", \"seed\"):\n      p.pop(k, None)\n\n  return p"
  },
  {
    "path": "app/server/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"plandex-server/model\"\n\t\"plandex-server/routes\"\n\t\"plandex-server/setup\"\n\n\t\"github.com/gorilla/mux\"\n)\n\nfunc main() {\n\t// Configure the default logger to include milliseconds in timestamps\n\tlog.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)\n\n\troutes.RegisterHandlePlandex(func(router *mux.Router, path string, isStreaming bool, handler routes.PlandexHandler) *mux.Route {\n\t\treturn router.HandleFunc(path, handler)\n\t})\n\n\terr := model.EnsureLiteLLM(2)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Failed to start LiteLLM proxy: %v\", err))\n\t}\n\tsetup.RegisterShutdownHook(func() {\n\t\tmodel.ShutdownLiteLLMServer()\n\t})\n\n\tr := mux.NewRouter()\n\troutes.AddHealthRoutes(r)\n\troutes.AddApiRoutes(r)\n\troutes.AddProxyableApiRoutes(r)\n\tsetup.MustLoadIp()\n\tsetup.MustInitDb()\n\tsetup.StartServer(r, nil, nil)\n\tos.Exit(0)\n}\n"
  },
  {
    "path": "app/server/migrations/2023120500_init.down.sql",
    "content": "DROP TABLE IF EXISTS branches;\n\nDROP TABLE IF EXISTS convo_summaries;\nDROP TABLE IF EXISTS plan_builds;\n\nDROP TABLE IF EXISTS users_projects;\nDROP TABLE IF EXISTS plans;\nDROP TABLE IF EXISTS projects;\nDROP TABLE IF EXISTS orgs_users;\n\nDROP TABLE IF EXISTS email_verifications;\nDROP TABLE IF EXISTS auth_tokens;\nDROP TABLE IF EXISTS invites;\n\nDROP TABLE IF EXISTS orgs;\nDROP TABLE IF EXISTS users;\n"
  },
  {
    "path": "app/server/migrations/2023120500_init.up.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n\nCREATE OR REPLACE FUNCTION update_updated_at_column()\nRETURNS TRIGGER AS $$\nBEGIN\n    NEW.updated_at = NOW();\n    RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TABLE IF NOT EXISTS users (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  name VARCHAR(255) NOT NULL,\n  email VARCHAR(255) NOT NULL,\n  domain VARCHAR(255) NOT NULL,\n  is_trial BOOLEAN NOT NULL,\n  num_non_draft_plans INTEGER NOT NULL DEFAULT 0,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_users_modtime BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nALTER TABLE users ADD UNIQUE (email);\nCREATE INDEX users_domain_idx ON users(domain);\n\nCREATE TABLE IF NOT EXISTS orgs (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  name VARCHAR(255) NOT NULL,\n  domain VARCHAR(255),\n  auto_add_domain_users BOOLEAN NOT NULL DEFAULT FALSE,\n  owner_id UUID NOT NULL REFERENCES users(id),\n  is_trial BOOLEAN NOT NULL,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_orgs_modtime BEFORE UPDATE ON orgs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nALTER TABLE orgs ADD UNIQUE (domain);\n\nCREATE TABLE IF NOT EXISTS orgs_users (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_orgs_users_modtime BEFORE UPDATE ON orgs_users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE INDEX orgs_users_user_idx ON orgs_users(user_id);\nCREATE INDEX orgs_users_org_idx ON orgs_users(org_id);\n\nCREATE TABLE IF NOT EXISTS invites (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  name VARCHAR(255) NOT NULL,\n  email VARCHAR(255) NOT NULL,\n  inviter_id UUID NOT NULL REFERENCES users(id),\n  invitee_id UUID REFERENCES users(id),\n  accepted_at TIMESTAMP,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_invites_modtime BEFORE UPDATE ON invites FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE INDEX invites_pending_idx ON invites(org_id, (accepted_at IS NULL));\nCREATE INDEX invites_email_idx ON invites(email, (accepted_at IS NULL));\nCREATE INDEX invites_org_user_idx ON invites(org_id, invitee_id);\n\nCREATE TABLE IF NOT EXISTS auth_tokens (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n  token_hash VARCHAR(64) NOT NULL,\n  is_trial BOOLEAN NOT NULL DEFAULT FALSE,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  deleted_at TIMESTAMP\n);\nCREATE UNIQUE INDEX auth_tokens_idx ON auth_tokens(token_hash);\n\nCREATE TABLE IF NOT EXISTS email_verifications (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  email VARCHAR(255) NOT NULL,\n  pin_hash VARCHAR(64) NOT NULL,\n  user_id UUID REFERENCES users(id),  \n  auth_token_id UUID REFERENCES auth_tokens(id),\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_email_verifications_modtime BEFORE UPDATE ON email_verifications FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE UNIQUE INDEX email_verifications_idx ON email_verifications(pin_hash, email, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS projects (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  name VARCHAR(255) NOT NULL,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_projects_modtime BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE TABLE IF NOT EXISTS plans (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n  project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n  name VARCHAR(255) NOT NULL,  \n  shared_with_org_at TIMESTAMP,\n  total_replies INTEGER NOT NULL DEFAULT 0,\n  active_branches INTEGER NOT NULL DEFAULT 0,\n  archived_at TIMESTAMP,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_plans_modtime BEFORE UPDATE ON plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE INDEX plans_name_idx ON plans(project_id, owner_id, name);\nCREATE INDEX plans_archived_idx ON plans(project_id, owner_id, archived_at);\n\nCREATE TABLE IF NOT EXISTS branches (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n  plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,\n  parent_branch_id UUID REFERENCES branches(id) ON DELETE CASCADE,\n  name VARCHAR(255) NOT NULL,\n  status VARCHAR(32) NOT NULL,\n  error TEXT,\n  context_tokens INTEGER NOT NULL DEFAULT 0,\n  convo_tokens INTEGER NOT NULL DEFAULT 0,\n  shared_with_org_at TIMESTAMP,\n  archived_at TIMESTAMP,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  deleted_at TIMESTAMP\n);\nCREATE TRIGGER update_branches_modtime BEFORE UPDATE ON branches FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE UNIQUE INDEX branches_name_idx ON branches(plan_id, name, archived_at, deleted_at);\n\n\nCREATE TABLE IF NOT EXISTS users_projects (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n  project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n  last_active_plan_id UUID REFERENCES plans(id),\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_users_projects_modtime BEFORE UPDATE ON users_projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE INDEX users_projects_idx ON users_projects(user_id, org_id, project_id);\n\nCREATE TABLE IF NOT EXISTS convo_summaries (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,\n  latest_convo_message_id UUID NOT NULL,\n  latest_convo_message_created_at TIMESTAMP NOT NULL,\n  summary TEXT NOT NULL,\n  tokens INTEGER NOT NULL,\n  num_messages INTEGER NOT NULL,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE IF NOT EXISTS plan_builds (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,\n  convo_message_id UUID NOT NULL,\n  file_path VARCHAR(255) NOT NULL,\n  error TEXT,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_plan_builds_modtime BEFORE UPDATE ON plan_builds FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();"
  },
  {
    "path": "app/server/migrations/2024011700_rbac.down.sql",
    "content": "ALTER TABLE orgs_users DROP COLUMN org_role_id;\nALTER TABLE invites DROP COLUMN org_role_id;\n\nDROP TABLE IF EXISTS org_roles_permissions;\nDROP TABLE IF EXISTS permissions;\nDROP TABLE IF EXISTS org_roles;\n\n\n"
  },
  {
    "path": "app/server/migrations/2024011700_rbac.up.sql",
    "content": "\nCREATE TABLE IF NOT EXISTS org_roles (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,\n  name VARCHAR(255) NOT NULL,\n  label VARCHAR(255) NOT NULL,\n  description TEXT NOT NULL,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_org_roles_modtime BEFORE UPDATE ON org_roles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE UNIQUE INDEX org_roles_org_idx ON org_roles(org_id, name);\n\nALTER TABLE orgs_users ADD COLUMN org_role_id UUID NOT NULL REFERENCES org_roles(id) ON DELETE RESTRICT;\nCREATE INDEX orgs_users_org_role_idx ON orgs_users(org_id, org_role_id);\n\nALTER TABLE invites ADD COLUMN org_role_id UUID NOT NULL REFERENCES org_roles(id) ON DELETE RESTRICT;\nCREATE INDEX invites_org_role_idx ON invites(org_id, org_role_id);\n\nCREATE TABLE IF NOT EXISTS permissions (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  name VARCHAR(255) NOT NULL,\n  description TEXT NOT NULL,\n  resource_id UUID,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\n\nCREATE TABLE IF NOT EXISTS org_roles_permissions (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_role_id UUID NOT NULL REFERENCES org_roles(id) ON DELETE CASCADE,\n  permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\n\nINSERT INTO org_roles (name, label, description) VALUES\n  ('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'),\n  ('billing_admin', 'Billing Admin', 'Can manage billing'),\n  ('admin', 'Admin', 'Can read and update any plan, invite other admins/members'),\n  ('member', 'Member', 'Can read and update their own plans or plans shared with them');\n\nDO $$\nDECLARE\n  owner_org_role_id UUID;\n  billing_admin_org_role_id UUID;\n  admin_org_role_id UUID;\n  member_org_role_id UUID;\nBEGIN\n  SELECT id INTO owner_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'owner';\n  SELECT id INTO billing_admin_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'billing_admin';\n  SELECT id INTO admin_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'admin';\n  SELECT id INTO member_org_role_id FROM org_roles WHERE org_id IS NULL AND name = 'member';\n\n  INSERT INTO permissions (name, description, resource_id) VALUES\n    ('delete_org', 'Delete an org', NULL),\n    ('manage_email_domain_auth', 'Configure whether orgs_users from the org''s email domain are auto-admitted to org', NULL),\n    ('manage_billing', 'Manage an org''s billing', NULL),\n    \n    ('invite_user', 'Invite owners to an org', owner_org_role_id),\n    ('invite_user', 'Invite admins to an org', admin_org_role_id),\n    ('invite_user', 'Invite billing admins to an org', billing_admin_org_role_id),\n    ('invite_user', 'Invite members to an org', member_org_role_id),\n    \n    ('remove_user', 'Remove owners from an org', owner_org_role_id),\n    ('remove_user', 'Remove admins from an org', admin_org_role_id),\n    ('remove_user', 'Remove billing admins from an org', billing_admin_org_role_id),\n    ('remove_user', 'Remove members from an org', member_org_role_id),\n    \n    ('set_user_role', 'Update an owner''s role in an org', owner_org_role_id),\n    ('set_user_role', 'Update an admin''s role in an org', admin_org_role_id),\n    ('set_user_role', 'Update a billing admin''s role in an org', billing_admin_org_role_id),\n    ('set_user_role', 'Update a member''s role in an org', member_org_role_id),\n\n    ('list_org_roles', 'List org roles', NULL),\n    \n    ('create_project', 'Create a project', NULL),\n    ('rename_any_project', 'Rename a project', NULL),\n    ('delete_any_project', 'Delete a project', NULL),\n\n    ('create_plan', 'Create a plan', NULL),\n\n    ('manage_any_plan_shares', 'Unshare a plan any user shared', NULL),\n    ('rename_any_plan', 'Rename a plan', NULL),\n    ('delete_any_plan', 'Delete a plan', NULL),\n    ('update_any_plan', 'Update a plan', NULL),\n    ('archive_any_plan', 'Archive a plan', NULL);\nEND $$;\n\n-- Insert all permissions for the 'org owner' role\nINSERT INTO org_roles_permissions (org_role_id, permission_id)\nSELECT \n    (SELECT id FROM org_roles WHERE name = 'owner') AS org_role_id, \n    p.id AS permission_id\nFROM \n    permissions p;\n\n-- Insert all permissions except specific ones and those exclusive to 'owner' or 'billing admin' for the 'org admin' role\nINSERT INTO org_roles_permissions (org_role_id, permission_id)\nSELECT \n    (SELECT id FROM org_roles WHERE name = 'admin') AS org_role_id, \n    p.id AS permission_id\nFROM \n    permissions p\nWHERE \n    p.name NOT IN ('delete_org', 'manage_email_domain_auth', 'manage_billing')\n    AND NOT EXISTS (\n        SELECT 1 FROM permissions p2\n        WHERE p2.resource_id IN (SELECT id FROM org_roles WHERE name IN ('owner', 'billing_admin'))\n        AND p2.id = p.id\n    );\n\nINSERT INTO org_roles_permissions (org_role_id, permission_id)\nSELECT \n    (SELECT id FROM org_roles WHERE name = 'billing_admin') AS org_role_id, \n    p.id AS permission_id\nFROM\n    permissions p\nWHERE \n    p.name IN (\n      'manage_billing'\n    );\n\nINSERT INTO org_roles_permissions (org_role_id, permission_id)\nSELECT \n    (SELECT id FROM org_roles WHERE name = 'member') AS org_role_id, \n    p.id AS permission_id\nFROM\n    permissions p\nWHERE \n    p.name IN (\n      'create_project',\n      'create_plan'\n    );\n"
  },
  {
    "path": "app/server/migrations/2024012400_streams.down.sql",
    "content": "DROP TABLE IF EXISTS model_streams;\n-- DROP TABLE IF EXISTS model_stream_subscriptions;"
  },
  {
    "path": "app/server/migrations/2024012400_streams.up.sql",
    "content": "CREATE TABLE IF NOT EXISTS model_streams (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,\n  branch VARCHAR(255) NOT NULL,\n  internal_ip VARCHAR(45) NOT NULL,\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  finished_at TIMESTAMP\n);\n\nCREATE UNIQUE INDEX model_streams_plan_idx ON model_streams(plan_id, branch, finished_at);\n\n-- CREATE TABLE IF NOT EXISTS model_stream_subscriptions (\n--   id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n--   org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n--   plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,\n--   model_stream_id UUID NOT NULL REFERENCES model_streams(id) ON DELETE CASCADE,\n--   user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n--   user_ip VARCHAR(45) NOT NULL,\n--   created_at TIMESTAMP NOT NULL DEFAULT NOW()\n--   finished_at TIMESTAMP\n-- );\n\n"
  },
  {
    "path": "app/server/migrations/2024012500_locks.down.sql",
    "content": "DROP TABLE IF EXISTS repo_locks;"
  },
  {
    "path": "app/server/migrations/2024012500_locks.up.sql",
    "content": "CREATE UNLOGGED TABLE IF NOT EXISTS repo_locks (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n  plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,\n  plan_build_id UUID REFERENCES plan_builds(id) ON DELETE CASCADE,\n  scope VARCHAR(1) NOT NULL,\n  branch VARCHAR(255),\n  created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX repo_locks_plan_idx ON repo_locks(plan_id);\n"
  },
  {
    "path": "app/server/migrations/2024013000_plan_build_convo_ids.down.sql",
    "content": "ALTER TABLE plan_builds\n  RENAME COLUMN convo_message_ids TO convo_message_id;\n\nALTER TABLE plan_builds\n  ALTER COLUMN convo_message_id TYPE UUID USING (convo_message_id[1]);"
  },
  {
    "path": "app/server/migrations/2024013000_plan_build_convo_ids.up.sql",
    "content": "ALTER TABLE plan_builds\n  RENAME COLUMN convo_message_id TO convo_message_ids;\n\nALTER TABLE plan_builds\n  ALTER COLUMN convo_message_ids TYPE UUID[] USING ARRAY[convo_message_ids];\n"
  },
  {
    "path": "app/server/migrations/2024020800_heartbeats.down.sql",
    "content": "ALTER TABLE repo_locks DROP COLUMN last_heartbeat_at;\nALTER TABLE model_streams DROP COLUMN last_heartbeat_at;"
  },
  {
    "path": "app/server/migrations/2024020800_heartbeats.up.sql",
    "content": "ALTER TABLE repo_locks ADD COLUMN last_heartbeat_at TIMESTAMP NOT NULL DEFAULT NOW();\nALTER TABLE model_streams ADD COLUMN last_heartbeat_at TIMESTAMP NOT NULL DEFAULT NOW();"
  },
  {
    "path": "app/server/migrations/2024022000_revert_plan_build_convo_ids.down.sql",
    "content": "ALTER TABLE plan_builds\n  RENAME COLUMN convo_message_id TO convo_message_ids;\n\nALTER TABLE plan_builds\n  ALTER COLUMN convo_message_ids TYPE UUID[] USING ARRAY[convo_message_ids];\n"
  },
  {
    "path": "app/server/migrations/2024022000_revert_plan_build_convo_ids.up.sql",
    "content": "ALTER TABLE plan_builds\n  RENAME COLUMN convo_message_ids TO convo_message_id;\n\nALTER TABLE plan_builds\n  ALTER COLUMN convo_message_id TYPE UUID USING (convo_message_id[1]);"
  },
  {
    "path": "app/server/migrations/2024032700_remove_billing_admin.down.sql",
    "content": ""
  },
  {
    "path": "app/server/migrations/2024032700_remove_billing_admin.up.sql",
    "content": "DELETE FROM org_roles WHERE name = 'billing_admin';"
  },
  {
    "path": "app/server/migrations/2024032701_drop_users_projects.down.sql",
    "content": "CREATE TABLE IF NOT EXISTS users_projects (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n  project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n  last_active_plan_id UUID REFERENCES plans(id),\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_users_projects_modtime BEFORE UPDATE ON users_projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE INDEX users_projects_idx ON users_projects(user_id, org_id, project_id);"
  },
  {
    "path": "app/server/migrations/2024032701_drop_users_projects.up.sql",
    "content": "DROP TABLE IF EXISTS users_projects;"
  },
  {
    "path": "app/server/migrations/2024040400_add_orgs_users_unique.down.sql",
    "content": "ALTER TABLE orgs_users DROP CONSTRAINT org_user_unique;"
  },
  {
    "path": "app/server/migrations/2024040400_add_orgs_users_unique.up.sql",
    "content": "-- clean up any duplicates added mistakenly earlier\nWITH ranked_duplicates AS (\n  SELECT id,\n         ROW_NUMBER() OVER (PARTITION BY org_id, user_id ORDER BY created_at) AS rn\n  FROM orgs_users\n)\nDELETE FROM orgs_users\nWHERE id IN (\n  SELECT id FROM ranked_duplicates WHERE rn > 1\n);\n \nALTER TABLE orgs_users ADD CONSTRAINT org_user_unique UNIQUE (org_id, user_id);\n"
  },
  {
    "path": "app/server/migrations/2024041500_model_sets_models.down.sql",
    "content": "DROP TABLE IF EXISTS model_sets;\nDROP TABLE IF EXISTS custom_models;"
  },
  {
    "path": "app/server/migrations/2024041500_model_sets_models.up.sql",
    "content": "CREATE TABLE IF NOT EXISTS model_sets (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n\n  name VARCHAR(255) NOT NULL,\n  description TEXT,\n\n  planner JSON,\n  plan_summary JSON,\n  builder JSON,\n  namer JSON,\n  commit_msg JSON,\n  exec_status JSON,\n\n  created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX model_sets_org_idx ON model_sets(org_id);\n\nCREATE TABLE IF NOT EXISTS custom_models (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n  \n  provider VARCHAR(255) NOT NULL,\n  custom_provider VARCHAR(255),\n  base_url VARCHAR(255) NOT NULL,\n  model_name VARCHAR(255) NOT NULL,\n  description TEXT,\n  max_tokens INTEGER NOT NULL,\n  api_key_env_var VARCHAR(255),\n\n  is_openai_compatible BOOLEAN NOT NULL,\n  has_json_mode BOOLEAN NOT NULL,\n  has_streaming BOOLEAN NOT NULL,\n  has_function_calling BOOLEAN NOT NULL,\n  has_streaming_function_calls BOOLEAN NOT NULL,\n\n  default_max_convo_tokens INTEGER NOT NULL,\n  default_reserved_output_tokens INTEGER NOT NULL,\n\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\n\nCREATE TRIGGER update_custom_models_modtime BEFORE UPDATE ON custom_models FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE INDEX custom_models_org_idx ON custom_models(org_id);\n"
  },
  {
    "path": "app/server/migrations/2024042600_default_plan_settings.down.sql",
    "content": "DROP TABLE IF EXISTS default_plan_settings;"
  },
  {
    "path": "app/server/migrations/2024042600_default_plan_settings.up.sql",
    "content": "CREATE TABLE IF NOT EXISTS default_plan_settings (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n\n  plan_settings JSON,\n\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  created_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_default_plan_settings_modtime BEFORE UPDATE ON default_plan_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE UNIQUE INDEX default_plan_settings_org_idx ON default_plan_settings(org_id);\n"
  },
  {
    "path": "app/server/migrations/2024091800_sign_in_codes.down.sql",
    "content": "DROP TABLE IF EXISTS sign_in_codes;"
  },
  {
    "path": "app/server/migrations/2024091800_sign_in_codes.up.sql",
    "content": "CREATE TABLE IF NOT EXISTS sign_in_codes (\n  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  pin_hash VARCHAR(64) NOT NULL,\n  user_id UUID REFERENCES users(id),  \n  org_id UUID REFERENCES orgs(id),\n  auth_token_id UUID REFERENCES auth_tokens(id),\n  created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE TRIGGER update_sign_in_codes_modtime BEFORE UPDATE ON sign_in_codes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE UNIQUE INDEX sign_in_codes_idx ON sign_in_codes(pin_hash, created_at DESC);"
  },
  {
    "path": "app/server/migrations/2024092100_remove_trial_fields.down.sql",
    "content": "ALTER TABLE auth_tokens ADD COLUMN is_trial BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE users ADD COLUMN is_trial BOOLEAN NOT NULL DEFAULT FALSE;"
  },
  {
    "path": "app/server/migrations/2024092100_remove_trial_fields.up.sql",
    "content": "ALTER TABLE auth_tokens DROP COLUMN is_trial;\nALTER TABLE users DROP COLUMN is_trial;"
  },
  {
    "path": "app/server/migrations/2024100900_update_locks.down.sql",
    "content": "-- Revert user_id to NOT NULL if no NULL values exist\nDO $$ \nBEGIN\n    -- Check for NULL values in user_id\n    IF EXISTS (SELECT 1 FROM repo_locks WHERE user_id IS NULL) THEN\n        RAISE EXCEPTION 'Cannot revert to NOT NULL, as there are rows with NULL values in user_id.';\n    ELSE\n        -- Proceed with setting the columns to NOT NULL\n        ALTER TABLE repo_locks\n          ALTER COLUMN user_id SET NOT NULL;\n    END IF;\nEND $$;\n"
  },
  {
    "path": "app/server/migrations/2024100900_update_locks.up.sql",
    "content": "ALTER TABLE repo_locks\n  ALTER COLUMN user_id DROP NOT NULL;\n"
  },
  {
    "path": "app/server/migrations/2024121400_plan_config.down.sql",
    "content": "ALTER TABLE plans DROP COLUMN IF EXISTS plan_config;\n\nALTER TABLE users DROP COLUMN IF EXISTS default_plan_config;"
  },
  {
    "path": "app/server/migrations/2024121400_plan_config.up.sql",
    "content": "ALTER TABLE plans ADD COLUMN IF NOT EXISTS plan_config JSON;\n\nALTER TABLE users ADD COLUMN IF NOT EXISTS default_plan_config JSON;\n"
  },
  {
    "path": "app/server/migrations/2025012600_update_custom_models.down.sql",
    "content": "ALTER TABLE custom_models DROP COLUMN preferred_output_format;\n\nALTER TABLE custom_models ADD COLUMN has_streaming BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE custom_models ADD COLUMN has_function_calling BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE custom_models ADD COLUMN has_streaming_function_calls BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE custom_models ADD COLUMN has_json_mode BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE custom_models ADD COLUMN is_openai_compatible BOOLEAN NOT NULL DEFAULT FALSE;\n\nALTER TABLE custom_models DROP COLUMN has_image_support;\n"
  },
  {
    "path": "app/server/migrations/2025012600_update_custom_models.up.sql",
    "content": "ALTER TABLE custom_models ADD COLUMN preferred_output_format VARCHAR(32) NOT NULL DEFAULT 'xml';\n\nALTER TABLE custom_models DROP COLUMN has_streaming_function_calls;\nALTER TABLE custom_models DROP COLUMN has_json_mode;\nALTER TABLE custom_models DROP COLUMN has_streaming;\nALTER TABLE custom_models DROP COLUMN has_function_calling;\nALTER TABLE custom_models DROP COLUMN is_openai_compatible;\n\nALTER TABLE custom_models ADD COLUMN has_image_support BOOLEAN NOT NULL DEFAULT FALSE;"
  },
  {
    "path": "app/server/migrations/2025021101_locks_unique.down.sql",
    "content": "DROP TABLE IF EXISTS lockable_plan_ids;\nDROP INDEX IF EXISTS repo_locks_single_write_lock;"
  },
  {
    "path": "app/server/migrations/2025021101_locks_unique.up.sql",
    "content": "\nCREATE UNIQUE INDEX repo_locks_single_write_lock\n  ON repo_locks(plan_id)\n  WHERE (scope = 'w');\n\nCREATE TABLE IF NOT EXISTS lockable_plan_ids (\n  plan_id UUID NOT NULL PRIMARY KEY REFERENCES plans(id) ON DELETE CASCADE\n);"
  },
  {
    "path": "app/server/migrations/2025022700_remove_models_col.down.sql",
    "content": "ALTER TABLE custom_models ADD COLUMN default_reserved_output_tokens INTEGER NOT NULL;"
  },
  {
    "path": "app/server/migrations/2025022700_remove_models_col.up.sql",
    "content": "ALTER TABLE custom_models DROP COLUMN default_reserved_output_tokens;"
  },
  {
    "path": "app/server/migrations/2025031300_add_model_roles.down.sql",
    "content": "ALTER TABLE model_sets DROP COLUMN context_loader;\nALTER TABLE model_sets DROP COLUMN whole_file_builder;\nALTER TABLE model_sets DROP COLUMN coder;"
  },
  {
    "path": "app/server/migrations/2025031300_add_model_roles.up.sql",
    "content": "ALTER TABLE model_sets ADD COLUMN context_loader JSON;\nALTER TABLE model_sets ADD COLUMN whole_file_builder JSON;\nALTER TABLE model_sets ADD COLUMN coder JSON;"
  },
  {
    "path": "app/server/migrations/2025031900_add_custom_model_cols.down.sql",
    "content": "ALTER TABLE custom_models DROP COLUMN max_output_tokens;\nALTER TABLE custom_models DROP COLUMN reserved_output_tokens;\nALTER TABLE custom_models DROP COLUMN model_id;\n"
  },
  {
    "path": "app/server/migrations/2025031900_add_custom_model_cols.up.sql",
    "content": "ALTER TABLE custom_models \n  ADD COLUMN max_output_tokens INTEGER NOT NULL,\n  ADD COLUMN reserved_output_tokens INTEGER NOT NULL,\n  ADD COLUMN model_id VARCHAR(255) NOT NULL;"
  },
  {
    "path": "app/server/migrations/2025032400_sign_in_codes_on_delete.down.sql",
    "content": "ALTER TABLE sign_in_codes\nDROP CONSTRAINT sign_in_codes_org_id_fkey,\nADD CONSTRAINT sign_in_codes_org_id_fkey\nFOREIGN KEY (org_id)\nREFERENCES orgs(id);\n\n"
  },
  {
    "path": "app/server/migrations/2025032400_sign_in_codes_on_delete.up.sql",
    "content": "ALTER TABLE sign_in_codes\nDROP CONSTRAINT sign_in_codes_org_id_fkey,\nADD CONSTRAINT sign_in_codes_org_id_fkey\nFOREIGN KEY (org_id)\nREFERENCES orgs(id)\nON DELETE SET NULL;\n\n"
  },
  {
    "path": "app/server/migrations/2025051600_custom_models_refactor.down.sql",
    "content": "DROP TABLE IF EXISTS custom_models;\nDROP TABLE IF EXISTS custom_providers;\n\nALTER TABLE custom_models_legacy RENAME TO custom_models;\n"
  },
  {
    "path": "app/server/migrations/2025051600_custom_models_refactor.up.sql",
    "content": "\nBEGIN;\nALTER TABLE custom_models RENAME TO custom_models_legacy;\n\nCREATE TABLE IF NOT EXISTS custom_models (\n  id                       UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id                   UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n\n  model_id                 VARCHAR(255) NOT NULL,\n  description              TEXT,\n  publisher                VARCHAR(64) NOT NULL DEFAULT '',\n\n  max_tokens               INTEGER NOT NULL,\n  default_max_convo_tokens INTEGER NOT NULL,\n  max_output_tokens        INTEGER NOT NULL,\n  reserved_output_tokens   INTEGER NOT NULL,\n\n  has_image_support        BOOLEAN NOT NULL DEFAULT FALSE,\n  preferred_output_format  VARCHAR(32) NOT NULL DEFAULT 'xml',\n\n  system_prompt_disabled       BOOLEAN NOT NULL DEFAULT FALSE,\n  role_params_disabled         BOOLEAN NOT NULL DEFAULT FALSE,\n  stop_disabled                BOOLEAN NOT NULL DEFAULT FALSE,\n  predicted_output_enabled     BOOLEAN NOT NULL DEFAULT FALSE,\n  reasoning_effort_enabled     BOOLEAN NOT NULL DEFAULT FALSE,\n  reasoning_effort             VARCHAR(32) NOT NULL DEFAULT '',\n  include_reasoning            BOOLEAN NOT NULL DEFAULT FALSE,\n  reasoning_budget             INTEGER NOT NULL DEFAULT 0,\n  supports_cache_control       BOOLEAN NOT NULL DEFAULT FALSE,\n  single_message_no_system_prompt BOOLEAN NOT NULL DEFAULT FALSE,\n  token_estimate_padding_pct   FLOAT NOT NULL DEFAULT 0.0,\n\n  providers JSON NOT NULL DEFAULT '[]',\n\n  created_at               TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at               TIMESTAMP NOT NULL DEFAULT NOW()\n);\nCREATE INDEX IF NOT EXISTS cmv_org_idx ON custom_models(org_id);\nCREATE UNIQUE INDEX IF NOT EXISTS cmv_unique_idx ON custom_models(org_id, model_id);\n\nCREATE TRIGGER cmv_modtime BEFORE UPDATE ON custom_models\n  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE TABLE IF NOT EXISTS custom_providers (\n  id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n  org_id          UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,\n\n  name            VARCHAR(255) NOT NULL,          \n  base_url        VARCHAR(255) NOT NULL,\n  skip_auth       BOOLEAN NOT NULL DEFAULT FALSE,\n  api_key_env_var VARCHAR(255),\n\n  extra_auth_vars JSON NOT NULL DEFAULT '[]',\n\n  created_at      TIMESTAMP NOT NULL DEFAULT NOW(),\n  updated_at      TIMESTAMP NOT NULL DEFAULT NOW()                          \n);\nCREATE INDEX IF NOT EXISTS cp_org_idx ON custom_providers(org_id);\nCREATE UNIQUE INDEX IF NOT EXISTS cp_unique_idx ON custom_providers(org_id, name);\nCREATE TRIGGER cp_modtime BEFORE UPDATE ON custom_providers\n  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\n/* ---- migrate base rows into the new custom_models ------------------ */\nINSERT INTO custom_models (\n    id, org_id, model_id, description,\n    max_tokens, default_max_convo_tokens,\n    max_output_tokens, reserved_output_tokens,\n    has_image_support, preferred_output_format,\n\n    providers,                 -- <-- aggregated JSON array\n    created_at, updated_at\n)\nSELECT\n    id, org_id, model_id, description,\n    max_tokens, default_max_convo_tokens,\n    max_output_tokens, reserved_output_tokens,\n    has_image_support, preferred_output_format,\n\n    /* -------- build a one-element providers array -------- */\n    json_build_array(\n        json_build_object(\n            'provider',        provider,\n            'custom_provider', custom_provider,\n            'model_name',      model_name\n        )\n    )::json,\n\n    created_at, updated_at\nFROM custom_models_legacy\nON CONFLICT (org_id, model_id) DO NOTHING;\n\n/* ---- migrate unique custom providers ------------------------------- */\nWITH src AS (\n  SELECT DISTINCT\n         org_id,\n         custom_provider AS name,\n         base_url,\n         api_key_env_var\n  FROM   custom_models_legacy\n  WHERE  custom_provider IS NOT NULL\n)\nINSERT INTO custom_providers (org_id, name, base_url, api_key_env_var)\nSELECT org_id, name, base_url, api_key_env_var\nFROM   src\nON CONFLICT (org_id, name) DO NOTHING;\n\nCOMMIT;"
  },
  {
    "path": "app/server/migrations/2025052200_model_pack_cols.down.sql",
    "content": "ALTER TABLE model_sets\n  DROP COLUMN IF EXISTS updated_at;\n\nDROP TRIGGER IF EXISTS model_set_modtime ON model_sets;\n\nDROP INDEX IF EXISTS model_set_unique_idx;\n"
  },
  {
    "path": "app/server/migrations/2025052200_model_pack_cols.up.sql",
    "content": "ALTER TABLE model_sets\n  ADD COLUMN updated_at TIMESTAMP NOT NULL DEFAULT NOW();\n\nCREATE TRIGGER model_set_modtime BEFORE UPDATE ON model_sets\n  FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();\n\nCREATE UNIQUE INDEX IF NOT EXISTS model_set_unique_idx ON model_sets(org_id, name);"
  },
  {
    "path": "app/server/migrations/2025070200_add_org_user_config.down.sql",
    "content": "ALTER TABLE orgs_users DROP COLUMN config;"
  },
  {
    "path": "app/server/migrations/2025070200_add_org_user_config.up.sql",
    "content": "ALTER TABLE orgs_users ADD COLUMN config JSON;"
  },
  {
    "path": "app/server/model/client.go",
    "content": "package model\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"plandex-server/db\"\n\t\"plandex-server/types\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\n// note that we are *only* using streaming requests now\n// non-streaming request handling has been removed completely\n// streams offer more predictable cancellation partial results\n\nconst (\n\tACTIVE_STREAM_CHUNK_TIMEOUT          = time.Duration(60) * time.Second\n\tUSAGE_CHUNK_TIMEOUT                  = time.Duration(10) * time.Second\n\tMAX_ADDITIONAL_RETRIES_WITH_FALLBACK = 1\n\tMAX_RETRIES_WITHOUT_FALLBACK         = 3\n\tMAX_RETRY_DELAY_SECONDS              = 10\n)\n\nvar httpClient = &http.Client{}\n\ntype ClientInfo struct {\n\tClient         *openai.Client\n\tProviderConfig shared.ModelProviderConfigSchema\n\tApiKey         string\n\tOpenAIOrgId    string\n}\n\nfunc InitClients(authVars map[string]string, settings *shared.PlanSettings, orgUserConfig *shared.OrgUserConfig) map[string]ClientInfo {\n\tclients := make(map[string]ClientInfo)\n\tproviders := shared.GetProvidersForAuthVars(authVars, settings, orgUserConfig)\n\n\tfor _, provider := range providers {\n\t\tclients[provider.ToComposite()] = newClient(provider, authVars)\n\t}\n\n\treturn clients\n}\n\nfunc newClient(providerConfig shared.ModelProviderConfigSchema, authVars map[string]string) ClientInfo {\n\tvar apiKey string\n\tif providerConfig.ApiKeyEnvVar != \"\" {\n\t\tapiKey = authVars[providerConfig.ApiKeyEnvVar]\n\t} else if providerConfig.HasClaudeMaxAuth {\n\t\tapiKey = authVars[shared.AnthropicClaudeMaxTokenEnvVar]\n\t}\n\n\tconfig := openai.DefaultConfig(apiKey)\n\tconfig.BaseURL = providerConfig.BaseUrl\n\n\tvar openAIOrgId string\n\tif providerConfig.Provider == shared.ModelProviderOpenAI && authVars[\"OPENAI_ORG_ID\"] != \"\" {\n\t\topenAIOrgId = authVars[\"OPENAI_ORG_ID\"]\n\t\tconfig.OrgID = openAIOrgId\n\t}\n\n\treturn ClientInfo{\n\t\tClient:         openai.NewClientWithConfig(config),\n\t\tApiKey:         apiKey,\n\t\tProviderConfig: providerConfig,\n\t\tOpenAIOrgId:    openAIOrgId,\n\t}\n}\n\n// ExtendedChatCompletionStream can wrap either a native OpenAI stream or our custom implementation\ntype ExtendedChatCompletionStream struct {\n\topenaiStream *openai.ChatCompletionStream\n\tcustomReader *StreamReader[types.ExtendedChatCompletionStreamResponse]\n\tctx          context.Context\n}\n\n// StreamReader handles the SSE stream reading\ntype StreamReader[T any] struct {\n\treader             *bufio.Reader\n\tresponse           *http.Response\n\temptyMessagesLimit int\n\terrAccumulator     *ErrorAccumulator\n\tunmarshaler        *JSONUnmarshaler\n}\n\n// ErrorAccumulator keeps track of errors during streaming\ntype ErrorAccumulator struct {\n\terrors []error\n\tmu     sync.Mutex\n}\n\n// JSONUnmarshaler handles JSON unmarshaling for stream responses\ntype JSONUnmarshaler struct{}\n\nfunc CreateChatCompletionStream(\n\tclients map[string]ClientInfo,\n\tauthVars map[string]string,\n\tmodelConfig *shared.ModelRoleConfig,\n\tsettings *shared.PlanSettings,\n\torgUserConfig *shared.OrgUserConfig,\n\tcurrentOrgId string,\n\tcurrentUserId string,\n\tctx context.Context,\n\treq types.ExtendedChatCompletionRequest,\n) (*ExtendedChatCompletionStream, error) {\n\tproviderComposite := modelConfig.GetProviderComposite(authVars, settings, orgUserConfig)\n\t_, ok := clients[providerComposite]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"client not found for provider composite: %s\", providerComposite)\n\t}\n\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\t// ensure the model name is set correctly on fallbacks\n\treq.Model = baseModelConfig.ModelName\n\n\tresolveReq(&req, modelConfig, baseModelConfig, settings)\n\n\t// choose the fastest provider by latency/throughput on openrouter\n\tif baseModelConfig.Provider == shared.ModelProviderOpenRouter {\n\t\tif !strings.HasSuffix(string(req.Model), \":nitro\") && !strings.HasSuffix(string(req.Model), \":free\") && !strings.HasSuffix(string(req.Model), \":floor\") {\n\t\t\treq.Model += \":nitro\"\n\t\t}\n\t}\n\n\tif baseModelConfig.ReasoningBudget > 0 {\n\t\treq.ReasoningConfig = &types.ReasoningConfig{\n\t\t\tMaxTokens: baseModelConfig.ReasoningBudget,\n\t\t\tExclude:   !baseModelConfig.IncludeReasoning || baseModelConfig.HideReasoning,\n\t\t}\n\t} else if baseModelConfig.ReasoningEffortEnabled {\n\t\treq.ReasoningConfig = &types.ReasoningConfig{\n\t\t\tEffort:  shared.ReasoningEffort(baseModelConfig.ReasoningEffort),\n\t\t\tExclude: !baseModelConfig.IncludeReasoning || baseModelConfig.HideReasoning,\n\t\t}\n\t} else if baseModelConfig.IncludeReasoning {\n\t\treq.ReasoningConfig = &types.ReasoningConfig{\n\t\t\tExclude: baseModelConfig.HideReasoning,\n\t\t}\n\t}\n\n\treturn withStreamingRetries(ctx, func(numTotalRetry int, didProviderFallback bool, modelErr *shared.ModelError) (*ExtendedChatCompletionStream, shared.FallbackResult, error) {\n\t\thandleClaudeMaxRateLimitedIfNeeded(\n\t\t\tmodelErr,\n\t\t\tmodelConfig,\n\t\t\tauthVars,\n\t\t\tsettings,\n\t\t\torgUserConfig,\n\t\t\tcurrentOrgId,\n\t\t\tcurrentUserId,\n\t\t)\n\n\t\tfallbackRes := modelConfig.GetFallbackForModelError(\n\t\t\tnumTotalRetry,\n\t\t\tdidProviderFallback,\n\t\t\tmodelErr,\n\t\t\tauthVars,\n\t\t\tsettings,\n\t\t\torgUserConfig,\n\t\t)\n\t\tresolvedModelConfig := fallbackRes.ModelRoleConfig\n\n\t\tif resolvedModelConfig == nil {\n\t\t\treturn nil, fallbackRes, fmt.Errorf(\"model config is nil\")\n\t\t}\n\n\t\tproviderComposite := resolvedModelConfig.GetProviderComposite(authVars, settings, orgUserConfig)\n\n\t\tbaseModelConfig := resolvedModelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\t\topClient, ok := clients[providerComposite]\n\n\t\tif !ok {\n\t\t\treturn nil, fallbackRes, fmt.Errorf(\"client not found for provider composite: %s\", providerComposite)\n\t\t}\n\n\t\tif modelErr != nil && modelErr.Kind == shared.ErrCacheSupport {\n\t\t\tfor i := range req.Messages {\n\t\t\t\tfor j := range req.Messages[i].Content {\n\t\t\t\t\tif req.Messages[i].Content[j].CacheControl != nil {\n\t\t\t\t\t\treq.Messages[i].Content[j].CacheControl = nil\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tmodelConfig = resolvedModelConfig\n\n\t\tlog.Println(\"createChatCompletionStreamExtended - modelConfig\")\n\t\tspew.Dump(map[string]interface{}{\n\t\t\t\"modelConfig.ModelId\":      baseModelConfig.ModelId,\n\t\t\t\"modelConfig.ModelTag\":     baseModelConfig.ModelTag,\n\t\t\t\"modelConfig.ModelName\":    baseModelConfig.ModelName,\n\t\t\t\"modelConfig.Provider\":     baseModelConfig.Provider,\n\t\t\t\"modelConfig.BaseUrl\":      baseModelConfig.BaseUrl,\n\t\t\t\"modelConfig.ApiKeyEnvVar\": baseModelConfig.ApiKeyEnvVar,\n\t\t})\n\n\t\tresp, err := createChatCompletionStreamExtended(resolvedModelConfig, opClient, authVars, settings, orgUserConfig, ctx, req)\n\t\treturn resp, fallbackRes, err\n\t}, func(resp *ExtendedChatCompletionStream, err error) {})\n}\n\nfunc createChatCompletionStreamExtended(\n\tmodelConfig *shared.ModelRoleConfig,\n\tclient ClientInfo,\n\tauthVars map[string]string,\n\tsettings *shared.PlanSettings,\n\torgUserConfig *shared.OrgUserConfig,\n\tctx context.Context,\n\textendedReq types.ExtendedChatCompletionRequest,\n) (*ExtendedChatCompletionStream, error) {\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\t// ensure the model name is set correctly on fallbacks\n\textendedReq.Model = baseModelConfig.ModelName\n\n\tvar openaiReq *types.ExtendedOpenAIChatCompletionRequest\n\tif baseModelConfig.Provider == shared.ModelProviderOpenAI {\n\t\topenaiReq = extendedReq.ToOpenAI()\n\t\tlog.Println(\"Creating chat completion stream with direct OpenAI provider request\")\n\t}\n\n\tswitch baseModelConfig.Provider {\n\tcase shared.ModelProviderGoogleVertex:\n\t\tif authVars[\"VERTEXAI_PROJECT\"] != \"\" {\n\t\t\textendedReq.VertexProject = authVars[\"VERTEXAI_PROJECT\"]\n\t\t}\n\t\tif authVars[\"VERTEXAI_LOCATION\"] != \"\" {\n\t\t\textendedReq.VertexLocation = authVars[\"VERTEXAI_LOCATION\"]\n\t\t}\n\t\tif authVars[\"GOOGLE_APPLICATION_CREDENTIALS\"] != \"\" {\n\t\t\textendedReq.VertexCredentials = authVars[\"GOOGLE_APPLICATION_CREDENTIALS\"]\n\t\t}\n\tcase shared.ModelProviderAzureOpenAI:\n\t\tif authVars[\"AZURE_API_BASE\"] != \"\" {\n\t\t\textendedReq.LiteLLMApiBase = authVars[\"AZURE_API_BASE\"]\n\t\t}\n\t\tif authVars[\"AZURE_API_VERSION\"] != \"\" {\n\t\t\textendedReq.AzureApiVersion = authVars[\"AZURE_API_VERSION\"]\n\t\t}\n\n\t\tif authVars[\"AZURE_DEPLOYMENTS_MAP\"] != \"\" {\n\t\t\tvar azureDeploymentsMap map[string]string\n\t\t\terr := json.Unmarshal([]byte(authVars[\"AZURE_DEPLOYMENTS_MAP\"]), &azureDeploymentsMap)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error unmarshalling AZURE_DEPLOYMENTS_MAP: %w\", err)\n\t\t\t}\n\t\t\tmodelName := string(extendedReq.Model)\n\t\t\tmodelName = strings.ReplaceAll(modelName, \"azure/\", \"\")\n\n\t\t\tdeploymentName, ok := azureDeploymentsMap[modelName]\n\t\t\tif ok {\n\t\t\t\tlog.Println(\"azure - deploymentName\", deploymentName)\n\t\t\t\tmodelName = \"azure/\" + deploymentName\n\t\t\t\textendedReq.Model = shared.ModelName(modelName)\n\t\t\t}\n\t\t}\n\n\t\t// azure uses 'reasoning_config' instead of 'reasoning' like direct openai api\n\t\tif extendedReq.ReasoningConfig != nil {\n\t\t\textendedReq.AzureReasoningEffort = extendedReq.ReasoningConfig.Effort\n\t\t\textendedReq.ReasoningConfig = nil\n\t\t}\n\tcase shared.ModelProviderAmazonBedrock:\n\t\tif authVars[\"AWS_ACCESS_KEY_ID\"] != \"\" {\n\t\t\textendedReq.BedrockAccessKeyId = authVars[\"AWS_ACCESS_KEY_ID\"]\n\t\t}\n\t\tif authVars[\"AWS_SECRET_ACCESS_KEY\"] != \"\" {\n\t\t\textendedReq.BedrockSecretAccessKey = authVars[\"AWS_SECRET_ACCESS_KEY\"]\n\t\t}\n\t\tif authVars[\"AWS_SESSION_TOKEN\"] != \"\" {\n\t\t\textendedReq.BedrockSessionToken = authVars[\"AWS_SESSION_TOKEN\"]\n\t\t}\n\t\tif authVars[\"AWS_REGION\"] != \"\" {\n\t\t\textendedReq.BedrockRegion = authVars[\"AWS_REGION\"]\n\t\t}\n\t\tif authVars[\"AWS_INFERENCE_PROFILE_ARN\"] != \"\" {\n\t\t\textendedReq.BedrockInferenceProfileArn = authVars[\"AWS_INFERENCE_PROFILE_ARN\"]\n\t\t}\n\n\tcase shared.ModelProviderOllama:\n\t\tif os.Getenv(\"OLLAMA_BASE_URL\") != \"\" {\n\t\t\textendedReq.LiteLLMApiBase = os.Getenv(\"OLLAMA_BASE_URL\")\n\t\t}\n\t}\n\n\tif client.ProviderConfig.HasClaudeMaxAuth {\n\t\textendedReq.Messages = append([]types.ExtendedChatMessage{\n\t\t\t{\n\t\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t{Type: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\tText: \"You are Claude Code, Anthropic's official CLI for Claude.\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}, extendedReq.Messages...)\n\n\t\tif extendedReq.ExtraHeaders == nil {\n\t\t\textendedReq.ExtraHeaders = make(map[string]string)\n\t\t}\n\t\textendedReq.ExtraHeaders[\"anthropic-beta\"] = shared.AnthropicClaudeMaxBetaHeader\n\t\textendedReq.ExtraHeaders[\"Authorization\"] = \"Bearer \" + authVars[shared.AnthropicClaudeMaxTokenEnvVar]\n\t\textendedReq.ExtraHeaders[\"anthropic-product\"] = \"claude-code\"\n\n\t}\n\n\t// Marshal the request body to JSON\n\tvar jsonBody []byte\n\tvar err error\n\tif openaiReq != nil {\n\t\tjsonBody, err = json.Marshal(openaiReq)\n\t} else {\n\t\tjsonBody, err = json.Marshal(extendedReq)\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\n\t// log.Println(\"request jsonBody\", string(jsonBody))\n\n\t// Create new request\n\tbaseUrl := baseModelConfig.BaseUrl\n\turl := baseUrl + \"/chat/completions\"\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", url, bytes.NewReader(jsonBody))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating request: %w\", err)\n\t}\n\n\t// Set required headers for streaming\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept\", \"text/event-stream\")\n\treq.Header.Set(\"Cache-Control\", \"no-cache\")\n\treq.Header.Set(\"Connection\", \"keep-alive\")\n\n\t// some providers send api key in the body, some in the header\n\t// some use other auth methods and so don't have a simple api key\n\tif client.ApiKey != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+client.ApiKey)\n\t}\n\tif client.OpenAIOrgId != \"\" {\n\t\treq.Header.Set(\"OpenAI-Organization\", client.OpenAIOrgId)\n\t}\n\n\taddOpenRouterHeaders(req)\n\n\t// Send the request\n\tresp, err := httpClient.Do(req) //nolint:bodyclose // body is closed in stream.Close()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error making request: %w\", err)\n\t}\n\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {\n\t\tdefer resp.Body.Close()\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading error response: %w\", err)\n\t\t}\n\t\treturn nil, &HTTPError{\n\t\t\tStatusCode: resp.StatusCode,\n\t\t\tBody:       string(body),\n\t\t\tHeader:     resp.Header.Clone(), // retain Retry-After etc.\n\t\t}\n\t}\n\n\t// Log response headers\n\t// log.Println(\"Response headers:\")\n\t// for key, values := range resp.Header {\n\t// \tlog.Printf(\"%s: %v\\n\", key, values)\n\t// }\n\n\treader := &StreamReader[types.ExtendedChatCompletionStreamResponse]{\n\t\treader:             bufio.NewReader(resp.Body),\n\t\tresponse:           resp,\n\t\temptyMessagesLimit: 30,\n\t\terrAccumulator:     NewErrorAccumulator(),\n\t\tunmarshaler:        &JSONUnmarshaler{},\n\t}\n\n\treturn &ExtendedChatCompletionStream{\n\t\tcustomReader: reader,\n\t\tctx:          ctx,\n\t}, nil\n}\n\nfunc NewErrorAccumulator() *ErrorAccumulator {\n\treturn &ErrorAccumulator{\n\t\terrors: make([]error, 0),\n\t}\n}\n\nfunc (ea *ErrorAccumulator) Add(err error) {\n\tea.mu.Lock()\n\tdefer ea.mu.Unlock()\n\tea.errors = append(ea.errors, err)\n}\n\nfunc (ea *ErrorAccumulator) GetErrors() []error {\n\tea.mu.Lock()\n\tdefer ea.mu.Unlock()\n\treturn ea.errors\n}\n\nfunc (ju *JSONUnmarshaler) Unmarshal(data []byte, v interface{}) error {\n\treturn json.Unmarshal(data, v)\n}\n\n// Recv reads from the stream\nfunc (stream *StreamReader[T]) Recv() (*T, error) {\n\tfor {\n\t\tline, err := stream.reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Trim any whitespace\n\t\tline = strings.TrimSpace(line)\n\n\t\t// Skip empty lines\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check for data prefix\n\t\tif !strings.HasPrefix(line, \"data: \") {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Extract the data\n\t\tdata := strings.TrimPrefix(line, \"data: \")\n\n\t\t// log.Println(\"\\n\\n--- stream data:\\n\", data, \"\\n\\n\")\n\n\t\t// Check for stream completion\n\t\tif data == \"[DONE]\" {\n\t\t\treturn nil, io.EOF\n\t\t}\n\n\t\t// Parse the response\n\t\tvar response T\n\t\terr = stream.unmarshaler.Unmarshal([]byte(data), &response)\n\t\tif err != nil {\n\t\t\tstream.errAccumulator.Add(err)\n\t\t\tcontinue\n\t\t}\n\n\t\treturn &response, nil\n\t}\n}\n\nfunc (stream *StreamReader[T]) Close() error {\n\tif stream.response != nil {\n\t\treturn stream.response.Body.Close()\n\t}\n\treturn nil\n}\n\n// Recv returns the next message in the stream\nfunc (stream *ExtendedChatCompletionStream) Recv() (*types.ExtendedChatCompletionStreamResponse, error) {\n\tselect {\n\tcase <-stream.ctx.Done():\n\t\treturn nil, stream.ctx.Err()\n\tdefault:\n\t\tif stream.openaiStream != nil {\n\t\t\tbytes, err := stream.openaiStream.RecvRaw()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tvar response types.ExtendedChatCompletionStreamResponse\n\t\t\terr = json.Unmarshal(bytes, &response)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &response, nil\n\t\t}\n\t\treturn stream.customReader.Recv()\n\t}\n}\n\n// Close the response body\nfunc (stream *ExtendedChatCompletionStream) Close() error {\n\tif stream.openaiStream != nil {\n\t\treturn stream.openaiStream.Close()\n\t}\n\treturn stream.customReader.Close()\n}\n\nfunc resolveReq(req *types.ExtendedChatCompletionRequest, modelConfig *shared.ModelRoleConfig, baseModelConfig *shared.BaseModelConfig, settings *shared.PlanSettings) {\n\t// if system prompt is disabled, change the role of the system message to user\n\tif modelConfig.GetSharedBaseConfig(settings).SystemPromptDisabled {\n\t\tlog.Println(\"System prompt disabled - changing role of system message to user\")\n\t\tfor i, msg := range req.Messages {\n\t\t\tlog.Println(\"Message role:\", msg.Role)\n\t\t\tif msg.Role == openai.ChatMessageRoleSystem {\n\t\t\t\tlog.Println(\"Changing role of system message to user\")\n\t\t\t\treq.Messages[i].Role = openai.ChatMessageRoleUser\n\t\t\t}\n\t\t}\n\n\t\tfor _, msg := range req.Messages {\n\t\t\tlog.Println(\"Final message role:\", msg.Role)\n\t\t}\n\t}\n\n\tif modelConfig.GetSharedBaseConfig(settings).RoleParamsDisabled {\n\t\tlog.Println(\"Role params disabled - setting temperature and top p to 0\")\n\t\treq.Temperature = 0\n\t\treq.TopP = 0\n\t}\n\n\tif baseModelConfig.Provider == shared.ModelProviderOllama {\n\t\t// ollama doesn't support temperature or top p params\n\t\tlog.Println(\"Ollama - clearing temperature and top p\")\n\t\treq.Temperature = 0\n\t\treq.TopP = 0\n\n\t}\n}\n\nfunc addOpenRouterHeaders(req *http.Request) {\n\treq.Header.Set(\"HTTP-Referer\", \"https://plandex.ai\")\n\treq.Header.Set(\"X-Title\", \"Plandex\")\n\treq.Header.Set(\"X-OR-Prefer\", \"ttft,throughput\")\n\tif os.Getenv(\"GOENV\") == \"production\" {\n\t\treq.Header.Set(\"X-OR-Region\", \"us-east-1\")\n\t}\n}\n\nfunc handleClaudeMaxRateLimitedIfNeeded(modelErr *shared.ModelError, modelConfig *shared.ModelRoleConfig, authVars map[string]string, settings *shared.PlanSettings, orgUserConfig *shared.OrgUserConfig, currentOrgId string, currentUserId string) {\n\n\t// 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\n\tif modelErr != nil && modelErr.Kind == shared.ErrRateLimited && modelErr.RetryAfterSeconds == 0 {\n\t\tbaseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\t\tif baseModelConfig.BaseModelProviderConfig.HasClaudeMaxAuth {\n\t\t\torgUserConfig.ClaudeSubscriptionCooldownStartedAt = time.Now()\n\n\t\t\tgo func() {\n\t\t\t\terr := db.UpdateOrgUserConfig(currentUserId, currentOrgId, orgUserConfig)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error updating org user config: %v\\n\", err)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "app/server/model/client_stream.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math/rand\"\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n\t\"time\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype OnStreamFn func(chunk string, buffer string) (shouldStop bool)\n\nfunc CreateChatCompletionWithInternalStream(\n\tclients map[string]ClientInfo,\n\tauthVars map[string]string,\n\tmodelConfig *shared.ModelRoleConfig,\n\tsettings *shared.PlanSettings,\n\torgUserConfig *shared.OrgUserConfig,\n\tcurrentOrgId string,\n\tcurrentUserId string,\n\tctx context.Context,\n\treq types.ExtendedChatCompletionRequest,\n\tonStream OnStreamFn,\n\treqStarted time.Time,\n) (*types.ModelResponse, error) {\n\tproviderComposite := modelConfig.GetProviderComposite(authVars, settings, orgUserConfig)\n\t_, ok := clients[providerComposite]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"client not found for provider composite: %s\", providerComposite)\n\t}\n\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\tresolveReq(&req, modelConfig, baseModelConfig, settings)\n\n\t// choose the fastest provider by latency/throughput on openrouter\n\tif baseModelConfig.Provider == shared.ModelProviderOpenRouter {\n\t\treq.Model += \":nitro\"\n\t}\n\n\t// Force streaming mode since we're using the streaming API\n\treq.Stream = true\n\n\t// Include usage in stream response\n\treq.StreamOptions = &openai.StreamOptions{\n\t\tIncludeUsage: true,\n\t}\n\n\treturn withStreamingRetries(ctx, func(numTotalRetry int, didProviderFallback bool, modelErr *shared.ModelError) (resp *types.ModelResponse, fallbackRes shared.FallbackResult, err error) {\n\t\thandleClaudeMaxRateLimitedIfNeeded(modelErr, modelConfig, authVars, settings, orgUserConfig, currentOrgId, currentUserId)\n\n\t\tfallbackRes = modelConfig.GetFallbackForModelError(numTotalRetry, didProviderFallback, modelErr, authVars, settings, orgUserConfig)\n\t\tresolvedModelConfig := fallbackRes.ModelRoleConfig\n\n\t\tif resolvedModelConfig == nil {\n\t\t\treturn nil, fallbackRes, fmt.Errorf(\"model config is nil\")\n\t\t}\n\n\t\tproviderComposite := resolvedModelConfig.GetProviderComposite(authVars, settings, orgUserConfig)\n\t\topClient, ok := clients[providerComposite]\n\n\t\tif !ok {\n\t\t\treturn nil, fallbackRes, fmt.Errorf(\"client not found for provider composite: %s\", providerComposite)\n\t\t}\n\n\t\tmodelConfig = resolvedModelConfig\n\t\tresp, err = processChatCompletionStream(resolvedModelConfig, opClient, authVars, settings, orgUserConfig, ctx, req, onStream, reqStarted)\n\t\tif err != nil {\n\t\t\treturn nil, fallbackRes, err\n\t\t}\n\t\treturn resp, fallbackRes, nil\n\t}, func(resp *types.ModelResponse, err error) {\n\t\tif resp != nil {\n\t\t\tresp.Stopped = true\n\t\t\tresp.Error = err.Error()\n\t\t}\n\t})\n}\n\nfunc processChatCompletionStream(\n\tmodelConfig *shared.ModelRoleConfig,\n\tclient ClientInfo,\n\tauthVars map[string]string,\n\tsettings *shared.PlanSettings,\n\torgUserConfig *shared.OrgUserConfig,\n\tctx context.Context,\n\treq types.ExtendedChatCompletionRequest,\n\tonStream OnStreamFn,\n\treqStarted time.Time,\n) (*types.ModelResponse, error) {\n\tstreamCtx, cancel := context.WithCancel(ctx)\n\n\tlog.Println(\"processChatCompletionStream - modelConfig\", spew.Sdump(map[string]interface{}{\n\t\t\"model\": modelConfig.ModelId,\n\t}))\n\n\tstream, err := createChatCompletionStreamExtended(modelConfig, client, authVars, settings, orgUserConfig, streamCtx, req)\n\n\tif err != nil {\n\t\tcancel()\n\t\treturn nil, fmt.Errorf(\"error creating chat completion stream: %w\", err)\n\t}\n\n\tdefer stream.Close()\n\tdefer cancel()\n\n\taccumulator := types.NewStreamCompletionAccumulator()\n\t// Create a timer that will trigger if no chunk is received within the specified duration\n\ttimer := time.NewTimer(ACTIVE_STREAM_CHUNK_TIMEOUT)\n\tdefer timer.Stop()\n\tstreamFinished := false\n\n\treceivedFirstChunk := false\n\n\t// Process stream until EOF or error\n\tfor {\n\t\tselect {\n\t\tcase <-streamCtx.Done():\n\t\t\tlog.Println(\"Stream canceled\")\n\t\t\treturn accumulator.Result(true, streamCtx.Err()), streamCtx.Err()\n\t\tcase <-timer.C:\n\t\t\tlog.Println(\"Stream timed out due to inactivity\")\n\t\t\tif streamFinished {\n\t\t\t\tlog.Println(\"Stream finished—timed out waiting for usage chunk\")\n\t\t\t\treturn accumulator.Result(false, nil), nil\n\t\t\t} else {\n\t\t\t\tlog.Println(\"Stream timed out due to inactivity\")\n\t\t\t\treturn accumulator.Result(true, fmt.Errorf(\"stream timed out due to inactivity. The model is not responding.\")), nil\n\t\t\t}\n\t\tdefault:\n\t\t\tresponse, err := stream.Recv()\n\t\t\tif err == io.EOF {\n\t\t\t\tif streamFinished {\n\t\t\t\t\treturn accumulator.Result(false, nil), nil\n\t\t\t\t}\n\n\t\t\t\terr = fmt.Errorf(\"model stream ended unexpectedly: %w\", err)\n\t\t\t\treturn accumulator.Result(true, err), err\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\terr = fmt.Errorf(\"error receiving stream chunk: %w\", err)\n\t\t\t\treturn accumulator.Result(true, err), err\n\t\t\t}\n\n\t\t\tif response.ID != \"\" {\n\t\t\t\taccumulator.SetGenerationId(response.ID)\n\t\t\t}\n\n\t\t\tif !receivedFirstChunk {\n\t\t\t\treceivedFirstChunk = true\n\t\t\t\taccumulator.SetFirstTokenAt(time.Now())\n\t\t\t}\n\n\t\t\tif !timer.Stop() {\n\t\t\t\t<-timer.C\n\t\t\t}\n\t\t\ttimer.Reset(ACTIVE_STREAM_CHUNK_TIMEOUT)\n\n\t\t\t// Process the response\n\t\t\tif response.Usage != nil {\n\t\t\t\taccumulator.SetUsage(response.Usage)\n\t\t\t\treturn accumulator.Result(false, nil), nil\n\t\t\t}\n\n\t\t\temptyChoices := false\n\t\t\tvar content string\n\n\t\t\tif len(response.Choices) == 0 {\n\t\t\t\t// 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\n\t\t\t\tlog.Println(\"processChatCompletionStream - no choices in response\")\n\t\t\t\t// err := fmt.Errorf(\"no choices in response\")\n\t\t\t\t// return accumulator.Result(false, err), err\n\t\t\t\temptyChoices = true\n\t\t\t}\n\n\t\t\t// We'll be more accepting of multiple choices and just take the first one\n\t\t\t// if len(response.Choices) > 1 {\n\t\t\t// \terr = fmt.Errorf(\"stream finished with more than one choice | The model failed to generate a valid response.\")\n\t\t\t// \treturn accumulator.Result(true, err), err\n\t\t\t// }\n\n\t\t\tif !emptyChoices {\n\t\t\t\tchoice := response.Choices[0]\n\n\t\t\t\tif choice.FinishReason != \"\" {\n\t\t\t\t\tif choice.FinishReason == \"error\" {\n\t\t\t\t\t\terr = fmt.Errorf(\"model stopped with error status | The model is not responding.\")\n\t\t\t\t\t\treturn accumulator.Result(true, err), err\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Reset the timer for the usage chunk\n\t\t\t\t\t\tif !timer.Stop() {\n\t\t\t\t\t\t\t<-timer.C\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttimer.Reset(USAGE_CHUNK_TIMEOUT)\n\t\t\t\t\t\tstreamFinished = true\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif req.Tools != nil {\n\t\t\t\t\tif choice.Delta.ToolCalls != nil {\n\t\t\t\t\t\ttoolCall := choice.Delta.ToolCalls[0]\n\t\t\t\t\t\tcontent = toolCall.Function.Arguments\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif choice.Delta.Content != \"\" {\n\t\t\t\t\t\tcontent = choice.Delta.Content\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\taccumulator.AddContent(content)\n\t\t\t// pass the chunk and the accumulated content to the callback\n\t\t\tif onStream != nil {\n\t\t\t\tshouldReturn := onStream(content, accumulator.Content())\n\t\t\t\tif shouldReturn {\n\t\t\t\t\treturn accumulator.Result(false, nil), nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc withStreamingRetries[T any](\n\tctx context.Context,\n\toperation func(numRetry int, didProviderFallback bool, modelErr *shared.ModelError) (resp *T, fallbackRes shared.FallbackResult, err error),\n\tonContextDone func(resp *T, err error),\n) (*T, error) {\n\tvar resp *T\n\tvar numTotalRetry int\n\tvar numFallbackRetry int\n\tvar fallbackRes shared.FallbackResult\n\tvar modelErr *shared.ModelError\n\tvar didProviderFallback bool\n\n\tfor {\n\t\tif ctx.Err() != nil {\n\t\t\tif resp != nil {\n\t\t\t\t// Return partial result with context error\n\t\t\t\tonContextDone(resp, ctx.Err())\n\t\t\t\treturn resp, ctx.Err()\n\t\t\t}\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\n\t\tvar err error\n\n\t\tvar numRetry int\n\t\tif numFallbackRetry > 0 {\n\t\t\tnumRetry = numFallbackRetry\n\t\t} else {\n\t\t\tnumRetry = numTotalRetry\n\t\t}\n\n\t\tlog.Printf(\"withStreamingRetries - will run operation\")\n\n\t\tlog.Println(spew.Sdump(map[string]interface{}{\n\t\t\t\"numTotalRetry\":       numTotalRetry,\n\t\t\t\"didProviderFallback\": didProviderFallback,\n\t\t\t\"modelErr\":            modelErr,\n\t\t}))\n\n\t\tresp, fallbackRes, err = operation(numTotalRetry, didProviderFallback, modelErr)\n\t\tif err == nil {\n\t\t\treturn resp, nil\n\t\t}\n\n\t\tlog.Printf(\"withStreamingRetries - operation returned error: %v\", err)\n\n\t\tisFallback := fallbackRes.IsFallback\n\t\tmaxRetries := MAX_RETRIES_WITHOUT_FALLBACK\n\t\tif isFallback {\n\t\t\tmaxRetries = MAX_ADDITIONAL_RETRIES_WITH_FALLBACK\n\t\t}\n\n\t\tif fallbackRes.FallbackType == shared.FallbackTypeProvider {\n\t\t\tdidProviderFallback = true\n\t\t}\n\n\t\tcompareRetries := numTotalRetry\n\t\tif isFallback {\n\t\t\tcompareRetries = numFallbackRetry\n\t\t}\n\n\t\tlog.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)\n\n\t\tclassifyRes := classifyBasicError(err, fallbackRes.BaseModelConfig.HasClaudeMaxAuth)\n\t\tmodelErr = &classifyRes\n\n\t\tnewFallback := false\n\t\tif !modelErr.Retriable {\n\t\t\tlog.Printf(\"withStreamingRetries - operation returned non-retriable error: %v\", err)\n\t\t\tspew.Dump(modelErr)\n\t\t\tif modelErr.Kind == shared.ErrContextTooLong && fallbackRes.ModelRoleConfig.LargeContextFallback == nil {\n\t\t\t\tlog.Printf(\"withStreamingRetries - non-retriable context too long error and no large context fallback is defined, returning error\")\n\t\t\t\t// if it's a context too long error and no large context fallback is defined, return the error\n\t\t\t\treturn resp, err\n\t\t\t} else if modelErr.Kind != shared.ErrContextTooLong && fallbackRes.ModelRoleConfig.ErrorFallback == nil {\n\t\t\t\tlog.Printf(\"withStreamingRetries - non-retriable error and no error fallback is defined, returning error\")\n\t\t\t\t// if it's any other error and no error fallback is defined, return the error\n\t\t\t\treturn resp, err\n\t\t\t}\n\t\t\tlog.Printf(\"withStreamingRetries - operation returned non-retriable error, but has fallback - resetting numFallbackRetry to 0 and continuing to retry\")\n\t\t\tnumFallbackRetry = 0\n\t\t\tnewFallback = true\n\t\t\tcompareRetries = 0\n\t\t\t// otherwise, continue to retry logic\n\t\t}\n\n\t\tif compareRetries >= maxRetries {\n\t\t\tlog.Printf(\"withStreamingRetries - compareRetries >= maxRetries - returning error\")\n\t\t\treturn resp, err\n\t\t}\n\n\t\tvar retryDelay time.Duration\n\t\tif modelErr != nil && modelErr.RetryAfterSeconds > 0 {\n\t\t\t// if the model err has a retry after, then use that with a bit of padding\n\t\t\tretryDelay = time.Duration(int(float64(modelErr.RetryAfterSeconds)*1.1)) * time.Second\n\t\t} else {\n\t\t\t// otherwise, use some jitter\n\t\t\tretryDelay = time.Duration(1000+rand.Intn(200)) * time.Millisecond\n\t\t}\n\n\t\tlog.Printf(\"withStreamingRetries - retrying stream in %v seconds\", retryDelay)\n\t\ttime.Sleep(retryDelay)\n\n\t\tif modelErr != nil && modelErr.ShouldIncrementRetry() {\n\t\t\tnumTotalRetry++\n\t\t\tif isFallback && !newFallback {\n\t\t\t\tnumFallbackRetry++\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/server/model/litellm.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar (\n\tliteLLMOnce sync.Once\n\tliteLLMCmd  *exec.Cmd\n)\n\nfunc EnsureLiteLLM(numWorkers int) error {\n\tvar finalErr error\n\tliteLLMOnce.Do(func() {\n\t\tif isLiteLLMHealthy() {\n\t\t\tlog.Println(\"LiteLLM proxy is already healthy\")\n\t\t\treturn\n\t\t}\n\n\t\tlog.Println(\"LiteLLM proxy is not running. Starting...\")\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\t\tdefer cancel()\n\n\t\terr := startLiteLLMServer(numWorkers)\n\t\tif err != nil {\n\t\t\tlog.Println(\"LiteLLM proxy launch failed:\", err)\n\t\t\tfinalErr = fmt.Errorf(\"LiteLLM proxy launch failed: %w\", err)\n\t\t\treturn\n\t\t}\n\n\t\tticker := time.NewTicker(500 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tlog.Println(\"LiteLLM proxy launch timed out\")\n\t\t\t\tfinalErr = fmt.Errorf(\"LiteLLM proxy launch timed out\")\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tif isLiteLLMHealthy() {\n\t\t\t\t\tlog.Println(\"LiteLLM proxy is healthy\")\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tlog.Println(\"LiteLLM proxy is not healthy yet, retrying after 500ms...\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n\n\treturn finalErr\n}\n\nfunc ShutdownLiteLLMServer() error {\n\tif liteLLMCmd != nil && liteLLMCmd.Process != nil {\n\t\tlog.Println(\"Shutting down LiteLLM proxy gracefully...\")\n\t\tif err := liteLLMCmd.Process.Signal(os.Interrupt); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to signal LiteLLM for shutdown: %w\", err)\n\t\t}\n\n\t\tdone := make(chan error, 1)\n\t\tgo func() {\n\t\t\tdone <- liteLLMCmd.Wait()\n\t\t}()\n\n\t\tselect {\n\t\tcase <-time.After(5 * time.Second):\n\t\t\tlog.Println(\"LiteLLM proxy shutdown timed out, forcing kill\")\n\t\t\treturn liteLLMCmd.Process.Kill()\n\t\tcase err := <-done:\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc isLiteLLMHealthy() bool {\n\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\tdefer cancel()\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", \"http://localhost:4000/health\", nil)\n\tif err != nil {\n\t\tlog.Println(\"LiteLLM health check request failed:\", err)\n\t\treturn false\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tlog.Println(\"LiteLLM health check failed:\", err)\n\t\treturn false\n\t}\n\tdefer resp.Body.Close()\n\n\treturn resp.StatusCode == 200\n}\n\nfunc startLiteLLMServer(numWorkers int) error {\n\tliteLLMCmd = exec.Command(\"python3\",\n\t\t\"-m\", \"uvicorn\",\n\t\t\"litellm_proxy:app\",\n\t\t\"--host\", \"0.0.0.0\",\n\t\t\"--port\", \"4000\",\n\t\t\"--workers\", strconv.Itoa(numWorkers),\n\t)\n\n\tif os.Getenv(\"LITELLM_PROXY_DIR\") != \"\" {\n\t\tliteLLMCmd.Dir = os.Getenv(\"LITELLM_PROXY_DIR\")\n\t}\n\n\t// clean env\n\tliteLLMCmd.Env = []string{\n\t\t\"PATH=\" + os.Getenv(\"PATH\"),\n\t\t\"HOME=\" + os.Getenv(\"HOME\"),\n\t}\n\n\tif os.Getenv(\"OLLAMA_BASE_URL\") != \"\" {\n\t\tlog.Println(\"OLLAMA_BASE_URL is set, so we can reach ollama from inside docker container in local mode\")\n\t\t// so we can reach ollama from inside docker container in local mode\n\t\tliteLLMCmd.Env = append(liteLLMCmd.Env, \"OLLAMA_BASE_URL=\"+os.Getenv(\"OLLAMA_BASE_URL\"))\n\t}\n\n\tliteLLMCmd.Stdout = os.Stdout\n\tliteLLMCmd.Stderr = os.Stderr\n\n\terr := liteLLMCmd.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Println(\"LiteLLM proxy launched\")\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/model/model_error.go",
    "content": "package model\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\tshared \"plandex-shared\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype HTTPError struct {\n\tStatusCode int\n\tBody       string\n\tHeader     http.Header\n}\n\nfunc (e *HTTPError) Error() string {\n\treturn fmt.Sprintf(\"status code: %d, body: %s\", e.StatusCode, e.Body)\n}\n\n// JSON-style  `\"retry_after_ms\":1234`\nvar reJSON = regexp.MustCompile(`\"retry_after_ms\"\\s*:\\s*(\\d+)`)\n\n// Header- or text-style  \"Retry-After: 12\" / \"retry_after: 12s\"\nvar reRetryAfter = regexp.MustCompile(\n\t`retry[_\\-\\s]?after[_\\-\\s]?(?:[:\\s]+)?(\\d+)(ms|seconds?|secs?|s)?`,\n)\n\n// Free-form Azure style  \"Try again in 59 seconds.\"\n// Also matches \"Retry in 10 seconds.\"\nvar reTryAgain = regexp.MustCompile(\n\t`(?:re)?try[_\\-\\s]+(?:again[_\\-\\s]+)?in[_\\-\\s]+(\\d+)(ms|seconds?|secs?|s)?`,\n)\n\nfunc ClassifyErrMsg(msg string) *shared.ModelError {\n\tlog.Printf(\"Classifying error message: %s\", msg)\n\n\tmsg = strings.ToLower(msg)\n\n\tif strings.Contains(msg, \"maximum context length\") ||\n\t\tstrings.Contains(msg, \"context length exceeded\") ||\n\t\tstrings.Contains(msg, \"exceed context limit\") ||\n\t\tstrings.Contains(msg, \"decrease input length\") ||\n\t\tstrings.Contains(msg, \"too many tokens\") ||\n\t\tstrings.Contains(msg, \"payload too large\") ||\n\t\tstrings.Contains(msg, \"payload is too large\") ||\n\t\tstrings.Contains(msg, \"input is too large\") ||\n\t\tstrings.Contains(msg, \"input too large\") ||\n\t\tstrings.Contains(msg, \"input is too long\") ||\n\t\tstrings.Contains(msg, \"input too long\") {\n\t\tlog.Printf(\"Context too long error: %s\", msg)\n\t\treturn &shared.ModelError{\n\t\t\tKind:              shared.ErrContextTooLong,\n\t\t\tRetriable:         false,\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\t}\n\n\tif strings.Contains(msg, \"model_overloaded\") ||\n\t\tstrings.Contains(msg, \"model overloaded\") ||\n\t\tstrings.Contains(msg, \"server is overloaded\") ||\n\t\tstrings.Contains(msg, \"model is currently overloaded\") ||\n\t\tstrings.Contains(msg, \"overloaded_error\") ||\n\t\tstrings.Contains(msg, \"resource has been exhausted\") {\n\t\tlog.Printf(\"Overloaded error: %s\", msg)\n\t\treturn &shared.ModelError{\n\t\t\tKind:              shared.ErrOverloaded,\n\t\t\tRetriable:         true,\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\t}\n\n\tif strings.Contains(msg, \"cache control\") {\n\t\tlog.Printf(\"Cache control error: %s\", msg)\n\t\treturn &shared.ModelError{\n\t\t\tKind:              shared.ErrCacheSupport,\n\t\t\tRetriable:         true,\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\t}\n\n\tlog.Println(\"No error classification based on message\")\n\n\treturn nil\n}\n\nfunc ClassifyModelError(code int, message string, headers http.Header, isClaudeMax bool) shared.ModelError {\n\tmsg := strings.ToLower(message)\n\n\t// first of all, if it's claude max and a 429, it means the subscription limit was reached, so handle it accordingly\n\tif isClaudeMax && code == 429 {\n\t\tretryAfter := extractRetryAfter(headers, msg)\n\t\tif retryAfter > 0 {\n\t\t\treturn shared.ModelError{\n\t\t\t\tKind:              shared.ErrSubscriptionQuotaExhausted,\n\t\t\t\tRetriable:         true,\n\t\t\t\tRetryAfterSeconds: retryAfter,\n\t\t\t}\n\t\t}\n\t\treturn shared.ModelError{\n\t\t\tKind:              shared.ErrSubscriptionQuotaExhausted,\n\t\t\tRetriable:         false,\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\t}\n\n\t// next try to classify the error based on the message only\n\tmsgRes := ClassifyErrMsg(msg)\n\tif msgRes != nil {\n\t\tlog.Printf(\"Classified error message: %+v\", msgRes)\n\t\treturn *msgRes\n\t}\n\n\tvar res shared.ModelError\n\n\tswitch code {\n\tcase 429, 529:\n\t\tres = shared.ModelError{\n\t\t\tKind:              shared.ErrRateLimited,\n\t\t\tRetriable:         true,\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\tcase 413:\n\t\tres = shared.ModelError{\n\t\t\tKind:              shared.ErrContextTooLong,\n\t\t\tRetriable:         false,\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\n\t// rare codes but they never succeed on retry if they do show up\n\tcase 501, 505:\n\t\tres = shared.ModelError{\n\t\t\tKind:              shared.ErrOther,\n\t\t\tRetriable:         false,\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\tdefault:\n\t\tres = shared.ModelError{\n\t\t\tKind:              shared.ErrOther,\n\t\t\tRetriable:         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\n\t\t\tRetryAfterSeconds: 0,\n\t\t}\n\t}\n\n\tlog.Printf(\"Model error: %+v\", res)\n\n\t// best‑effort parse of \"Retry‑After\" style hints in the message\n\tif res.Retriable {\n\t\tretryAfter := extractRetryAfter(headers, msg)\n\n\t\t// if the retry after is greater than the max delay, then the error is not retriable\n\t\tif retryAfter > MAX_RETRY_DELAY_SECONDS {\n\t\t\tlog.Printf(\"Retry after %d seconds is greater than the max delay of %d seconds - not retriable\", retryAfter, MAX_RETRY_DELAY_SECONDS)\n\t\t\tres.Retriable = false\n\t\t} else {\n\t\t\tres.RetryAfterSeconds = retryAfter\n\t\t}\n\n\t}\n\n\treturn res\n}\n\nfunc extractRetryAfter(h http.Header, body string) (sec int) {\n\tnow := time.Now()\n\n\t// Retry-After header: seconds or HTTP-date\n\tif v := h.Get(\"Retry-After\"); v != \"\" {\n\t\tif n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil {\n\t\t\treturn n\n\t\t}\n\t\tif t, err := time.Parse(http.TimeFormat, v); err == nil {\n\t\t\td := int(t.Sub(now).Seconds())\n\t\t\tif d > 0 {\n\t\t\t\treturn d\n\t\t\t}\n\t\t}\n\t}\n\n\t// X-RateLimit-Reset epoch\n\tif v := h.Get(\"X-RateLimit-Reset\"); v != \"\" {\n\t\tif reset, _ := strconv.ParseInt(v, 10, 64); reset > now.Unix() {\n\t\t\treturn int(reset - now.Unix())\n\t\t}\n\t}\n\n\tlower := strings.ToLower(strings.TrimSpace(body))\n\n\t// \"retry_after_ms\": 1234\n\tif m := reJSON.FindStringSubmatch(lower); len(m) == 2 {\n\t\tn, _ := strconv.Atoi(m[1])\n\t\treturn n / 1000\n\t}\n\t// \"retry after 12\"\n\tif m := reRetryAfter.FindStringSubmatch(lower); len(m) >= 2 {\n\t\tunit := \"\"\n\t\tif len(m) == 3 {\n\t\t\tunit = m[2]\n\t\t}\n\t\treturn normalizeUnit(m[1], unit)\n\t}\n\n\t// \"try again in 8\"\n\tif m := reTryAgain.FindStringSubmatch(lower); len(m) >= 2 {\n\t\tunit := \"\"\n\t\tif len(m) == 3 {\n\t\t\tunit = m[2]\n\t\t}\n\t\treturn normalizeUnit(m[1], unit)\n\t}\n\treturn 0\n}\n\nfunc normalizeUnit(numStr, unit string) int {\n\tn, _ := strconv.Atoi(numStr) // safe because the regex matched \\d+\n\n\tswitch unit {\n\tcase \"ms\": // milliseconds\n\t\treturn n / 1000\n\tcase \"sec\", \"secs\", \"second\", \"seconds\", \"s\":\n\t\treturn n // already in seconds\n\tdefault: // unit omitted ⇒ assume seconds\n\t\treturn n\n\t}\n}\n\nfunc classifyBasicError(err error, isClaudeMax bool) shared.ModelError {\n\t// if it's an http error, classify it based on the status code and body\n\tif httpErr, ok := err.(*HTTPError); ok {\n\t\tme := ClassifyModelError(\n\t\t\thttpErr.StatusCode,\n\t\t\thttpErr.Body,\n\t\t\thttpErr.Header,\n\t\t\tisClaudeMax,\n\t\t)\n\t\treturn me\n\t}\n\n\t// try to classify the error based on the message only\n\tmsgRes := ClassifyErrMsg(err.Error())\n\tif msgRes != nil {\n\t\treturn *msgRes\n\t}\n\n\t// Fall back to old heuristic – still keeps the signature identical\n\tif isNonRetriableBasicErr(err) {\n\t\treturn shared.ModelError{Kind: shared.ErrOther, Retriable: false}\n\t}\n\treturn shared.ModelError{Kind: shared.ErrOther, Retriable: true}\n}\n\nfunc isNonRetriableBasicErr(err error) bool {\n\terrStr := err.Error()\n\n\t// we don't want to retry on the errors below\n\tif strings.Contains(errStr, \"context deadline exceeded\") || strings.Contains(errStr, \"context canceled\") {\n\t\tlog.Println(\"Context deadline exceeded or canceled - no retry\")\n\t\treturn true\n\t}\n\n\tif strings.Contains(errStr, \"status code: 400\") &&\n\t\tstrings.Contains(errStr, \"reduce the length of the messages\") {\n\t\tlog.Println(\"Token limit exceeded - no retry\")\n\t\treturn true\n\t}\n\n\tif strings.Contains(errStr, \"status code: 401\") {\n\t\tlog.Println(\"Invalid auth or api key - no retry\")\n\t\treturn true\n\t}\n\n\tif strings.Contains(errStr, \"status code: 429\") && strings.Contains(errStr, \"exceeded your current quota\") {\n\t\tlog.Println(\"Current quota exceeded - no retry\")\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "app/server/model/model_request.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype ModelRequestParams struct {\n\tClients       map[string]ClientInfo\n\tAuthVars      map[string]string\n\tAuth          *types.ServerAuth\n\tPlan          *db.Plan\n\tModelConfig   *shared.ModelRoleConfig\n\tSettings      *shared.PlanSettings\n\tOrgUserConfig *shared.OrgUserConfig\n\tPurpose       string\n\n\tMessages   []types.ExtendedChatMessage\n\tPrediction string\n\tStop       []string\n\tTools      []openai.Tool\n\tToolChoice *openai.ToolChoice\n\n\tEstimatedOutputTokens int // optional\n\n\tModelStreamId  string\n\tConvoMessageId string\n\tBuildId        string\n\tModelPackName  string\n\tSessionId      string\n\n\tBeforeReq func()\n\tAfterReq  func()\n\n\tOnStream func(string, string) bool\n\n\tWillCacheNumTokens int\n}\n\nfunc ModelRequest(\n\tctx context.Context,\n\tparams ModelRequestParams,\n) (*types.ModelResponse, error) {\n\tclients := params.Clients\n\tauthVars := params.AuthVars\n\tauth := params.Auth\n\tplan := params.Plan\n\tmessages := params.Messages\n\tprediction := params.Prediction\n\tstop := params.Stop\n\ttools := params.Tools\n\ttoolChoice := params.ToolChoice\n\tmodelConfig := params.ModelConfig\n\tmodelStreamId := params.ModelStreamId\n\tconvoMessageId := params.ConvoMessageId\n\tbuildId := params.BuildId\n\tmodelPackName := params.ModelPackName\n\tpurpose := params.Purpose\n\tsessionId := params.SessionId\n\tsettings := params.Settings\n\torgUserConfig := params.OrgUserConfig\n\tcurrentOrgId := auth.OrgId\n\tcurrentUserId := auth.User.Id\n\n\tif purpose == \"\" {\n\t\treturn nil, fmt.Errorf(\"purpose is required\")\n\t}\n\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\tmessages = FilterEmptyMessages(messages)\n\tmessages = CheckSingleSystemMessage(modelConfig, baseModelConfig, messages)\n\tinputTokensEstimate := GetMessagesTokenEstimate(messages...) + TokensPerRequest\n\n\tconfig := modelConfig.GetRoleForInputTokens(inputTokensEstimate, settings)\n\tmodelConfig = &config\n\n\tif params.EstimatedOutputTokens != 0 {\n\t\tconfig = modelConfig.GetRoleForOutputTokens(params.EstimatedOutputTokens, settings)\n\t\tmodelConfig = &config\n\t}\n\n\tlog.Println(\"ModelRequest - modelConfig:\")\n\tspew.Dump(modelConfig)\n\tlog.Println(\"ModelRequest - baseModelConfig:\")\n\tspew.Dump(baseModelConfig)\n\n\tlog.Printf(\"Model config - role: %s, model: %s, max output tokens: %d\\n\", modelConfig.Role, baseModelConfig.ModelName, baseModelConfig.MaxOutputTokens)\n\n\texpectedOutputTokens := baseModelConfig.MaxOutputTokens - inputTokensEstimate\n\tif params.EstimatedOutputTokens != 0 {\n\t\texpectedOutputTokens = params.EstimatedOutputTokens\n\t}\n\n\t_, apiErr := hooks.ExecHook(hooks.WillSendModelRequest, hooks.HookParams{\n\t\tAuth: auth,\n\t\tPlan: plan,\n\t\tWillSendModelRequestParams: &hooks.WillSendModelRequestParams{\n\t\t\tInputTokens:  inputTokensEstimate,\n\t\t\tOutputTokens: expectedOutputTokens,\n\t\t\tModelName:    baseModelConfig.ModelName,\n\t\t\tModelId:      baseModelConfig.ModelId,\n\t\t\tModelTag:     baseModelConfig.ModelTag,\n\t\t},\n\t})\n\n\tif apiErr != nil {\n\t\treturn nil, apiErr\n\t}\n\n\tif params.BeforeReq != nil {\n\t\tparams.BeforeReq()\n\t}\n\n\treqStarted := time.Now()\n\n\treq := types.ExtendedChatCompletionRequest{\n\t\tModel:    baseModelConfig.ModelName,\n\t\tMessages: messages,\n\t}\n\n\tif !baseModelConfig.RoleParamsDisabled {\n\t\treq.Temperature = modelConfig.Temperature\n\t\treq.TopP = modelConfig.TopP\n\t}\n\n\tif len(tools) > 0 {\n\t\treq.Tools = tools\n\t}\n\n\tif toolChoice != nil {\n\t\treq.ToolChoice = toolChoice\n\t}\n\n\tonStream := params.OnStream\n\tif baseModelConfig.StopDisabled {\n\t\tif len(stop) > 0 {\n\t\t\tonStream = func(chunk string, buffer string) (shouldStop bool) {\n\t\t\t\tfor _, stopSequence := range stop {\n\t\t\t\t\tif strings.Contains(buffer, stopSequence) {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif params.OnStream != nil {\n\t\t\t\t\treturn params.OnStream(chunk, buffer)\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t} else {\n\t\treq.Stop = stop\n\t}\n\n\tif prediction != \"\" {\n\t\treq.Prediction = &types.OpenAIPrediction{\n\t\t\tType:    \"content\",\n\t\t\tContent: prediction,\n\t\t}\n\t}\n\n\tres, err := CreateChatCompletionWithInternalStream(clients, authVars, modelConfig, settings, orgUserConfig, currentOrgId, currentUserId, ctx, req, onStream, reqStarted)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif baseModelConfig.StopDisabled && len(stop) > 0 {\n\t\tearliest := len(res.Content)\n\t\tfound := false\n\t\tfor _, s := range stop {\n\t\t\tif i := strings.Index(res.Content, s); i != -1 && i < earliest {\n\t\t\t\tearliest = i\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t\tif found {\n\t\t\tres.Content = res.Content[:earliest]\n\t\t}\n\t}\n\n\tif params.AfterReq != nil {\n\t\tparams.AfterReq()\n\t}\n\n\t// log.Printf(\"\\n\\n**\\n\\nModel response: %s\\n\\n**\\n\\n\", res.Content)\n\n\tvar inputTokens int\n\tvar outputTokens int\n\tvar cachedTokens int\n\n\tif res.Usage != nil {\n\t\tif res.Usage.PromptTokensDetails != nil {\n\t\t\tcachedTokens = res.Usage.PromptTokensDetails.CachedTokens\n\t\t}\n\t\tinputTokens = res.Usage.PromptTokens\n\t\toutputTokens = res.Usage.CompletionTokens\n\t} else {\n\t\tinputTokens = inputTokensEstimate\n\t\toutputTokens = shared.GetNumTokensEstimate(res.Content)\n\n\t\tif params.WillCacheNumTokens > 0 {\n\t\t\tcachedTokens = params.WillCacheNumTokens\n\t\t}\n\t}\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in DidSendModelRequest hook: %v\\n%s\", r, debug.Stack())\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic in DidSendModelRequest hook: %v\\n%s\", r, debug.Stack()))\n\t\t\t}\n\t\t}()\n\n\t\t_, apiErr := hooks.ExecHook(hooks.DidSendModelRequest, hooks.HookParams{\n\t\t\tAuth: auth,\n\t\t\tPlan: plan,\n\t\t\tDidSendModelRequestParams: &hooks.DidSendModelRequestParams{\n\t\t\t\tInputTokens:    inputTokens,\n\t\t\t\tOutputTokens:   outputTokens,\n\t\t\t\tCachedTokens:   cachedTokens,\n\t\t\t\tModelId:        baseModelConfig.ModelId,\n\t\t\t\tModelTag:       baseModelConfig.ModelTag,\n\t\t\t\tModelName:      baseModelConfig.ModelName,\n\t\t\t\tModelProvider:  baseModelConfig.Provider,\n\t\t\t\tModelPackName:  modelPackName,\n\t\t\t\tModelRole:      modelConfig.Role,\n\t\t\t\tPurpose:        purpose,\n\t\t\t\tGenerationId:   res.GenerationId,\n\t\t\t\tPlanId:         plan.Id,\n\t\t\t\tModelStreamId:  modelStreamId,\n\t\t\t\tConvoMessageId: convoMessageId,\n\t\t\t\tBuildId:        buildId,\n\n\t\t\t\tRequestStartedAt: reqStarted,\n\t\t\t\tStreaming:        true,\n\t\t\t\tReq:              &req,\n\t\t\t\tStreamResult:     res.Content,\n\t\t\t\tModelConfig:      modelConfig,\n\t\t\t\tFirstTokenAt:     res.FirstTokenAt,\n\t\t\t\tSessionId:        sessionId,\n\t\t\t},\n\t\t})\n\n\t\tif apiErr != nil {\n\t\t\tlog.Printf(\"buildWholeFile - error executing DidSendModelRequest hook: %v\", apiErr)\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error executing DidSendModelRequest hook: %v\", apiErr))\n\t\t}\n\t}()\n\n\treturn res, nil\n}\n\nfunc FilterEmptyMessages(messages []types.ExtendedChatMessage) []types.ExtendedChatMessage {\n\tfilteredMessages := []types.ExtendedChatMessage{}\n\tfor _, message := range messages {\n\t\tvar content []types.ExtendedChatMessagePart\n\t\tfor _, part := range message.Content {\n\t\t\tif part.Type != openai.ChatMessagePartTypeText || part.Text != \"\" {\n\t\t\t\tcontent = append(content, part)\n\t\t\t}\n\t\t}\n\t\tif len(content) > 0 {\n\t\t\tfilteredMessages = append(filteredMessages, types.ExtendedChatMessage{\n\t\t\t\tRole:    message.Role,\n\t\t\t\tContent: content,\n\t\t\t})\n\t\t}\n\t}\n\treturn filteredMessages\n}\n\nfunc CheckSingleSystemMessage(modelConfig *shared.ModelRoleConfig, baseModelConfig *shared.BaseModelConfig, messages []types.ExtendedChatMessage) []types.ExtendedChatMessage {\n\tif len(messages) == 1 && baseModelConfig.SingleMessageNoSystemPrompt {\n\t\tif messages[0].Role == openai.ChatMessageRoleSystem {\n\t\t\tmsg := messages[0]\n\t\t\tmsg.Role = openai.ChatMessageRoleUser\n\t\t\treturn []types.ExtendedChatMessage{msg}\n\t\t}\n\t}\n\n\treturn messages\n}\n"
  },
  {
    "path": "app/server/model/name.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/types\"\n\t\"plandex-server/utils\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc GenPlanName(\n\tauth *types.ServerAuth,\n\tplan *db.Plan,\n\tsettings *shared.PlanSettings,\n\torgUserConfig *shared.OrgUserConfig,\n\tclients map[string]ClientInfo,\n\tauthVars map[string]string,\n\tplanContent string,\n\tsessionId string,\n\tctx context.Context,\n) (string, error) {\n\tconfig := settings.GetModelPack().Namer\n\n\tvar tools []openai.Tool\n\tvar toolChoice *openai.ToolChoice\n\n\tbaseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\tvar sysPrompt string\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tsysPrompt = prompts.SysPlanNameXml\n\t} else {\n\t\tsysPrompt = prompts.SysPlanName\n\t\ttools = []openai.Tool{\n\t\t\t{\n\t\t\t\tType:     \"function\",\n\t\t\t\tFunction: &prompts.PlanNameFn,\n\t\t\t},\n\t\t}\n\t\tchoice := openai.ToolChoice{\n\t\t\tType: \"function\",\n\t\t\tFunction: openai.ToolFunction{\n\t\t\t\tName: prompts.PlanNameFn.Name,\n\t\t\t},\n\t\t}\n\t\ttoolChoice = &choice\n\t}\n\n\tprompt := prompts.GetPlanNamePrompt(sysPrompt, planContent)\n\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodelRes, err := ModelRequest(ctx, ModelRequestParams{\n\t\tClients:       clients,\n\t\tAuthVars:      authVars,\n\t\tAuth:          auth,\n\t\tPlan:          plan,\n\t\tModelConfig:   &config,\n\t\tOrgUserConfig: orgUserConfig,\n\t\tPurpose:       \"Plan name\",\n\t\tMessages:      messages,\n\t\tTools:         tools,\n\t\tToolChoice:    toolChoice,\n\t\tSessionId:     sessionId,\n\t\tSettings:      settings,\n\t})\n\n\tif err != nil {\n\t\tfmt.Printf(\"Error during plan name model call: %v\\n\", err)\n\t\treturn \"\", err\n\t}\n\n\tvar planName string\n\tcontent := modelRes.Content\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tplanName = utils.GetXMLContent(content, \"planName\")\n\t\tif planName == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"No planName tag found in XML response\")\n\t\t}\n\t} else {\n\t\tif content == \"\" {\n\t\t\tfmt.Println(\"no namePlan function call found in response\")\n\t\t\treturn \"\", fmt.Errorf(\"No namePlan function call found in response. The model failed to generate a valid response.\")\n\t\t}\n\n\t\tvar nameRes prompts.PlanNameRes\n\t\terr = json.Unmarshal([]byte(content), &nameRes)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error unmarshalling plan description response: %v\\n\", err)\n\t\t\treturn \"\", err\n\t\t}\n\t\tplanName = nameRes.PlanName\n\t}\n\n\treturn planName, nil\n}\n\ntype GenPipedDataNameParams struct {\n\tCtx           context.Context\n\tAuth          *types.ServerAuth\n\tPlan          *db.Plan\n\tSettings      *shared.PlanSettings\n\tOrgUserConfig *shared.OrgUserConfig\n\tAuthVars      map[string]string\n\tSessionId     string\n\tClients       map[string]ClientInfo\n\tPipedContent  string\n}\n\nfunc GenPipedDataName(\n\tparams GenPipedDataNameParams,\n) (string, error) {\n\tctx := params.Ctx\n\tauth := params.Auth\n\tplan := params.Plan\n\tsettings := params.Settings\n\tclients := params.Clients\n\tauthVars := params.AuthVars\n\tpipedContent := params.PipedContent\n\tsessionId := params.SessionId\n\torgUserConfig := params.OrgUserConfig\n\n\tconfig := settings.GetModelPack().Namer\n\n\tvar sysPrompt string\n\tvar tools []openai.Tool\n\tvar toolChoice *openai.ToolChoice\n\n\tbaseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tsysPrompt = prompts.SysPipedDataNameXml\n\t} else {\n\t\tsysPrompt = prompts.SysPipedDataName\n\t\ttools = []openai.Tool{\n\t\t\t{\n\t\t\t\tType:     \"function\",\n\t\t\t\tFunction: &prompts.PipedDataNameFn,\n\t\t\t},\n\t\t}\n\t\tchoice := openai.ToolChoice{\n\t\t\tType: \"function\",\n\t\t\tFunction: openai.ToolFunction{\n\t\t\t\tName: prompts.PipedDataNameFn.Name,\n\t\t\t},\n\t\t}\n\t\ttoolChoice = &choice\n\t}\n\n\tprompt := prompts.GetPipedDataNamePrompt(sysPrompt, pipedContent)\n\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodelRes, err := ModelRequest(ctx, ModelRequestParams{\n\t\tClients:       clients,\n\t\tAuth:          auth,\n\t\tAuthVars:      authVars,\n\t\tPlan:          plan,\n\t\tModelConfig:   &config,\n\t\tPurpose:       \"Piped data name\",\n\t\tMessages:      messages,\n\t\tTools:         tools,\n\t\tToolChoice:    toolChoice,\n\t\tSessionId:     sessionId,\n\t\tSettings:      settings,\n\t\tOrgUserConfig: orgUserConfig,\n\t})\n\n\tif err != nil {\n\t\tfmt.Printf(\"Error during piped data name model call: %v\\n\", err)\n\t\treturn \"\", err\n\t}\n\n\tvar name string\n\tcontent := modelRes.Content\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tname = utils.GetXMLContent(content, \"name\")\n\t\tif name == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"No name tag found in XML response\")\n\t\t}\n\t} else {\n\t\tif content == \"\" {\n\t\t\tfmt.Println(\"no namePipedData function call found in response\")\n\t\t\treturn \"\", fmt.Errorf(\"No namePipedData function call found in response. The model failed to generate a valid response.\")\n\t\t}\n\n\t\tvar nameRes prompts.PipedDataNameRes\n\t\terr = json.Unmarshal([]byte(content), &nameRes)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error unmarshalling piped data name response: %v\\n\", err)\n\t\t\treturn \"\", err\n\t\t}\n\t\tname = nameRes.Name\n\t}\n\n\treturn name, nil\n}\n\nfunc GenNoteName(\n\tctx context.Context,\n\tauth *types.ServerAuth,\n\tplan *db.Plan,\n\tsettings *shared.PlanSettings,\n\torgUserConfig *shared.OrgUserConfig,\n\tclients map[string]ClientInfo,\n\tauthVars map[string]string,\n\tnote string,\n\tsessionId string,\n) (string, error) {\n\tconfig := settings.GetModelPack().Namer\n\n\tvar sysPrompt string\n\tvar tools []openai.Tool\n\tvar toolChoice *openai.ToolChoice\n\n\tbaseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tsysPrompt = prompts.SysNoteNameXml\n\t} else {\n\t\tsysPrompt = prompts.SysNoteName\n\t\ttools = []openai.Tool{\n\t\t\t{\n\t\t\t\tType:     \"function\",\n\t\t\t\tFunction: &prompts.NoteNameFn,\n\t\t\t},\n\t\t}\n\t\tchoice := openai.ToolChoice{\n\t\t\tType: \"function\",\n\t\t\tFunction: openai.ToolFunction{\n\t\t\t\tName: prompts.NoteNameFn.Name,\n\t\t\t},\n\t\t}\n\t\ttoolChoice = &choice\n\t}\n\n\tprompt := prompts.GetNoteNamePrompt(sysPrompt, note)\n\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodelRes, err := ModelRequest(ctx, ModelRequestParams{\n\t\tClients:       clients,\n\t\tAuth:          auth,\n\t\tAuthVars:      authVars,\n\t\tPlan:          plan,\n\t\tModelConfig:   &config,\n\t\tPurpose:       \"Note name\",\n\t\tMessages:      messages,\n\t\tTools:         tools,\n\t\tToolChoice:    toolChoice,\n\t\tSessionId:     sessionId,\n\t\tSettings:      settings,\n\t\tOrgUserConfig: orgUserConfig,\n\t})\n\n\tif err != nil {\n\t\tfmt.Printf(\"Error during note name model call: %v\\n\", err)\n\t\treturn \"\", err\n\t}\n\n\tvar name string\n\tcontent := modelRes.Content\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tname = utils.GetXMLContent(content, \"name\")\n\t\tif name == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"No name tag found in XML response\")\n\t\t}\n\t} else {\n\t\tif content == \"\" {\n\t\t\tfmt.Println(\"no nameNote function call found in response\")\n\t\t\treturn \"\", fmt.Errorf(\"No nameNote function call found in response. The model failed to generate a valid response.\")\n\t\t}\n\n\t\tvar nameRes prompts.NoteNameRes\n\t\terr = json.Unmarshal([]byte(content), &nameRes)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error unmarshalling note name response: %v\\n\", err)\n\t\t\treturn \"\", err\n\t\t}\n\t\tname = nameRes.Name\n\t}\n\n\treturn name, nil\n}\n"
  },
  {
    "path": "app/server/model/parse/subtasks.go",
    "content": "package parse\n\nimport (\n\t\"log\"\n\t\"plandex-server/db\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc ParseSubtasks(replyContent string) []*db.Subtask {\n\tsplit := strings.Split(replyContent, \"### Tasks\")\n\tif len(split) < 2 {\n\t\tsplit = strings.Split(replyContent, \"### Task\")\n\t\tif len(split) < 2 {\n\t\t\tlog.Println(\"[Subtasks] No tasks section found in reply\")\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tlines := strings.Split(split[1], \"\\n\")\n\n\tvar subtasks []*db.Subtask\n\tvar currentTask *db.Subtask\n\tvar descLines []string\n\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check for any number followed by a period and space\n\t\tif matched, _ := regexp.MatchString(`^\\d+\\.\\s`, line); matched {\n\t\t\t// Save previous task if exists\n\t\t\tif currentTask != nil {\n\t\t\t\tcurrentTask.Description = strings.Join(descLines, \"\\n\")\n\t\t\t\tlog.Printf(\"[Subtasks] Adding subtask: %q with %d uses files\", currentTask.Title, len(currentTask.UsesFiles))\n\t\t\t\tsubtasks = append(subtasks, currentTask)\n\t\t\t}\n\n\t\t\t// Start new task\n\t\t\tparts := strings.SplitN(line, \". \", 2)\n\t\t\tif len(parts) == 2 {\n\t\t\t\ttitle := parts[1]\n\t\t\t\tcurrentTask = &db.Subtask{\n\t\t\t\t\tTitle: title,\n\t\t\t\t}\n\t\t\t\tdescLines = nil\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Handle Uses: section\n\t\tif strings.HasPrefix(line, \"Uses:\") {\n\t\t\tif currentTask != nil {\n\t\t\t\tusesStr := strings.TrimPrefix(line, \"Uses:\")\n\t\t\t\tfor _, use := range strings.Split(usesStr, \",\") {\n\t\t\t\t\tuse = strings.TrimSpace(use)\n\t\t\t\t\tuse = strings.Trim(use, \"`\")\n\t\t\t\t\tif use != \"\" {\n\t\t\t\t\t\tcurrentTask.UsesFiles = append(currentTask.UsesFiles, use)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"[Subtasks] Added uses files for %q: %v\", currentTask.Title, currentTask.UsesFiles)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Add to description if we have a current task\n\t\tif currentTask != nil {\n\t\t\t// Remove bullet point if present, but don't require it\n\t\t\tline = strings.TrimPrefix(line, \"-\")\n\t\t\tline = strings.TrimSpace(line)\n\t\t\tif line != \"\" {\n\t\t\t\tdescLines = append(descLines, line)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Save final task if exists\n\tif currentTask != nil {\n\t\tcurrentTask.Description = strings.Join(descLines, \"\\n\")\n\t\tlog.Printf(\"[Subtasks] Adding final subtask: %q with %d uses files\", currentTask.Title, len(currentTask.UsesFiles))\n\t\tsubtasks = append(subtasks, currentTask)\n\t}\n\n\tlog.Printf(\"[Subtasks] Parsed %d total subtasks\", len(subtasks))\n\treturn subtasks\n}\n\nfunc ParseRemoveSubtasks(replyContent string) []string {\n\tsplit := strings.Split(replyContent, \"### Remove Tasks\")\n\tif len(split) < 2 {\n\t\treturn nil\n\t}\n\n\tsection := split[1]\n\tlines := strings.Split(section, \"\\n\")\n\tvar tasksToRemove []string\n\n\tsawEmptyLine := false\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" {\n\t\t\tsawEmptyLine = true\n\t\t\tcontinue\n\t\t}\n\t\tif sawEmptyLine && !strings.HasPrefix(line, \"-\") {\n\t\t\tbreak\n\t\t}\n\t\tif strings.HasPrefix(line, \"- \") {\n\t\t\ttitle := strings.TrimPrefix(line, \"- \")\n\t\t\ttitle = strings.TrimSpace(title)\n\t\t\tif title != \"\" {\n\t\t\t\ttasksToRemove = append(tasksToRemove, title)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn tasksToRemove\n}\n"
  },
  {
    "path": "app/server/model/parse/subtasks_test.go",
    "content": "package parse\n\nimport (\n\t\"plandex-server/db\"\n\t\"testing\"\n)\n\nfunc TestParseSubtasks(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected []*db.Subtask\n\t}{\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    \"\",\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"single task without description\",\n\t\t\tinput: `### Tasks\n1. Create a new file`,\n\t\t\texpected: []*db.Subtask{\n\t\t\t\t{\n\t\t\t\t\tTitle:       \"Create a new file\",\n\t\t\t\t\tDescription: \"\",\n\t\t\t\t\tUsesFiles:   nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple tasks with descriptions and uses\",\n\t\t\tinput: `### Tasks\n1. Create config file\n- Will store application settings\n- Contains environment variables\nUses: ` + \"`config/settings.yml`\" + `, ` + \"`config/defaults.yml`\" + `\n\n2. Update main function\n- Add configuration loading\nUses: ` + \"`main.go`\",\n\t\t\texpected: []*db.Subtask{\n\t\t\t\t{\n\t\t\t\t\tTitle:       \"Create config file\",\n\t\t\t\t\tDescription: \"Will store application settings\\nContains environment variables\",\n\t\t\t\t\tUsesFiles:   []string{\"config/settings.yml\", \"config/defaults.yml\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTitle:       \"Update main function\",\n\t\t\t\t\tDescription: \"Add configuration loading\",\n\t\t\t\t\tUsesFiles:   []string{\"main.go\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"alternative task header\",\n\t\t\tinput: `### Task\n1. Simple task`,\n\t\t\texpected: []*db.Subtask{\n\t\t\t\t{\n\t\t\t\t\tTitle:       \"Simple task\",\n\t\t\t\t\tDescription: \"\",\n\t\t\t\t\tUsesFiles:   nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"tasks with empty lines between\",\n\t\t\tinput: `### Tasks\n1. First task\n- Description one\n\n2. Second task\n- Description two`,\n\t\t\texpected: []*db.Subtask{\n\t\t\t\t{\n\t\t\t\t\tTitle:       \"First task\",\n\t\t\t\t\tDescription: \"Description one\",\n\t\t\t\t\tUsesFiles:   nil,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTitle:       \"Second task\",\n\t\t\t\t\tDescription: \"Description two\",\n\t\t\t\t\tUsesFiles:   nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single task from pong\",\n\t\t\tinput: \"### Tasks\" + `\n\n9. Update Makefile to include Homebrew-specific include and library search paths\n- Modify CFLAGS in Makefile to add -I/opt/homebrew/include\n- Modify LDFLAGS in Makefile to add -L/opt/homebrew/lib\nUses: ` + \"`Makefile`\" + `, ` + \"`_apply.sh`\",\n\t\t\texpected: []*db.Subtask{\n\t\t\t\t{\n\t\t\t\t\tTitle:       \"Update Makefile to include Homebrew-specific include and library search paths\",\n\t\t\t\t\tDescription: \"Modify CFLAGS in Makefile to add -I/opt/homebrew/include\\nModify LDFLAGS in Makefile to add -L/opt/homebrew/lib\",\n\t\t\t\t\tUsesFiles:   []string{\"Makefile\", \"_apply.sh\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := ParseSubtasks(tt.input)\n\n\t\t\tif len(got) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"ParseSubtasks() returned %d subtasks, want %d\", len(got), len(tt.expected))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor i := range got {\n\t\t\t\tif got[i].Title != tt.expected[i].Title {\n\t\t\t\t\tt.Errorf(\"Subtask[%d].Title = %q, want %q\", i, got[i].Title, tt.expected[i].Title)\n\t\t\t\t}\n\t\t\t\tif got[i].Description != tt.expected[i].Description {\n\t\t\t\t\tt.Errorf(\"Subtask[%d].Description = %q, want %q\", i, got[i].Description, tt.expected[i].Description)\n\t\t\t\t}\n\t\t\t\tif !sliceEqual(got[i].UsesFiles, tt.expected[i].UsesFiles) {\n\t\t\t\t\tt.Errorf(\"Subtask[%d].UsesFiles = %v, want %v\", i, got[i].UsesFiles, tt.expected[i].UsesFiles)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// sliceEqual compares two string slices for equality\nfunc sliceEqual(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "app/server/model/plan/activate.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/db\"\n\t\"plandex-server/host\"\n\t\"plandex-server/model\"\n\t\"plandex-server/types\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc activatePlan(\n\tclients map[string]model.ClientInfo,\n\tplan *db.Plan,\n\tbranch string,\n\tauth *types.ServerAuth,\n\tprompt string,\n\tbuildOnly,\n\tautoContext bool,\n\tsessionId string,\n) (*types.ActivePlan, error) {\n\tlog.Printf(\"Activate plan: plan ID %s on branch %s\\n\", plan.Id, branch)\n\n\t// Just in case this request was made immediately after another stream finished, wait a little to allow for cleanup\n\tlog.Println(\"Waiting 100ms before checking for active plan\")\n\ttime.Sleep(100 * time.Millisecond)\n\tlog.Println(\"Done waiting, checking for active plan\")\n\n\tactive := GetActivePlan(plan.Id, branch)\n\tif active != nil {\n\t\tlog.Printf(\"Tell: Active plan found for plan ID %s on branch %s\\n\", plan.Id, branch) // Log if an active plan is found\n\t\treturn nil, fmt.Errorf(\"plan %s branch %s already has an active stream on this host\", plan.Id, branch)\n\t}\n\n\tmodelStream, err := db.GetActiveModelStream(plan.Id, branch)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting active model stream: %v\\n\", err)\n\t\treturn nil, fmt.Errorf(\"error getting active model stream: %v\", err)\n\t}\n\n\tif modelStream != nil {\n\t\tlog.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\n\t\treturn nil, fmt.Errorf(\"plan %s branch %s already has an active stream on host %s\", plan.Id, branch, modelStream.InternalIp)\n\t}\n\n\tactive = CreateActivePlan(\n\t\tauth.OrgId,\n\t\tauth.User.Id,\n\t\tplan.Id,\n\t\tbranch,\n\t\tprompt,\n\t\tbuildOnly,\n\t\tautoContext,\n\t\tsessionId,\n\t)\n\n\tmodelStream = &db.ModelStream{\n\t\tOrgId:      auth.OrgId,\n\t\tPlanId:     plan.Id,\n\t\tInternalIp: host.Ip,\n\t\tBranch:     branch,\n\t}\n\terr = db.StoreModelStream(modelStream, active.Ctx, active.CancelFn)\n\tif err != nil {\n\t\tlog.Printf(\"Tell: Error storing model stream for plan ID %s on branch %s: %v\\n\", plan.Id, branch, err) // Log error storing model stream\n\t\tlog.Printf(\"Error storing model stream: %v\\n\", err)\n\t\tlog.Printf(\"Tell: Error storing model stream: %v\\n\", err) // Log error storing model stream\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{Msg: fmt.Sprintf(\"Error storing model stream: %v\", err)}\n\n\t\treturn nil, fmt.Errorf(\"error storing model stream: %v\", err)\n\t}\n\n\tactive.ModelStreamId = modelStream.Id\n\n\tlog.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\n\tlog.Println(\"Model stream id:\", modelStream.Id)\n\n\treturn active, nil\n}\n"
  },
  {
    "path": "app/server/model/plan/build_exec.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/model\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\ntype BuildParams struct {\n\tClients       map[string]model.ClientInfo\n\tAuthVars      map[string]string\n\tPlan          *db.Plan\n\tBranch        string\n\tAuth          *types.ServerAuth\n\tSessionId     string\n\tOrgUserConfig *shared.OrgUserConfig\n\tSettings      *shared.PlanSettings\n}\n\nfunc Build(params BuildParams) (int, error) {\n\tclients := params.Clients\n\tauthVars := params.AuthVars\n\tplan := params.Plan\n\tbranch := params.Branch\n\tauth := params.Auth\n\tsessionId := params.SessionId\n\torgUserConfig := params.OrgUserConfig\n\tsettings := params.Settings\n\n\tlog.Printf(\"Build: Called with plan ID %s on branch %s\\n\", plan.Id, branch)\n\tlog.Println(\"Build: Starting Build operation\")\n\n\tstate := activeBuildStreamState{\n\t\tclients:       clients,\n\t\tauthVars:      authVars,\n\t\tauth:          auth,\n\t\tcurrentOrgId:  auth.OrgId,\n\t\tcurrentUserId: auth.User.Id,\n\t\torgUserConfig: orgUserConfig,\n\t\tplan:          plan,\n\t\tbranch:        branch,\n\t\tsettings:      settings,\n\t}\n\n\tstreamDone := func() {\n\t\tactive := GetActivePlan(plan.Id, branch)\n\t\tif active != nil {\n\t\t\tactive.StreamDoneCh <- nil\n\t\t}\n\t}\n\n\tonErr := func(err error) (int, error) {\n\t\tlog.Printf(\"Build error: %v\\n\", err)\n\t\tstreamDone()\n\t\treturn 0, err\n\t}\n\n\tpendingBuildsByPath, err := state.loadPendingBuilds(sessionId)\n\tif err != nil {\n\t\treturn onErr(err)\n\t}\n\n\tif len(pendingBuildsByPath) == 0 {\n\t\tlog.Println(\"No pending builds\")\n\t\tstreamDone()\n\t\treturn 0, nil\n\t}\n\n\terr = db.SetPlanStatus(plan.Id, branch, shared.PlanStatusBuilding, \"\")\n\n\tif err != nil {\n\t\tlog.Printf(\"Error setting plan status to building: %v\\n\", err)\n\t\treturn onErr(fmt.Errorf(\"error setting plan status to building: %v\", err))\n\t}\n\n\tlog.Printf(\"Starting %d builds\\n\", len(pendingBuildsByPath))\n\n\tfor _, pendingBuilds := range pendingBuildsByPath {\n\t\tgo state.queueBuilds(pendingBuilds)\n\t}\n\n\treturn len(pendingBuildsByPath), nil\n}\n\nfunc (state *activeBuildStreamState) queueBuild(activeBuild *types.ActiveBuild) {\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\n\tfilePath := activeBuild.Path\n\n\t// log.Printf(\"Queue:\")\n\t// spew.Dump(activePlan.BuildQueuesByPath[filePath])\n\n\tvar isBuilding bool\n\n\tUpdateActivePlan(planId, branch, func(active *types.ActivePlan) {\n\t\tactive.BuildQueuesByPath[filePath] = append(active.BuildQueuesByPath[filePath], activeBuild)\n\t\tisBuilding = active.IsBuildingByPath[filePath]\n\t})\n\tlog.Printf(\"Queued build for file %s\\n\", filePath)\n\n\tif isBuilding {\n\t\tlog.Printf(\"Already building file %s\\n\", filePath)\n\t\treturn\n\t} else {\n\t\tlog.Printf(\"Not building file %s\\n\", filePath)\n\n\t\tactive := GetActivePlan(planId, branch)\n\t\tif active == nil {\n\t\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\\n\", planId, branch)\n\t\t\treturn\n\t\t}\n\n\t\tUpdateActivePlan(planId, branch, func(active *types.ActivePlan) {\n\t\t\tactive.IsBuildingByPath[filePath] = true\n\t\t})\n\n\t\tgo state.execPlanBuild(activeBuild)\n\t}\n}\n\nfunc (state *activeBuildStreamState) queueBuilds(activeBuilds []*types.ActiveBuild) {\n\tlog.Printf(\"Queueing %d builds\\n\", len(activeBuilds))\n\n\tfor _, activeBuild := range activeBuilds {\n\t\tstate.queueBuild(activeBuild)\n\t}\n}\n\nfunc (buildState *activeBuildStreamState) execPlanBuild(activeBuild *types.ActiveBuild) {\n\tif activeBuild == nil {\n\t\tlog.Println(\"No active build\")\n\t\treturn\n\t}\n\n\tlog.Printf(\"execPlanBuild - %s\\n\", activeBuild.Path)\n\t// log.Println(spew.Sdump(activeBuild))\n\n\tplanId := buildState.plan.Id\n\tbranch := buildState.branch\n\n\tactivePlan := GetActivePlan(planId, branch)\n\tif activePlan == nil {\n\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"execPlanBuild: Panic: %v\\n%s\\n\", r, string(debug.Stack()))\n\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"execPlanBuild: Panic: %v\\n%s\", r, string(debug.Stack())))\n\n\t\t\tactivePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"Panic in execPlanBuild\",\n\t\t\t}\n\t\t}\n\t}()\n\n\tfilePath := activeBuild.Path\n\n\tif !activePlan.IsBuildingByPath[filePath] {\n\t\tUpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {\n\t\t\tap.IsBuildingByPath[filePath] = true\n\t\t})\n\t}\n\n\tfileState := &activeBuildStreamFileState{\n\t\tactiveBuildStreamState: buildState,\n\t\tfilePath:               filePath,\n\t\tactiveBuild:            activeBuild,\n\t\tbuilderRun: hooks.DidFinishBuilderRunParams{\n\t\t\tStartedAt: time.Now(),\n\t\t\tPlanId:    activePlan.Id,\n\t\t\tFilePath:  filePath,\n\t\t\tFileExt:   filepath.Ext(filePath),\n\t\t},\n\t}\n\n\tlog.Printf(\"execPlanBuild - %s - calling fileState.loadBuildFile()\\n\", filePath)\n\terr := fileState.loadBuildFile(activeBuild)\n\tif err != nil {\n\t\tlog.Printf(\"Error loading build file: %v\\n\", err)\n\t\tfileState.onBuildFileError(fmt.Errorf(\"error loading build file: %v\", err))\n\t\treturn\n\t}\n\n\tfileState.resolvePreBuildState()\n\n\t// unless it's a file operation, stream initial status to client\n\tif !activeBuild.IsFileOperation() && !fileState.isNewFile {\n\t\tlog.Printf(\"execPlanBuild - %s - streaming initial build info\\n\", filePath)\n\t\t// spew.Dump(activeBuild)\n\t\tbuildInfo := &shared.BuildInfo{\n\t\t\tPath:      filePath,\n\t\t\tNumTokens: 0,\n\t\t\tFinished:  false,\n\t\t}\n\t\tactivePlan.Stream(shared.StreamMessage{\n\t\t\tType:      shared.StreamMessageBuildInfo,\n\t\t\tBuildInfo: buildInfo,\n\t\t})\n\t} else if activeBuild.IsFileOperation() {\n\t\tlog.Printf(\"execPlanBuild - %s - file operation - won't stream initial build info\\n\", filePath)\n\t} else if fileState.isNewFile {\n\t\tlog.Printf(\"execPlanBuild - %s - new file - won't stream initial build info\\n\", filePath)\n\t}\n\n\tlog.Printf(\"execPlanBuild - %s - calling fileState.buildFile()\\n\", filePath)\n\tfileState.buildFile()\n}\n\nfunc (fileState *activeBuildStreamFileState) buildFile() {\n\tfilePath := fileState.filePath\n\tactiveBuild := fileState.activeBuild\n\tplanId := fileState.plan.Id\n\tbranch := fileState.branch\n\tcurrentOrgId := fileState.currentOrgId\n\tbuild := fileState.build\n\n\tactivePlan := GetActivePlan(planId, branch)\n\n\tif activePlan == nil {\n\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\n\tlog.Printf(\"Building file %s\\n\", filePath)\n\tlog.Printf(\"%d files in context\\n\", len(activePlan.ContextsByPath))\n\t// log.Println(\"activePlan.ContextsByPath files:\")\n\t// for k := range activePlan.ContextsByPath {\n\t// \tlog.Println(k)\n\t// }\n\n\tif activeBuild.IsMoveOp {\n\t\tlog.Printf(\"File %s is a move operation. Moving to %s\\n\", filePath, activeBuild.MoveDestination)\n\n\t\t// For move operations, we split it into two separate builds:\n\t\t// 1. A removal build for the source file\n\t\t// 2. A creation build for the destination file with the current content\n\t\t// This is simpler than handling moves in a single build since our build system\n\t\t// is designed around operating on one path at a time\n\t\tfileState.activeBuildStreamState.queueBuilds([]*types.ActiveBuild{\n\t\t\t{\n\t\t\t\tReplyId:    activeBuild.ReplyId,\n\t\t\t\tPath:       activeBuild.Path,\n\t\t\t\tIsRemoveOp: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tReplyId:           activeBuild.ReplyId,\n\t\t\t\tPath:              activeBuild.MoveDestination,\n\t\t\t\tFileContent:       fileState.preBuildState,\n\t\t\t\tFileContentTokens: 0,\n\t\t\t},\n\t\t})\n\n\t\t// Mark this move operation as successful since we've queued the actual work\n\t\tactiveBuild.Success = true\n\n\t\tUpdateActivePlan(planId, branch, func(active *types.ActivePlan) {\n\t\t\tactive.IsBuildingByPath[filePath] = false\n\t\t\tactive.BuiltFiles[filePath] = true\n\t\t})\n\n\t\t// Process the next build in queue (which will be our removal build)\n\t\t// We need to explicitly advance the queue for the source path since this\n\t\t// current build is holding the 'building' state open\n\t\t// The create build for the destination will be handled automatically by the queue logic\n\t\tfileState.buildNextInQueue()\n\t\treturn\n\t}\n\n\tif activeBuild.IsRemoveOp {\n\t\tlog.Printf(\"File %s is a remove operation. Removing file.\\n\", filePath)\n\n\t\tlog.Printf(\"streaming remove build info for file %s\\n\", filePath)\n\t\tbuildInfo := &shared.BuildInfo{\n\t\t\tPath:      filePath,\n\t\t\tNumTokens: 0,\n\t\t\tRemoved:   true,\n\t\t\tFinished:  true,\n\t\t}\n\n\t\tactivePlan.Stream(shared.StreamMessage{\n\t\t\tType:      shared.StreamMessageBuildInfo,\n\t\t\tBuildInfo: buildInfo,\n\t\t})\n\n\t\tplanRes := &db.PlanFileResult{\n\t\t\tOrgId:          currentOrgId,\n\t\t\tPlanId:         planId,\n\t\t\tPlanBuildId:    build.Id,\n\t\t\tConvoMessageId: build.ConvoMessageId,\n\t\t\tPath:           filePath,\n\t\t\tContent:        \"\",\n\t\t\tRemovedFile:    true,\n\t\t}\n\t\tfileState.onFinishBuildFile(planRes)\n\t\treturn\n\t}\n\n\tif activeBuild.IsResetOp {\n\t\tlog.Printf(\"File %s is a reset operation. Resetting file.\\n\", filePath)\n\n\t\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\t\tOrgId:       currentOrgId,\n\t\t\tUserId:      fileState.currentUserId,\n\t\t\tPlanId:      planId,\n\t\t\tBranch:      branch,\n\t\t\tPlanBuildId: build.Id,\n\t\t\tScope:       db.LockScopeWrite,\n\t\t\tReason:      \"reset file op\",\n\t\t\tCtx:         activePlan.Ctx,\n\t\t\tCancelFn:    activePlan.CancelFn,\n\t\t}, func(repo *db.GitRepo) error {\n\t\t\tnow := time.Now()\n\t\t\treturn db.RejectPlanFile(currentOrgId, planId, filePath, now)\n\t\t})\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error rejecting plan file: %v\\n\", err)\n\t\t\tfileState.onBuildFileError(fmt.Errorf(\"error rejecting plan file: %v\", err))\n\t\t\treturn\n\t\t}\n\n\t\tbuildInfo := &shared.BuildInfo{\n\t\t\tPath:      filePath,\n\t\t\tNumTokens: 0,\n\t\t\tFinished:  true,\n\t\t\tRemoved:   fileState.contextPart == nil,\n\t\t}\n\n\t\tactivePlan.Stream(shared.StreamMessage{\n\t\t\tType:      shared.StreamMessageBuildInfo,\n\t\t\tBuildInfo: buildInfo,\n\t\t})\n\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\tfileState.onBuildProcessed(activeBuild)\n\t\treturn\n\t}\n\n\tif fileState.preBuildState == \"\" {\n\t\tlog.Printf(\"File %s not found in model context or current plan. Creating new file.\\n\", filePath)\n\n\t\tbuildInfo := &shared.BuildInfo{\n\t\t\tPath:      filePath,\n\t\t\tNumTokens: 0,\n\t\t\tFinished:  true,\n\t\t}\n\n\t\tlog.Printf(\"streaming new file build info for file %s\\n\", filePath)\n\n\t\tactivePlan.Stream(shared.StreamMessage{\n\t\t\tType:      shared.StreamMessageBuildInfo,\n\t\t\tBuildInfo: buildInfo,\n\t\t})\n\n\t\t// new file\n\t\tplanRes := &db.PlanFileResult{\n\t\t\tOrgId:          currentOrgId,\n\t\t\tPlanId:         planId,\n\t\t\tPlanBuildId:    build.Id,\n\t\t\tConvoMessageId: build.ConvoMessageId,\n\t\t\tPath:           filePath,\n\t\t\tContent:        activeBuild.FileContent,\n\t\t}\n\n\t\t// log.Println(\"build exec - new file result\")\n\t\t// spew.Dump(planRes)\n\t\tfileState.onFinishBuildFile(planRes)\n\t\treturn\n\t} else {\n\t\tcurrentNumTokens := shared.GetNumTokensEstimate(fileState.preBuildState)\n\n\t\tlog.Printf(\"Current state num tokens: %d\\n\", currentNumTokens)\n\n\t\tactiveBuild.CurrentFileTokens = currentNumTokens\n\t\tactivePlan.DidEditFiles = true\n\t}\n\n\t// build structured edits strategy now works regardless of language/tree-sitter support\n\tlog.Println(\"buildFile - building structured edits\")\n\tfileState.buildStructuredEdits()\n}\n\nfunc (fileState *activeBuildStreamFileState) resolvePreBuildState() {\n\tfilePath := fileState.filePath\n\tcurrentPlan := fileState.currentPlanState\n\tplanId := fileState.plan.Id\n\tbranch := fileState.branch\n\n\tactivePlan := GetActivePlan(planId, branch)\n\n\tif activePlan == nil {\n\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\tcontextPart := activePlan.ContextsByPath[filePath]\n\n\tvar currentState string\n\tcurrentPlanFile, fileInCurrentPlan := currentPlan.CurrentPlanFiles.Files[filePath]\n\n\t// log.Println(\"plan files:\")\n\t// spew.Dump(currentPlan.CurrentPlanFiles.Files)\n\n\tif fileInCurrentPlan {\n\t\tlog.Printf(\"File %s found in current plan.\\n\", filePath)\n\t\tfileState.isNewFile = false\n\t\tcurrentState = currentPlanFile\n\t\t// log.Println(\"\\n\\nCurrent state:\\n\", currentState, \"\\n\\n\")\n\n\t} else if contextPart != nil {\n\t\tlog.Printf(\"File %s found in model context. Using context state.\\n\", filePath)\n\t\tfileState.isNewFile = false\n\t\tcurrentState = contextPart.Body\n\t\t// log.Println(\"\\n\\nCurrent state:\\n\", currentState, \"\\n\\n\")\n\t} else {\n\t\tfileState.isNewFile = true\n\t}\n\n\tfileState.preBuildState = currentState\n\tfileState.contextPart = contextPart\n}\n"
  },
  {
    "path": "app/server/model/plan/build_finish.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc (state *activeBuildStreamFileState) onFinishBuild() {\n\tlog.Println(\"Build finished\")\n\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tcurrentOrgId := state.currentOrgId\n\tcurrentUserId := state.currentUserId\n\tconvoMessageId := state.convoMessageId\n\tbuild := state.build\n\n\t// first check if any of the messages we're building hasen't finished streaming yet\n\tstillStreaming := false\n\tvar doneCh chan bool\n\tap := GetActivePlan(planId, branch)\n\n\tif ap == nil {\n\t\tlog.Println(\"onFinishBuild - Active plan not found\")\n\t\treturn\n\t}\n\n\tif ap.CurrentStreamingReplyId == convoMessageId {\n\t\tstillStreaming = true\n\t\tdoneCh = ap.CurrentReplyDoneCh\n\t}\n\tif stillStreaming {\n\t\tlog.Println(\"Reply is still streaming, waiting for it to finish before finishing build\")\n\t\t<-doneCh\n\t}\n\n\t// Check again if build is finished\n\t// (more builds could have been queued while we were waiting for the reply to finish streaming)\n\tap = GetActivePlan(planId, branch)\n\n\tif ap == nil {\n\t\tlog.Println(\"onFinishBuild - Active plan not found\")\n\t\treturn\n\t}\n\n\tif !ap.BuildFinished() {\n\t\tlog.Println(\"Build not finished after waiting for reply to finish streaming\")\n\t\treturn\n\t}\n\n\tlog.Println(\"Locking repo for finished build\")\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:       currentOrgId,\n\t\tUserId:      currentUserId,\n\t\tPlanId:      planId,\n\t\tBranch:      branch,\n\t\tPlanBuildId: build.Id,\n\t\tScope:       db.LockScopeWrite,\n\t\tCtx:         ap.Ctx,\n\t\tCancelFn:    ap.CancelFn,\n\t\tReason:      \"finish build\",\n\t}, func(repo *db.GitRepo) error {\n\t\t// get plan descriptions\n\t\tvar planDescs []*db.ConvoMessageDescription\n\t\tplanDescs, err := db.GetConvoMessageDescriptions(currentOrgId, planId)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting pending build descriptions: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error getting pending build descriptions: %v\", err)\n\t\t}\n\n\t\tvar unbuiltDescs []*db.ConvoMessageDescription\n\t\tfor _, desc := range planDescs {\n\t\t\tif !desc.DidBuild || len(desc.BuildPathsInvalidated) > 0 {\n\t\t\t\tunbuiltDescs = append(unbuiltDescs, desc)\n\t\t\t}\n\t\t}\n\n\t\t// get fresh current plan state\n\t\tvar currentPlan *shared.CurrentPlanState\n\t\tcurrentPlan, err = db.GetCurrentPlanState(db.CurrentPlanStateParams{\n\t\t\tOrgId:                    currentOrgId,\n\t\t\tPlanId:                   planId,\n\t\t\tConvoMessageDescriptions: planDescs,\n\t\t})\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error getting current plan state: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error getting current plan state: %v\", err)\n\t\t}\n\n\t\tdescErrCh := make(chan error, len(unbuiltDescs))\n\t\tfor _, desc := range unbuiltDescs {\n\t\t\tif len(desc.Operations) > 0 {\n\t\t\t\tdesc.DidBuild = true\n\t\t\t\tdesc.BuildPathsInvalidated = map[string]bool{}\n\t\t\t}\n\n\t\t\tgo func(desc *db.ConvoMessageDescription) {\n\t\t\t\terr := db.StoreDescription(desc)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tdescErrCh <- fmt.Errorf(\"error storing description: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tdescErrCh <- nil\n\t\t\t}(desc)\n\t\t}\n\n\t\tfor range unbuiltDescs {\n\t\t\terr = <-descErrCh\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error storing description: %v\\n\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\terr = repo.GitAddAndCommit(branch, currentPlan.PendingChangesSummaryForBuild())\n\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"nothing to commit\") {\n\t\t\t\tlog.Println(\"Nothing to commit\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"error committing plan build: %v\", err)\n\t\t}\n\n\t\tlog.Println(\"Plan build committed\")\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error finishing build: %v\\n\", err)\n\n\t\tif err.Error() != context.Canceled.Error() {\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error finishing build: %v\", err))\n\n\t\t\tap.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"Error finishing build: \" + err.Error(),\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\tactive := GetActivePlan(planId, branch)\n\n\tif active != nil && (active.RepliesFinished || active.BuildOnly) {\n\t\tactive.Finish()\n\t}\n}\n\nfunc (fileState *activeBuildStreamFileState) onFinishBuildFile(planRes *db.PlanFileResult) {\n\tplanId := fileState.plan.Id\n\tbranch := fileState.branch\n\tcurrentOrgId := fileState.currentOrgId\n\tbuild := fileState.build\n\tactiveBuild := fileState.activeBuild\n\n\tactivePlan := GetActivePlan(planId, branch)\n\n\tif activePlan == nil {\n\t\tlog.Println(\"onFinishBuildFile - Active plan not found\")\n\t\treturn\n\t}\n\n\tfilePath := fileState.filePath\n\n\tlog.Printf(\"onFinishBuildFile: %s\\n\", filePath)\n\n\tif planRes == nil {\n\t\tlog.Println(\"onFinishBuildFile - planRes is nil\")\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"onFinishBuildFile: planRes is nil\"))\n\n\t\tactivePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Error storing plan result: planRes is nil\",\n\t\t}\n\t\treturn\n\t}\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:       currentOrgId,\n\t\tUserId:      fileState.currentUserId,\n\t\tPlanId:      planId,\n\t\tBranch:      branch,\n\t\tPlanBuildId: build.Id,\n\t\tScope:       db.LockScopeWrite,\n\t\tCtx:         activePlan.Ctx,\n\t\tCancelFn:    activePlan.CancelFn,\n\t\tReason:      \"store plan result\",\n\t}, func(repo *db.GitRepo) error {\n\t\tlog.Println(\"Storing plan result\", planRes.Path)\n\n\t\terr := db.StorePlanResult(planRes)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error storing plan result: %v\\n\", err)\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error storing plan build result: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error storing plan build result: %v\", err))\n\n\t\tactivePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Error storing plan build result: \" + err.Error(),\n\t\t}\n\t\treturn\n\t}\n\n\tfileState.builderRun.FinishedAt = time.Now()\n\thooks.ExecHook(hooks.DidFinishBuilderRun, hooks.HookParams{\n\t\tAuth:                      fileState.auth,\n\t\tPlan:                      fileState.plan,\n\t\tDidFinishBuilderRunParams: &fileState.builderRun,\n\t})\n\n\tlog.Printf(\"Finished building file %s - setting activeBuild.Success to true\\n\", filePath)\n\t// log.Println(spew.Sdump(activeBuild))\n\n\tfileState.onBuildProcessed(activeBuild)\n}\n\nfunc (fileState *activeBuildStreamFileState) onBuildProcessed(activeBuild *types.ActiveBuild) {\n\tfilePath := fileState.filePath\n\tplanId := fileState.plan.Id\n\tbranch := fileState.branch\n\n\tactiveBuild.Success = true\n\n\tstillBuildingPath := fileState.buildNextInQueue()\n\tif stillBuildingPath {\n\t\treturn\n\t}\n\n\tlog.Printf(\"No more builds for path %s, checking if entire build is finished\\n\", filePath)\n\n\tbuildFinished := false\n\n\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\tap.BuiltFiles[filePath] = true\n\t\tap.IsBuildingByPath[filePath] = false\n\t\tif ap.BuildFinished() {\n\t\t\tbuildFinished = true\n\t\t}\n\t})\n\n\tlog.Printf(\"Finished building file %s\\n\", filePath)\n\n\tif buildFinished {\n\t\tlog.Println(\"Finished building plan, calling onFinishBuild\")\n\t\tfileState.onFinishBuild()\n\t} else {\n\t\tlog.Println(\"Finished building file, but plan is not finished\")\n\t}\n}\n\nfunc (fileState *activeBuildStreamFileState) onBuildFileError(err error) {\n\tplanId := fileState.plan.Id\n\tbranch := fileState.branch\n\tfilePath := fileState.filePath\n\tbuild := fileState.build\n\tactiveBuild := fileState.activeBuild\n\n\tactivePlan := GetActivePlan(planId, branch)\n\n\tif activePlan == nil {\n\t\tlog.Println(\"onBuildFileError - Active plan not found\")\n\t\treturn\n\t}\n\n\tlog.Printf(\"Error for file %s: %v\\n\", filePath, err)\n\n\tactiveBuild.Success = false\n\tactiveBuild.Error = err\n\n\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error for file %s: %v\", filePath, err))\n\n\tactivePlan.StreamDoneCh <- &shared.ApiError{\n\t\tType:   shared.ApiErrorTypeOther,\n\t\tStatus: http.StatusInternalServerError,\n\t\tMsg:    err.Error(),\n\t}\n\n\tif err != nil {\n\t\tlog.Printf(\"Error storing plan error result: %v\\n\", err)\n\t}\n\n\tbuild.Error = err.Error()\n\n\terr = db.SetBuildError(build)\n\tif err != nil {\n\t\tlog.Printf(\"Error setting build error: %v\\n\", err)\n\t}\n}\n\nfunc (fileState *activeBuildStreamFileState) buildNextInQueue() bool {\n\tfilePath := fileState.filePath\n\tactivePlan := GetActivePlan(fileState.plan.Id, fileState.branch)\n\tif activePlan == nil {\n\t\tlog.Println(\"onFinishBuildFile - Active plan not found\")\n\t\treturn false\n\t}\n\n\t// if more builds are queued, start the next one\n\tif !activePlan.PathQueueEmpty(filePath) {\n\t\tlog.Printf(\"Processing next build for file %s\\n\", filePath)\n\t\tqueue := activePlan.BuildQueuesByPath[filePath]\n\t\tvar nextBuild *types.ActiveBuild\n\t\tfor _, build := range queue {\n\t\t\tif !build.BuildFinished() {\n\t\t\t\tnextBuild = build\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif nextBuild != nil {\n\t\t\tlog.Println(\"Calling execPlanBuild for next build in queue\")\n\t\t\tgo fileState.execPlanBuild(nextBuild)\n\t\t}\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "app/server/model/plan/build_load.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/syntax\"\n\t\"plandex-server/types\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc (state *activeBuildStreamState) loadPendingBuilds(sessionId string) (map[string][]*types.ActiveBuild, error) {\n\tclients := state.clients\n\tplan := state.plan\n\tbranch := state.branch\n\tauth := state.auth\n\n\tactive, err := activatePlan(clients, plan, branch, auth, \"\", true, false, sessionId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error activating plan: %v\\n\", err)\n\t}\n\n\tmodelStreamId := active.ModelStreamId\n\tstate.modelStreamId = modelStreamId\n\n\tvar modelContext []*db.Context\n\tvar pendingBuildsByPath map[string][]*types.ActiveBuild\n\tvar settings *shared.PlanSettings\n\tvar orgUserConfig *shared.OrgUserConfig\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   plan.Id,\n\t\tBranch:   branch,\n\t\tScope:    db.LockScopeRead,\n\t\tCtx:      active.Ctx,\n\t\tCancelFn: active.CancelFn,\n\t\tReason:   \"load pending builds\",\n\t}, func(repo *db.GitRepo) error {\n\t\terrCh := make(chan error, 4)\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan modelContext: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tres, err := db.GetPlanContexts(auth.OrgId, plan.Id, true, false)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan modelContext: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan modelContext: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmodelContext = res\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanSettings: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan settings: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tres, err := active.PendingBuildsByPath(auth.OrgId, auth.User.Id, nil)\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting pending builds by path: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting pending builds by path: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpendingBuildsByPath = res\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanSettings: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan settings: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tres, err := db.GetPlanSettings(plan)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan settings: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan settings: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsettings = res\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tgo func() {\n\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getOrgUserConfig: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting org user config: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tres, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting org user config: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting org user config: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\torgUserConfig = res\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tfor i := 0; i < 4; i++ {\n\t\t\terr = <-errCh\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan data: %v\\n\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting plan data: %v\", err)\n\t}\n\n\tUpdateActivePlan(plan.Id, branch, func(ap *types.ActivePlan) {\n\t\tap.Contexts = modelContext\n\t\tfor _, context := range modelContext {\n\t\t\tif context.FilePath != \"\" {\n\t\t\t\tap.ContextsByPath[context.FilePath] = context\n\t\t\t}\n\t\t}\n\t})\n\n\tstate.modelContext = modelContext\n\tstate.settings = settings\n\tstate.orgUserConfig = orgUserConfig\n\n\treturn pendingBuildsByPath, nil\n}\n\nfunc (state *activeBuildStreamFileState) loadBuildFile(activeBuild *types.ActiveBuild) error {\n\tcurrentOrgId := state.currentOrgId\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tfilePath := state.filePath\n\n\tactivePlan := GetActivePlan(planId, branch)\n\n\tif activePlan == nil {\n\t\treturn fmt.Errorf(\"active plan not found\")\n\t}\n\n\tconvoMessageId := activeBuild.ReplyId\n\n\tparser, lang, fallbackParser, fallbackLang := syntax.GetParserForPath(filePath)\n\n\tif parser != nil {\n\t\tvalidationRes, err := syntax.ValidateWithParsers(activePlan.Ctx, lang, parser, fallbackLang, fallbackParser, state.preBuildState)\n\t\tif err != nil {\n\t\t\tlog.Printf(\" error validating original file syntax: %v\\n\", err)\n\t\t\treturn fmt.Errorf(\"error validating original file syntax: %v\", err)\n\t\t}\n\n\t\tstate.language = validationRes.Lang\n\t\tstate.parser = validationRes.Parser\n\n\t\tstate.builderRun.Lang = string(validationRes.Lang)\n\n\t\tif validationRes.TimedOut {\n\t\t\tstate.syntaxCheckTimedOut = true\n\t\t} else if !validationRes.Valid {\n\t\t\tstate.preBuildStateSyntaxInvalid = true\n\t\t}\n\t}\n\n\tbuild := &db.PlanBuild{\n\t\tOrgId:          currentOrgId,\n\t\tPlanId:         planId,\n\t\tConvoMessageId: convoMessageId,\n\t\tFilePath:       filePath,\n\t}\n\terr := db.StorePlanBuild(build)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error storing plan build: %v\\n\", err)\n\t\tUpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {\n\t\t\tap.IsBuildingByPath[filePath] = false\n\t\t})\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error storing plan build: %v\", err))\n\n\t\tactivePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Error storing plan build: \" + err.Error(),\n\t\t}\n\t\treturn err\n\t}\n\n\tvar currentPlan *shared.CurrentPlanState\n\tvar convo []*db.ConvoMessage\n\n\tlog.Println(\"Locking repo for load build file\")\n\n\terr = db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:       currentOrgId,\n\t\tUserId:      state.activeBuildStreamState.currentUserId,\n\t\tPlanId:      planId,\n\t\tBranch:      branch,\n\t\tPlanBuildId: build.Id,\n\t\tScope:       db.LockScopeRead,\n\t\tCtx:         activePlan.Ctx,\n\t\tCancelFn:    activePlan.CancelFn,\n\t\tReason:      \"load build file\",\n\t}, func(repo *db.GitRepo) error {\n\t\terrCh := make(chan error, 2)\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getCurrentPlanState: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting current plan state: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tlog.Println(\"loadBuildFile - Getting current plan state\")\n\t\t\tres, err := db.GetCurrentPlanState(db.CurrentPlanStateParams{\n\t\t\t\tOrgId:  currentOrgId,\n\t\t\t\tPlanId: planId,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting current plan state: %v\\n\", err)\n\t\t\t\tUpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {\n\t\t\t\t\tap.IsBuildingByPath[filePath] = false\n\t\t\t\t})\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error getting current plan state: %v\", err))\n\n\t\t\t\tactivePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\t\tMsg:    \"Error getting current plan state: \" + err.Error(),\n\t\t\t\t}\n\t\t\t\terrCh <- fmt.Errorf(\"error getting current plan state: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcurrentPlan = res\n\n\t\t\tlog.Println(\"Got current plan state\")\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanConvo: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan convo: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tres, err := db.GetPlanConvo(currentOrgId, planId)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan convo: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan convo: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconvo = res\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tfor i := 0; i < 2; i++ {\n\t\t\terr = <-errCh\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan data: %v\\n\", err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error loading build file: %v\\n\", err)\n\t\tUpdateActivePlan(activePlan.Id, activePlan.Branch, func(ap *types.ActivePlan) {\n\t\t\tap.IsBuildingByPath[filePath] = false\n\t\t})\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error loading build file: %v\", err))\n\n\t\tactivePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Error loading build file: \" + err.Error(),\n\t\t}\n\t\treturn err\n\t}\n\n\tstate.filePath = filePath\n\tstate.convoMessageId = convoMessageId\n\tstate.build = build\n\tstate.currentPlanState = currentPlan\n\tstate.convo = convo\n\n\treturn nil\n\n}\n"
  },
  {
    "path": "app/server/model/plan/build_race.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/syntax\"\n\t\"plandex-server/utils\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype raceResult struct {\n\tcontent string\n\tvalid   bool\n}\n\ntype buildRaceParams struct {\n\tupdated         string\n\tproposedContent string\n\tdesc            string\n\treasons         []syntax.NeedsVerifyReason\n\tsyntaxErrors    []string\n\n\tdidCallFastApply bool\n\tfastApplyCh      chan string\n\n\tsessionId string\n}\n\nfunc (fileState *activeBuildStreamFileState) buildRace(\n\tbuildCtx context.Context,\n\tcancelBuild context.CancelFunc,\n\tparams buildRaceParams,\n) (raceResult, error) {\n\tlog.Printf(\"buildRace - starting race for file\")\n\tdefer func() {\n\t\tlog.Printf(\"buildRace - canceling build context\")\n\t\tcancelBuild()\n\t}()\n\n\toriginalFile := fileState.preBuildState\n\n\tupdated := params.updated\n\tproposedContent := params.proposedContent\n\tdesc := params.desc\n\treasons := params.reasons\n\tsyntaxErrors := params.syntaxErrors\n\tfastApplyCh := params.fastApplyCh\n\tsessionId := params.sessionId\n\tlog.Printf(\"buildRace - original file length: %d, updated length: %d\", len(originalFile), len(updated))\n\tlog.Printf(\"buildRace - has %d syntax errors and %d verify reasons\", len(syntaxErrors), len(reasons))\n\n\tmaxErrs := 3\n\n\tresCh := make(chan raceResult, 1)\n\terrCh := make(chan error, maxErrs)\n\n\tsendRes := func(res raceResult) {\n\t\tselect {\n\t\tcase resCh <- res:\n\t\tcase <-buildCtx.Done():\n\t\t\tlog.Printf(\"buildRace - context canceled, skipping sendRes\")\n\t\t}\n\t}\n\n\tsendErr := func(err error) {\n\t\tselect {\n\t\tcase errCh <- err:\n\t\tcase <-buildCtx.Done():\n\t\t\tlog.Printf(\"buildRace - context canceled, skipping sendErr\")\n\t\t}\n\t}\n\n\tstartedFallbacks := false\n\n\tstartWholeFileBuild := func(comments string) {\n\t\tlog.Printf(\"buildRace - starting whole file fallback build\")\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in startWholeFileBuild: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\tsendErr(fmt.Errorf(\"error starting whole file build: %v\", r))\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tselect {\n\t\t\tcase <-buildCtx.Done():\n\t\t\t\tlog.Printf(\"buildRace - context already canceled, skipping whole file build\")\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tcontent, err := fileState.buildWholeFileFallback(buildCtx, proposedContent, desc, comments, sessionId)\n\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\t\tlog.Printf(\"Context canceled during whole file build\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tlog.Printf(\"buildRace - whole file build failed: %v\", err)\n\t\t\t\tsendErr(fmt.Errorf(\"error building whole file: %w\", err))\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"buildRace - whole file build succeeded\")\n\t\t\t\tsendRes(raceResult{content: content, valid: true})\n\t\t\t}\n\t\t}()\n\t}\n\n\tmaybeStartFastApply := func(onFail func()) {\n\t\tlog.Printf(\"buildRace - starting fast apply\")\n\t\tif !params.didCallFastApply {\n\t\t\tlog.Printf(\"buildRace - fast apply isn't defined, skipping\")\n\t\t\tsendErr(nil) // no error, just no fast apply\n\t\t\tonFail()\n\t\t\treturn\n\t\t}\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in maybeStartFastApply: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\tsendErr(fmt.Errorf(\"error starting fast apply: %v\", r))\n\t\t\t\t\tonFail()\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\t\t\tvar fastApplyRes string\n\n\t\t\tselect {\n\t\t\tcase fastApplyRes = <-fastApplyCh:\n\t\t\tcase <-buildCtx.Done():\n\t\t\t\tlog.Printf(\"buildRace - context canceled, skipping fast apply\")\n\t\t\t\tsendErr(nil) // no error, just no fast apply\n\t\t\t\tonFail()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif fastApplyRes == \"\" {\n\t\t\t\tlog.Printf(\"buildRace - fast apply isn't defined or failed to run\")\n\t\t\t\tsendErr(nil) // no error, just no fast apply\n\t\t\t\tonFail()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// log.Printf(\"buildRace - fast apply result:\\n\\n%s\", fastApplyRes)\n\n\t\t\tfastApplySyntaxErrors := fileState.validateSyntax(buildCtx, fastApplyRes)\n\t\t\tfileState.builderRun.FastApplySyntaxErrors = fastApplySyntaxErrors\n\n\t\t\tif len(fastApplySyntaxErrors) > 0 {\n\t\t\t\tlog.Printf(\"buildRace - fast apply succeeded, but has %d syntax errors\", len(fastApplySyntaxErrors))\n\t\t\t\tsendErr(fmt.Errorf(\"fast apply succeeded, but has %d syntax errors\", len(fastApplySyntaxErrors)))\n\t\t\t\tonFail()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Printf(\"buildRace - fast apply returned, validating...\t\")\n\t\t\tvalidateResult, err := fileState.buildValidateLoop(buildCtx, buildValidateLoopParams{\n\t\t\t\toriginalFile:    originalFile,\n\t\t\t\tupdated:         fastApplyRes,\n\t\t\t\tproposedContent: proposedContent,\n\t\t\t\tdesc:            desc,\n\t\t\t\treasons:         reasons,\n\n\t\t\t\t// just validate since we're already building replacements in parallel\n\t\t\t\tmaxAttempts:                1,\n\t\t\t\tvalidateOnlyOnFinalAttempt: true,\n\t\t\t\tisInitial:                  false,\n\t\t\t\tsessionId:                  sessionId,\n\t\t\t})\n\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\t\tlog.Printf(\"Context canceled during fast apply validation\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tlog.Printf(\"buildRace - fast apply validation failed with error: %v\", err)\n\t\t\t\tsendErr(fmt.Errorf(\"fast apply validation failed: %w\", err))\n\t\t\t\tonFail()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif validateResult.valid {\n\t\t\t\tlog.Printf(\"buildRace - fast apply validation succeeded\")\n\t\t\t\tfileState.builderRun.FastApplySuccess = true\n\t\t\t\tsendRes(raceResult{content: validateResult.updated, valid: validateResult.valid})\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"buildRace - fast apply validation failed with problem: %s\", validateResult.problem)\n\t\t\t\tfileState.builderRun.FastApplyFailureResponse = validateResult.problem\n\t\t\t\tsendErr(fmt.Errorf(\"fast apply validation failed: %s\", validateResult.problem))\n\t\t\t\tonFail()\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\t}\n\n\tstartFallbacks := func(comments string) {\n\t\tstartedFallbacks = true\n\t\t// try fast apply + validation first if it's defined\n\t\t// if it's undefined or fails, start the whole file build fallback\n\t\tmaybeStartFastApply(func() {\n\t\t\tstartWholeFileBuild(comments)\n\t\t})\n\t}\n\n\t// If we get an incorrect marker, start the whole file build in the background while the validation/replacement loop continues\n\tonInitialStream := func(chunk string, buffer string) bool {\n\t\tif !startedFallbacks && strings.Contains(buffer, \"<PlandexIncorrect/>\") && strings.Contains(buffer, \"<PlandexComments>\") {\n\t\t\tlog.Printf(\"buildRace - detected incorrect marker, triggering whole file build\")\n\n\t\t\tcomments := utils.GetXMLContent(buffer, \"PlandexComments\")\n\n\t\t\tstartFallbacks(comments)\n\t\t}\n\t\t// keep streaming\n\t\treturn false\n\t}\n\n\tfileState.builderRun.AutoApplyValidationStartedAt = time.Now()\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in buildRace validation loop: %v\\n%s\", r, debug.Stack())\n\t\t\t\tsendErr(fmt.Errorf(\"error building validate loop: %v\", r))\n\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t}\n\t\t}()\n\n\t\tlog.Printf(\"buildRace - starting validation loop\")\n\t\tvalidateResult, err := fileState.buildValidateLoop(buildCtx, buildValidateLoopParams{\n\t\t\toriginalFile:         originalFile,\n\t\t\tupdated:              updated,\n\t\t\tproposedContent:      proposedContent,\n\t\t\tdesc:                 desc,\n\t\t\treasons:              reasons,\n\t\t\tsyntaxErrors:         syntaxErrors,\n\t\t\tinitialPhaseOnStream: onInitialStream,\n\t\t\tisInitial:            true,\n\t\t\tsessionId:            sessionId,\n\t\t})\n\n\t\tfileState.builderRun.AutoApplyValidationFinishedAt = time.Now()\n\n\t\tif err != nil {\n\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\tlog.Printf(\"Context canceled during buildValidate\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Printf(\"buildRace - validation loop failed: %v\", err)\n\t\t\tsendErr(fmt.Errorf(\"error building validate loop: %w\", err))\n\t\t} else {\n\t\t\tlog.Printf(\"buildRace - validation loop finished, valid: %v\", validateResult.valid)\n\t\t\tif validateResult.valid {\n\t\t\t\tlog.Printf(\"buildRace - validation loop succeeded, valid: %v\", validateResult.valid)\n\t\t\t\tsendRes(raceResult{content: validateResult.updated, valid: validateResult.valid})\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"buildRace - validation loop failed, valid: %v\", validateResult.valid)\n\t\t\t\tsendErr(fmt.Errorf(\"validation loop failed: %s\", validateResult.problem))\n\t\t\t}\n\t\t}\n\t}()\n\n\terrs := []error{}\n\terrChNumReceived := 0\n\n\tfor {\n\t\tselect {\n\t\tcase <-buildCtx.Done():\n\t\t\tlog.Printf(\"buildRace - context canceled\")\n\t\t\treturn raceResult{}, buildCtx.Err()\n\t\tcase err := <-errCh:\n\t\t\terrChNumReceived++\n\t\t\tlog.Printf(\"buildRace - error channel received %d: %v\\n\", errChNumReceived, err)\n\n\t\t\tif err != nil {\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\n\t\t\tif errChNumReceived >= maxErrs {\n\t\t\t\tlog.Printf(\"buildRace - all attempts failed with %d errors\", len(errs))\n\t\t\t\treturn raceResult{}, fmt.Errorf(\"all build attempts failed: %v\", errs)\n\t\t\t}\n\n\t\t\tif !startedFallbacks {\n\t\t\t\tlog.Printf(\"buildRace - starting build fallbacks\")\n\t\t\t\tstartFallbacks(\"\") // since replacements failed, pass an empty string for comments -- this causes whole file build to classify comments first\n\t\t\t}\n\t\tcase res := <-resCh:\n\t\t\tlog.Printf(\"buildRace - got successful result\")\n\t\t\treturn res, nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/server/model/plan/build_state.go",
    "content": "package plan\n\nimport (\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/model\"\n\t\"plandex-server/types\"\n\n\tshared \"plandex-shared\"\n\n\tsitter \"github.com/smacker/go-tree-sitter\"\n)\n\nconst MaxBuildErrorRetries = 3 // uses semi-exponential backoff so be careful with this\n\ntype activeBuildStreamState struct {\n\tmodelStreamId string\n\tclients       map[string]model.ClientInfo\n\tauthVars      map[string]string\n\tauth          *types.ServerAuth\n\tcurrentOrgId  string\n\tcurrentUserId string\n\torgUserConfig *shared.OrgUserConfig\n\tplan          *db.Plan\n\tbranch        string\n\tsettings      *shared.PlanSettings\n\tmodelContext  []*db.Context\n\tconvo         []*db.ConvoMessage\n}\n\ntype activeBuildStreamFileState struct {\n\t*activeBuildStreamState\n\tfilePath                   string\n\tconvoMessageId             string\n\tbuild                      *db.PlanBuild\n\tcurrentPlanState           *shared.CurrentPlanState\n\tactiveBuild                *types.ActiveBuild\n\tpreBuildState              string\n\tparser                     *sitter.Parser\n\tlanguage                   shared.Language\n\tsyntaxCheckTimedOut        bool\n\tpreBuildStateSyntaxInvalid bool\n\tvalidationNumRetry         int\n\twholeFileNumRetry          int\n\tisNewFile                  bool\n\tcontextPart                *db.Context\n\n\tbuilderRun hooks.DidFinishBuilderRunParams\n}\n"
  },
  {
    "path": "app/server/model/plan/build_structured_edits.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/db\"\n\tdiff_pkg \"plandex-server/diff\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/syntax\"\n\t\"plandex-server/utils\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc (fileState *activeBuildStreamFileState) buildStructuredEdits() {\n\tfilePath := fileState.filePath\n\tactiveBuild := fileState.activeBuild\n\tplanId := fileState.plan.Id\n\tbranch := fileState.branch\n\toriginalFile := fileState.preBuildState\n\tparser := fileState.parser\n\n\tif parser == nil {\n\t\tlog.Printf(\"buildStructuredEdits - tree-sitter parser is nil for file %s\\n\", filePath)\n\t}\n\n\tactivePlan := GetActivePlan(planId, branch)\n\tif activePlan == nil {\n\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\\n\", planId, branch)\n\t\tfileState.onBuildFileError(fmt.Errorf(\"active plan not found for plan ID %s and branch %s\", planId, branch))\n\t\treturn\n\t}\n\n\tbuildCtx, cancelBuild := context.WithCancel(activePlan.Ctx)\n\n\tproposedContent := activeBuild.FileContent\n\tdesc := activeBuild.FileDescription\n\n\tdescLower := strings.ToLower(desc)\n\tisReplaceOrRemove := strings.Contains(descLower, \"type: replace\") || strings.Contains(descLower, \"type: remove\") || strings.Contains(descLower, \"type: overwrite\")\n\n\tvar autoApplyRes *syntax.ApplyChangesResult\n\tvar autoApplySyntaxErrors []string\n\n\tcalledFastApply := false\n\tvar fastApplyRes string\n\tfastApplyCh := make(chan string, 1)\n\n\tcallFastApply := func() {\n\t\tlog.Printf(\"buildStructuredEdits - %s - calling fast apply hook\\n\", filePath)\n\t\tfileState.builderRun.DidFastApply = true\n\t\tfileState.builderRun.FastApplyStartedAt = time.Now()\n\t\tcalledFastApply = true\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in callFastApply: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\tfastApplyCh <- \"\"\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tres, err := hooks.ExecHook(hooks.CallFastApply, hooks.HookParams{\n\t\t\t\tFastApplyParams: &hooks.FastApplyParams{\n\t\t\t\t\tInitialCode: originalFile,\n\t\t\t\t\tEditSnippet: proposedContent,\n\t\t\t\t\tLanguage:    fileState.language,\n\t\t\t\t\tCtx:         buildCtx,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"buildStructuredEdits - error executing fast apply hook: %v\\n\", err)\n\t\t\t\t// empty string acts as a no-op\n\t\t\t\tfastApplyCh <- \"\"\n\t\t\t\treturn\n\t\t\t} else if res.FastApplyResult == nil {\n\t\t\t\tlog.Printf(\"buildStructuredEdits - fast apply hook returned nil result\\n\")\n\t\t\t\t// empty string acts as a no-op\n\t\t\t\tfastApplyCh <- \"\"\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfastApplyRes = res.FastApplyResult.MergedCode\n\t\t\tlog.Printf(\"buildStructuredEdits - %s - got fast apply hook result\\n\", filePath)\n\t\t\t// fmt.Printf(\"buildStructuredEdits - fastApplyRes:\\n%s\", fastApplyRes)\n\n\t\t\tfileState.builderRun.FastApplyFinishedAt = time.Now()\n\n\t\t\tfastApplyCh <- fastApplyRes\n\t\t}()\n\t}\n\n\tif isReplaceOrRemove {\n\t\tcallFastApply()\n\t}\n\n\tlog.Printf(\"buildStructuredEdits - %s - applying changes\\n\", filePath)\n\t// Apply plan logic\n\tlog.Printf(\"buildStructuredEdits - %s - calling ApplyChanges\\n\", filePath)\n\tautoApplyRes = syntax.ApplyChanges(\n\t\tbuildCtx,\n\t\tsyntax.ApplyChangesParams{\n\t\t\tOriginal:               originalFile,\n\t\t\tProposed:               proposedContent,\n\t\t\tDesc:                   desc,\n\t\t\tAddMissingStartEndRefs: true,\n\t\t\tParser:                 fileState.parser,\n\t\t\tLanguage:               fileState.language,\n\t\t},\n\t)\n\tlog.Printf(\"buildStructuredEdits - %s - got ApplyChanges result\\n\", filePath)\n\t// log.Printf(\"buildStructuredEdits - autoApplyRes.NewFile:\\n\\n%s\", autoApplyRes.NewFile)\n\tlog.Println(\"buildStructuredEdits - autoApplyRes.NeedsVerifyReasons:\", autoApplyRes.NeedsVerifyReasons)\n\n\tautoApplySyntaxErrors = fileState.validateSyntax(buildCtx, autoApplyRes.NewFile)\n\n\thasNeedsVerifyReasons := len(autoApplyRes.NeedsVerifyReasons) > 0\n\n\tautoApplyHasSyntaxErrors := len(autoApplySyntaxErrors) > 0\n\tautoApplyIsValid := !autoApplyHasSyntaxErrors && !hasNeedsVerifyReasons\n\n\tif !autoApplyIsValid && !calledFastApply {\n\t\tcallFastApply()\n\t}\n\n\tlog.Printf(\"buildStructuredEdits - %s - autoApplyHasSyntaxErrors: %t, hasNeedsVerifyReasons: %t, autoApplyIsValid: %t\\n\",\n\t\tfilePath, autoApplyHasSyntaxErrors, hasNeedsVerifyReasons, autoApplyIsValid)\n\n\tupdated := autoApplyRes.NewFile\n\n\t// If no problems, we trust the direct ApplyChanges result\n\tif autoApplyIsValid {\n\t\tlog.Printf(\"buildStructuredEdits - %s - changes are valid, using ApplyChanges result\\n\", filePath)\n\t\tfileState.builderRun.AutoApplySuccess = true\n\t} else {\n\t\tlog.Printf(\"buildStructuredEdits - %s - auto apply has syntax errors or NeedsVerifyReasons\", filePath)\n\t\tfileState.builderRun.AutoApplyValidationReasons = make([]string, len(autoApplyRes.NeedsVerifyReasons))\n\t\tfor i, reason := range autoApplyRes.NeedsVerifyReasons {\n\t\t\tfileState.builderRun.AutoApplyValidationReasons[i] = string(reason)\n\t\t}\n\n\t\tfileState.builderRun.AutoApplyValidationSyntaxErrors = autoApplySyntaxErrors\n\n\t\tbuildRaceParams := buildRaceParams{\n\t\t\tupdated:         updated,\n\t\t\tproposedContent: proposedContent,\n\t\t\tdesc:            desc,\n\t\t\treasons:         autoApplyRes.NeedsVerifyReasons,\n\t\t\tsyntaxErrors:    autoApplySyntaxErrors,\n\n\t\t\tdidCallFastApply: calledFastApply,\n\t\t\tfastApplyCh:      fastApplyCh,\n\n\t\t\tsessionId: activePlan.SessionId,\n\t\t}\n\n\t\tbuildRaceResult, err := fileState.buildRace(buildCtx, cancelBuild, buildRaceParams)\n\t\tif err != nil {\n\t\t\tif apiErr, ok := err.(*shared.ApiError); ok {\n\t\t\t\tactivePlan.StreamDoneCh <- apiErr\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"buildStructuredEdits - %s - error building race: %v\\n\", filePath, err)\n\t\t\t\tfileState.onBuildFileError(fmt.Errorf(\"error building race: %v\", err))\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tupdated = buildRaceResult.content\n\t}\n\n\t// output diff and store build results\n\tbuildInfo := &shared.BuildInfo{\n\t\tPath:      filePath,\n\t\tNumTokens: 0,\n\t\tFinished:  true,\n\t}\n\tlog.Printf(\"streaming build info for finished file %s\\n\", filePath)\n\tactivePlan.Stream(shared.StreamMessage{\n\t\tType:      shared.StreamMessageBuildInfo,\n\t\tBuildInfo: buildInfo,\n\t})\n\ttime.Sleep(50 * time.Millisecond)\n\n\t// strip any blank lines from beginning/end of updated file\n\tupdated = utils.StripAddedBlankLines(originalFile, updated)\n\n\tlog.Printf(\"buildStructuredEdits - %s - getting diff replacements\\n\", filePath)\n\treplacements, err := diff_pkg.GetDiffReplacements(originalFile, updated)\n\tif err != nil {\n\t\tlog.Printf(\"buildStructuredEdits - error getting diff replacements: %v\\n\", err)\n\t\tfileState.onBuildFileError(fmt.Errorf(\"error getting diff replacements: %v\", err))\n\t\treturn\n\t}\n\tlog.Printf(\"buildStructuredEdits - %s - got %d replacements\\n\", filePath, len(replacements))\n\n\tfor _, replacement := range replacements {\n\t\treplacement.Summary = strings.TrimSpace(desc)\n\t}\n\n\tres := db.PlanFileResult{\n\t\tTypeVersion:    1,\n\t\tOrgId:          fileState.plan.OrgId,\n\t\tPlanId:         fileState.plan.Id,\n\t\tPlanBuildId:    fileState.build.Id,\n\t\tConvoMessageId: fileState.convoMessageId,\n\t\tContent:        \"\",\n\t\tPath:           filePath,\n\t\tReplacements:   replacements,\n\t}\n\n\tlog.Printf(\"buildStructuredEdits - %s - finishing build file\\n\", filePath)\n\tfileState.onFinishBuildFile(&res)\n}\n\nfunc (fileState *activeBuildStreamFileState) validateSyntax(buildCtx context.Context, updated string) []string {\n\tif fileState.parser != nil && !fileState.preBuildStateSyntaxInvalid && !fileState.syntaxCheckTimedOut {\n\t\tvalidationRes, 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\n\t\tif err != nil {\n\t\t\tlog.Printf(\"buildStructuredEdits - error validating updated file: %v\\n\", err)\n\t\t} else if validationRes.TimedOut {\n\t\t\tlog.Printf(\"buildStructuredEdits - syntax check timed out for updated file\\n\")\n\t\t\tfileState.syntaxCheckTimedOut = true\n\t\t\treturn nil\n\t\t} else {\n\t\t\treturn validationRes.Errors\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/model/plan/build_validate_and_fix.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\tdiff_pkg \"plandex-server/diff\"\n\t\"plandex-server/model\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/syntax\"\n\t\"plandex-server/types\"\n\t\"plandex-server/utils\"\n\tshared \"plandex-shared\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nconst MaxValidationFixAttempts = 3\n\ntype buildValidateLoopParams struct {\n\toriginalFile               string\n\tupdated                    string\n\tproposedContent            string\n\tdesc                       string\n\tsyntaxErrors               []string\n\treasons                    []syntax.NeedsVerifyReason\n\tinitialPhaseOnStream       func(chunk string, buffer string) bool\n\tvalidateOnlyOnFinalAttempt bool\n\tmaxAttempts                int\n\tisInitial                  bool\n\tsessionId                  string\n}\n\ntype buildValidateLoopResult struct {\n\tvalid   bool\n\tupdated string\n\tproblem string\n}\n\nfunc (fileState *activeBuildStreamFileState) buildValidateLoop(\n\tctx context.Context,\n\tparams buildValidateLoopParams,\n) (buildValidateLoopResult, error) {\n\tlog.Printf(\"Starting buildValidateLoop for file: %s\", fileState.filePath)\n\n\toriginalFile := params.originalFile\n\tupdated := params.updated\n\tproposedContent := params.proposedContent\n\tdesc := params.desc\n\n\tsyntaxErrors := params.syntaxErrors\n\tnumAttempts := 0\n\n\tproblems := []string{}\n\n\tmaxAttempts := MaxValidationFixAttempts\n\tif params.maxAttempts > 0 {\n\t\tmaxAttempts = params.maxAttempts\n\t}\n\n\tfor numAttempts < maxAttempts {\n\t\tcurrentAttempt := numAttempts + 1\n\t\tlog.Printf(\"Starting validation attempt %d/%d\", currentAttempt, MaxValidationFixAttempts)\n\n\t\t// check for context cancellation\n\t\tif ctx.Err() != nil {\n\t\t\tlog.Printf(\"Context cancelled during attempt %d\", currentAttempt)\n\t\t\treturn buildValidateLoopResult{}, ctx.Err()\n\t\t}\n\n\t\t// reset retry count for each phase\n\t\tfileState.validationNumRetry = 0\n\t\tlog.Printf(\"Reset validation retry count for attempt %d\", currentAttempt)\n\n\t\tvar onStream func(chunk string, buffer string) bool\n\t\tif numAttempts == 0 {\n\t\t\tonStream = params.initialPhaseOnStream\n\t\t\tlog.Printf(\"Using initial phase onStream handler\")\n\t\t} else {\n\t\t\tonStream = nil\n\t\t\tlog.Printf(\"No onStream handler for attempt %d\", currentAttempt)\n\t\t}\n\n\t\tvar reasons []syntax.NeedsVerifyReason\n\t\tif numAttempts == 0 {\n\t\t\treasons = params.reasons\n\t\t\tlog.Printf(\"Using initial reasons for validation\")\n\t\t} else {\n\t\t\treasons = []syntax.NeedsVerifyReason{}\n\t\t\tlog.Printf(\"Using empty reasons list for attempt %d\", currentAttempt)\n\t\t}\n\n\t\tmodelConfig := fileState.settings.GetModelPack().Builder\n\t\t// if available, switch to stronger model after the first attempt failed\n\t\tif currentAttempt > 2 && modelConfig.StrongModel != nil {\n\t\t\tlog.Printf(\"Switching to strong model for attempt %d\", currentAttempt)\n\t\t\tmodelConfig = *modelConfig.StrongModel\n\t\t}\n\n\t\tisLastAttempt := numAttempts == maxAttempts-1\n\n\t\t// build validate params\n\t\tvalidateParams := buildValidateParams{\n\t\t\toriginalFile:    originalFile,\n\t\t\tupdated:         updated,\n\t\t\tproposedContent: proposedContent,\n\t\t\tdesc:            desc,\n\t\t\tonStream:        onStream,\n\t\t\tsyntaxErrors:    syntaxErrors,\n\t\t\treasons:         reasons,\n\t\t\tmodelConfig:     &modelConfig,\n\t\t\tvalidateOnly:    isLastAttempt && params.validateOnlyOnFinalAttempt,\n\t\t\tphase:           currentAttempt,\n\t\t\tisInitial:       params.isInitial,\n\t\t\tsessionId:       params.sessionId,\n\t\t}\n\n\t\tlog.Printf(\"Calling buildValidate for attempt %d\", currentAttempt)\n\t\tres, err := fileState.buildValidate(ctx, validateParams)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\tlog.Printf(\"Context canceled during buildValidate\")\n\t\t\t\treturn buildValidateLoopResult{}, err\n\t\t\t}\n\n\t\t\tlog.Printf(\"Error in buildValidate during attempt %d: %v\", currentAttempt, err)\n\t\t\treturn buildValidateLoopResult{}, fmt.Errorf(\"error building validate: %v\", err)\n\t\t}\n\t\tupdated = res.updated\n\n\t\tsyntaxErrors = fileState.validateSyntax(ctx, updated)\n\t\tlog.Printf(\"Found %d syntax errors after attempt %d\", len(syntaxErrors), currentAttempt)\n\n\t\tif res.valid && len(syntaxErrors) == 0 {\n\t\t\tlog.Printf(\"Validation succeeded in attempt %d\", currentAttempt)\n\t\t\treturn buildValidateLoopResult{\n\t\t\t\tvalid:   res.valid,\n\t\t\t\tupdated: res.updated,\n\t\t\t}, nil\n\t\t}\n\n\t\tproblems = append(problems, res.problem)\n\n\t\tlog.Printf(\"Validation failed in attempt %d, preparing for next attempt\", currentAttempt)\n\n\t\tnumAttempts++\n\t}\n\n\tlog.Printf(\"Validation failed after %d attempts\", MaxValidationFixAttempts)\n\treturn buildValidateLoopResult{\n\t\tvalid:   false,\n\t\tupdated: updated,\n\t\tproblem: strings.Join(problems, \"\\n\\n\"),\n\t}, nil\n}\n\ntype buildValidateParams struct {\n\toriginalFile    string\n\tupdated         string\n\tproposedContent string\n\tdesc            string\n\tsyntaxErrors    []string\n\treasons         []syntax.NeedsVerifyReason\n\tonStream        func(chunk string, buffer string) bool\n\tphase           int\n\tmodelConfig     *shared.ModelRoleConfig\n\tvalidateOnly    bool\n\tisInitial       bool\n\tsessionId       string\n}\n\ntype buildValidateResult struct {\n\tvalid   bool\n\tupdated string\n\tproblem string\n}\n\nfunc (fileState *activeBuildStreamFileState) buildValidate(\n\tctx context.Context,\n\tparams buildValidateParams,\n) (buildValidateResult, error) {\n\tlog.Printf(\"Starting buildValidate for phase %d\", params.phase)\n\n\tauth := fileState.auth\n\tfilePath := fileState.filePath\n\tclients := fileState.clients\n\tauthVars := fileState.authVars\n\tmodelConfig := params.modelConfig\n\n\toriginalFile := params.originalFile\n\tupdated := params.updated\n\tproposedContent := params.proposedContent\n\tdesc := params.desc\n\tonStream := params.onStream\n\tsyntaxErrors := params.syntaxErrors\n\treasons := params.reasons\n\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(authVars, fileState.settings, fileState.orgUserConfig)\n\n\t// Get diff for validation\n\tlog.Printf(\"Getting diffs between original and updated content\")\n\tdiff, err := diff_pkg.GetDiffs(originalFile, updated)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting diffs: %v\", err)\n\t\treturn buildValidateResult{}, fmt.Errorf(\"error getting diffs: %v\", err)\n\t}\n\n\toriginalWithLineNums := shared.AddLineNums(originalFile)\n\tproposedWithLineNums := shared.AddLineNums(proposedContent)\n\n\tmaxExpectedOutputTokens := shared.GetNumTokensEstimate(originalFile)/2 + shared.GetNumTokensEstimate(proposedContent)\n\n\t// Choose prompt and tools based on preferred format\n\n\tlog.Printf(\"Building XML validation replacements prompt\")\n\tpromptText, headNumTokens := prompts.GetValidationReplacementsXmlPrompt(prompts.ValidationPromptParams{\n\t\tPath:                 filePath,\n\t\tOriginalWithLineNums: originalWithLineNums,\n\t\tDesc:                 desc,\n\t\tProposedWithLineNums: proposedWithLineNums,\n\t\tDiff:                 diff,\n\t\tSyntaxErrors:         syntaxErrors,\n\t\tReasons:              reasons,\n\t})\n\n\t// log.Printf(\"Prompt to LLM: %s\", promptText)\n\n\tlog.Printf(\"Creating initial messages for phase 1\")\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: promptText,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\treqStarted := time.Now()\n\tfileState.builderRun.ReplacementStartedAt = reqStarted\n\n\tif params.validateOnly {\n\t\tlog.Printf(\"Making validation-only model request\")\n\t} else {\n\t\tlog.Printf(\"Making validation-replacements model request\")\n\t}\n\t// log.Printf(\"Messages: %v\", messages)\n\n\tstop := []string{\"<PlandexFinish/>\"}\n\tif params.validateOnly {\n\t\tstop = []string{\"<PlandexComments>\", \"<PlandexReplacements>\"}\n\t}\n\n\tvar willCacheNumTokens int\n\tisFirstPass := params.isInitial && params.phase == 1\n\tif !isFirstPass && baseModelConfig.Provider == shared.ModelProviderOpenAI {\n\t\twillCacheNumTokens = headNumTokens\n\t}\n\n\tlog.Printf(\"buildValidate - calling model.ModelRequest\")\n\t// spew.Dump(messages)\n\n\t// Use ModelRequest for both formats\n\tres, err := model.ModelRequest(ctx, model.ModelRequestParams{\n\t\tClients:        clients,\n\t\tAuth:           auth,\n\t\tAuthVars:       authVars,\n\t\tPlan:           fileState.plan,\n\t\tModelConfig:    modelConfig,\n\t\tPurpose:        \"File edit\",\n\t\tMessages:       messages,\n\t\tModelStreamId:  fileState.modelStreamId,\n\t\tConvoMessageId: fileState.convoMessageId,\n\t\tBuildId:        fileState.build.Id,\n\t\tModelPackName:  fileState.settings.GetModelPack().Name,\n\t\tStop:           stop,\n\t\tBeforeReq: func() {\n\t\t\tlog.Printf(\"Starting model request\")\n\t\t\tfileState.builderRun.ReplacementStartedAt = time.Now()\n\t\t},\n\t\tAfterReq: func() {\n\t\t\tlog.Printf(\"Finished model request\")\n\t\t\tfileState.builderRun.ReplacementFinishedAt = time.Now()\n\t\t},\n\t\tOnStream: onStream,\n\n\t\tWillCacheNumTokens:    willCacheNumTokens,\n\t\tSessionId:             params.sessionId,\n\t\tEstimatedOutputTokens: maxExpectedOutputTokens,\n\t\tSettings:              fileState.settings,\n\t\tOrgUserConfig:         fileState.orgUserConfig,\n\t})\n\n\tif err != nil {\n\t\tif errors.Is(err, context.Canceled) {\n\t\t\tlog.Printf(\"Context canceled during model request\")\n\t\t\treturn buildValidateResult{}, err\n\t\t}\n\n\t\tlog.Printf(\"Error calling model: %v\", err)\n\t\treturn fileState.validationRetryOrError(ctx, params, err)\n\t}\n\n\t// log.Printf(\"Model response:\\n\\n%s\", res.Content)\n\n\tfileState.builderRun.GenerationIds = append(fileState.builderRun.GenerationIds, res.GenerationId)\n\tlog.Printf(\"Added generation ID: %s\", res.GenerationId)\n\n\t// Handle response based on format\n\tparseRes, err := handleXMLResponse(fileState, res.Content, originalWithLineNums, updated, params.validateOnly)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error handling response: %v\", err)\n\t\treturn fileState.validationRetryOrError(ctx, params, err)\n\t}\n\n\tlog.Printf(\"Validation result: valid=%v\", parseRes.valid)\n\n\treturn parseRes, nil\n}\n\nfunc handleXMLResponse(\n\tfileState *activeBuildStreamFileState,\n\tcontent string,\n\toriginalWithLineNums shared.LineNumberedTextType,\n\tupdated string,\n\tvalidateOnly bool,\n) (buildValidateResult, error) {\n\tlog.Printf(\"Handling XML response for file: %s\", fileState.filePath)\n\n\tif strings.Contains(content, \"<PlandexCorrect/>\") {\n\t\tlog.Printf(\"XML response indicates changes are correct\")\n\t\tfileState.builderRun.ReplacementSuccess = true\n\t\treturn buildValidateResult{\n\t\t\tvalid:   true,\n\t\t\tupdated: updated,\n\t\t}, nil\n\t}\n\n\tif validateOnly {\n\t\tlog.Printf(\"Validation-only mode, skipping replacements\")\n\t\treturn buildValidateResult{\n\t\t\tvalid:   false,\n\t\t\tupdated: updated,\n\t\t}, nil\n\t}\n\n\toriginalFileLines := strings.Split(string(originalWithLineNums), \"\\n\")\n\n\tincremental := originalWithLineNums\n\n\tlog.Printf(\"Processing XML replacement blocks\")\n\n\treplacementsOuter := utils.GetXMLContent(content, \"PlandexReplacements\")\n\n\tif replacementsOuter == \"\" {\n\t\tlog.Printf(\"No replacements found in XML response\")\n\t\treturn buildValidateResult{\n\t\t\tvalid:   false,\n\t\t\tupdated: shared.RemoveLineNums(incremental),\n\t\t\tproblem: \"No replacements found in XML response\",\n\t\t}, nil\n\t}\n\n\treplacements := utils.GetAllXMLContent(replacementsOuter, \"Replacement\")\n\n\tfor i, replacement := range replacements {\n\t\tlog.Printf(\"Processing replacement: %d/%d\", i+1, len(replacements))\n\n\t\told := utils.GetXMLContent(replacement, \"Old\")\n\t\tnew := utils.GetXMLContent(replacement, \"New\")\n\n\t\tif old == \"\" {\n\t\t\tlog.Printf(\"No old content found for replacement\")\n\t\t\treturn buildValidateResult{valid: false, updated: updated}, fmt.Errorf(\"no old content found for replacement\")\n\t\t}\n\n\t\told = strings.TrimSpace(old)\n\n\t\t// log.Printf(\"Old content trimmed:\\n\\n%s\", strconv.Quote(old))\n\n\t\t// log.Printf(\"New content:\\n\\n%s\", strconv.Quote(new))\n\n\t\tif !strings.HasPrefix(old, \"pdx-\") {\n\t\t\tlog.Printf(\"Old content does not have a line number prefix for first line\")\n\t\t\treturn buildValidateResult{valid: false, updated: updated}, fmt.Errorf(\"old content does not have a line number prefix for first line\")\n\t\t}\n\n\t\toldLines := strings.Split(old, \"\\n\")\n\n\t\tvar lastLine string\n\t\tvar lastLineNum int\n\t\tfirstLine := oldLines[0]\n\t\tif len(oldLines) > 1 {\n\t\t\tlastLine = oldLines[len(oldLines)-1]\n\t\t}\n\n\t\tfirstLineNum, err := shared.ExtractLineNumberWithPrefix(firstLine, \"pdx-\")\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error extracting line number from first line: %v\", err)\n\t\t\treturn buildValidateResult{valid: false, updated: updated}, fmt.Errorf(\"error extracting line number from first line: %v\", err)\n\t\t}\n\n\t\tif lastLine != \"\" {\n\t\t\tlastLineNum, err = shared.ExtractLineNumberWithPrefix(lastLine, \"pdx-\")\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error extracting line number from last line: %v\", err)\n\t\t\t\treturn buildValidateResult{valid: false, updated: updated}, fmt.Errorf(\"error extracting line number from last line: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\tif lastLineNum == 0 {\n\t\t\tif !(firstLineNum > 0 && firstLineNum <= len(originalFileLines)) {\n\t\t\t\tlog.Printf(\"Invalid line number for first line: %d\", firstLineNum)\n\t\t\t\treturn buildValidateResult{valid: false, updated: updated}, fmt.Errorf(\"invalid line number for first line: %d\", firstLineNum)\n\t\t\t}\n\t\t\told = originalFileLines[firstLineNum-1]\n\t\t} else {\n\t\t\tif !(firstLineNum > 0 && firstLineNum <= len(originalFileLines) && lastLineNum > firstLineNum && lastLineNum <= len(originalFileLines)) {\n\t\t\t\tlog.Printf(\"Invalid line numbers for first and last lines: %d-%d\", firstLineNum, lastLineNum)\n\t\t\t\treturn buildValidateResult{valid: false, updated: updated}, fmt.Errorf(\"invalid line numbers: %d-%d\", firstLineNum, lastLineNum)\n\t\t\t}\n\t\t\told = strings.Join(originalFileLines[firstLineNum-1:lastLineNum], \"\\n\")\n\t\t}\n\n\t\t// log.Printf(\"Applying replacement.\\n\\nOld:\\n\\n%s\\n\\nNew:\\n\\n%s\", old, new)\n\n\t\tincremental = shared.LineNumberedTextType(strings.Replace(string(incremental), old, new, 1))\n\n\t\t// log.Printf(\"Updated content:\\n\\n%s\", string(incremental))\n\t}\n\n\tvar problem string\n\n\tif strings.Contains(content, \"<PlandexIncorrect/>\") {\n\t\tsplit := strings.Split(content, \"<PlandexIncorrect/>\")\n\t\tproblem = split[0]\n\t} else if strings.Contains(content, \"<PlandexReplacements>\") {\n\t\tsplit := strings.Split(content, \"<PlandexReplacements>\")\n\t\tproblem = split[0]\n\t}\n\n\tfinal := shared.RemoveLineNums(incremental)\n\n\t// log.Printf(\"Final content:\\n\\n%s\", final)\n\n\treturn buildValidateResult{valid: false, updated: final, problem: problem}, nil\n}\n\nfunc (fileState *activeBuildStreamFileState) validationRetryOrError(buildCtx context.Context, validateParams buildValidateParams, err error) (buildValidateResult, error) {\n\tlog.Printf(\"Handling validation error for file: %s\", fileState.filePath)\n\tif fileState.validationNumRetry < MaxBuildErrorRetries {\n\t\tfileState.validationNumRetry++\n\n\t\tlog.Printf(\"Retrying validation (attempt %d/%d) due to error: %v\",\n\t\t\tfileState.validationNumRetry, MaxBuildErrorRetries, err)\n\n\t\tactivePlan := GetActivePlan(fileState.plan.Id, fileState.branch)\n\n\t\tif activePlan == nil {\n\t\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\",\n\t\t\t\tfileState.plan.Id, fileState.branch)\n\t\t\treturn buildValidateResult{}, fmt.Errorf(\"active plan not found for plan ID %s and branch %s\",\n\t\t\t\tfileState.plan.Id, fileState.branch)\n\t\t}\n\n\t\tselect {\n\t\tcase <-buildCtx.Done():\n\t\t\tlog.Printf(\"Context canceled during retry wait\")\n\t\t\treturn buildValidateResult{}, context.Canceled\n\t\tcase <-time.After(time.Duration(fileState.validationNumRetry*fileState.validationNumRetry)*200*time.Millisecond + time.Duration(rand.Intn(500))*time.Millisecond):\n\t\t\tlog.Printf(\"Retry wait completed, attempting validation again\")\n\t\t\tbreak\n\t\t}\n\n\t\treturn fileState.buildValidate(buildCtx, validateParams)\n\t} else {\n\t\tlog.Printf(\"Max retries (%d) exceeded, returning error\", MaxBuildErrorRetries)\n\t\treturn buildValidateResult{}, err\n\t}\n}\n"
  },
  {
    "path": "app/server/model/plan/build_whole_file.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"plandex-server/model\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/types\"\n\t\"plandex-server/utils\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (fileState *activeBuildStreamFileState) buildWholeFileFallback(buildCtx context.Context, proposedContent string, desc string, comments string, sessionId string) (string, error) {\n\tauth := fileState.auth\n\tfilePath := fileState.filePath\n\tclients := fileState.clients\n\tauthVars := fileState.authVars\n\tplanId := fileState.plan.Id\n\tbranch := fileState.branch\n\toriginalFile := fileState.preBuildState\n\tconfig := fileState.settings.GetModelPack().GetWholeFileBuilder()\n\n\tactivePlan := GetActivePlan(planId, branch)\n\n\tif activePlan == nil {\n\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\\n\", planId, branch)\n\t\tfileState.onBuildFileError(fmt.Errorf(\"active plan not found for plan ID %s and branch %s\", planId, branch))\n\t\treturn \"\", fmt.Errorf(\"active plan not found for plan ID %s and branch %s\", planId, branch)\n\t}\n\n\tbaseModelConfig := config.GetBaseModelConfig(authVars, fileState.settings, fileState.orgUserConfig)\n\n\toriginalFileWithLineNums := shared.AddLineNums(originalFile)\n\tproposedContentWithLineNums := shared.AddLineNums(proposedContent)\n\n\tsysPrompt, headNumTokens := prompts.GetWholeFilePrompt(filePath, originalFileWithLineNums, proposedContentWithLineNums, desc, comments)\n\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: sysPrompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tinputTokens := model.GetMessagesTokenEstimate(messages...) + model.TokensPerRequest\n\tmaxExpectedOutputTokens := shared.GetNumTokensEstimate(originalFile + proposedContent)\n\n\tmodelConfig := config.GetRoleForInputTokens(inputTokens, fileState.settings)\n\tmodelConfig = modelConfig.GetRoleForOutputTokens(maxExpectedOutputTokens, fileState.settings)\n\n\tlog.Println(\"buildWholeFile - calling model for whole file write\")\n\n\tvar prediction string\n\n\tif baseModelConfig.PredictedOutputEnabled && comments != \"\" {\n\t\tprediction = `\n<PlandexWholeFile>\n` + originalFile + `\n</PlandexWholeFile>\n`\n\n\t}\n\n\t// This allows proper accounting for cached input tokens even when the stream is cancelled -- OpenAI only for now\n\tvar willCacheNumTokens int\n\tif baseModelConfig.Provider == shared.ModelProviderOpenAI {\n\t\twillCacheNumTokens = headNumTokens\n\t}\n\n\tlog.Println(\"buildWholeFile - calling model.ModelRequest\")\n\t// spew.Dump(messages)\n\n\tmodelRes, err := model.ModelRequest(buildCtx, model.ModelRequestParams{\n\t\tClients:     clients,\n\t\tAuth:        auth,\n\t\tAuthVars:    authVars,\n\t\tPlan:        fileState.plan,\n\t\tModelConfig: &config,\n\t\tPurpose:     \"File edit\",\n\n\t\tMessages:   messages,\n\t\tPrediction: prediction,\n\n\t\tModelStreamId:  fileState.modelStreamId,\n\t\tConvoMessageId: fileState.convoMessageId,\n\t\tBuildId:        fileState.build.Id,\n\n\t\tBeforeReq: func() {\n\t\t\tfileState.builderRun.BuiltWholeFile = true\n\t\t\tfileState.builderRun.BuildWholeFileStartedAt = time.Now()\n\t\t},\n\n\t\tAfterReq: func() {\n\t\t\tfileState.builderRun.BuildWholeFileFinishedAt = time.Now()\n\t\t},\n\n\t\tWillCacheNumTokens:    willCacheNumTokens,\n\t\tEstimatedOutputTokens: maxExpectedOutputTokens,\n\n\t\tSessionId:     sessionId,\n\t\tSettings:      fileState.settings,\n\t\tOrgUserConfig: fileState.orgUserConfig,\n\t})\n\n\tif err != nil {\n\t\tif errors.Is(err, context.Canceled) {\n\t\t\tlog.Printf(\"buildWholeFileFallback - context canceled during model request for file %s\", filePath)\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn \"\", fmt.Errorf(\"error calling model: %v\", err)\n\t}\n\n\tfileState.builderRun.GenerationIds = append(fileState.builderRun.GenerationIds, modelRes.GenerationId)\n\tfileState.builderRun.BuildWholeFileFinishedAt = time.Now()\n\n\tcontent := modelRes.Content\n\n\t// log.Printf(\"buildWholeFile - %s - content:\\n%s\\n\", filePath, content)\n\n\twholeFile := utils.GetXMLContent(content, \"PlandexWholeFile\")\n\n\tif wholeFile == \"\" {\n\t\tlog.Printf(\"buildWholeFile - no whole file found in response\\n\")\n\t\treturn fileState.wholeFileRetryOrError(buildCtx, proposedContent, desc, comments, sessionId, fmt.Errorf(\"no whole file found in response\"))\n\t}\n\n\treturn wholeFile, nil\n}\n\nfunc (fileState *activeBuildStreamFileState) wholeFileRetryOrError(buildCtx context.Context, proposedContent string, desc string, comments string, sessionId string, err error) (string, error) {\n\tif fileState.wholeFileNumRetry < MaxBuildErrorRetries {\n\t\tfileState.wholeFileNumRetry++\n\n\t\tlog.Printf(\"buildWholeFile - retrying whole file file '%s' due to error: %v\\n\", fileState.filePath, err)\n\n\t\tactivePlan := GetActivePlan(fileState.plan.Id, fileState.branch)\n\n\t\tif activePlan == nil {\n\t\t\tlog.Printf(\"buildWholeFile - active plan not found for plan ID %s and branch %s\\n\", fileState.plan.Id, fileState.branch)\n\t\t\t// fileState.onBuildFileError(fmt.Errorf(\"active plan not found for plan ID %s and branch %s\", fileState.plan.Id, fileState.branch))\n\t\t\treturn \"\", fmt.Errorf(\"active plan not found for plan ID %s and branch %s\", fileState.plan.Id, fileState.branch)\n\t\t}\n\n\t\tselect {\n\t\tcase <-buildCtx.Done():\n\t\t\tlog.Printf(\"buildWholeFile - context canceled\\n\")\n\t\t\treturn \"\", context.Canceled\n\t\tcase <-time.After(time.Duration(fileState.wholeFileNumRetry*fileState.wholeFileNumRetry)*200*time.Millisecond + time.Duration(rand.Intn(500))*time.Millisecond):\n\t\t\tbreak\n\t\t}\n\n\t\treturn fileState.buildWholeFileFallback(buildCtx, proposedContent, desc, comments, sessionId)\n\t} else {\n\t\t// fileState.onBuildFileError(err)\n\t\treturn \"\", err\n\t}\n\n}\n"
  },
  {
    "path": "app/server/model/plan/commit_msg.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"plandex-server/utils\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (state *activeTellStreamState) genPlanDescription() (*db.ConvoMessageDescription, *shared.ApiError) {\n\tauth := state.auth\n\tplan := state.plan\n\tplanId := plan.Id\n\tbranch := state.branch\n\tsettings := state.settings\n\tclients := state.clients\n\tauthVars := state.authVars\n\torgUserConfig := state.orgUserConfig\n\tconfig := settings.GetModelPack().CommitMsg\n\n\tactivePlan := GetActivePlan(planId, branch)\n\tif activePlan == nil {\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"active plan not found for plan %s and branch %s\", planId, branch))\n\n\t\treturn nil, &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"active plan not found for plan %s and branch %s\", planId, branch),\n\t\t}\n\t}\n\n\tbaseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\tvar sysPrompt string\n\tvar tools []openai.Tool\n\tvar toolChoice *openai.ToolChoice\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tsysPrompt = prompts.SysDescribeXml\n\t} else {\n\t\tsysPrompt = prompts.SysDescribe\n\t\ttools = []openai.Tool{\n\t\t\t{\n\t\t\t\tType:     \"function\",\n\t\t\t\tFunction: &prompts.DescribePlanFn,\n\t\t\t},\n\t\t}\n\t\tchoice := openai.ToolChoice{\n\t\t\tType: \"function\",\n\t\t\tFunction: openai.ToolFunction{\n\t\t\t\tName: prompts.DescribePlanFn.Name,\n\t\t\t},\n\t\t}\n\t\ttoolChoice = &choice\n\t}\n\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: sysPrompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleAssistant,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: activePlan.CurrentReplyContent,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\treqParams := model.ModelRequestParams{\n\t\tClients:        clients,\n\t\tAuth:           auth,\n\t\tAuthVars:       authVars,\n\t\tPlan:           plan,\n\t\tModelConfig:    &config,\n\t\tPurpose:        \"Response summary\",\n\t\tMessages:       messages,\n\t\tModelStreamId:  state.modelStreamId,\n\t\tConvoMessageId: state.replyId,\n\t\tSessionId:      activePlan.SessionId,\n\t\tSettings:       settings,\n\t\tOrgUserConfig:  orgUserConfig,\n\t}\n\n\tif tools != nil {\n\t\treqParams.Tools = tools\n\t}\n\tif toolChoice != nil {\n\t\treqParams.ToolChoice = toolChoice\n\t}\n\n\tmodelRes, err := model.ModelRequest(activePlan.Ctx, reqParams)\n\n\tif err != nil {\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error during plan description model call: %v\", err))\n\n\t\treturn nil, &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"error during plan description model call: %v\", err),\n\t\t}\n\t}\n\n\tlog.Println(\"Plan description model call complete\")\n\n\tcontent := modelRes.Content\n\n\tvar commitMsg string\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\tcommitMsg = utils.GetXMLContent(content, \"commitMsg\")\n\t\tif commitMsg == \"\" {\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"no commitMsg tag found in XML response\"))\n\n\t\t\treturn nil, &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"No commitMsg tag found in XML response\",\n\t\t\t}\n\t\t}\n\t} else {\n\n\t\tif content == \"\" {\n\t\t\tfmt.Println(\"no describePlan function call found in response\")\n\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"no describePlan function call found in response\"))\n\n\t\t\treturn nil, &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"No describePlan function call found in response. The model failed to generate a valid response.\",\n\t\t\t}\n\t\t}\n\n\t\tvar desc shared.ConvoMessageDescription\n\t\terr = json.Unmarshal([]byte(content), &desc)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Error unmarshalling plan description response: %v\\n\", err)\n\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error unmarshalling plan description response: %v\", err))\n\n\t\t\treturn nil, &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    fmt.Sprintf(\"error unmarshalling plan description response: %v\", err),\n\t\t\t}\n\t\t}\n\t\tcommitMsg = desc.CommitMsg\n\t}\n\n\treturn &db.ConvoMessageDescription{\n\t\tPlanId:    planId,\n\t\tCommitMsg: commitMsg,\n\t}, nil\n}\n\ntype GenCommitMsgForPendingResultsParams struct {\n\tAuth      *types.ServerAuth\n\tPlan      *db.Plan\n\tSettings  *shared.PlanSettings\n\tCurrent   *shared.CurrentPlanState\n\tSessionId string\n\tCtx       context.Context\n\tClients   map[string]model.ClientInfo\n\tAuthVars  map[string]string\n}\n\nfunc GenCommitMsgForPendingResults(params GenCommitMsgForPendingResultsParams) (string, error) {\n\tauth := params.Auth\n\tplan := params.Plan\n\tsettings := params.Settings\n\tcurrent := params.Current\n\tsessionId := params.SessionId\n\tctx := params.Ctx\n\tclients := params.Clients\n\tauthVars := params.AuthVars\n\n\tconfig := settings.GetModelPack().CommitMsg\n\n\ts := \"\"\n\n\tnum := 0\n\tfor _, desc := range current.ConvoMessageDescriptions {\n\t\tif desc.WroteFiles && desc.DidBuild && len(desc.BuildPathsInvalidated) == 0 && desc.AppliedAt == nil {\n\t\t\ts += desc.CommitMsg + \"\\n\"\n\t\t\tnum++\n\t\t}\n\t}\n\n\tif num <= 1 {\n\t\treturn s, nil\n\t}\n\n\tprompt := \"Pending changes:\\n\\n\" + s\n\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompts.SysPendingResults,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodelRes, err := model.ModelRequest(ctx, model.ModelRequestParams{\n\t\tClients:     clients,\n\t\tAuthVars:    authVars,\n\t\tAuth:        auth,\n\t\tPlan:        plan,\n\t\tModelConfig: &config,\n\t\tPurpose:     \"Commit message\",\n\t\tMessages:    messages,\n\t\tSessionId:   sessionId,\n\t\tSettings:    settings,\n\t})\n\n\tif err != nil {\n\t\tfmt.Println(\"Generate commit message error:\", err)\n\n\t\treturn \"\", err\n\t}\n\n\tcontent := modelRes.Content\n\n\tif content == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no response from model\")\n\t}\n\n\treturn content, nil\n}\n"
  },
  {
    "path": "app/server/model/plan/exec_status.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/model\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"plandex-server/utils\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\n// controls the number steps to spent trying to finish a single subtask\n// if a subtask is not finished in this number of steps, we'll give up and mark it done\n// necessary to prevent infinite loops\nconst MaxPreviousMessages = 4\n\ntype execStatusShouldContinueResult struct {\n\tsubtaskFinished bool\n}\n\nfunc (state *activeTellStreamState) execStatusShouldContinue(currentMessage string, sessionId string, ctx context.Context) (execStatusShouldContinueResult, *shared.ApiError) {\n\tauth := state.auth\n\tplan := state.plan\n\tsettings := state.settings\n\tclients := state.clients\n\tauthVars := state.authVars\n\tconfig := settings.GetModelPack().ExecStatus\n\torgUserConfig := state.orgUserConfig\n\n\tbaseModelConfig := config.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\tcurrentSubtask := state.currentSubtask\n\n\tif currentSubtask == nil {\n\t\tlog.Printf(\"[ExecStatus] No current subtask\")\n\t\treturn execStatusShouldContinueResult{\n\t\t\tsubtaskFinished: true,\n\t\t}, nil\n\t}\n\n\t// Check subtask completion\n\tcompletionMarker := fmt.Sprintf(\"**%s** has been completed\", currentSubtask.Title)\n\tlog.Printf(\"[ExecStatus] Checking for subtask completion marker: %q\", completionMarker)\n\tlog.Printf(\"[ExecStatus] Current subtask: %q\", currentSubtask.Title)\n\n\tif strings.Contains(currentMessage, completionMarker) {\n\t\tlog.Printf(\"[ExecStatus] ✓ Subtask completion marker found\")\n\t\treturn execStatusShouldContinueResult{\n\t\t\tsubtaskFinished: true,\n\t\t}, nil\n\n\t\t// 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.)\n\t\t// now just going back to trusting the completion marker... basically it's better to err on the side of marking tasks done.\n\n\t\t// var potentialProblem bool\n\n\t\t// if len(state.chunkProcessor.replyOperations) == 0 {\n\t\t// \tlog.Printf(\"[ExecStatus] ✗ Subtask completion marker found, but there are no operations to execute\")\n\t\t// \tpotentialProblem = true\n\t\t// } else {\n\t\t// wroteToPaths := map[string]bool{}\n\t\t// for _, op := range state.chunkProcessor.replyOperations {\n\t\t// \tif op.Type == shared.OperationTypeFile {\n\t\t// \t\twroteToPaths[op.Path] = true\n\t\t// \t}\n\t\t// }\n\n\t\t// for _, path := range currentSubtask.UsesFiles {\n\t\t// \tif !wroteToPaths[path] {\n\t\t// \t\tlog.Printf(\"[ExecStatus] ✗ Subtask completion marker found, but the operations did not write to the file %q from the 'Uses' list\", path)\n\t\t// \t\tpotentialProblem = true\n\t\t// \t\tbreak\n\t\t// \t}\n\t\t// }\n\t\t// }\n\n\t\t// if !potentialProblem {\n\t\t// \tlog.Printf(\"[ExecStatus] ✓ Subtask completion marker found and no potential problem - will mark as completed\")\n\n\t\t// \treturn execStatusShouldContinueResult{\n\t\t// \t\tsubtaskFinished: true,\n\t\t// \t}, nil\n\t\t// } else if currentSubtask.NumTries >= 1 {\n\t\t// \tlog.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\")\n\n\t\t// \treturn execStatusShouldContinueResult{\n\t\t// \t\tsubtaskFinished: true,\n\t\t// \t}, nil\n\t\t// } else {\n\t\t// \tlog.Printf(\"[ExecStatus] ✗ Subtask completion marker found, but the operations are questionable -- will verify with LLM call\")\n\t\t// }\n\t} else {\n\t\tlog.Printf(\"[ExecStatus] ✗ No subtask completion marker found in message\")\n\t}\n\n\tlog.Println(\"[ExecStatus] Current subtasks state:\")\n\tfor i, task := range state.subtasks {\n\t\tlog.Printf(\"[ExecStatus] Task %d: %q (finished=%v)\", i+1, task.Title, task.IsFinished)\n\t}\n\n\tlog.Println(\"Checking if plan should continue based on exec status\")\n\n\tfullSubtask := currentSubtask.Title\n\tfullSubtask += \"\\n\\n\" + currentSubtask.Description\n\n\tpreviousMessages := []string{}\n\tfor _, msg := range state.convo {\n\t\tif msg.Subtask != nil && msg.Subtask.Title == currentSubtask.Title {\n\t\t\tpreviousMessages = append(previousMessages, msg.Message)\n\t\t}\n\t}\n\n\tif len(previousMessages) >= MaxPreviousMessages {\n\t\tlog.Printf(\"[ExecStatus] ✗ Max previous messages reached - will mark as completed and move on to next subtask\")\n\t\treturn execStatusShouldContinueResult{\n\t\t\tsubtaskFinished: true,\n\t\t}, nil\n\t}\n\n\tprompt := prompts.GetExecStatusFinishedSubtask(prompts.GetExecStatusFinishedSubtaskParams{\n\t\tUserPrompt:            state.userPrompt,\n\t\tCurrentSubtask:        fullSubtask,\n\t\tCurrentMessage:        currentMessage,\n\t\tPreviousMessages:      previousMessages,\n\t\tPreferredOutputFormat: baseModelConfig.PreferredOutputFormat,\n\t})\n\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tmodelRes, err := model.ModelRequest(ctx, model.ModelRequestParams{\n\t\tClients:        clients,\n\t\tAuth:           auth,\n\t\tAuthVars:       authVars,\n\t\tPlan:           plan,\n\t\tModelConfig:    &config,\n\t\tPurpose:        \"Task completion check\",\n\t\tMessages:       messages,\n\t\tModelStreamId:  state.modelStreamId,\n\t\tConvoMessageId: state.replyId,\n\t\tSessionId:      sessionId,\n\t\tSettings:       settings,\n\t\tOrgUserConfig:  orgUserConfig,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"[ExecStatus] Error in model call: %v\", err)\n\t\treturn execStatusShouldContinueResult{}, nil\n\t}\n\n\tcontent := modelRes.Content\n\n\tvar reasoning string\n\tvar subtaskFinished bool\n\n\tif baseModelConfig.PreferredOutputFormat == shared.ModelOutputFormatXml {\n\t\treasoning = utils.GetXMLContent(content, \"reasoning\")\n\t\tsubtaskFinishedStr := utils.GetXMLContent(content, \"subtaskFinished\")\n\t\tsubtaskFinished = subtaskFinishedStr == \"true\"\n\n\t\tif reasoning == \"\" || subtaskFinishedStr == \"\" {\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"execStatusShouldContinue: missing required XML tags in response\"))\n\n\t\t\treturn execStatusShouldContinueResult{}, &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"Missing required XML tags in response\",\n\t\t\t}\n\t\t}\n\t} else {\n\n\t\tif content == \"\" {\n\t\t\tlog.Printf(\"[ExecStatus] No function response found in model output\")\n\t\t\treturn execStatusShouldContinueResult{}, nil\n\t\t}\n\n\t\tvar res types.ExecStatusResponse\n\t\tif err := json.Unmarshal([]byte(content), &res); err != nil {\n\t\t\tlog.Printf(\"[ExecStatus] Failed to parse response: %v\", err)\n\t\t\treturn execStatusShouldContinueResult{}, nil\n\t\t}\n\n\t\treasoning = res.Reasoning\n\t\tsubtaskFinished = res.SubtaskFinished\n\t}\n\n\tlog.Printf(\"[ExecStatus] Decision: subtaskFinished=%v, reasoning=%v\",\n\t\tsubtaskFinished, reasoning)\n\n\treturn execStatusShouldContinueResult{\n\t\tsubtaskFinished: subtaskFinished,\n\t}, nil\n}\n"
  },
  {
    "path": "app/server/model/plan/shutdown.go",
    "content": "package plan\n"
  },
  {
    "path": "app/server/model/plan/state.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/db\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/shutdown\"\n\t\"plandex-server/types\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\nvar (\n\tactivePlans types.SafeMap[*types.ActivePlan] = *types.NewSafeMap[*types.ActivePlan]()\n)\n\nfunc GetActivePlan(planId, branch string) *types.ActivePlan {\n\treturn activePlans.Get(strings.Join([]string{planId, branch}, \"|\"))\n}\n\nfunc CreateActivePlan(orgId, userId, planId, branch, prompt string, buildOnly, autoContext bool, sessionId string) *types.ActivePlan {\n\tactivePlan := types.NewActivePlan(orgId, userId, planId, branch, prompt, buildOnly, autoContext, sessionId)\n\tkey := strings.Join([]string{planId, branch}, \"|\")\n\n\tactivePlans.Set(key, activePlan)\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-activePlan.Ctx.Done():\n\t\t\t\tlog.Printf(\"case <-activePlan.Ctx.Done(): %s\\n\", planId)\n\n\t\t\t\terr := db.SetPlanStatus(planId, branch, shared.PlanStatusStopped, \"\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error setting plan %s status to stopped: %v\\n\", planId, err)\n\t\t\t\t}\n\n\t\t\t\tDeleteActivePlan(orgId, userId, planId, branch)\n\n\t\t\t\treturn\n\t\t\tcase apiErr := <-activePlan.StreamDoneCh:\n\t\t\t\tlog.Printf(\"case apiErr := <-activePlan.StreamDoneCh: %s\\n\", planId)\n\t\t\t\tlog.Printf(\"apiErr: %v\\n\", apiErr)\n\n\t\t\t\tif apiErr == nil {\n\t\t\t\t\tlog.Printf(\"Plan %s stream completed successfully\", planId)\n\n\t\t\t\t\terr := db.SetPlanStatus(planId, branch, shared.PlanStatusFinished, \"\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"Error setting plan %s status to ready: %v\\n\", planId, err)\n\t\t\t\t\t}\n\n\t\t\t\t\t// cancel *after* the DeleteActivePlan call\n\t\t\t\t\t// allows queued operations to complete\n\t\t\t\t\tDeleteActivePlan(orgId, userId, planId, branch)\n\t\t\t\t\tactivePlan.CancelFn()\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tlog.Printf(\"Error streaming plan %s: %v\\n\", planId, apiErr)\n\n\t\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error streaming plan %s: %v\", planId, apiErr))\n\n\t\t\t\t\terr := db.SetPlanStatus(planId, branch, shared.PlanStatusError, apiErr.Msg)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"Error setting plan %s status to error: %v\\n\", planId, err)\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.Println(\"Sending error message to client\")\n\t\t\t\t\tactivePlan.Stream(shared.StreamMessage{\n\t\t\t\t\t\tType:  shared.StreamMessageError,\n\t\t\t\t\t\tError: apiErr,\n\t\t\t\t\t})\n\t\t\t\t\tactivePlan.FlushStreamBuffer()\n\n\t\t\t\t\tlog.Println(\"Stopping any active summary stream\")\n\t\t\t\t\tactivePlan.SummaryCancelFn()\n\n\t\t\t\t\tlog.Println(\"Waiting 100ms after streaming error before canceling active plan\")\n\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t\t\t// cancel *before* the DeleteActivePlan call below\n\t\t\t\t\t// short circuits any active operations\n\t\t\t\t\tlog.Println(\"Cancelling active plan\")\n\t\t\t\t\tactivePlan.CancelFn()\n\t\t\t\t\tDeleteActivePlan(orgId, userId, planId, branch)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn activePlan\n}\n\nfunc DeleteActivePlan(orgId, userId, planId, branch string) {\n\tlog.Printf(\"Deleting active plan %s - %s - %s\\n\", planId, branch, orgId)\n\n\tactivePlan := GetActivePlan(planId, branch)\n\tif activePlan == nil {\n\t\tlog.Printf(\"DeleteActivePlan - No active plan found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\n\tctx, cancelFn := context.WithTimeout(shutdown.ShutdownCtx, 10*time.Second)\n\tdefer cancelFn()\n\n\tlog.Printf(\"Clearing uncommitted changes for plan %s - %s - %s\\n\", planId, branch, orgId)\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    orgId,\n\t\tUserId:   userId,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tScope:    db.LockScopeWrite,\n\t\tCtx:      ctx,\n\t\tCancelFn: cancelFn,\n\t\tReason:   \"delete active plan\",\n\t}, func(repo *db.GitRepo) error {\n\t\tlog.Printf(\"Starting clear uncommitted changes for plan %s - %s - %s\\n\", planId, branch, orgId)\n\t\terr := repo.GitClearUncommittedChanges(branch)\n\t\tlog.Printf(\"Finished clear uncommitted changes for plan %s - %s - %s\\n\", planId, branch, orgId)\n\t\tlog.Printf(\"Error: %v\\n\", err)\n\t\treturn err\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error clearing uncommitted changes for plan %s: %v\\n\", planId, err)\n\t}\n\n\tactivePlans.Delete(strings.Join([]string{planId, branch}, \"|\"))\n\n\tlog.Printf(\"Deleted active plan %s - %s - %s\\n\", planId, branch, orgId)\n}\n\nfunc UpdateActivePlan(planId, branch string, fn func(*types.ActivePlan)) {\n\tactivePlans.Update(strings.Join([]string{planId, branch}, \"|\"), fn)\n}\n\nfunc SubscribePlan(ctx context.Context, planId, branch string) (string, chan string) {\n\tlog.Printf(\"Subscribing to plan %s\\n\", planId)\n\tvar id string\n\tvar ch chan string\n\n\tactivePlan := GetActivePlan(planId, branch)\n\tif activePlan == nil {\n\t\tlog.Printf(\"SubscribePlan - No active plan found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn \"\", nil\n\t}\n\n\tUpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {\n\t\tid, ch = activePlan.Subscribe(ctx)\n\t})\n\treturn id, ch\n}\n\nfunc UnsubscribePlan(planId, branch, subscriptionId string) {\n\tlog.Printf(\"UnsubscribePlan %s - %s - %s\\n\", planId, branch, subscriptionId)\n\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\"No active plan found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\n\tUpdateActivePlan(planId, branch, func(activePlan *types.ActivePlan) {\n\t\tactivePlan.Unsubscribe(subscriptionId)\n\t\tlog.Printf(\"Unsubscribed from plan %s - %s - %s\\n\", planId, branch, subscriptionId)\n\t})\n}\n\nfunc NumActivePlans() int {\n\treturn activePlans.Len()\n}\n"
  },
  {
    "path": "app/server/model/plan/stop.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"plandex-server/db\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc Stop(planId, branch, currentUserId, currentOrgId string) error {\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\treturn fmt.Errorf(\"no active plan with id %s\", planId)\n\t}\n\n\tactive.SummaryCancelFn()\n\tactive.CancelFn()\n\n\treturn nil\n}\n\nfunc StorePartialReply(repo *db.GitRepo, planId, branch, currentUserId, currentOrgId string) error {\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\treturn fmt.Errorf(\"no active plan with id %s\", planId)\n\t}\n\n\tif !active.BuildOnly && !active.RepliesFinished {\n\t\tnum := active.MessageNum + 1\n\n\t\tuserMsg := db.ConvoMessage{\n\t\t\tOrgId:   currentOrgId,\n\t\t\tPlanId:  planId,\n\t\t\tUserId:  currentUserId,\n\t\t\tRole:    openai.ChatMessageRoleAssistant,\n\t\t\tTokens:  active.NumTokens,\n\t\t\tNum:     num,\n\t\t\tStopped: true,\n\t\t\tMessage: active.CurrentReplyContent,\n\t\t}\n\n\t\t_, err := db.StoreConvoMessage(repo, &userMsg, currentUserId, branch, true)\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error storing convo message: %v\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_build_pending.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/notify\"\n\t\"runtime/debug\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc (state *activeTellStreamState) queuePendingBuilds() {\n\tplan := state.plan\n\tplanId := plan.Id\n\tbranch := state.branch\n\tauth := state.auth\n\tclients := state.clients\n\tauthVars := state.authVars\n\tcurrentOrgId := state.currentOrgId\n\tcurrentUserId := state.currentUserId\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\"execTellPlan: Active plan not found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"panic in queuePendingBuilds: %v\\n%s\", r, debug.Stack())\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error getting pending builds by path: %v\", r))\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    fmt.Sprintf(\"Error getting pending builds by path: %v\\n%s\", r, debug.Stack()),\n\t\t\t}\n\t\t}\n\t}()\n\n\tpendingBuildsByPath, err := active.PendingBuildsByPath(auth.OrgId, auth.User.Id, state.convo)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error getting pending builds by path: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error getting pending builds by path: %v\", err))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"Error getting pending builds by path: %v\", err),\n\t\t}\n\t\treturn\n\t}\n\n\tif len(pendingBuildsByPath) == 0 {\n\t\tlog.Println(\"Tell plan: no pending builds\")\n\t\treturn\n\t}\n\n\tlog.Printf(\"Tell plan: found %d pending builds\\n\", len(pendingBuildsByPath))\n\t// spew.Dump(pendingBuildsByPath)\n\n\tbuildState := &activeBuildStreamState{\n\t\tmodelStreamId: active.ModelStreamId,\n\t\tclients:       clients,\n\t\tauthVars:      authVars,\n\t\tauth:          auth,\n\t\tcurrentOrgId:  currentOrgId,\n\t\tcurrentUserId: currentUserId,\n\t\tplan:          plan,\n\t\tbranch:        branch,\n\t\tsettings:      state.settings,\n\t\tmodelContext:  state.modelContext,\n\t\torgUserConfig: state.orgUserConfig,\n\t}\n\n\tfor _, pendingBuilds := range pendingBuildsByPath {\n\t\tbuildState.queueBuilds(pendingBuilds)\n\t}\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_context.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/types\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype formatModelContextParams struct {\n\tincludeMaps          bool\n\tsmartContextEnabled  bool\n\tincludeApplyScript   bool\n\tbaseOnly             bool\n\tcacheControl         bool\n\tactiveOnly           bool\n\tautoOnly             bool\n\tactivatePaths        map[string]bool\n\tactivatePathsOrdered []string\n\tmaxTokens            int\n}\n\nfunc (state *activeTellStreamState) formatModelContext(params formatModelContextParams) []*types.ExtendedChatMessagePart {\n\tlog.Println(\"Tell plan - formatModelContext\")\n\n\tincludeMaps := params.includeMaps\n\tsmartContextEnabled := params.smartContextEnabled\n\tincludeApplyScript := params.includeApplyScript\n\tcurrentStage := state.currentStage\n\n\tbasicOnly := params.baseOnly\n\tactiveOnly := params.activeOnly\n\tautoOnly := params.autoOnly\n\tactivatePaths := params.activatePaths\n\tactivatePathsOrdered := params.activatePathsOrdered\n\tif activatePaths == nil {\n\t\tactivatePaths = map[string]bool{}\n\t}\n\n\tmaxTokens := params.maxTokens\n\n\t// log all the flags\n\tlog.Printf(\"Tell plan - formatModelContext - basicOnly: %t, activeOnly: %t, autoOnly: %t, smartContextEnabled: %t, execEnabled: %t, includeMaps: %t, activatePaths: %v, activatePathsOrdered: %v, maxTokens: %d\\n\",\n\t\tbasicOnly, activeOnly, autoOnly, smartContextEnabled, includeApplyScript, includeMaps, activatePaths, activatePathsOrdered, params.maxTokens)\n\n\tvar contextBodies []string = []string{\n\t\t\"### LATEST PLAN CONTEXT ###\",\n\t}\n\taddedFilesSet := map[string]bool{}\n\n\tuses := map[string]bool{}\n\n\t// log.Println(\"Tell plan - formatModelContext - state.currentSubtask:\\n\", spew.Sdump(state.currentSubtask))\n\t// if state.currentSubtask != nil {\n\t// \tlog.Println(\"Tell plan - formatModelContext - state.currentSubtask.UsesFiles:\\n\", spew.Sdump(state.currentSubtask.UsesFiles))\n\t// }\n\t// log.Println(\"Tell plan - formatModelContext - currentStage.TellStage:\\n\", currentStage.TellStage)\n\t// log.Println(\"Tell plan - formatModelContext - smartContextEnabled:\\n\", smartContextEnabled)\n\n\tif currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil {\n\t\tlog.Println(\"Tell plan - formatModelContext - implementation stage - smart context enabled for current subtask\")\n\t\tfor _, path := range state.currentSubtask.UsesFiles {\n\t\t\tuses[path] = true\n\t\t}\n\t\tif verboseLogging {\n\t\t\tlog.Printf(\"Tell plan - formatModelContext - uses: %v\\n\", uses)\n\t\t}\n\t}\n\n\t// log.Println(\"Tell plan - formatModelContext - state.modelContext:\\n\", spew.Sdump(state.modelContext))\n\n\ttotalTokens := 0\n\n\ttype toLoad struct {\n\t\tFilePath    string\n\t\tName        string\n\t\tUrl         string\n\t\tNumTokens   int\n\t\tBody        string\n\t\tContextType shared.ContextType\n\t\tImageDetail openai.ImageURLDetail\n\t\tIsPending   bool\n\t}\n\tvar toLoadAll []toLoad\n\n\tfor _, part := range state.modelContext {\n\t\tif verboseLogging {\n\t\t\tlog.Printf(\"Tell plan - formatModelContext - part: %s - %s - %s - %d tokens\\n\", part.ContextType, part.Name, part.FilePath, part.NumTokens)\n\t\t}\n\t\tif !(part.ContextType == shared.ContextMapType && includeMaps) {\n\t\t\tif basicOnly && part.AutoLoaded {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"Tell plan - formatModelContext - skipping auto loaded part -- basicOnly && part.AutoLoaded\")\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif autoOnly && !part.AutoLoaded {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"Tell plan - formatModelContext - skipping auto loaded part -- autoOnly && !part.AutoLoaded\")\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil && part.ContextType == shared.ContextFileType && !uses[part.FilePath] {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Tell plan - formatModelContext - skipping part -- currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil && part.ContextType == shared.ContextFileType && !uses[part.FilePath]\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif activeOnly && !activatePaths[part.FilePath] {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Tell plan - formatModelContext - skipping part -- activeOnly && !activatePaths[part.FilePath]\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif part.ContextType == shared.ContextMapType && !includeMaps {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Tell plan - formatModelContext - skipping part -- part.ContextType == shared.ContextMapType && !includeMaps\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\ttoLoadAll = append(toLoadAll, toLoad{\n\t\t\tFilePath:    part.FilePath,\n\t\t\tNumTokens:   part.NumTokens,\n\t\t\tBody:        part.Body,\n\t\t\tContextType: part.ContextType,\n\t\t\tName:        part.Name,\n\t\t\tUrl:         part.Url,\n\t\t\tImageDetail: part.ImageDetail,\n\t\t})\n\n\t\tif part.ContextType == shared.ContextFileType {\n\t\t\taddedFilesSet[part.FilePath] = true\n\t\t}\n\t}\n\n\t// Add any current pendingFiles in plan that weren't added to the context\n\tvar currentPlanFiles *shared.CurrentPlanFiles\n\tvar pendingFiles map[string]string = map[string]string{}\n\tif state.currentPlanState != nil && state.currentPlanState.CurrentPlanFiles != nil && state.currentPlanState.CurrentPlanFiles.Files != nil {\n\t\tcurrentPlanFiles = state.currentPlanState.CurrentPlanFiles\n\t\tpendingFiles = state.currentPlanState.CurrentPlanFiles.Files\n\t}\n\n\tfor filePath, body := range pendingFiles {\n\t\tif !addedFilesSet[filePath] {\n\n\t\t\tif currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && !uses[filePath] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif filePath == \"_apply.sh\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif activeOnly && !activatePaths[filePath] {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tnumTokens := shared.GetNumTokensEstimate(body)\n\n\t\t\ttoLoadAll = append(toLoadAll, toLoad{\n\t\t\t\tFilePath:    filePath,\n\t\t\t\tNumTokens:   numTokens,\n\t\t\t\tBody:        body,\n\t\t\t\tContextType: shared.ContextFileType,\n\t\t\t\tName:        filePath,\n\t\t\t\tIsPending:   true,\n\t\t\t})\n\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Printf(\"Tell plan - formatModelContext - added current plan file - %s\\n\", filePath)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(activatePathsOrdered) > 0 {\n\t\tindexByPath := map[string]int{}\n\t\tfor i, path := range activatePathsOrdered {\n\t\t\tindexByPath[path] = i\n\t\t}\n\n\t\tsort.Slice(toLoadAll, func(i, j int) bool {\n\t\t\tiIndex, ok1 := indexByPath[toLoadAll[i].FilePath]\n\t\t\tjIndex, ok2 := indexByPath[toLoadAll[j].FilePath]\n\n\t\t\t// If neither has an index, sort by Name so we are using a stable order for caching\n\t\t\tif !ok1 && !ok2 {\n\t\t\t\treturn toLoadAll[i].Name < toLoadAll[j].Name\n\t\t\t}\n\n\t\t\t// If only i doesn't have an index, it goes after j\n\t\t\tif !ok1 {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// If only j doesn't have an index, it goes after i\n\t\t\tif !ok2 {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\t// Both have indices, compare them\n\t\t\treturn iIndex < jIndex\n\t\t})\n\t}\n\n\tfor _, part := range toLoadAll {\n\t\ttotalTokens += part.NumTokens\n\n\t\tif maxTokens > 0 && totalTokens > maxTokens {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Printf(\"Tell plan - formatModelContext - total tokens: %d\\n\", totalTokens)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tvar message string\n\t\tvar fmtStr string\n\t\tvar args []any\n\n\t\tif part.ContextType == shared.ContextDirectoryTreeType {\n\t\t\tfmtStr = \"\\n\\n- %s | directory tree:\\n\\n```\\n%s\\n```\"\n\t\t\targs = append(args, part.FilePath, part.Body)\n\t\t} else if part.ContextType == shared.ContextFileType {\n\t\t\t// if we're in the context phase and the file is pending, just include that the file is pending, not the full content\n\t\t\t// there is generally enough related context from the conversation and summary to decide on whether to load the file or not\n\t\t\t// without this, the context phase can get overloaded with pending file content\n\t\t\tif currentStage.TellStage == shared.TellStagePlanning &&\n\t\t\t\tcurrentStage.PlanningPhase == shared.PlanningPhaseContext &&\n\t\t\t\tpart.IsPending {\n\t\t\t\tfmtStr = \"\\n\\n- File `%s` has pending changes (%d 🪙)\"\n\t\t\t\targs = append(args, part.FilePath, part.NumTokens)\n\t\t\t} else {\n\n\t\t\t\tfmtStr = \"\\n\\n- %s:\\n\\n```\\n%s\\n```\"\n\n\t\t\t\t// use pending file value if available\n\t\t\t\tvar body string\n\t\t\t\tvar found bool\n\t\t\t\tres, ok := pendingFiles[part.FilePath]\n\t\t\t\tif ok {\n\t\t\t\t\tbody = res\n\t\t\t\t\tfound = true\n\t\t\t\t}\n\t\t\t\tif !found {\n\t\t\t\t\tbody = part.Body\n\t\t\t\t}\n\n\t\t\t\targs = append(args, part.FilePath, body)\n\t\t\t}\n\t\t} else if part.ContextType == shared.ContextMapType {\n\t\t\tfmtStr = \"\\n\\n- %s | map:\\n\\n```\\n%s\\n```\"\n\t\t\targs = append(args, part.FilePath, part.Body)\n\t\t} else if part.Url != \"\" {\n\t\t\tfmtStr = \"\\n\\n- %s:\\n\\n```\\n%s\\n```\"\n\t\t\targs = append(args, part.Url, part.Body)\n\t\t} else if part.ContextType != shared.ContextImageType {\n\t\t\tfmtStr = \"\\n\\n- content%s:\\n\\n```\\n%s\\n```\"\n\t\t\targs = append(args, part.Name, part.Body)\n\t\t}\n\n\t\tif part.ContextType != shared.ContextImageType {\n\t\t\tmessage = fmt.Sprintf(fmtStr, args...)\n\t\t\tcontextBodies = append(contextBodies, message)\n\t\t}\n\n\t\tif verboseLogging {\n\t\t\tlog.Printf(\"Tell plan - formatModelContext - added context: %s - %s - %s - %d tokens\\n\", part.ContextType, part.Name, part.FilePath, part.NumTokens)\n\t\t}\n\t}\n\n\tif currentPlanFiles != nil && len(currentPlanFiles.Removed) > 0 {\n\t\tcontextBodies = append(contextBodies, \"*Removed files:*\\n\")\n\t\tfor path := range currentPlanFiles.Removed {\n\t\t\tcontextBodies = append(contextBodies, fmt.Sprintf(\"- %s\", path))\n\t\t}\n\t\tcontextBodies = 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.\")\n\n\t\tlog.Println(\"Tell plan - formatModelContext - added removed files\")\n\t\tlog.Println(contextBodies)\n\t}\n\n\tvar execScriptLines []string\n\n\tif includeApplyScript &&\n\t\t// don't show _apply.sh history and content if smart context is enabled and the current subtask doesn't use it\n\t\t!(currentStage.TellStage == shared.TellStageImplementation && smartContextEnabled && state.currentSubtask != nil && !uses[\"_apply.sh\"]) {\n\n\t\texecHistory := state.currentPlanState.ExecHistory()\n\n\t\texecScriptLines = append(execScriptLines, execHistory)\n\n\t\tscriptContent, ok := pendingFiles[\"_apply.sh\"]\n\t\tvar isEmpty bool\n\t\tif !ok || scriptContent == \"\" {\n\t\t\tscriptContent = \"[empty]\"\n\t\t\tisEmpty = true\n\t\t}\n\n\t\texecScriptLines = append(execScriptLines, \"*Current* state of _apply.sh script:\")\n\t\texecScriptLines = append(execScriptLines, fmt.Sprintf(\"\\n\\n- _apply.sh:\\n\\n```\\n%s\\n```\", scriptContent))\n\n\t\tif isEmpty && currentStage.TellStage == shared.TellStagePlanning && currentStage.PlanningPhase != shared.PlanningPhaseContext {\n\t\t\texecScriptLines = 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.\")\n\n\t\t\tif execHistory != \"\" {\n\t\t\t\texecScriptLines = 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.\")\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.Println(\"Tell plan - formatModelContext - contextMessages:\", len(contextBodies))\n\n\ttextMsg := &types.ExtendedChatMessagePart{\n\t\tType: openai.ChatMessagePartTypeText,\n\t\tText: strings.Join(contextBodies, \"\\n\"),\n\t}\n\n\tres := []*types.ExtendedChatMessagePart{textMsg}\n\n\t// now add any images that should be included\n\t// we'll check later for model image support once the final model config is set\n\tfor _, load := range toLoadAll {\n\t\tif load.ContextType == shared.ContextImageType {\n\t\t\tres = append(res, &types.ExtendedChatMessagePart{\n\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\tText: fmt.Sprintf(\"Image: %s\", load.Name),\n\t\t\t})\n\t\t\tres = append(res, &types.ExtendedChatMessagePart{\n\t\t\t\tType:     openai.ChatMessagePartTypeImageURL,\n\t\t\t\tImageURL: &openai.ChatMessageImageURL{URL: shared.GetImageDataURI(load.Body, load.FilePath), Detail: load.ImageDetail},\n\t\t\t})\n\t\t}\n\t}\n\n\tif params.cacheControl && len(res) > 0 {\n\t\tres[len(res)-1].CacheControl = &types.CacheControlSpec{\n\t\t\tType: types.CacheControlTypeEphemeral,\n\t\t}\n\t}\n\n\tif len(execScriptLines) > 0 {\n\t\tres = append(res, &types.ExtendedChatMessagePart{\n\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\tText: strings.Join(execScriptLines, \"\\n\"),\n\t\t})\n\t}\n\n\tres = append(res, &types.ExtendedChatMessagePart{\n\t\tType: openai.ChatMessagePartTypeText,\n\t\tText: \"### END OF CONTEXT ###\\n\\n\",\n\t})\n\n\treturn res\n}\n\nvar pathRegex = regexp.MustCompile(\"`(.+?)`\")\n\ntype checkAutoLoadContextResult struct {\n\tautoLoadPaths        []string\n\tactivatePaths        map[string]bool\n\thasExplicitPaths     bool\n\tactivatePathsOrdered []string\n}\n\nfunc (state *activeTellStreamState) checkAutoLoadContext() checkAutoLoadContextResult {\n\treq := state.req\n\tactivePlan := state.activePlan\n\tcontextsByPath := activePlan.ContextsByPath\n\tcurrentStage := state.currentStage\n\n\t// can only auto load context in planning stage\n\t// context phase is primary loading phase\n\t// planning phase can still load additional context files as a backup\n\tif currentStage.TellStage != shared.TellStagePlanning {\n\t\treturn checkAutoLoadContextResult{}\n\t}\n\n\t// for chat responses, only auto load context if we're in the context phase\n\tif req.IsChatOnly && currentStage.PlanningPhase != shared.PlanningPhaseContext {\n\t\treturn checkAutoLoadContextResult{}\n\t}\n\n\tlog.Printf(\"%d existing contexts by path\\n\", len(contextsByPath))\n\n\t// pick out all potential file paths within backticks\n\tmatches := pathRegex.FindAllStringSubmatch(activePlan.CurrentReplyContent, -1)\n\n\ttoAutoLoad := map[string]bool{}\n\ttoActivate := map[string]bool{}\n\ttoActivateOrdered := []string{}\n\tallSet := map[string]bool{}\n\tallFiles := []string{}\n\n\tfor _, match := range matches {\n\t\ttrimmed := strings.TrimSpace(match[1])\n\t\tif trimmed == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif req.ProjectPaths[trimmed] {\n\t\t\tif !allSet[trimmed] {\n\t\t\t\tallFiles = append(allFiles, trimmed)\n\t\t\t\tallSet[trimmed] = true\n\n\t\t\t\ttoActivate[trimmed] = true\n\t\t\t\ttoActivateOrdered = append(toActivateOrdered, trimmed)\n\t\t\t\tif contextsByPath[trimmed] == nil {\n\t\t\t\t\ttoAutoLoad[trimmed] = true\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\ttoAutoLoadPaths := []string{}\n\tfor path := range toAutoLoad {\n\t\ttoAutoLoadPaths = append(toAutoLoadPaths, path)\n\t}\n\n\thasExplicitPaths := strings.Contains(activePlan.CurrentReplyContent, \"### Files\")\n\n\tlog.Printf(\"Tell plan - checkAutoLoadContext - toAutoLoad: %v\\n\", toAutoLoadPaths)\n\tlog.Printf(\"Tell plan - checkAutoLoadContext - toActivate: %v\\n\", toActivateOrdered)\n\n\treturn checkAutoLoadContextResult{\n\t\tautoLoadPaths:        toAutoLoadPaths,\n\t\tactivatePaths:        toActivate,\n\t\tactivatePathsOrdered: toActivateOrdered,\n\t\thasExplicitPaths:     hasExplicitPaths,\n\t}\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_exec.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\t\"plandex-server/db\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/model\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/google/uuid\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype TellParams struct {\n\tClients  map[string]model.ClientInfo\n\tAuthVars map[string]string\n\tPlan     *db.Plan\n\tBranch   string\n\tAuth     *types.ServerAuth\n\tReq      *shared.TellPlanRequest\n}\n\nfunc Tell(params TellParams) error {\n\tclients := params.Clients\n\tplan := params.Plan\n\tbranch := params.Branch\n\tauth := params.Auth\n\treq := params.Req\n\tauthVars := params.AuthVars\n\n\tlog.Printf(\"Tell: Called with plan ID %s on branch %s\\n\", plan.Id, branch)\n\n\t_, err := activatePlan(\n\t\tclients,\n\t\tplan,\n\t\tbranch,\n\t\tauth,\n\t\treq.Prompt,\n\t\tfalse,\n\t\treq.AutoContext,\n\t\treq.SessionId,\n\t)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error activating plan: %v\\n\", err)\n\t\treturn err\n\t}\n\n\tgo execTellPlan(execTellPlanParams{\n\t\tclients:            clients,\n\t\tplan:               plan,\n\t\tbranch:             branch,\n\t\tauth:               auth,\n\t\treq:                req,\n\t\titeration:          0,\n\t\tshouldBuildPending: !req.IsChatOnly && req.BuildMode == shared.BuildModeAuto,\n\t\tauthVars:           authVars,\n\t})\n\n\tlog.Printf(\"Tell: Tell operation completed successfully for plan ID %s on branch %s\\n\", plan.Id, branch)\n\treturn nil\n}\n\ntype execTellPlanParams struct {\n\tclients                    map[string]model.ClientInfo\n\tauthVars                   map[string]string\n\tplan                       *db.Plan\n\tbranch                     string\n\tauth                       *types.ServerAuth\n\treq                        *shared.TellPlanRequest\n\titeration                  int\n\tmissingFileResponse        shared.RespondMissingFileChoice\n\tshouldBuildPending         bool\n\tunfinishedSubtaskReasoning string\n}\n\nfunc execTellPlan(params execTellPlanParams) {\n\tclients := params.clients\n\tauthVars := params.authVars\n\tplan := params.plan\n\tbranch := params.branch\n\tauth := params.auth\n\treq := params.req\n\titeration := params.iteration\n\tmissingFileResponse := params.missingFileResponse\n\tshouldBuildPending := params.shouldBuildPending\n\tunfinishedSubtaskReasoning := params.unfinishedSubtaskReasoning\n\n\tlog.Printf(\"[TellExec] Starting iteration %d for plan %s on branch %s\", iteration, plan.Id, branch)\n\n\tcurrentUserId := auth.User.Id\n\tcurrentOrgId := auth.OrgId\n\n\tactive := GetActivePlan(plan.Id, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\"execTellPlan: Active plan not found for plan ID %s on branch %s\\n\", plan.Id, branch)\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"execTellPlan: Panic: %v\\n%s\\n\", r, string(debug.Stack()))\n\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"execTellPlan: Panic: %v\\n%s\", r, string(debug.Stack())))\n\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    fmt.Sprintf(\"Panic in execTellPlan: %v\\n%s\", r, string(debug.Stack())),\n\t\t\t}\n\t\t}\n\t}()\n\n\tif missingFileResponse == \"\" {\n\t\tlog.Println(\"Executing WillExecPlanHook\")\n\t\t_, apiErr := hooks.ExecHook(hooks.WillExecPlan, hooks.HookParams{\n\t\t\tAuth: auth,\n\t\t\tPlan: plan,\n\t\t})\n\n\t\tif apiErr != nil {\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\tactive.StreamDoneCh <- apiErr\n\t\t\treturn\n\t\t}\n\t}\n\n\tplanId := plan.Id\n\tlog.Println(\"execTellPlan - Setting plan status to replying\")\n\terr := db.SetPlanStatus(planId, branch, shared.PlanStatusReplying, \"\")\n\tif err != nil {\n\t\tlog.Printf(\"Error setting plan %s status to replying: %v\\n\", planId, err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error setting plan %s status to replying: %v\", planId, err))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"Error setting plan status to replying: %v\", err),\n\t\t}\n\n\t\tlog.Printf(\"execTellPlan: execTellPlan operation completed for plan ID %s on branch %s, iteration %d\\n\", plan.Id, branch, iteration)\n\t\treturn\n\t}\n\tlog.Println(\"execTellPlan - Plan status set to replying\")\n\n\tstate := &activeTellStreamState{\n\t\tmodelStreamId:       active.ModelStreamId,\n\t\tclients:             clients,\n\t\tauthVars:            authVars,\n\t\treq:                 req,\n\t\tauth:                auth,\n\t\tcurrentOrgId:        currentOrgId,\n\t\tcurrentUserId:       currentUserId,\n\t\tplan:                plan,\n\t\tbranch:              branch,\n\t\titeration:           iteration,\n\t\tmissingFileResponse: missingFileResponse,\n\t}\n\n\tlog.Println(\"execTellPlan - Loading tell plan\")\n\terr = state.loadTellPlan()\n\tif err != nil {\n\t\treturn\n\t}\n\tlog.Println(\"execTellPlan - Tell plan loaded\")\n\n\tactivatePaths, activatePathsOrdered := state.resolveCurrentStage()\n\n\tvar tentativeModelConfig shared.ModelRoleConfig\n\tvar tentativeMaxTokens int\n\tif state.currentStage.TellStage == shared.TellStagePlanning {\n\t\tif state.currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\tlog.Println(\"Tell plan - isContextStage - setting modelConfig to context loader\")\n\t\t\ttentativeModelConfig = state.settings.GetModelPack().GetArchitect()\n\t\t\ttentativeMaxTokens = state.settings.GetArchitectEffectiveMaxTokens()\n\t\t} else {\n\t\t\tplannerConfig := state.settings.GetModelPack().Planner\n\t\t\ttentativeModelConfig = plannerConfig.ModelRoleConfig\n\t\t\ttentativeMaxTokens = state.settings.GetPlannerEffectiveMaxTokens()\n\t\t}\n\t} else if state.currentStage.TellStage == shared.TellStageImplementation {\n\t\ttentativeModelConfig = state.settings.GetModelPack().GetCoder()\n\t\ttentativeMaxTokens = state.settings.GetCoderEffectiveMaxTokens()\n\t} else {\n\t\tlog.Printf(\"Tell plan - execTellPlan - unknown tell stage: %s\\n\", state.currentStage.TellStage)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"execTellPlan: unknown tell stage: %s\", state.currentStage.TellStage))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Unknown tell stage\",\n\t\t}\n\t\treturn\n\t}\n\n\tok, tokensWithoutContext := state.dryRunCalculateTokensWithoutContext(tentativeMaxTokens, unfinishedSubtaskReasoning)\n\tif !ok {\n\t\treturn\n\t}\n\n\tvar planStageSharedMsgs []*types.ExtendedChatMessagePart\n\tvar planningPhaseOnlyMsgs []*types.ExtendedChatMessagePart\n\tvar implementationMsgs []*types.ExtendedChatMessagePart\n\n\tif state.currentStage.TellStage == shared.TellStageImplementation {\n\t\timplementationMsgs = state.formatModelContext(formatModelContextParams{\n\t\t\tincludeMaps:         false,\n\t\t\tsmartContextEnabled: req.SmartContext,\n\t\t\tincludeApplyScript:  req.ExecEnabled,\n\t\t})\n\t} else if state.currentStage.TellStage == shared.TellStagePlanning {\n\t\t// add the shared context between planning and context phases first so it can be cached\n\t\t// this is just for the map and any manually loaded contexts - auto contexts will be added later\n\t\tplanStageSharedMsgs = state.formatModelContext(formatModelContextParams{\n\t\t\tincludeMaps:         true,\n\t\t\tsmartContextEnabled: req.SmartContext,\n\t\t\tincludeApplyScript:  req.ExecEnabled,\n\t\t\tbaseOnly:            true,\n\t\t\tcacheControl:        true,\n\t\t})\n\n\t\tif state.currentStage.PlanningPhase == shared.PlanningPhaseTasks {\n\t\t\tif req.AutoContext {\n\t\t\t\tmsg := types.ExtendedChatMessage{\n\t\t\t\t\tRole:    openai.ChatMessageRoleSystem,\n\t\t\t\t\tContent: []types.ExtendedChatMessagePart{},\n\t\t\t\t}\n\t\t\t\tfor _, part := range planStageSharedMsgs {\n\t\t\t\t\tmsg.Content = append(msg.Content, *part)\n\t\t\t\t}\n\t\t\t\tsharedMsgsTokens := model.GetMessagesTokenEstimate(msg)\n\n\t\t\t\ttokensRemaining := tentativeMaxTokens - (sharedMsgsTokens + tokensWithoutContext)\n\n\t\t\t\tif tokensRemaining < 0 {\n\t\t\t\t\tlog.Println(\"tokensRemaining is negative\")\n\t\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"tokensRemaining is negative\"))\n\n\t\t\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\t\t\tMsg:    \"Max tokens exceeded before adding context\",\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tplanningPhaseOnlyMsgs = state.formatModelContext(formatModelContextParams{\n\t\t\t\t\tincludeMaps:          false,\n\t\t\t\t\tsmartContextEnabled:  req.SmartContext,\n\t\t\t\t\tincludeApplyScript:   false, // already included in planStageSharedMsgs\n\t\t\t\t\tactiveOnly:           true,\n\t\t\t\t\tactivatePaths:        activatePaths,\n\t\t\t\t\tactivatePathsOrdered: activatePathsOrdered,\n\t\t\t\t\tmaxTokens:            int(float64(tokensRemaining) * 0.95), // leave a little extra room\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\t// if auto context is disabled, just dump in any remaining auto contexts, since all basic contexts have already been added in planStageSharedMsgs\n\t\t\t\tplanningPhaseOnlyMsgs = state.formatModelContext(formatModelContextParams{\n\t\t\t\t\tincludeMaps:         false,\n\t\t\t\t\tsmartContextEnabled: req.SmartContext,\n\t\t\t\t\tincludeApplyScript:  false, // already included in planStageSharedMsgs\n\t\t\t\t\tautoOnly:            true,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tgetTellSysPromptParams := getTellSysPromptParams{\n\t\tplanStageSharedMsgs:   planStageSharedMsgs,\n\t\tplanningPhaseOnlyMsgs: planningPhaseOnlyMsgs,\n\t\timplementationMsgs:    implementationMsgs,\n\t\tcontextTokenLimit:     tentativeMaxTokens,\n\t}\n\n\t// log.Println(\"getTellSysPromptParams:\\n\", spew.Sdump(getTellSysPromptParams))\n\n\tsysParts, err := state.getTellSysPrompt(getTellSysPromptParams)\n\tif err != nil {\n\t\tlog.Printf(\"Error getting tell sys prompt: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error getting tell sys prompt: %v\", err))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"Error getting tell sys prompt: %v\", err),\n\t\t}\n\t\treturn\n\t}\n\n\t// log.Println(\"**sysPrompt:**\\n\", spew.Sdump(sysParts))\n\n\tstate.messages = []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole:    openai.ChatMessageRoleSystem,\n\t\t\tContent: sysParts,\n\t\t},\n\t}\n\n\tpromptMessage, ok := state.resolvePromptMessage(unfinishedSubtaskReasoning)\n\tif !ok {\n\t\treturn\n\t}\n\n\t// log.Println(\"messages:\\n\\n\", spew.Sdump(state.messages))\n\n\t// log.Println(\"promptMessage:\", spew.Sdump(promptMessage))\n\n\tstate.tokensBeforeConvo =\n\t\tmodel.GetMessagesTokenEstimate(state.messages...) +\n\t\t\tmodel.GetMessagesTokenEstimate(*promptMessage) +\n\t\t\tstate.latestSummaryTokens +\n\t\t\tmodel.TokensPerRequest\n\n\t// print out breakdown of token usage\n\tlog.Printf(\"Latest summary tokens: %d\\n\", state.latestSummaryTokens)\n\tlog.Printf(\"Total tokens before convo: %d\\n\", state.tokensBeforeConvo)\n\n\tvar effectiveMaxTokens int\n\tif state.currentStage.TellStage == shared.TellStagePlanning {\n\t\tif state.currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\teffectiveMaxTokens = state.settings.GetArchitectEffectiveMaxTokens()\n\t\t} else {\n\t\t\teffectiveMaxTokens = state.settings.GetPlannerEffectiveMaxTokens()\n\t\t}\n\t} else if state.currentStage.TellStage == shared.TellStageImplementation {\n\t\teffectiveMaxTokens = state.settings.GetCoderEffectiveMaxTokens()\n\t}\n\n\tif state.tokensBeforeConvo > effectiveMaxTokens {\n\t\t// token limit already exceeded before adding conversation\n\t\terr := fmt.Errorf(\"token limit exceeded before adding conversation\")\n\t\tlog.Printf(\"Error: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"token limit exceeded before adding conversation\"))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Token limit exceeded before adding conversation\",\n\t\t}\n\t\treturn\n\t}\n\n\tif !state.addConversationMessages() {\n\t\treturn\n\t}\n\n\t// add the prompt message to the end of the messages slice\n\tif promptMessage != nil {\n\t\tstate.messages = append(state.messages, *promptMessage)\n\t} else {\n\t\tlog.Println(\"promptMessage is nil\")\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"promptMessage is nil\"))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Prompt message isn't set\",\n\t\t}\n\t\treturn\n\t}\n\n\tstate.replyId = uuid.New().String()\n\tstate.replyParser = types.NewReplyParser()\n\n\tif missingFileResponse != \"\" && !state.handleMissingFileResponse(unfinishedSubtaskReasoning) {\n\t\treturn\n\t}\n\n\t// filter out any messages that are empty\n\tstate.messages = model.FilterEmptyMessages(state.messages)\n\n\tlog.Printf(\"\\n\\nMessages: %d\\n\", len(state.messages))\n\t// for _, message := range state.messages {\n\t// \tlog.Printf(\"%s: %v\\n\", message.Role, message.Content)\n\t// }\n\n\trequestTokens := model.GetMessagesTokenEstimate(state.messages...) + model.TokensPerRequest\n\tstate.totalRequestTokens = requestTokens\n\n\tmodelConfig := tentativeModelConfig\n\n\tlog.Println(\"Tell plan - setting modelConfig\")\n\tlog.Println(\"Tell plan - requestTokens:\", requestTokens)\n\tlog.Println(\"Tell plan - state.currentStage.TellStage:\", state.currentStage.TellStage)\n\tlog.Println(\"Tell plan - state.currentStage.PlanningPhase:\", state.currentStage.PlanningPhase)\n\n\tif state.currentStage.TellStage == shared.TellStagePlanning {\n\t\tif state.currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\tlog.Println(\"Tell plan - isContextStage - setting modelConfig to context loader\")\n\t\t\tmodelConfig = state.settings.GetModelPack().GetArchitect().GetRoleForInputTokens(requestTokens, state.settings)\n\t\t\tlog.Println(\"Tell plan - got modelConfig for context phase\")\n\t\t} else if state.currentStage.PlanningPhase == shared.PlanningPhaseTasks {\n\t\t\tmodelConfig = state.settings.GetModelPack().Planner.GetRoleForInputTokens(requestTokens, state.settings)\n\t\t\tlog.Println(\"Tell plan - got modelConfig for tasks phase\")\n\t\t}\n\t} else if state.currentStage.TellStage == shared.TellStageImplementation {\n\t\tmodelConfig = state.settings.GetModelPack().GetCoder().GetRoleForInputTokens(requestTokens, state.settings)\n\t\tlog.Println(\"Tell plan - got modelConfig for implementation stage\")\n\t}\n\n\tstate.modelConfig = &modelConfig\n\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(authVars, state.settings, state.orgUserConfig)\n\n\tif baseModelConfig == nil {\n\t\tlog.Println(\"Tell plan - baseModelConfig is nil\")\n\t\tlog.Println(\"Tell plan - modelConfig id:\", modelConfig.ModelId)\n\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"No model config found for: %s\", state.modelConfig.ModelId))\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"No model config found for: \" + string(state.modelConfig.ModelId),\n\t\t}\n\t\treturn\n\t}\n\n\tstate.baseModelConfig = baseModelConfig\n\n\t// if the model doesn't support cache control, remove the cache control spec from the messages\n\tif !baseModelConfig.SupportsCacheControl {\n\t\tfor i := range state.messages {\n\t\t\tfor j := range state.messages[i].Content {\n\t\t\t\tif state.messages[i].Content[j].CacheControl != nil {\n\t\t\t\t\tstate.messages[i].Content[j].CacheControl = nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// if the model doesn't support images, remove any image parts from the messages\n\tif !baseModelConfig.HasImageSupport {\n\t\tlog.Println(\"Tell exec - model doesn't support images. Removing image parts from messages. File name will still be included.\")\n\n\t\tfor i := range state.messages {\n\t\t\tfilteredContent := []types.ExtendedChatMessagePart{}\n\t\t\tfor _, part := range state.messages[i].Content {\n\t\t\t\tif part.Type != openai.ChatMessagePartTypeImageURL {\n\t\t\t\t\tfilteredContent = append(filteredContent, part)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstate.messages[i].Content = filteredContent\n\t\t}\n\t}\n\n\tlog.Println(\"tell exec - will send model request with:\", spew.Sdump(map[string]interface{}{\n\t\t\"provider\":  baseModelConfig.Provider,\n\t\t\"modelId\":   baseModelConfig.ModelId,\n\t\t\"modelTag\":  baseModelConfig.ModelTag,\n\t\t\"modelName\": baseModelConfig.ModelName,\n\t\t\"tokens\":    requestTokens,\n\t}))\n\n\t_, apiErr := hooks.ExecHook(hooks.WillSendModelRequest, hooks.HookParams{\n\t\tAuth: auth,\n\t\tPlan: plan,\n\t\tWillSendModelRequestParams: &hooks.WillSendModelRequestParams{\n\t\t\tInputTokens:  requestTokens,\n\t\t\tOutputTokens: baseModelConfig.MaxOutputTokens - requestTokens,\n\t\t\tModelName:    baseModelConfig.ModelName,\n\t\t\tModelId:      baseModelConfig.ModelId,\n\t\t\tModelTag:     baseModelConfig.ModelTag,\n\t\t\tIsUserPrompt: true,\n\t\t},\n\t})\n\tif apiErr != nil {\n\t\tactive.StreamDoneCh <- apiErr\n\t\treturn\n\t}\n\n\tstate.doTellRequest()\n\n\tif shouldBuildPending {\n\t\tgo state.queuePendingBuilds()\n\t}\n\n\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\tap.CurrentStreamingReplyId = state.replyId\n\t\tap.CurrentReplyDoneCh = make(chan bool, 1)\n\t})\n\n}\n\nfunc (state *activeTellStreamState) doTellRequest() {\n\tclients := state.clients\n\tauthVars := state.authVars\n\tmodelConfig := state.modelConfig\n\tactive := state.activePlan\n\n\tfallbackRes := modelConfig.GetFallbackForModelError(state.numErrorRetry, state.didProviderFallback, state.modelErr, authVars, state.settings, state.orgUserConfig)\n\tmodelConfig = fallbackRes.ModelRoleConfig\n\tstop := []string{\"<PlandexFinish/>\"}\n\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)\n\n\tif fallbackRes.FallbackType == shared.FallbackTypeProvider {\n\t\tstate.didProviderFallback = true\n\t}\n\n\t// log.Println(\"Stop:\", stop)\n\t// spew.Dump(state.messages)\n\n\tlog.Println(\"modelConfig:\", spew.Sdump(map[string]interface{}{\n\t\t\"modelName\": baseModelConfig.ModelName,\n\t\t\"modelId\":   baseModelConfig.ModelId,\n\t\t\"modelTag\":  baseModelConfig.ModelTag,\n\t}))\n\n\tif state.noCacheSupportErr {\n\t\tlog.Println(\"Tell exec - request failed with cache support error. Removing cache control breakpoints from messages.\")\n\t\tfor i := range state.messages {\n\t\t\tfor j := range state.messages[i].Content {\n\t\t\t\tif state.messages[i].Content[j].CacheControl != nil {\n\t\t\t\t\tstate.messages[i].Content[j].CacheControl = nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tmodelReq := types.ExtendedChatCompletionRequest{\n\t\tModel:    baseModelConfig.ModelName,\n\t\tMessages: state.messages,\n\t\tStream:   true,\n\t\tStreamOptions: &openai.StreamOptions{\n\t\t\tIncludeUsage: true,\n\t\t},\n\t\tTemperature: modelConfig.Temperature,\n\t\tTopP:        modelConfig.TopP,\n\t}\n\n\tif baseModelConfig.StopDisabled {\n\t\tstate.manualStop = stop\n\t} else {\n\t\tmodelReq.Stop = stop\n\t}\n\n\t// update state\n\tstate.fallbackRes = fallbackRes\n\tstate.requestStartedAt = time.Now()\n\tstate.originalReq = &modelReq\n\tstate.modelConfig = modelConfig\n\n\t// output the modelReq to a json file\n\t// if jsonData, err := json.MarshalIndent(modelReq, \"\", \"  \"); err == nil {\n\t// \ttimestamp := time.Now().Format(\"2006-01-02-150405\")\n\t// \tfilename := fmt.Sprintf(\"generations/model-request-%s.json\", timestamp)\n\t// \tif err := os.WriteFile(filename, jsonData, 0644); err != nil {\n\t// \t\tlog.Printf(\"Error writing model request to file: %v\\n\", err)\n\t// \t}\n\t// } else {\n\t// \tlog.Printf(\"Error marshaling model request to JSON: %v\\n\", err)\n\t// }\n\n\tlog.Printf(\"[Tell] doTellRequest retry=%d fallbackRetry=%d using model=%s\",\n\t\tstate.numErrorRetry, state.numFallbackRetry, baseModelConfig.ModelName)\n\n\t// start the stream\n\tstream, err := model.CreateChatCompletionStream(clients, authVars, modelConfig, state.settings, state.orgUserConfig, state.currentOrgId, state.currentUserId, active.ModelStreamCtx, modelReq)\n\tif err != nil {\n\t\tlog.Printf(\"Error starting reply stream: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error starting reply stream: %v\", err))\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Error starting reply stream: \" + err.Error(),\n\t\t}\n\t\treturn\n\t}\n\n\t// handle stream chunks\n\tgo state.listenStream(stream)\n}\n\nfunc (state *activeTellStreamState) dryRunCalculateTokensWithoutContext(tentativeMaxTokens int, unfinishedSubtaskReasoning string) (bool, int) {\n\tclone := &activeTellStreamState{\n\t\tmodelStreamId:       state.modelStreamId,\n\t\tclients:             state.clients,\n\t\treq:                 state.req,\n\t\tauth:                state.auth,\n\t\tcurrentOrgId:        state.currentOrgId,\n\t\tcurrentUserId:       state.currentUserId,\n\t\tplan:                state.plan,\n\t\tbranch:              state.branch,\n\t\titeration:           state.iteration,\n\t\tmissingFileResponse: state.missingFileResponse,\n\t\tsettings:            state.settings,\n\t\tcurrentStage:        state.currentStage,\n\t\tsubtasks:            state.subtasks,\n\t\tcurrentSubtask:      state.currentSubtask,\n\t\tconvo:               state.convo,\n\t\tsummaries:           state.summaries,\n\t\tlatestSummaryTokens: state.latestSummaryTokens,\n\t\tuserPrompt:          state.userPrompt,\n\t\tpromptMessage:       state.promptMessage,\n\t\thasContextMap:       state.hasContextMap,\n\t\tcontextMapEmpty:     state.contextMapEmpty,\n\t\thasAssistantReply:   state.hasAssistantReply,\n\t\tmodelContext:        state.modelContext,\n\t\tactivePlan:          state.activePlan,\n\t}\n\n\tsysParts, err := clone.getTellSysPrompt(getTellSysPromptParams{\n\t\tcontextTokenLimit:    tentativeMaxTokens,\n\t\tdryRunWithoutContext: true,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error getting tell sys prompt for dry run token calculation: %v\", err)\n\n\t\tmsg := \"Error getting tell sys prompt for dry run token calculation\"\n\t\tif err.Error() == AllTasksCompletedMsg {\n\t\t\tmsg = \"There's no current task to implement. Try a prompt instead of the 'continue' command.\"\n\t\t\tgo notify.NotifyErr(notify.SeverityInfo, msg)\n\t\t} else {\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error getting tell sys prompt for dry run token calculation: %v\", err))\n\t\t}\n\n\t\tstate.activePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    msg,\n\t\t}\n\t\treturn false, 0\n\t}\n\n\tclone.messages = []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole:    openai.ChatMessageRoleSystem,\n\t\t\tContent: sysParts,\n\t\t},\n\t}\n\n\tpromptMessage, ok := clone.resolvePromptMessage(unfinishedSubtaskReasoning)\n\tif !ok {\n\t\treturn false, 0\n\t}\n\n\tclone.tokensBeforeConvo =\n\t\tmodel.GetMessagesTokenEstimate(clone.messages...) +\n\t\t\tmodel.GetMessagesTokenEstimate(*promptMessage) +\n\t\t\tclone.latestSummaryTokens +\n\t\t\tmodel.TokensPerRequest\n\n\tvar effectiveMaxTokens int\n\tif clone.currentStage.TellStage == shared.TellStagePlanning {\n\t\tif clone.currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\teffectiveMaxTokens = clone.settings.GetArchitectEffectiveMaxTokens()\n\t\t} else {\n\t\t\teffectiveMaxTokens = clone.settings.GetPlannerEffectiveMaxTokens()\n\t\t}\n\t} else if clone.currentStage.TellStage == shared.TellStageImplementation {\n\t\teffectiveMaxTokens = clone.settings.GetCoderEffectiveMaxTokens()\n\t}\n\n\tif clone.tokensBeforeConvo > effectiveMaxTokens {\n\t\tlog.Println(\"tokensBeforeConvo exceeds max tokens during dry run\")\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"tokensBeforeConvo exceeds max tokens during dry run\"))\n\n\t\tstate.activePlan.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Max tokens exceeded before adding conversation\",\n\t\t}\n\t\treturn false, 0\n\t}\n\n\tif !clone.addConversationMessages() {\n\t\treturn false, 0\n\t}\n\n\tclone.messages = append(clone.messages, *promptMessage)\n\n\treturn true, model.GetMessagesTokenEstimate(clone.messages...) + model.TokensPerRequest\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_load.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/jmoiron/sqlx\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (state *activeTellStreamState) loadTellPlan() error {\n\tclients := state.clients\n\tauthVars := state.authVars\n\treq := state.req\n\tauth := state.auth\n\tplan := state.plan\n\tplanId := plan.Id\n\tbranch := state.branch\n\tcurrentUserId := state.currentUserId\n\tcurrentOrgId := state.currentOrgId\n\titeration := state.iteration\n\tmissingFileResponse := state.missingFileResponse\n\n\terr := state.setActivePlan()\n\tif err != nil {\n\t\treturn err\n\t}\n\tactive := state.activePlan\n\n\tlockScope := db.LockScopeWrite\n\tif iteration > 0 || missingFileResponse != \"\" {\n\t\tlockScope = db.LockScopeRead\n\t}\n\n\tvar modelContext []*db.Context\n\tvar convo []*db.ConvoMessage\n\tvar promptMsg *db.ConvoMessage\n\tvar summaries []*db.ConvoSummary\n\tvar subtasks []*db.Subtask\n\tvar settings *shared.PlanSettings\n\tvar orgUserConfig *shared.OrgUserConfig\n\tvar latestSummaryTokens int\n\tvar currentPlan *shared.CurrentPlanState\n\n\tlog.Printf(\"[TellLoad] Tell plan - loadTellPlan - iteration: %d, missingFileResponse: %s, req.IsUserContinue: %t, lockScope: %s\\n\", iteration, missingFileResponse, req.IsUserContinue, lockScope)\n\n\tdb.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    auth.OrgId,\n\t\tUserId:   auth.User.Id,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tScope:    lockScope,\n\t\tCtx:      active.Ctx,\n\t\tCancelFn: active.CancelFn,\n\t\tReason:   \"load tell plan\",\n\t}, func(repo *db.GitRepo) error {\n\t\terrCh := make(chan error, 4)\n\n\t\t// get name for plan and rename if it's a draft\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanSettings: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"panic getting plan settings: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tres, err := db.GetPlanSettings(plan)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan settings: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan settings: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsettings = res\n\n\t\t\torgUserConfigRes, err := db.GetOrgUserConfig(auth.User.Id, auth.OrgId)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting org user config: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting org user config: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\torgUserConfig = orgUserConfigRes\n\n\t\t\tif plan.Name == \"draft\" {\n\t\t\t\tname, err := model.GenPlanName(\n\t\t\t\t\tauth,\n\t\t\t\t\tplan,\n\t\t\t\t\tsettings,\n\t\t\t\t\torgUserConfig,\n\t\t\t\t\tclients,\n\t\t\t\t\tauthVars,\n\t\t\t\t\treq.Prompt,\n\t\t\t\t\tactive.SessionId,\n\t\t\t\t\tactive.Ctx,\n\t\t\t\t)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error generating plan name: %v\\n\", err)\n\t\t\t\t\terrCh <- fmt.Errorf(\"error generating plan name: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\terr = db.WithTx(active.Ctx, \"rename plan\", func(tx *sqlx.Tx) error {\n\t\t\t\t\terr := db.RenamePlan(planId, name, tx)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"Error renaming plan: %v\\n\", err)\n\t\t\t\t\t\treturn fmt.Errorf(\"error renaming plan: %v\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\terr = db.IncNumNonDraftPlans(currentUserId, tx)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"Error incrementing num non draft plans: %v\\n\", err)\n\t\t\t\t\t\treturn fmt.Errorf(\"error incrementing num non draft plans: %v\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error renaming plan: %v\\n\", err)\n\t\t\t\t\terrCh <- fmt.Errorf(\"error renaming plan: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanContexts: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan modelContext: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tif iteration > 0 || missingFileResponse != \"\" {\n\t\t\t\tmodelContext = active.Contexts\n\t\t\t} else {\n\t\t\t\tres, err := db.GetPlanContexts(currentOrgId, planId, true, false)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error getting plan modelContext: %v\\n\", err)\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan modelContext: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tlog.Printf(\"[TellLoad] Tell plan - loadTellPlan - modelContext: %v\\n\", len(modelContext))\n\t\t\t\t// for _, part := range modelContext {\n\t\t\t\t// \tlog.Printf(\"[TellLoad] Tell plan - loadTellPlan - part: %s - %s - %s - %d tokens\\n\", part.ContextType, part.Name, part.FilePath, part.NumTokens)\n\t\t\t\t// }\n\n\t\t\t\tmodelContext = res\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanConvo: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan convo: %v\", r)\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tres, err := db.GetPlanConvo(currentOrgId, planId)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan convo: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan convo: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconvo = res\n\t\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\t\tap.MessageNum = len(convo)\n\t\t\t})\n\n\t\t\tpromptTokens := shared.GetNumTokensEstimate(req.Prompt)\n\t\t\tinnerErrCh := make(chan error, 2)\n\n\t\t\tgo func() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"panic in storeUserMessage: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\tinnerErrCh <- fmt.Errorf(\"error storing user message: %v\", r)\n\t\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tif iteration == 0 && missingFileResponse == \"\" && !req.IsUserContinue {\n\t\t\t\t\tnum := len(convo) + 1\n\n\t\t\t\t\tlog.Printf(\"[TellLoad] storing user message | len(convo): %d | num: %d\\n\", len(convo), num)\n\n\t\t\t\t\tpromptMsg = &db.ConvoMessage{\n\t\t\t\t\t\tOrgId:   currentOrgId,\n\t\t\t\t\t\tPlanId:  planId,\n\t\t\t\t\t\tUserId:  currentUserId,\n\t\t\t\t\t\tRole:    openai.ChatMessageRoleUser,\n\t\t\t\t\t\tTokens:  promptTokens,\n\t\t\t\t\t\tNum:     num,\n\t\t\t\t\t\tMessage: req.Prompt,\n\t\t\t\t\t\tFlags: shared.ConvoMessageFlags{\n\t\t\t\t\t\t\tIsApplyDebug: req.IsApplyDebug,\n\t\t\t\t\t\t\tIsUserDebug:  req.IsUserDebug,\n\t\t\t\t\t\t\tIsChat:       req.IsChatOnly,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\n\t\t\t\t\tlog.Println(\"[TellLoad] storing user message\")\n\t\t\t\t\t// repo.LogGitRepoState()\n\n\t\t\t\t\t_, err = db.StoreConvoMessage(repo, promptMsg, auth.User.Id, branch, true)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"[TellLoad] Error storing user message: %v\\n\", err)\n\t\t\t\t\t\tinnerErrCh <- fmt.Errorf(\"error storing user message: %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\t\t\t\tap.MessageNum = num\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tinnerErrCh <- nil\n\t\t\t}()\n\n\t\t\tgo func() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"panic in getPlanSummaries: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\t\tinnerErrCh <- fmt.Errorf(\"error getting plan summaries: %v\", r)\n\t\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tvar convoMessageIds []string\n\n\t\t\t\tfor _, convoMessage := range convo {\n\t\t\t\t\tconvoMessageIds = append(convoMessageIds, convoMessage.Id)\n\t\t\t\t}\n\n\t\t\t\tlog.Println(\"getting plan summaries\")\n\t\t\t\tlog.Println(\"convoMessageIds:\", convoMessageIds)\n\n\t\t\t\tres, err := db.GetPlanSummaries(planId, convoMessageIds)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error getting plan summaries: %v\\n\", err)\n\t\t\t\t\tinnerErrCh <- fmt.Errorf(\"error getting plan summaries: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tsummaries = res\n\n\t\t\t\tlog.Printf(\"got %d plan summaries\", len(summaries))\n\n\t\t\t\tif len(summaries) > 0 {\n\t\t\t\t\tlatestSummaryTokens = shared.GetNumTokensEstimate(summaries[len(summaries)-1].Summary)\n\t\t\t\t}\n\n\t\t\t\tinnerErrCh <- nil\n\t\t\t}()\n\n\t\t\tfor i := 0; i < 2; i++ {\n\t\t\t\terr := <-innerErrCh\n\t\t\t\tif err != nil {\n\t\t\t\t\terrCh <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif promptMsg != nil {\n\t\t\t\tconvo = append(convo, promptMsg)\n\t\t\t}\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in getPlanSubtasks: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- fmt.Errorf(\"error getting plan subtasks: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\truntime.Goexit() // don't allow outer function to continue and double-send to channel\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tres, err := db.GetPlanSubtasks(auth.OrgId, planId)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error getting plan subtasks: %v\\n\", err)\n\t\t\t\terrCh <- fmt.Errorf(\"error getting plan subtasks: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsubtasks = res\n\t\t\terrCh <- nil\n\t\t}()\n\n\t\tfor i := 0; i < 4; i++ {\n\t\t\terr = <-errCh\n\t\t\tif err != nil {\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error loading plan: %v\", err))\n\n\t\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\t\tMsg:    fmt.Sprintf(\"Error loading plan: %v\", err),\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tres, err := db.GetCurrentPlanState(db.CurrentPlanStateParams{\n\t\t\tOrgId:    currentOrgId,\n\t\t\tPlanId:   planId,\n\t\t\tContexts: modelContext,\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting current plan state: %v\", err)\n\t\t}\n\n\t\tcurrentPlan = res\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"execTellPlan: error loading tell plan: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error loading tell plan: %v\", err))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"Error loading tell plan: %v\", err),\n\t\t}\n\t\treturn err\n\t}\n\n\tstate.modelContext = modelContext\n\tstate.convo = convo\n\tstate.promptConvoMessage = promptMsg\n\tstate.summaries = summaries\n\tstate.latestSummaryTokens = latestSummaryTokens\n\tstate.settings = settings\n\tstate.currentPlanState = currentPlan\n\tstate.subtasks = subtasks\n\n\tfor _, subtask := range state.subtasks {\n\t\tif !subtask.IsFinished {\n\t\t\tstate.currentSubtask = subtask\n\t\t\tbreak\n\t\t}\n\t}\n\n\tlog.Printf(\"[TellLoad] Subtasks: %+v\", state.subtasks)\n\tlog.Printf(\"[TellLoad] Current subtask: %+v\", state.currentSubtask)\n\n\tstate.hasContextMap = false\n\tstate.contextMapEmpty = true\n\tfor _, context := range state.modelContext {\n\t\tif context.ContextType == shared.ContextMapType {\n\t\t\tstate.hasContextMap = true\n\t\t\tif context.NumTokens > 0 {\n\t\t\t\tstate.contextMapEmpty = false\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tstate.hasAssistantReply = false\n\tfor _, convoMessage := range state.convo {\n\t\tif convoMessage.Role == openai.ChatMessageRoleAssistant {\n\t\t\tstate.hasAssistantReply = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif iteration == 0 && missingFileResponse == \"\" {\n\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\tap.Contexts = state.modelContext\n\n\t\t\tfor _, context := range state.modelContext {\n\t\t\t\tif context.FilePath != \"\" {\n\t\t\t\t\tap.ContextsByPath[context.FilePath] = context\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t} else if missingFileResponse == \"\" {\n\t\t// reset current reply content and num tokens\n\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\tap.CurrentReplyContent = \"\"\n\t\t\tap.NumTokens = 0\n\t\t})\n\t}\n\n\t// if any skipped paths have since been added to context, remove them from skipped paths\n\tif len(active.SkippedPaths) > 0 {\n\t\tvar toUnskipPaths []string\n\t\tfor contextPath := range active.ContextsByPath {\n\t\t\tif active.SkippedPaths[contextPath] {\n\t\t\t\ttoUnskipPaths = append(toUnskipPaths, contextPath)\n\t\t\t}\n\t\t}\n\t\tif len(toUnskipPaths) > 0 {\n\t\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\t\tfor _, path := range toUnskipPaths {\n\t\t\t\t\tdelete(ap.SkippedPaths, path)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (state *activeTellStreamState) setActivePlan() error {\n\tplan := state.plan\n\tbranch := state.branch\n\n\tactive := GetActivePlan(plan.Id, branch)\n\n\tif active == nil {\n\t\treturn fmt.Errorf(\"no active plan with id %s\", plan.Id)\n\t}\n\n\tstate.activePlan = active\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_missing_file.go",
    "content": "package plan\n\nimport (\n\t\"log\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/types\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (state *activeTellStreamState) handleMissingFileResponse(unfinishedSubtaskReasoning string) bool {\n\tmissingFileResponse := state.missingFileResponse\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\treq := state.req\n\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\"execTellPlan: Active plan not found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn false\n\t}\n\n\tlog.Println(\"Missing file response:\", missingFileResponse, \"setting replyParser\")\n\t// log.Printf(\"Current reply content:\\n%s\\n\", active.CurrentReplyContent)\n\n\tstate.replyParser.AddChunk(active.CurrentReplyContent, true)\n\tres := state.replyParser.Read()\n\tcurrentFile := res.CurrentFilePath\n\n\tlog.Printf(\"Current file: %s\\n\", currentFile)\n\t// log.Println(\"Current reply content:\\n\", active.CurrentReplyContent)\n\n\treplyContent := active.CurrentReplyContent\n\tnumTokens := active.NumTokens\n\n\tif missingFileResponse == shared.RespondMissingFileChoiceSkip {\n\t\treplyBeforeCurrentFile := state.replyParser.GetReplyBeforeCurrentPath()\n\t\tnumTokens = shared.GetNumTokensEstimate(replyBeforeCurrentFile)\n\n\t\treplyContent = replyBeforeCurrentFile\n\t\tstate.replyParser = types.NewReplyParser()\n\t\tstate.replyParser.AddChunk(replyContent, true)\n\n\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\tap.CurrentReplyContent = replyContent\n\t\t\tap.NumTokens = numTokens\n\t\t\tap.SkippedPaths[currentFile] = true\n\t\t})\n\n\t} else {\n\t\tif missingFileResponse == shared.RespondMissingFileChoiceOverwrite {\n\t\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\t\tap.AllowOverwritePaths[currentFile] = true\n\t\t\t})\n\t\t}\n\t}\n\n\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\tRole: openai.ChatMessageRoleAssistant,\n\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t{\n\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\tText: active.CurrentReplyContent,\n\t\t\t},\n\t\t},\n\t})\n\n\tif missingFileResponse == shared.RespondMissingFileChoiceSkip {\n\t\tres := state.replyParser.FinishAndRead()\n\t\tskipPrompt := prompts.GetSkipMissingFilePrompt(res.CurrentFilePath)\n\n\t\tparams := prompts.UserPromptParams{\n\t\t\tCreatePromptParams: prompts.CreatePromptParams{\n\t\t\t\tExecMode:          req.ExecEnabled,\n\t\t\t\tAutoContext:       req.AutoContext,\n\t\t\t\tIsUserDebug:       req.IsUserDebug,\n\t\t\t\tIsApplyDebug:      req.IsApplyDebug,\n\t\t\t\tContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),\n\t\t\t},\n\t\t\tPrompt:                     skipPrompt,\n\t\t\tOsDetails:                  req.OsDetails,\n\t\t\tCurrentStage:               state.currentStage,\n\t\t\tUnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,\n\t\t}\n\n\t\tprompt := prompts.GetWrappedPrompt(params) + \"\\n\\n\" + skipPrompt // repetition of skip prompt to improve instruction following\n\n\t\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t} else {\n\t\tmissingPrompt := prompts.GetMissingFileContinueGeneratingPrompt(res.CurrentFilePath)\n\n\t\tparams := prompts.UserPromptParams{\n\t\t\tCreatePromptParams: prompts.CreatePromptParams{\n\t\t\t\tExecMode:          req.ExecEnabled,\n\t\t\t\tAutoContext:       req.AutoContext,\n\t\t\t\tIsUserDebug:       req.IsUserDebug,\n\t\t\t\tIsApplyDebug:      req.IsApplyDebug,\n\t\t\t\tContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),\n\t\t\t},\n\t\t\tPrompt:                     missingPrompt,\n\t\t\tOsDetails:                  req.OsDetails,\n\t\t\tCurrentStage:               state.currentStage,\n\t\t\tUnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,\n\t\t}\n\n\t\tprompt := prompts.GetWrappedPrompt(params) + \"\\n\\n\" + missingPrompt // repetition of missing prompt to improve instruction following\n\n\t\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_prompt_message.go",
    "content": "package plan\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (state *activeTellStreamState) resolvePromptMessage(\n\tunfinishedSubtaskReasoning string,\n) (*types.ExtendedChatMessage, bool) {\n\treq := state.req\n\tactive := state.activePlan\n\titeration := state.iteration\n\n\tvar promptMessage *types.ExtendedChatMessage\n\n\tstate.skipConvoMessages = map[string]bool{}\n\n\tlastMessage := state.lastSuccessfulConvoMessage()\n\n\tif req.IsUserContinue {\n\t\tif lastMessage == nil {\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeContinueNoMessages,\n\t\t\t\tStatus: http.StatusBadRequest,\n\t\t\t\tMsg:    \"No messages yet. Can't continue plan.\",\n\t\t\t}\n\t\t\treturn nil, false\n\t\t}\n\t\tlog.Println(\"User is continuing plan. Last message role:\", lastMessage.Role)\n\t}\n\n\tif req.IsChatOnly {\n\t\tvar prompt string\n\t\tif req.IsUserContinue {\n\t\t\tif lastMessage.Role == openai.ChatMessageRoleUser {\n\t\t\t\tlog.Println(\"User is continuing plan in chat only mode. Last message was user message. Using last user message as prompt\")\n\t\t\t\tcontent := lastMessage.Message\n\t\t\t\tprompt = content\n\t\t\t\tstate.userPrompt = content\n\t\t\t\tstate.skipConvoMessages[lastMessage.Id] = true\n\t\t\t} else {\n\t\t\t\tlog.Println(\"User is continuing plan in chat only mode. Last message was assistant message. Using user continue prompt\")\n\t\t\t\tprompt = prompts.UserContinuePrompt\n\t\t\t}\n\t\t} else {\n\t\t\tprompt = req.Prompt\n\t\t}\n\n\t\twrapped := prompts.GetWrappedChatOnlyPrompt(prompts.ChatUserPromptParams{\n\t\t\tCreatePromptParams: prompts.CreatePromptParams{\n\t\t\t\tAutoContext: req.AutoContext,\n\t\t\t\tExecMode:    req.ExecEnabled,\n\t\t\t\tIsGitRepo:   req.IsGitRepo,\n\t\t\t\t// no need to pass in IsUserDebug or IsApplyDebug here because it's a chat message\n\t\t\t},\n\t\t\tPrompt:    prompt,\n\t\t\tOsDetails: req.OsDetails,\n\t\t\t// no current task for chat only mode\n\t\t})\n\n\t\tpromptMessage = &types.ExtendedChatMessage{\n\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: wrapped,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t} else if req.IsUserContinue {\n\t\t// log.Println(\"User is continuing plan. Last message:\\n\\n\", lastMessage.Content)\n\t\tif lastMessage.Role == openai.ChatMessageRoleUser {\n\t\t\t// 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\n\n\t\t\tlog.Println(\"User is continuing plan in tell mode. Last message was user message. Using last user message as prompt\")\n\t\t\tcontent := lastMessage.Message\n\n\t\t\tparams := prompts.UserPromptParams{\n\t\t\t\tCreatePromptParams: prompts.CreatePromptParams{\n\t\t\t\t\tExecMode:          req.ExecEnabled,\n\t\t\t\t\tAutoContext:       req.AutoContext,\n\t\t\t\t\tIsGitRepo:         req.IsGitRepo,\n\t\t\t\t\tContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),\n\t\t\t\t\t// no need to pass in IsUserDebug or IsApplyDebug here because we're continuing\n\t\t\t\t},\n\t\t\t\tPrompt:                     content,\n\t\t\t\tOsDetails:                  req.OsDetails,\n\t\t\t\tCurrentStage:               state.currentStage,\n\t\t\t\tUnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,\n\t\t\t}\n\n\t\t\tpromptMessage = &types.ExtendedChatMessage{\n\t\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\tText: prompts.GetWrappedPrompt(params),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tstate.userPrompt = content\n\t\t} else {\n\n\t\t\t// if the last message was an assistant message, we'll use the user continue prompt\n\t\t\tlog.Println(\"User is continuing plan in tell mode. Last message was assistant message. Using user continue prompt\")\n\n\t\t\tparams := prompts.UserPromptParams{\n\t\t\t\tCreatePromptParams: prompts.CreatePromptParams{\n\t\t\t\t\tExecMode:          req.ExecEnabled,\n\t\t\t\t\tAutoContext:       req.AutoContext,\n\t\t\t\t\tIsGitRepo:         req.IsGitRepo,\n\t\t\t\t\tContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),\n\t\t\t\t\t// no need to pass in IsUserDebug or IsApplyDebug here because we're continuing\n\t\t\t\t},\n\t\t\t\tPrompt:                     prompts.UserContinuePrompt,\n\t\t\t\tOsDetails:                  req.OsDetails,\n\t\t\t\tCurrentStage:               state.currentStage,\n\t\t\t\tUnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,\n\t\t\t}\n\n\t\t\tpromptMessage = &types.ExtendedChatMessage{\n\t\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\tText: prompts.GetWrappedPrompt(params),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t} else {\n\t\tvar prompt string\n\t\tif iteration == 0 {\n\t\t\tprompt = req.Prompt\n\t\t} else if state.currentStage.TellStage == shared.TellStageImplementation {\n\t\t\tprompt = prompts.AutoContinueImplementationPrompt\n\t\t} else {\n\t\t\tprompt = prompts.AutoContinuePlanningPrompt\n\t\t}\n\n\t\tstate.userPrompt = prompt\n\n\t\tparams := prompts.UserPromptParams{\n\t\t\tCreatePromptParams: prompts.CreatePromptParams{\n\t\t\t\tExecMode:          req.ExecEnabled,\n\t\t\t\tAutoContext:       req.AutoContext,\n\t\t\t\tIsUserDebug:       req.IsUserDebug,\n\t\t\t\tIsApplyDebug:      req.IsApplyDebug,\n\t\t\t\tIsGitRepo:         req.IsGitRepo,\n\t\t\t\tContextTokenLimit: state.settings.GetPlannerEffectiveMaxTokens(),\n\t\t\t},\n\t\t\tPrompt:                     prompt,\n\t\t\tOsDetails:                  req.OsDetails,\n\t\t\tCurrentStage:               state.currentStage,\n\t\t\tUnfinishedSubtaskReasoning: unfinishedSubtaskReasoning,\n\t\t}\n\n\t\tfinalPrompt := prompts.GetWrappedPrompt(params)\n\n\t\t// log.Println(\"Final prompt:\", finalPrompt)\n\n\t\tpromptMessage = &types.ExtendedChatMessage{\n\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: finalPrompt,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\t// log.Println(\"Prompt message:\", promptMessage.Content)\n\n\treturn promptMessage, true\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stage.go",
    "content": "package plan\n\nimport (\n\t\"log\"\n\t\"plandex-server/db\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (state *activeTellStreamState) lastSuccessfulConvoMessage() *db.ConvoMessage {\n\tfor i := len(state.convo) - 1; i >= 0; i-- {\n\t\tmsg := state.convo[i]\n\t\tif msg.Stopped || msg.Flags.HasError {\n\t\t\tcontinue\n\t\t}\n\t\treturn msg\n\t}\n\treturn nil\n}\n\nfunc (state *activeTellStreamState) resolveCurrentStage() (activatePaths map[string]bool, activatePathsOrdered []string) {\n\treq := state.req\n\titeration := state.iteration\n\thasContextMap := state.hasContextMap\n\tconvo := state.convo\n\tcontextMapEmpty := state.contextMapEmpty\n\n\tlog.Printf(\"[resolveCurrentStage] Initial state: hasContextMap: %v, convo len: %d\", hasContextMap, len(convo))\n\n\tlastConvoMsg := state.lastSuccessfulConvoMessage()\n\n\tactivatePaths = map[string]bool{}\n\tactivatePathsOrdered = []string{}\n\n\tisContinueFromAssistantMsg := false\n\n\tif lastConvoMsg != nil {\n\t\tisContinueFromAssistantMsg = iteration == 0 && req.IsUserContinue && lastConvoMsg.Role == openai.ChatMessageRoleAssistant\n\t\tlog.Printf(\"[resolveCurrentStage] isContinueFromAssistantMsg: %v (IsUserContinue: %v, LastMsgRole: %s)\",\n\t\t\tisContinueFromAssistantMsg, req.IsUserContinue, lastConvoMsg.Role)\n\t} else {\n\t\tlog.Println(\"[resolveCurrentStage] No previous successful conversation message found\")\n\t}\n\n\tisUserPrompt := false\n\n\tif !isContinueFromAssistantMsg {\n\t\tisUserPrompt = lastConvoMsg == nil || lastConvoMsg.Role == openai.ChatMessageRoleUser\n\t\tlog.Printf(\"[resolveCurrentStage] isUserPrompt: %v\", isUserPrompt)\n\t}\n\n\tvar tellStage shared.TellStage\n\tvar planningPhase shared.PlanningPhase\n\n\tif isUserPrompt {\n\t\ttellStage = shared.TellStagePlanning\n\t\tlog.Println(\"[resolveCurrentStage] Set tellStage to Planning due to user prompt\")\n\t} else {\n\t\tif lastConvoMsg != nil && lastConvoMsg.Flags.DidMakePlan {\n\t\t\ttellStage = shared.TellStageImplementation\n\t\t\tlog.Println(\"[resolveCurrentStage] Set tellStage to Implementation - DidMakePlan: true, IsChatOnly: false\")\n\t\t} else if lastConvoMsg != nil && lastConvoMsg.Flags.CurrentStage.TellStage == shared.TellStageImplementation {\n\t\t\ttellStage = shared.TellStageImplementation\n\t\t\tlog.Println(\"[resolveCurrentStage] Set tellStage to Implementation - CurrentStage: implementation\")\n\t\t} else {\n\t\t\ttellStage = shared.TellStagePlanning\n\t\t\tlog.Printf(\"[resolveCurrentStage] Set tellStage to Planning - DidMakePlan: %v, IsChatOnly: %v\",\n\t\t\t\tlastConvoMsg != nil && lastConvoMsg.Flags.DidMakePlan, req.IsChatOnly)\n\t\t}\n\t}\n\n\twasContextStage := false\n\tif lastConvoMsg != nil {\n\t\tflags := lastConvoMsg.Flags\n\t\tlog.Printf(\"[resolveCurrentStage] Last convo message flags: %+v\", flags)\n\t\tif flags.CurrentStage.TellStage == shared.TellStagePlanning && flags.CurrentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\twasContextStage = true\n\t\t\tactivatePaths = lastConvoMsg.ActivatedPaths\n\t\t\tactivatePathsOrdered = lastConvoMsg.ActivatedPathsOrdered\n\t\t\tlog.Printf(\"[resolveCurrentStage] Was context stage, copied activatePaths: %v\", activatePaths)\n\t\t}\n\t}\n\n\tif tellStage == shared.TellStagePlanning {\n\t\tif req.AutoContext && hasContextMap && !contextMapEmpty && !wasContextStage {\n\t\t\tplanningPhase = shared.PlanningPhaseContext\n\t\t\tlog.Printf(\"[resolveCurrentStage] Set planningPhase to Context - AutoContext: %v, hasContextMap: %v, contextMapEmpty: %v, wasContextStage: %v\",\n\t\t\t\treq.AutoContext, hasContextMap, contextMapEmpty, wasContextStage)\n\t\t} else {\n\t\t\tplanningPhase = shared.PlanningPhaseTasks\n\t\t\tlog.Printf(\"[resolveCurrentStage] Set planningPhase to Tasks - AutoContext: %v, hasContextMap: %v, contextMapEmpty: %v, wasContextStage: %v\",\n\t\t\t\treq.AutoContext, hasContextMap, contextMapEmpty, wasContextStage)\n\t\t}\n\t}\n\n\tstate.currentStage = shared.CurrentStage{\n\t\tTellStage:     tellStage,\n\t\tPlanningPhase: planningPhase,\n\t}\n\tlog.Printf(\"[resolveCurrentStage] Final state - TellStage: %s, PlanningPhase: %s\", tellStage, planningPhase)\n\n\treturn activatePaths, activatePathsOrdered\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_state.go",
    "content": "package plan\n\nimport (\n\t\"plandex-server/db\"\n\t\"plandex-server/model\"\n\t\"plandex-server/types\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype activeTellStreamState struct {\n\tactivePlan            *types.ActivePlan\n\tmodelStreamId         string\n\tclients               map[string]model.ClientInfo\n\tauthVars              map[string]string\n\treq                   *shared.TellPlanRequest\n\tauth                  *types.ServerAuth\n\tcurrentOrgId          string\n\tcurrentUserId         string\n\torgUserConfig         *shared.OrgUserConfig\n\tplan                  *db.Plan\n\tbranch                string\n\titeration             int\n\treplyId               string\n\tmodelContext          []*db.Context\n\thasContextMap         bool\n\tcontextMapEmpty       bool\n\tconvo                 []*db.ConvoMessage\n\tpromptConvoMessage    *db.ConvoMessage\n\tcurrentPlanState      *shared.CurrentPlanState\n\tmissingFileResponse   shared.RespondMissingFileChoice\n\tsummaries             []*db.ConvoSummary\n\tsummarizedToMessageId string\n\tlatestSummaryTokens   int\n\tuserPrompt            string\n\tpromptMessage         *openai.ChatCompletionMessage\n\treplyParser           *types.ReplyParser\n\treplyNumTokens        int\n\tmessages              []types.ExtendedChatMessage\n\ttokensBeforeConvo     int\n\ttotalRequestTokens    int\n\tsettings              *shared.PlanSettings\n\tsubtasks              []*db.Subtask\n\tcurrentSubtask        *db.Subtask\n\thasAssistantReply     bool\n\tcurrentStage          shared.CurrentStage\n\tchunkProcessor        *chunkProcessor\n\tgenerationId          string\n\n\trequestStartedAt time.Time\n\tfirstTokenAt     time.Time\n\toriginalReq      *types.ExtendedChatCompletionRequest\n\tmodelConfig      *shared.ModelRoleConfig\n\tbaseModelConfig  *shared.BaseModelConfig\n\tfallbackRes      shared.FallbackResult\n\n\tskipConvoMessages map[string]bool\n\n\tmanualStop []string\n\n\tnumErrorRetry       int\n\tnumFallbackRetry    int\n\tmodelErr            *shared.ModelError\n\tnoCacheSupportErr   bool\n\tdidProviderFallback bool\n}\n\ntype chunkProcessor struct {\n\treplyOperations                 []*shared.Operation\n\tchunksReceived                  int\n\tmaybeRedundantOpeningTagContent string\n\tfileOpen                        bool\n\tcontentBuffer                   string\n\tawaitingBlockOpeningTag         bool\n\tawaitingBlockClosingTag         bool\n\tawaitingOpClosingTag            bool\n\tawaitingBackticks               bool\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_error.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/shutdown\"\n\t\"strconv\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\ntype onErrorParams struct {\n\tstreamErr      error\n\tstreamApiErr   *shared.ApiError\n\tstoreDesc      bool\n\tconvoMessageId string\n\tcommitMsg      string\n\tcanRetry       bool\n\tmodelErr       *shared.ModelError\n}\n\ntype onErrorResult struct {\n\tshouldContinueMainLoop bool\n\tshouldReturn           bool\n}\n\nfunc (state *activeTellStreamState) onError(params onErrorParams) onErrorResult {\n\tlog.Printf(\"\\nStream error: %v\\n\", params.streamErr)\n\tstreamErr := params.streamErr\n\tstoreDesc := params.storeDesc\n\tconvoMessageId := params.convoMessageId\n\tcommitMsg := params.commitMsg\n\tmodelErr := params.modelErr\n\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tcurrentOrgId := state.currentOrgId\n\tsummarizedToMessageId := state.summarizedToMessageId\n\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\"tellStream onError - Active plan not found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn onErrorResult{\n\t\t\tshouldReturn: true,\n\t\t}\n\t}\n\n\tcanRetry := params.canRetry\n\tisFallback := state.fallbackRes.IsFallback\n\n\tmaxRetries := model.MAX_RETRIES_WITHOUT_FALLBACK\n\tif isFallback {\n\t\tmaxRetries = model.MAX_ADDITIONAL_RETRIES_WITH_FALLBACK\n\t}\n\n\tcompareRetries := state.numErrorRetry\n\tif isFallback {\n\t\tcompareRetries = state.numFallbackRetry\n\t}\n\n\tpotentialFallback := state.modelConfig.GetFallbackForModelError(\n\t\tstate.numErrorRetry,\n\t\tstate.didProviderFallback,\n\t\tmodelErr,\n\t\tstate.authVars,\n\t\tstate.settings,\n\t\tstate.orgUserConfig,\n\t)\n\n\tnewFallback := false\n\tif modelErr != nil {\n\t\tif !modelErr.Retriable {\n\t\t\tlog.Printf(\"tellStream onError - operation returned non-retriable error: %v\", modelErr)\n\t\t\tif !potentialFallback.IsFallback {\n\t\t\t\tcanRetry = false\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"tellStream onError - operation returned non-retriable error, but has fallback - resetting numFallbackRetry to 0 and continuing to retry\")\n\t\t\t\tstate.numFallbackRetry = 0\n\t\t\t\t// otherwise, continue to retry logic\n\t\t\t\tcanRetry = true\n\t\t\t\tnewFallback = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif canRetry {\n\t\tlog.Println(\"tellStream onError - canRetry\", canRetry)\n\n\t\tif compareRetries >= maxRetries {\n\t\t\tlog.Printf(\"tellStream onError - Max retries reached for plan ID %s on branch %s\\n\", planId, branch)\n\n\t\t\tcanRetry = false\n\t\t}\n\t}\n\n\tif canRetry {\n\t\tlog.Println(\"tellStream onError - retrying stream\")\n\t\t// stop stream via context (ensures we stop child streams too)\n\t\tactive.CancelModelStreamFn()\n\n\t\tactive.ResetModelCtx()\n\n\t\tvar retryDelay time.Duration\n\t\tif modelErr != nil && modelErr.RetryAfterSeconds > 0 {\n\t\t\t// if the model err has a retry after, then use that with a bit of padding\n\t\t\tretryDelay = time.Duration(int(float64(modelErr.RetryAfterSeconds)*1.1)) * time.Second\n\t\t} else {\n\t\t\t// otherwise, use some jitter\n\t\t\tretryDelay = time.Duration(1000+rand.Intn(200)) * time.Millisecond\n\t\t}\n\n\t\tcacheSupportErr := modelErr != nil && modelErr.Kind == shared.ErrCacheSupport\n\n\t\tnumErrorRetry := state.numErrorRetry\n\t\tif modelErr != nil && modelErr.ShouldIncrementRetry() {\n\t\t\tnumErrorRetry = numErrorRetry + 1\n\t\t}\n\n\t\tlog.Printf(\"tellStream onError - Retry %d/%d - Retrying stream in %v\", numErrorRetry, maxRetries, retryDelay)\n\t\ttime.Sleep(retryDelay)\n\n\t\tstate.numErrorRetry = numErrorRetry\n\t\tif isFallback && !newFallback && modelErr != nil && modelErr.ShouldIncrementRetry() {\n\t\t\tstate.numFallbackRetry = state.numFallbackRetry + 1\n\t\t}\n\n\t\t// 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\n\t\tif cacheSupportErr {\n\t\t\tstate.noCacheSupportErr = true\n\t\t} else {\n\t\t\tstate.modelErr = modelErr\n\n\t\t\tif newFallback {\n\t\t\t\t// if we got a new fallback, we need to reset the noCacheSupportErr flag since we're using a different model now\n\t\t\t\tstate.noCacheSupportErr = false\n\t\t\t}\n\t\t}\n\n\t\t// retry the request\n\t\tstate.doTellRequest()\n\t\treturn onErrorResult{\n\t\t\tshouldReturn: true,\n\t\t}\n\t}\n\n\tstoreDescAndReply := func() error {\n\t\tlog.Println(\"tellStream onError - storing desc and reply\")\n\t\tctx, cancelFn := context.WithTimeout(shutdown.ShutdownCtx, 5*time.Second)\n\n\t\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\t\tOrgId:    currentOrgId,\n\t\t\tUserId:   state.currentUserId,\n\t\t\tPlanId:   planId,\n\t\t\tBranch:   branch,\n\t\t\tScope:    db.LockScopeWrite,\n\t\t\tCtx:      ctx,\n\t\t\tCancelFn: cancelFn,\n\t\t\tReason:   \"store desc and reply\",\n\t\t}, func(repo *db.GitRepo) error {\n\t\t\tstoredMessage := false\n\t\t\tstoredDesc := false\n\n\t\t\tif convoMessageId == \"\" {\n\t\t\t\thasUnfinishedSubtasks := false\n\t\t\t\tfor _, subtask := range state.subtasks {\n\t\t\t\t\tif !subtask.IsFinished {\n\t\t\t\t\t\thasUnfinishedSubtasks = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassistantMsg, msg, err := state.storeAssistantReply(repo, storeAssistantReplyParams{\n\t\t\t\t\tflags: shared.ConvoMessageFlags{\n\t\t\t\t\t\tCurrentStage:          state.currentStage,\n\t\t\t\t\t\tHasUnfinishedSubtasks: hasUnfinishedSubtasks,\n\t\t\t\t\t\tHasError:              true,\n\t\t\t\t\t},\n\t\t\t\t\tsubtask:       nil,\n\t\t\t\t\taddedSubtasks: nil,\n\t\t\t\t})\n\t\t\t\tif err == nil {\n\t\t\t\t\tconvoMessageId = assistantMsg.Id\n\t\t\t\t\tcommitMsg = msg\n\t\t\t\t\tstoredMessage = true\n\t\t\t\t} else {\n\t\t\t\t\tlog.Printf(\"Error storing assistant message after stream error: %v\\n\", err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif storeDesc && convoMessageId != \"\" {\n\t\t\t\terr := db.StoreDescription(&db.ConvoMessageDescription{\n\t\t\t\t\tOrgId:                 currentOrgId,\n\t\t\t\t\tPlanId:                planId,\n\t\t\t\t\tSummarizedToMessageId: summarizedToMessageId,\n\t\t\t\t\tWroteFiles:            false,\n\t\t\t\t\tConvoMessageId:        convoMessageId,\n\t\t\t\t\tBuildPathsInvalidated: map[string]bool{},\n\t\t\t\t\tError:                 streamErr.Error(),\n\t\t\t\t})\n\t\t\t\tif err == nil {\n\t\t\t\t\tstoredDesc = true\n\t\t\t\t} else {\n\t\t\t\t\tlog.Printf(\"Error storing description after stream error: %v\\n\", err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif storedMessage || storedDesc {\n\t\t\t\terr := repo.GitAddAndCommit(branch, commitMsg)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error committing after stream error: %v\\n\", err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error storing description and reply after stream error: %v\\n\", err)\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif active.CurrentReplyContent != \"\" {\n\t\tstoreDescAndReply() // best effort to store description and reply, ignore errors\n\t}\n\n\tif params.streamApiErr != nil {\n\t\tactive.StreamDoneCh <- params.streamApiErr\n\t} else {\n\t\tmsg := \"Stream error: \" + streamErr.Error()\n\t\tif params.canRetry && state.numErrorRetry >= maxRetries {\n\t\t\tmsg += \" | Failed after \" + strconv.Itoa(state.numErrorRetry) + \" retries\"\n\t\t}\n\n\t\tgo notify.NotifyErr(notify.SeverityInfo, fmt.Sprintf(\"tellStream stream error after %d retries: %v\", state.numErrorRetry, streamErr))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    msg,\n\t\t}\n\t}\n\n\treturn onErrorResult{\n\t\tshouldContinueMainLoop: true,\n\t}\n}\n\nfunc (state *activeTellStreamState) onActivePlanMissingError() {\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tlog.Printf(\"Active plan not found for plan ID %s on branch %s\\n\", planId, branch)\n\tstate.onError(onErrorParams{\n\t\tstreamErr: fmt.Errorf(\"active plan not found for plan ID %s on branch %s\", planId, branch),\n\t\tstoreDesc: true,\n\t})\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_finish.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\nconst MaxAutoContinueIterations = 200\n\ntype handleStreamFinishedResult struct {\n\tshouldContinueMainLoop bool\n\tshouldReturn           bool\n}\n\nfunc (state *activeTellStreamState) handleStreamFinished() handleStreamFinishedResult {\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tauth := state.auth\n\tplan := state.plan\n\treq := state.req\n\tclients := state.clients\n\tauthVars := state.authVars\n\tsettings := state.settings\n\torgUserConfig := state.orgUserConfig\n\tcurrentOrgId := state.currentOrgId\n\tsummaries := state.summaries\n\tconvo := state.convo\n\titeration := state.iteration\n\treplyOperations := state.chunkProcessor.replyOperations\n\n\terr := state.setActivePlan()\n\tif err != nil {\n\t\tstate.onActivePlanMissingError()\n\t\treturn handleStreamFinishedResult{\n\t\t\tshouldContinueMainLoop: true,\n\t\t\tshouldReturn:           false,\n\t\t}\n\t}\n\n\tactive := state.activePlan\n\n\ttime.Sleep(30 * time.Millisecond)\n\tactive.FlushStreamBuffer()\n\ttime.Sleep(100 * time.Millisecond)\n\n\tactive.Stream(shared.StreamMessage{\n\t\tType: shared.StreamMessageDescribing,\n\t})\n\tactive.FlushStreamBuffer()\n\n\terr = db.SetPlanStatus(planId, branch, shared.PlanStatusDescribing, \"\")\n\tif err != nil {\n\t\tres := state.onError(onErrorParams{\n\t\t\tstreamErr: fmt.Errorf(\"failed to set plan status to describing: %v\", err),\n\t\t\tstoreDesc: true,\n\t\t})\n\n\t\treturn handleStreamFinishedResult{\n\t\t\tshouldContinueMainLoop: res.shouldContinueMainLoop,\n\t\t\tshouldReturn:           res.shouldReturn,\n\t\t}\n\t}\n\n\tautoLoadContextResult := state.checkAutoLoadContext()\n\tcheckNewSubtasksResult := state.checkNewSubtasks()\n\n\thasExplicitTasks := checkNewSubtasksResult.hasExplicitTasks\n\taddedSubtasks := checkNewSubtasksResult.newSubtasks\n\n\tcheckRemoveSubtasksResult := state.checkRemoveSubtasks()\n\n\tremovedSubtasks := checkRemoveSubtasksResult.removedSubtasks\n\thasExplicitRemoveTasks := checkRemoveSubtasksResult.hasExplicitRemoveTasks\n\n\tlog.Println(\"removedSubtasks:\\n\", spew.Sdump(removedSubtasks))\n\tlog.Println(\"addedSubtasks:\\n\", spew.Sdump(addedSubtasks))\n\tlog.Println(\"hasNewSubtasks:\\n\", hasExplicitTasks)\n\n\thandleDescAndExecStatusRes := state.handleDescAndExecStatus()\n\tif handleDescAndExecStatusRes.shouldContinueMainLoop || handleDescAndExecStatusRes.shouldReturn {\n\t\treturn handleDescAndExecStatusRes.handleStreamFinishedResult\n\t}\n\tgeneratedDescription := handleDescAndExecStatusRes.generatedDescription\n\tsubtaskFinished := handleDescAndExecStatusRes.subtaskFinished\n\n\tlog.Printf(\"subtaskFinished: %v\\n\", subtaskFinished)\n\n\tstoreOnFinishedResult := state.storeOnFinished(storeOnFinishedParams{\n\t\treplyOperations:       replyOperations,\n\t\tgeneratedDescription:  generatedDescription,\n\t\tsubtaskFinished:       subtaskFinished,\n\t\thasNewSubtasks:        hasExplicitTasks,\n\t\tautoLoadContextResult: autoLoadContextResult,\n\t\taddedSubtasks:         addedSubtasks,\n\t\tremovedSubtasks:       removedSubtasks,\n\t})\n\tif storeOnFinishedResult.shouldContinueMainLoop || storeOnFinishedResult.shouldReturn {\n\t\treturn storeOnFinishedResult.handleStreamFinishedResult\n\t}\n\tallSubtasksFinished := storeOnFinishedResult.allSubtasksFinished\n\n\tlog.Println(\"allSubtasksFinished:\\n\", spew.Sdump(allSubtasksFinished))\n\n\t// summarize convo needs to come *after* the reply is stored in order to correctly summarize the latest message\n\tlog.Println(\"summarizing convo in background\")\n\t// summarize in the background\n\tgo func() {\n\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in summarizeConvo: %v\\n%s\", r, debug.Stack())\n\t\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\t\tMsg:    fmt.Sprintf(\"Error summarizing convo: %v\", r),\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\terr := summarizeConvo(clients, authVars, settings, orgUserConfig, summarizeConvoParams{\n\t\t\tauth:                  auth,\n\t\t\tplan:                  plan,\n\t\t\tbranch:                branch,\n\t\t\tconvo:                 convo,\n\t\t\tsummaries:             summaries,\n\t\t\tuserPrompt:            state.userPrompt,\n\t\t\tcurrentOrgId:          currentOrgId,\n\t\t\tcurrentReply:          active.CurrentReplyContent,\n\t\t\tcurrentReplyNumTokens: active.NumTokens,\n\t\t\tmodelPackName:         settings.GetModelPack().Name,\n\t\t}, active.SummaryCtx)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error summarizing convo: %v\\n\", err)\n\t\t\tactive.StreamDoneCh <- err\n\t\t}\n\t}()\n\n\tlog.Println(\"Sending active.CurrentReplyDoneCh <- true\")\n\n\tactive.CurrentReplyDoneCh <- true\n\n\tlog.Println(\"Resetting active.CurrentReplyDoneCh\")\n\n\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\tap.CurrentStreamingReplyId = \"\"\n\t\tap.CurrentReplyDoneCh = nil\n\t})\n\n\tautoLoadPaths := autoLoadContextResult.autoLoadPaths\n\tlog.Printf(\"len(autoLoadPaths): %d\\n\", len(autoLoadPaths))\n\tif len(autoLoadPaths) > 0 {\n\t\tlog.Println(\"Sending stream message to load context files\")\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic streaming auto-load context: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic streaming auto-load context: %v\\n%s\", r, debug.Stack()))\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tactive.Stream(shared.StreamMessage{\n\t\t\t\tType:             shared.StreamMessageLoadContext,\n\t\t\t\tLoadContextFiles: autoLoadPaths,\n\t\t\t})\n\t\t\tactive.FlushStreamBuffer()\n\t\t}()\n\n\t\tlog.Println(\"Waiting for client to auto load context (30s timeout)\")\n\n\t\tselect {\n\t\tcase <-active.Ctx.Done():\n\t\t\tlog.Println(\"Context cancelled while waiting for auto load context\")\n\t\t\tstate.execHookOnStop(false)\n\t\t\treturn handleStreamFinishedResult{\n\t\t\t\tshouldContinueMainLoop: false,\n\t\t\t\tshouldReturn:           true,\n\t\t\t}\n\t\tcase <-time.After(30 * time.Second):\n\t\t\tlog.Println(\"Timeout waiting for auto load context\")\n\t\t\tres := state.onError(onErrorParams{\n\t\t\t\tstreamErr: fmt.Errorf(\"timeout waiting for auto load context response\"),\n\t\t\t\tstoreDesc: true,\n\t\t\t})\n\t\t\treturn handleStreamFinishedResult{\n\t\t\t\tshouldContinueMainLoop: res.shouldContinueMainLoop,\n\t\t\t\tshouldReturn:           res.shouldReturn,\n\t\t\t}\n\t\tcase <-active.AutoLoadContextCh:\n\t\t}\n\t}\n\n\twillContinue := state.willContinuePlan(willContinuePlanParams{\n\t\thasNewSubtasks:      hasExplicitTasks,\n\t\tallSubtasksFinished: allSubtasksFinished,\n\t\tactivatePaths:       autoLoadContextResult.activatePaths,\n\t\tremovedSubtasks:     hasExplicitRemoveTasks,\n\t\thasExplicitPaths:    autoLoadContextResult.hasExplicitPaths,\n\t})\n\n\tif willContinue {\n\t\tlog.Println(\"Auto continue plan\")\n\t\t// continue plan\n\t\texecTellPlan(execTellPlanParams{\n\t\t\tclients:   clients,\n\t\t\tplan:      plan,\n\t\t\tbranch:    branch,\n\t\t\tauth:      auth,\n\t\t\treq:       req,\n\t\t\titeration: iteration + 1,\n\t\t\tauthVars:  authVars,\n\t\t})\n\t} else {\n\t\tvar buildFinished bool\n\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\tbuildFinished = ap.BuildFinished()\n\t\t\tap.RepliesFinished = true\n\t\t})\n\n\t\tlog.Printf(\"Won't continue plan. Build finished: %v\\n\", buildFinished)\n\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\tif buildFinished {\n\t\t\tlog.Println(\"Reply is finished and build is finished, calling active.Finish()\")\n\t\t\tactive := GetActivePlan(planId, branch)\n\n\t\t\tif active == nil {\n\t\t\t\tstate.onActivePlanMissingError()\n\t\t\t\treturn handleStreamFinishedResult{\n\t\t\t\t\tshouldContinueMainLoop: true,\n\t\t\t\t\tshouldReturn:           false,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tactive.Finish()\n\t\t} else {\n\t\t\tlog.Println(\"Plan is still building\")\n\t\t\tlog.Println(\"Updating status to building\")\n\t\t\terr := db.SetPlanStatus(planId, branch, shared.PlanStatusBuilding, \"\")\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error setting plan status to building: %v\\n\", err)\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error setting plan status to building: %v\", err))\n\n\t\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\t\tMsg:    fmt.Sprintf(\"Error setting plan status to building: %v\", err),\n\t\t\t\t}\n\n\t\t\t\treturn handleStreamFinishedResult{\n\t\t\t\t\tshouldContinueMainLoop: true,\n\t\t\t\t\tshouldReturn:           false,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlog.Println(\"Sending RepliesFinished stream message\")\n\t\t\tactive.Stream(shared.StreamMessage{\n\t\t\t\tType: shared.StreamMessageRepliesFinished,\n\t\t\t})\n\n\t\t}\n\t}\n\n\treturn handleStreamFinishedResult{}\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_main.go",
    "content": "package plan\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/model\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\nfunc (state *activeTellStreamState) listenStream(stream *model.ExtendedChatCompletionStream) {\n\tdefer stream.Close()\n\n\tplan := state.plan\n\tplanId := plan.Id\n\tbranch := state.branch\n\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\"listenStream - Active plan not found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"listenStream: Panic: %v\\n%s\\n\", r, string(debug.Stack()))\n\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"listenStream: Panic: %v\\n%s\", r, string(debug.Stack())))\n\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"Panic in listenStream\",\n\t\t\t}\n\t\t}\n\t}()\n\n\tstate.chunkProcessor = &chunkProcessor{\n\t\treplyOperations:                 []*shared.Operation{},\n\t\tchunksReceived:                  0,\n\t\tmaybeRedundantOpeningTagContent: \"\",\n\t\tfileOpen:                        false,\n\t\tcontentBuffer:                   \"\",\n\t\tawaitingBlockOpeningTag:         false,\n\t\tawaitingBlockClosingTag:         false,\n\t\tawaitingBackticks:               false,\n\t}\n\n\t// Create a timer that will trigger if no chunk is received within the specified duration\n\tfirstTokenTimeout := firstTokenTimeout(state.totalRequestTokens, state.baseModelConfig.LocalOnly)\n\tlog.Printf(\"listenStream - firstTokenTimeout: %s\\n\", firstTokenTimeout)\n\ttimer := time.NewTimer(firstTokenTimeout)\n\tdefer timer.Stop()\n\tstreamFinished := false\n\n\tbaseModelConfig := state.modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)\n\n\tmodelProvider := baseModelConfig.Provider\n\tmodelName := baseModelConfig.ModelName\n\n\trespCh := make(chan *types.ExtendedChatCompletionStreamResponse)\n\tstreamErrCh := make(chan error)\n\n\t// receive chunks from the stream in a separate goroutine so that we can handle errors and timeouts — needed because stream.Recv() blocks forever\n\tgo func() {\n\t\tfor {\n\t\t\tresp, err := stream.Recv()\n\t\t\tif err != nil {\n\t\t\t\tstreamErrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\trespCh <- resp\n\t\t}\n\t}()\n\nmainLoop:\n\tfor {\n\t\tselect {\n\t\tcase <-active.Ctx.Done():\n\t\t\t// The main modelContext was canceled (not the timer)\n\t\t\tlog.Println(\"\\nTell: stream canceled\")\n\t\t\tstate.execHookOnStop(false)\n\t\t\treturn\n\t\tcase <-timer.C:\n\t\t\t// Timer triggered because no new chunk was received in time\n\t\t\tlog.Println(\"\\nTell: stream timeout due to inactivity\")\n\t\t\tif streamFinished {\n\t\t\t\tlog.Println(\"Tell stream finished—timed out waiting for usage chunk\")\n\t\t\t\tstate.execHookOnStop(false)\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tres := state.onError(onErrorParams{\n\t\t\t\t\tstreamErr: fmt.Errorf(\"stream timeout due to inactivity: The AI model (%s/%s) is not responding\", modelProvider, modelName),\n\t\t\t\t\tstoreDesc: true,\n\t\t\t\t\tcanRetry:  active.CurrentReplyContent == \"\", // if there was no output yet, we can retry\n\t\t\t\t})\n\n\t\t\t\tif res.shouldReturn {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif res.shouldContinueMainLoop {\n\t\t\t\t\tcontinue mainLoop\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase err := <-streamErrCh:\n\t\t\tlog.Printf(\"listenStream - received from streamErrCh: %v\\n\", err)\n\n\t\t\tif err.Error() == \"context canceled\" {\n\t\t\t\tlog.Println(\"Tell: stream context canceled\")\n\t\t\t\tstate.execHookOnStop(false)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Printf(\"Tell: error receiving stream chunk: %v\\n\", err)\n\t\t\tstate.execHookOnStop(true)\n\n\t\t\tvar msg string\n\t\t\tname := modelName\n\t\t\tif !strings.Contains(string(modelName), string(modelProvider)) {\n\t\t\t\tname = shared.ModelName(fmt.Sprintf(\"%s/%s\", modelProvider, modelName))\n\t\t\t}\n\t\t\tif active.CurrentReplyContent == \"\" {\n\t\t\t\tmsg = fmt.Sprintf(\"The AI model (%s) didn't respond: %v\", name, err)\n\t\t\t} else {\n\t\t\t\tmsg = fmt.Sprintf(\"The AI model (%s) stopped responding: %v\", name, err)\n\t\t\t}\n\t\t\tstate.onError(onErrorParams{\n\t\t\t\tstreamErr: errors.New(msg),\n\t\t\t\tstoreDesc: true,\n\t\t\t\tcanRetry:  active.CurrentReplyContent == \"\", // if there was no output yet, we can retry\n\t\t\t})\n\t\t\t// here we want to return no matter what -- state.onError will decide whether to retry or not\n\t\t\treturn\n\t\tcase response := <-respCh:\n\t\t\t// Successfully received a chunk, reset the timer\n\t\t\tif !timer.Stop() {\n\t\t\t\t<-timer.C\n\t\t\t}\n\t\t\ttimer.Reset(model.ACTIVE_STREAM_CHUNK_TIMEOUT)\n\n\t\t\t// log.Println(\"tell stream main: received stream response\", spew.Sdump(response))\n\n\t\t\tif response.ID != \"\" && state.generationId == \"\" {\n\t\t\t\tstate.generationId = response.ID\n\t\t\t}\n\n\t\t\tif state.firstTokenAt.IsZero() {\n\t\t\t\tstate.firstTokenAt = time.Now()\n\t\t\t}\n\n\t\t\tif response.Error != nil {\n\t\t\t\tlog.Println(\"listenStream - stream finished with error\", spew.Sdump(response.Error))\n\n\t\t\t\tbaseModelConfig := state.fallbackRes.BaseModelConfig\n\t\t\t\tmodelErr := model.ClassifyModelError(response.Error.Code, response.Error.Message, nil, baseModelConfig.HasClaudeMaxAuth)\n\n\t\t\t\tres := state.onError(onErrorParams{\n\t\t\t\t\tstreamErr: fmt.Errorf(\"The AI model (%s/%s) stopped streaming with error code %d: %s\", modelProvider, modelName, response.Error.Code, response.Error.Message),\n\t\t\t\t\tstoreDesc: true,\n\t\t\t\t\tcanRetry:  active.CurrentReplyContent == \"\",\n\t\t\t\t\tmodelErr:  &modelErr,\n\t\t\t\t})\n\t\t\t\tif res.shouldReturn {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif res.shouldContinueMainLoop {\n\t\t\t\t\tcontinue mainLoop\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(response.Choices) == 0 {\n\t\t\t\tif response.Usage != nil {\n\t\t\t\t\tstate.handleUsageChunk(response.Usage)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tlog.Println(\"listenStream - stream finished with no choices\", spew.Sdump(response))\n\n\t\t\t\t// 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\n\t\t\t\t// res := state.onError(onErrorParams{\n\t\t\t\t// \tstreamErr: fmt.Errorf(\"stream finished with no choices | The model failed to generate a valid response.\"),\n\t\t\t\t// \tstoreDesc: true,\n\t\t\t\t// \tcanRetry:  true,\n\t\t\t\t// })\n\t\t\t\t// if res.shouldReturn {\n\t\t\t\t// \treturn\n\t\t\t\t// }\n\t\t\t\t// if res.shouldContinueMainLoop {\n\t\t\t\t// \t// continue instead of returning so that context cancellation is handled\n\t\t\t\t// \tcontinue mainLoop\n\t\t\t\t// }\n\n\t\t\t\tcontinue mainLoop\n\t\t\t}\n\n\t\t\tchoice := response.Choices[0]\n\n\t\t\tprocessChunkRes := state.processChunk(choice)\n\t\t\tif processChunkRes.shouldReturn {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thandleFinished := func() handleStreamFinishedResult {\n\t\t\t\tstreamFinishResult := state.handleStreamFinished()\n\t\t\t\tif streamFinishResult.shouldReturn || streamFinishResult.shouldContinueMainLoop {\n\t\t\t\t\treturn streamFinishResult\n\t\t\t\t}\n\n\t\t\t\t// usage can either be included in the final chunk (openrouter) or in a separate chunk (openai)\n\t\t\t\t// if the usage chunk is included, handle it and then return out of listener\n\t\t\t\t// otherwise keep listening for the usage chunk\n\t\t\t\tif response.Usage != nil {\n\t\t\t\t\tstate.handleUsageChunk(response.Usage)\n\t\t\t\t\treturn handleStreamFinishedResult{\n\t\t\t\t\t\tshouldReturn: true,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Reset the timer for the usage chunk\n\t\t\t\tif !timer.Stop() {\n\t\t\t\t\t<-timer.C\n\t\t\t\t}\n\t\t\t\ttimer.Reset(model.USAGE_CHUNK_TIMEOUT)\n\t\t\t\tstreamFinished = true\n\n\t\t\t\treturn handleStreamFinishedResult{\n\t\t\t\t\tshouldContinueMainLoop: true,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif processChunkRes.shouldStop {\n\t\t\t\tlog.Println(\"Model stream reached stop sequence\")\n\n\t\t\t\tres := handleFinished()\n\t\t\t\tif res.shouldReturn {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif choice.FinishReason != \"\" {\n\t\t\t\tlog.Println(\"Model stream finished\")\n\t\t\t\tlog.Println(\"Finish reason: \", choice.FinishReason)\n\n\t\t\t\tif choice.FinishReason == \"error\" {\n\t\t\t\t\tlog.Println(\"Model stream finished with error\")\n\n\t\t\t\t\tres := state.onError(onErrorParams{\n\t\t\t\t\t\tstreamErr: fmt.Errorf(\"The AI model (%s/%s) stopped streaming with an error status\", modelProvider, modelName),\n\t\t\t\t\t\tstoreDesc: true,\n\t\t\t\t\t\tcanRetry:  active.CurrentReplyContent == \"\",\n\t\t\t\t\t})\n\t\t\t\t\tif res.shouldReturn {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif res.shouldContinueMainLoop {\n\t\t\t\t\t\tcontinue mainLoop\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tres := handleFinished()\n\t\t\t\tif res.shouldReturn {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t} else if response.Usage != nil {\n\t\t\t\tstate.handleUsageChunk(response.Usage)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// let main loop continue\n\t\t}\n\t}\n}\n\nfunc firstTokenTimeout(tok int, isLocalModel bool) time.Duration {\n\tconst (\n\t\tbase  = 90 * time.Second\n\t\tslope = 90 * time.Second\n\t\tstep  = 150_000\n\t\tcap   = 15 * time.Minute\n\t)\n\n\t// local models can have a long cold start, and timeouts are less relevant\n\tif isLocalModel {\n\t\treturn cap\n\t}\n\n\tif tok <= step {\n\t\treturn base\n\t}\n\textra := time.Duration((tok-step)/step) * slope\n\tif extra > cap-base {\n\t\textra = cap - base\n\t}\n\treturn base + extra\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_processor.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"regexp\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\nconst verboseLogging = false\n\nvar openingTagRegex = regexp.MustCompile(`<PlandexBlock\\s+lang=\"(.+?)\"\\s+path=\"(.+?)\".*?>`)\n\ntype processChunkResult struct {\n\tshouldReturn bool\n\tshouldStop   bool\n}\n\nfunc (state *activeTellStreamState) processChunk(choice types.ExtendedChatCompletionStreamChoice) processChunkResult {\n\treq := state.req\n\t// missingFileResponse := state.missingFileResponse\n\tprocessor := state.chunkProcessor\n\treplyParser := state.replyParser\n\tplan := state.plan\n\tplanId := plan.Id\n\tbranch := state.branch\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tstate.onActivePlanMissingError()\n\t\treturn processChunkResult{}\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"processChunk: Panic: %v\\n%s\\n\", r, string(debug.Stack()))\n\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"processChunk: Panic: %v\\n%s\", r, string(debug.Stack())))\n\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    fmt.Sprintf(\"Panic in processChunk: %v\\n%s\", r, string(debug.Stack())),\n\t\t\t}\n\t\t}\n\t}()\n\n\tdelta := choice.Delta\n\tcontent := delta.Content\n\n\tbaseModelConfig := state.modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)\n\n\tif baseModelConfig.IncludeReasoning && !baseModelConfig.HideReasoning && delta.Reasoning != \"\" {\n\t\tcontent = delta.Reasoning\n\t}\n\n\tif content == \"\" {\n\t\treturn processChunkResult{}\n\t}\n\n\tprocessor.chunksReceived++\n\n\tif verboseLogging {\n\t\tlog.Printf(\"Adding chunk to parser: %s\\n\", content)\n\t\tlog.Printf(\"fileOpen: %v\\n\", processor.fileOpen)\n\t}\n\n\treplyParser.AddChunk(content, true)\n\tparserRes := replyParser.Read()\n\n\tif !processor.fileOpen && parserRes.CurrentFilePath != \"\" {\n\t\tif verboseLogging {\n\t\t\tlog.Printf(\"File open: %s\\n\", parserRes.CurrentFilePath)\n\t\t}\n\t\tprocessor.fileOpen = true\n\t}\n\n\tif processor.fileOpen && strings.HasSuffix(active.CurrentReplyContent+content, \"</PlandexBlock>\") {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"FinishAndRead because of closing tag\")\n\t\t}\n\t\tparserRes = replyParser.FinishAndRead()\n\t\tprocessor.fileOpen = false\n\t}\n\n\tif processor.fileOpen && parserRes.CurrentFilePath == \"\" {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"File open but current file path is empty, closing file\")\n\t\t}\n\t\tprocessor.fileOpen = false\n\t}\n\n\toperations := parserRes.Operations\n\tstate.replyNumTokens = parserRes.TotalTokens\n\tcurrentFile := parserRes.CurrentFilePath\n\n\t// log.Printf(\"currentFile: %s\\n\", currentFile)\n\t// log.Println(\"files:\")\n\t// spew.Dump(files)\n\n\t// Handle file that is present in project paths but not in context\n\t// Prompt user for what to do on the client side, stop the stream, and wait for user response before proceeding\n\tbufferOrStreamRes := processor.bufferOrStream(content, &parserRes, state.currentStage, state.manualStop)\n\n\tif currentFile != \"\" &&\n\t\t!req.IsChatOnly &&\n\t\tactive.ContextsByPath[currentFile] == nil &&\n\t\treq.ProjectPaths[currentFile] &&\n\t\t!active.AllowOverwritePaths[currentFile] {\n\t\treturn state.handleMissingFile(bufferOrStreamRes.content, currentFile, bufferOrStreamRes.blockLang)\n\t}\n\n\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\tap.CurrentReplyContent += content\n\t\tap.NumTokens++\n\t})\n\n\tif verboseLogging {\n\t\tlog.Println(\"processor before bufferOrStream\")\n\t\tspew.Dump(processor)\n\t\tlog.Println(\"maybeFilePath\", parserRes.MaybeFilePath)\n\t\tlog.Println(\"currentFilePath\", parserRes.CurrentFilePath)\n\t\tlog.Println(\"bufferOrStreamRes\")\n\t\tspew.Dump(bufferOrStreamRes)\n\t}\n\n\tif bufferOrStreamRes.shouldStream {\n\t\tactive.Stream(shared.StreamMessage{\n\t\t\tType:       shared.StreamMessageReply,\n\t\t\tReplyChunk: bufferOrStreamRes.content,\n\t\t})\n\t}\n\n\tif verboseLogging {\n\t\tlog.Println(\"processor after bufferOrStream\")\n\t\tspew.Dump(processor)\n\t}\n\n\tif !req.IsChatOnly && len(operations) > len(processor.replyOperations) {\n\t\tstate.handleNewOperations(&parserRes)\n\t}\n\n\treturn processChunkResult{\n\t\tshouldStop: bufferOrStreamRes.shouldStop,\n\t}\n}\n\ntype bufferOrStreamResult struct {\n\tshouldStream bool\n\tcontent      string\n\tblockLang    string\n\tshouldStop   bool\n}\n\nfunc (processor *chunkProcessor) bufferOrStream(content string, parserRes *types.ReplyParserRes, currentStage shared.CurrentStage, manualStopSequences []string) bufferOrStreamResult {\n\tif len(manualStopSequences) > 0 {\n\t\tfor _, stopSequence := range manualStopSequences {\n\n\t\t\t// if the chunk contains the entire stop sequence, stream everything before it then caller can stop the stream\n\t\t\tif strings.Contains(content, stopSequence) {\n\t\t\t\tsplit := strings.Split(content, stopSequence)\n\t\t\t\tif len(split) > 1 {\n\t\t\t\t\treturn bufferOrStreamResult{\n\t\t\t\t\t\tshouldStream: true,\n\t\t\t\t\t\tcontent:      split[0],\n\t\t\t\t\t\tshouldStop:   true,\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// there was nothing before the stop sequence, so nothing to stream\n\t\t\t\t\treturn bufferOrStreamResult{\n\t\t\t\t\t\tshouldStream: false,\n\t\t\t\t\t\tshouldStop:   true,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// otherwise if the buffer plus chunk contains the stop sequence, don't stream anything and stop the stream\n\t\t\tif strings.Contains(processor.contentBuffer+content, stopSequence) {\n\t\t\t\tlog.Printf(\"bufferOrStream - stop sequence found in buffer plus chunk\\n\")\n\t\t\t\tsplit := strings.Split(content, stopSequence)\n\t\t\t\tif len(split) > 1 {\n\t\t\t\t\t// we'll stream the part before the stop sequence\n\t\t\t\t\treturn bufferOrStreamResult{\n\t\t\t\t\t\tshouldStream: true,\n\t\t\t\t\t\tcontent:      split[0],\n\t\t\t\t\t\tshouldStop:   true,\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// there was nothing before the stop sequence, so nothing to stream\n\t\t\t\t\treturn bufferOrStreamResult{\n\t\t\t\t\t\tshouldStream: false,\n\t\t\t\t\t\tshouldStop:   true,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// otherwise if the buffer plus chunk ends with a prefix of the stop sequence, buffer it and continue\n\n\t\t\ttoCheck := processor.contentBuffer + content\n\t\t\ttailLen := len(stopSequence) - 1\n\t\t\tif tailLen > len(toCheck) {\n\t\t\t\ttailLen = len(toCheck)\n\t\t\t}\n\t\t\tsuffix := toCheck[len(toCheck)-tailLen:]\n\n\t\t\tif strings.HasPrefix(stopSequence, suffix) {\n\t\t\t\tlog.Printf(\"bufferOrStream - stop sequence prefix found in buffer plus chunk. buffer and continue\\n\")\n\t\t\t\tprocessor.contentBuffer += content\n\t\t\t\treturn bufferOrStreamResult{\n\t\t\t\t\tshouldStream: false,\n\t\t\t\t\tcontent:      content,\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t}\n\n\t// apart from manual stop sequences, no buffering in planning stages\n\tif currentStage.TellStage == shared.TellStagePlanning {\n\t\treturn bufferOrStreamResult{\n\t\t\tshouldStream: true,\n\t\t\tcontent:      content,\n\t\t}\n\t}\n\n\tvar shouldStream bool\n\tvar blockLang string\n\n\tawaitingTag := processor.awaitingBlockOpeningTag || processor.awaitingBlockClosingTag || processor.awaitingOpClosingTag\n\tawaitingAny := awaitingTag || processor.awaitingBackticks\n\n\tif awaitingAny {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"awaitingAny\")\n\t\t}\n\t\tprocessor.contentBuffer += content\n\t\tcontent = processor.contentBuffer\n\n\t\tif verboseLogging {\n\t\t\tlog.Printf(\"awaitingBlockOpeningTag: %v\\n\", processor.awaitingBlockOpeningTag)\n\t\t\tlog.Printf(\"awaitingBlockClosingTag: %v\\n\", processor.awaitingBlockClosingTag)\n\t\t\tlog.Printf(\"awaitingBackticks: %v\\n\", processor.awaitingBackticks)\n\t\t\tlog.Printf(\"awaitingOpClosingTag: %v\\n\", processor.awaitingOpClosingTag)\n\t\t\tlog.Printf(\"content: %q\\n\", content)\n\t\t}\n\t}\n\n\tif processor.awaitingBackticks {\n\t\tif strings.Contains(content, \"```\") {\n\t\t\tprocessor.awaitingBackticks = false\n\t\t\tcontent = strings.ReplaceAll(content, \"```\", \"\\\\`\\\\`\\\\`\")\n\n\t\t\tif !(processor.awaitingBlockOpeningTag || processor.awaitingBlockClosingTag) {\n\t\t\t\tshouldStream = true\n\t\t\t}\n\t\t} else if !strings.HasSuffix(content, \"`\") {\n\t\t\t// fewer than 3 backticks, no need to escape\n\t\t\tprocessor.awaitingBackticks = false\n\n\t\t\tif !(processor.awaitingBlockOpeningTag || processor.awaitingBlockClosingTag) {\n\t\t\t\tshouldStream = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif awaitingTag {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"awaitingTag\")\n\t\t}\n\t\tif processor.awaitingBlockOpeningTag {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"processor.awaitingBlockOpeningTag\")\n\t\t\t}\n\t\t\tvar matchedPrefix bool\n\n\t\t\tif parserRes.CurrentFilePath != \"\" {\n\t\t\t\tmatched, replaced := replaceCodeBlockOpeningTag(content, func(lang string) string {\n\t\t\t\t\tblockLang = lang\n\t\t\t\t\treturn \"```\" + lang\n\t\t\t\t})\n\n\t\t\t\tif matched {\n\t\t\t\t\tshouldStream = true\n\t\t\t\t\tprocessor.awaitingBlockOpeningTag = false\n\t\t\t\t\tprocessor.fileOpen = true\n\t\t\t\t\tcontent = replaced\n\t\t\t\t} else {\n\t\t\t\t\t// tag is missing - something is wrong - we shouldn't be here but let's try to recover anyway\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tlog.Printf(\"Opening <PlandexBlock> tag is missing even though parserRes.CurrentFile is set - something is wrong: %s\\n\", content)\n\t\t\t\t\t}\n\t\t\t\t\tprocessor.awaitingBlockOpeningTag = false\n\t\t\t\t\tprocessor.fileOpen = false\n\t\t\t\t\tcontent += \"\\n```\" // add ``` to the end of the line to close the markdown code block\n\t\t\t\t\tshouldStream = true\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsplit := strings.Split(content, \"<\")\n\n\t\t\t\tif len(split) > 1 {\n\t\t\t\t\tlast := split[len(split)-1]\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tlog.Printf(\"last: %s\\n\", last)\n\t\t\t\t\t}\n\t\t\t\t\tif strings.HasPrefix(`PlandexBlock lang=\"`, last) {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tlog.Println(\"strings.HasPrefix(`PlandexBlock lang=\", last)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tshouldStream = false\n\t\t\t\t\t\tmatchedPrefix = true\n\t\t\t\t\t} else if strings.HasPrefix(last, `PlandexBlock lang=\"`) {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tlog.Println(\"partialOpeningTagRegex.MatchString(last)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tshouldStream = false\n\t\t\t\t\t\tmatchedPrefix = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tlog.Println(\"partialOpeningTagRegex.MatchString(last) is false\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !matchedPrefix && parserRes.MaybeFilePath == \"\" && parserRes.CurrentFilePath == \"\" {\n\t\t\t\t// wasn't really a file path / code block\n\t\t\t\tprocessor.awaitingBlockOpeningTag = false\n\t\t\t\tshouldStream = true\n\t\t\t}\n\t\t} else if processor.awaitingBlockClosingTag {\n\t\t\tif parserRes.CurrentFilePath == \"\" {\n\t\t\t\tif strings.Contains(content, \"</PlandexBlock>\") {\n\t\t\t\t\tshouldStream = true\n\t\t\t\t\tprocessor.awaitingBlockClosingTag = false\n\t\t\t\t\tprocessor.fileOpen = false\n\t\t\t\t\t// replace </PlandexBlock> with ``` to close the markdown code block\n\t\t\t\t\tcontent = strings.ReplaceAll(content, \"</PlandexBlock>\", \"```\")\n\t\t\t\t} else {\n\t\t\t\t\tlog.Printf(\"Closing </PlandexBlock> tag is missing even though parserRes.CurrentOperation is nil - something is wrong: %s\\n\", content)\n\t\t\t\t\tprocessor.awaitingBlockClosingTag = false\n\t\t\t\t\tshouldStream = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else if processor.awaitingOpClosingTag {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Printf(\"awaitingOpClosingTag: %v\\n\", processor.awaitingOpClosingTag)\n\t\t\t}\n\t\t\tif strings.Contains(content, \"<EndPlandexFileOps/>\") {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Printf(\"Found <EndPlandexFileOps/>\\n\")\n\t\t\t\t}\n\t\t\t\tprocessor.awaitingOpClosingTag = false\n\t\t\t\tcontent = strings.Replace(content, \"\\n<EndPlandexFileOps/>\", \"\", 1)\n\t\t\t\tcontent = strings.Replace(content, \"<EndPlandexFileOps/>\", \"\", 1)\n\t\t\t\tshouldStream = true\n\t\t\t}\n\t\t}\n\n\t} else {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"not awaiting tag\")\n\t\t}\n\n\t\tif parserRes.MaybeFilePath != \"\" && parserRes.CurrentFilePath == \"\" {\n\t\t\tprocessor.awaitingBlockOpeningTag = true\n\t\t} else {\n\t\t\t// this will set processor.awaitingBlockOpeningTag to true if the content starts with any prefix of<PlandexBlock lang=\" *or* any prefix of a full opening tag\n\t\t\t// if the full tag is in the content, it will later get set to false again when the full tag is handled\n\t\t\tsplit := strings.Split(content, \"<\")\n\t\t\tif len(split) > 1 {\n\t\t\t\tlast := split[len(split)-1]\n\n\t\t\t\tif strings.HasPrefix(`PlandexBlock lang=\"`, last) {\n\t\t\t\t\tprocessor.awaitingBlockOpeningTag = true\n\t\t\t\t} else if strings.HasPrefix(last, `PlandexBlock lang=\"`) {\n\t\t\t\t\tprocessor.awaitingBlockOpeningTag = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif parserRes.CurrentFilePath != \"\" {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"parserRes.CurrentFilePath != \\\"\\\"\")\n\t\t\t}\n\t\t\tif strings.Contains(content, \"</PlandexBlock>\") {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"strings.Contains(content, \\\"</PlandexBlock>\\\")\")\n\t\t\t\t}\n\t\t\t\tprocessor.awaitingBlockClosingTag = true\n\t\t\t} else {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"not strings.Contains(content, \\\"</PlandexBlock>\\\")\")\n\t\t\t\t}\n\t\t\t\tsplit := strings.Split(content, \"<\")\n\t\t\t\t// log.Printf(\"split: %v\\n\", split)\n\t\t\t\tif len(split) > 1 {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tlog.Println(\"len(split) > 1\")\n\t\t\t\t\t}\n\t\t\t\t\tlast := split[len(split)-1]\n\t\t\t\t\t// log.Printf(\"last: %s\\n\", last)\n\t\t\t\t\tif strings.HasPrefix(\"/PlandexBlock>\", last) {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tlog.Println(\"strings.HasPrefix(\\\"/PlandexBlock>\\\", last)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprocessor.awaitingBlockClosingTag = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if parserRes.FileOperationBlockOpen() {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"parserRes.FileOperationBlockOpen()\")\n\t\t\t}\n\t\t\tif strings.Contains(content, \"<EndPlandexFileOps/>\") {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"strings.Contains(content, \\\"<EndPlandexFileOps/>\\\")\")\n\t\t\t\t}\n\t\t\t\tprocessor.awaitingOpClosingTag = true\n\t\t\t} else {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"not strings.Contains(content, \\\"<EndPlandexFileOps/>\\\")\")\n\t\t\t\t}\n\t\t\t\tsplit := strings.Split(content, \"<\")\n\t\t\t\tif len(split) > 1 {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tlog.Println(\"len(split) > 1\")\n\t\t\t\t\t}\n\t\t\t\t\tlast := split[len(split)-1]\n\t\t\t\t\tif strings.HasPrefix(\"EndPlandexFileOps/>\", last) {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tlog.Println(\"strings.HasPrefix(\\\"EndPlandexFileOps/>\\\", last)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprocessor.awaitingOpClosingTag = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if strings.Contains(content, \"</PlandexBlock>\") {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"strings.Contains(content, \\\"</PlandexBlock>\\\")\")\n\t\t\t}\n\t\t\tcontent = strings.Replace(content, \"</PlandexBlock>\", \"```\", 1)\n\t\t} else if strings.Contains(content, \"<EndPlandexFileOps/>\") {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"strings.Contains(content, \\\"<EndPlandexFileOps/>\\\")\")\n\t\t\t}\n\t\t\tcontent = strings.Replace(content, \"\\n<EndPlandexFileOps/>\", \"\", 1)\n\t\t\tcontent = strings.Replace(content, \"<EndPlandexFileOps/>\", \"\", 1)\n\t\t}\n\n\t\tif processor.fileOpen && (strings.Contains(content, \"```\") || strings.HasSuffix(content, \"`\")) {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"processor.fileOpen && (strings.Contains(content, \\\"```\\\") || strings.HasSuffix(content, \\\"`\\\"))\")\n\t\t\t}\n\t\t\tprocessor.awaitingBackticks = true\n\t\t}\n\n\t\tvar matchedOpeningTag bool\n\t\tif processor.fileOpen {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"processor.fileOpen\")\n\t\t\t}\n\t\t\tvar replaced string\n\n\t\t\tmatchedOpeningTag, replaced = replaceCodeBlockOpeningTag(content, func(lang string) string {\n\t\t\t\tblockLang = lang\n\t\t\t\treturn \"```\" + lang\n\t\t\t})\n\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"matchedOpeningTag\", matchedOpeningTag)\n\t\t\t\tlog.Println(\"replaced\", replaced)\n\t\t\t}\n\n\t\t\tif matchedOpeningTag {\n\t\t\t\tprocessor.awaitingBlockOpeningTag = false\n\t\t\t\tcontent = replaced\n\t\t\t}\n\t\t}\n\n\t\tshouldStream = !processor.awaitingBlockOpeningTag && !processor.awaitingBlockClosingTag && !processor.awaitingOpClosingTag && !processor.awaitingBackticks\n\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"processor.awaitingBlockOpeningTag\", processor.awaitingBlockOpeningTag)\n\t\t\tlog.Println(\"processor.awaitingBlockClosingTag\", processor.awaitingBlockClosingTag)\n\t\t\tlog.Println(\"processor.awaitingOpClosingTag\", processor.awaitingOpClosingTag)\n\t\t\tlog.Println(\"processor.awaitingBackticks\", processor.awaitingBackticks)\n\n\t\t\tlog.Println(\"shouldStream\", shouldStream)\n\t\t}\n\t}\n\n\tif verboseLogging {\n\t\tlog.Println(\"returning bufferOrStreamResult\")\n\t\tlog.Println(\"shouldStream\", shouldStream)\n\t\tlog.Println(\"content\", content)\n\t\tlog.Println(\"blockLang\", blockLang)\n\t}\n\n\tif shouldStream {\n\t\tprocessor.contentBuffer = \"\"\n\t} else {\n\t\tprocessor.contentBuffer = content\n\t}\n\n\treturn bufferOrStreamResult{\n\t\tshouldStream: shouldStream,\n\t\tcontent:      content,\n\t\tblockLang:    blockLang,\n\t}\n}\n\nfunc (state *activeTellStreamState) handleNewOperations(parserRes *types.ReplyParserRes) {\n\tprocessor := state.chunkProcessor\n\tplan := state.plan\n\tplanId := plan.Id\n\tbranch := state.branch\n\tclients := state.clients\n\tauth := state.auth\n\tauthVars := state.authVars\n\treq := state.req\n\treplyId := state.replyId\n\tcurrentOrgId := state.currentOrgId\n\tcurrentUserId := state.currentUserId\n\tsettings := state.settings\n\n\toperations := parserRes.Operations\n\n\tlog.Printf(\"%d new operations\\n\", len(operations)-len(processor.replyOperations))\n\n\tfor i, op := range operations {\n\t\tif i < len(processor.replyOperations) {\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Printf(\"Detected operation: %s\\n\", op.Name())\n\n\t\tif req.BuildMode == shared.BuildModeAuto {\n\t\t\tlog.Printf(\"Queuing build for %s\\n\", op.Name())\n\t\t\t// log.Println(\"Content:\")\n\t\t\t// log.Println(strconv.Quote(op.Content))\n\n\t\t\tbuildState := &activeBuildStreamState{\n\t\t\t\tmodelStreamId: state.modelStreamId,\n\t\t\t\tclients:       clients,\n\t\t\t\tauthVars:      authVars,\n\t\t\t\tauth:          auth,\n\t\t\t\tcurrentOrgId:  currentOrgId,\n\t\t\t\tcurrentUserId: currentUserId,\n\t\t\t\tplan:          plan,\n\t\t\t\tbranch:        branch,\n\t\t\t\tsettings:      settings,\n\t\t\t\tmodelContext:  state.modelContext,\n\t\t\t\torgUserConfig: state.orgUserConfig,\n\t\t\t}\n\n\t\t\tvar opContentTokens int\n\t\t\tif op.Type == shared.OperationTypeFile {\n\t\t\t\topContentTokens = shared.GetNumTokensEstimate(op.Content)\n\t\t\t} else {\n\t\t\t\topContentTokens = op.NumTokens\n\t\t\t}\n\n\t\t\t// log.Printf(\"buildState.queueBuilds - op.Description:\\n%s\\n\", op.Description)\n\n\t\t\tbuildState.queueBuilds([]*types.ActiveBuild{{\n\t\t\t\tReplyId:           replyId,\n\t\t\t\tFileDescription:   op.Description,\n\t\t\t\tFileContent:       op.Content,\n\t\t\t\tFileContentTokens: opContentTokens,\n\t\t\t\tPath:              op.Path,\n\t\t\t\tMoveDestination:   op.Destination,\n\t\t\t\tIsMoveOp:          op.Type == shared.OperationTypeMove,\n\t\t\t\tIsRemoveOp:        op.Type == shared.OperationTypeRemove,\n\t\t\t\tIsResetOp:         op.Type == shared.OperationTypeReset,\n\t\t\t}})\n\t\t}\n\t\tprocessor.replyOperations = append(processor.replyOperations, op)\n\t\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\t\tap.Operations = append(ap.Operations, op)\n\t\t})\n\t}\n\n}\n\nfunc (state *activeTellStreamState) handleMissingFile(content, currentFile, blockLang string) processChunkResult {\n\tbranch := state.branch\n\tplan := state.plan\n\tplanId := plan.Id\n\treplyParser := state.replyParser\n\titeration := state.iteration\n\tclients := state.clients\n\tauth := state.auth\n\treq := state.req\n\tauthVars := state.authVars\n\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tstate.onActivePlanMissingError()\n\t\treturn processChunkResult{}\n\t}\n\n\tlog.Printf(\"Attempting to overwrite a file that isn't in context: %s\\n\", currentFile)\n\n\t// attempting to overwrite a file that isn't in context\n\t// we will stop the stream and ask the user what to do\n\terr := db.SetPlanStatus(planId, branch, shared.PlanStatusMissingFile, \"\")\n\n\tif err != nil {\n\t\tlog.Printf(\"Error setting plan %s status to prompting: %v\\n\", planId, err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error setting plan %s status to prompting: %v\", planId, err))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"Error setting plan status to prompting: %v\", err),\n\t\t}\n\t\treturn processChunkResult{}\n\t}\n\n\tvar trimmedReply string\n\n\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\tap.MissingFilePath = currentFile\n\t\ttrimmedReply = replyParser.GetReplyForMissingFile()\n\t\tap.CurrentReplyContent = trimmedReply\n\t})\n\n\t// log.Println(\"Content:\")\n\t// log.Println(content)\n\n\t// log.Println(\"Block lang:\")\n\t// log.Println(blockLang)\n\n\t// log.Println(\"Trimmed content:\")\n\t// log.Println(trimmedReply)\n\n\t// try to replace the code block opening tag in the chunk with an empty string\n\t// this will remove the code block opening tag if it exists\n\tsplitBy := \"```\" + blockLang\n\tsplit := strings.Split(content, splitBy)\n\tchunkToStream := split[0] + splitBy + \"\\n\"\n\n\t// log.Printf(\"chunkToStream: %s\\n\", chunkToStream)\n\n\tif chunkToStream != \"\" {\n\t\tlog.Printf(\"Streaming remaining chunk before missing file prompt: %s\\n\", chunkToStream)\n\t\tactive.Stream(shared.StreamMessage{\n\t\t\tType:       shared.StreamMessageReply,\n\t\t\tReplyChunk: chunkToStream,\n\t\t})\n\t\tactive.FlushStreamBuffer()\n\t\ttime.Sleep(20 * time.Millisecond)\n\t}\n\n\tlog.Printf(\"Prompting user for missing file: %s\\n\", currentFile)\n\n\tactive.Stream(shared.StreamMessage{\n\t\tType:                   shared.StreamMessagePromptMissingFile,\n\t\tMissingFilePath:        currentFile,\n\t\tMissingFileAutoContext: active.AutoContext,\n\t})\n\n\tlog.Printf(\"Stopping stream for missing file: %s\\n\", currentFile)\n\t// log.Printf(\"Chunk content: %s\\n\", content)\n\t// log.Printf(\"Current reply content: %s\\n\", active.CurrentReplyContent)\n\n\t// stop stream for now\n\tactive.CancelModelStreamFn()\n\n\tlog.Printf(\"Stopped stream for missing file: %s\\n\", currentFile)\n\n\t// wait for user response to come in\n\tvar userChoice shared.RespondMissingFileChoice\n\tselect {\n\tcase <-active.Ctx.Done():\n\t\tlog.Println(\"Context cancelled while waiting for missing file response\")\n\t\tstate.execHookOnStop(false)\n\t\treturn processChunkResult{shouldReturn: true}\n\n\tcase <-time.After(30 * time.Minute): // long timeout here since we're waiting for user input\n\t\tlog.Println(\"Timeout waiting for missing file choice\")\n\t\tstate.onError(onErrorParams{\n\t\t\tstreamErr: fmt.Errorf(\"timeout waiting for missing file choice\"),\n\t\t\tstoreDesc: true,\n\t\t})\n\t\treturn processChunkResult{}\n\n\tcase userChoice = <-active.MissingFileResponseCh:\n\t}\n\n\tlog.Printf(\"User choice for missing file: %s\\n\", userChoice)\n\n\tactive.ResetModelCtx()\n\n\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\tap.MissingFilePath = \"\"\n\t\tap.CurrentReplyContent = replyParser.GetReplyForMissingFile()\n\t})\n\n\tlog.Println(\"Continuing stream\")\n\n\t// continue plan\n\texecTellPlan(execTellPlanParams{\n\t\tclients:             clients,\n\t\tplan:                plan,\n\t\tbranch:              branch,\n\t\tauth:                auth,\n\t\treq:                 req,\n\t\titeration:           iteration, // keep the same iteration\n\t\tmissingFileResponse: userChoice,\n\t\tauthVars:            authVars,\n\t})\n\n\treturn processChunkResult{shouldReturn: true}\n}\n\nfunc getCroppedChunk(uncropped, cropped, chunk string) string {\n\tuncroppedIdx := strings.Index(uncropped, chunk)\n\tif uncroppedIdx == -1 {\n\t\treturn \"\"\n\t}\n\tcroppedChunk := cropped[uncroppedIdx:]\n\treturn croppedChunk\n}\n\nfunc replaceCodeBlockOpeningTag(content string, replaceWithFn func(lang string) string) (bool, string) {\n\t// check for opening tag matching <PlandexBlock lang=\"...\" path=\"...\">\n\tmatch := openingTagRegex.FindStringSubmatch(content)\n\n\tif match != nil {\n\t\t// Found complete opening tag with lang and path attributes\n\t\tlang := match[1] // Extract the language from the first capture group\n\t\treturn true, strings.Replace(content, match[0], replaceWithFn(lang), 1)\n\t} else if strings.Contains(content, \"<PlandexBlock>\") {\n\t\t// This is a fallback case that should probably be removed since we now require both attributes\n\t\treturn true, strings.Replace(content, \"<PlandexBlock>\", replaceWithFn(\"\"), 1)\n\t}\n\n\treturn false, \"\"\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_processor_test.go",
    "content": "package plan\n\nimport (\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n\t\"testing\"\n)\n\nfunc TestBufferOrStream(t *testing.T) {\n\ttests := []struct {\n\t\tonly            bool\n\t\tname            string\n\t\tinitialState    *chunkProcessor\n\t\tchunk           string\n\t\tmaybeFilePath   string\n\t\tcurrentFilePath string\n\t\tisInMoveBlock   bool\n\t\tisInRemoveBlock bool\n\t\tisInResetBlock  bool\n\t\twant            bufferOrStreamResult\n\t\twantState       *chunkProcessor // To verify state transitions\n\t\tmanualStop      []string\n\t}{\n\t\t{\n\t\t\tname: \"streams regular content\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"\",\n\t\t\t},\n\t\t\tchunk: \"some regular text\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"some regular text\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       false,\n\t\t\t\tfileOpen:                false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"buffers partial opening tag\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t\tfileOpen:                false,\n\t\t\t\tcontentBuffer:           \"\",\n\t\t\t},\n\t\t\tchunk:           `<Pland`,\n\t\t\tmaybeFilePath:   \"main.go\",\n\t\t\tcurrentFilePath: \"\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       false,\n\t\t\t\tfileOpen:                false,\n\t\t\t\tcontentBuffer:           \"<Pland\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"converts opening tag\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:                true,\n\t\t\t\tcontentBuffer:           `<PlandexBlock lang=\"go\" path=\"main.go\">` + \"\\n\",\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t},\n\t\t\tchunk:           `package`,\n\t\t\tmaybeFilePath:   \"\",\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"```go\\npackage\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       false,\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// occurs when replayParser can't identify a 'maybeFilePath' prior a full opening tag being sent ('maybeFilePath' gets skipped and 'currentFilePath' is set immediately)\n\t\t\tname: \"converts opening tag without awaitingOpeningTag\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:                true,\n\t\t\t\tcontentBuffer:           \"\",\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t},\n\t\t\tchunk:           `<PlandexBlock lang=\"go\" path=\"main.go\">` + \"\\npackage\",\n\t\t\tmaybeFilePath:   \"\",\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"```go\\npackage\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       false,\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"buffers partial backticks\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:      true,\n\t\t\t\tcontentBuffer: \"here's some co\",\n\t\t\t},\n\t\t\tchunk:           \"de:`\",\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBackticks: true,\n\t\t\t\tfileOpen:          true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"escapes backticks in content\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:          true,\n\t\t\t\tawaitingBackticks: true,\n\t\t\t\tcontentBuffer:     \"here's some code:\\n`\",\n\t\t\t},\n\t\t\tchunk:           \"``\\npackage\",\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"here's some code:\\n\\\\`\\\\`\\\\`\\npackage\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       false,\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"buffers partial closing tag\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:                true,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tcontentBuffer:           \"\",\n\t\t\t},\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\tchunk:           \"\\n}</Plan\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockClosingTag: true,\n\t\t\t\tfileOpen:                true,\n\t\t\t\tcontentBuffer:           \"\\n}</Plan\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"buffers full closing tag with file open\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:                true,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tcontentBuffer:           \"\",\n\t\t\t},\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\tchunk:           \"\\n}</PlandexBlock>\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockClosingTag: true,\n\t\t\t\tfileOpen:                true,\n\t\t\t\tcontentBuffer:           \"\\n}</PlandexBlock>\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaces full closing tag with file closed\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:                false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tcontentBuffer:           \"\",\n\t\t\t},\n\t\t\tcurrentFilePath: \"\",\n\t\t\tchunk:           \"\\n}</PlandexBlock>\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"\\n}```\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tfileOpen:                false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaces full closing tag with file closed and awaiting backticks\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:                false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       true,\n\t\t\t\tcontentBuffer:           \"\",\n\t\t\t},\n\t\t\tcurrentFilePath: \"\",\n\t\t\tchunk:           \" ONLY this one-line title and nothing else.`\\n</PlandexBlock>\\n\\nNow let\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \" ONLY this one-line title and nothing else.`\\n```\\n\\nNow let\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tfileOpen:                false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"handles single backticks\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:          true,\n\t\t\t\tawaitingBackticks: true,\n\t\t\t\tcontentBuffer:     \"`file.go`\",\n\t\t\t},\n\t\t\tchunk:           \"\\nsomething\",\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"`file.go`\\nsomething\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       false,\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"handles close and re-open backticks\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen:          true,\n\t\t\t\tawaitingBackticks: true,\n\t\t\t\tcontentBuffer:     \"`file.go`\",\n\t\t\t},\n\t\t\tchunk:           \"\\n`file2.go`\",\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tawaitingBlockClosingTag: false,\n\t\t\t\tawaitingBackticks:       true,\n\t\t\t\tfileOpen:                true,\n\t\t\t\tcontentBuffer:           \"`file.go`\\n`file2.go`\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"buffers for end of file operations\",\n\t\t\tinitialState:  &chunkProcessor{},\n\t\t\tisInMoveBlock: true,\n\t\t\tchunk:         \"\\n<EndPlandexFileOps/>\\nmore\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingOpClosingTag: true,\n\t\t\t\tcontentBuffer:        \"\\n<EndPlandexFileOps/>\\nmore\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaces full end of file operations tag\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tawaitingOpClosingTag: true,\n\t\t\t\tcontentBuffer:        \"\\n<EndPlandexFileOps/>\\nmore\",\n\t\t\t},\n\t\t\tchunk: \" stuff\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"\\nmore stuff\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingOpClosingTag: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"buffers for end of file operations with partial tag\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tawaitingOpClosingTag: true,\n\t\t\t},\n\t\t\tchunk: \"\\n<EndPlandex\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingOpClosingTag: true,\n\t\t\t\tcontentBuffer:        \"\\n<EndPlandex\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaces end of file operation closing partial tag\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tawaitingOpClosingTag: true,\n\t\t\t\tcontentBuffer:        \"\\n<EndPlandex\",\n\t\t\t},\n\t\t\tchunk: \"FileOps/>\\nmore\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"\\nmore\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingOpClosingTag: false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"buffers for partial opening tag with no file path label\",\n\t\t\tinitialState: &chunkProcessor{},\n\t\t\tchunk:        \"something<Pland\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"continues buffering partial opening tag with no file path label\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t\tcontentBuffer:           \"something<Pland\",\n\t\t\t},\n\t\t\tchunk: \"exBlock lang=\\\"go\\\" path=\\\"main\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t\tcontentBuffer:           \"something<PlandexBlock lang=\\\"go\\\" path=\\\"main\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaces opening tag with no file path label when it completes\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t\tcontentBuffer:           \"something\\n<Pland\",\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\tchunk:           \"exBlock lang=\\\"go\\\" path=\\\"main.go\\\">\\npackage\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"something\\n```go\\npackage\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"replaces full opening tag without file path label\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tfileOpen: true,\n\t\t\t},\n\t\t\tcurrentFilePath: \"main.go\",\n\t\t\tchunk:           \"something\\n<PlandexBlock lang=\\\"go\\\" path=\\\"main.go\\\">\\npackage\",\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"something\\n```go\\npackage\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tname:         \"stop tag entirely in one chunk\",\n\t\t\tinitialState: &chunkProcessor{}, // empty buffer\n\t\t\tchunk:        \"hello <PlandexFinish/>bye\",\n\t\t\tmanualStop:   []string{\"<PlandexFinish/>\"},\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,     // stream only the prefix\n\t\t\t\tcontent:      \"hello \", // text before the tag\n\t\t\t\tshouldStop:   true,     // tell caller to stop\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"\", // nothing left buffered\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stop tag split across two chunks (prefix + rest)\",\n\t\t\tonly: true, // helper if you want to run just this one\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"\", // begins empty\n\t\t\t},\n\t\t\t// FIRST CHUNK —— just a proper prefix\n\t\t\tchunk:      \"<PlandexFin\", // no '<' in second part\n\t\t\tmanualStop: []string{\"<PlandexFinish/>\"},\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false, // nothing streams yet\n\t\t\t\tshouldStop:   false, // not complete, keep going\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"<PlandexFin\", // prefix is buffered\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// SECOND CHUNK —— completes the tag; nothing should stream\n\t\t\tname: \"stop tag split across two chunks (completes)\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"<PlandexFin\", // leftover from previous call\n\t\t\t},\n\t\t\tchunk:      \"ish/>\\nmore text\", // completes tag + trailing text\n\t\t\tmanualStop: []string{\"<PlandexFinish/>\"},\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false, // do NOT leak \"more text\"\n\t\t\t\tshouldStop:   true,  // signal caller to stop\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"<PlandexFinish/>\", // may keep full tag inside\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stop prefix turns out to be different tag, falls through to other parsing logic\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"<Plandex\",\n\t\t\t},\n\t\t\tchunk:      \"Blo\",\n\t\t\tmanualStop: []string{\"<PlandexFinish/>\"},\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: false,\n\t\t\t\tshouldStop:   false,\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tcontentBuffer:           \"<PlandexBlo\",\n\t\t\t\tawaitingBlockOpeningTag: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"stop prefix turns out to be different tag, falls through to other parsing logic #2\",\n\t\t\tinitialState: &chunkProcessor{\n\t\t\t\tcontentBuffer: \"something\\n<Plandex\",\n\t\t\t},\n\t\t\tchunk:      \"exBlock lang=\\\"go\\\" path=\\\"main.go\\\">\\npackage\",\n\t\t\tmanualStop: []string{\"<PlandexFinish/>\"},\n\t\t\twant: bufferOrStreamResult{\n\t\t\t\tshouldStream: true,\n\t\t\t\tcontent:      \"something\\n```go\\npackage\",\n\t\t\t},\n\t\t\twantState: &chunkProcessor{\n\t\t\t\tawaitingBlockOpeningTag: false,\n\t\t\t\tfileOpen:                true,\n\t\t\t},\n\t\t},\n\t}\n\n\tonly := map[int]bool{}\n\tfor i, tt := range tests {\n\t\tif tt.only {\n\t\t\tonly[i] = true\n\t\t}\n\t}\n\n\tfor i, tt := range tests {\n\t\tif len(only) > 0 && !only[i] {\n\t\t\tcontinue\n\t\t}\n\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprocessor := tt.initialState\n\n\t\t\tgot := processor.bufferOrStream(tt.chunk, &types.ReplyParserRes{\n\t\t\t\tMaybeFilePath:   tt.maybeFilePath,\n\t\t\t\tCurrentFilePath: tt.currentFilePath,\n\t\t\t\tIsInMoveBlock:   tt.isInMoveBlock,\n\t\t\t\tIsInRemoveBlock: tt.isInRemoveBlock,\n\t\t\t\tIsInResetBlock:  tt.isInResetBlock,\n\t\t\t}, shared.CurrentStage{\n\t\t\t\tTellStage: shared.TellStageImplementation,\n\t\t\t}, tt.manualStop)\n\n\t\t\tif got.shouldStream != tt.want.shouldStream {\n\t\t\t\tt.Errorf(\"shouldStream = %v, want %v\", got.shouldStream, tt.want.shouldStream)\n\t\t\t}\n\t\t\tif got.shouldStream && got.content != tt.want.content {\n\t\t\t\tt.Errorf(\"content = %q, want %q\", got.content, tt.want.content)\n\t\t\t}\n\n\t\t\t// Check all state transitions\n\t\t\tif processor.fileOpen != tt.wantState.fileOpen {\n\t\t\t\tt.Errorf(\"fileOpen = %v, want %v\", processor.fileOpen, tt.wantState.fileOpen)\n\t\t\t}\n\t\t\tif processor.awaitingBlockOpeningTag != tt.wantState.awaitingBlockOpeningTag {\n\t\t\t\tt.Errorf(\"awaitingOpeningTag = %v, want %v\", processor.awaitingBlockOpeningTag, tt.wantState.awaitingBlockOpeningTag)\n\t\t\t}\n\t\t\tif processor.awaitingBlockClosingTag != tt.wantState.awaitingBlockClosingTag {\n\t\t\t\tt.Errorf(\"awaitingClosingTag = %v, want %v\", processor.awaitingBlockClosingTag, tt.wantState.awaitingBlockClosingTag)\n\t\t\t}\n\t\t\tif processor.awaitingBackticks != tt.wantState.awaitingBackticks {\n\t\t\t\tt.Errorf(\"awaitingBackticks = %v, want %v\", processor.awaitingBackticks, tt.wantState.awaitingBackticks)\n\t\t\t}\n\n\t\t\tif tt.wantState.contentBuffer != \"\" {\n\t\t\t\tif processor.contentBuffer != tt.wantState.contentBuffer {\n\t\t\t\t\tt.Errorf(\"content buffer = %q, want %q\", processor.contentBuffer, tt.wantState.contentBuffer)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check buffer is reset when it should be\n\t\t\tif tt.want.shouldStream && processor.contentBuffer != \"\" {\n\t\t\t\tt.Error(\"content buffer should be reset after streaming\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_status.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\tshared \"plandex-shared\"\n\t\"runtime/debug\"\n)\n\ntype handleDescAndExecStatusResult struct {\n\thandleStreamFinishedResult\n\tsubtaskFinished      bool\n\tgeneratedDescription *db.ConvoMessageDescription\n}\n\nfunc (state *activeTellStreamState) handleDescAndExecStatus() handleDescAndExecStatusResult {\n\tcurrentOrgId := state.currentOrgId\n\tsummarizedToMessageId := state.summarizedToMessageId\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\treplyOperations := state.chunkProcessor.replyOperations\n\n\tactive := GetActivePlan(planId, branch)\n\tif active == nil {\n\t\tstate.onActivePlanMissingError()\n\t\treturn handleDescAndExecStatusResult{\n\t\t\thandleStreamFinishedResult: handleStreamFinishedResult{\n\t\t\t\tshouldContinueMainLoop: true,\n\t\t\t\tshouldReturn:           false,\n\t\t\t},\n\t\t}\n\t}\n\n\tvar generatedDescription *db.ConvoMessageDescription\n\tvar subtaskFinished bool\n\n\tvar errCh = make(chan *shared.ApiError, 2)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in genPlanDescription: %v\\n%s\", r, debug.Stack())\n\t\t\t\terrCh <- &shared.ApiError{\n\t\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\t\tMsg:    fmt.Sprintf(\"Error generating plan description: %v\", r),\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tif len(replyOperations) > 0 {\n\t\t\tlog.Println(\"Generating plan description\")\n\n\t\t\tres, err := state.genPlanDescription()\n\t\t\tif err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgeneratedDescription = res\n\t\t\tgeneratedDescription.OrgId = currentOrgId\n\t\t\tgeneratedDescription.SummarizedToMessageId = summarizedToMessageId\n\t\t\tgeneratedDescription.WroteFiles = true\n\t\t\tgeneratedDescription.Operations = replyOperations\n\n\t\t\tlog.Println(\"Generated plan description.\")\n\t\t}\n\t\terrCh <- nil\n\t}()\n\n\tif state.currentStage.TellStage == shared.TellStageImplementation {\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tlog.Printf(\"panic in execStatusShouldContinue: %v\\n%s\", r, debug.Stack())\n\t\t\t\t\terrCh <- &shared.ApiError{\n\t\t\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\t\t\tMsg:    fmt.Sprintf(\"Error getting exec status: %v\", r),\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tlog.Println(\"Getting exec status\")\n\t\t\tvar err *shared.ApiError\n\t\t\tres, err := state.execStatusShouldContinue(active.CurrentReplyContent, active.SessionId, active.Ctx)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsubtaskFinished = res.subtaskFinished\n\n\t\t\tlog.Printf(\"subtaskFinished: %v\\n\", subtaskFinished)\n\n\t\t\terrCh <- nil\n\t\t}()\n\n\t} else {\n\t\terrCh <- nil\n\t}\n\n\tfor i := 0; i < 2; i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tres := state.onError(onErrorParams{\n\t\t\t\tstreamApiErr: err,\n\t\t\t\tstoreDesc:    true,\n\t\t\t})\n\t\t\treturn handleDescAndExecStatusResult{\n\t\t\t\thandleStreamFinishedResult: handleStreamFinishedResult{\n\t\t\t\t\tshouldContinueMainLoop: res.shouldContinueMainLoop,\n\t\t\t\t\tshouldReturn:           res.shouldReturn,\n\t\t\t\t},\n\t\t\t\tsubtaskFinished:      subtaskFinished,\n\t\t\t\tgeneratedDescription: generatedDescription,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn handleDescAndExecStatusResult{\n\t\thandleStreamFinishedResult: handleStreamFinishedResult{},\n\t\tsubtaskFinished:            subtaskFinished,\n\t\tgeneratedDescription:       generatedDescription,\n\t}\n}\n\ntype willContinuePlanParams struct {\n\thasNewSubtasks      bool\n\tremovedSubtasks     bool\n\tallSubtasksFinished bool\n\tactivatePaths       map[string]bool\n\thasExplicitPaths    bool\n}\n\nfunc (state *activeTellStreamState) willContinuePlan(params willContinuePlanParams) bool {\n\thasNewSubtasks := params.hasNewSubtasks\n\tremovedSubtasks := params.removedSubtasks\n\tallSubtasksFinished := params.allSubtasksFinished\n\tactivatePaths := params.activatePaths\n\tcurrentSubtask := state.currentSubtask\n\n\tlog.Printf(\"[willContinuePlan] currentStage: %v\", state.currentStage)\n\n\tlog.Printf(\"[willContinuePlan] Initial state - hasNewSubtasks: %v, allSubtasksFinished: %v, tellStage: %v, planningPhase: %v, iteration: %d, autoContinue: %v\",\n\t\thasNewSubtasks, allSubtasksFinished, state.currentStage.TellStage, state.currentStage.PlanningPhase, state.iteration, state.req.AutoContinue)\n\n\tif state.currentStage.TellStage == shared.TellStagePlanning {\n\t\tlog.Println(\"[willContinuePlan] In planning stage\")\n\n\t\t// always continue to response or planning phase after context phase\n\t\tif state.currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\n\t\t\t// if it's the context stage but it's chat mode and no files were loaded, don't continue\n\t\t\tif state.req.IsChatOnly && len(activatePaths) == 0 {\n\t\t\t\tlog.Println(\"[willContinuePlan] Chat only - no files loaded - stopping\")\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// if no files were listed explicitly in a ### Files section, don't continue if it's chat mode\n\t\t\tif state.req.IsChatOnly && !params.hasExplicitPaths {\n\t\t\t\tlog.Println(\"[willContinuePlan] Chat only - no files loaded - stopping\")\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tlog.Println(\"[willContinuePlan] In context phase - continuing to planning phase\")\n\t\t\treturn true\n\t\t}\n\n\t\tif state.req.IsChatOnly {\n\t\t\tlog.Println(\"[willContinuePlan] Chat only - stopping\")\n\t\t\treturn false\n\t\t}\n\n\t\t// otherwise, if auto-continue is disabled, never continue\n\t\tif !state.req.AutoContinue {\n\t\t\tlog.Println(\"[willContinuePlan] Auto-continue disabled - stopping\")\n\t\t\treturn false\n\t\t}\n\n\t\t// if there are new subtasks, continue\n\t\tif hasNewSubtasks && !allSubtasksFinished {\n\t\t\tlog.Println(\"[willContinuePlan] Has new subtasks - continuing\")\n\t\t\treturn true\n\t\t}\n\n\t\tif removedSubtasks && !allSubtasksFinished {\n\t\t\tlog.Println(\"[willContinuePlan] Removed subtasks - continuing\")\n\t\t\treturn true\n\t\t}\n\n\t\t// if all subtasks are finished, don't continue\n\t\tlog.Printf(\"[willContinuePlan] Checking subtasks finished - allSubtasksFinished: %v, will continue: %v\",\n\t\t\tallSubtasksFinished, !allSubtasksFinished)\n\n\t\tlog.Printf(\"[willContinuePlan] currentSubtask: %v\", currentSubtask)\n\n\t\treturn !allSubtasksFinished && currentSubtask != nil\n\n\t} else if state.currentStage.TellStage == shared.TellStageImplementation {\n\t\tlog.Println(\"[willContinuePlan] In implementation stage\")\n\n\t\t// if all subtasks are finished, don't continue\n\t\tif allSubtasksFinished {\n\t\t\tlog.Println(\"[willContinuePlan] All subtasks finished - stopping\")\n\t\t\treturn false\n\t\t}\n\n\t\t// if we've automatically continued too many times, don't continue\n\t\tif state.iteration >= MaxAutoContinueIterations {\n\t\t\tlog.Printf(\"[willContinuePlan] Reached max iterations (%d) - stopping\", MaxAutoContinueIterations)\n\t\t\treturn false\n\t\t}\n\n\t\t// otherwise, continue with implementation\n\t\tlog.Println(\"[willContinuePlan] Continuing implementation\")\n\t\treturn true\n\t}\n\n\tlog.Printf(\"[willContinuePlan] Unknown tell stage: %v - won't continue\", state.currentStage.TellStage)\n\treturn false\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_store.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype storeOnFinishedParams struct {\n\treplyOperations       []*shared.Operation\n\tgeneratedDescription  *db.ConvoMessageDescription\n\tsubtaskFinished       bool\n\thasNewSubtasks        bool\n\tautoLoadContextResult checkAutoLoadContextResult\n\taddedSubtasks         []*db.Subtask\n\tremovedSubtasks       []string\n}\n\ntype storeOnFinishedResult struct {\n\thandleStreamFinishedResult\n\tallSubtasksFinished bool\n}\n\nfunc (state *activeTellStreamState) storeOnFinished(params storeOnFinishedParams) storeOnFinishedResult {\n\treplyOperations := params.replyOperations\n\tgeneratedDescription := params.generatedDescription\n\tsubtaskFinished := params.subtaskFinished\n\thasNewSubtasks := params.hasNewSubtasks\n\tautoLoadContextResult := params.autoLoadContextResult\n\tcurrentOrgId := state.currentOrgId\n\tcurrentUserId := state.currentUserId\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tsummarizedToMessageId := state.summarizedToMessageId\n\tactive := state.activePlan\n\taddedSubtasks := params.addedSubtasks\n\tremovedSubtasks := params.removedSubtasks\n\tvar allSubtasksFinished bool\n\n\tlog.Println(\"[storeOnFinished] Locking repo to store assistant reply and description\")\n\n\terr := db.ExecRepoOperation(db.ExecRepoOperationParams{\n\t\tOrgId:    currentOrgId,\n\t\tUserId:   currentUserId,\n\t\tPlanId:   planId,\n\t\tBranch:   branch,\n\t\tScope:    db.LockScopeWrite,\n\t\tCtx:      active.Ctx,\n\t\tCancelFn: active.CancelFn,\n\t\tReason:   \"store on finished\",\n\t}, func(repo *db.GitRepo) error {\n\t\tlog.Println(\"storeOnFinished: hasNewSubtasks\", hasNewSubtasks)\n\t\tlog.Println(\"storeOnFinished: subtaskFinished\", subtaskFinished)\n\t\tlog.Println(\"storeOnFinished: removedSubtasks\", removedSubtasks)\n\n\t\tmessageSubtask := state.currentSubtask\n\n\t\t// first resolve subtask state\n\t\tif hasNewSubtasks || len(removedSubtasks) > 0 || subtaskFinished {\n\t\t\tif subtaskFinished && state.currentSubtask != nil {\n\t\t\t\tlog.Printf(\"[storeOnFinished] Marking subtask as finished: %q\", state.currentSubtask.Title)\n\t\t\t\tstate.currentSubtask.IsFinished = true\n\n\t\t\t\tlog.Printf(\"[storeOnFinished] Current subtask state after marking as finished: %+v\", state.currentSubtask)\n\t\t\t}\n\n\t\t\tlog.Printf(\"[storeOnFinished] Storing plan subtasks (hasNewSubtasks=%v, subtaskFinished=%v)\", hasNewSubtasks, subtaskFinished)\n\t\t\tlog.Printf(\"[storeOnFinished] Current subtasks state before storing:\")\n\t\t\tfor i, task := range state.subtasks {\n\t\t\t\tlog.Printf(\"[storeOnFinished] Task %d: %q (finished=%v)\", i+1, task.Title, task.IsFinished)\n\t\t\t}\n\n\t\t\tstate.currentSubtask = nil\n\t\t\tallSubtasksFinished = true\n\t\t\tfor _, subtask := range state.subtasks {\n\t\t\t\tif !subtask.IsFinished {\n\t\t\t\t\tstate.currentSubtask = subtask\n\t\t\t\t\tallSubtasksFinished = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif state.currentSubtask != nil {\n\t\t\t\tlog.Printf(\"[storeOnFinished] Set new current subtask: %q\", state.currentSubtask.Title)\n\t\t\t} else {\n\t\t\t\tlog.Println(\"[storeOnFinished] No new current subtask set\")\n\t\t\t}\n\t\t\tlog.Printf(\"[storeOnFinished] All subtasks finished: %v\", allSubtasksFinished)\n\t\t} else if state.currentSubtask != nil && !subtaskFinished {\n\t\t\tlog.Printf(\"[storeOnFinished] Current subtask is not finished: %q\", state.currentSubtask.Title)\n\t\t\tstate.currentSubtask.NumTries++\n\t\t}\n\n\t\tlog.Println(\"storeOnFinished: state.currentSubtask\", state.currentSubtask)\n\t\tlog.Println(\"storeOnFinished: state.subtasks\", state.subtasks)\n\t\tlog.Println(\"storeOnFinished: state.currentStage\", state.currentStage)\n\n\t\tvar flags shared.ConvoMessageFlags\n\n\t\tflags.CurrentStage = state.currentStage\n\n\t\tif len(replyOperations) > 0 {\n\t\t\tflags.DidWriteCode = true\n\t\t}\n\t\tif hasNewSubtasks {\n\t\t\tlog.Println(\"storeOnFinished: hasNewSubtasks\")\n\t\t\tflags.DidMakePlan = true\n\t\t}\n\t\tif len(removedSubtasks) > 0 {\n\t\t\tlog.Println(\"storeOnFinished: len(removedSubtasks) > 0\")\n\t\t\tflags.DidMakePlan = true\n\t\t\tflags.DidRemoveTasks = true\n\t\t}\n\t\tif len(autoLoadContextResult.autoLoadPaths) > 0 {\n\t\t\tflags.DidLoadContext = true\n\t\t}\n\t\tif subtaskFinished && messageSubtask != nil {\n\t\t\tflags.DidCompleteTask = true\n\t\t}\n\t\tif allSubtasksFinished {\n\t\t\tlog.Println(\"storeOnFinished: allSubtasksFinished\")\n\t\t\tflags.DidCompletePlan = true\n\t\t}\n\t\tif hasNewSubtasks && (state.req.IsApplyDebug || state.req.IsUserDebug) {\n\t\t\tlog.Println(\"storeOnFinished: hasNewSubtasks && (state.req.IsApplyDebug || state.req.IsUserDebug)\")\n\t\t\tflags.DidMakeDebuggingPlan = true\n\t\t}\n\n\t\tlog.Println(\"storeOnFinished: flags\", flags)\n\n\t\tassistantMsg, convoCommitMsg, err := state.storeAssistantReply(repo, storeAssistantReplyParams{\n\t\t\tflags:                flags,\n\t\t\tsubtask:              messageSubtask,\n\t\t\taddedSubtasks:        addedSubtasks,\n\t\t\tactivatePaths:        autoLoadContextResult.activatePaths,\n\t\t\tactivatePathsOrdered: autoLoadContextResult.activatePathsOrdered,\n\t\t\tremovedSubtasks:      removedSubtasks,\n\t\t}) // updates state.convo\n\n\t\tif err != nil {\n\t\t\tstate.onError(onErrorParams{\n\t\t\t\tstreamErr: fmt.Errorf(\"failed to store assistant message: %v\", err),\n\t\t\t\tstoreDesc: true,\n\t\t\t})\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Println(\"getting description for assistant message: \", assistantMsg.Id)\n\n\t\tvar description *db.ConvoMessageDescription\n\t\tif len(replyOperations) == 0 {\n\t\t\tdescription = &db.ConvoMessageDescription{\n\t\t\t\tOrgId:                 currentOrgId,\n\t\t\t\tPlanId:                planId,\n\t\t\t\tConvoMessageId:        assistantMsg.Id,\n\t\t\t\tSummarizedToMessageId: summarizedToMessageId,\n\t\t\t\tBuildPathsInvalidated: map[string]bool{},\n\t\t\t\tWroteFiles:            false,\n\t\t\t}\n\t\t} else {\n\t\t\tdescription = generatedDescription\n\t\t\tdescription.ConvoMessageId = assistantMsg.Id\n\t\t}\n\n\t\tlog.Println(\"[storeOnFinished] Storing description\")\n\t\terr = db.StoreDescription(description)\n\n\t\tif err != nil {\n\t\t\tstate.onError(onErrorParams{\n\t\t\t\tstreamErr:      fmt.Errorf(\"failed to store description: %v\", err),\n\t\t\t\tstoreDesc:      false,\n\t\t\t\tconvoMessageId: assistantMsg.Id,\n\t\t\t\tcommitMsg:      convoCommitMsg,\n\t\t\t})\n\t\t\treturn err\n\t\t}\n\t\tlog.Println(\"[storeOnFinished] Description stored\")\n\n\t\t// store subtasks\n\t\terr = db.StorePlanSubtasks(currentOrgId, planId, state.subtasks)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error storing plan subtasks: %v\\n\", err)\n\t\t\tstate.onError(onErrorParams{\n\t\t\t\tstreamErr:      fmt.Errorf(\"failed to store plan subtasks: %v\", err),\n\t\t\t\tstoreDesc:      false,\n\t\t\t\tconvoMessageId: assistantMsg.Id,\n\t\t\t\tcommitMsg:      convoCommitMsg,\n\t\t\t})\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Println(\"Comitting after store on finished\")\n\n\t\terr = repo.GitAddAndCommit(branch, convoCommitMsg)\n\t\tif err != nil {\n\t\t\tstate.onError(onErrorParams{\n\t\t\t\tstreamErr:      fmt.Errorf(\"failed to commit: %v\", err),\n\t\t\t\tstoreDesc:      false,\n\t\t\t\tconvoMessageId: assistantMsg.Id,\n\t\t\t\tcommitMsg:      convoCommitMsg,\n\t\t\t})\n\t\t\treturn err\n\t\t}\n\t\tlog.Println(\"Assistant reply, description, and subtasks committed\")\n\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Error storing on finished: %v\\n\", err)\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error storing on finished: %v\", err))\n\n\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"Error storing on finished: %v\", err),\n\t\t}\n\t\treturn storeOnFinishedResult{\n\t\t\thandleStreamFinishedResult: handleStreamFinishedResult{\n\t\t\t\tshouldContinueMainLoop: true,\n\t\t\t\tshouldReturn:           false,\n\t\t\t},\n\t\t\tallSubtasksFinished: false,\n\t\t}\n\t}\n\n\treturn storeOnFinishedResult{\n\t\thandleStreamFinishedResult: handleStreamFinishedResult{},\n\t\tallSubtasksFinished:        allSubtasksFinished,\n\t}\n}\n\ntype storeAssistantReplyParams struct {\n\tflags                shared.ConvoMessageFlags\n\tsubtask              *db.Subtask\n\taddedSubtasks        []*db.Subtask\n\tactivatePaths        map[string]bool\n\tactivatePathsOrdered []string\n\tremovedSubtasks      []string\n}\n\nfunc (state *activeTellStreamState) storeAssistantReply(repo *db.GitRepo, params storeAssistantReplyParams) (*db.ConvoMessage, string, error) {\n\tflags := params.flags\n\tsubtask := params.subtask\n\taddedSubtasks := params.addedSubtasks\n\tactivatePaths := params.activatePaths\n\tactivatePathsOrdered := params.activatePathsOrdered\n\tremovedSubtasks := params.removedSubtasks\n\n\tcurrentOrgId := state.currentOrgId\n\tcurrentUserId := state.currentUserId\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tauth := state.auth\n\treplyNumTokens := state.replyNumTokens\n\treplyId := state.replyId\n\tconvo := state.convo\n\tnum := len(convo) + 1\n\n\tlog.Printf(\"storing assistant reply | len(convo) %d | num %d\\n\", len(convo), num)\n\n\tactivePlan := state.activePlan\n\n\t// fmt.Println(\"raw message: \", activePlan.CurrentReplyContent)\n\n\tassistantMsg := db.ConvoMessage{\n\t\tId:                    replyId,\n\t\tOrgId:                 currentOrgId,\n\t\tPlanId:                planId,\n\t\tUserId:                currentUserId,\n\t\tRole:                  openai.ChatMessageRoleAssistant,\n\t\tTokens:                replyNumTokens,\n\t\tNum:                   num,\n\t\tMessage:               activePlan.CurrentReplyContent,\n\t\tFlags:                 flags,\n\t\tSubtask:               subtask,\n\t\tAddedSubtasks:         addedSubtasks,\n\t\tActivatedPaths:        activatePaths,\n\t\tActivatedPathsOrdered: activatePathsOrdered,\n\t\tRemovedSubtasks:       removedSubtasks,\n\t}\n\n\tcommitMsg, err := db.StoreConvoMessage(repo, &assistantMsg, auth.User.Id, branch, false)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error storing assistant message: %v\\n\", err)\n\t\treturn nil, \"\", err\n\t}\n\n\tUpdateActivePlan(planId, branch, func(ap *types.ActivePlan) {\n\t\tap.MessageNum = num\n\t\tap.StoredReplyIds = append(ap.StoredReplyIds, replyId)\n\t})\n\n\tconvo = append(convo, &assistantMsg)\n\tstate.convo = convo\n\n\treturn &assistantMsg, commitMsg, err\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_stream_usage.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/hooks\"\n\t\"plandex-server/notify\"\n\t\"runtime/debug\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (state *activeTellStreamState) handleUsageChunk(usage *openai.Usage) {\n\tauth := state.auth\n\tplan := state.plan\n\tgenerationId := state.generationId\n\n\tlog.Println(\"Tell stream usage:\")\n\tlog.Println(spew.Sdump(usage))\n\n\tvar cachedTokens int\n\tif usage.PromptTokensDetails != nil {\n\t\tcachedTokens = usage.PromptTokensDetails.CachedTokens\n\t}\n\n\tsessionId := state.activePlan.SessionId\n\n\tmodelConfig := state.modelConfig\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in handleUsageChunk: %v\\n%s\", r, debug.Stack())\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic in handleUsageChunk: %v\\n%s\", r, debug.Stack()))\n\t\t\t}\n\t\t}()\n\n\t\t_, apiErr := hooks.ExecHook(hooks.DidSendModelRequest, hooks.HookParams{\n\t\t\tAuth: auth,\n\t\t\tPlan: plan,\n\t\t\tDidSendModelRequestParams: &hooks.DidSendModelRequestParams{\n\t\t\t\tInputTokens:    usage.PromptTokens,\n\t\t\t\tOutputTokens:   usage.CompletionTokens,\n\t\t\t\tCachedTokens:   cachedTokens,\n\t\t\t\tModelId:        baseModelConfig.ModelId,\n\t\t\t\tModelTag:       baseModelConfig.ModelTag,\n\t\t\t\tModelName:      baseModelConfig.ModelName,\n\t\t\t\tModelProvider:  baseModelConfig.Provider,\n\t\t\t\tModelPackName:  state.settings.GetModelPack().Name,\n\t\t\t\tModelRole:      modelConfig.Role,\n\t\t\t\tPurpose:        \"Response\",\n\t\t\t\tGenerationId:   generationId,\n\t\t\t\tPlanId:         plan.Id,\n\t\t\t\tModelStreamId:  state.modelStreamId,\n\t\t\t\tConvoMessageId: state.replyId,\n\n\t\t\t\tRequestStartedAt: state.requestStartedAt,\n\t\t\t\tStreaming:        true,\n\t\t\t\tFirstTokenAt:     state.firstTokenAt,\n\t\t\t\tReq:              state.originalReq,\n\t\t\t\tStreamResult:     state.activePlan.CurrentReplyContent,\n\t\t\t\tModelConfig:      state.modelConfig,\n\n\t\t\t\tSessionId: sessionId,\n\t\t\t},\n\t\t})\n\n\t\tif apiErr != nil {\n\t\t\tlog.Printf(\"handleUsageChunk - error executing DidSendModelRequest hook: %v\", apiErr)\n\t\t}\n\t}()\n}\n\nfunc (state *activeTellStreamState) execHookOnStop(sendStreamErr bool) {\n\tgenerationId := state.generationId\n\n\tlog.Printf(\"execHookOnStop - sendStreamErr: %t\\n\", sendStreamErr)\n\n\tplanId := state.plan.Id\n\tbranch := state.branch\n\tauth := state.auth\n\tplan := state.plan\n\tactive := GetActivePlan(planId, branch)\n\n\tif active == nil {\n\t\tlog.Printf(\" Active plan not found for plan ID %s on branch %s\\n\", planId, branch)\n\t\treturn\n\t}\n\n\tmodelConfig := state.modelConfig\n\tbaseModelConfig := modelConfig.GetBaseModelConfig(state.authVars, state.settings, state.orgUserConfig)\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in execHookOnStop: %v\\n%s\", r, debug.Stack())\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic in execHookOnStop: %v\\n%s\", r, debug.Stack()))\n\t\t\t}\n\t\t}()\n\n\t\t_, apiErr := hooks.ExecHook(hooks.DidSendModelRequest, hooks.HookParams{\n\t\t\tAuth: auth,\n\t\t\tPlan: plan,\n\t\t\tDidSendModelRequestParams: &hooks.DidSendModelRequestParams{\n\t\t\t\tInputTokens:     state.totalRequestTokens,\n\t\t\t\tOutputTokens:    active.NumTokens,\n\t\t\t\tModelId:         baseModelConfig.ModelId,\n\t\t\t\tModelTag:        baseModelConfig.ModelTag,\n\t\t\t\tModelName:       baseModelConfig.ModelName,\n\t\t\t\tModelProvider:   baseModelConfig.Provider,\n\t\t\t\tModelPackName:   state.settings.GetModelPack().Name,\n\t\t\t\tModelRole:       modelConfig.Role,\n\t\t\t\tPurpose:         \"Response\",\n\t\t\t\tGenerationId:    generationId,\n\t\t\t\tPlanId:          plan.Id,\n\t\t\t\tModelStreamId:   state.modelStreamId,\n\t\t\t\tConvoMessageId:  state.replyId,\n\t\t\t\tStoppedEarly:    true,\n\t\t\t\tUserCancelled:   !sendStreamErr,\n\t\t\t\tHadError:        sendStreamErr,\n\t\t\t\tNoReportedUsage: true,\n\n\t\t\t\tRequestStartedAt: state.requestStartedAt,\n\t\t\t\tStreaming:        true,\n\t\t\t\tFirstTokenAt:     state.firstTokenAt,\n\t\t\t\tReq:              state.originalReq,\n\t\t\t\tStreamResult:     state.activePlan.CurrentReplyContent,\n\t\t\t\tModelConfig:      state.modelConfig,\n\n\t\t\t\tSessionId: active.SessionId,\n\t\t\t},\n\t\t})\n\n\t\tif apiErr != nil {\n\t\t\tlog.Printf(\"execHookOnStop - error executing DidSendModelRequest hook: %v\", apiErr)\n\t\t}\n\t}()\n\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_subtasks.go",
    "content": "package plan\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model/parse\"\n\tshared \"plandex-shared\"\n\t\"strings\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\nfunc (state *activeTellStreamState) formatSubtasks() string {\n\tsubtasksText := \"### LATEST PLAN TASKS ###\\n\\n\"\n\n\tvar current *db.Subtask\n\n\tfor idx, subtask := range state.subtasks {\n\t\tsubtasksText += fmt.Sprintf(\"%d. %s\\n\", idx+1, subtask.Title)\n\t\tif subtask.Description != \"\" {\n\t\t\tsubtasksText += \"\\n\" + subtask.Description + \"\\n\"\n\t\t}\n\t\tif len(subtask.UsesFiles) > 0 {\n\t\t\tsubtasksText += \"Uses: \"\n\t\t\tusesFiles := []string{}\n\t\t\tfor _, file := range subtask.UsesFiles {\n\t\t\t\tusesFiles = append(usesFiles, fmt.Sprintf(\"`%s`\", file))\n\t\t\t}\n\t\t\tsubtasksText += strings.Join(usesFiles, \", \") + \"\\n\"\n\t\t}\n\t\tsubtasksText += \"Done: \"\n\t\tif subtask.IsFinished {\n\t\t\tsubtasksText += \"yes\"\n\t\t} else {\n\t\t\tsubtasksText += \"no\"\n\t\t}\n\t\tsubtasksText += \"\\n\"\n\n\t\tif state.currentSubtask != nil && subtask.Title == state.currentSubtask.Title && state.currentStage.TellStage == shared.TellStageImplementation {\n\t\t\tcurrent = subtask\n\t\t\tsubtasksText += \"Current subtask: yes\"\n\t\t}\n\n\t\tsubtasksText += \"\\n\"\n\t}\n\n\tif current != nil && state.currentStage.TellStage == shared.TellStageImplementation {\n\t\tsubtasksText += fmt.Sprintf(\"\\n### Current subtask\\n%s\\n\", current.Title)\n\t\tif current.Description != \"\" {\n\t\t\tsubtasksText += \"\\n\" + current.Description + \"\\n\"\n\t\t}\n\t\tif len(current.UsesFiles) > 0 {\n\t\t\tsubtasksText += \"Uses: \"\n\t\t\tusesFiles := []string{}\n\t\t\tfor _, file := range current.UsesFiles {\n\t\t\t\tusesFiles = append(usesFiles, fmt.Sprintf(\"`%s`\", file))\n\t\t\t}\n\t\t\tsubtasksText += strings.Join(usesFiles, \", \") + \"\\n\"\n\t\t}\n\t} else if state.currentStage.TellStage == shared.TellStagePlanning {\n\t\tif state.currentStage.PlanningPhase == shared.PlanningPhaseTasks {\n\t\t\tsubtasksText += `\n\t\t\t\n\t\t\tRemember, 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.\n\n\t\t`\n\t\t} else if state.currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\tsubtasksText += `\n\t\t\t\n\t\t\tRemember, 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.\n\n\t\t`\n\t\t}\n\t}\n\n\treturn subtasksText\n}\n\ntype checkNewSubtasksResult struct {\n\thasExplicitTasks bool\n\tnewSubtasks      []*db.Subtask\n}\n\nfunc (state *activeTellStreamState) checkNewSubtasks() checkNewSubtasksResult {\n\tactivePlan := GetActivePlan(state.plan.Id, state.branch)\n\n\tif activePlan == nil {\n\t\treturn checkNewSubtasksResult{\n\t\t\thasExplicitTasks: false,\n\t\t\tnewSubtasks:      nil,\n\t\t}\n\t}\n\n\tcontent := activePlan.CurrentReplyContent\n\n\tsubtasks := parse.ParseSubtasks(content)\n\n\tif len(subtasks) == 0 {\n\t\tlog.Println(\"No new subtasks found\")\n\t\treturn checkNewSubtasksResult{\n\t\t\thasExplicitTasks: false,\n\t\t\tnewSubtasks:      nil,\n\t\t}\n\t}\n\n\tlog.Println(\"Found new subtasks:\", len(subtasks))\n\t// log.Println(spew.Sdump(subtasks))\n\n\tsubtasksByName := map[string]*db.Subtask{}\n\n\t// Only index unfinished subtasks by name\n\tfor _, subtask := range state.subtasks {\n\t\tif !subtask.IsFinished {\n\t\t\tsubtasksByName[subtask.Title] = subtask\n\t\t}\n\t}\n\n\tvar newSubtasks []*db.Subtask\n\tvar updatedSubtasks []*db.Subtask\n\n\t// Keep finished subtasks\n\tfor _, subtask := range state.subtasks {\n\t\tif subtask.IsFinished {\n\t\t\tupdatedSubtasks = append(updatedSubtasks, subtask)\n\t\t}\n\t}\n\n\t// Add new subtasks if they don't exist\n\tfor _, subtask := range subtasks {\n\t\tif subtasksByName[subtask.Title] == nil {\n\t\t\tnewSubtasks = append(newSubtasks, subtask)\n\t\t\tupdatedSubtasks = append(updatedSubtasks, subtask)\n\t\t}\n\t}\n\n\tstate.subtasks = updatedSubtasks\n\n\tvar currentSubtaskName string\n\tif state.currentSubtask != nil {\n\t\tcurrentSubtaskName = state.currentSubtask.Title\n\t}\n\n\tfound := false\n\tfor _, subtask := range state.subtasks {\n\t\tif subtask.Title == currentSubtaskName {\n\t\t\tfound = true\n\t\t\tstate.currentSubtask = subtask\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tstate.currentSubtask = nil\n\t}\n\n\tif state.currentSubtask == nil {\n\t\tfor _, subtask := range state.subtasks {\n\t\t\tif !subtask.IsFinished {\n\t\t\t\tstate.currentSubtask = subtask\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// log.Println(\"state.subtasks:\\n\", spew.Sdump(state.subtasks))\n\tlog.Println(\"state.currentSubtask:\\n\", spew.Sdump(state.currentSubtask))\n\n\treturn checkNewSubtasksResult{\n\t\thasExplicitTasks: len(subtasks) > 0,\n\t\tnewSubtasks:      newSubtasks,\n\t}\n}\n\ntype checkRemoveSubtasksResult struct {\n\thasExplicitRemoveTasks bool\n\tremovedSubtasks        []string\n}\n\nfunc (state *activeTellStreamState) checkRemoveSubtasks() checkRemoveSubtasksResult {\n\tactivePlan := GetActivePlan(state.plan.Id, state.branch)\n\n\tif activePlan == nil {\n\t\treturn checkRemoveSubtasksResult{\n\t\t\thasExplicitRemoveTasks: false,\n\t\t\tremovedSubtasks:        nil,\n\t\t}\n\t}\n\n\tcontent := activePlan.CurrentReplyContent\n\n\t// Parse tasks to remove\n\ttasksToRemove := parse.ParseRemoveSubtasks(content)\n\n\tif len(tasksToRemove) == 0 {\n\t\tlog.Println(\"No tasks to remove found\")\n\t\treturn checkRemoveSubtasksResult{\n\t\t\thasExplicitRemoveTasks: false,\n\t\t\tremovedSubtasks:        nil,\n\t\t}\n\t}\n\n\tlog.Println(\"Found tasks to remove:\", len(tasksToRemove))\n\t// log.Println(spew.Sdump(tasksToRemove))\n\n\t// Create a map of task titles to remove for efficient lookup\n\tremoveMap := make(map[string]bool)\n\tfor _, task := range tasksToRemove {\n\t\tremoveMap[task] = true\n\t}\n\n\tvar removedSubtasks []*db.Subtask\n\tvar remainingSubtasks []*db.Subtask\n\n\t// Keep tasks that aren't in the remove list\n\tfor _, subtask := range state.subtasks {\n\t\tif removeMap[subtask.Title] {\n\t\t\t// Only track unfinished tasks that are being removed\n\t\t\tif !subtask.IsFinished {\n\t\t\t\tremovedSubtasks = append(removedSubtasks, subtask)\n\t\t\t}\n\t\t} else {\n\t\t\tremainingSubtasks = append(remainingSubtasks, subtask)\n\t\t}\n\t}\n\n\tstate.subtasks = remainingSubtasks\n\n\t// Update current subtask if it was removed\n\tif state.currentSubtask != nil && removeMap[state.currentSubtask.Title] {\n\t\tstate.currentSubtask = nil\n\t\t// Find the first unfinished subtask to set as current\n\t\tfor _, subtask := range state.subtasks {\n\t\t\tif !subtask.IsFinished {\n\t\t\t\tstate.currentSubtask = subtask\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tremovedSubtaskTitles := []string{}\n\tfor _, subtask := range removedSubtasks {\n\t\tremovedSubtaskTitles = append(removedSubtaskTitles, subtask.Title)\n\t}\n\tlog.Println(\"removedSubtaskTitles:\\n\", spew.Sdump(removedSubtaskTitles))\n\n\treturn checkRemoveSubtasksResult{\n\t\thasExplicitRemoveTasks: len(tasksToRemove) > 0,\n\t\tremovedSubtasks:        removedSubtaskTitles,\n\t}\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_summary.go",
    "content": "package plan\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/types\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nfunc (state *activeTellStreamState) addConversationMessages() bool {\n\tsummaries := state.summaries\n\ttokensBeforeConvo := state.tokensBeforeConvo\n\tactive := GetActivePlan(state.plan.Id, state.branch)\n\n\tconvo := []*db.ConvoMessage{}\n\tfor _, msg := range state.convo {\n\t\tif state.skipConvoMessages != nil && state.skipConvoMessages[msg.Id] {\n\t\t\tcontinue\n\t\t}\n\t\tconvo = append(convo, msg)\n\t}\n\n\tif active == nil {\n\t\tlog.Println(\"summarizeMessagesIfNeeded - Active plan not found\")\n\t\treturn false\n\t}\n\n\tconversationTokens := 0\n\ttokensUpToTimestamp := make(map[int64]int)\n\tconvoMessagesById := make(map[string]*db.ConvoMessage)\n\tfor _, convoMessage := range convo {\n\t\tconversationTokens += convoMessage.Tokens + model.TokensPerMessage + model.TokensPerName\n\t\ttimestamp := convoMessage.CreatedAt.UnixNano() / int64(time.Millisecond)\n\t\ttokensUpToTimestamp[timestamp] = conversationTokens\n\t\tconvoMessagesById[convoMessage.Id] = convoMessage\n\t\t// log.Printf(\"Timestamp: %s | Tokens: %d | Total: %d | conversationTokens\\n\", convoMessage.Timestamp, convoMessage.Tokens, conversationTokens)\n\t}\n\n\tlog.Printf(\"Conversation tokens: %d\\n\", conversationTokens)\n\tlog.Printf(\"Max conversation tokens: %d\\n\", state.settings.GetPlannerMaxConvoTokens())\n\n\t// log.Println(\"Tokens up to timestamp:\")\n\t// spew.Dump(tokensUpToTimestamp)\n\n\tlog.Printf(\"Total tokens: %d\\n\", tokensBeforeConvo+conversationTokens)\n\tlog.Printf(\"Max tokens: %d\\n\", state.settings.GetPlannerEffectiveMaxTokens())\n\n\tvar summary *db.ConvoSummary\n\tif (tokensBeforeConvo+conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens() ||\n\t\tconversationTokens > state.settings.GetPlannerMaxConvoTokens() {\n\t\tlog.Println(\"Token limit exceeded. Attempting to reduce via conversation summary.\")\n\n\t\t// log.Printf(\"(tokensBeforeConvo+conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens(): %v\\n\", (tokensBeforeConvo+conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens())\n\t\t// log.Printf(\"conversationTokens > state.settings.GetPlannerMaxConvoTokens(): %v\\n\", conversationTokens > state.settings.GetPlannerMaxConvoTokens())\n\n\t\tlog.Printf(\"Num summaries: %d\\n\", len(summaries))\n\n\t\t// token limit exceeded after adding conversation\n\t\t// get summary for as much as the conversation as necessary to stay under the token limit\n\t\tfor _, s := range summaries {\n\t\t\ttimestamp := s.LatestConvoMessageCreatedAt.UnixNano() / int64(time.Millisecond)\n\n\t\t\ttokens, ok := tokensUpToTimestamp[timestamp]\n\n\t\t\tlog.Printf(\"Last message timestamp: %d | found: %v\\n\", timestamp, ok)\n\t\t\tlog.Printf(\"Tokens up to timestamp: %d\\n\", tokens)\n\n\t\t\tif !ok {\n\t\t\t\t// try a fallback by id instead of timestamp, in case timestamp rounding caused it to be missing\n\t\t\t\tconvoMessage, ok := convoMessagesById[s.LatestConvoMessageId]\n\n\t\t\t\tif ok {\n\t\t\t\t\ttimestamp = convoMessage.CreatedAt.UnixNano() / int64(time.Millisecond)\n\t\t\t\t\ttokens, ok = tokensUpToTimestamp[timestamp]\n\t\t\t\t}\n\n\t\t\t\tif !ok {\n\t\t\t\t\t// instead of erroring here as we did previously, we'll just log and continue\n\t\t\t\t\t// if no summary is found, we still handle it as an error below\n\t\t\t\t\t// but this way we don't error out completely for  a single detached summary\n\n\t\t\t\t\tlog.Println(\"conversation summary timestamp not found in conversation\")\n\t\t\t\t\tlog.Println(\"timestamp:\", timestamp)\n\n\t\t\t\t\t// log.Println(\"Conversation summary:\")\n\t\t\t\t\t// spew.Dump(s)\n\n\t\t\t\t\tlog.Println(\"tokensUpToTimestamp:\")\n\t\t\t\t\tlog.Println(spew.Sdump(tokensUpToTimestamp))\n\n\t\t\t\t\tgo notify.NotifyErr(notify.SeverityInfo, fmt.Errorf(\"conversation summary timestamp not found in conversation\"))\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tupdatedConversationTokens := (conversationTokens - tokens) + s.Tokens\n\t\t\tsavedTokens := conversationTokens - updatedConversationTokens\n\n\t\t\tlog.Printf(\"Conversation summary tokens: %d\\n\", tokens)\n\t\t\tlog.Printf(\"Updated conversation tokens: %d\\n\", updatedConversationTokens)\n\t\t\tlog.Printf(\"Saved tokens: %d\\n\", savedTokens)\n\n\t\t\tif updatedConversationTokens <= state.settings.GetPlannerMaxConvoTokens() &&\n\t\t\t\t(tokensBeforeConvo+updatedConversationTokens) <= state.settings.GetPlannerEffectiveMaxTokens() {\n\t\t\t\tlog.Printf(\"Summarizing up to %s | saving %d tokens\\n\", s.LatestConvoMessageCreatedAt.Format(time.RFC3339), savedTokens)\n\t\t\t\tsummary = s\n\t\t\t\tconversationTokens = updatedConversationTokens\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif summary == nil && tokensBeforeConvo+conversationTokens > state.settings.GetPlannerEffectiveMaxTokens() {\n\t\t\terr := errors.New(\"couldn't get under token limit with conversation summary\")\n\t\t\tlog.Printf(\"Error: %v\\n\", err)\n\t\t\tgo notify.NotifyErr(notify.SeverityInfo, fmt.Errorf(\"couldn't get under token limit with conversation summary\"))\n\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"Couldn't get under token limit with conversation summary\",\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t}\n\n\tvar latestSummary *db.ConvoSummary\n\tif len(summaries) > 0 {\n\t\tlatestSummary = summaries[len(summaries)-1]\n\t}\n\n\tif summary == nil {\n\t\tfor _, convoMessage := range convo {\n\t\t\t// this gets added later in tell_exec.go\n\t\t\tif state.promptConvoMessage != nil && convoMessage.Id == state.promptConvoMessage.Id {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\tText: convoMessage.Message,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t// 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\n\t\t\tif latestSummary != nil && convoMessage.Id == latestSummary.LatestConvoMessageId {\n\t\t\t\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\t\t\t\tRole: openai.ChatMessageRoleAssistant,\n\t\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\t\tText: latestSummary.Summary,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif (tokensBeforeConvo + conversationTokens) > state.settings.GetPlannerEffectiveMaxTokens() {\n\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"token limit still exceeded after summarizing conversation\"))\n\n\t\t\tactive.StreamDoneCh <- &shared.ApiError{\n\t\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\t\tStatus: http.StatusInternalServerError,\n\t\t\t\tMsg:    \"Token limit still exceeded after summarizing conversation\",\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t\tstate.summarizedToMessageId = summary.LatestConvoMessageId\n\t\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\t\tRole: openai.ChatMessageRoleAssistant,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: summary.Summary,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t\t// add messages after the last message in the summary\n\t\tfor _, convoMessage := range convo {\n\t\t\t// this gets added later in tell_exec.go\n\t\t\tif state.promptConvoMessage != nil && convoMessage.Id == state.promptConvoMessage.Id {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif convoMessage.CreatedAt.After(summary.LatestConvoMessageCreatedAt) {\n\t\t\t\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\t\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\t\tText: convoMessage.Message,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\t// 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\n\t\t\t\tif latestSummary != nil && convoMessage.Id == latestSummary.LatestConvoMessageId {\n\t\t\t\t\tstate.messages = append(state.messages, types.ExtendedChatMessage{\n\t\t\t\t\t\tRole: openai.ChatMessageRoleAssistant,\n\t\t\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\t\t\tText: latestSummary.Summary,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\ntype summarizeConvoParams struct {\n\tauth                  *types.ServerAuth\n\tplan                  *db.Plan\n\tbranch                string\n\tconvo                 []*db.ConvoMessage\n\tsummaries             []*db.ConvoSummary\n\tuserPrompt            string\n\tcurrentReply          string\n\tcurrentReplyNumTokens int\n\tcurrentOrgId          string\n\tmodelPackName         string\n}\n\nfunc summarizeConvo(clients map[string]model.ClientInfo, authVars map[string]string, settings *shared.PlanSettings, orgUserConfig *shared.OrgUserConfig, params summarizeConvoParams, ctx context.Context) *shared.ApiError {\n\tplan := params.plan\n\tplanId := plan.Id\n\tlog.Printf(\"summarizeConvo: Called for plan ID %s on branch %s\\n\", planId, params.branch)\n\tlog.Printf(\"summarizeConvo: Starting summarizeConvo for planId: %s\\n\", planId)\n\n\tbranch := params.branch\n\tconvo := params.convo\n\tsummaries := params.summaries\n\tuserPrompt := params.userPrompt\n\tcurrentReply := params.currentReply\n\tactive := GetActivePlan(planId, branch)\n\n\tconfig := settings.GetModelPack().PlanSummary\n\n\tif active == nil {\n\t\tlog.Printf(\"Active plan not found for plan ID %s and branch %s\\n\", planId, branch)\n\n\t\treturn &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"active plan not found for plan ID %s and branch %s\", planId, branch),\n\t\t}\n\t}\n\n\tlog.Println(\"Generating plan summary for planId:\", planId)\n\n\t// log.Printf(\"planId: %s\\n\", planId)\n\t// log.Printf(\"convo: \")\n\t// spew.Dump(convo)\n\t// log.Printf(\"summaries: \")\n\t// spew.Dump(summaries)\n\t// log.Printf(\"promptMessage: \")\n\t// spew.Dump(promptMessage)\n\t// log.Printf(\"currentOrgId: %s\\n\", currentOrgId)\n\n\tvar summaryMessages []*types.ExtendedChatMessage\n\tvar latestSummary *db.ConvoSummary\n\tvar numMessagesSummarized int = 0\n\tvar latestMessageSummarizedAt time.Time\n\tvar latestMessageId string\n\tif len(summaries) > 0 {\n\t\tlatestSummary = summaries[len(summaries)-1]\n\t\tnumMessagesSummarized = latestSummary.NumMessages\n\t}\n\n\t// log.Println(\"Generating plan summary - latest summary:\")\n\t// spew.Dump(latestSummary)\n\n\t// log.Println(\"Generating plan summary - convo:\")\n\t// spew.Dump(convo)\n\n\tnumTokens := 0\n\n\tif latestSummary == nil {\n\t\tfor _, convoMessage := range convo {\n\t\t\tsummaryMessages = append(summaryMessages, &types.ExtendedChatMessage{\n\t\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\tText: convoMessage.Message,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t\tlatestMessageId = convoMessage.Id\n\t\t\tlatestMessageSummarizedAt = convoMessage.CreatedAt\n\t\t\tnumMessagesSummarized++\n\t\t\tnumTokens += convoMessage.Tokens + model.TokensPerMessage + model.TokensPerName\n\t\t}\n\t} else {\n\t\tsummaryMessages = append(summaryMessages, &types.ExtendedChatMessage{\n\t\t\tRole: openai.ChatMessageRoleAssistant,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: latestSummary.Summary,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t\tnumTokens += latestSummary.Tokens + model.TokensPerMessage + model.TokensPerName\n\n\t\tvar found bool\n\t\tfor _, convoMessage := range convo {\n\t\t\tif convoMessage.Id == latestSummary.LatestConvoMessageId {\n\t\t\t\tfound = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif found {\n\t\t\t\tsummaryMessages = append(summaryMessages, &types.ExtendedChatMessage{\n\t\t\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\t\tText: convoMessage.Message,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tnumMessagesSummarized++\n\t\t\t\tnumTokens += convoMessage.Tokens + model.TokensPerMessage + model.TokensPerName\n\t\t\t}\n\t\t}\n\n\t\tlatestConvoMessage := convo[len(convo)-1]\n\t\tlatestMessageId = latestConvoMessage.Id\n\t\tlatestMessageSummarizedAt = latestConvoMessage.CreatedAt\n\t}\n\n\tlog.Println(\"generating summary - latestMessageId:\", latestMessageId)\n\tlog.Println(\"generating summary - latestMessageSummarizedAt:\", latestMessageSummarizedAt)\n\n\tif userPrompt != \"\" {\n\t\tif userPrompt != prompts.UserContinuePrompt && userPrompt != prompts.AutoContinuePlanningPrompt && userPrompt != prompts.AutoContinueImplementationPrompt {\n\t\t\tsummaryMessages = append(summaryMessages, &types.ExtendedChatMessage{\n\t\t\t\tRole: openai.ChatMessageRoleUser,\n\t\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\tText: userPrompt,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\ttokens := shared.GetNumTokensEstimate(userPrompt)\n\t\t\tnumTokens += tokens + model.TokensPerMessage + model.TokensPerName\n\t\t}\n\t}\n\n\tif currentReply != \"\" {\n\t\tsummaryMessages = append(summaryMessages, &types.ExtendedChatMessage{\n\t\t\tRole: openai.ChatMessageRoleAssistant,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: currentReply,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t\tnumTokens += params.currentReplyNumTokens + model.TokensPerMessage + model.TokensPerName\n\t}\n\n\tlog.Printf(\"Calling model for plan summary. Summarizing %d messages\\n\", len(summaryMessages))\n\n\t// log.Println(\"Generating summary - summary messages:\")\n\t// spew.Dump(summaryMessages)\n\n\t// latestSummaryCh := make(chan *db.ConvoSummary, 1)\n\t// active.LatestSummaryCh = latestSummaryCh\n\n\tsummary, apiErr := model.PlanSummary(clients, authVars, settings, orgUserConfig, config, model.PlanSummaryParams{\n\t\tConversation:                summaryMessages,\n\t\tConversationNumTokens:       numTokens,\n\t\tLatestConvoMessageId:        latestMessageId,\n\t\tLatestConvoMessageCreatedAt: latestMessageSummarizedAt,\n\t\tNumMessages:                 numMessagesSummarized,\n\t\tAuth:                        params.auth,\n\t\tPlan:                        plan,\n\t\tModelPackName:               params.modelPackName,\n\t\tModelStreamId:               active.ModelStreamId,\n\t\tSessionId:                   active.SessionId,\n\t}, ctx)\n\n\tif apiErr != nil {\n\t\tlog.Printf(\"summarizeConvo: Error generating plan summary for plan %s: %v\\n\", planId, apiErr)\n\t\treturn apiErr\n\t}\n\n\tlog.Printf(\"summarizeConvo: Summary generated and stored for plan %s\\n\", planId)\n\n\t// log.Println(\"Generated summary:\")\n\t// spew.Dump(summary)\n\n\terr := db.StoreSummary(summary)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error storing plan summary for plan %s: %v\\n\", planId, err)\n\t\treturn &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"error storing plan summary for plan %s: %v\", planId, err),\n\t\t}\n\t}\n\n\t// latestSummaryCh <- summary\n\n\treturn nil\n}\n"
  },
  {
    "path": "app/server/model/plan/tell_sys_prompt.go",
    "content": "package plan\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nconst AllTasksCompletedMsg = \"All tasks have been completed. There is no current task to implement.\"\n\ntype getTellSysPromptParams struct {\n\tplanStageSharedMsgs   []*types.ExtendedChatMessagePart\n\tplanningPhaseOnlyMsgs []*types.ExtendedChatMessagePart\n\timplementationMsgs    []*types.ExtendedChatMessagePart\n\tcontextTokenLimit     int\n\tdryRunWithoutContext  bool\n}\n\nfunc (state *activeTellStreamState) getTellSysPrompt(params getTellSysPromptParams) ([]types.ExtendedChatMessagePart, error) {\n\tplanningSharedMsgs := params.planStageSharedMsgs\n\tplannerOnlyMsgs := params.planningPhaseOnlyMsgs\n\timplementationMsgs := params.implementationMsgs\n\tcontextTokenLimit := params.contextTokenLimit\n\treq := state.req\n\tactive := state.activePlan\n\tcurrentStage := state.currentStage\n\n\tsysParts := []types.ExtendedChatMessagePart{}\n\n\tcreatePromptParams := prompts.CreatePromptParams{\n\t\tExecMode:          req.ExecEnabled,\n\t\tAutoContext:       req.AutoContext,\n\t\tIsUserDebug:       req.IsUserDebug,\n\t\tIsApplyDebug:      req.IsApplyDebug,\n\t\tIsGitRepo:         req.IsGitRepo,\n\t\tContextTokenLimit: contextTokenLimit,\n\t}\n\n\t// log.Println(\"getTellSysPrompt - prompt params:\", spew.Sdump(params))\n\n\tif currentStage.TellStage == shared.TellStagePlanning {\n\t\tif len(planningSharedMsgs) == 0 && !params.dryRunWithoutContext {\n\t\t\tlog.Println(\"planningSharedMsgs is empty - required for planning stage\")\n\t\t\treturn nil, fmt.Errorf(\"planningSharedMsgs is empty - required for planning stage\")\n\t\t}\n\n\t\tfor _, msg := range planningSharedMsgs {\n\t\t\tsysParts = append(sysParts, *msg)\n\t\t}\n\n\t\tif currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\tlog.Println(\"Planning phase is context -- adding auto context prompt\")\n\n\t\t\tvar txt string\n\t\t\tif req.IsChatOnly {\n\t\t\t\ttxt = prompts.GetAutoContextChatPrompt(createPromptParams)\n\t\t\t} else {\n\t\t\t\ttxt = prompts.GetAutoContextTellPrompt(createPromptParams)\n\t\t\t}\n\n\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\tText: txt,\n\t\t\t\tCacheControl: &types.CacheControlSpec{\n\t\t\t\t\tType: types.CacheControlTypeEphemeral,\n\t\t\t\t},\n\t\t\t})\n\t\t} else if currentStage.PlanningPhase == shared.PlanningPhaseTasks {\n\n\t\t\tvar txt string\n\t\t\tif req.IsChatOnly {\n\t\t\t\ttxt = prompts.GetChatSysPrompt(createPromptParams)\n\t\t\t} else {\n\t\t\t\ttxt = prompts.GetPlanningPrompt(createPromptParams)\n\t\t\t}\n\n\t\t\tif len(state.subtasks) > 0 {\n\t\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: txt,\n\t\t\t\t})\n\t\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: state.formatSubtasks(),\n\t\t\t\t\tCacheControl: &types.CacheControlSpec{\n\t\t\t\t\t\tType: types.CacheControlTypeEphemeral,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: txt,\n\t\t\t\t\tCacheControl: &types.CacheControlSpec{\n\t\t\t\t\t\tType: types.CacheControlTypeEphemeral,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif !req.IsChatOnly {\n\t\t\t\tif len(active.SkippedPaths) > 0 {\n\t\t\t\t\tskippedPrompt := prompts.SkippedPathsPrompt\n\t\t\t\t\tfor skippedPath := range active.SkippedPaths {\n\t\t\t\t\t\tskippedPrompt += fmt.Sprintf(\"- %s\\n\", skippedPath)\n\t\t\t\t\t}\n\t\t\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\t\tText: skippedPrompt,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor _, msg := range plannerOnlyMsgs {\n\t\t\tsysParts = append(sysParts, *msg)\n\t\t}\n\n\t\tif len(implementationMsgs) > 0 {\n\t\t\treturn nil, fmt.Errorf(\"implementationMsgs not supported during planning phase\")\n\t\t}\n\n\t} else if currentStage.TellStage == shared.TellStageImplementation {\n\t\tif state.currentSubtask == nil {\n\t\t\treturn nil, errors.New(AllTasksCompletedMsg)\n\t\t}\n\n\t\tif len(state.subtasks) > 0 {\n\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\tText: prompts.GetImplementationPrompt(state.currentSubtask.Title),\n\t\t\t})\n\t\t\tsysParts = append(sysParts,\n\t\t\t\ttypes.ExtendedChatMessagePart{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: state.formatSubtasks(),\n\t\t\t\t\tCacheControl: &types.CacheControlSpec{\n\t\t\t\t\t\tType: types.CacheControlTypeEphemeral,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t} else {\n\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\tText: prompts.GetImplementationPrompt(state.currentSubtask.Title),\n\t\t\t\tCacheControl: &types.CacheControlSpec{\n\t\t\t\t\tType: types.CacheControlTypeEphemeral,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\tif !req.IsChatOnly {\n\t\t\tif len(active.SkippedPaths) > 0 {\n\t\t\t\tskippedPrompt := prompts.SkippedPathsPrompt\n\t\t\t\tfor skippedPath := range active.SkippedPaths {\n\t\t\t\t\tskippedPrompt += fmt.Sprintf(\"- %s\\n\", skippedPath)\n\t\t\t\t}\n\t\t\t\tsysParts = append(sysParts, types.ExtendedChatMessagePart{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: skippedPrompt,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif implementationMsgs != nil {\n\t\t\tfor _, msg := range implementationMsgs {\n\t\t\t\tsysParts = append(sysParts, *msg)\n\t\t\t}\n\t\t} else if !params.dryRunWithoutContext {\n\t\t\tlog.Println(\"implementationMsgs is nil - required for implementation stage\")\n\t\t\treturn nil, fmt.Errorf(\"implementationMsgs is nil - required for implementation stage\")\n\t\t}\n\n\t\tif planningSharedMsgs != nil {\n\t\t\tlog.Println(\"planningSharedMsgs not supported during implementation stage - only basic or smart context is supported\")\n\t\t\treturn nil, fmt.Errorf(\"planningSharedMsgs not supported during implementation stage - only basic or smart context is supported\")\n\t\t}\n\t}\n\n\treturn sysParts, nil\n}\n"
  },
  {
    "path": "app/server/model/plan/utils.go",
    "content": "package plan\n\nimport (\n\t\"plandex-server/types\"\n\t\"strings\"\n)\n\nfunc StripBackticksWrapper(s string) string {\n\tcheck := strings.TrimSpace(s)\n\tsplit := strings.Split(check, \"\\n\")\n\n\tif len(split) > 2 {\n\t\tfirstLine := strings.TrimSpace(split[0])\n\t\tsecondLine := strings.TrimSpace(split[1])\n\t\tlastLine := strings.TrimSpace(split[len(split)-1])\n\t\tif types.LineMaybeHasFilePath(firstLine) && strings.HasPrefix(secondLine, \"```\") {\n\t\t\tif lastLine == \"```\" {\n\t\t\t\treturn strings.Join(split[1:len(split)-1], \"\\n\")\n\t\t\t}\n\t\t} else if strings.HasPrefix(firstLine, \"```\") && lastLine == \"```\" {\n\t\t\treturn strings.Join(split[1:len(split)-1], \"\\n\")\n\t\t}\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "app/server/model/prompts/apply_exec.go",
    "content": "package prompts\n\nconst ApplyScriptSharedPrompt = `\n## _apply.sh file and command execution\n\n**Execution mode is enabled.** \n\nIn 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\n\n### Core _apply.sh Concepts\n\nThe _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.\n\n#### Core Restrictions\n\nYou ABSOLUTELY MUST NOT:\n- Use _apply.sh to create files or directories (use code blocks instead - necessary directories will be created automatically)\n- Use _apply.sh for file operations (move/remove/reset) on files in context\n- Include shebang lines or error handling (this is handled externally)\n- Give _apply.sh execution privileges (this is handled externally)\n- Tell users to run the script (it runs automatically)\n- Use separate script files unless specifically requested or absolutely necessary due to complexity\n\n#### Safety and Security\n\nBE CAREFUL AND CONSERVATIVE when making changes to the user's machine:\n- Only make changes that are strictly necessary for the plan\n- If a command is highly risky, tell the user to run it themselves\n- 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.\n- Prefer local changes over global system changes (e.g., npm install --save-dev over --global)\n- Only modify files/directories in the root directory of the plan unless specifically directed otherwise\n- Unless some commands are risky/dangerous, include ALL commands in _apply.sh rather than telling users to run them later\n\n#### Avoid User Prompts\n\nAvoid 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.\n\n#### Keep It Lightweight And Simple\n\nThe _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.\n\nDo 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.\n\nABSOLUTELY 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.\n\n#### Startup Logic\n\n` + ApplyScriptStartupLogic + `\n\n❌ DO NOT include complex startup logic or commands with flags in _apply.sh:\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\necho \"Importing project resources...\"\ngodot --headless --quit\n\n# Check if the main scene file exists\nif [ ! -f \"scenes/main.tscn\" ]; then\n   echo \"Error: Main scene file 'scenes/main.tscn' not found.\"\n   exit 1\nfi\n\necho \"Validating main scene file...\"\nif ! godot --headless --check-only --quit scenes/main.tscn; then\n   echo \"Error: The main scene file 'scenes/main.tscn' contains errors.\"\n   exit 1\nfi\n\necho \"Checking for resource loading issues...\"\nif ! godot --headless --check-only --quit project.godot; then\n   echo \"Error: The project contains resource loading issues.\"\n   exit 1\nfi\n\necho \"Starting Godot project...\"\ngodot --position 100,100 --resolution 1280x720 --verbose\n</PlandexBlock>\n\n✅ 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:\n\n- run.sh:\n<PlandexBlock lang=\"bash\" path=\"run.sh\">\n#!/bin/bash\nset -euo pipefail\n\necho \"Importing project resources...\"\ngodot --headless --quit\n\n# Check if the main scene file exists\nif [ ! -f \"scenes/main.tscn\" ]; then\n   echo \"Error: Main scene file 'scenes/main.tscn' not found.\"\n   exit 1\nfi\n\necho \"Validating main scene file...\"\nif ! godot --headless --check-only --quit scenes/main.tscn; then\n   echo \"Error: The main scene file 'scenes/main.tscn' contains errors.\"\n   exit 1\nfi\n\necho \"Checking for resource loading issues...\"\nif ! godot --headless --check-only --quit project.godot; then\n   echo \"Error: The project contains resource loading issues.\"\n   exit 1\nfi\n\necho \"Starting Godot project...\"\ngodot --position 100,100 --resolution 1280x720 --verbose\n</PlandexBlock>\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\nchmod +x run.sh\n./run.sh\n</PlandexBlock>\n\n#### Command Preservation Rules\n\nThe _apply.sh script accumulates commands during the plan:\n- ALL commands must be preserved until successful application\n- Each update ADDS to or MODIFIES existing commands but NEVER removes them\n- When updating an existing command, modify it rather than duplicating it\n- After successful application, the script resets to empty\n- Current state and history of previously executed scripts will be provided in the prompt\n- Use script history to inform what commands might need to be re-run\n\n#### Dependencies and Tools\n\nWhen handling tools and dependencies:\n\n1. Context-based Assumptions:\n- Make reasonable assumptions about installed tools based on:\n  * The user's operating system\n  * Files and paths in the context\n  * Project structure and existing configuration\n  * Conversation history\n- For example, if working with an existing Node.js project (has package.json), do NOT include commands to install Node.js/npm\n- Similarly for other languages/frameworks: don't install Go for a Go project, Python for a Python project, etc.\n\n2. Checking for Tools:\n- For tools that aren't clearly present in context:\n  * Always check if the tool is installed before using it\n  * Either install missing tools or exit with a clear error\n  * Make the check specific and informative\n- If no commands need to be run, do not write anything to _apply.sh\n\n3. Dependency Management:\n- DO NOT install dependencies that are already used in the project\n- Only install new dependencies that are specifically needed for new features\n- When working with an entirely new project, you can include basic tooling installation\n- When adding to an existing project, assume core tooling is present\n\nFor example, in an existing Node.js project:\n❌ DO NOT: Install Node.js or npm\n❌ DO NOT: Reinstall dependencies listed in package.json\n✅ DO: Install only new packages needed for new features\n✅ DO: Check for specific tools needed for new functionality\n\n#### Avoid Heavy Commands Unless Directed\n\nYou must be conservative about running 'heavy' commands like tests that could be slow or resource intensive to run.\n\nThis also applies to other potentially heavy commands like building Docker images. Use your best judgement.\n\n#### Additional Requirements\n\nScript execution:\n- Assumes bash/zsh shell is available (OS/shell details provided in prompt)\n- The script runs in the root directory of the plan\n- All commands execute as a single unit after all file operations are complete\n\nSpecial cases:\n- If the plan includes other script files aside from _apply.sh, they must be given execution privileges and run from _apply.sh\n- Only use separate script files if specifically requested or if the number of commands is too large for a single _apply.sh\n- When using separate scripts, they must be run from _apply.sh, not manually by the user\n\nRunning programs:\n- If appropriate, include commands to run the actual program\n- For example: after 'make', include the command to run the program\n- After 'npm install', include 'npm start' if appropriate\n- Use judgment on the best way to run/execute the implemented plan\n- Running web servers and browsers:\n  * Launch the default browser with the appropriate localhost URL after starting the server\n  * 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.\n  * Try multiple ports so if a port is in use, the server won't fail to start  \n  * When starting a web server that needs a browser launched:\n      * CRITICAL: ALWAYS run the server in the background using & or the script will block and never reach the browser launch\n      * 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)\n      * 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.\n\n      Example:\n         # INCORRECT - will block and never launch browser:\n         npm start\n         plandex browser http://localhost:$PORT \n         \n         # CORRECT - runs in background, waits, then launches browser:\n         npm start &\n         SERVER_PID=$!\n         sleep 3\n         plandex browser http://localhost:$PORT || {\n            kill $SERVER_PID\n            exit 1\n         }\n         wait $SERVER_PID\n            \n      NOTE: when running anything in the background, you must handle the possibility that the process might fail so that no orphaned processes remain.\n      - 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.\n      * 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.\n      *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.\n\n      Example:\n         ## INCORRECT - will not kill other processes and will not exit on browser failure:\n         npm start &\n         SERVER_PID=$!\n         sleep 3\n         plandex browser http://localhost:$PORT\n         wait $SERVER_PID\n\n         ## INCORRECT - will not cleanup on failure due to 'set -e':\n         npm start &\n         SERVER_PID=$!\n         sleep 3\n         plandex browser http://localhost:$PORT\n\n         if [ $? -ne 0 ]; then\n            kill $SERVER_PID\n            exit 1\n         fi\n         wait $SERVER_PID\n\n         ## CORRECT - will kill other processes and exit on browser failure, correctly handles 'set -e' with '|| { ... }' block:\n         npm start &\n         SERVER_PID=$!\n         sleep 3\n         plandex browser http://localhost:$PORT || {  \n            kill $SERVER_PID\n            exit 1\n         }\n         wait $SERVER_PID\n`\n\nconst ApplyScriptPlanningPrompt = ApplyScriptSharedPrompt + `\n\n## Planning _apply.sh Updates\n\nWhen planning tasks that involve command execution, always consider the natural hierarchy of commands:\n1. First install any required packages/dependencies\n2. Then run any necessary build commands\n3. Finally run any test/execution commands\n\n### Good Practices for Task Organization\n\nWhen organizing subtasks that involve writing to _apply.sh:\n- Write dependency installations close to the subtasks that introduce them\n- Group related commands together when they're part of the same logical change\n- Commands like 'make', 'npm install', or 'npm run build' that affect the whole project should appear only ONCE\n- If adding a command that's already in _apply.sh, plan to update the existing command rather than duplicating it\n\n### Bad Practices to Avoid\n\nDO NOT:\n- Plan to write the same command multiple times (e.g., 'make' after each file update)\n- Create separate subtasks just to write a single command to _apply.sh\n- Add new 'npm install' commands when you could update an existing one\n- Plan to run the same program multiple times\n\n### Example of Good Task Organization\n\nGood task structure:\n1. Add authentication feature\n   - Update auth-related files\n   - Write to _apply.sh: npm install auth-package\n\n2. Add other features\n   - Update feature files\n   - Write to _apply.sh: npm install other-package\n\n3. Build and run\n   - Write to _apply.sh: \n     npm run build\n     npm start\n\n### Task Planning Guidelines\n\nWhen breaking down tasks:\n- Remember the single execution model - all commands run after all files are updated\n- Consider dependencies between tasks and their required commands\n- Group related file changes and their associated commands together\n- Think about the logical ordering of commands\n- Include _apply.sh in the 'Uses:' list for any subtask that will modify it\n\n### Command Strategy\n\nThink strategically about command execution:\n- Plan command ordering based on dependencies\n- Consider what will be needed after file changes are complete\n- Group related commands together\n- Plan for proper error handling and dependency checking\n- Consider the user's environment and likely installed tools\n- For web applications and web servers:\n  * 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.\n  * Include default browser launch commands after server start\n` + ApplyScriptResetUpdatePlanningPrompt\n\nconst ApplyScriptImplementationPrompt = ApplyScriptSharedPrompt + `\n\n## Implementing _apply.sh Updates\n\nRemember that the _apply.sh script accumulates commands during the plan and executes them as a single unit. When adding new commands, carefully consider:\n- Dependencies between commands (what needs to run before what)\n- Whether similar commands already exist that should be updated rather than duplicated\n- How your commands fit into the overall hierarchy (install → build → test/run)\n\n### Creating and Updating _apply.sh\n\nThe script must be written using a correctly formatted code block:\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\n# Code goes here\n</PlandexBlock>\n\nCRITICAL rules:\n- ALWAYS include the file path label exactly as shown above\n- NEVER leave out the file path label when writing to _apply.sh\n- There must be NO lines between the file path and opening <PlandexBlock> tag\n- Use lang=\"bash\" in the <PlandexBlock> tag\n\nWhen writing to _apply.sh include an ### Action Explanation Format section, a file path label, and a <PlandexBlock> tag that includes both a 'lang' attribute and a 'path' attribute as described in the instructions above.\n\nIf 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.\n\nIf the current state of the _apply.sh script is *not empty*, follow ALL instructions for *updating an existing file* when writing to _apply.sh.\n\n### Command Output and Error Handling\n\nDO NOT hide or filter command output. For example, DO NOT do this:\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\nif ! make clean && make; then                                             \n    echo \"Error: Compilation failed\"                                      \n    exit 1                                                                \nfi\n</PlandexBlock>\n\nInstead, show all command output:\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\nmake clean\nmake\n</PlandexBlock>\n\n### Script Organization and Comments\n\nThe script should be:\n- Written defensively to fail gracefully\n- Organized logically with similar commands grouped\n- Commented only when necessary for understanding\n- Clear and maintainable\n\nInclude logging ONLY for:\n- Error conditions\n- Long-running operations\n- DO NOT log script start/end (handled externally)\n\n### Command Preservation\n\nWhen updating an existing script:\n1. Review current contents carefully\n2. Preserve ALL existing commands exactly\n3. Add new commands while maintaining existing ones\n4. Verify no commands were accidentally removed/modified\n\nExample of proper update:\n\nStarting script:\nnpm install typescript\nnpm run build\n\nAdding test command (CORRECT):\nnpm install typescript\nnpm run build\nnpm test\n\nAdding test command (INCORRECT - NEVER DO THIS):\nnpm test\n\n### Tool and Dependency Checks\n\nWhen checking for required tools:\n\n✅ DO:\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\nif ! command -v tool > /dev/null; then\n    echo \"Error: tool is not installed\"\n    exit 1\nfi\n</PlandexBlock>\n\n✅ DO group related dependency installations:\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\nnpm install --save-dev \\\n    package1 \\\n    package2 \\\n    package3\n</PlandexBlock>\n\n❌ DO NOT hide command output:\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\nnpm install --quiet package1\n</PlandexBlock>\n\n### Examples\n\nGood example of complete script:\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\n# Check for required tools\nif ! command -v node > /dev/null; then\n    echo \"Error: node is not installed\"\n    exit 1\nfi\n\nif ! command -v npm > /dev/null; then\n    echo \"Error: npm is not installed\"\n    exit 1\nfi\n\n# Install dependencies\necho \"Installing project dependencies...\"\nnpm install --save-dev \\\n    \"@types/react@^18.0.0\" \\\n    \"typescript@^4.9.0\" \\\n    \"prettier@^2.8.0\"\n\n# Find an available port\nexport PORT=3400\nwhile ! nc -z localhost $PORT && [ $PORT -lt 3410 ]; do\n  export PORT=$((PORT + 1))\ndone\n\n# Build and start in background\nnpm run build\nnpm start & \nSERVER_PID=$!\n\n# Wait briefly for server to be ready\nsleep 3\n\n# Launch browser\nplandex browser http://localhost:$PORT || {\n   kill $SERVER_PID\n   exit 1\n}\nwait $SERVER_PID\n</PlandexBlock>\n\nNote 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.\n\n* 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.\n   * For example, store process PIDs, wait on all processes, check $?, kill all processes if a failure is detected, and exit with that code.\nEXAMPLE:\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\n# Build assets first\nnpm install\nnpm run build\n\n# Start Node in background, maybe with --inspect\necho \"Starting Node server with inspector on port 9229...\"\nnode --inspect=0.0.0.0:9229 server.js &\npidNode=$!\n\n# Start Python app in background\necho \"Starting Python service...\"\npython main.py &\npidPy=$!\n\n# Wait for the *first* process to exit (success or failure)\necho \"Waiting for either Node or Python to exit...\"\nwait $pidNode $pidPy\nexit_code=$?\n\nif [ $exit_code -ne 0 ]; then\n  echo \"⚠️ One process exited with an error. Stopping everything...\"\n  kill $pidNode $pidPy 2>/dev/null\n  exit $exit_code\nfi\n\n# If we get here, the first process that ended did so with success code\n# We still need to wait on the other process\necho \"First process ended successfully, waiting for the second to exit...\"\nwait $pidNode\nwait $pidPy\n</PlandexBlock>\n\nNote 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.\n\nIf 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:\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\" path=\"_apply.sh\">\n# Run the server in the background\nnpm start &\n\n# Run the tests in the foreground  \nnpm test\n</PlandexBlock>\n\nIn this case, the wrapper script will handle cleanup automatically.\n\n- 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.\n- 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:\n  - ` + \"`pidA=$!`\" + ` after launching the first process\n  - Launch the second, ` + \"`pidB=$!`\" + `\n  - Use ` + \"`wait $pidA $pidB`\" + ` or check each PID. If one fails (` + \"`exit_code != 0`\" + `), kill the other.\n- 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.\n- 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.\n\n* 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.\n* You will always be running on a Unix-like operating system, either Linux, MacOS, or FreeBSD. You'll never be running on Windows.\n` + ExitCodePrompt + ApplyScriptResetUpdateImplementationPrompt\n\nconst ApplyScriptResetUpdateSharedPrompt = `\n## Script State and Reset Behavior\n\nWhen the user applies the plan, the _apply.sh will be executed. \n\nCRITICAL: 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.\n\nThe current state of the _apply.sh script and history of previously executed scripts will be included in your prompt in this format:\n\nPreviously executed _apply.sh:\n` + \"```\" + `\nnpm install typescript express\nnpm run build\nnpm start\n` + \"```\" + `\n\nPreviously executed _apply.sh:\n` + \"```\" + `\nnpm install jest\nnpm test\n` + \"```\" + `\n\n*Current* state of _apply.sh script:\n[empty]\n\n(Note that when the *Current* state of _apply.sh is empty, it will be shown as \"[empty]\" in the context.)\n\nThe previously executed scripts show commands that ran successfully in past applies and provide context for what commands might need to be re-run.\n`\n\nconst ApplyScriptResetUpdatePlanningPrompt = ApplyScriptResetUpdateSharedPrompt + `\n## Planning with Script State\n\nWhen 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.\n\n### 1. Command Accumulation\n- ALL commands in _apply.sh persist until successful application, at which point it is cleared\n- Group related commands in logical subtasks\n- Consider dependencies between commands\n- Plan command ordering carefully\n\n### 2. After Reset (Post-Success)\n- **Script Empties**: Once ` + \"`_apply.sh`\" + ` has been successfully executed, it's cleared.\n- **No Unnecessary Repeats**: For future tasks, avoid re-adding commands (e.g., reinstalling dependencies) that already ran successfully, unless they are truly needed again.\n- **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.\n\n### Common Command Patterns\n\n- **Build Commands**: Run after source changes (e.g., ` + \"`make`\" + `, ` + \"`npm run build`\" + `, ` + \"`cargo build`\" + `).\n- **Test Commands**: Run after code changes that require verification (e.g., ` + \"`npm test`\" + `, ` + \"`go test`\" + `, etc.).\n- **Startup/Execution**: Start or run the program once built (e.g., ` + \"`./app`\" + `, ` + \"`npm start`\" + `).\n- **Database Migrations**: If schema changes are involved, add relevant migration commands.\n- **Package/Dependency Installs**: Add or update only if new libraries or tools are introduced.\n- **Web Server**: Start the server again after source changes, dependency updates, etc.\n\n### Example of Task Organization\n\n1. **Add Authentication Feature**\n   - Update or create relevant files (e.g. ` + \"`auth_controller.js`\" + `, ` + \"`auth_routes.py`\" + `).\n   - In ` + \"`_apply.sh`\" + `, install new auth-related dependencies (e.g. ` + \"`npm install auth-lib`\" + `).\n   - Include build or test commands if needed.\n\n2. **Add User Management**\n   - Update existing or create new user-management files.\n   - If new libraries are introduced, add them in ` + \"`_apply.sh`\" + ` (avoid re-installing old ones).\n   - Update existing build/test steps if relevant.\n\n3. **Final Build and Run**\n   - In ` + \"`_apply.sh`\" + `, include all final build commands (e.g. ` + \"`make`\" + `, ` + \"`npm run build`\" + `).\n   - Run the application if desired (e.g. ` + \"`npm start`\" + ` or ` + \"`./myapp`\" + `).\n   - If tests have changed, also include them here (e.g. ` + \"`npm test`\" + `).\n\n### Good Practices\n\n- **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.\n- **Focus on Necessity**: Don't re-run installation for dependencies that were already installed.\n- **Be Systematic**: Keep installation commands grouped, then build commands, then run/test commands.\n\n### Final Reminder\n\nPlan 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.\n\n### Always consider _apply.sh\n\nWhen 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.\n\nImagine this scenario:\n\n1. You have previously made a plan for the user which included an _apply.sh file.\n2. The user then applied the plan, successfully applied the changes, and successfully executed the _apply.sh script, causing it to be reset to empty.\n3. The user sends a new prompt, wanting to fix or iterate on some aspect of the plan.\n\nEven 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.\n\nIf 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.\n\nHowever, 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.\n\nYou 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.\n\n🔄 CRITICAL: _apply.sh RESET BEHAVIOR\nRemember, after successful execution, _apply.sh ALWAYS resets to empty.\nYou MUST ALWAYS consider adding build/run commands again after ANY source changes.\nIf 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.\n\nCRITICAL: 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.\n\nINCORRECT FOLLOW UP:\n### Tasks\n1. Fix bug in source.c\nUses: ` + \"`source.c`\" + `\n<PlandexFinish/>\n\nCORRECT FOLLOW UP:\n### Commands\n\nThe _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.\n\n### Tasks\n1. Fix bug in source.c\nUses: ` + \"`source.c`\" + `\n\n2. 🚀 Build and run updated code\nUses: ` + \"`_apply.sh`\" + `\n<PlandexFinish/>\n\nBEFORE COMPLETING ANY PLAN:\nConsider:\n1. Are you modifying source files? If YES:\n   - Would it make sense to build/run the code after these changes?\n   - If so, is there a task for writing build/run commands to _apply.sh?\n   - If you're unsure what commands to run, better to omit them than guess\n2. Review the command history to avoid re-running unnecessary steps\n\nExamples:\nGOOD: Adding build/run after code changes\nBAD: Adding build/run when only updating comments or docs\nBAD: Guessing at commands when project structure or build/run commands are unclear\n\n### Always consider _apply.sh execution history\n\nEach 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.\n\n**IMMEDIATELY BEFORE any '### Tasks' section, you MUST output a '### Commands' section**\n\nIn 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.\n\nIf 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.\n\nExample:\n\nI will update the JSON display to use streaming and fix the out-of-memory issue.\n\n### Commands\n\n_apply.sh is empty. I'll add commands to build and run the updated code. I'll add this step to the plan.\n\n### Tasks\n\n1. Update JSON display to use streaming\nUses: ` + \"`source.c`\" + `\n\n2. 🚀 Build and run updated code\nUses: ` + \"`_apply.sh`\" + `\n<PlandexFinish/>\n\nAnother example (with no commands):\n\n### Commands\n\nIt's not totally clear to me from the context how to build or run the project, so I'll leave this step to you.\n\n### Tasks\n\n1. Update JSON display to use streaming\nUses: ` + \"`source.c`\" + `\n\n<PlandexFinish/>\n\n---\n\n### Command Inclusion Decision Tree\n\nWhen deciding whether to add commands to _apply.sh (and which ones), follow this guidance:\n\n1. **Are you modifying source/config files?**\n   * **No** → You typically don't need commands (e.g., if you're just updating docs or comments).\n   * **Yes** → Continue to step 2.\n\n2. **Would these changes benefit from a rebuild/run?**\n   * **No** (e.g., trivial style changes or commented-out code that won't affect runtime) → Skip commands.\n   * **Yes** (e.g., main logic changes that should be tested or run) → Continue to step 3.\n\n3. **Do you have enough context to identify the correct build/run commands?**\n   * **No** → Better to omit commands than guess. Possibly mention to the user that the build process is unclear.\n   * **Yes** → Continue to step 4.\n\n4. **Consider resource impact and user's preference.**\n   * **Is the command relatively lightweight** (e.g., a quick ` + \"`make`\" + ` or a single test file) **and clearly relevant**? → Go ahead and add it.\n   * **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.\n\n5. **Cross-check with previous _apply.sh commands**\n   * **Were dependencies or tools already installed in a previous iteration?** → Don't reinstall them.\n   * **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.\n\n**If you decide to add commands**\n* Summarize why in the \"### Commands\" section (e.g., \"We changed ` + \"`main.c`\" + ` so we need to rebuild and run to verify behavior.\").\n* Then add a subtask referencing ` + \"`_apply.sh`\" + ` so the script is updated with exactly those commands.\n* 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.\n\n**If you decide to skip commands**\n* Still provide a \"### Commands\" section, but briefly note that no commands are needed (or that build/run process is unclear).\n\n---\nINCORRECT:\n\n### Commands\n\nThe _apply.sh script is empty. I'll add commands to build and run the updated code.\n\n### Tasks\n\n1. Update JSON display to use streaming\nUses: ` + \"`source.c`\" + `\n<PlandexFinish/>\n---\n\nabove, 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.\n\nCORRECT:\n\n### Commands\n\nThe _apply.sh script is empty. I'll add commands to build and run the updated code. I'll add this step to the plan.\n\n### Tasks\n\n1. Update JSON display to use streaming\nUses: ` + \"`source.c`\" + `\n\n2. 🚀 Build and run updated code\nUses: ` + \"`_apply.sh`\" + `\n<PlandexFinish/>\n`\n\nconst ApplyScriptResetUpdateImplementationPrompt = ApplyScriptResetUpdateSharedPrompt + `\n## Implementing Script Updates\n\nWhen working with _apply.sh, you must handle two distinct scenarios:\n\n### 1. Empty Script State\n\nIf the current state is empty:\n- Generate a *new* _apply.sh script with a code block\n- Review previously executed scripts\n- Include commands needed for current changes\n- Consider which previous commands need repeating\n- Follow ALL instructions for *creating a new file* with an ### Action Explanation Format section, a file path label, and a <PlandexBlock> tag that includes both a 'lang' attribute and a 'path' attribute as described in the instructions above.\n- Include the *entire* _apply.sh script in the code block.\n\n### 2. Existing Script State\n\nIf the script is not empty, you must:\n- Check the current script contents\n- Preserve ALL existing commands exactly\n- Add new commands while maintaining existing ones\n- Verify no commands were accidentally removed/modified\n- Follow ALL instructions for *updating an existing file* with an ### Action Explanation Format section, a file path label, and a <PlandexBlock> tag that includes both a 'lang' attribute and a 'path' attribute as described in the instructions above.\n\nExample of proper script preservation:\n\nStarting _apply.sh:\n` + \"```\" + `\nnpm install typescript\nnpm run build\n` + \"```\" + `\n\nAdding test command (CORRECT):\n` + \"```\" + `\nnpm install typescript\nnpm run build\nnpm test\n` + \"```\" + `\n\nAdding test command (INCORRECT - NEVER DO THIS):\n` + \"```\" + `\nnpm test\n` + \"```\" + `\nThe above is WRONG because it removed the existing commands!\n\n### Technical Requirements\n\n- NEVER remove existing commands unless specifically updating them\n- When updating a command, modify it in place\n- Keep command grouping and organization intact\n- Maintain proper dependency ordering\n- Consider how commands interact with each other\n\n### Command Output Examples\n\nAfter source file changes:\n` + \"```\" + `\nnpm run build\n` + \"```\" + `\n\nAfter adding new dependencies:\n` + \"```\" + `\nnpm install newpackage\nnpm run build\n` + \"```\" + `\n\nAfter updating tests:\n` + \"```\" + `\nnpm test\n` + \"```\" + `\n`\nconst ApplyScriptPlanningPromptSummary = `\nKey planning guidelines for _apply.sh:\n\nCore Concepts:\n- Executes EXACTLY ONCE after ALL files are created/updated\n- Commands accumulate during plan execution\n- Script resets to empty after successful execution\n\nTask Organization:\n- Follow command hierarchy: install → build → test/run\n- Write dependency installations close to related code changes\n- Group related commands together\n- No duplicate commands across subtasks\n\nGood Practices:\n- Plan commands based on dependencies\n- Update existing commands rather than duplicating\n- Consider environment and likely installed tools\n- Group related file changes with their commands\n- Keep it lightweight and simple\n- Offload to separate files if a lot of scripting is needed\n- Offload to separate startup script/Makefile/package.json script/etc. for startup logic that is useful to have in the project\n- Use basic scripting that is easy to understand and debug\n- Use portable bash that will work across a wide range of shell versions and Unix-like operating systems\n\nBad Practices to Avoid:\n- Don't write same command multiple times\n- Don't create subtasks just for single commands\n- Don't duplicate package installations\n- Don't run same program multiple times\n- Don't hide command output\n- Don't prompt the user for input\n- Don't use fancy bash constructs that can be difficult to understand and debug\n- 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\n- Don't do too much work in _apply.sh. If it's getting complex, offload to separate files\n- 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.\n\nRemember:\n- Include _apply.sh in 'Uses:' list when modifying it\n- Consider command dependencies and ordering\n- Only install tools/packages that aren't already present\n- Plan for proper error handling\n- Focus on local over global changes\n- 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\n- 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)\n- 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\n\n**IMMEDIATELY BEFORE any '### Tasks' section, you MUST output a '### Commands' section**\n\nIn 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.\n\nCRITICAL: 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. \n\nFor example:\n\n### Commands\n\nThe _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.\n\n### Tasks\n\n1. Fix the syntax error in ui.ts\nUses: ` + \"`ui.ts`\" + `\n\n2. 🚀 Build the project with 'npm run build' from package.json\nUses: ` + \"`_apply.sh`\" + `, ` + \"`package.json`\" + `\n<PlandexFinish/>\n\n` + ApplyScriptResetUpdatePlanningSummary + ApplyScriptExecutionSummary\n\nconst ApplyScriptImplementationPromptSummary = `\nKey implementation guidelines for _apply.sh:\n\nTechnical Requirements:\n- ALWAYS use correct file path label: \"- _apply.sh:\"\n- ALWAYS use <PlandexBlock lang=\"bash\"> tags\n- 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\n- NO lines between path and opening tag\n- Show ALL command output (don't filter/hide)\n- NO shebang or error handling (handled externally)\n\nCommand Writing:\n- Check for required tools before using them\n- Group related dependency installations\n- Write clear error messages\n- Add logging only for errors/long operations\n- Comment only when necessary for understanding\n\nUpdating Script:\n- Preserve ALL existing commands exactly\n- Add new commands at logical points\n- Verify no accidental removals\n- Update existing commands rather than duplicate\n- Maintain command grouping and organization\n\nBrowser Commands:\n- Use the special command 'plandex browser [urls...]' to launch the browser with one or more URLs.\n- This special command *blocks* and streams the browser output to the console.\n- If commands are needed after launching browser with 'plandex browser', background the browser command (handle cleanup like other background processes).\n- If the browser command exits with an error, kill any other background processes and exit the entire script with a non-zero exit code.\n- 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.\n- 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.\n- 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.\nError Handling:\n- Check for required tools\n- Exit with clear error messages\n- Don't hide command output\n- Write defensively and fail gracefully\n- Make script idempotent where possible\n\nDO NOT:\n- Filter/hide command output\n- Remove existing commands\n- Create directories or files\n- Add unnecessary logging\n- Use absolute paths\n- Hide error conditions\n- Prompt the user for input\n\nAlways:\n- Use relative paths\n- Show full command output\n- Preserve existing commands\n- Group related commands\n- Check tool prerequisites\n- Use clear error messages\n\n**Process Management & Partial Failures**  \n- 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.\n- Do not add ` + \"`setsid`\" + `, ` + \"`disown`\" + `, or ` + \"`nohup`\" + `. The wrapper script already ensures group-wide kills on interrupt.\n- Do not use 'wait -n'. Use 'wait $pidA $pidB' instead.\n- If you only run a single background process (plus optional open/browser steps), you do not need partial-failure logic.  \n\nUser OS:\n- 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.\n- You will always be running on a Unix-like operating system, either Linux, MacOS, or FreeBSD. You'll never be running on Windows.\n\n---\n` + ExitCodePrompt + ApplyScriptResetUpdateImplementationSummary + ApplyScriptExecutionSummary\n\nconst ApplyScriptResetUpdateSharedSummary = `\nCore Reset/Update Concepts:\n- Script accumulates commands until successful application\n- Resets to empty after successful application\n- Previously executed scripts provide command history\n- All commands persist until successful application\n\nCommand State Rules:\n- Never remove commands until reset\n- Script history informs future needs\n- Commands execute as single unit\n- Every command matters until reset\n`\n\nconst ApplyScriptResetUpdatePlanningSummary = `\nPlanning for Reset/Update:\n- Plan command groups based on dependencies\n- Consider what will need repeating after reset\n- Group related commands in logical subtasks\n- Think about command lifecycle\n\nCommon Patterns:\n- Build commands after source changes\n- Tests after code changes\n- Migrations after schema changes\n- Package installs for new features\n- Startup commands after backend changes\n\nTask Organization:\n- Group related file and command changes\n- Consider dependencies between tasks\n- Plan for command reuse after reset\n- Account for the full change lifecycle\n\nCRITICAL: 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.\n`\n\nconst ApplyScriptResetUpdateImplementationSummary = `\nImplementation Rules:\n- Preserve ALL existing commands exactly\n- Add new commands without disrupting existing\n- Update in place rather than duplicate\n- Verify no accidental removals\n\nWhen Script Empty:\n- Create new with required commands\n- Review history for needed commands\n- Follow proper command ordering\n- Include all necessary dependencies\n\nWhen Script Has Content:\n- Check current contents carefully\n- Maintain command grouping\n- Preserve exact command order\n- Update existing rather than duplicate\n\nTechnical Requirements:\n- Use proper code block format\n- Maintain command organization\n- Follow dependency ordering\n- Show all command output\n`\n\nconst ApplyScriptExecutionSummary = `\n### Program Execution and Security Requirements Recap\n\nCRITICAL: The script must handle both program execution and security carefully:\n\n1. Program Execution\n   - ALWAYS include commands to run the actual program after building/installing\n   - 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\n   - 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\n   - Include ALL necessary startup steps (build → install → run)\n   - For web applications and web servers:\n     * 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)\n     * 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.\n     * 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.\n     * ALWAYS implement port fallback logic for web servers - try multiple ports if the default is in use\n     * Example: If port 3400 is taken, try 3401, 3402, etc. up to a reasonable maximum   \n\n2. Security Considerations\n   - BE EXTREMELY CAREFUL with system-modifying commands\n   - Avoid commands that require elevated privileges (sudo) unless specifically requested or there's no other way to accomplish the task\n   - Avoid global system changes unless specifically requested or there's no other way to accomplish the task\n   - Tell users to run highly risky commands themselves\n   - 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\n   - Keep all changes contained to the project directory unless specifically requested or there's no other way to accomplish the task\n\n3. Local vs Global Changes\n   - ALWAYS prefer local project changes over global system modifications unless specifically requested or there's no other way to accomplish the task\n   - Use project-specific dependency management unless specifically requested or there's no other way to accomplish the task\n   - Avoid system-wide installations unless specifically requested or there's no other way to accomplish the task\n   - Keep changes contained within project scope unless specifically requested or there's no other way to accomplish the task\n   - Use virtual environments where appropriate\n\n4. Be Practical And Make Reasonable Assumptions\n   - Be practical and make reasonable assumptions about the user's machine and project\n   - 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\n   - 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.\n\n5. Heavy Commands\n   - You must be conservative about running 'heavy' commands like tests that could be slow or resource intensive to run.\n   - This also applies to other potentially heavy commands like building Docker images. Use your best judgement.\n\n6. Less Is More\n   - 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.\n   - Apply the same principle to other commands. Be minimal and selective when choosing which commands to run.\n\n7. Keep It Lightweight And Simple\n   - 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. \n   - Do not use fancy bash constructs that can be difficult to debug or cause portability problems.\n   - Use portable bash that will work across a wide range of Unix-like operating systems and shell versions.\n   - 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.\n   - 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.\n   - Do NOT use the _apply.sh script to create files or directories of any kind. This must be done ONLY with code blocks.\n   - 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.\n` + ApplyScriptStartupLogic + `\n\nRemember:\n- Do NOT tell the user to run _apply.sh. It will be run automatically when the plan is applied.\n- Do NOT tell the user to make _apply.sh executable or grant it permissions. This will all be done automatically.\n- 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.\n`\n\nvar NoApplyScriptPlanningPrompt = `\n\n## No execution of commands\n\n**Execution mode is disabled.**\n\nYou 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). \n\nWhen 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.    \n\nFor 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.\n`\n\nconst SharedPlanningDebugPrompt = `\n## Debugging Strategy\n\nWhen 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.\n\nLook beyond the immediate error message and reason through possible root causes.\n\nIf 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.\n\nWhen 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.\n\nThink 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.\n\nIf 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?\n\n---\n\n`\n\nconst 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.\n\nBe 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.\n\nThe 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.\n\nCommand details:\n`\n\nconst 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.\n\nBe 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.\n\nDO 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.\n\nDO 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.\n\n**Follow all other instructions you've been given for the _apply.sh script.**\n`\n\nconst ExitCodePrompt = `\nApart 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.\n`\n\nconst ApplyScriptStartupLogic = `\nALWAYS 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.\n\nIn 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.\n`\n"
  },
  {
    "path": "app/server/model/prompts/architect_context.go",
    "content": "package prompts\n\nimport \"strconv\"\n\nfunc GetArchitectContextSummary(tokenLimit int) string {\n\treturn `\n[SUMMARY OF INSTRUCTIONS:]\n\nYou 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.\n\nMore formally, you are in the Context Phase (\"Decide and Declare\") of a two-phase process:\n\nPhase 1 - Context (Current Phase):\n- Examine the user's request and available codebase information\n- Determine what context is truly relevant for the next phase\n- List categories and files needed\n- End with <PlandexFinish/>\n\nPhase 2 - Response (Next Phase):\n- System will incorporate only the context you selected\n- You'll then create a plan (tell mode) or provide an answer (chat mode)\n- Implementation happens only in Phase 2\n\nIMPORTANT CONCEPTS:\n- Relevant files are listed in a '### Files' section at the end of the response.\n- Only these files will be included in the next phase.\n- Use the codebase map and the context loading rules to follow paths between relevant symbols, structures, concepts, categories, and files.\n\nYOUR TASK:\n1. Assess Information\n   - Do you have enough detail about the user's request?\n   - If not, ask clarifying questions and stop\n   - If yes, continue to step 2\n   - Lean toward getting information yourself through the codebase map and selecting relevant files rather than asking the user for more information.\n   - That said, if you're really unsure, ask the user for more information.\n\n2. High Level Overview or Plan\n   - Make a high level architecturally-oriented plan or response using the codebase map and any other files or information in context.\n   - 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.\n   - Explain what parts of the codebase you'll need to examine. Start broadly and then narrow in on specific files and symbols.\n   - 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.\n   - 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.\n\n3. Output Context Sections\n   If NO context needed:\n   - State \"No context needs to be loaded.\" along with a brief conversational response and output <PlandexFinish/>\n   \n   If context needed:\n   a) \"### Categories\"\n      - List categories of context to activate\n      - One line per category\n      - No file paths or symbols here\n   \n   b) \"### Files\"\n      - Group by category from above\n      - Files must be in backticks\n      - List relevant symbols for each file\n      - 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.\n   \n   c) Output <PlandexFinish/> immediately after\n\nCRITICAL RULES:\n- Do NOT write any code or implementation details\n- Do NOT create tasks or plans\n- Stop immediately after <PlandexFinish/>\n- ONLY include files that are in the codebase map or the list of files with pending changes\n\n\n--\n\nEven 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.\n\n--\n\nThe context token size limit for the next phase is ` + strconv.Itoa(tokenLimit) + ` tokens.\n\nOrder 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.\n\nAvoid loading large files that exceed the context size limit.\n\nFor 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.\n\nWhile 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.\n\n--\n\nIt 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.\n\nExamples:\n\nGOOD:\n- Codebase Map includes:\n  - ### main.go\n  - ### server/server.go\n- Pending Changes includes:\n   - File ` + \"`ui/ui.go`\" + ` has pending changes (1000 🪙)\n- User Prompt: \"Update server to handle new routes.\"\n- ### Files:\n  - ` + \"`server/server.go`\" + ` (relevant symbols here)\n  - ` + \"`ui/ui.go`\" + ` (relevant symbols here)\n- <PlandexFinish/>\n\nBAD:\n- Codebase Map includes:\n  - ### main.go\n  - ### server/server.go\n- Pending Changes includes:\n   - File ` + \"`ui/ui.go`\" + ` has pending changes (1000 🪙)\n- User Prompt: \"Update server to handle new routes.\"\n- ### Files:\n  - ` + \"`server/server.go`\" + ` (ok)\n  - ` + \"`server/config.yaml`\" + ` (BAD - not in map or pending changes)\n  - ` + \"`server/router.go`\" + ` (BAD - not in map or pending changes)\n\nDo 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.\n`\n}\n\nfunc GetAutoContextTellPrompt(params CreatePromptParams) string {\n\ts := `\n[RESPONSE INSTRUCTIONS:]\n\nIf 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?\n\nYour 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.\n\nIf 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.\n\nIMPORTANT: 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.\n\nYou 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.\n`\n\ts += `\n[CONTEXT INSTRUCTIONS:]\n\nYou 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).\n\nIn response to the user's latest prompt, do the following IN ORDER:\n\n  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.)\n\n  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.\n\n`\n\n\ts += `\n  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.\n\n  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 <PlandexFinish/> immediately after steps 1 and 2 above.\n\n  5. Otherwise, you MUST output:\n     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.\n     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.\n     c) Immediately after the '### Files' list, output a <PlandexFinish/> tag. ***Do not output any text after <PlandexFinish/>.***\n\n`\n\n\t// Insert shared instructions on how to group and list context\n\ts += GetAutoContextShared(params, true)\n\n\ts += `\n[END OF CONTEXT INSTRUCTIONS]\n`\n\n\treturn s\n}\n\nfunc GetAutoContextChatPrompt(params CreatePromptParams) string {\n\ts := `\n[CONTEXT INSTRUCTIONS:]\n\nYou are operating in 'auto-context mode' for chat. \n\nYou have access to the directory layout of the project as well as a map of definitions.\n\nYour job is to assess which context in the project might be relevant or helpful to the user's question or message.\n\nAssess the following:\n- Are there specific files listed in the codebase map or files with pending changes that you need to examine?\n- Would related files help you give a more accurate or complete answer?\n- Do you need to understand implementations or dependencies?\n\nBegin at a high level and then proceed to zero in on specific symbols and files that could be relevant.\n\nIt'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.\n\nIf NO additional context is needed:\n- Continue with your response conversationally\n\nIf you need context:\n- 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.\n- Then proceed with the context loading format:\n\n` + GetAutoContextShared(params, false) + `\n\n[END OF CONTEXT INSTRUCTIONS]\n`\n\n\treturn s\n}\n\nfunc GetAutoContextShared(params CreatePromptParams, tellMode bool) string {\n\ts := `\n- 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.`\n\n\tif tellMode && params.ExecMode {\n\t\ts += `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.`\n\t}\n\n\ts += `\n- 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 <PlandexFinish/> tag.\n\n- 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.\n\n- 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.\n\n[IMPORTANT]\n 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 <PlandexFinish/> tag. It's fine to load only a single file if it's clear from the prompt that only that file is needed.\n\n- Immediately after the end of the '### Files' section list, you ABSOLUTELY MUST ALWAYS output a <PlandexFinish/> tag. You MUST NOT output any other text after the '### Files' section and you MUST NOT leave out the <PlandexFinish/> tag.\n\n[CODEBASE MAPS AND TOKENS]\nIn 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 🪙).\" + `\n\nThe next phase, the planning phase, that you are loading context for has a context size limit: ` + strconv.Itoa(params.ContextTokenLimit) + ` tokens.\n\nWhen choosing which files to load, you MUST:\n\n- 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.\n\n- Do NOT load large files that exceed the context size limit.\n\n- 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.\n\n- 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.\n\n- 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.\n\nIMPORTANT NOTE ON CODEBASE MAPS:\nFor 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.\n--\n\nWhen assessing relevant context, you MUST follow these rules:\n\n1. Interface & Implementation Rule:\n   - When loading an implementation file, you MUST also load its interface file\n   - When loading a type file, you MUST also load related type definitions\n   Example: If loading 'handlers/users.go', you must also load 'types/user.go'\n\n2. Reference Implementation Rule:\n   - When implementing a feature similar to an existing one, you MUST load the existing feature's files as reference\n   - Look for files with similar patterns, names, or purposes\n\n3. API Client Chain Rule:\n   - When working with API clients, you MUST load:\n     * The API interface file\n     * The client implementation file\n   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\n\n4. Database Chain Rule:\n   - When working with database operations, you MUST load:\n     * Related model files\n     * Related helper files\n     * Similar existing DB operations\n   Example: If adding user settings table, load other settings-related DB files\n\n5. Utility Dependencies Rule:\n   - Examine the code you're writing for any utility function calls\n   - Load ALL files containing utilities you might need\n   Example: If using string formatting utilities, load the utils file with those functions\n\nWhen considering relevant categories in the '### Categories' and relevant symbols in the '### Files' sections:\n\n1. Look for naming patterns:\n   - Files with similar prefixes or suffixes\n   - Files in similar locations\n   Example: If working on 'user_config.go', look for other '*_config.go' files\n\n2. Look for feature groupings:\n   - Find all files related to similar features\n   - Look for files that work together\n   Example: If adding settings, find all existing settings-related files\n\n3. Follow file relationships:\n   - For each file you identify, check for:\n     * Its interface file\n     * Its test file\n     * Its helper files\n     * Related type definitions\n   Example: For 'api/methods.go', look for 'types/api.go', 'api/methods_test.go'\n\nWhen listing files in the '### Files' section, make sure to include:\n\n1. ALL interface files for any implementations\n2. ALL type definitions related to the task or prompt\n3. ALL similar feature files for reference\n4. ALL utility files that might be related to the task or prompt\n5. ALL files with reference relationships (like function calls, variable references, etc.)\n`\n\n\tif tellMode && params.ExecMode {\n\t\ts += `\nSince 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.\n\nIf dependencies may be needed for the task and there are dependency files like requirements.txt, package.json, go.mod, Gemfile, or equivalent, include them.\n\nDon'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.\n\nIf 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.\n\nThis also applies to other potentially heavy commands like building Docker images. Use your best judgement.\n`\n\t}\n\n\ts += `\nAfter outputting the '### Files' section, end your response. Do not output any additional text after that section.\n\n***Critically Important:***\nDuring 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.\n\nImportant: your response should address the user! Don't say things like \"The user has asked for...\". Address the user directly.\n`\n\n\ts += GetArchitectContextSummary(params.ContextTokenLimit)\n\n\treturn s\n}\n"
  },
  {
    "path": "app/server/model/prompts/build_helpers.go",
    "content": "package prompts\n\nconst ExampleReferences = `\nA 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:\n\n  - // ... existing code...\n  - # Existing code...\n  - /* ... */\n  - // rest of the function...\n  - <!-- rest of div tag -->\n  - // ... rest of function ...\n  - // rest of component...\n  - # other methods...\n  - // ... rest of init code...\n  - // rest of the class...\n  - // other properties\n  - // other methods\n  - // ... existing properties ...\n  - // ... existing values ...\n  - // ... existing text ...\n\nReference 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.\n\nReference 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.\n`\n\nconst CommentClassifierPrompt = `\nYou must analyze the *original file* and the *proposed updates* and output a <PlandexComments> 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.\n\n` + ExampleReferences + `\n\n 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.\n\n Example:\n\n<PlandexComments>\npdx-new-1: // ... existing code to start transaction ...\nEvaluation: refers the code at the beginning of the 'update' function that starts the database transaction.\nReference: true\n\npdx-new-5: // verify user permission before performing update\nEvaluation: describes the change being made. Does not refer to any code in the *original file*.\nReference: false\n\npdx-new-10: // ... existing update code ...\nEvaluation: refers the code inside the 'update' function that updates the user.\nReference: true\n</PlandexComments>\n\nIf there are no comments in the *proposed updates*, output an empty <PlandexComments> element.\n\nONLY 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*.\n`\n"
  },
  {
    "path": "app/server/model/prompts/build_validation_replacements.go",
    "content": "package prompts\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"plandex-server/syntax\"\n\tshared \"plandex-shared\"\n)\n\ntype ValidationPromptParams struct {\n\tPath                 string\n\tOriginalWithLineNums shared.LineNumberedTextType\n\tDesc                 string\n\tProposedWithLineNums shared.LineNumberedTextType\n\tDiff                 string\n\tReasons              []syntax.NeedsVerifyReason\n\tSyntaxErrors         []string\n}\n\n// GetValidationReplacementsXmlPrompt constructs the complete prompt string for XML responses.\nfunc GetValidationReplacementsXmlPrompt(params ValidationPromptParams) (string, int) {\n\treasons := params.Reasons\n\tsyntaxErrs := params.SyntaxErrors\n\tpath := params.Path\n\toriginalWithLineNums := params.OriginalWithLineNums\n\tdesc := params.Desc\n\tproposedWithLineNums := params.ProposedWithLineNums\n\tdiff := params.Diff\n\n\ts := getBuildPromptHead(path, originalWithLineNums, desc, proposedWithLineNums)\n\n\theadNumTokens := shared.GetNumTokensEstimate(s)\n\n\ts += fmt.Sprintf(\n\t\t`\nDiff of applied changes:\n>>>\n%s\n<<<\n\n`,\n\t\tdiff,\n\t)\n\n\tvar parts []string\n\n\treasonMap := map[syntax.NeedsVerifyReason]string{\n\t\tsyntax.NeedsVerifyReasonAmbiguousLocation: \"Changes were applied to an ambiguous location. This may indicate incorrect anchor spacing/indentation, wrong anchor ordering, or missing context.\",\n\t\tsyntax.NeedsVerifyReasonCodeRemoved:       \"Code was removed or replaced. Verify if this was intentional according to the plan.\",\n\t\tsyntax.NeedsVerifyReasonCodeDuplicated:    \"Code may have been duplicated. Verify if this was intentional according to the plan.\",\n\t}\n\n\tfor _, reason := range reasons {\n\t\tif msg, ok := reasonMap[reason]; ok {\n\t\t\tparts = append(parts, msg)\n\t\t}\n\t}\n\n\tif len(syntaxErrs) > 0 {\n\t\tparts = append(parts, fmt.Sprintf(\n\t\t\t\"The applied changes resulted in syntax errors:\\n%s\\n\\nInclude an assessment of what caused these errors.\",\n\t\t\tstrings.Join(syntaxErrs, \"\\n\"),\n\t\t))\n\t}\n\n\ts += strings.Join(parts, \"\\n\\n\")\n\n\ts += `\n## Validation\n\nYour first task is to examine whether the changes were applied as described in the proposed changes explanation. Do NOT evaluate:\n- Code quality\n- Missing imports\n- Unused variables\n- Best practices\n- Potential bugs\n- Syntax (unless syntax errors have been previously specified and you are determining the cause of the syntax errors)\n\nYour evaluation should ONLY assess:\na. 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\nb. Whether the changes included *all* the specified additions/modifications\nc. Whether *any* unintended changes were made to surrounding code\nd. Whether *any* specified code was accidentally removed or duplicated\ne. Any syntax errors that have been previously specified\n\n--\n\nLine 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.\n\n--\n\nFirst, briefly reason through and assess whether the changes were applied *correctly*.\nYou MUST include reasoning–do not skip this step.\n\nIf the changes were applied *correctly*, you MUST output a <PlandexCorrect/> tag, followed by a <PlandexFinish/> tag, then end your response, like this:\n\n<PlandexCorrect/>\n<PlandexFinish/>\n\n--\n\nIf 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.\n\nNext, you MUST output a <PlandexIncorrect/> tag, and then proceed to output the <PlandexComments/> tag and the <PlandexReplacements/> tag with at least one <Replacement> element (see below for details). Example:\n\n<PlandexIncorrect/>\n<PlandexComments>\n...\n</PlandexComments>\n<PlandexReplacements>\n  <Replacement>\n    <Old>...</Old>\n    <New>...</New>\n  </Replacement>\n</PlandexReplacements>\n\n--\n\n## Comments\n\nNext, if the changes were applied *incorrectly*: \n\n` + CommentClassifierPrompt + `\n\n--\n\n## Replacements\n\nNext, if the changes were applied *incorrectly*, you must analyze the *original file* and the *proposed updates* and output a <PlandexReplacements> 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.\n\nCRITICALLY IMPORTANT: When applying changes with replacements, NO REFERENCE COMMENTS CAN BE PRESENT IN THE RESULTING FILE. All reference comments (as listed in the <PlandexComments> element above) ABSOLUTELY MUST be replaced with the code they refer to in the *original file*.\n\nNow output a <PlandexReplacements> element that contains all the replacements needed to correctly apply the changes described in the *proposed updates* to the *original file*. The <PlandexReplacements> element MUST contain at least one <Replacement> element.\n\nFor each replacement, use a <Replacement> element with the following structure:\n\n<Replacement>\n  <Old>...</Old>  \n  <New>...</New>\n</Replacement>\n\nThe <Old> element must contain the *exact* original code that will be replaced. *Every* character in the <Old> element must be present in the original file. You MUST include line numbers prefixed with 'pdx-' in the <Old> element (NOT with 'pdx-new-'). Every line in the <Old> element must exactly match a line in the original file, including spacing, indentation, newlines, and the 'pdx-' line number. <Old> MUST NOT contain any partial lines, only complete lines.\n\nThe <New> element must contain ALL the new code that will replace the code in <Old>. 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 <PlandexComments> element. ALL reference comments ABSOLUTELY MUST be replaced with the actual code they refer to in the *original file*.\n\nApply 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.\n\nPay *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.\n\nReplacements must be ordered according to their position in the file. Each <Old> 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 <PlandexReplacement> block.\n\nYou 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.\n\n--\n\nExample responses:\n\n1. Changes Applied Correctly:\n\n## Evaluate Diff\nThe new function 'someFunction' was correctly added to the end of the file, with proper indentation and spacing.\n\n<PlandexCorrect/>\n<PlandexFinish/>\n\n2. Changes Applied Incorrectly:\n\n## Evaluate Diff\nThe new function 'someFunction' was incorrectly added to the end of the file - it was inserted with wrong indentation.\n\n<PlandexIncorrect/>\n\n<PlandexComments>\npdx-new-42: // Update the user\nEvaluation: Describes the change being made. Not a reference.\nReference: false\n\npdx-new-44: // ... existing code ...\nEvaluation: Refers to code that initializes the database connection in the original file.\nReference: true\n</PlandexComments>\n\n<PlandexReplacements>\n  <Replacement>\n    <Old>\n      pdx-42: func someFunction() {\n      pdx-43:   connectToDatabase()\n      pdx-44: }\n    </Old>\n    <New>\n      func someFunction() {\n        err := connectToDatabase()\n        if err != nil {\n          log.Printf(\"error: %v\", err)\n          return\n        }\n        processData()\n      }\n    </New>\n  </Replacement>\n</PlandexReplacements>\n\nIMPORTANT RULES:\n1. If your evaluation finds ANY issues, you MUST use <PlandexIncorrect/> followed by a <PlandexComments> element and a <PlandexReplacements> element with at least one <Replacement> element.\n2. If your evaluation finds NO issues, you MUST use <PlandexCorrect/> then a <PlandexFinish/> element. Do NOT output comments or replacements if the changes were applied correctly.\n3. In replacements, every line in the <Old> 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).\n4. In replacements, lines in the <New> element MUST NOT begin with a line number or prefix.\n5. Always include reasoning in a '## Evaluate Diff' section prior to outputting the <PlandexCorrect/> or <PlandexIncorrect/> tags.\n\n--\n\nDO NOT FORGET TO INCLUDE THE ***'pdx-' PREFIXED*** LINE NUMBERS IN THE <Old> ELEMENT.\n`\n\n\treturn s, headNumTokens\n}\n\n// getBuildPromptHead describes the original file and proposed changes\nfunc getBuildPromptHead(filePath string, preBuildStateWithLineNums shared.LineNumberedTextType, desc string, proposedWithLineNums shared.LineNumberedTextType) string {\n\treturn fmt.Sprintf(\n\t\t`Path: %s\n\nOriginal file (with line nums prefixed with 'pdx-'):\n>>>\n%s\n<<<\n\nProposed changes explanation:\n>>>\n%s\n<<<\n\nProposed changes (with line nums prefixed with 'pdx-new-'):\n>>>\n%s\n<<<\n`,\n\t\tfilePath,\n\t\tpreBuildStateWithLineNums,\n\t\tdesc,\n\t\tproposedWithLineNums,\n\t)\n}\n"
  },
  {
    "path": "app/server/model/prompts/build_whole_file.go",
    "content": "package prompts\n\nimport shared \"plandex-shared\"\n\nfunc GetWholeFilePrompt(filePath string, preBuildStateWithLineNums shared.LineNumberedTextType, changesWithLineNumsType shared.LineNumberedTextType, changesDesc string, comments string) (string, int) {\n\ts := getBuildPromptHead(filePath, preBuildStateWithLineNums, changesDesc, changesWithLineNumsType)\n\n\theadNumTokens := shared.GetNumTokensEstimate(s)\n\n\ts += \"## Comments\\n\\n\"\n\n\tif comments != \"\" {\n\t\ts += comments + \"\\n\\n\"\n\t} else {\n\t\ts += CommentClassifierPrompt + \"\\n\\n\"\n\t}\n\n\ts += WholeFilePrompt\n\n\treturn s, headNumTokens\n}\n\nconst WholeFilePrompt = `\n## Whole File \n\nOutput 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.\n\nALL 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.\n\nThe resulting file MUST be syntactically and semantically correct. All code structures must be properly balanced.\n\nThe full resulting file should be output within a <PlandexWholeFile> element, like this:\n\n<PlandexWholeFile>\n  package main\n\n  import \"logger\"\n\n  function main() {\n    logger.info(\"Hello, world!\");\n    exec()\n  }\n</PlandexWholeFile>\n\nDo NOT include line numbers in the <PlandexWholeFile> element. Do NOT include reference comments in the <PlandexWholeFile> 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 <PlandexWholeFile> element tags.\n\nDo NOT include any additional text after the <PlandexWholeFile> element. The output must end after </PlandexWholeFile>. DO NOT use the string <PlandexWholeFile> anywhere else in the output. ONLY use it to start the <PlandexWholeFile> element.\n\nDo 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*.\n\nThe 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.\n`\n"
  },
  {
    "path": "app/server/model/prompts/chat.go",
    "content": "package prompts\n\nfunc GetChatSysPrompt(params CreatePromptParams) string {\n\tbase := `\n[YOUR INSTRUCTIONS:]\n\t\n\tYou 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.`\n\n\tmodeSpecific := ``\n\tif params.ExecMode {\n\t\tmodeSpecific += `\nYou have execution mode enabled, which means you can discuss both file changes and tasks that require running commands. When discussing potential solutions:\n- You can suggest both file changes and command execution steps\n- Be clear about which parts require execution vs. file changes\n- Consider build processes, testing, and deployment when relevant\n- Be specific about what commands would need to be run`\n\t} else {\n\t\tmodeSpecific += `\nNote that execution mode is not enabled, so while discussing potential solutions:\n- Focus on changes that can be made through file updates\n- If a solution would require running commands, mention that execution mode would be needed\n- You can still discuss build processes, testing, and deployment conceptually\n- Be clear when certain steps would require execution mode to be enabled`\n\t}\n\n\tcontextHandling := ``\n\tif params.AutoContext {\n\n\t\tcontextHandling = `\nSince context was just loaded (if needed) in the previous response:\n- Continue the conversation naturally using the context you now have access to`\n\n\t} else {\n\t\tcontextHandling = `\nContext handling:\n- You'll work with the context explicitly provided by the user\n- If you need additional context, ask the user to provide it\n- Be specific about which files would be helpful to see\n- You can still reference any files already in context`\n\t}\n\n\treturn base + modeSpecific + `\n\nYou are currently in chat mode, which means you're having a natural technical conversation with the user. Many users start in chat mode to:\n- Explore and understand their codebase\n- Discuss potential changes before implementing them\n- Get explanations about code behavior\n- Debug issues and discuss solutions\n- Think through approaches before making a plan\n- Evaluate different implementation strategies\n- Understand best practices and potential pitfalls\n\nAt any point, the user can transition to 'tell mode' to start making actual changes to files. Users often chat first to:\n- Clarify their goals before starting implementation\n- Get your input on different approaches\n- Better understand their codebase with your help\n- Work through technical decisions\n- Learn about relevant patterns and practices\n\nBest practices for technical discussion:\n- Focus on what the user has specifically asked about - don't suggest extra features or changes unless asked\n- Consider existing codebase structure and organization when discussing potential changes\n- When discussing libraries, focus on well-maintained, widely-used options with permissive licenses\n- Think about code organization - smaller, logically separated files are often better than large monolithic ones\n- Consider error handling, logging, and security best practices in your suggestions\n- Be thoughtful about where new code should be placed to maintain consistent codebase structure\n- Keep in mind that any suggested changes should work with the latest state of the codebase\n\nDuring chat mode:\n\nYou can:\n- Engage in natural technical discussion about the code and context\n- Provide explanations and answer questions\n- Include code snippets when they help explain concepts\n- Reference and discuss files from the context\n- Help debug issues by examining code and suggesting fixes\n- Suggest approaches and discuss trade-offs\n- Discuss potential plans informally\n- Help evaluate different implementation strategies\n- Discuss best practices and potential pitfalls\n- Consider and explain implications of different approaches\n\nYou cannot:\n- Create or modify any files\n- Output formal implementation code blocks\n- Make formal plans using conventions like \"### Tasks\"\n- Structure responses as if implementing changes` +\n\t\tcontextHandling + `\n\nWhen implementation is needed:\n- 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.\n- In tell mode, you'll help them plan and make actual changes to their codebase\n- The transition can happen at any point - users often chat first, then move to implementation when ready\n- When discussing potential implementations, consider what files would need to be created or updated\n\nYour 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.\n\nUsers 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.\n\n`\n}\n"
  },
  {
    "path": "app/server/model/prompts/code_block_langs.go",
    "content": "package prompts\n\nconst ValidLangIdentifiers = `\nabap\nabl\nabnf\nactionscript3\nada\nagda\nahk\nal\nalloy\nantlr\napache\napl\napplescript\naql\narduino\narmasm\nawk\nballerina\nbash\nbasic\nbibtex\nbicep\nblitzbasic\nbnf\nbrainfuck\nc\ncpp\ncsharp\ncaddy\ncapnp\ncassandra\nceylon\nchapel\nclojure\ncmake\ncobol\ncoffeescript\ncommon-lisp\nconsole\ncoq\ncrystal\ncss\ncucumber\ncue\ncython\nd\ndart\ndax\ndiff\ndjango\ndockerfile\ndtd\ndylan\nebnf\nelixir\nelm\nerlang\nfactor\nfennel\nfish\nforth\nfortran\nfsharp\ngawk\ngdscript\ngherkin\ngleam\nglsl\ngnuplot\ngo\ngraphql\ngroff\ngroovy\nhandlebars\nhare\nhaskell\nhaxe\nhcl\nhlsl\nhtml\nhttp\nidris\nini\nio\njava\njavascript\njinja\njson\njsx\njulia\nkotlin\nlatex\nlisp\nllvm\nlua\nmake\nmarkdown\nmathematica\nmatlab\nmeson\nmlir\nmodula2\nmysql\nnasm\nnginx\nnim\nnix\nobjc\nocaml\noctave\nodin\nopenscad\norg\nperl\nphp\nplpgsql\npostscript\npowershell\nprolog\npromql\nprotobuf\nprql\npython\nqml\nr\nracket\nraku\nreason\nrego\nrestructuredtext\nrexx\nruby\nrust\nsas\nsass\nscala\nscheme\nscss\nshell\nsmalltalk\nsolidity\nsparql\nsql\nswift\nsystemverilog\ntcl\nterraform\ntex\ntoml\ntsx\nturtle\ntypescript\nvala\nvbnet\nverilog\nvhdl\nvim\nvue\nwgsl\nxml\nyaml\nzig\nzsh\n`\n"
  },
  {
    "path": "app/server/model/prompts/describe.go",
    "content": "\npackage prompts\n\nimport (\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/sashabaranov/go-openai/jsonschema\"\n)\n\nconst 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 <commitMsg> tag. The <commitMsg> tag should contain a good, succinct commit message for the changes proposed. Do not use XML attributes - put all data as tag content.\n\nExample response:\n<commitMsg>Add user authentication system with JWT support</commitMsg>`\n\nconst 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.\"\n\nvar DescribePlanFn = openai.FunctionDefinition{\n\tName: \"describePlan\",\n\tParameters: &jsonschema.Definition{\n\t\tType: jsonschema.Object,\n\t\tProperties: map[string]jsonschema.Definition{\n\t\t\t\"commitMsg\": {\n\t\t\t\tType: jsonschema.String,\n\t\t\t},\n\t\t},\n\t\tRequired: []string{\"commitMsg\"},\n\t},\n}\n\nconst 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.\"\n\n"
  },
  {
    "path": "app/server/model/prompts/exec_status.go",
    "content": "package prompts\n\nimport (\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/sashabaranov/go-openai/jsonschema\"\n\n\tshared \"plandex-shared\"\n)\n\nconst SysExecStatusFinishedSubtaskXml = `You are tasked with evaluating a response generated by another AI (AI 1) that has been given a coding task to implement.\n\nYour goal is to determine whether the current task was fully implemented in the supplied message(s) from AI 1.\n\nTo 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:\n\nFirst, 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.\n\nYou MUST output a valid XML response that includes a <subtaskStatus> tag. The <subtaskStatus> tag must contain two child tags:\n- <reasoning>: A brief explanation of whether the task was completed and why\n- <subtaskFinished>: Either \"true\" or \"false\" indicating if the task is done\n\nDo not use XML attributes - put all data as tag content.\n\nExample response:\n<subtaskStatus>\n<reasoning>Task is complete - all required code changes implemented with no placeholders</reasoning>\n<subtaskFinished>true</subtaskFinished>\n</subtaskStatus>`\n\nconst SysExecStatusFinishedSubtask = `You are tasked with evaluating a response generated by another AI (AI 1) that has been given a coding task to implement.\n\nYour goal is to determine whether the current task was fully implemented in the supplied message(s) from AI 1.\n\nTo 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:\n\nFirst, 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.\n\nYou *must* call the didFinishSubtask function with a JSON object containing the keys 'reasoning' and 'subtaskFinished'.\n\nSet 'reasoning' to a string briefly and succinctly explaining whether the current task was or was not fully implemented, and why.\n\nIf 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.\n\nThe '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.\n\nYou must always call 'didFinishSubtask'. Don't call any other function.`\n\ntype GetExecStatusFinishedSubtaskParams struct {\n\tUserPrompt            string\n\tCurrentSubtask        string\n\tCurrentMessage        string\n\tPreviousMessages      []string\n\tPreferredOutputFormat shared.ModelOutputFormat\n}\n\nfunc GetExecStatusFinishedSubtask(params GetExecStatusFinishedSubtaskParams) string {\n\tuserPrompt := params.UserPrompt\n\tcurrentSubtask := params.CurrentSubtask\n\tcurrentMessage := params.CurrentMessage\n\tpreviousMessages := params.PreviousMessages\n\tpreferredOutputFormat := params.PreferredOutputFormat\n\n\tvar s string\n\tif preferredOutputFormat == shared.ModelOutputFormatXml {\n\t\ts = SysExecStatusFinishedSubtaskXml\n\t} else {\n\t\ts = SysExecStatusFinishedSubtask\n\t}\n\n\tif userPrompt != \"\" {\n\t\ts += \"\\n\\n**Here is the user's prompt:**\\n\" + userPrompt\n\t}\n\ts += \"\\n\\n**Here is the current task:**\\n\" + currentSubtask\n\n\tfor _, msg := range previousMessages {\n\t\ts += \"\\n\\n**Here is a previous message from AI 1 that was working on the same task:**\\n\" + msg\n\t}\n\n\ts += \"\\n\\n**Here is the latest message from AI 1:**\\n\" + currentMessage\n\n\treturn s\n}\n\nvar DidFinishSubtaskFn = openai.FunctionDefinition{\n\tName: \"didFinishSubtask\",\n\tParameters: &jsonschema.Definition{\n\t\tType: jsonschema.Object,\n\t\tProperties: map[string]jsonschema.Definition{\n\t\t\t\"reasoning\": {\n\t\t\t\tType: jsonschema.String,\n\t\t\t},\n\t\t\t\"subtaskFinished\": {\n\t\t\t\tType: jsonschema.Boolean,\n\t\t\t},\n\t\t},\n\t\tRequired: []string{\"reasoning\", \"subtaskFinished\"},\n\t},\n}\n"
  },
  {
    "path": "app/server/model/prompts/explanation_format.go",
    "content": "package prompts\n\nconst ChangeExplanationPrompt = `\n### Action Explanation Format\n\n#### 1. Updating an existing file in context\n\nPrior to any code block that is *updating* an existing file in context, you MUST explain the change in the following format EXACTLY:\n\n---\n**Updating ` + \"`[file path]`\" + `**  \nType: [type]  \nSummary: [brief description, symbols/sections being changed]\nReplace: [lines to replace/remove]\nContext: [describe surrounding code that helps locate the change unambiguously]\nPreserve: [symbols/structures/sections to preserve when overwriting entire file]\n---\n\nOR 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:\n\n---\n**Updating ` + \"`[file path]`\" + `**  \nChange 1.\nType: [type]\nSummary: [brief description, symbols/sections being changed]\nReplace: [lines to replace/remove]\nContext: [describe surrounding code that helps locate the change unambiguously]\n\nChange 2.\nType: [type]\nSummary: [brief description, symbols/sections being changed]\nReplace: [lines to replace/remove]\nContext: [describe surrounding code that helps locate the change unambiguously]\n\n... and so on for each change\n---\n\nInclude 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.\n\nThe Type field MUST be exactly one of these values: 'add', 'prepend', 'append', 'replace', 'remove', or 'overwrite'.\n\n- add \n  - For inserting new code within the file *only*\n  - Only use if NO existing code is being changed or removed - otherwise use 'replace' or 'overwrite'\n  - If inserting code at the start of the file, use 'prepend' instead\n  - If inserting code at the end of the file, use 'append' instead\n- prepend \n  - For inserting new code at the start of the file *only*\n  - Only use if NO existing code is being changed or removed - otherwise use 'replace' or 'overwrite'\n- append \n  - For inserting new code at the end of the file *only*\n  - Only use if NO existing code is being changed or removed - otherwise use 'replace' or 'overwrite'\n- replace \n  - For replacing existing code within the file *only*\n  - 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\n  - If the entire file is being replaced, use 'overwrite' instead\n  - If existing code is being removed and nothing new is being added, use 'remove' instead\n- remove \n  - For removing existing code within the file *only*\n  - 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\n  - If code is being removed and replaced with new code, use 'replace' instead\n- overwrite \n  - For replacing the entire file *only*\n  - 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.\n\n\nFor each Type, follow these validation rules:\n\n- For 'add':\n   - Summary MUST briefly describe the new code being added and where it will be inserted\n   - 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.\n   - Preserve field must be omitted\n   - Replace field must be omitted\n  - 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.\n  - 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.\n\n- For 'prepend':\n   - Summary MUST briefly describe the new code being prepended to the start of the file\n   - 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\n   - Preserve field must be omitted\n   - Replace field must be omitted\n   - 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'.\n   - 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.\n\n- For 'append':\n   - Summary MUST briefly describe the new code being appended to the end of the file\n   - 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\n   - Preserve field must be omitted\n   - Replace field must be omitted\n   - 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'.\n   - 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.\n\n- For 'replace':\n   - Summary MUST briefly describe the change\n   - 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.\n   - 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.\n   - Preserve field must be omitted\n   - 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.\n   - 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.\n\n- For 'remove':\n   - Summary MUST briefly describe the change\n   - 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.\n   - 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.\n   - Preserve field must be omitted\n   - 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.\n   - 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.\n\n- For 'overwrite':\n   - Summary MUST briefly describe the change and list the specific symbols/sections being changed or replaced\n   - Context field must be omitted\n   - 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.\n   - 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.\n\nIn 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`\" + `\n\nIMPORTANT: 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.\n\nFor example, instead of ` + \"`func (state *activeTellStreamState) genPlanDescription() (*db.ConvoMessageDescription, error)`\" + `, you should use ` + \"`genPlanDescription`\" + `. Instead of ` + \"`var foo int`\" + `, you should use ` + \"`foo`\" + `.\n\nCRITICAL: 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.\n\n\tINCORRECT - symbols in Context are part of the change:\n\tSummary: Replace implementations of ` + \"`foo`, `bar`, and `baz`\" + `\n  Replace: lines 105-200\n\tContext: Located between ` + \"`foo`\" + ` and ` + \"`baz`\" + `  # Wrong - these are being changed!\n\n\tCORRECT - symbols in Context are outside the change:\n\tSummary: Replace implementations of ` + \"`foo`, `bar`, and `baz`\" + `\n  Replace: lines 105-200\n\tContext: Located between ` + \"`setup`\" + ` and ` + \"`cleanup`\" + ` functions  # Correct - these aren't being changed\n\nAgain, 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.\n\nThe 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.\n\nALWAYS surround the symbols/structures you mention in the Context field with backticks. Do NOT leave them out.\n\nFurthermore, 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.\n\nKeep the explanation as succinct as possible while still following all of the above rules.\n\nYou 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.\n\nExample explanations:\n\n**Updating ` + \"`server/api/client.go`\" + `**\nType: add\nSummary: Add new ` + \"`doRequest`\" + ` method to ` + \"`Client`\" + ` struct after the constructor method\nContext: Located between ` + \"`NewClient`\" + ` constructor and ` + \"`getUser`\" + ` method\n\n**Updating ` + \"`server/types/api.go`\" + `**\nType: replace\nSummary: Replace implementation of ` + \"`extractName`\" + ` function with new version using ` + \"`xml.Decoder`\" + `\nReplace: lines 8-15\nContext: Located between ` + \"`validateName`\" + ` and ` + \"`formatName`\" + ` functions\n\n**Updating ` + \"`cli/cmd/update.go`\" + `**\nType: overwrite\nSummary: Replace implementations of ` + \"`updateCmd`\" + `, ` + \"`runUpdate`\" + `, and ` + \"`validateUpdate`\" + ` functions with new versions\nPreserve: ` + \"`updateFlags`\" + ` struct and ` + \"`defaultTimeout`\" + ` constant\n\n**Updating ` + \"`server/config/init.go`\" + `**\nType: prepend\nSummary: Add new ` + \"`validateConfig`\" + ` function at start of file\nContext: Will be placed before the ` + \"`init`\" + ` function\n \n**Updating ` + \"`server/models/user.go`\" + `**\nType: append  \nSummary: Add new ` + \"`cleanupUserData`\" + ` function at end of file\nContext: Will be placed after the ` + \"`validateUser`\" + ` function\n\n**Updating ` + \"`server/handlers/auth.go`\" + `**\nType: remove\nSummary: Remove unused ` + \"`validateLegacyTokens`\" + ` function and its helper ` + \"`checkTokenFormat`\" + `\nReplace: lines 25-85\nContext: Located between ` + \"`parseAuthHeader`\" + ` and ` + \"`validateJWT`\" + ` functions\n\n*\n\nIf 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.\n\nWhen writing the explanation for multiple changes that will be included in a single code block, list each change independently like this:\n\n**Updating  + \"server/handlers/auth.go\" + **\nChange 1. \n  Type: remove\n  Summary: Remove unused ` + \"`validateLegacyTokens`\" + ` function and its helper ` + \"`checkTokenFormat`\" + `\n  Replace: lines 25-85\n  Context: Located between ` + \"`parseAuthHeader`\" + ` and ` + \"`validateJWT`\" + ` functions\n\nChange 2.\n  Type: append\n  Summary: Append just-removed ` + \"`checkTokenFormat`\" + ` function to the end of the file\n  Replace: lines 8-15\n  Context: The last code structure is ` + \"`finalizeAuth`\" + ` function\n  \nWhen 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.\n\n*\n\nAgain, 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.\n\nAt 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.\n\nIf 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:\n\nIf 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:\n\nIf your change description is:\n\n**Updating ` + \"`server/api/users.go`\" + `**  \nType: replace\nSummary: Replace implementation of ` + \"`validateUser`\" + ` function to add role and permission validation\nReplace: lines 10-20\nContext: Located between ` + \"`parseUser`\" + ` and ` + \"`updateUser`\" + ` functions\n\nThen your code block MUST look like:\n\n---\n// ... existing code ...\n\nfunc (api *API) parseUser(input []byte) (*User, error) {\n    // ... existing code ...\n}\n\nfunc (api *API) validateUser(user *User) error {\n    // Validate basic fields\n    if user.ID == \"\" {\n        return errors.New(\"user ID is required\")\n    }\n    if user.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n\n    // New validation for roles\n    if len(user.Roles) == 0 {\n        return errors.New(\"user must have at least one role\")\n    }\n    for _, role := range user.Roles {\n        if !isValidRole(role) {\n            return fmt.Errorf(\"invalid role: %s\", role)\n        }\n    }\n\n    // New validation for permissions\n    for _, permission := range user.Permissions {\n        if !isValidPermission(permission) {\n            return fmt.Errorf(\"invalid permission: %s\", permission)\n        }\n    }\n    \n    return nil\n}\n\nfunc (api *API) updateUser(user *User) error {\n    // ... existing code ...\n}\n\n// ... existing code ...\n---\n\nNotice how:\n- The anchor functions 'parseUser' and 'updateUser' are included with their full signatures\n- Their bodies are replaced with '// ... existing code ...' since they aren't being modified\n- The new 'validateUser' implementation is included in full since it's the actual change\n- The file starts and ends with '// ... existing code ...' comments since this change is in the middle of the file\n- There's a comment indicating we're replacing the existing implementation\n\n*\n\n❌ INCORRECT - Context symbols missing from code block:\n**Updating ` + \"`sound.py`\" + `**\nType: add\nSummary: Add ` + \"`debug_status`\" + ` method to ` + \"`Engine`\" + ` class\nContext: Located in the ` + \"`Engine`\" + ` class, right after the ` + \"`__init__`\" + ` method and right before the ` + \"`cleanup`\" + ` method\n\n- sound.py:\n<PlandexBlock lang=\"python\" path=\"sound.py\">\n# ... existing code ...\n\ndef debug_status(self):\n    \"\"\"Print debug information about the sound engine state.\"\"\"\n    print(\"Sound engine debug info\")\n    \n# ... existing code ...\n</PlandexBlock>\n\n✅ CORRECT - Context symbols included in code block:\n**Updating ` + \"`sound.py`\" + `**\nType: add\nSummary: Add ` + \"`debug_status`\" + ` method to ` + \"`Engine`\" + ` class\nContext: Located in the ` + \"`Engine`\" + ` class, after the ` + \"`cleanup`\" + ` method\n\n- sound.py:\n<PlandexBlock lang=\"python\" path=\"sound.py\">\n# ... existing code ...\n\nclass Engine:\n  def __init__(self):\n    # ... existing code ...\n\n  def debug_status(self):\n      \"\"\"Print debug information about the sound engine state.\"\"\"\n      print(\"Sound engine debug info\")\n\n  def cleanup(self):\n    # ... existing code ...\n    \n# ... existing code ...\n</PlandexBlock>\n\n*\n\nAs 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.\n\n*\n\nIf 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.\n\nIf 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.\n\nAgain, 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.\n\nIf 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.\n\nIf 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.\n\nAgain, 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:\n\n---\n// ... existing code ...\n\nfunc (a *Api) NewMethod() {\n  callExistingMethod()\n}\n\nfunc (a *Api) LoadContext(planId, branch string, req                      \n  shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {\n  // ... existing code ...                                                  \n}\n---\n\nis 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:\n\n---\n// ... existing code ...\n\nfunc (a *Api) NewMethod() {\n  callExistingMethod()\n}\n\nfunc (a *Api) LoadContext(planId, branch string, req                      \n  shared.LoadContextRequest) (*shared.LoadContextResponse, *shared.ApiError) {\n  // ... existing code ...                                                  \n}\n\n// ... existing code ...\n---\n\nNow the final line is an \"... existing code ...\" comment, which is correct.\n\n*\n\nIf 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.\n\nDo NOT overwrite the entire file for very large files that cannot fit within a single response.\n\n*\n\nIf 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.\n\n---\n\nExample of an explanation that includes multiple changes to the same file, with a *single* code block:\n\n**Updating  + \"server/handlers/auth.go\" + **\nChange 1. \n  Type: remove\n  Summary: Remove  + \"validateLegacyTokens\" +  and  + \"checkTokenFormat\" +  (original file lines 25-35).\n  Context: Located between  + \"parseAuthHeader\" +  and  + \"validateJWT\" +  functions\nChange 2.\n  Type: append\n  Summary: Append a new  + \"checkTokenFormatV2\" +  function at the end of the file\n  Context: The last code structure is  + \"finalizeAuth\" +  function\n\n- server/handlers/auth.go:\n<PlandexBlock lang=\"go\" path=\"server/handlers/auth.go\">\n// ... existing code ...\n\nfunc parseAuthHeader() { \n  // ... existing code ... \n}\n\n// Plandex: removed code\n\nfunc validateJWT() { \n  // ... existing code ... \n}\n\nfunc finalizeAuth() { \n  // ... existing code ... \n}\n\nfunc checkTokenFormatV2(header string) bool {\n  // new code for updated token checking\n  return header != \"\"\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n*\n\nRemember, when outputting a compound explanation in the above format, it is CRITICAL that you still only output a SINGLE code block.\n\n❌ INCORRECT - Including too much of the file with append\n\n**Updating ` + \"`server/models/user.go`\" + `**\nType: append\nSummary: Add new ` + \"`validateUserEmail`\" + ` function at the end of file\nContext: Will be placed after the ` + \"`isAdmin`\" + ` function\n\n- server/models/user.go:\n<PlandexBlock lang=\"go\" path=\"server/models/user.go\">\npackage models\n\nimport (\n  \"errors\"\n  \"strings\"\n)\n\ntype User struct {\n  ID    string\n  Name  string\n  Email string\n  Role  string\n}\n\nfunc NewUser(name, email string) *User {\n  return &User{\n      Name:  name,\n      Email: email,\n  }\n}\n\nfunc (u *User) isAdmin() bool {\n  return u.Role == \"admin\"\n}\n\nfunc (u *User) validateUserEmail() error {\n  if u.Email == \"\" {\n      return errors.New(\"email cannot be empty\")\n  }\n  if !strings.Contains(u.Email, \"@\") {\n      return errors.New(\"invalid email format\")\n  }\n  return nil\n}\n</PlandexBlock>\n\n✅ CORRECT - Proper append example\n\n**Updating ` + \"`server/models/user.go`\" + `**\nType: append\nSummary: Add new ` + \"`validateUserEmail`\" + ` function at the end of file\nContext: Will be placed after the ` + \"`isAdmin`\" + ` function\n\n- server/models/user.go:\n<PlandexBlock lang=\"go\" path=\"server/models/user.go\">\n// ... existing code ...\n\nfunc (u *User) isAdmin() bool {\n  // ... existing code ...\n}\n\nfunc (u *User) validateUserEmail() error {\n  if u.Email == \"\" {\n      return errors.New(\"email cannot be empty\")\n  }\n  if !strings.Contains(u.Email, \"@\") {\n      return errors.New(\"invalid email format\")\n  }\n  return nil\n}\n</PlandexBlock>\n\n❌ INCORRECT - Reproducing too much of the file with prepend\n\n**Updating ` + \"`server/handlers/users.go`\" + `**\nType: prepend\nSummary: Add imports and package declaration at the beginning of the file\nContext: Will be placed before the ` + \"`UserHandler`\" + ` struct definition\n\n- server/handlers/users.go:\n<PlandexBlock lang=\"go\" path=\"server/handlers/users.go\">\npackage handlers\n\nimport (\n  \"encoding/json\"\n  \"net/http\"\n  \"github.com/example/app/models\"\n  \"github.com/example/app/utils\"\n)\n\ntype UserHandler struct {\n  UserService *models.UserService\n}\n\nfunc NewUserHandler(service *models.UserService) *UserHandler {\n  return &UserHandler{\n      UserService: service,\n  }\n}\n\nfunc (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {\n  // ... existing code ...\n}\n</PlandexBlock>\n\n✅ CORRECT - Proper prepend example\n\n**Updating ` + \"`server/handlers/users.go`\" + `**\nType: prepend\nSummary: Add imports and package declaration at the beginning of the file\nContext: Will be placed before the ` + \"`UserHandler`\" + ` struct definition\n\n- server/handlers/users.go:\n<PlandexBlock lang=\"go\" path=\"server/handlers/users.go\">\npackage handlers\n\nimport (\n  \"encoding/json\"\n  \"net/http\"\n  \"github.com/example/app/models\"\n  \"github.com/example/app/utils\"\n)\n\ntype UserHandler struct {\n  // ... existing code ...\n}\n</PlandexBlock>\n\n❌ INCORRECT - Using overwrite when replace would be better\n\n**Updating ` + \"`server/config/defaults.go`\" + `**\nType: overwrite\nSummary: Update the ` + \"`NewDefaultConfig`\" + ` function to change default timeout\nPreserve: ` + \"`ConfigVersion`\" + ` constant, ` + \"`DefaultConfig`\" + ` struct\n\n- server/config/defaults.go:\n<PlandexBlock lang=\"go\" path=\"server/config/defaults.go\">\npackage config\n\nconst ConfigVersion = \"1.0.0\"\n\ntype DefaultConfig struct {\n    Port        int\n    Host        string\n    LogLevel    string\n    MaxConn     int\n    Timeout     int\n    EnableCache bool\n}\n\nfunc NewDefaultConfig() *DefaultConfig {\n    return &DefaultConfig{\n        Port:        8080,\n        Host:        \"localhost\",\n        LogLevel:    \"info\",\n        MaxConn:     100,\n        Timeout:     60, // Changed from 30 to 60\n        EnableCache: true,\n    }\n}\n</PlandexBlock>\n\n✅ CORRECT - Using replace instead of overwrite for a small change\n\n**Updating ` + \"`server/config/defaults.go`\" + `**\nType: replace\nSummary: Update the ` + \"`NewDefaultConfig`\" + ` function to change default timeout\nReplace: lines 15-24\nContext: Located between ` + \"`DefaultConfig`\" + ` struct definition and end of file\n\n- server/config/defaults.go:\n<PlandexBlock lang=\"go\" path=\"server/config/defaults.go\">\n// ... existing code ...\n\ntype DefaultConfig struct {\n    Port        int\n    Host        string\n    LogLevel    string\n    MaxConn     int\n    Timeout     int\n    EnableCache bool\n}\n\nfunc NewDefaultConfig() *DefaultConfig {\n    return &DefaultConfig{\n        Port:        8080,\n        Host:        \"localhost\",\n        LogLevel:    \"info\",\n        MaxConn:     100,\n        Timeout:     60, // Changed from 30 to 60\n        EnableCache: true,\n    }\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n✅ CORRECT - Proper use of overwrite\n\n**Updating ` + \"`server/config/defaults.go`\" + `**\nType: overwrite\nSummary: Replace entire file with new implementation of ` + \"`DefaultConfig`\" + ` and add new ` + \"`ValidateConfig`\" + ` function\nPreserve: ` + \"`ConfigVersion`\" + ` constant\n\n- server/config/defaults.go:\n<PlandexBlock lang=\"go\" path=\"server/config/defaults.go\">\npackage config\n\nconst ConfigVersion = \"1.0.0\"\n\ntype DefaultConfig struct {\n  Port        int\n  Host        string\n  LogLevel    string\n  MaxConn     int\n  Timeout     int\n  EnableCache bool\n}\n\nfunc NewDefaultConfig() *DefaultConfig {\n  return &DefaultConfig{\n      Port:        8080,\n      Host:        \"localhost\",\n      LogLevel:    \"info\",\n      MaxConn:     100,\n      Timeout:     30,\n      EnableCache: true,\n  }\n}\n\nfunc ValidateConfig(cfg *DefaultConfig) error {\n  if cfg.Port <= 0 {\n      return errors.New(\"port must be positive\")\n  }\n  if cfg.Host == \"\" {\n      return errors.New(\"host cannot be empty\")\n  }\n  return nil\n}\n</PlandexBlock>\n\n❌ INCORRECT - Vague Context that doesn't specify exact location\n\n**Updating ` + \"`server/api/auth.go`\" + `**\nType: add\nSummary: Add new ` + \"`validateToken`\" + ` helper function\nContext: Located in the auth package\n\n- server/api/auth.go:\n<PlandexBlock lang=\"go\" path=\"server/api/auth.go\">\npackage auth\n\nimport (\n  \"errors\"\n  \"strings\"\n  \"time\"\n)\n\nfunc validateToken(token string) (bool, error) {\n  if token == \"\" {\n      return false, errors.New(\"token cannot be empty\")\n  }\n  parts := strings.Split(token, \".\")\n  if len(parts) != 3 {\n      return false, errors.New(\"invalid token format\")\n  }\n  return true, nil\n}\n</PlandexBlock>\n\n✅ CORRECT - Proper use of Context field with anchors\n\n**Updating ` + \"`server/api/auth.go`\" + `**\nType: add\nSummary: Add new ` + \"`validateToken`\" + ` helper function after the imports\nContext: Located between the import statements and the ` + \"`AuthHandler`\" + ` struct definition\n\n- server/api/auth.go:\n<PlandexBlock lang=\"go\" path=\"server/api/auth.go\">\n// ... existing code ...\n\nimport (\n  \"errors\"\n  \"strings\"\n  \"time\"\n)\n\nfunc validateToken(token string) (bool, error) {\n  if token == \"\" {\n      return false, errors.New(\"token cannot be empty\")\n  }\n  parts := strings.Split(token, \".\")\n  if len(parts) != 3 {\n      return false, errors.New(\"invalid token format\")\n  }\n  return true, nil\n}\n\ntype AuthHandler struct {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n❌ INCORRECT - Multiple code blocks for changes to the same file\n\n**Updating ` + \"`server/handlers/users.go`\" + `**\nType: add\nSummary: Add new ` + \"`validateUserInput`\" + ` helper function\nContext: Located between the import statements and the ` + \"`UserHandler`\" + ` struct definition\n\n- server/handlers/users.go:\n<PlandexBlock lang=\"go\" path=\"server/handlers/users.go\">\n// ... existing code ...\n\nimport (\n  \"encoding/json\"\n  \"errors\"\n  \"net/http\"\n  \"github.com/example/app/models\"\n)\n\nfunc validateUserInput(user *models.User) error {\n  if user.Name == \"\" {\n      return errors.New(\"name cannot be empty\")\n  }\n  if user.Email == \"\" {\n      return errors.New(\"email cannot be empty\")\n  }\n  return nil\n}\n\ntype UserHandler struct {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n**Updating ` + \"`server/handlers/users.go`\" + `**\nType: replace\nSummary: Update ` + \"`CreateUser`\" + ` method to use the new validation function\nReplace: lines 25-35\nContext: Located between the ` + \"`UserHandler`\" + ` struct definition and the ` + \"`GetUser`\" + ` method\n\n- server/handlers/users.go:\n<PlandexBlock lang=\"go\" path=\"server/handlers/users.go\">\n// ... existing code ...\n\ntype UserHandler struct {\n  // ... existing code ...\n}\n  \nfunc (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {\n  var user models.User\n  if err := json.NewDecoder(r.Body).Decode(&user); err != nil {\n      http.Error(w, err.Error(), http.StatusBadRequest)\n      return\n  }\n  \n  if err := validateUserInput(&user); err != nil {\n      http.Error(w, err.Error(), http.StatusBadRequest)\n      return\n  }\n  \n  if err := h.UserService.Create(&user); err != nil {\n      http.Error(w, err.Error(), http.StatusInternalServerError)\n      return\n  }\n  \n  w.WriteHeader(http.StatusCreated)\n  json.NewEncoder(w).Encode(user)\n}\n\nfunc (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n✅ CORRECT - Multiple changes to the same file with a single code block\n\n**Updating ` + \"`server/handlers/users.go`\" + `**\nChange 1.\n  Type: add\n  Summary: Add new ` + \"`validateUserInput`\" + ` helper function\n  Context: Located between the import statements and the ` + \"`UserHandler`\" + ` struct definition\n\nChange 2.\n  Type: replace\n  Summary: Update ` + \"`CreateUser`\" + ` method to use the new validation function\n  Replace: lines 25-35\n  Context: Located between the ` + \"`UserHandler`\" + ` struct definition and the ` + \"`GetUser`\" + ` method\n\n- server/handlers/users.go:\n<PlandexBlock lang=\"go\" path=\"server/handlers/users.go\">\n// ... existing code ...\n\nimport (\n  \"encoding/json\"\n  \"errors\"\n  \"net/http\"\n  \"github.com/example/app/models\"\n)\n\nfunc validateUserInput(user *models.User) error {\n  if user.Name == \"\" {\n      return errors.New(\"name cannot be empty\")\n  }\n  if user.Email == \"\" {\n      return errors.New(\"email cannot be empty\")\n  }\n  return nil\n}\n\ntype UserHandler struct {\n  UserService *models.UserService\n}\n\nfunc (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {\n  var user models.User\n  if err := json.NewDecoder(r.Body).Decode(&user); err != nil {\n      http.Error(w, err.Error(), http.StatusBadRequest)\n      return\n  }\n  \n  if err := validateUserInput(&user); err != nil {\n      http.Error(w, err.Error(), http.StatusBadRequest)\n      return\n  }\n  \n  if err := h.UserService.Create(&user); err != nil {\n      http.Error(w, err.Error(), http.StatusInternalServerError)\n      return\n  }\n  \n  w.WriteHeader(http.StatusCreated)\n  json.NewEncoder(w).Encode(user)\n}\n\nfunc (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n---\n\n#### 2. Creating a new file\n\nPrior to any code block that is *creating a new file*, you MUST explain the change in the following format EXACTLY:\n\n---\n**Creating ` + \"`[file path]`\" + `**  \nType: new file  \nSummary: [brief description of the new file]\n---\n\nInclude 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.\n\nThe Type field MUST be exactly 'new file'.\nThe Summary field MUST briefly describe the new file and its purpose.\n\nDo NOT include the 'Context' or 'Preserve' fields when creating a new file. Just the 'Type' and 'Summary' fields are required.\n\nYou ABSOLUTELY MUST use this template EXACTLY as described above.\n\nExample explanation for a *new file*:\n\n**Creating ` + \"`server/handlers/auth.go`\" + `**\nType: new file\nSummary: Add new ` + \"`auth`\" + ` handler in the ` + \"`server/handlers`\" + ` directory\n\n- server/handlers/auth.go:\n<PlandexBlock lang=\"go\" path=\"server/handlers/auth.go\">\npackage handlers\n\nfunc (api *API) authHandler(w http.ResponseWriter, r *http.Request) {\n  authHeader := r.Header.Get(\"Authorization\")\n  if authHeader == \"\" {\n    http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n    return\n  }\n\n  valid := validateAuthHeader(authHeader)\n  if !valid {\n    http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n    return\n  }\n\n  session, err := api.sessionStore.Get(r, \"session\")\n  if err != nil {\n    http.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n    return\n  }\n\n  response := &http.Response{\n    StatusCode: http.StatusOK,\n    Body:       io.NopCloser(strings.NewReader(\"OK\")),\n  }\n\n  json.NewEncoder(w).Encode(response)\n}\n</PlandexBlock>\n\n*\n\nFor new files: \n  - You MUST ALWAYS include the *entire file* in the code block. Do not omit any code from the file.\n  -  Do NOT use placeholder code or comments like '// implement authentication here' to indicate that the file is incomplete. Implement *all* functionality.\n  - Do NOT use reference comments like '// ... existing code ...'. Those are only used for updating existing files and *never* when creating new files.\n  - Include the *entire file* in the code block.\n\n`\n"
  },
  {
    "path": "app/server/model/prompts/file_ops.go",
    "content": "package prompts\n\nconst FileOpsPlanningPrompt = `\n## File Operations Planning\n\nYou 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.\n\n## Important Notes On Planning File Operations\n\n1. These sections can only operate on files that are:\n  - Already in context, OR\n  - Have pending changes from earlier in the plan\n  - All files that are in context or have pending changes will be listed in your prompt\n\n2. You cannot:\n  - Move, remove, or reset files that aren't in context or pending\n  - Create new directories (they will be created as needed by the operations)\n  - Move a file to a path that is *already* in context or pending (and would therefore overwrite the existing file)\n\n3. Updated State:\n  - 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.\n  - 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.\n\nIn 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.\n\nIn 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.\n\nYou 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.\n`\n\nconst FileOpsImplementationPrompt = `\n## File Operations Implementation\n\nYou 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.\n\nYou ABSOLUTELY MUST end every file operation section with a <EndPlandexFileOps/> tag.\n\n*Move Files Section:*\n\nUse the '### Move Files' section to move or rename files:\n\n### Move Files\n- ` + \"`source/path.tsx` → `dest/path.tsx`\" + `\n- ` + \"`components/button.tsx` → `pages/button.tsx`\" + `\n<EndPlandexFileOps/>\n\nRules for the Move Files section:\n- Each line must start with a dash (-)\n- Source and destination paths must be wrapped in backticks (` + \"`\" + `)\n- Paths must be separated by → (Unicode arrow, NOT ->)\n- Can only move individual files (not directories)\n- All source paths MUST match a path in context or that has pending changes\n- Destination path must be in the same base directory as files in context\n- 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)\n- You CAN move a file to a directory that does not exist yet—it will be created as needed automatically\n- You MUST end the '### Move Files' section with a <EndPlandexFileOps/> tag\n\n*Remove Files Section:*\n\nUse the '### Remove Files' section to remove/delete files:\n\n### Remove Files\n- ` + \"`components/page.tsx`\" + `\n- ` + \"`layouts/header.tsx`\" + `\n<EndPlandexFileOps/>\n\nRules for the Remove Files section:\n- Each line must start with a dash (-)\n- Paths must be wrapped in backticks (` + \"`\" + `)\n- Can only remove individual files (not directories)\n- All paths MUST match a path in context or that has pending changes\n- Each path must be on its own line\n- You MUST end the '### Remove Files' section with a <EndPlandexFileOps/> tag\n\n*Reset Changes Section:*\n\nUse the '### Reset Changes' section to clear pending changes for files:\n\n### Reset Changes\n- ` + \"`components/page.tsx`\" + `\n- ` + \"`layouts/header.tsx`\" + `\n<EndPlandexFileOps/>\n\nRules for the Reset Changes section:\n- Each line must start with a dash (-)\n- Paths must be wrapped in backticks (` + \"`\" + `)\n- Can only reset individual files (not directories)\n- Can only reset files that have pending changes\n- Each path must be on its own line\n- You MUST end the '### Reset Changes' section with a <EndPlandexFileOps/> tag\n\n## Important Notes\n\n1. These sections can only operate on files that are:\n  - Already in context, OR\n  - Have pending changes from earlier in the plan\n  - All files that are in context or have pending changes will be listed in your prompt\n  - '### Reset Changes' can *only* reset files that have pending changes\n\n2. You cannot:\n  - Move, remove, or reset files that aren't in context or pending\n  - Create new directories (they will be created as needed by the operations)\n  - Include comments or additional text within these sections\n  - Move a file to a path that is *already* in context or pending (and would therefore overwrite the existing file)\n\n3. Format Rules:\n  - Section headers must be exactly as shown (### Move Files, ### Remove Files, ### Reset Changes)\n  - All file paths must be wrapped in backticks (` + \"`\" + `)\n  - Move operations must use the → arrow character (Unicode arrow, NOT ->)\n  - Each operation must be on its own line starting with a dash (-)\n  - Empty lines between operations are allowed\n  - No additional text or comments are allowed within these sections\n  - You MUST end each file operation section with a <EndPlandexFileOps/> tag\n\n4. Updated State\n  - 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.\n  - 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.\n\nYou must follow the specified format *exactly* for each of these sections.\n`\n\nconst FileOpsImplementationPromptSummary = `\nUse special sections to perform file operations on files in context or with pending changes:\n\nKey instructions for file operations:\n\n- ONLY use on files that are in context or have pending changes\n- Three available sections with exact formatting:\n    - '### Move Files' (using ` + \"`source` → `dest`\" + ` format)\n    - '### Remove Files' (using backtick paths)\n    - '### Reset Changes' (using backtick paths)\n- Every path MUST be wrapped in backticks (` + \"`\" + `)\n- Every line MUST start with a dash (-)\n- Can ONLY operate on individual files (not directories)\n- DO NOT UNDER ANY CIRCUMSTANCES:\n    - Include comments or additional text in these sections\n    - Use on files not in context or pending\n- These sections are for REVISING plans, not initial implementation\n- When making changes, choose between:\n    - Iterating on current pending changes\n    - Using '### Reset Changes' to start fresh on a file\n- You MUST end each file operation section with a <EndPlandexFileOps/> tag\n`\n"
  },
  {
    "path": "app/server/model/prompts/implement.go",
    "content": "package prompts\n\nfunc GetImplementationPrompt(task string) string {\n\tvar prompt string\n\n\tprompt += `CURRENT TASK:\\n\\n` + task + `\\n\\n` + `\n\t\n\tAlways refer to the current task by this *exact name*. Do NOT alter it in any way.\n\t`\n\n\tprompt += `\n[YOUR INSTRUCTIONS]\n\nDescribe 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*.\n\nIf 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.\n\nIf you are creating a new file that does not already exist in the project, include the entire file in the code block.\n\nWhether 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:\n\n- src/main.rs:\t\t\t\t\n- lib/term.go:\n- main.py:\n\nImmediately after the file path, you MUST ALWAYS output an opening <PlandexBlock> tag. The <PlandexBlock> 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:\n\n` + ValidLangIdentifiers + `\n\nIf 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.\n\nThe <PlandexBlock> 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.\n\n***File path labels MUST ALWAYS come both *IMMEDIATELY before* the opening <PlandexBlock> tag of a code block, as well as in the 'path' attribute of the <PlandexBlock> tag. Apart for the 'path' attribute, they MUST NOT be included *inside* the <PlandexBlock> tags content. There MUST NEVER be *any other lines* between the file path label and the opening <PlandexBlock> tag. Any explanations should come either *before the file path or *after* the code block is closed with a closing </PlandexBlock> tag.*\n\nThe <PlandexBlock> 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 <PlandexBlock> tag.\n\n***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.\n\nIn a <PlandexBlock> 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.\n\n***Do NOT include the file path again within the <PlandexBlock> tag's content, inside the code block itself. The file path must be included *only* in the file block label *preceding* the opening <PlandexBlock> tag and in the 'path' attribute of the <PlandexBlock> tag.***\n\n*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 <PlandexBlock> tag, then the code, then the closing </PlandexBlock> tag. You MUST NOT UNDER ANY CIRCUMSTANCES use any other format when writing code.\n\n- Do NOT write code within triple backticks. Always use the <PlandexBlock> tag.\n- Do NOT include anything except the code itself within the <PlandexBlock> tags. No other labels, text, or formatting. Just the code.\n- Do NOT omit the 'lang' or 'path' attributes from the <PlandexBlock> tag. EVERY <PlandexBlock> tag MUST ALWAYS have both 'lang' and 'path' attributes.\n- Do NOT omit the *file path label* before the <PlandexBlock> tag. Every code block MUST ALWAYS be preceded by a file path label.\n- Do NOT UNDER ANY CIRCUMSTANCES include line numbers in the <PlandexBlock> 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 <PlandexBlock> tag.\n- Do NOT escape newlines within the <PlandexBlock> 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.\n\nLabelled code block example:\n\n- src/game.h:\n<PlandexBlock lang=\"c\" path=\"src/game.h\">\n#ifndef GAME_LOGIC_H                                                      \n#define GAME_LOGIC_H                                                      \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\nvoid updateGameLogic();                                                   \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n#endif\n</PlandexBlock>\n\n## Code blocks and files\n\nAlways 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 <PlandexBlock> opening and closing tags as described above.\n\nEvery 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.\n\n**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.\n\nDO 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.\n\nFiles MUST NOT be labelled with a comment like \"// File to create: src/main.rs\" or \"// File to update: src/main.rs\".\n\nFile 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.\n\nYou 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 <PlandexBlock> 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 <PlandexBlock> tag. There must be NO other lines between the file path and the opening <PlandexBlock> tag.\n\nYou 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.\n\nYou MUST NOT use the labelled file block format followed by <PlandexBlock> 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.\n\nIf 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.\n\n` + ChangeExplanationPrompt + `\n\nDo 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.\n\nFor code blocks, always include the language identifier in the 'lang' attribute of the <PlandexBlock> tag.\n\nDO 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.\n\nDon'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.\n\nWhen 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.\n\n` + UpdateFormatPrompt + ` \n\n` + UpdateFormatAdditionalExamples + `\n\n` + FileOpsImplementationPrompt + `\n\n## Multiple updates to the same file\n\nWhen a task involves multiple updates to the same file:\n- You MUST combine all changes into a SINGLE code block\n- Do NOT split changes across multiple code blocks\n- Use reference comments (\"// ... existing code ...\") for unchanged sections between changes\n- Include sufficient context to unambiguously locate each change\n- Preserve the exact order of changes as they appear in the original file\n- Make all changes in a single pass through the file\n- Strictly follow the change explanation format and update format instructions, as with any other code block\n- Expand the change explanation as needed in order to properly describe *all* the changes, and correctly locate them in the original file\n\n❌ INCORRECT - Multiple code blocks for the same file:\n\n>>>\n\n**Updating ` + \"`main.go`\" + `**\nType: add\nSummary: Add new ` + \"`NewFeature`\" + ` function \nContext: Located between ` + \"`foo`\" + ` and ` + \"`bar`\" + ` functions\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\n// ... existing code ...\n\nfunc foo() {\n  // ... existing code ...\n}\n\nfunc NewFeature() {\n  doSomething()\n}\n\nfunc bar() {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n**Updating ` + \"`main.go`\" + `**\nType: add  \nSummary: Add new ` + \"`AnotherFeature`\" + ` function\nContext: Located between ` + \"`help`\" + ` function and ` + \"`finalizer`\" + ` function\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\n// ... existing code ...\n\nfunc help() {\n  // ... existing code ...\n}\n\nfunc AnotherFeature() {\n  doSomethingElse()\n}\n\nfunc finalizer() {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n<<<\n\n✅ CORRECT - Single code block with multiple changes:\n\n>>>\n\n**Updating ` + \"`main.go`\" + `**\nType: add\nSummary: Add functions ` + \"`NewFeature`\" + ` and ` + \"`AnotherFeature`\" + `\nContext: ` + \"`NewFeature`\" + ` between ` + \"`foo`\" + ` and ` + \"`bar`\" + ` functions, ` + \"`AnotherFeature`\" + ` between ` + \"`help`\" + ` and ` + \"`finalizer`\" + ` functions\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\n// ... existing code ...\n\nfunc foo() {\n  // ... existing code ...\n}\n\nfunc NewFeature() {\n  doSomething()\n}\n\nfunc bar() {\n  // ... existing code ...\n}\n\n// ... existing code ...\n\nfunc help() {\n  // ... existing code ...\n}\n\nfunc AnotherFeature() {\n  doSomethingElse()\n}\n\nfunc finalizer() {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n<<<\n\n## Placeholders\n\nAs 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.\n\n## Explanatory code\n\nIf 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.\n\n## Do not remove code unrelated to the specific task at hand\n\nDO 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.\n\n## Do the task yourself and don't give up\n\n**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.\n\n**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.\n\n**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. \n\n## Working on tasks\n\n` + CurrentSubtaskPrompt + `\n\nYou 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.\n\nIf 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.\n\nAfter implementing a task or task with code, you MUST *explicitly mark it done*. \n\n` + MarkSubtaskDonePrompt + `\n\nDo 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.\n\nYou 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.\n    \nIf 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.\n\nNever 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.\n\nNEVER repeat any part of your previous response. Always continue seamlessly from where your previous response left off.\n\nDO 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.\n\n## Consider the latest context\n\nIf 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.\"\n\n` + SharedPlanningImplementationPrompt\n\n\tprompt += `\n[END OF YOUR INSTRUCTIONS]\n`\n\treturn prompt\n}\n\nconst CurrentSubtaskPrompt = `\nYou 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 <PlandexFinish/>, and then end your response.\n\nBefore 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.\n\n`\n\nconst MarkSubtaskDonePrompt = `\n## Marking Tasks as Done Or In Progress\n\nAt the end of your response, you ABSOLUTELY MUST either mark the task as 'done' or mark it as 'in progress', and then output <PlandexFinish/> and immediately end the response.\n\n### To mark a task done:\n\n1. Explictly state: \"**[task name]** has been completed\". For example, \"**Adding the update function** has been completed.\" \n2. Output <PlandexFinish/>\n3. Immediately end the response.\n\nExample:\n\n**Adding the update function** has been completed.\n<PlandexFinish/>\n\nIt'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 <PlandexFinish/> and immediately end the response.\n\n### To mark a task as in progress:\n\n1. 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.\"\n2. Output <PlandexFinish/>\n3. Immediately end the response.\n\n### Important\n\nDo 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.\n\n## .gitignore files\n\nIf 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.\n\nIf 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.\n`\n\n// 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.\n"
  },
  {
    "path": "app/server/model/prompts/missing_file.go",
    "content": "package prompts\n\nimport \"fmt\"\n\nfunc GetSkipMissingFilePrompt(path string) string {\n\treturn 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)\n}\n\nfunc GetMissingFileContinueGeneratingPrompt(path string) string {\n\treturn 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 <PlandexBlock> 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 </PlandexBlock> 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 <PlandexBlock> 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)\n}\n"
  },
  {
    "path": "app/server/model/prompts/name.go",
    "content": "package prompts\n\nimport (\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/sashabaranov/go-openai/jsonschema\"\n)\n\nconst 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 <planName> tag. The <planName> 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.\n\nExample response:\n<planName>add-auth-system</planName>`\n\nconst 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.\"\n\nvar PlanNameFn = openai.FunctionDefinition{\n\tName: \"namePlan\",\n\tParameters: &jsonschema.Definition{\n\t\tType: jsonschema.Object,\n\t\tProperties: map[string]jsonschema.Definition{\n\t\t\t\"planName\": {\n\t\t\t\tType: jsonschema.String,\n\t\t\t},\n\t\t},\n\t\tRequired: []string{\"planName\"},\n\t},\n}\n\ntype PlanNameRes struct {\n\tPlanName string `json:\"planName\"`\n}\n\nfunc GetPlanNamePrompt(sysPrompt, text string) string {\n\treturn sysPrompt + \"\\n\\nContent:\\n\" + text\n}\n\ntype PipedDataNameRes struct {\n\tName string `json:\"name\"`\n}\n\nconst 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 <name> tag. The <name> 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.\n\nExample response:\n<name>git-status</name>`\n\nconst 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.\"\n\nvar PipedDataNameFn = openai.FunctionDefinition{\n\tName: \"namePipedData\",\n\tParameters: &jsonschema.Definition{\n\t\tType: jsonschema.Object,\n\t\tProperties: map[string]jsonschema.Definition{\n\t\t\t\"name\": {\n\t\t\t\tType: jsonschema.String,\n\t\t\t},\n\t\t},\n\t\tRequired: []string{\"name\"},\n\t},\n}\n\nfunc GetPipedDataNamePrompt(sysPrompt, text string) string {\n\treturn SysPipedDataName + \"\\n\\nContent:\\n\" + text\n}\n\ntype NoteNameRes struct {\n\tName string `json:\"name\"`\n}\n\nconst 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 <name> tag. The <name> 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.\n\nExample response:\n<name>meeting-notes</name>`\n\nconst 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.\"\n\nvar NoteNameFn = openai.FunctionDefinition{\n\tName: \"nameNote\",\n\tParameters: &jsonschema.Definition{\n\t\tType: jsonschema.Object,\n\t\tProperties: map[string]jsonschema.Definition{\n\t\t\t\"name\": {\n\t\t\t\tType: jsonschema.String,\n\t\t\t},\n\t\t},\n\t\tRequired: []string{\"name\"},\n\t},\n}\n\nfunc GetNoteNamePrompt(sysPrompt, text string) string {\n\treturn sysPrompt + \"\\n\\nNote:\\n\" + text\n}\n"
  },
  {
    "path": "app/server/model/prompts/planning.go",
    "content": "package prompts\n\ntype CreatePromptParams struct {\n\tAutoContext       bool\n\tExecMode          bool\n\tIsUserDebug       bool\n\tIsApplyDebug      bool\n\tIsGitRepo         bool\n\tContextTokenLimit int\n}\n\nfunc GetPlanningPrompt(params CreatePromptParams) string {\n\tprompt := Identity + ` A plan is a set of files with an attached context.\n  \n  [YOUR INSTRUCTIONS:]\n\t\n  First, decide if the user has a task for you.\n  \n  *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.\n  \n  *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:\n  \n  `\n\n\tif params.AutoContext {\n\t\tprompt += `    \n    1. Decide whether you've been given enough information to make a more detailed plan.\n      - 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. \n      a. If you really don't have enough information from the user's prompt to make a plan:\n        - Explicitly say \"I need more information to make a plan for this task.\"\n        - Ask the user for more information and stop there.\n    `\n\t} else {\n\t\tprompt += `\n    1. Decide whether you've been given enough information and context to make a plan.\n      - 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. \n      a. If you really don't have enough information or context to make a plan:\n        - Explicitly say \"I need more information or context to make a plan for this task.\"\n        - Ask the user for more information or context and stop there.\n\t\t`\n\t}\n\n\tif params.ExecMode {\n\t\tprompt += `\n    2a. Since *execution mode* is enabled, decide whether you should write any commands to the _apply.sh script in a '### Commands' section.\n      - 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.\n      - 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.\n      - 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.\n      - 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.\n    \n    2b.`\n\t} else {\n\t\tprompt += `2.`\n\t}\n\n\tprompt += `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. \n  \n  Example:\n\n\t\t\t\t---\n`\n\n\tif params.ExecMode {\n\t\tprompt += `\n        ### Commands\n\n        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.\n`\n\t}\n\n\tprompt += `\n        ### Tasks\n\n        1. Create a new file called 'game_logic.h'\n\t\t\t\t\t- This file will be used to define the 'updateGameLogic' function\n\t\t\t\t\t- This file will be created in the 'src' directory\n        Uses: ` + \"`src/game_logic.h`\" + `\n\n        2. Add the necessary code to the 'game_logic.h' file to define the 'updateGameLogic' function\n\t\t\t\t\t- This file will be created in the 'src' directory\n        Uses: ` + \"`src/game_logic.h`\" + `\n\n        3. Create a new file called 'game_logic.c'\n        Uses: ` + \"`src/game_logic.c`\" + `\n        \n        4. Add the necessary code to the 'game_logic.c' file to implement the 'updateGameLogic' function\n        Uses: ` + \"`src/game_logic.c`\" + `\n        \n        5. Update the 'main.c' file to call the 'updateGameLogic' function\n        Uses: ` + \"`src/main.c`\" + `\n        `\n\tif params.ExecMode {\n\t\tprompt += `\n    6. 🚀 Create the _apply.sh file to install dependencies, then build and run the project\n    Uses: ` + \"`_apply.sh`\" + `\n    `\n\t}\n\n\tprompt += `\n        <PlandexFinish/>\n\t\t\t\t---\n\n        - After you have broken a task up in to multiple subtasks and output a '### Tasks' section, you *ABSOLUTELY MUST ALWAYS* output a <PlandexFinish/> tag and then end the response. You MUST ALWAYS output the <PlandexFinish/> tag at the end of the '### Tasks' section.\n\n        - Output a <PlandexFinish/> tag after the '### Tasks' section. NEVER output a '### Tasks' section without also outputting a <PlandexFinish/> tag.\n\n        ` + ReviseSubtasksPrompt + `\n\n        - 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.\n\n\t\t\t\t- 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.\n\n\t\t\t\t- ` + CombineSubtasksPrompt + `\n\n        - 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.\n\n        - 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.\n\n        - 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.\n\n        - Add a line break after between each subtask so the list of subtasks is easy to read.\n\n        - 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.\n\n\t\t\t\t- 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*.\n\n\t\t\t\t- 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.\n\n        - 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.\n\n        - 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.\n\n    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.\n    `\n\n\tif params.IsGitRepo {\n\t\tprompt += `\n    This project is a git repository. When creating a new project from scratch, include a .gitignore file in the root of the project.\n    \n    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.\n\n    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.\n\n    Apart from sensitive files, ensure build directories, cache directories, and other temporary/ephemeral files and directories are included in the .gitignore file.\n    `\n\n\t\tif params.ExecMode {\n\t\t\tprompt += `\n      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.\n      `\n\t\t}\n\t} else {\n\t\tprompt += `\n    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.\n\n    .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.\n    \n    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.\n\n    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.\n\n    Apart from sensitive files, ensure build directories, cache directories, and other temporary/ephemeral files and directories are included in the .plandexignore file.\n    `\n\n\t\tif params.ExecMode {\n\t\t\tprompt += `\n      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.\n      `\n\t\t}\n\t}\n\n\tif params.AutoContext {\n\t\tprompt += `        \n\t\t\t\t\tSince 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.\n          \n          IMPORTANT NOTE ON CODEBASE MAPS:\nFor 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.\n    `\n\t}\n\n\tprompt += getUsesPrompt(params)\n\n\tprompt += `\n## Responding to user questions\n\nIf 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.\n`\n\n\tprompt += FileOpsPlanningPrompt\n\n\tprompt += SharedPlanningImplementationPrompt\n\n\tprompt += `\nIf 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.\n\nIMPORTANT: 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.\n\nDo not attempt to write any code or show any implementation details at this stage.\n\n[END OF YOUR INSTRUCTIONS]\n`\n\n\treturn prompt\n}\n\nfunc getUsesPrompt(params CreatePromptParams) string {\n\ts := `\n- 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`.\" + `\n\nExample:\n`\n\n\tif params.ExecMode {\n\t\ts += `\n### Commands\n\nThe _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.\n  `\n\t}\n\n\ts += `\n---\n### Tasks\n\n1. Add the necessary code to the 'game_logic.h' and 'game_logic.c' files to define the 'updateGameLogic' function\nUses: ` + \"`src/game_logic.h`\" + `, ` + \"`src/game_logic.c`\" + `\n\n2. Update the 'main.c' file to call the 'updateGameLogic' function\nUses: ` + \"`src/main.c`\" + `\n\n<PlandexFinish/>\n---\n\nBe 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.\n\nIf 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.\n\nYou MUST USE 'Uses:' *exactly* for this purpose. DO NOT use 'Files:' or 'Files needed:' or anything else. ONLY use 'Uses:' for this purpose.\n\nALWAYS place 'Uses:' at the *end* of each task description.\n\nIf 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.\n\n'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.\n\n- 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.\n`\n\n\treturn s\n}\n\nvar UsesPromptNumTokens int\n\nconst SharedPlanningImplementationPrompt = `\nAs 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.\n\n## Code Organization\nWhen implementing features that require new files, follow these guidelines for code organization:\n- Prefer a larger number of *smaller*, focused files over large monolithic files\n- Break up complex functionality into separate files based on responsibility\n- Keep each file focused on a specific concern or piece of functionality\n- Follow the best practices and conventions of the language/framework\nThis is about the end result - how the code will be organized in the filesystem. The goal is maintainable, well-structured code.\n\n## Task Planning\nWhen planning how to implement changes:\n- Group related file changes into cohesive subtasks \n- 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\n- The key is that all changes in a subtask should be part of implementing one cohesive piece of functionality\nThis is about the process - how to efficiently break down the work into manageable steps.\n\nFor 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.\n\n## Focus on what the user has asked for and don't add extra code or features\n\nDon'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.\n\n## Things you can and can't do\n\nYou 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.\n\nImages may be added to the context, but you are not able to create or update images.\n\nDo 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.\n\n## Use open source libraries when appropriate\n\nWhen 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. \n\nConsider 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. \n\nTry 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. \n\nDon'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.\n\nIf the user asks you to use a specific library, then use that library.\n\nIf a subtask is small and the implementation is trivial, don't use a library. Use libraries when they can significantly simplify a subtask.\n\nDo 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.\n\n## Consider the latest context\n\nBe 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.\n\nAlways 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.\n\nSimilarly, if you have made updates to any files, you MUST always work from the *latest version* of the files when continuing the plan.\n\n`\nconst ReviseSubtasksPrompt = `\n- 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.\n\n- 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.\n\nIf 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.\n\nYou MUST NOT UNDER ANY CIRCUMSTANCES remove a task using a '### Remove Tasks' section if it has already been finished.\n\nThe '### Remove Tasks' section must list a single task per line in exactly this format:\n\n### Remove Tasks\n- Task name\n- Task name\n- Task name\n\nExample:\n\n### Remove Tasks\n- Update the user interface\n- Add a new feature\n- Remove a deprecated function\n\nDo NOT use any other format for the '### Remove Tasks' section. Do NOT use a numbered list. Identify tasks *only* by exact name matching.\n\n`\n"
  },
  {
    "path": "app/server/model/prompts/shared.go",
    "content": "package prompts\n\nconst 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.\"\n"
  },
  {
    "path": "app/server/model/prompts/summary.go",
    "content": "package prompts\n\nconst PlanSummary = `\nYou 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.\n\nIf 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.\n\nBased on the existing summary and the conversation so far, make a summary of the current state of the plan.\n\nDo not include any heading or title for the summary. Just start with the summary of the plan.\n\n- 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.\n\n- Next, summarize what has been discussed and accomplished in the conversation so far. This should include:\n  - Key decisions that have been made\n  - Major changes or updates to the plan\n  - Any significant challenges or considerations that have been identified\n  - Important requirements or constraints that have been established\n\n- Last, summarize what has been done in the latest messages and any next steps or action items that have been discussed.\n\n- Do not include code in the summary. Explain in words what has been done and what needs to be done.\n\n- 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.\n\nOutput only the summary of the current state of the plan and nothing else.\n`\n"
  },
  {
    "path": "app/server/model/prompts/update_format.go",
    "content": "package prompts\n\nconst UpdateFormatPrompt = `\nYou 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.\n\nDO 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).\n\nWhen 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:\n\n- example.js:\n<PlandexBlock lang=\"javascript\" path=\"example.js\">\n// ... existing code ...\n\nfunction fooBar() {\n  // ... existing code ...\n\n  updateState();\n}\n\n// ... existing code ...\n</PlandexBlock>\n\nALWAYS 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:\n\n- example.js:\n<PlandexBlock lang=\"javascript\" path=\"example.js\">\n// ... existing code ...\n\nclass FooBar {\n  // ... existing code ...\n\n  updateState() {\n    doSomething();\n  }\n}\n</PlandexBlock>\n\nDO 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.\n\nBelow, if the 'update' function is being added to an existing class, you MUST NOT leave out the code structure like this:\n\n- example.js:\n<PlandexBlock lang=\"javascript\" path=\"example.js\">\n// ... existing code ...\n\n  update() {\n    doSomething();\n  }\n\n// ... existing code ...\n</PlandexBlock>\n\nYou ABSOLUTELY MUST include the full code structure like this:\n\n- example.js:\n<PlandexBlock lang=\"javascript\" path=\"example.js\">\n// ... existing code ...\n\nclass FooBar {\n  // ... existing code ...\n\n  update() {\n    doSomething();\n  }\n}\n</PlandexBlock>\n\nALWAYS 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.\n\nIf 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.\n\nFor example, in a JSON file:\n\n- config.json:\n<PlandexBlock lang=\"json\" path=\"config.json\">\n{\n  // ... existing code ...\n\n  \"foo\": \"bar\",\n\n  \"baz\": {\n    // ... existing code ...\n\n    \"arr\": [\n      // ... existing code ...\n      \"val\"\n    ]\n  },\n\n  // ... existing code ...\n}\n</PlandexBlock>\n\nYou 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.\n\nEven 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.\n\nIf you have a JSON file like:\n\n- package.json:\n<PlandexBlock lang=\"json\" path=\"package.json\">\n{                                                                         \n  \"name\": \"vscode-plandex\",                                  \n  \"contributes\": {                                                        \n    \"languages\": [{                                                       \n      \"id\": \"plandex\",\n    }],\n    \"commands\": [\n      {\n        \"command\": \"plandex.tellPlandex\",\n      }\n    ],\n    \"keybindings\": [{\n      \"command\": \"plandex.showFilePicker\",\n    }]\n  },\n  \"scripts\": {\n    \"compile\": \"webpack\",\n  },\n}\n</PlandexBlock>\n\nAnd you are adding a new key to the 'contributes' object, you MUST NOT output a code block like:\n\n- package.json:\n<PlandexBlock lang=\"json\" path=\"package.json\">\n{\n  \"contributes\": {\n    \"languages\": [{\n      \"id\": \"plandex\",\n    }],\n    \"grammars\": [\n      {\n        \"language\": \"plandex\",\n      }\n    ]\n  }\n}\n</PlandexBlock>\n\nThe 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.\n\nYou also MUST NOT output a code block like:\n\n- package.json:\n<PlandexBlock lang=\"json\" path=\"package.json\">\n{\n  // ... existing code ...\n\n  \"contributes\":{\n    \"languages\": [{\n      \"id\": \"plandex\",\n    }],\n    \"grammars\": [\n      {\n        \"language\": \"plandex\",\n      }\n    ]\n  }\n}\n</PlandexBlock>\n\nThis 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.\n                 \nHere's the CORRECT way to output the code block for this change:\n\n- package.json:\n<PlandexBlock lang=\"json\" path=\"package.json\">\n{\n  // ... existing code ...\n\n  \"contributes\": {\n    \"languages\": [{\n      \"id\": \"plandex\",\n    }],\n    \"grammars\": [\n      {\n        \"language\": \"plandex\",\n      }\n    ]\n\n    // ... existing code ...\n  },\n\n  // ... existing code ...\n}\n</PlandexBlock>\n\nYou 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.\n\nYou 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.\n\nYou MUST NOT use any other form of reference comment. ONLY use \"// ... existing code ...\".\n\nWhen 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.\n\nYou MUST NOT output multiple references with no changes in between them. DO NOT UNDER ANY CIRCUMSTANCES DO THIS:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunction fooBar() error {\n  log.Println(\"fooBar\")\n\n  // ... existing code ...\n\n  // ... existing code ...\n\n  return nil\n}\n</PlandexBlock>\n\nIt must instead be:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunction fooBar() error {\n  log.Println(\"fooBar\")\n\n  // ... existing code ...\n\n  return nil\n}\n</PlandexBlock>\n\nYou 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).\n\nFor example, if the original file looks like this:\n\n- array.js:\n<PlandexBlock lang=\"javascript\" path=\"array.js\">\nconst a = [\n  8,\n  9,\n  10,\n  11,\n  12,\n  13,\n  14,\n  15,\n]\n</PlandexBlock>\n\nyou MUST NOT do this:\n\n- array.js:\n<PlandexBlock lang=\"javascript\" path=\"array.js\">\nconst a = [\n  // ... existing code ...\n  1,\n  5,\t\n  7,\n  // ... existing code ...\n]\n</PlandexBlock>\n\nBecause 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. \n\nThe correct way to do it is:\n\n- array.js:\n<PlandexBlock lang=\"javascript\" path=\"array.js\">\nconst a = [\n  // ... existing code ...\n  10,\n  1,\n  5,\n  7,\n  11,\n  // ... existing code ...\n]\n</PlandexBlock>\n\nIn 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.\n\nWhen 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.\n\nYou MUST NOT do this:\n\n- array.js:\n<PlandexBlock lang=\"javascript\" path=\"array.js\">\nconst a = [\n  1,\n  5\n  // ... existing code ...\n]\n</PlandexBlock>\n\nBecause it leaves out a necessary trailing comman after the '5'. Instead do this:\n\n- array.js:\n<PlandexBlock lang=\"javascript\" path=\"array.js\">\nconst a = [\n  1,\n  5,\n  // ... existing code ...\n]\n</PlandexBlock>\n\nReference comments MUST ALWAYS be on their *OWN LINES*. You MUST NEVER include a reference comment on the same line as code.\n\nYou MUST NOT do this:\n\n- array.js:\n<PlandexBlock lang=\"javascript\" path=\"array.js\">\nconst a = [1, 2, /* ... existing code ... */, 4, 5]\n</PlandexBlock>\n\nInstead, rewrite the entire line to include the new code without using a reference comment:\n\n- array.js:\n<PlandexBlock lang=\"javascript\" path=\"array.js\">\nconst a = [1, 2, 11, 15, 14, 4, 5]\n</PlandexBlock>\n\nYou 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.\n\nIf the original file looks like this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\npackage main\n\nimport (\n  \"fmt\"\n  \"os\"\n)\n\nfunc main() {\n  fmt.Println(\"Hello, World!\")\n  exec()\n  measure()\n  os.Exit(0)\n}\n</PlandexBlock>\n\nDO NOT output superfluous newlines before or after reference comments like this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\n// ... existing code ...\n\nfunc main() {\n  fmt.Println(\"Hello, World!\")\n  prepareData()\n\n  // ... existing code ...\n\n}\n</PlandexBlock>\n\nInstead, do this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\n// ... existing code ...\n\nfunc main() {\n  fmt.Println(\"Hello, World!\")\n  prepareData()\n  // ... existing code ...\n}\n</PlandexBlock>\n\nNote 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.\n\n*\n\nIf 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.\n\n'// 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.\n    \nDo NOT use any other formatting apart from a labelled code block with the comment \"// Plandex: removed code\" to denote code removal.\n\nExample of code being removed and not replaced with new code:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunction fooBar() {\n  log.Println(\"called fooBar\")\n  // Plandex: removed code\n}\n</PlandexBlock>\n\nAs with reference comments, code removal comments MUST ALWAYS:\n  - Be on their own line. They must not be on the same line as any other code.\n  - Be on the same line as the code being removed\n  - Be surrounded by enough context so that the location and nesting depth of the code being removed is obvious and unambiguous.\n\nAlso like reference comments, you MUST NOT use multiple code removal comments in a row without any code in between them.\n\nYou MUST NOT do this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunction fooBar() {\n  // Plandex: removed code\n  // Plandex: removed code\n  exec()\n}\n</PlandexBlock>\n\nInstead, do this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunction fooBar() {\t\n  // Plandex: removed code\n  exec()\n}\n</PlandexBlock>\n\nYou MUST NOT use reference comments and removal comments together in an ambiguous way. Do NOT do this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunction fooBar() {\n  log.Println(\"called fooBar\")\n  // Plandex: removed code\n  // ... existing code ...\n}\n</PlandexBlock>\n\nAbove, 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:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunction fooBar() {\n  log.Println(\"called fooBar\")\n  // Plandex: removed code\n  exec()\n  // ... existing code ...\n}\n</PlandexBlock>\n\nBy 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.\n\n*\n\nWhen *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).\n\nIf the original file looks like this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nclass FooBar {\t\n  func baz() {\n    log.Println(\"baz\")\n  }\n\n  func bar() {\n    log.Println(\"bar\")\n    sendMessage(\"bar\")\n    reportSentMessage()\n  }\n  \n  func qux() {\n    log.Println(\"qux\")\n  }\n\n  func axon() {\n    log.Println(\"axon\")\n    escapeFromBar()\n    runAway()\n  }\n\n  func tango() {\n    log.Println(\"tango\")\n  }\n}\n</PlandexBlock>\n\nand 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:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nclass FooBar {\n  // ... existing code ...\n\n  func updatedQux() {\n    log.Println(\"updatedQux\")\n  }\n\n  // ... existing code ...\n}\n</PlandexBlock>\n\nThe 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:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nclass FooBar {\n  // ... existing code ...\n\n  func bar() {\n    // ... existing code ...\n  }\n\n  func updatedQux() {\n    log.Println(\"updatedQux\")\n  }\n\n  func axon() {\n    // ... existing code ...\n  }\n  \n  // ... existing code ...\n}\n</PlandexBlock>\n\nBy 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.\n\n*\n\nWhen 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:\n\n- config.json:\n<PlandexBlock lang=\"json\" path=\"config.json\">\n{\n  \"key1\": [{\n    \"subkey1\": \"value1\",\n    \"subkey2\": \"value2\"\n  }],\n  \"key2\": \"value2\"\n}\n</PlandexBlock>\n\nDO NOT output a code block like this:\n\n- config.json:\n<PlandexBlock lang=\"json\" path=\"config.json\">\n{\n  \"key1\": [\n    // ... existing code ...\n  ],\n  \"key2\": \"updatedValue2\"\n}\n</PlandexBlock>\n\nThe 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:\n\n- config.json:\n<PlandexBlock lang=\"json\" path=\"config.json\">\n{\n  \"key1\": [{\n    // ... existing code ...\n  }],\n  \"key2\": \"updatedValue2\"\n}\n</PlandexBlock>\n\nNote that the lines around the \"... existing code ...\" comment exactly match the lines in the original file.\n\n*\n\nWhen 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.\n\nFor example, if the original file looks like this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n  fmt.Println(\"Hello, World!\")\n}\n\nfunc fooBar() {\n  fmt.Println(\"fooBar\")\n}\n</PlandexBlock>\n\nDO NOT output a code block like this:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\nfunc main() {\n  fmt.Println(\"Hello, World!\")\n  fooBar()\n}\n</PlandexBlock>\n\nThe 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:\n\n- main.go:\n<PlandexBlock lang=\"go\" path=\"main.go\">\n// ... existing code ...\n\nfunc main() {\n  fmt.Println(\"Hello, World!\")\n  fooBar()\n}\n\n// ... existing code ...\n</PlandexBlock>\n\nNow the code before and after the change is accounted for.\n\nUnless 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.\n\n*\n\nWhen 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:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\nfunction someFunction() {\n  console.log(\"someFunction\")\n  const res = await fetch(\"https://example.com\")\n  processResponse(res)\n  return res\n}\n\nfunction processResponse(res) {\n  console.log(\"processing response\")\n  callSomeOtherFunction(res)\n  return res\n}\n\nfunction yetAnotherFunction() {\n  console.log(\"yetAnotherFunction\")\n}\n\nfunction callSomething() {\n  console.log(\"callSomething\")\n  await logSomething()\n  return \"something\"\n}\n</PlandexBlock>\n\nDO NOT output a code block like this:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\n// ... existing code ...\n\nfunction newFunction() {\n  console.log(\"newFunction\")\n  const res = await callSomething()\n  return res\n}\n\n// ... existing code ...\n</PlandexBlock>\n\nThe 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:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\n// ... existing code ...\n\nfunction processResponse(res) {\n  // ... existing code ...\n}\n\nfunction newFunction() {\n  console.log(\"newFunction\")\n  const res = await callSomething()\n  return res\n}\n\n// ... existing code ...\n</PlandexBlock>\n\nBy 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.\n\nIt'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.\n\nEven 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.\n\n*\n\nWhen 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:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\nfunc main() {\n  console.log(\"main\")\n}\n\nfunc fooBar() {\n  console.log(\"fooBar\")\n}\n\nfunc baz() {\n  console.log(\"baz\")\n}\n\nfunc qux() {\n  console.log(\"qux\")\n}\n\nfunc quix() {\n  console.log(\"quix\")\n}\n\nfunc qwoo() {\n  console.log(\"qwoo\")\n}\n\nfunc last() {\n  console.log(\"last\")\n}\n</PlandexBlock>\n\nDO NOT output a code block like this to demonstrate that new code will be inserted somewhere between the 'fooBar' and 'last' functions:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\n// ... existing code ...\n\nfunc fooBar() {\n  console.log(\"fooBar\")\n}\n\nfunc newCode() {\n  console.log(\"newCode\")\n}\n\nfunc last() {\n  console.log(\"last\")\n}\n</PlandexBlock>\n\nIf 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:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\n// ... existing code ...\n\nfunc baz() {\n  // ... existing code ...\n}\n\nfunc newCode() {\n  console.log(\"newCode\")\n}\n\nfunc qux() {\n  // ... existing code ...\n}\n\n// ... existing code ...\n</PlandexBlock>\n\nOr this to show that the new function will be inserted *somehwere* between the 'fooBar' and 'last' functions:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\n// ... existing code ...\n\nfunc fooBar() {\n  console.log(\"fooBar\")\n}\n\n// ... existing code ...\n\nfunc newCode() {\n  console.log(\"newCode\")\n}\n\n// ... existing code ...\n\nfunc last() {\n  console.log(\"last\")\n}\n</PlandexBlock>\n\nEither 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.\n\n*\n\nWhen 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.\n\nIf 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.\n\nIf the original file is:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\nfunc buck() {\n  console.log(\"buck\")\n}\n\nfunc qux() {\n  console.log(\"qux\")\n}\n\nfunc fooBar() {\n  console.log(\"fooBar\")\n}\n\nfunc baz() {\n  console.log(\"baz\")\n}\n\nfunc yup() {\n  console.log(\"yup\")\n}\n</PlandexBlock>\n\nDO NOT output a code block like this to demonstrate that new code will be inserted between the 'fooBar' and 'baz' functions:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\n// ... existing code ...\n\nfunc baz() {\n  console.log(\"baz-updated\")\n}\n\n// ... existing code ...\n\nfunc qux() {\n  console.log(\"qux-updated\")\n}\n\n// ... existing code ...\n</PlandexBlock>\n\nThe problem is that the order of the 'baz' and 'qux' functions has been changed in the proposed changes unnecessarily. Instead, do this:\n\n- main.js:\n<PlandexBlock lang=\"javascript\" path=\"main.js\">\n// ... existing code ...\n\nfunc qux() {\n  console.log(\"qux-updated\")\n}\n\n// ... existing code ...\n\nfunc baz() {\n  console.log(\"baz-updated\")\n}\n\n// ... existing code ...\n</PlandexBlock>\n\nNow the order of the 'baz' and 'qux' functions is preserved exactly as it is in the original file.\n\n*\n\nWhen 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 '<!-- ... existing code ... -->'. 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 ... */}'.\n\nNow the order of the 'baz' and 'qux' functions is preserved exactly as it is in the original file.\n\n*\n\nWhen 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 '<!-- ... existing code ... -->'. 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 ... */}'.\n`\n\nconst UpdateFormatAdditionalExamples = `\nHere are some important examples of INCORRECT vs CORRECT file updates:\n\nExample 1 - Adding a new route:\n\n❌ INCORRECT - Replacing instead of inserting:\n- src/main.go:\n<PlandexBlock lang=\"go\" path=\"src/main.go\">\n// ... existing code ...\n\nr.HandleFunc(prefix+\"/api/users\", handlers.ListUsersHandler).Methods(\"GET\")\n\nr.HandleFunc(prefix+\"/api/config\", handlers.GetConfigHandler).Methods(\"GET\")\n\n// ... existing code ...\n</PlandexBlock>\nThis is wrong because it doesn't show enough context to know what surrounding routes were preserved.\n\n✅ CORRECT - Proper insertion with context:\n- src/main.go:\n<PlandexBlock lang=\"go\" path=\"src/main.go\">\n// ... existing code ...\n\nr.HandleFunc(prefix+\"/api/users\", handlers.ListUsersHandler).Methods(\"GET\")\nr.HandleFunc(prefix+\"/api/teams\", handlers.ListTeamsHandler).Methods(\"GET\")\n\nr.HandleFunc(prefix+\"/api/config\", handlers.GetConfigHandler).Methods(\"GET\")\n\nr.HandleFunc(prefix+\"/api/settings\", handlers.GetSettingsHandler).Methods(\"GET\")\nr.HandleFunc(prefix+\"/api/status\", handlers.GetStatusHandler).Methods(\"GET\")\n\n// ... existing code ...\n</PlandexBlock>\n\nExample 2 - Adding a method to a class:\n\n❌ INCORRECT - Ambiguous insertion:\n- src/main.go:\n<PlandexBlock lang=\"go\" path=\"src/main.go\">\nclass UserService {\n  // ... existing code ...\n  \n  async createUser(data) {\n    // new method\n  }\n  \n  // ... existing code ...\n}\n</PlandexBlock>\nThis is wrong because it doesn't show where exactly the new method should go.\n\n✅ CORRECT - Clear insertion point:\n- src/main.go:\n<PlandexBlock lang=\"go\" path=\"src/main.go\">\nclass UserService {\n  // ... existing code ...\n  \n  async getUser(id) {\n    return await this.db.users.findOne(id)\n  }\n  \n  async createUser(data) {\n    return await this.db.users.create(data)\n  }\n  \n  async updateUser(id, data) {\n    return await this.db.users.update(id, data)\n  }\n  \n  // ... existing code ...\n}\n</PlandexBlock>\n\nExample 3 - Adding a configuration section:\n\n❌ INCORRECT - Lost context:\n- src/config.json:\n<PlandexBlock lang=\"json\" path=\"src/config.json\">\n{\n  \"database\": {\n    \"host\": \"localhost\",\n    \"port\": 5432\n  },\n  \"newFeature\": {\n    \"enabled\": true,\n    \"timeout\": 30\n  }\n}\n</PlandexBlock>\nThis is wrong because it dropped existing configuration sections.\n\n✅ CORRECT - Preserved context:\n- src/config.json:\n<PlandexBlock lang=\"json\" path=\"src/config.json\">\n{\n  // ... existing code ...\n  \n  \"database\": {\n    \"host\": \"localhost\",\n    \"port\": 5432,\n    \"username\": \"admin\"\n  },\n  \n  \"newFeature\": {\n    \"enabled\": true,\n    \"timeout\": 30\n  },\n  \n  \"logging\": {\n    \"level\": \"info\",\n    \"file\": \"app.log\"\n  }\n  \n  // ... existing code ...\n}\n</PlandexBlock>\n\nKey principles demonstrated in these examples:\n1. Always show the surrounding context that will be preserved\n2. Make insertion points unambiguous by showing adjacent code\n3. Never remove existing functionality unless explicitly instructed to do so\n4. Use \"... existing code ...\" comments properly to indicate preserved sections\n5. Show enough context to understand the code structure\n`\n"
  },
  {
    "path": "app/server/model/prompts/user_prompt.go",
    "content": "package prompts\n\nimport (\n\t\"fmt\"\n\tshared \"plandex-shared\"\n\t\"time\"\n)\n\nconst SharedPromptWrapperFormatStr = \"# The user's latest prompt:\\n```\\n%s\\n```\\n\\n\" + `Please respond according to the 'Your instructions' section above.\n\nDo 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.\n\nIf a high quality, well-respected open source library is available that can simplify a task or subtask, use it.\n\nThe 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.\n\nDo 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.\n\nUser's operating system details:\n%s\n\n---\n%s\n---\n`\n\nfunc GetContextLoadingPromptWrapperFormatStr(params CreatePromptParams) string {\n\ts := SharedPromptWrapperFormatStr + `\n\t` + GetArchitectContextSummary(params.ContextTokenLimit)\n\n\treturn s\n}\n\nfunc GetPlanningPromptWrapperFormatStr(params CreatePromptParams) string {\n\ts := SharedPromptWrapperFormatStr + `\n\n` + GetPlanningFlowControl(params) + `\n\nDo 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.\n\n` + ReviseSubtasksPrompt + `\n\n` + CombineSubtasksPrompt + `\n\nAt the end of the '### Tasks' section, you ABSOLUTELY MUST ALWAYS include a <PlandexFinish/> tag, then end the response.\n\nExample:\n`\n\n\tif params.ExecMode {\n\t\ts += `\n### Commands\n\nThe _apply.sh script is empty. I'll create it with commands to compile the project and run the new test with cargo.\n`\n\t}\n\n\ts += `\n### Tasks\n\n1. Create a new file called 'src/main.rs' with a 'main' function that returns 'Hello, world!'\nUses: ` + \"`src/main.rs`\" + `\n\n2. Write a basic test for the 'main' function\nUses: ` + \"`src/main.rs`\"\n\n\tif params.ExecMode {\n\t\ts += `\n3. 🚀 Run the new test with cargo\nUses: ` + \"`_apply.sh`\" + `\n\t`\n\t}\n\n\ts += `\n<PlandexFinish/>\n\nAfter you have broken a task up in to multiple subtasks and output a '### Tasks' section, you *ABSOLUTELY MUST ALWAYS* output a <PlandexFinish/> tag and then end the response. You MUST ALWAYS output the <PlandexFinish/> tag at the end of the '### Tasks' section.\n\nOutput a <PlandexFinish/> tag after the '### Tasks' section. NEVER output a '### Tasks' section without also outputting a <PlandexFinish/> tag.\n\nUse 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.\n\nModular 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.\n\nOngoing 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.\n\nForward-Thinking Design: Organize code to accommodate growth and evolution, following language conventions while keeping files small, focused, and maintainable.\n\nIMPORTANT: 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.\n\nDo not attempt to write any code or show any implementation details at this stage.\n\nThe 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.\n`\n\n\tif params.IsUserDebug {\n\t\ts += UserPlanningDebugPrompt\n\t} else if params.IsApplyDebug {\n\t\ts += ApplyPlanningDebugPrompt\n\t} else if !params.ExecMode {\n\t\ts += NoApplyScriptPlanningPrompt\n\t}\n\n\treturn s\n}\n\nfunc GetImplementationPromptWrapperFormatStr(params CreatePromptParams) string {\n\ts := SharedPromptWrapperFormatStr + `\n\nIf 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.**\n\nYou 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. \n\nAlways use an opening <PlandexBlock> tag to start a code block and a closing </PlandexBlock> tag to end a code block.\n\nThe <PlandexBlock> 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 <PlandexBlock> tag.\n\nThe <PlandexBlock> 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.\n\nWhen *updating an existing file*, you MUST follow the instructions you've been given on how to update code in code blocks:\n\n\t- 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.\n\n\t- Include enough code from the original file to precisely and unambiguously locate where the changes should be applied and their level of nesting.\n\n\t- Match the indentation of the original file exactly.\n\n\t- Do NOT include line numbers in the <PlandexBlock> 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 <PlandexBlock> tag.\n\n\t- Do NOT output multiple references with no changes in between them.\n\n\t- Do NOT add superfluous newlines around reference comments.\n\n\t- 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.\n\n\t- 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.\n\n\t- 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.\n\n\t- 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.\n\n\t- Never remove existing functionality unless explicitly instructed to do so.\n\n\t- DO NOT remove comments, logging statements, code that is commented out, or ANY code that is not related to the specific task at hand.\n\n\t- Do NOT escape newlines within the <PlandexBlock> 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.\n\t\n\t- Strive to make changes that are minimally intrusive and do not change the existing code beyond what is necessary to complete the task.\n\n\t- Show enough surrounding context to understand the code structure.\n\n\t- When outputting the explanation, do *NOT* insert code between two code structures that aren't *immediately adjacent* in the original file.\n\n  -\tEvery 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.\n\n\t- 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)'\n\n\t- CRITICAL: When writing the Context field in an Action Explanation:\n\t\t- The symbols/structures mentioned MUST be code that is NOT being changed\n\t\t- These symbols serve as ANCHORS to precisely locate where the change should be applied\n\t\t- Every symbol/structure mentioned in the Context MUST appear in the code block\n\t\t- These anchors MUST be immediately adjacent to where the change occurs\n\t\t- Do NOT use distant symbols with other code between them and the change\n\t\t- All symbols must be surrounded with backticks\n\t\t- The code block MUST include these anchors to unambiguously locate the change\n\t\t- If you mention \"Located between ` + \"`functionA`\" + \"` and `\" + \"`functionB`\" + `, both functions MUST appear in your code block\n\n\t\tFAILURE TO INCLUDE THE CONTEXT SYMBOLS IN THE CODE BLOCK MAKES CHANGES IMPOSSIBLE TO APPLY CORRECTLY AND IS A CRITICAL ERROR.\n\nWhen *creating a new file*, follow the instructions in the \"### Action Explanation Format\" section for creating a new file.\n \n  - The Type field MUST be exactly 'new file'.\n  - The Summary field MUST briefly describe the new file and its purpose.\n\t- The file path MUST be included in the code block label.\n\t- The code itself MUST be written within a <PlandexBlock> tag.\n\t- The <PlandexBlock> tag MUST include both a 'lang' attribute and a 'path' attribute as described in the instructions above. It must not include any other attributes.\n\t- The <PlandexBlock> 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 <PlandexBlock> tag.\n\t- The code block MUST include the *entire file* to be created. Do not omit any code from the file.\n\t- Do NOT use placeholder code or comments like '// implement authentication here' to indicate that the file is incomplete. Implement *all* functionality.\n\t- Do NOT use reference comments ('// ... existing code ...'). Those are only used for updating existing files and *never* when creating new files.\n\t- Include the *entire file* in the code block.\n\n\nIf 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:\n\n\t- Include all changes in a single code block that follows the file's structure\n\t- Use \"... existing code ...\" comments between changes\n\t- Show enough context around each change for unambiguous location\n\t- Maintain the original file's order of elements\n\t- Only reproduce parts of the file necessary to show structure and locate changes\n\t- Make all changes in a single pass from top to bottom of the file\n\n\tWhen writing the explanation for multiple changes that will be included in a single code block, list each change independently like this:\n\n\t**Updating  + \"server/handlers/auth.go\" + **\n\tChange 1. \n\t\tType: remove\n\t\tSummary: Remove unused  + \"validateLegacyTokens\" +  function and its helper  +    \"checkTokenFormat\" + . Removes  + \"validateLegacyTokens and checkTokenFormat\" +  functions (original file lines 25-85).\n\t\tContext: Located between  + \"parseAuthHeader\" +  and  + \"validateJWT\" +  functions\n\tChange 2.\n\t\tType: append\n\t\tSummary: Append just-removed + \"checkTokenFormat\" + function to the end of the file\"\t\n\n\nOnly 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.\n\nDo 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**.\n\nDo NOT repeat any part of your previous response. Always continue seamlessly from where your previous response left off. \n\nALWAYS 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.\n\n` + CurrentSubtaskPrompt + `\n\n` + MarkSubtaskDonePrompt + `\n\n` + FileOpsImplementationPromptSummary\n\n\tfile := \".gitignore\"\n\tif !params.IsGitRepo {\n\t\tfile = \".plandexignore\"\n\t}\n\n\ts += fmt.Sprintf(`\n- Create or update the %s file if necessary.\n- If you write commands to _apply.sh, consider if output should be added to %s.\n`, file, file)\n\n\ts += `\n## Is the task done or in progress?\n\nRemember, you must follow these instructions on marking tasks as done or in progress:\n\n- When a subtask is *completed*, you *must* either: \n\n1. Mark it as 'done' in the format described in the 'Marking Tasks as Done Or In Progress' section.\n2. Mark it as 'in progress' by explaining that the task is not yet complete and will be continued in the next response.\n\nRemember, 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.\n\n## The Most Critical Factor\n\nRemember, 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.\n\nDefinitions 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.\n\nThis 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.\n\nEven 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.\n\nIt'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.\n\nThis disambiguation technique is the *most important* part of correctly implementing a plan.\n`\n\n\treturn s\n}\n\ntype UserPromptParams struct {\n\tCreatePromptParams\n\tPrompt                     string\n\tOsDetails                  string\n\tCurrentStage               shared.CurrentStage\n\tUnfinishedSubtaskReasoning string\n}\n\nfunc GetWrappedPrompt(params UserPromptParams) string {\n\tcurrentStage := params.CurrentStage\n\n\tprompt := params.Prompt\n\tosDetails := params.OsDetails\n\n\tvar promptWrapperFormatStr string\n\tif currentStage.TellStage == shared.TellStagePlanning {\n\t\tif currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\tpromptWrapperFormatStr = GetContextLoadingPromptWrapperFormatStr(params.CreatePromptParams)\n\t\t} else {\n\t\t\tpromptWrapperFormatStr = GetPlanningPromptWrapperFormatStr(params.CreatePromptParams)\n\t\t}\n\t} else {\n\t\tpromptWrapperFormatStr = GetImplementationPromptWrapperFormatStr(params.CreatePromptParams)\n\t}\n\n\t// If we're in the context loading stage, we don't need to include the apply script summary\n\tvar applyScriptSummary string\n\tif currentStage.TellStage == shared.TellStagePlanning && currentStage.PlanningPhase == shared.PlanningPhaseTasks {\n\t\tapplyScriptSummary = ApplyScriptPlanningPromptSummary\n\t} else if currentStage.TellStage == shared.TellStageImplementation {\n\t\tapplyScriptSummary = ApplyScriptImplementationPromptSummary\n\t}\n\n\tts := time.Now().Format(time.RFC3339)\n\n\ts := \"The current stage is: \"\n\tif currentStage.TellStage == shared.TellStagePlanning {\n\t\tif currentStage.PlanningPhase == shared.PlanningPhaseContext {\n\t\t\ts += \"CONTEXT\"\n\t\t} else {\n\t\t\ts += \"PLANNING\"\n\t\t}\n\t} else if currentStage.TellStage == shared.TellStageImplementation {\n\t\ts += \"IMPLEMENTATION\"\n\t}\n\ts += \"\\n\\n\"\n\ts += fmt.Sprintf(promptWrapperFormatStr, prompt, ts, osDetails, applyScriptSummary)\n\n\tif currentStage.TellStage == shared.TellStageImplementation && params.UnfinishedSubtaskReasoning != \"\" {\n\t\ts += \"\\n\\n\" + `\nThe current task was not completed in the previous response and remains unfinished. Here is the reasoning for why it was not completed:\n\n` + params.UnfinishedSubtaskReasoning + `\n\nYou 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.\n\t\t`\n\n\t}\n\n\treturn s\n}\n\nconst UserContinuePrompt = \"Continue the plan according to your instructions for the current stage. Don't repeat any part of your previous response.\"\n\nconst AutoContinuePlanningPrompt = UserContinuePrompt\n\nconst AutoContinueImplementationPrompt = `Continue the plan from where you left off in the previous response. Don't repeat any part of your previous response. \n\nContinue seamlessly from where your previous response left off. \n\nAlways name the subtask you are working on before starting it, and mark it as done before moving on to the next subtask.\n\n` + CurrentSubtaskPrompt + `\n\n` + MarkSubtaskDonePrompt + `\n\nALWAYS 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.\n\nIf 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. \n\nDo 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.`\n\nconst 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\"\n\nconst CombineSubtasksPrompt = `\n- 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.\n\n- 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.\n\n- 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.\n\n- 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.\n\nHere are examples of good and poor task division:\n\nExample 1 - Poor (tasks too small and fragmented):\n1. Create the product.js file\nUses: ` + \"`src/models/product.js`\" + `\n\n2. Add the product schema\nUses: ` + \"`src/models/product.js`\" + `\n\n3. Add the validate() method\nUses: ` + \"`src/models/product.js`\" + `\n\n4. Add the save() method\nUses: ` + \"`src/models/product.js`\" + `\n\nBetter:\n1. Create product model with core functionality\n- Create product.js with schema definition\n- Add validate() and save() methods\nUses: ` + \"`src/models/product.js`\" + `\n\nExample 2 - Poor (task too large with unrelated changes):\n1. Implement user profile features\n- Add user avatar upload\n- Add profile settings page\n- Implement friend requests\n- Add user search\n- Create notification system\nUses: ` + \"`src/components/Profile.tsx`\" + `, ` + \"`src/components/Avatar.tsx`\" + `, ` + \"`src/components/Settings.tsx`\" + `, ` + \"`src/services/friends.ts`\" + `, ` + \"`src/services/search.ts`\" + `, ` + \"`src/services/notifications.ts`\" + `\n\nBetter:\n1. Implement user avatar upload functionality\n- Add avatar component with upload UI\n- Add avatar upload service\nUses: ` + \"`src/components/Avatar.tsx`\" + `, ` + \"`src/services/avatar.ts`\" + `\n\n2. Create profile settings page\n- Add settings form components\n- Implement save/load settings\nUses: ` + \"`src/components/Settings.tsx`\" + `, ` + \"`src/services/settings.ts`\" + `\n\n3. Add friend request system\nUses: ` + \"`src/services/friends.ts`\" + `, ` + \"`src/components/Profile.tsx`\" + `\n\nExample 3 - Good (related changes properly grouped):\n1. Update error handling in authentication flow\n- Add error handling to login function\n- Add corresponding error states in auth context\n- Update error display in login form\nUses: ` + \"`src/auth/login.ts`\" + `, ` + \"`src/context/auth.tsx`\" + `, ` + \"`src/components/LoginForm.tsx`\" + `\n\nExample 4 - Good (tightly coupled file updates):\n1. Rename UserType enum to AccountType\n- Update enum definition\n- Update all imports and usages\nUses: ` + \"`src/types/user.ts`\" + `, ` + \"`src/auth/account.ts`\" + `, ` + \"`src/components/UserProfile.tsx`\" + `\n\nNotice in these examples:\n- Tasks that are too granular waste responses on tiny changes\n- Tasks that are too large mix unrelated changes and become hard to implement\n- Good tasks group related changes that make sense to implement together\n- Multiple files can be included when the changes are tightly coupled\n- Bullet points describe steps in a cohesive change, not separate features\n`\n\ntype ChatUserPromptParams struct {\n\tCreatePromptParams\n\tPrompt    string\n\tOsDetails string\n}\n\nfunc GetWrappedChatOnlyPrompt(params ChatUserPromptParams) string {\n\t// Base wrapper that's always included\n\tbaseWrapper := \"# The user's latest prompt:\\n```\\n%s\\n```\\n\\n\" + `Please respond according to the 'Your instructions' section above.\n\nThe current UTC timestamp is: %s\n\nUser's operating system details:\n%s`\n\n\t// Build additional instructions based on parameter combinations\n\tvar additionalInstructions string\n\n\t// Execution mode handling\n\tif params.ExecMode {\n\t\tadditionalInstructions += `\n*Execution mode is enabled.*\n- If you switch to tell mode, you can execute commands locally as needed\n- 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)\n- Be specific about what commands would need to be run\n- Consider build processes, testing, and deployment\n- Distinguish between file changes and execution steps`\n\t} else {\n\t\tadditionalInstructions += `\n*Execution mode is disabled.*\n- 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.\n- You can discuss build/test/deploy conceptually, but you cannot execute commands either in chat mode or in tell mode\n- Be clear when certain steps would need execution mode enabled`\n\t}\n\n\tadditionalInstructions += `\nKeep in mind:\n- Stay conversational while being technically precise\n- Reference and explain code when helpful, but don't output formal implementation blocks\n- Focus on what's specifically asked - don't suggest extra features\n- Consider existing codebase structure in your explanations\n- When discussing libraries, focus on well-maintained, widely-used options\n- If the user wants to implement changes, remind them about 'tell mode'\n- Use error handling, logging, and security best practices in your suggestions\n- Be thoughtful about code organization and structure\n- Consider implications of suggested changes on the existing codebase\n\nRemember you're in chat mode:\n- Engage in natural technical discussion about code and context\n- Help users understand their codebase and plan potential changes\n- Provide explanations and answer questions thoroughly\n- Include code snippets only when they help explain concepts\n- Help debug issues by examining and explaining code\n- Suggest approaches and discuss trade-offs\n- Help evaluate different implementation strategies\n- Consider and explain implications of different approaches\n- Stay focused on understanding and planning rather than implementation\n\nYou cannot:\n- Create or modify any files\n- Output formal implementation code blocks\n- Make plans using \"### Tasks\" sections\n- Structure responses as if implementing changes\n- Load context multiple times in consecutive responses\n- Switch to implementation mode without user request\n\nEven if a plan is in progress:\n- Stay in discussion mode, don't attempt to implement anything\n- You can discuss the current tasks and progress\n- You can provide explanations and suggestions\n- You can help debug issues or clarify approach\n- But you must not output any implementation code\n- Return to implementation only when user switches back to tell mode\n\nRemember that users often:\n- Switch between chat and tell mode during implementation\n- Use chat mode to understand before implementing\n- Need detailed technical discussion to plan effectively\n- Want to explore options before committing to changes\n- May need to debug or understand issues mid-implementation\n- You may receive a list of tasks that are in progress, including a 'current subtask'. You MUST NOT implement any tasks—only discuss them.\n`\n\n\tpromptWrapperFormatStr := baseWrapper + additionalInstructions\n\n\tts := time.Now().Format(time.RFC3339)\n\treturn fmt.Sprintf(promptWrapperFormatStr,\n\t\tparams.Prompt,\n\t\tts,\n\t\tparams.OsDetails)\n}\n\nfunc GetPlanningFlowControl(params CreatePromptParams) string {\n\ts := `\nCRITICAL PLANNING RULES:\n1. For ANY update/revision to tasks:\n`\n\n\tif params.ExecMode {\n\t\ts += `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.`\n\t}\n\n\ts += `\n   - You MUST output a new/updated ### Tasks list\n\t`\n\n\tif params.ExecMode {\n\t\ts += `\n   - 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\n\t`\n\t}\n\n\ts += `\n   - 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.\n   - You MUST follow planning phase format exactly\n\n2. Even for small changes:\n   - Create/update task list first\n   - No implementation and NO CODE until planning is complete, and you have output a '### Tasks' section and a <PlandexFinish/>\n   - All changes must be in task list\n\n3. 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 <PlandexFinish/> before you can start implementing code.\n`\n\n\treturn s\n}\n\n// func GetFollowUpRequiredPrompt(params CreatePromptParams) string {\n// \ts := `\n// [MANDATORY FOLLOW-UP FLOW]\n\n// CRITICAL FLOW CONTROL:\n// 1. You MUST FIRST respond naturally to what the user has said/asked\n// 2. Then classify the prompt as either:\n//    A. Update/revision to tasks (A1/A2/A3)\n//    B. Conversation prompt (question/comment)\n\n// 3. IF classified as A (update/revision):\n//    - You MUST create/update the task list with ### Tasks\n//    - You MUST output <PlandexFinish/> immediately after the task list\n//    - You MUST end your response immediately after <PlandexFinish/>\n//    - You ABSOLUTELY MUST NOT proceed to implementation\n//    - You MUST follow planning format exactly\n//    Even if:\n//    - The change is small\n//    - You know the exact code to write\n//    - You're continuing an existing plan\n\n// 4. IF classified as B (conversation):\n//    - Continue conversation naturally\n//    - Do not create tasks or implement code\n\n// 5. After responding and classifying, output EXACTLY ONE of these statements (naturally incorporated):\n//    A. \"I have the context I need to continue.\"\n//    B. \"I have the context I need to respond.\"\n//    C. \"I need more context to continue. <PlandexFinish/>\"\n//    D. \"I need more context to respond. <PlandexFinish/>\"\n//    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. <PlandexFinish/>\"\n//    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. <PlandexFinish/>\"\n\n// For statements A/B: You may rephrase naturally while keeping the meaning.\n// For statements C/D: MUST include exact phrase \"need more context\" and <PlandexFinish/>.\n// For statements E/F: MUST include exact phrase \"clear all context\" and <PlandexFinish/>.\n\n// 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.\n// `\n\n// \treturn s\n// }\n"
  },
  {
    "path": "app/server/model/summarize.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/model/prompts\"\n\t\"plandex-server/types\"\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype PlanSummaryParams struct {\n\tAuth                        *types.ServerAuth\n\tPlan                        *db.Plan\n\tModelStreamId               string\n\tModelPackName               string\n\tConversation                []*types.ExtendedChatMessage\n\tConversationNumTokens       int\n\tLatestConvoMessageId        string\n\tLatestConvoMessageCreatedAt time.Time\n\tNumMessages                 int\n\tSessionId                   string\n}\n\nfunc 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) {\n\tmessages := []types.ExtendedChatMessage{\n\t\t{\n\t\t\tRole: openai.ChatMessageRoleSystem,\n\t\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t\t{\n\t\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\t\tText: prompts.Identity,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, message := range params.Conversation {\n\t\tmessages = append(messages, *message)\n\t}\n\n\tmessages = append(messages, types.ExtendedChatMessage{\n\t\tRole: openai.ChatMessageRoleUser,\n\t\tContent: []types.ExtendedChatMessagePart{\n\t\t\t{\n\t\t\t\tType: openai.ChatMessagePartTypeText,\n\t\t\t\tText: prompts.PlanSummary,\n\t\t\t},\n\t\t},\n\t})\n\n\tmodelRes, err := ModelRequest(ctx, ModelRequestParams{\n\t\tClients:        clients,\n\t\tAuth:           params.Auth,\n\t\tAuthVars:       authVars,\n\t\tPlan:           params.Plan,\n\t\tModelConfig:    &config,\n\t\tPurpose:        \"Conversation summary\",\n\t\tConvoMessageId: params.LatestConvoMessageId,\n\t\tModelStreamId:  params.ModelStreamId,\n\t\tMessages:       messages,\n\t\tSessionId:      params.SessionId,\n\t\tSettings:       settings,\n\t\tOrgUserConfig:  orgUserConfig,\n\t})\n\n\tif err != nil {\n\t\treturn nil, &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    fmt.Sprintf(\"error generating plan summary: %v\", err),\n\t\t}\n\t}\n\n\tsummary := modelRes.Content\n\tif !strings.HasPrefix(summary, \"## Summary of the plan so far:\") {\n\t\tsummary = \"## Summary of the plan so far:\\n\\n\" + summary\n\t}\n\n\tvar tokens int\n\tif modelRes.Usage != nil {\n\t\ttokens = modelRes.Usage.CompletionTokens\n\t}\n\n\treturn &db.ConvoSummary{\n\t\tOrgId:                       params.Auth.OrgId,\n\t\tPlanId:                      params.Plan.Id,\n\t\tSummary:                     summary,\n\t\tTokens:                      tokens,\n\t\tLatestConvoMessageId:        params.LatestConvoMessageId,\n\t\tLatestConvoMessageCreatedAt: params.LatestConvoMessageCreatedAt,\n\t\tNumMessages:                 params.NumMessages,\n\t}, nil\n\n}\n"
  },
  {
    "path": "app/server/model/tokens.go",
    "content": "package model\n\nimport (\n\t\"plandex-server/types\"\n\tshared \"plandex-shared\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\nconst (\n\t// Per OpenAI's documentation:\n\t// Every message follows this format: {\"role\": \"role_name\", \"content\": \"content\"}\n\t// which has a 4-token overhead per message\n\tTokensPerMessage = 4\n\n\t// System, user, or assistant - each role name costs 1 token\n\tTokensPerName = 1\n\n\t// Tokens per request\n\tTokensPerRequest = 3\n\n\tTokensPerExtendedPart = 6\n)\n\nfunc GetMessagesTokenEstimate(messages ...types.ExtendedChatMessage) int {\n\ttokens := 0\n\n\tfor _, msg := range messages {\n\t\ttokens += TokensPerMessage // Base message overhead\n\t\ttokens += TokensPerName    // Role name\n\n\t\tif len(msg.Content) > 0 {\n\t\t\t// For each extended part, we need to account for the JSON structure\n\t\t\t// Each part follows format: {\"type\": \"type_value\", \"text\": \"content\"}\n\t\t\t// or {\"type\": \"type_value\", \"image_url\": {\"url\": \"url_value\"}}\n\t\t\tfor _, part := range msg.Content {\n\t\t\t\tif part.Type == openai.ChatMessagePartTypeText {\n\t\t\t\t\ttokens += TokensPerExtendedPart // Overhead for the part object structure\n\t\t\t\t\ttokens += shared.GetNumTokensEstimate(part.Text)\n\t\t\t\t}\n\n\t\t\t\t// images are handled separately\n\n\t\t\t}\n\t\t}\n\n\t}\n\n\treturn tokens\n}\n"
  },
  {
    "path": "app/server/notify/errors.go",
    "content": "package notify\n\nimport (\n\t\"log\"\n\t\"runtime/debug\"\n)\n\n// this allows Plandex Cloud to inject error monitoring\n// all non-streaming handlers are already wrapped with different logic, so this is only needed for errors in streaming handlers\n\ntype Severity int\n\nconst (\n\tSeverityInfo Severity = iota\n\tSeverityError\n)\n\nvar NotifyErrFn func(severity Severity, data ...interface{})\n\nfunc RegisterNotifyErrFn(fn func(severity Severity, data ...interface{})) {\n\tNotifyErrFn = fn\n}\n\nfunc NotifyErr(severity Severity, data ...interface{}) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"panic in NotifyErr: %v\\n%s\", r, debug.Stack())\n\t\t}\n\t}()\n\n\tif NotifyErrFn != nil {\n\t\tNotifyErrFn(severity, data...)\n\t}\n}\n"
  },
  {
    "path": "app/server/routes/routes.go",
    "content": "package routes\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"plandex-server/handlers\"\n\t\"plandex-server/hooks\"\n\n\t\"github.com/gorilla/mux\"\n)\n\ntype PlandexHandler func(w http.ResponseWriter, r *http.Request)\ntype HandlePlandex func(router *mux.Router, path string, isStreaming bool, handler PlandexHandler) *mux.Route\n\nvar HandlePlandexFn HandlePlandex\n\nfunc RegisterHandlePlandex(fn HandlePlandex) {\n\tHandlePlandexFn = fn\n}\n\nfunc EnsureHandlePlandex() {\n\tif HandlePlandexFn == nil {\n\t\tpanic(\"handlePlandexFn is not set\")\n\t}\n}\n\nfunc AddHealthRoutes(r *mux.Router) {\n\tEnsureHandlePlandex()\n\n\tHandlePlandexFn(r, \"/health\", false, func(w http.ResponseWriter, r *http.Request) {\n\t\t_, apiErr := hooks.ExecHook(hooks.HealthCheck, hooks.HookParams{})\n\t\tif apiErr != nil {\n\t\t\tlog.Printf(\"Error in health check hook: %v\\n\", apiErr)\n\t\t\thttp.Error(w, apiErr.Msg, apiErr.Status)\n\t\t\treturn\n\t\t}\n\t\tfmt.Fprint(w, \"OK\")\n\t})\n\n\tHandlePlandexFn(r, \"/version\", false, func(w http.ResponseWriter, r *http.Request) {\n\t\t// Log the host\n\t\thost := r.Host\n\t\tlog.Printf(\"Host header: %s\", host)\n\n\t\texecPath, err := os.Executable()\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"Error getting current directory: \", err)\n\t\t}\n\t\tcurrentDir := filepath.Dir(execPath)\n\n\t\t// get version from version.txt\n\t\tvar path string\n\t\tif os.Getenv(\"IS_CLOUD\") != \"\" {\n\t\t\tpath = filepath.Join(currentDir, \"..\", \"version.txt\")\n\t\t} else {\n\t\t\tpath = filepath.Join(currentDir, \"version.txt\")\n\t\t}\n\n\t\tbytes, err := os.ReadFile(path)\n\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Error getting version\", http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tfmt.Fprint(w, string(bytes))\n\t})\n}\n\nfunc AddApiRoutes(r *mux.Router) {\n\taddApiRoutes(r, \"\")\n}\n\nfunc AddApiRoutesWithPrefix(r *mux.Router, prefix string) {\n\taddApiRoutes(r, prefix)\n}\n\nfunc AddProxyableApiRoutes(r *mux.Router) {\n\taddProxyableApiRoutes(r, \"\")\n}\n\nfunc AddProxyableApiRoutesWithPrefix(r *mux.Router, prefix string) {\n\taddProxyableApiRoutes(r, prefix)\n}\n\nfunc addApiRoutes(r *mux.Router, prefix string) {\n\tEnsureHandlePlandex()\n\n\tHandlePlandexFn(r, prefix+\"/accounts/email_verifications\", false, handlers.CreateEmailVerificationHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/accounts/email_verifications/check_pin\", false, handlers.CheckEmailPinHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/accounts/sign_in_codes\", false, handlers.CreateSignInCodeHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/accounts/sign_in\", false, handlers.SignInHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/accounts/sign_out\", false, handlers.SignOutHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/accounts\", false, handlers.CreateAccountHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/orgs/session\", false, handlers.GetOrgSessionHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/orgs\", false, handlers.ListOrgsHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/orgs\", false, handlers.CreateOrgHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/users\", false, handlers.ListUsersHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/orgs/users/{userId}\", false, handlers.DeleteOrgUserHandler).Methods(\"DELETE\")\n\tHandlePlandexFn(r, prefix+\"/orgs/roles\", false, handlers.ListOrgRolesHandler).Methods(\"GET\")\n\n\tHandlePlandexFn(r, prefix+\"/invites\", false, handlers.InviteUserHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/invites/pending\", false, handlers.ListPendingInvitesHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/invites/accepted\", false, handlers.ListAcceptedInvitesHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/invites/all\", false, handlers.ListAllInvitesHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/invites/{inviteId}\", false, handlers.DeleteInviteHandler).Methods(\"DELETE\")\n\n\tHandlePlandexFn(r, prefix+\"/projects\", false, handlers.CreateProjectHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/projects\", false, handlers.ListProjectsHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/projects/{projectId}/set_plan\", false, handlers.ProjectSetPlanHandler).Methods(\"PUT\")\n\tHandlePlandexFn(r, prefix+\"/projects/{projectId}/rename\", false, handlers.RenameProjectHandler).Methods(\"PUT\")\n\n\tHandlePlandexFn(r, prefix+\"/projects/{projectId}/plans/current_branches\", false, handlers.GetCurrentBranchByPlanIdHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/plans\", false, handlers.ListPlansHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/archive\", false, handlers.ListArchivedPlansHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/ps\", false, handlers.ListPlansRunningHandler).Methods(\"GET\")\n\n\tHandlePlandexFn(r, prefix+\"/projects/{projectId}/plans\", false, handlers.CreatePlanHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/projects/{projectId}/plans\", false, handlers.DeleteAllPlansHandler).Methods(\"DELETE\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}\", false, handlers.GetPlanHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}\", false, handlers.DeletePlanHandler).Methods(\"DELETE\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/current_plan/{sha}\", false, handlers.CurrentPlanHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/current_plan\", false, handlers.CurrentPlanHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/apply\", false, handlers.ApplyPlanHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/archive\", false, handlers.ArchivePlanHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/unarchive\", false, handlers.UnarchivePlanHandler).Methods(\"PATCH\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/rename\", false, handlers.RenamePlanHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/reject_all\", false, handlers.RejectAllChangesHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/reject_file\", false, handlers.RejectFileHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/reject_files\", false, handlers.RejectFilesHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/diffs\", false, handlers.GetPlanDiffsHandler).Methods(\"GET\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/context\", false, handlers.ListContextHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/context\", false, handlers.LoadContextHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/context/{contextId}/body\", false, handlers.GetContextBodyHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/context\", false, handlers.UpdateContextHandler).Methods(\"PUT\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/context\", false, handlers.DeleteContextHandler).Methods(\"DELETE\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/convo\", false, handlers.ListConvoHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/rewind\", false, handlers.RewindPlanHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/logs\", false, handlers.ListLogsHandler).Methods(\"GET\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/branches\", false, handlers.ListBranchesHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/branches/{branch}\", false, handlers.DeleteBranchHandler).Methods(\"DELETE\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/branches\", false, handlers.CreateBranchHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/settings\", false, handlers.GetSettingsHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/settings\", false, handlers.UpdateSettingsHandler).Methods(\"PUT\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/status\", false, handlers.GetPlanStatusHandler).Methods(\"GET\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/tell\", true, handlers.TellPlanHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/build\", true, handlers.BuildPlanHandler).Methods(\"PATCH\")\n\n\tHandlePlandexFn(r, prefix+\"/custom_models\", false, handlers.ListCustomModelsHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/custom_models\", false, handlers.UpsertCustomModelsHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/custom_models/{modelId}\", false, handlers.GetCustomModelHandler).Methods(\"GET\")\n\n\tHandlePlandexFn(r, prefix+\"/custom_providers\", false, handlers.ListCustomProvidersHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/custom_providers/{providerId}\", false, handlers.GetCustomProviderHandler).Methods(\"GET\")\n\n\tHandlePlandexFn(r, prefix+\"/model_sets\", false, handlers.ListModelPacksHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/model_sets\", false, handlers.CreateModelPackHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/model_sets/{setId}\", false, handlers.UpdateModelPackHandler).Methods(\"PUT\")\n\tHandlePlandexFn(r, prefix+\"/default_settings\", false, handlers.GetDefaultSettingsHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/default_settings\", false, handlers.UpdateDefaultSettingsHandler).Methods(\"PUT\")\n\n\tHandlePlandexFn(r, prefix+\"/file_map\", false, handlers.GetFileMapHandler).Methods(\"POST\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/load_cached_file_map\", false, handlers.LoadCachedFileMapHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/config\", false, handlers.GetPlanConfigHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/config\", false, handlers.UpdatePlanConfigHandler).Methods(\"PUT\")\n\n\tHandlePlandexFn(r, prefix+\"/default_plan_config\", false, handlers.GetDefaultPlanConfigHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/default_plan_config\", false, handlers.UpdateDefaultPlanConfigHandler).Methods(\"PUT\")\n\n\tHandlePlandexFn(r, prefix+\"/org_user_config\", false, handlers.GetOrgUserConfigHandler).Methods(\"GET\")\n\tHandlePlandexFn(r, prefix+\"/org_user_config\", false, handlers.UpdateOrgUserConfigHandler).Methods(\"PUT\")\n}\n\nfunc addProxyableApiRoutes(r *mux.Router, prefix string) {\n\tEnsureHandlePlandex()\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/connect\", true, handlers.ConnectPlanHandler).Methods(\"PATCH\")\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/stop\", false, handlers.StopPlanHandler).Methods(\"DELETE\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/respond_missing_file\", false, handlers.RespondMissingFileHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/auto_load_context\", false, handlers.AutoLoadContextHandler).Methods(\"POST\")\n\n\tHandlePlandexFn(r, prefix+\"/plans/{planId}/{branch}/build_status\", false, handlers.GetBuildStatusHandler).Methods(\"GET\")\n}\n"
  },
  {
    "path": "app/server/setup/setup.go",
    "content": "package setup\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"plandex-server/db\"\n\t\"plandex-server/host\"\n\t\"plandex-server/model/plan\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/shutdown\"\n\t\"runtime/debug\"\n\t\"syscall\"\n\t\"time\"\n)\n\nfunc MustLoadIp() {\n\terr := host.LoadIp()\n\tif err != nil {\n\t\tlog.Fatal(\"Error loading IP: \", err)\n\t}\n}\n\nfunc MustInitDb() {\n\terr := db.Connect()\n\tif err != nil {\n\t\tlog.Fatal(\"Error initializing database: \", err)\n\t}\n\n\terr = db.MigrationsUp()\n\tif err != nil {\n\t\tlog.Fatal(\"Error running migrations: \", err)\n\t}\n\n\terr = db.CacheOrgRoleIds()\n\tif err != nil {\n\t\tlog.Fatal(\"Error caching org role ids: \", err)\n\t}\n}\n\nvar shutdownHooks []func()\n\nfunc RegisterShutdownHook(hook func()) {\n\tshutdownHooks = append(shutdownHooks, hook)\n}\n\nfunc loggingMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Skip logging for monitoring endpoints\n\t\tif r.URL.Path == \"/health\" || r.URL.Path == \"/version\" {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\n\t\tstart := time.Now()\n\n\t\tlog.Printf(\"\\n\\nRequest: %s %s\\n\\n\", r.Method, r.URL.Path)\n\t\tnext.ServeHTTP(w, r)\n\t\tlog.Printf(\"\\n\\nCompleted: %s %s in %v\\n\\n\", r.Method, r.URL.Path, time.Since(start))\n\t})\n}\n\nfunc StartServer(handler http.Handler, configureFn func(handler http.Handler) http.Handler, afterStart func()) {\n\tif os.Getenv(\"GOENV\") == \"development\" {\n\t\tlog.Println(\"In development mode.\")\n\t}\n\n\tshutdown.ShutdownCtx, shutdown.ShutdownCancel = context.WithCancel(context.Background())\n\tdefer shutdown.ShutdownCancel()\n\n\t// Ensure database connection is closed\n\tdefer func() {\n\t\tlog.Println(\"Closing database connection...\")\n\t\terr := db.Conn.Close()\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error closing database connection: %v\", err)\n\t\t}\n\t\tlog.Println(\"Database connection closed\")\n\t}()\n\n\t// Get externalPort from the environment variable or default to 8099\n\texternalPort := os.Getenv(\"PORT\")\n\tif externalPort == \"\" {\n\t\texternalPort = \"8099\"\n\t}\n\n\t// Add logging middleware before the maxBytes middleware\n\thandler = loggingMiddleware(handler)\n\n\t// Apply the maxBytesMiddleware to limit request size to 1 GB\n\thandler = maxBytesMiddleware(handler, 1000<<20) // 1 GB limit\n\n\tif configureFn != nil {\n\t\thandler = configureFn(handler)\n\t}\n\n\tserver := &http.Server{\n\t\tAddr:              \":\" + externalPort,\n\t\tHandler:           handler,\n\t\tMaxHeaderBytes:    1 << 20, // 1 MB\n\t\tReadHeaderTimeout: 5 * time.Second,\n\t}\n\n\tgo func() {\n\t\tif err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n\t\t\tlog.Fatalf(\"Failed to start server: %v\", err)\n\t\t}\n\t}()\n\n\tlog.Println(\"Started Plandex server on port \" + externalPort)\n\n\tif afterStart != nil {\n\t\tafterStart()\n\t}\n\n\t// Capture SIGTERM and SIGINT signals\n\tsigTermChan := make(chan os.Signal, 1)\n\n\tsignal.Notify(sigTermChan, syscall.SIGTERM, syscall.SIGINT)\n\n\tsig := <-sigTermChan\n\tlog.Printf(\"Received signal %v, shutting down gracefully...\\n\", sig)\n\n\t// Create a channel to track completion of active plans\n\tplansDone := make(chan struct{})\n\n\t// Start goroutine to monitor active plans\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in waitForActivePlans: %v\\n%s\", r, debug.Stack())\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic in waitForActivePlans: %v\\n%s\", r, debug.Stack()))\n\t\t\t}\n\t\t\tclose(plansDone)\n\t\t}()\n\n\t\t// First wait for active plans to complete or timeout\n\t\tlog.Println(\"Waiting for active plans to complete...\")\n\t\tactivePlansCtx, cancel := context.WithTimeout(shutdown.ShutdownCtx, 60*time.Second)\n\t\tdefer cancel()\n\n\t\tselect {\n\t\tcase <-activePlansCtx.Done():\n\t\t\tif activePlansCtx.Err() == context.DeadlineExceeded {\n\t\t\t\tlog.Println(\"Timeout waiting for active plans. Forcing shutdown.\")\n\t\t\t}\n\t\tcase <-waitForActivePlans():\n\t\t\tlog.Println(\"All active plans finished.\")\n\t\t}\n\n\t\t// Then clean up any remaining locks\n\t\tlog.Println(\"Cleaning up any remaining locks...\")\n\t\tif err := db.CleanupActiveLocks(shutdown.ShutdownCtx); err != nil {\n\t\t\tlog.Printf(\"Error cleaning up locks: %v\", err)\n\t\t}\n\t}()\n\n\t// Wait for plans to finish or timeout\n\tselect {\n\tcase <-shutdown.ShutdownCtx.Done():\n\t\tlog.Println(\"Global shutdown timeout reached\")\n\tcase <-plansDone:\n\t\tlog.Println(\"All cleanup tasks completed\")\n\t}\n\n\t// Shutdown the HTTP server\n\tlog.Println(\"Shutting down http server...\")\n\thttpCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\tif err := server.Shutdown(httpCtx); err != nil {\n\t\tlog.Printf(\"Http server forced to shutdown: %v\", err)\n\t}\n\n\t// Execute shutdown hooks\n\tlog.Println(\"Executing shutdown hooks...\")\n\tfor _, hook := range shutdownHooks {\n\t\thook()\n\t}\n\n\tlog.Println(\"Shutdown complete\")\n}\n\nfunc waitForActivePlans() chan struct{} {\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tticker := time.NewTicker(1 * time.Second)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ticker.C:\n\t\t\t\tif plan.NumActivePlans() == 0 {\n\t\t\t\t\tclose(done)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\treturn done\n}\n\nfunc maxBytesMiddleware(next http.Handler, maxBytes int64) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// log the size of the request body\n\t\t// log.Printf(\"Request body size: %d\", r.ContentLength)\n\n\t\tr.Body = http.MaxBytesReader(w, r.Body, maxBytes)\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n"
  },
  {
    "path": "app/server/shutdown/shutdown.go",
    "content": "package shutdown\n\nimport (\n\t\"context\"\n)\n\nvar ShutdownCtx context.Context\nvar ShutdownCancel context.CancelFunc\n"
  },
  {
    "path": "app/server/syntax/comments.go",
    "content": "package syntax\n\nimport shared \"plandex-shared\"\n\n// // FindComments parses the given source code for the language implied by path\n// // and returns a slice of all comment strings plus an IsRef field indicating\n// // whether the comment is referencing original code (heuristic).\n// func FindComments(ctx context.Context, parser *tree_sitter.Parser, source string) ([]Comment, error) {\n// \tnodes, err := findCommentNodesForPath(ctx, parser, source)\n// \tif err != nil {\n// \t\treturn nil, err\n// \t}\n// \tvar comments []Comment\n// \tfor _, node := range nodes {\n// \t\tstart := node.StartByte()\n// \t\tend := node.EndByte()\n// \t\traw := source[start:end]\n\n// \t\tcomments = append(comments, Comment{\n// \t\t\tTxt:   raw,\n// \t\t\tIsRef: isRef(raw), // your existing logic\n// \t\t})\n// \t}\n// \treturn comments, nil\n// }\n\n// // StripComments removes all comments from the given source code using the appropriate parser\n// func StripComments(ctx context.Context, parser *tree_sitter.Parser, source string) (string, error) {\n// \t// Find the comment nodes first:\n// \tcommentNodes, err := findCommentNodesForPath(ctx, parser, source)\n// \tif err != nil {\n// \t\t// If parsing fails, return the source as-is along with an error.\n// \t\treturn source, fmt.Errorf(\"failed to parse the content: %v\", err)\n// \t}\n\n// \t// If no parser is available or no comments found, just return the source unmodified.\n// \tif len(commentNodes) == 0 {\n// \t\treturn source, nil\n// \t}\n\n// \t// Sort comment nodes in reverse order to remove them from the source safely.\n// \tsort.Slice(commentNodes, func(i, j int) bool {\n// \t\treturn commentNodes[i].StartByte() > commentNodes[j].StartByte()\n// \t})\n\n// \t// Remove comments from the source.\n// \tresult := []byte(source)\n// \tfor _, node := range commentNodes {\n// \t\tstart := node.StartByte()\n// \t\tend := node.EndByte()\n// \t\tresult = append(result[:start], result[end:]...)\n// \t}\n\n// \treturn string(result), nil\n// }\n\n// func findCommentNodesForPath(ctx context.Context, parser *tree_sitter.Parser, source string) ([]*tree_sitter.Node, error) {\n// \tif parser == nil {\n// \t\t// If no parser is available for this file type, return empty.\n// \t\treturn nil, nil\n// \t}\n\n// \t// Use a context with timeout (from your existing parserTimeout).\n// \tctx, cancel := context.WithTimeout(ctx, parserTimeout)\n// \tdefer cancel()\n\n// \ttree, err := parser.ParseCtx(ctx, nil, []byte(source))\n// \tif err != nil {\n// \t\treturn nil, err\n// \t}\n// \tdefer tree.Close()\n\n// \t// Gather all comment nodes.\n// \troot := tree.RootNode()\n// \tcommentNodes := findCommentNodes(root)\n// \treturn commentNodes, nil\n// }\n\n// func findCommentNodes(node *tree_sitter.Node) []*tree_sitter.Node {\n// \tvar commentNodes []*tree_sitter.Node\n\n// \tvisitNodes(node, func(n *tree_sitter.Node) {\n// \t\tif n.Type() == \"comment\" {\n// \t\t\tcommentNodes = append(commentNodes, n)\n// \t\t}\n// \t})\n\n// \treturn commentNodes\n// }\n\nfunc GetCommentSymbols(lang shared.Language) (string, string) {\n\tswitch lang {\n\tcase 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:\n\t\treturn \"//\", \"\"\n\tcase shared.LanguageBash, shared.LanguageDockerfile, shared.LanguageElixir, shared.LanguageHcl, shared.LanguagePython, shared.LanguageRuby, shared.LanguageToml, shared.LanguageYaml:\n\t\treturn \"#\", \"\"\n\tcase shared.LanguageLua, shared.LanguageElm:\n\t\treturn \"--\", \"\"\n\tcase shared.LanguageCss:\n\t\treturn \"/*\", \"*/\"\n\tcase shared.LanguageHtml:\n\t\treturn \"<!--\", \"-->\"\n\tcase shared.LanguageOCaml:\n\t\treturn \"(*\", \"*)\"\n\tcase shared.LanguageSvelte, shared.LanguageJsx, shared.LanguageTsx, shared.LanguageJson:\n\t\treturn \"\", \"\" // comments are either not allowed or correct symbols depend on the context\n\t}\n\n\treturn \"\", \"\"\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/cli/.gitignore",
    "content": "mapper"
  },
  {
    "path": "app/server/syntax/file_map/cli/go.mod",
    "content": "module mapper\n\ngo 1.23.3\n\nreplace plandex-server => ../../../\n\nreplace plandex-shared => ../../../../shared\n\nreplace plandex-cli => ../../../../cli\n\nrequire (\n\tplandex-cli v0.0.0-00010101000000-000000000000\n\tplandex-server v0.0.0-00010101000000-000000000000\n\tplandex-shared v0.0.0-00010101000000-000000000000\n)\n\nrequire (\n\tgithub.com/Masterminds/semver v1.5.0 // indirect\n\tgithub.com/PuerkitoBio/goquery v1.10.3 // indirect\n\tgithub.com/alecthomas/chroma v0.10.0 // indirect\n\tgithub.com/alecthomas/chroma/v2 v2.18.0 // indirect\n\tgithub.com/andybalholm/cascadia v1.3.3 // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aws/aws-sdk-go v1.55.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.36.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect\n\tgithub.com/aws/smithy-go v1.22.4 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/briandowns/spinner v1.23.2 // indirect\n\tgithub.com/calmh/randomart v1.1.0 // indirect\n\tgithub.com/charmbracelet/bubbles v0.21.0 // indirect\n\tgithub.com/charmbracelet/bubbletea v1.3.5 // indirect\n\tgithub.com/charmbracelet/charm v0.8.7 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.3.1 // indirect\n\tgithub.com/charmbracelet/glamour v0.10.0 // indirect\n\tgithub.com/charmbracelet/glow v1.5.1 // indirect\n\tgithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.9.3 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13 // indirect\n\tgithub.com/charmbracelet/x/exp/slice v0.0.0-20250623112707-45752038d08d // indirect\n\tgithub.com/charmbracelet/x/term v0.2.1 // indirect\n\tgithub.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect\n\tgithub.com/chromedp/chromedp v0.13.3 // indirect\n\tgithub.com/chromedp/sysutil v1.1.0 // indirect\n\tgithub.com/coreos/go-systemd/v22 v22.5.0 // indirect\n\tgithub.com/cqroot/multichoose v0.1.1 // indirect\n\tgithub.com/cqroot/prompt v0.9.4 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/fsnotify/fsnotify v1.6.0 // indirect\n\tgithub.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 // indirect\n\tgithub.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect\n\tgithub.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect\n\tgithub.com/gobwas/httphead v0.1.0 // indirect\n\tgithub.com/gobwas/pool v0.2.1 // indirect\n\tgithub.com/gobwas/ws v1.4.0 // indirect\n\tgithub.com/godbus/dbus/v5 v5.1.0 // indirect\n\tgithub.com/golang-migrate/migrate/v4 v4.18.3 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/gorilla/mux v1.8.1 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jinzhu/copier v0.4.0 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/jmoiron/sqlx v1.4.0 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/lithammer/fuzzysearch v1.1.8 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/magiconair/properties v1.8.6 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/mattn/go-tty v0.0.3 // indirect\n\tgithub.com/meowgorithm/babyenv v1.3.1 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/microcosm-cc/bluemonday v1.0.27 // indirect\n\tgithub.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/gitcha v0.2.0 // indirect\n\tgithub.com/muesli/go-app-paths v0.2.2 // indirect\n\tgithub.com/muesli/reflow v0.3.0 // indirect\n\tgithub.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect\n\tgithub.com/olekukonko/errors v1.1.0 // indirect\n\tgithub.com/olekukonko/ll v0.0.9 // indirect\n\tgithub.com/olekukonko/tablewriter v0.0.5 // indirect\n\tgithub.com/pelletier/go-toml v1.9.5 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.0.5 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pkg/term v1.2.0-beta.2 // indirect\n\tgithub.com/pkoukk/tiktoken-go v0.1.7 // indirect\n\tgithub.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c // indirect\n\tgithub.com/plandex-ai/survey/v2 v2.3.7 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect\n\tgithub.com/sahilm/fuzzy v0.1.1 // indirect\n\tgithub.com/sashabaranov/go-openai v1.40.3 // indirect\n\tgithub.com/segmentio/ksuid v1.0.4 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 // indirect\n\tgithub.com/spf13/afero v1.9.2 // indirect\n\tgithub.com/spf13/cast v1.5.0 // indirect\n\tgithub.com/spf13/cobra v1.8.0 // indirect\n\tgithub.com/spf13/jwalterweatherman v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/spf13/viper v1.14.0 // indirect\n\tgithub.com/subosito/gotenv v1.4.1 // indirect\n\tgithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xeipuuv/gojsonschema v1.2.0 // indirect\n\tgithub.com/xlab/treeprint v1.2.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/yuin/goldmark v1.7.12 // indirect\n\tgithub.com/yuin/goldmark-emoji v1.0.6 // indirect\n\tgo.uber.org/atomic v1.11.0 // indirect\n\tgolang.org/x/crypto v0.39.0 // indirect\n\tgolang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect\n\tgolang.org/x/image v0.28.0 // indirect\n\tgolang.org/x/mod v0.25.0 // indirect\n\tgolang.org/x/net v0.41.0 // indirect\n\tgolang.org/x/sync v0.15.0 // indirect\n\tgolang.org/x/sys v0.33.0 // indirect\n\tgolang.org/x/term v0.32.0 // indirect\n\tgolang.org/x/text v0.26.0 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "app/server/syntax/file_map/cli/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=\ncloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=\ncloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=\ncloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=\ncloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=\ncloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=\ncloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=\ncloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=\ncloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=\ncloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=\ncloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=\ncloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=\ncloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=\ncloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=\ncloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=\ncloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=\ncloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=\ncloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=\ncloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=\ncloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=\ncloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=\ncloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=\ncloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=\ncloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=\ncloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=\ncloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=\ncloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=\ncloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=\ncloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=\ncloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=\ncloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=\ncloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=\ncloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=\ncloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=\ncloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=\ncloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=\ncloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=\ncloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=\ncloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=\ncloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=\ncloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=\ncloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=\ncloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=\ncloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=\ncloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=\ncloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=\ncloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=\ncloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=\ncloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=\ncloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=\ncloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=\ncloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=\ncloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=\ncloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=\ncloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=\ncloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=\ncloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=\ncloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=\ncloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=\ncloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=\ncloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=\ncloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=\ncloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=\ncloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=\ncloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=\ncloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=\ncloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=\ncloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=\ncloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=\ncloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=\ncloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=\ncloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=\ncloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=\ncloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=\ncloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=\ncloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=\ncloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=\ncloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=\ncloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=\ncloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=\ncloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=\ncloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=\ncloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=\ncloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=\ncloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=\ncloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=\ncloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=\ncloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=\ncloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=\ncloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=\ncloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=\ncloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=\ncloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=\ncloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=\ncloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=\ncloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=\ncloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=\ncloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=\ncloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=\ncloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=\ncloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=\ncloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=\ncloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=\ncloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=\ncloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=\ncloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=\ncloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=\ncloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=\ncloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=\ncloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=\ncloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=\ncloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=\ncloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=\ncloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=\ncloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=\ncloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=\ncloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=\ncloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=\ncloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=\ncloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=\ncloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=\ncloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=\ncloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=\ncloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=\ncloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=\ncloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=\ncloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=\ncloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=\ncloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=\ncloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=\ncloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=\ngithub.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=\ngithub.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=\ngithub.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=\ngithub.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=\ngithub.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=\ngithub.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=\ngithub.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=\ngithub.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=\ngithub.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=\ngithub.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=\ngithub.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=\ngithub.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=\ngithub.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=\ngithub.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=\ngithub.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=\ngithub.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=\ngithub.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=\ngithub.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=\ngithub.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=\ngithub.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=\ngithub.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=\ngithub.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=\ngithub.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=\ngithub.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A=\ngithub.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE=\ngithub.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=\ngithub.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=\ngithub.com/calmh/randomart v1.1.0 h1:evl+iwc10LXtHdMZhzLxmsCQVmWnkXs44SbC6Uk0Il8=\ngithub.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charmbracelet/bubbles v0.7.5/go.mod h1:IRTORFvhEI6OUH7WhN2Ks8Z8miNGimk1BE6cmHijOkM=\ngithub.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=\ngithub.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=\ngithub.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=\ngithub.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=\ngithub.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=\ngithub.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=\ngithub.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=\ngithub.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=\ngithub.com/charmbracelet/bubbletea v1.3.0 h1:fPMyirm0u3Fou+flch7hlJN9krlnVURrkUVDwqXjoAc=\ngithub.com/charmbracelet/bubbletea v1.3.0/go.mod h1:eTaHfqbIwvBhFQM/nlT1NsGc4kp8jhF8LfUK67XiTDM=\ngithub.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=\ngithub.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=\ngithub.com/charmbracelet/charm v0.8.7 h1:FJ9b7IxWUWHOPR72zS/QJLEqtudOB2Mwfc+Sir0eZR8=\ngithub.com/charmbracelet/charm v0.8.7/go.mod h1:ApJYwJljEjODkOYJgFDzbUqztLrCWQct9zyPD+xcVr4=\ngithub.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=\ngithub.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=\ngithub.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=\ngithub.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=\ngithub.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=\ngithub.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=\ngithub.com/charmbracelet/glow v1.5.1 h1:o1mwT4xXXpkfUhJG6euQayNxLZf9yKctOCNHLztrwdE=\ngithub.com/charmbracelet/glow v1.5.1/go.mod h1:rGgop0a2/4gXWiAxUW1iEQseoE+9Ctpb7M4sM9cY9CU=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=\ngithub.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=\ngithub.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=\ngithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=\ngithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=\ngithub.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=\ngithub.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=\ngithub.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=\ngithub.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=\ngithub.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=\ngithub.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/exp/slice v0.0.0-20250623112707-45752038d08d h1:U/S+aA/k7pkJdYkBUhbmMOZXszU19WmauJ4bXe+7zRc=\ngithub.com/charmbracelet/x/exp/slice v0.0.0-20250623112707-45752038d08d/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=\ngithub.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=\ngithub.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0=\ngithub.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=\ngithub.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=\ngithub.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cqroot/multichoose v0.1.1 h1:diGuKYKea9ePOTwUyUDor9zKRqKFWXGkYGqUa9+firU=\ngithub.com/cqroot/multichoose v0.1.1/go.mod h1:BJzIGqbQZNADPDuA3IzhmTMpRc2F3fZKysMRYP+Ydw8=\ngithub.com/cqroot/prompt v0.9.3 h1:00Sjiasl1QL7ttEphJ+1xAl0fKQi+7s2F3aY0x7wnz4=\ngithub.com/cqroot/prompt v0.9.3/go.mod h1:NZvCTeuvR9ew9Hkk7xlrZ9xdVH4AmkO9R0eeBkzOHXQ=\ngithub.com/cqroot/prompt v0.9.4 h1:uFRlhXuOP3CSD+Pii0Z8VJhgXpavSloFf7/KAERwjz8=\ngithub.com/cqroot/prompt v0.9.4/go.mod h1:6BVZiEv7XkW1K64y1k2wdzToDwspL3n/RkUIyPjQ808=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=\ngithub.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg=\ngithub.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4 h1:ygs9POGDQpQGLJPlq4+0LBUmMBNox1N4JSpw+OETcvI=\ngithub.com/gen2brain/beeep v0.0.0-20240516210008-9c006672e7f4/go.mod h1:0W7dI87PvXJ1Sjs0QPvWXKcQmNERY77e8l7GFhZB/s4=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=\ngithub.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=\ngithub.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=\ngithub.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=\ngithub.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=\ngithub.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=\ngithub.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=\ngithub.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=\ngithub.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=\ngithub.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=\ngithub.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=\ngithub.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=\ngithub.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=\ngithub.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY=\ngithub.com/hashicorp/consul/sdk v0.11.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=\ngithub.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=\ngithub.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=\ngithub.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=\ngithub.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=\ngithub.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=\ngithub.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=\ngithub.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/meowgorithm/babyenv v1.3.0/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=\ngithub.com/meowgorithm/babyenv v1.3.1 h1:18ZEYIgbzoFQfRLF9+lxjRfk/ui6w8U0FWl07CgWvvc=\ngithub.com/meowgorithm/babyenv v1.3.1/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=\ngithub.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=\ngithub.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=\ngithub.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=\ngithub.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=\ngithub.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=\ngithub.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/gitcha v0.2.0 h1:+wOgT2dI9s2Tznj1t1rb/qkK5e0cb6qD8c4IX2TR/YY=\ngithub.com/muesli/gitcha v0.2.0/go.mod h1:Ri8m9TZS4+ORG4JVmVKUQcWZuxDvUW3UKxMdQfzG2zI=\ngithub.com/muesli/go-app-paths v0.2.1/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=\ngithub.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI=\ngithub.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=\ngithub.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a h1:Hw/15RYEOUD6T9UCRkUmNBa33kJkH33Fui6hE4sRLKU=\ngithub.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a/go.mod h1:+XG0ne5zXWBTSbbe7Z3/RWxaT8PZY6zaZ1dX6KjprYY=\ngithub.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=\ngithub.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=\ngithub.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=\ngithub.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=\ngithub.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=\ngithub.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=\ngithub.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=\ngithub.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=\ngithub.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=\ngithub.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=\ngithub.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=\ngithub.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=\ngithub.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=\ngithub.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=\ngithub.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=\ngithub.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=\ngithub.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=\ngithub.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=\ngithub.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=\ngithub.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c h1:bki/wkg5iFBOv3jCPUDNuH5yLngUPUdEJCSuvc2tiQ0=\ngithub.com/plandex-ai/go-prompt v0.0.0-20250304173555-1f364907fc6c/go.mod h1:SqEsJfsIr0GYUyLatvezDOBe6XsCw64E7v33QzeH5PM=\ngithub.com/plandex-ai/survey/v2 v2.3.7 h1:u1o6bflbaBpW8i8krm+91Z2cOcvZcMVS+AjV+rgR8Rk=\ngithub.com/plandex-ai/survey/v2 v2.3.7/go.mod h1:RiBOKRDB5fOQrOzsiAPAN57hYqFKPkCxgSK7twcDOys=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=\ngithub.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=\ngithub.com/sagikazarmark/crypt v0.8.0/go.mod h1:TmKwZAo97S4Fy4sfMH/HX/cQP5D+ijra2NyLpNNmttY=\ngithub.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=\ngithub.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/sashabaranov/go-openai v1.40.0 h1:Peg9Iag5mUJtPW00aYatlsn97YML0iNULiLNe74iPrU=\ngithub.com/sashabaranov/go-openai v1.40.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/sashabaranov/go-openai v1.40.3 h1:PkOw0SK34wrvYVOuXF1HZzuTBRh992qRZHil4kG3eYE=\ngithub.com/sashabaranov/go-openai v1.40.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=\ngithub.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4=\ngithub.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=\ngithub.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=\ngithub.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=\ngithub.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=\ngithub.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=\ngithub.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=\ngithub.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=\ngithub.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=\ngithub.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=\ngithub.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=\ngithub.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=\ngithub.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=\ngithub.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=\ngithub.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=\ngithub.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=\ngithub.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=\ngithub.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=\ngo.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=\ngo.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=\ngo.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4=\ngo.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=\ngo.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=\ngo.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=\ngolang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=\ngolang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=\ngolang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=\ngolang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=\ngolang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=\ngolang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=\ngolang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=\ngolang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=\ngolang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=\ngolang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=\ngolang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=\ngolang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=\ngolang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=\ngolang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=\ngolang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=\ngolang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=\ngolang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=\ngolang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=\ngolang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=\ngolang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=\ngolang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngolang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=\ngoogle.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=\ngoogle.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=\ngoogle.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=\ngoogle.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=\ngoogle.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=\ngoogle.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=\ngoogle.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=\ngoogle.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=\ngoogle.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=\ngoogle.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=\ngoogle.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=\ngoogle.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=\ngoogle.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=\ngoogle.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=\ngoogle.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=\ngoogle.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=\ngoogle.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=\ngoogle.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=\ngoogle.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=\ngoogle.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=\ngoogle.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=\ngoogle.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=\ngoogle.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=\ngoogle.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=\ngoogle.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=\ngoogle.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=\ngoogle.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=\ngoogle.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=\ngoogle.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=\ngopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "app/server/syntax/file_map/cli/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-server/syntax/file_map\"\n\t\"sync\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc main() {\n\tctx, cancel := context.WithTimeout(context.Background(), time.Minute)\n\tdefer cancel()\n\n\targs := os.Args[1:]\n\n\tif len(args) < 1 {\n\t\tfmt.Println(\"usage: mapper [files-or-dirs...]\")\n\t\tos.Exit(1)\n\t}\n\n\tvar parserTree bool = false\n\n\tfor i, arg := range args {\n\t\tif arg == \"--trees\" {\n\t\t\tparserTree = true\n\t\t\targs = append(args[:i], args[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tpaths := args\n\n\tvar filteredPaths []string\n\tfor _, path := range paths {\n\t\tif shared.HasFileMapSupport(path) {\n\t\t\tfilteredPaths = append(filteredPaths, path)\n\t\t}\n\t}\n\n\terrCh := make(chan error, len(filteredPaths))\n\tfileInputs := map[string]string{}\n\tvar mu sync.Mutex\n\n\tfor _, path := range filteredPaths {\n\t\tgo func(path string) {\n\t\t\tcontent, err := os.ReadFile(path)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error reading file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmu.Lock()\n\t\t\tfileInputs[path] = string(content)\n\t\t\tmu.Unlock()\n\t\t\terrCh <- nil\n\t\t}(path)\n\t}\n\n\tfor i := 0; i < len(filteredPaths); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"error reading file: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tif parserTree {\n\t\ttrees, err := file_map.ProcessMapTrees(ctx, fileInputs)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"error processing map files: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tfmt.Println(trees.CombinedTrees())\n\t} else {\n\t\tmapBodies, err := file_map.ProcessMapFiles(ctx, fileInputs)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"error processing map files: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tfmt.Println(mapBodies.CombinedMap(map[string]int{}))\n\t}\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/bash_example.sh",
    "content": "#!/bin/bash\n\n# Global variables\nGLOBAL_VAR=\"Hello World\"\nreadonly CONSTANT_VAR=\"This is constant\"\n\n# Function definition\nfunction print_message() {\n    local message=\"$1\"\n    echo \"$message\"\n}\n\n# Function with return value\nget_date() {\n    echo $(date +%Y-%m-%d)\n}\n\n# Array declaration\ndeclare -a fruits=(\"apple\" \"banana\" \"orange\")\n\n# Associative array\ndeclare -A user_info=(\n    [\"name\"]=\"John\"\n    [\"age\"]=\"30\"\n    [\"city\"]=\"New York\"\n)\n\n# Main script execution\nmain() {\n    print_message \"$GLOBAL_VAR\"\n    current_date=$(get_date)\n    echo \"Today is: $current_date\"\n    \n    # Loop through array\n    for fruit in \"${fruits[@]}\"; do\n        echo \"Fruit: $fruit\"\n    done\n    \n    # Access associative array\n    echo \"User ${user_info[name]} is ${user_info[age]} years old\"\n}\n\n# Call main function\nmain\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/c_example.c",
    "content": "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n// Macro definitions\n#define MAX_SIZE 100\n#define SQUARE(x) ((x) * (x))\n\n// Type definitions\ntypedef struct {\n    char name[50];\n    int age;\n} Person;\n\ntypedef enum {\n    MONDAY,\n    TUESDAY,\n    WEDNESDAY,\n    THURSDAY,\n    FRIDAY\n} Weekday;\n\n// Global variables\nstatic const double PI = 3.14159;\nint globalCounter = 0;\n\n// Function declarations\nvoid printPerson(const Person* p);\nint factorial(int n);\n\n// Union example\nunion Data {\n    int i;\n    float f;\n    char str[20];\n};\n\n// Function pointer type\ntypedef int (*Operation)(int, int);\n\n// Function implementations\nint add(int a, int b) {\n    return a + b;\n}\n\nint subtract(int a, int b) {\n    return a - b;\n}\n\nvoid printPerson(const Person* p) {\n    printf(\"Name: %s, Age: %d\\n\", p->name, p->age);\n}\n\nint factorial(int n) {\n    if (n <= 1) return 1;\n    return n * factorial(n - 1);\n}\n\n// Main function\nint main() {\n    // Local variable declarations\n    Person person = {\"John Doe\", 30};\n    union Data data;\n    Operation op = add;\n    \n    // Using structs\n    printPerson(&person);\n    \n    // Using unions\n    data.i = 10;\n    printf(\"data.i: %d\\n\", data.i);\n    \n    // Using function pointers\n    printf(\"10 + 20 = %d\\n\", op(10, 20));\n    op = subtract;\n    printf(\"10 - 20 = %d\\n\", op(10, 20));\n    \n    // Using macros\n    printf(\"Square of 5 is %d\\n\", SQUARE(5));\n    \n    // Using enums\n    Weekday today = WEDNESDAY;\n    printf(\"Day number: %d\\n\", today);\n    \n    // Using global variables\n    globalCounter++;\n    printf(\"Global counter: %d\\n\", globalCounter);\n    \n    return 0;\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/cpp_example.cpp",
    "content": "#include <iostream>\n#include <memory>\n#include <vector>\n#include <string>\n#include <functional>\n\n// Global variables at file scope\nint globalInteger = 42;\nconst double PI = 3.14159;\nstatic std::string globalString = \"Global static string\";\n\n// Global enum and constant\nenum Color { RED, GREEN, BLUE };\nconstexpr int MAX_SIZE = 100;\n\n// Template class\ntemplate<typename T>\nclass Container {\npublic:\n    static T defaultValue;  // Static class member\n    const T maxCapacity = MAX_SIZE;  // Class constant\n    void add(T item) { items.push_back(item); }\n    const std::vector<T>& getItems() const { return items; }\nprivate:\n    std::vector<T> items;\n};\n\n// Static member definition\ntemplate<typename T>\nT Container<T>::defaultValue = T();\n\n// Abstract base class\nclass Animal {\npublic:\n    virtual ~Animal() = default;\n    virtual void makeSound() const = 0;\nprotected:\n    std::string name;\n};\n\n// Derived class with virtual inheritance\nclass Dog : virtual public Animal {\npublic:\n    Dog(const std::string& dogName) { name = dogName; }\n    void makeSound() const override {\n        std::cout << name << \" says: Woof!\" << std::endl;\n    }\n};\n\n// Namespace example\nnamespace Utils {\n    // Namespace-level variables\n    inline int counter = 0;\n    const std::string VERSION = \"1.0.0\";\n    \n    // Function template\n    template<typename T>\n    T max(T a, T b) {\n        return (a > b) ? a : b;\n    }\n\n    // Lambda function stored in variable\n    const auto printer = [](const std::string& msg) {\n        std::cout << \"Message: \" << msg << std::endl;\n    };\n}\n\n// Smart pointer and move semantics example\nclass Resource {\npublic:\n    Resource(const std::string& data) : data_(data) {\n        std::cout << \"Resource constructed\" << std::endl;\n    }\n    ~Resource() {\n        std::cout << \"Resource destroyed\" << std::endl;\n    }\n    Resource(Resource&& other) noexcept : data_(std::move(other.data_)) {}\n    std::string getData() const { return data_; }\nprivate:\n    std::string data_;\n};\n\n// Static member variable and function\nclass Counter {\npublic:\n    static int getCount() { return count; }\n    Counter() { ++count; }\n    ~Counter() { --count; }\nprivate:\n    static int count;\n};\nint Counter::count = 0;\n\n// Friend function example\nclass Box {\n    friend std::ostream& operator<<(std::ostream& os, const Box& box);\npublic:\n    Box(int w, int h) : width(w), height(h) {}\nprivate:\n    int width;\n    int height;\n};\n\nstd::ostream& operator<<(std::ostream& os, const Box& box) {\n    return os << \"Box(\" << box.width << \"x\" << box.height << \")\";\n}\n\n// Main function\nint main() {\n    // Smart pointer usage\n    auto resource = std::make_unique<Resource>(\"Hello\");\n    std::cout << resource->getData() << std::endl;\n\n    // Template class usage\n    Container<int> numbers;\n    numbers.add(1);\n    numbers.add(2);\n\n    // Polymorphism\n    std::unique_ptr<Animal> dog = std::make_unique<Dog>(\"Rex\");\n    dog->makeSound();\n\n    // Template function\n    std::cout << \"Max: \" << Utils::max(10, 20) << std::endl;\n\n    // Lambda function\n    Utils::printer(\"Hello from lambda!\");\n\n    // Counter static example\n    Counter c1, c2;\n    std::cout << \"Count: \" << Counter::getCount() << std::endl;\n\n    // Friend function\n    Box box(10, 20);\n    std::cout << box << std::endl;\n\n    return 0;\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/csharp_example.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing System.Linq;\n\n// Namespace declaration\nnamespace ExampleApp\n{\n    // Interface definition\n    public interface IProcessor<T>\n    {\n        Task<T> ProcessAsync(T input);\n        bool Validate(T input);\n    }\n\n    // Enum definition\n    public enum Status\n    {\n        Pending,\n        Active,\n        Completed,\n        Failed\n    }\n\n    // Delegate declaration\n    public delegate void StatusChangedEventHandler(Status oldStatus, Status newStatus);\n\n    // Generic class implementing interface\n    public class DataProcessor<T> : IProcessor<T> where T : class\n    {\n        // Event declaration\n        public event StatusChangedEventHandler StatusChanged;\n\n        // Auto-implemented property\n        public Status CurrentStatus { get; private set; }\n\n        // Static field\n        private static readonly Dictionary<Type, int> _processedItems = new();\n\n        // Constructor\n        public DataProcessor()\n        {\n            CurrentStatus = Status.Pending;\n        }\n\n        // Async method implementation\n        public async Task<T> ProcessAsync(T input)\n        {\n            var oldStatus = CurrentStatus;\n            CurrentStatus = Status.Active;\n            OnStatusChanged(oldStatus, CurrentStatus);\n\n            await Task.Delay(100); // Simulate work\n\n            if (_processedItems.ContainsKey(typeof(T)))\n                _processedItems[typeof(T)]++;\n            else\n                _processedItems[typeof(T)] = 1;\n\n            CurrentStatus = Status.Completed;\n            OnStatusChanged(Status.Active, CurrentStatus);\n\n            return input;\n        }\n\n        // Interface method implementation\n        public bool Validate(T input) => input != null;\n\n        // Protected virtual method\n        protected virtual void OnStatusChanged(Status oldStatus, Status newStatus)\n        {\n            StatusChanged?.Invoke(oldStatus, newStatus);\n        }\n\n        // Static method\n        public static int GetProcessedCount<TItem>() where TItem : class\n        {\n            return _processedItems.GetValueOrDefault(typeof(TItem));\n        }\n    }\n\n    // Record type (C# 9.0+)\n    public record Person(string Name, int Age)\n    {\n        // Property with validation\n        public string Email { get; init; } = string.Empty;\n    }\n\n    // Extension method\n    public static class StringExtensions\n    {\n        public static int WordCount(this string str)\n        {\n            return str.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length;\n        }\n    }\n\n    // Main program class\n    public class Program\n    {\n        public static async Task Main(string[] args)\n        {\n            var processor = new DataProcessor<Person>();\n            processor.StatusChanged += (old, @new) => \n                Console.WriteLine($\"Status changed from {old} to {@new}\");\n\n            var person = new Person(\"John Doe\", 30) { Email = \"john@example.com\" };\n            \n            if (processor.Validate(person))\n            {\n                var result = await processor.ProcessAsync(person);\n                Console.WriteLine($\"Processed person: {result.Name}\");\n                Console.WriteLine($\"Word count in name: {result.Name.WordCount()}\");\n            }\n\n            Console.WriteLine($\"Total processed persons: {DataProcessor<Person>.GetProcessedCount<Person>()}\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/css_example.css",
    "content": "/* Global variables */\n:root {\n  --primary-color: #3498db;\n  --secondary-color: #2ecc71;\n  --text-color: #2c3e50;\n  --spacing-unit: 1rem;\n  --border-radius: 4px;\n}\n\n/* Reset and base styles */\n* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: 'Arial', sans-serif;\n  line-height: 1.6;\n  color: var(--text-color);\n}\n\n/* Layout containers */\n.container {\n  max-width: 1200px;\n  margin: 0 auto;\n  padding: var(--spacing-unit);\n}\n\n/* Grid system */\n.grid {\n  display: grid;\n  grid-template-columns: repeat(12, 1fr);\n  gap: var(--spacing-unit);\n}\n\n/* Flexbox components */\n.flex-container {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n/* Component styles */\n.button {\n  padding: calc(var(--spacing-unit) / 2) var(--spacing-unit);\n  border-radius: var(--border-radius);\n  border: none;\n  cursor: pointer;\n  transition: background-color 0.3s ease;\n}\n\n.button:hover {\n  opacity: 0.9;\n}\n\n.button--primary {\n  background-color: var(--primary-color);\n  color: white;\n}\n\n.button--secondary {\n  background-color: var(--secondary-color);\n  color: white;\n}\n\n/* Card component */\n.card {\n  border-radius: var(--border-radius);\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  padding: var(--spacing-unit);\n}\n\n/* Media queries */\n@media screen and (max-width: 768px) {\n  .grid {\n    grid-template-columns: repeat(6, 1fr);\n  }\n}\n\n@media screen and (max-width: 480px) {\n  .grid {\n    grid-template-columns: 1fr;\n  }\n  \n  .flex-container {\n    flex-direction: column;\n    gap: var(--spacing-unit);\n  }\n}\n\n/* Animation keyframes */\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n.fade-in {\n  animation: fadeIn 0.3s ease-in;\n}\n\n/* Form styles */\n.form-group {\n  margin-bottom: var(--spacing-unit);\n}\n\n.form-input {\n  width: 100%;\n  padding: calc(var(--spacing-unit) / 2);\n  border: 1px solid #ddd;\n  border-radius: var(--border-radius);\n}\n\n/* Navigation */\n.nav {\n  background-color: var(--primary-color);\n  padding: var(--spacing-unit);\n}\n\n.nav__list {\n  list-style: none;\n  display: flex;\n  gap: var(--spacing-unit);\n}\n\n.nav__link {\n  color: white;\n  text-decoration: none;\n}\n\n/* Utility classes */\n.text-center { text-align: center; }\n.text-right { text-align: right; }\n.text-left { text-align: left; }\n\n.mt-1 { margin-top: var(--spacing-unit); }\n.mb-1 { margin-bottom: var(--spacing-unit); }\n.ml-1 { margin-left: var(--spacing-unit); }\n.mr-1 { margin-right: var(--spacing-unit); }\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/cue_example.cue",
    "content": "// Package definition\npackage example\n\n// Import statements\nimport (\n    \"strings\"\n    \"time\"\n)\n\n// Schema definitions\n#Person: {\n    name:     string\n    age:      int & >=0 & <=120\n    email?:   string & =~\"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$\"\n    address?: #Address\n}\n\n#Address: {\n    street:  string\n    city:    string\n    country: string\n    zip:     string & =~\"^[0-9]{5}$\"\n}\n\n// Default values and constraints\n#DefaultPerson: #Person & {\n    name: string | *\"John Doe\"\n    age:  int | *30\n}\n\n// List type definition\n#Team: {\n    name:    string\n    members: [...#Person]\n    leader:  #Person\n}\n\n// Computed fields\n#Employee: #Person & {\n    role:    string\n    salary:  float\n    taxRate: float\n\n    // Computed field\n    netSalary: salary * (1 - taxRate)\n}\n\n// Concrete values\nexampleTeam: #Team & {\n    name: \"Engineering\"\n    members: [\n        {\n            name: \"Alice Smith\"\n            age:  28\n            email: \"alice@example.com\"\n        },\n        {\n            name: \"Bob Jones\"\n            age:  35\n            email: \"bob@example.com\"\n        }\n    ]\n    leader: {\n        name:  \"Carol Wilson\"\n        age:   40\n        email: \"carol@example.com\"\n    }\n}\n\n// Configuration with references and templates\n#Config: {\n    environment: \"development\" | \"staging\" | \"production\"\n    database: {\n        host:     string\n        port:     int & >0 & <65536\n        username: string\n        password: string\n    }\n    features: [string]: bool\n}\n\n// Template usage\nproductionConfig: #Config & {\n    environment: \"production\"\n    database: {\n        host:     \"db.example.com\"\n        port:     5432\n        username: \"admin\"\n        password: \"secret\"\n    }\n    features: {\n        \"feature1\": true\n        \"feature2\": false\n    }\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/dockerfile_example",
    "content": "# Multi-stage build example\nFROM golang:1.21-alpine AS builder\n\n# Build arguments\nARG VERSION=1.0.0\nARG BUILD_DATE\n\n# Set working directory\nWORKDIR /app\n\n# Copy only necessary files for dependency resolution\nCOPY go.mod go.sum ./\n\n# Download dependencies\nRUN go mod download\n\n# Copy source code\nCOPY . .\n\n# Build the application\nRUN CGO_ENABLED=0 GOOS=linux go build -ldflags=\"-X main.Version=${VERSION} -X main.BuildDate=${BUILD_DATE}\" -o /app/server\n\n# Create final lightweight image\nFROM alpine:latest\n\n# Labels for metadata\nLABEL maintainer=\"example@example.com\" \\\n      version=\"${VERSION}\" \\\n      description=\"Example Dockerfile with various syntax elements\"\n\n# Environment variables\nENV APP_ENV=production \\\n    PORT=8080\n\n# Create non-root user\nRUN addgroup -S appgroup && adduser -S appuser -G appgroup\n\n# Install runtime dependencies\nRUN apk add --no-cache \\\n    ca-certificates \\\n    tzdata\n\n# Set working directory\nWORKDIR /app\n\n# Copy binary from builder stage\nCOPY --from=builder /app/server .\n\n# Copy configuration files\nCOPY config/production.yaml /etc/app/config.yaml\n\n# Create volume mount points\nVOLUME [\"/data\", \"/logs\"]\n\n# Expose ports\nEXPOSE 8080 8443\n\n# Switch to non-root user\nUSER appuser\n\n# Health check\nHEALTHCHECK --interval=30s --timeout=3s \\\n    CMD wget --quiet --tries=1 --spider http://localhost:${PORT}/health || exit 1\n\n# Set entry point and default command\nENTRYPOINT [\"/app/server\"]\nCMD [\"--config\", \"/etc/app/config.yaml\"]\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/elixir_example.ex",
    "content": "defmodule ExampleApp do                                                   \n  # Module attributes                                                     \n  @default_timeout 5000                                                   \n  @version \"1.0.0\"                                                        \n                                                                          \n  # Protocol definition                                                   \n  defprotocol Formatter do                                                \n    @doc \"Format the data for display\"                                    \n    def format(data)                                                      \n  end                                                                     \n                                                                          \n  # Protocol implementation                                               \n  defimpl Formatter, for: Map do                                          \n    def format(data) do                                                   \n      inspect(data, pretty: true)                                         \n    end                                                                   \n  end                                                                     \n                                                                          \n  # Struct definition                                                     \n  defstruct name: \"\", age: 0, email: nil                                  \n                                                                          \n  # Exception definition                                                  \n  defexception message: \"A custom error occurred\"                         \n                                                                          \n  # Callback definition                                                   \n  @callback process(term) :: {:ok, term} | {:error, term}                 \n  @macrocallback validate(term) :: Macro.t()                              \n                                                                          \n  # Public function                                                       \n  def calculate_age(birth_year) when is_integer(birth_year) do            \n    current_year = DateTime.utc_now().year                                \n    current_year - birth_year                                             \n  end                                                                     \n                                                                          \n  # Private function                                                      \n  defp validate_email(email) do                                           \n    String.match?(email, ~r/^[^\\s]+@[^\\s]+\\.[^\\s]+$/)                     \n  end                                                                     \n                                                                          \n  # Function with pattern matching                                        \n  def handle_result({:ok, value}), do: \"Success: #{value}\"                \n  def handle_result({:error, reason}), do: \"Error: #{reason}\"             \n  def handle_result(_), do: \"Unknown result\"                              \n                                                                          \n  # Macro definition                                                      \n  defmacro debug(expression) do                                           \n    quote do                                                              \n      IO.puts \"Debug: #{inspect(unquote(expression))}\"                    \n    end                                                                   \n  end                                                                     \n                                                                          \n  # Private macro                                                         \n  defmacrop log(message) do                                               \n    quote do                                                              \n      IO.puts(\"[#{__MODULE__}] #{unquote(message)}\")                      \n    end                                                                   \n  end                                                                     \n                                                                          \n  # Guard definition                                                      \n  defguard is_positive(value) when is_integer(value) and value > 0        \n  defguardp is_even(value) when is_integer(value) and rem(value, 2) == 0  \n                                                                          \n  # Function delegation                                                   \n  defdelegate parse_int(string), to: String, as: :to_integer              \n                                                                          \n  # Overridable function                                                  \n  defoverridable [process: 1]                                             \n                                                                          \n  # Using with for complex operations                                     \n  def create_user(params) do                                              \n    with {:ok, name} <- Map.fetch(params, \"name\"),                        \n          {:ok, email} <- Map.fetch(params, \"email\"),                      \n          true <- validate_email(email) do                                 \n      %ExampleApp{name: name, email: email}                               \n    else                                                                  \n      :error -> {:error, \"Missing required fields\"}                       \n      false -> {:error, \"Invalid email format\"}                           \n    end                                                                   \n  end                                                                     \nend   "
  },
  {
    "path": "app/server/syntax/file_map/examples/elm_example.elm",
    "content": "module Example exposing (..)\n\nimport Browser\nimport Html exposing (..)\nimport Html.Attributes exposing (..)\nimport Html.Events exposing (..)\nimport Http\nimport Json.Decode as Decode exposing (Decoder)\nimport Json.Encode as Encode\n\n-- MODEL\n\ntype alias Model =\n    { users : List User\n    , inputText : String\n    , error : Maybe String\n    }\n\ntype alias User =\n    { id : Int\n    , name : String\n    , email : String\n    }\n\ninit : () -> (Model, Cmd Msg)\ninit _ =\n    ( { users = []\n      , inputText = \"\"\n      , error = Nothing\n      }\n    , fetchUsers\n    )\n\n-- UPDATE\n\ntype Msg\n    = GotUsers (Result Http.Error (List User))\n    | InputChanged String\n    | AddUser\n    | UserAdded (Result Http.Error User)\n\nupdate : Msg -> Model -> (Model, Cmd Msg)\nupdate msg model =\n    case msg of\n        GotUsers result ->\n            case result of\n                Ok users ->\n                    ( { model | users = users, error = Nothing }\n                    , Cmd.none\n                    )\n                Err _ ->\n                    ( { model | error = Just \"Failed to fetch users\" }\n                    , Cmd.none\n                    )\n\n        InputChanged text ->\n            ( { model | inputText = text }\n            , Cmd.none\n            )\n\n        AddUser ->\n            ( model\n            , addUser model.inputText\n            )\n\n        UserAdded result ->\n            case result of\n                Ok user ->\n                    ( { model \n                      | users = user :: model.users\n                      , inputText = \"\"\n                      , error = Nothing\n                      }\n                    , Cmd.none\n                    )\n                Err _ ->\n                    ( { model | error = Just \"Failed to add user\" }\n                    , Cmd.none\n                    )\n\n-- HTTP\n\nfetchUsers : Cmd Msg\nfetchUsers =\n    Http.get\n        { url = \"/api/users\"\n        , expect = Http.expectJson GotUsers usersDecoder\n        }\n\naddUser : String -> Cmd Msg\naddUser name =\n    Http.post\n        { url = \"/api/users\"\n        , body = Http.jsonBody (userEncoder name)\n        , expect = Http.expectJson UserAdded userDecoder\n        }\n\n-- JSON\n\nuserEncoder : String -> Encode.Value\nuserEncoder name =\n    Encode.object\n        [ (\"name\", Encode.string name)\n        ]\n\nuserDecoder : Decoder User\nuserDecoder =\n    Decode.map3 User\n        (Decode.field \"id\" Decode.int)\n        (Decode.field \"name\" Decode.string)\n        (Decode.field \"email\" Decode.string)\n\nusersDecoder : Decoder (List User)\nusersDecoder =\n    Decode.list userDecoder\n\n-- VIEW\n\nview : Model -> Html Msg\nview model =\n    div []\n        [ h1 [] [ text \"User Management\" ]\n        , viewError model.error\n        , viewInput model.inputText\n        , viewUsers model.users\n        ]\n\nviewError : Maybe String -> Html Msg\nviewError maybeError =\n    case maybeError of\n        Just error ->\n            div [ class \"error\" ] [ text error ]\n        Nothing ->\n            text \"\"\n\nviewInput : String -> Html Msg\nviewInput inputText =\n    div []\n        [ input\n            [ value inputText\n            , onInput InputChanged\n            , placeholder \"Enter user name\"\n            ] []\n        , button [ onClick AddUser ] [ text \"Add User\" ]\n        ]\n\nviewUsers : List User -> Html Msg\nviewUsers users =\n    div []\n        [ h2 [] [ text \"Users\" ]\n        , ul [] (List.map viewUser users)\n        ]\n\nviewUser : User -> Html Msg\nviewUser user =\n    li []\n        [ text (user.name ++ \" (\" ++ user.email ++ \")\")\n        ]\n\n-- MAIN\n\nmain : Program () Model Msg\nmain =\n    Browser.element\n        { init = init\n        , update = update\n        , view = view\n        , subscriptions = \\_ -> Sub.none\n        }\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/go_example.go",
    "content": "//go:build ignore\n\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Interface definition\ntype DataProcessor interface {\n\tProcess(ctx context.Context, data interface{}) error\n\tValidate(data interface{}) bool\n}\n\n// Custom error type\ntype ValidationError struct {\n\tField   string\n\tMessage string\n}\n\nfunc (e *ValidationError) Error() string {\n\treturn fmt.Sprintf(\"validation error on field %s: %s\", e.Field, e.Message)\n}\n\n// Struct with embedded type and tags\ntype User struct {\n\tsync.Mutex\n\tID        int64     `json:\"id\"`\n\tName      string    `json:\"name\"`\n\tEmail     string    `json:\"email\"`\n\tCreatedAt time.Time `json:\"created_at\"`\n\tUpdatedAt time.Time `json:\"updated_at,omitempty\"`\n}\n\n// Type alias and constants\ntype UserID = int64\n\nconst (\n\tMaxRetries   = 3\n\tDefaultLimit = 100\n)\n\n// single line const\nconst singleLineConst string = \"single line const\"\n\n// Global variables\nvar (\n\tdefaultTimeout = time.Second * 30\n\tprocessor      DataProcessor\n)\n\n// single line var\nvar singleLineVar string = \"single line var\"\n\n// Generic type\ntype Result[T any] struct {\n\tData    T\n\tError   error\n\tRetries int\n}\n\n// Implementation of DataProcessor\ntype UserProcessor struct {\n\tcache map[UserID]*User\n\tmu    sync.RWMutex\n}\n\nfunc NewUserProcessor() *UserProcessor {\n\treturn &UserProcessor{\n\t\tcache: make(map[UserID]*User),\n\t}\n}\n\nfunc (p *UserProcessor) Process(ctx context.Context, data interface{}) error {\n\tuser, ok := data.(*User)\n\tif !ok {\n\t\treturn fmt.Errorf(\"invalid data type: expected *User\")\n\t}\n\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tp.cache[user.ID] = user\n\treturn nil\n}\n\nfunc (p *UserProcessor) Validate(data interface{}) bool {\n\tuser, ok := data.(*User)\n\treturn ok && user.Name != \"\" && user.Email != \"\"\n}\n\n// Channel operations\nfunc processUsers(ctx context.Context, users <-chan *User) <-chan *Result[*User] {\n\tresults := make(chan *Result[*User])\n\n\tgo func() {\n\t\tdefer close(results)\n\t\tfor user := range users {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase results <- &Result[*User]{Data: user}:\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn results\n}\n\n// Function with multiple return values and named returns\nfunc createUser(name, email string) (user *User, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"panic recovered: %v\", r)\n\t\t}\n\t}()\n\n\tuser = &User{\n\t\tName:      name,\n\t\tEmail:     email,\n\t\tCreatedAt: time.Now(),\n\t}\n\n\tif !processor.Validate(user) {\n\t\treturn nil, &ValidationError{Field: \"user\", Message: \"invalid user data\"}\n\t}\n\n\treturn user, nil\n}\n\nfunc main() {\n\tctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)\n\tdefer cancel()\n\n\tprocessor = NewUserProcessor()\n\n\tusers := make(chan *User)\n\tresults := processUsers(ctx, users)\n\n\t// Anonymous struct\n\tconfig := struct {\n\t\tWorkers int\n\t\tBuffer  int\n\t}{\n\t\tWorkers: 3,\n\t\tBuffer:  10,\n\t}\n\n\tvar wg sync.WaitGroup\n\twg.Add(config.Workers)\n\n\tfor i := 0; i < config.Workers; i++ {\n\t\tgo func(id int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor result := range results {\n\t\t\t\tif result.Error != nil {\n\t\t\t\t\tlog.Printf(\"Worker %d: Error processing user: %v\", id, result.Error)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"Worker %d: Processed user: %+v\", id, result.Data)\n\t\t\t}\n\t\t}(i)\n\t}\n\n\t// Cleanup\n\tclose(users)\n\twg.Wait()\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/groovy_example.groovy",
    "content": "#!/usr/bin/env groovy\n\n// Trait definition\ntrait Loggable {\n    def log(String message) {\n        println \"[${new Date()}] $message\"\n    }\n}\n\n// Abstract class definition\nabstract class Vehicle {\n    String make\n    String model\n    Integer year\n    \n    abstract void start()\n    abstract void stop()\n}\n\n// Class implementation - fixed inheritance syntax\nclass Car extends Vehicle implements Loggable {\n    // Properties with type definitions\n    private BigDecimal price\n    protected Boolean running = false\n    \n    // Static fields with proper type declarations\n    static final String MANUFACTURER = \"Generic Motors\"\n    static int carCount = 0\n    \n    // Constructor - fixed parameter initialization\n    Car(String make = 'Unknown', String model = 'Generic', Integer year = 2024) {\n        this.make = make\n        this.model = model\n        this.year = year\n        carCount++\n    }\n    \n    // Lazy property evaluation\n    @Lazy String fullName = \"$make $model ($year)\"\n    \n    // Method implementation with synchronized block\n    void start() {\n        running = true\n        log(\"Starting ${fullName}\")\n        synchronized(this) {\n            println(\"Engine started\")\n        }\n    }\n    \n    void stop() {\n        running = false\n        log(\"Stopping ${fullName}\")\n    }\n    \n    // Operator overloading - fixed map construction\n    def plus(Car other) {\n        return new Car(\n            make: \"${this.make}-${other.make}\",\n            model: \"${this.model}-${other.model}\",\n            year: Math.max(this.year, other.year)\n        )\n    }\n    \n    // Property accessors\n    private BigDecimal _price\n    \n    void setPrice(BigDecimal price) {\n        if (price < 0) {\n            throw new IllegalArgumentException(\"Price cannot be negative\")\n        }\n        this._price = price\n    }\n    \n    BigDecimal getPrice() {\n        return _price\n    }\n}\n\n// Enum definition\nenum Status {\n    ACTIVE('A'),\n    INACTIVE('I'),\n    PENDING('P')\n    \n    final String code\n    \n    private Status(String code) {\n        this.code = code\n    }\n    \n    @Override\n    String toString() {\n        return code\n    }\n}\n\n// Category class with explicit return\nclass StringExtensions {\n    static String truncate(String self, Integer length) {\n        return self.size() <= length ? self : self[0..<length] + \"...\"\n    }\n}\n\n// Builder pattern class with proper return statements\nclass EmailBuilder {\n    private Map email = [:]\n    \n    EmailBuilder to(String recipient) {\n        email.to = recipient\n        return this\n    }\n    \n    EmailBuilder from(String sender) {\n        email.from = sender\n        return this\n    }\n    \n    EmailBuilder subject(String subject) {\n        email.subject = subject\n        return this\n    }\n    \n    EmailBuilder body(@DelegatesTo(StringBuilder) Closure body) {\n        def builder = new StringBuilder()\n        body.delegate = builder\n        body.resolveStrategy = Closure.DELEGATE_FIRST\n        body()\n        email.body = builder.toString()\n        return this\n    }\n    \n    Map build() {\n        return email.clone() as Map\n    }\n}\n\n// Main execution with proper closure syntax\ndef main() {\n    use(StringExtensions) {\n        def description = \"This is a very long description that needs truncating\"\n        println(description.truncate(20))\n    }\n    \n    def car = new Car(make: \"Tesla\", model: \"Model S\")\n    car.start()\n    \n    def email = new EmailBuilder()\n        .to(\"recipient@example.com\")\n        .from(\"sender@example.com\")\n        .subject(\"Test Email\")\n        .body {\n            append(\"Hello\\n\")\n            append(\"This is a test email.\\n\")\n            append(\"Regards\")\n        }\n        .build()\n    \n    println(email)\n}\n\n// Script execution with proper binding check\nif (this.binding.hasVariable('main')) {\n    main()\n}"
  },
  {
    "path": "app/server/syntax/file_map/examples/hcl_example.hcl",
    "content": "# Variable definitions\nvariable \"environment\" {\n  type        = string\n  description = \"Deployment environment\"\n  default     = \"development\"\n  validation {\n    condition     = contains([\"development\", \"staging\", \"production\"], var.environment)\n    error_message = \"Environment must be development, staging, or production.\"\n  }\n}\n\n# Local values\nlocals {\n  common_tags = {\n    Environment = var.environment\n    Project     = \"example\"\n    ManagedBy  = \"terraform\"\n  }\n  \n  region_config = {\n    us-east-1 = {\n      instance_type = \"t3.micro\"\n      az_count      = 2\n    }\n    us-west-2 = {\n      instance_type = \"t3.small\"\n      az_count      = 3\n    }\n  }\n}\n\n# Provider configuration\nprovider \"aws\" {\n  region = \"us-west-2\"\n  \n  assume_role {\n    role_arn = \"arn:aws:iam::123456789012:role/terraform\"\n  }\n  \n  default_tags {\n    tags = local.common_tags\n  }\n}\n\n# Data source\ndata \"aws_availability_zones\" \"available\" {\n  state = \"available\"\n}\n\n# Resource block with dynamic blocks\nresource \"aws_security_group\" \"example\" {\n  name_prefix = \"example-sg\"\n  vpc_id      = aws_vpc.main.id\n  \n  dynamic \"ingress\" {\n    for_each = var.service_ports\n    content {\n      from_port   = ingress.value\n      to_port     = ingress.value\n      protocol    = \"tcp\"\n      cidr_blocks = [\"0.0.0.0/0\"]\n    }\n  }\n  \n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n  \n  lifecycle {\n    create_before_destroy = true\n  }\n}\n\n# Module usage\nmodule \"vpc\" {\n  source = \"terraform-aws-modules/vpc/aws\"\n  \n  name = \"example-vpc\"\n  cidr = \"10.0.0.0/16\"\n  \n  azs             = data.aws_availability_zones.available.names\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\"]\n  \n  enable_nat_gateway = true\n  single_nat_gateway = true\n  \n  tags = merge(\n    local.common_tags,\n    {\n      Name = \"example-vpc\"\n    }\n  )\n}\n\n# Output values\noutput \"vpc_id\" {\n  description = \"The ID of the VPC\"\n  value       = module.vpc.vpc_id\n}\n\noutput \"private_subnets\" {\n  description = \"List of private subnet IDs\"\n  value       = module.vpc.private_subnets\n}\n\n# Terraform settings block\nterraform {\n  required_version = \">= 1.0.0\"\n  \n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 4.0\"\n    }\n  }\n  \n  backend \"s3\" {\n    bucket         = \"terraform-state-example\"\n    key            = \"example/terraform.tfstate\"\n    region         = \"us-west-2\"\n    encrypt        = true\n    dynamodb_table = \"terraform-locks\"\n  }\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/html_example.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <!-- Example HTML file demonstrating various HTML syntax elements -->\n    <title>HTML Syntax Example</title>\n    <link rel=\"stylesheet\" href=\"styles.css\">\n    <script defer src=\"main.js\"></script>\n</head>\n\n<body>\n    <!-- Header section with navigation -->\n    <header class=\"main-header\" role=\"banner\">\n        <nav class=\"main-nav\" aria-label=\"Main navigation\">\n            <ul class=\"nav-list\">\n                <li><a href=\"#home\" class=\"nav-link active\">Home</a></li>\n                <li><a href=\"#about\" class=\"nav-link\">About</a></li>\n                <li><a href=\"#contact\" class=\"nav-link\">Contact</a></li>\n            </ul>\n        </nav>\n    </header>\n\n    <!-- Main content area -->\n    <main id=\"main-content\" role=\"main\">\n        <!-- Hero section -->\n        <section class=\"hero\" aria-labelledby=\"hero-title\">\n            <h1 id=\"hero-title\">Welcome to Our Site</h1>\n            <p class=\"lead\">This is an example of semantic HTML structure.</p>\n        </section>\n\n        <!-- Article with sections -->\n        <article class=\"content-article\">\n            <header>\n                <h2>Main Article</h2>\n                <p class=\"article-meta\">Published on <time datetime=\"2024-02-20\">February 20, 2024</time></p>\n            </header>\n\n            <section class=\"article-section\">\n                <h3>Section One</h3>\n                <p>This section demonstrates text content with <em>emphasis</em> and <strong>strong importance</strong>.</p>\n                <figure>\n                    <img src=\"example.jpg\" alt=\"Example image\" width=\"600\" height=\"400\">\n                    <figcaption>An example image with caption</figcaption>\n                </figure>\n            </section>\n\n            <section class=\"article-section\">\n                <h3>Interactive Elements</h3>\n                <!-- Form example -->\n                <form class=\"contact-form\" action=\"/submit\" method=\"POST\">\n                    <fieldset>\n                        <legend>Contact Information</legend>\n                        \n                        <div class=\"form-group\">\n                            <label for=\"name\">Name:</label>\n                            <input type=\"text\" id=\"name\" name=\"name\" required \n                                   placeholder=\"Enter your name\">\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"email\">Email:</label>\n                            <input type=\"email\" id=\"email\" name=\"email\" required \n                                   placeholder=\"Enter your email\">\n                        </div>\n\n                        <div class=\"form-group\">\n                            <label for=\"message\">Message:</label>\n                            <textarea id=\"message\" name=\"message\" rows=\"4\" \n                                    placeholder=\"Your message\"></textarea>\n                        </div>\n\n                        <button type=\"submit\" class=\"btn btn-primary\">Send Message</button>\n                    </fieldset>\n                </form>\n            </section>\n\n            <!-- Table example -->\n            <section class=\"article-section\">\n                <h3>Data Table</h3>\n                <table class=\"data-table\">\n                    <caption>Monthly Sales Data</caption>\n                    <thead>\n                        <tr>\n                            <th scope=\"col\">Month</th>\n                            <th scope=\"col\">Sales</th>\n                            <th scope=\"col\">Growth</th>\n                        </tr>\n                    </thead>\n                    <tbody>\n                        <tr>\n                            <th scope=\"row\">January</th>\n                            <td>$10,000</td>\n                            <td>5%</td>\n                        </tr>\n                        <tr>\n                            <th scope=\"row\">February</th>\n                            <td>$12,000</td>\n                            <td>20%</td>\n                        </tr>\n                    </tbody>\n                    <tfoot>\n                        <tr>\n                            <th scope=\"row\">Total</th>\n                            <td>$22,000</td>\n                            <td>25%</td>\n                        </tr>\n                    </tfoot>\n                </table>\n            </section>\n        </article>\n\n        <!-- Aside content -->\n        <aside class=\"sidebar\" role=\"complementary\">\n            <h2>Related Information</h2>\n            <div class=\"widget\">\n                <h3>Categories</h3>\n                <ul class=\"category-list\">\n                    <li><a href=\"#tech\">Technology</a></li>\n                    <li><a href=\"#design\">Design</a></li>\n                    <li><a href=\"#business\">Business</a></li>\n                </ul>\n            </div>\n        </aside>\n    </main>\n\n    <!-- Footer section -->\n    <footer class=\"site-footer\" role=\"contentinfo\">\n        <div class=\"footer-content\">\n            <p>&copy; 2024 Example Site. All rights reserved.</p>\n            <address>\n                Contact us at: <a href=\"mailto:info@example.com\">info@example.com</a>\n            </address>\n        </div>\n    </footer>\n\n    <!-- Dialog for modal content -->\n    <dialog id=\"modal\" class=\"modal\">\n        <header>\n            <h2>Modal Title</h2>\n            <button type=\"button\" class=\"close-button\" aria-label=\"Close modal\">×</button>\n        </header>\n        <div class=\"modal-content\">\n            <p>This is an example of the dialog element for modal content.</p>\n        </div>\n        <footer>\n            <button type=\"button\" class=\"btn\">Close</button>\n        </footer>\n    </dialog>\n</body>\n</html>\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/java_example.java",
    "content": "package example;\n\nimport java.util.*;\nimport java.util.concurrent.*;\nimport java.util.function.*;\nimport java.util.stream.*;\nimport java.time.*;\n\n// Generic interface with type bounds\ninterface DataProcessor<T extends Comparable<T>> {\n    CompletableFuture<T> processAsync(T input);\n    boolean validate(T input);\n}\n\n// Enum with methods and fields\nenum Status {\n    PENDING(\"P\"),\n    ACTIVE(\"A\"),\n    COMPLETED(\"C\"),\n    FAILED(\"F\");\n\n    private final String code;\n\n    Status(String code) {\n        this.code = code;\n    }\n\n    public String getCode() {\n        return code;\n    }\n}\n\n// Abstract class with generic type\nabstract class BaseEntity<ID> {\n    protected ID id;\n    protected LocalDateTime createdAt;\n    protected LocalDateTime updatedAt;\n\n    public abstract void validate();\n}\n\n// Record type (Java 16+)\nrecord UserDTO(\n    String name,\n    String email,\n    Set<String> roles\n) {}\n\n// Annotation definition\n@interface Audited {\n    String value() default \"\";\n    boolean required() default true;\n}\n\n// Main class with various Java features\npublic class Example extends BaseEntity<UUID> implements DataProcessor<String> {\n    // Static fields and initialization block\n    private static final int MAX_RETRIES = 3;\n    private static final Map<String, Integer> CACHE;\n    \n    static {\n        CACHE = new ConcurrentHashMap<>();\n    }\n\n    // Instance fields with different access modifiers\n    private final Queue<String> queue;\n    protected Status status;\n    @Audited\n    public String name;\n\n    // Constructor with builder pattern\n    private Example(Builder builder) {\n        this.queue = new LinkedBlockingQueue<>();\n        this.status = Status.PENDING;\n        this.name = builder.name;\n    }\n\n    // Builder static class\n    public static class Builder {\n        private String name;\n\n        public Builder name(String name) {\n            this.name = name;\n            return this;\n        }\n\n        public Example build() {\n            return new Example(this);\n        }\n    }\n\n    // Interface implementation\n    @Override\n    public CompletableFuture<String> processAsync(String input) {\n        return CompletableFuture.supplyAsync(() -> {\n            try {\n                queue.offer(input);\n                return input.toUpperCase();\n            } catch (Exception e) {\n                throw new CompletionException(e);\n            }\n        });\n    }\n\n    @Override\n    public boolean validate(String input) {\n        return input != null && !input.isEmpty();\n    }\n\n    // Abstract method implementation\n    @Override\n    public void validate() {\n        if (name == null || name.isEmpty()) {\n            throw new IllegalStateException(\"Name is required\");\n        }\n    }\n\n    // Generic method with wildcards\n    public <T extends Comparable<? super T>> List<T> sort(Collection<T> items) {\n        return items.stream()\n                   .sorted()\n                   .collect(Collectors.toList());\n    }\n\n    // Method with functional interfaces\n    public void processItems(\n        List<String> items,\n        Predicate<String> filter,\n        Consumer<String> processor\n    ) {\n        items.stream()\n             .filter(filter)\n             .forEach(processor);\n    }\n\n    // Exception class\n    public static class ProcessingException extends RuntimeException {\n        public ProcessingException(String message) {\n            super(message);\n        }\n    }\n\n    // Main method demonstrating usage\n    public static void main(String[] args) {\n        var example = new Builder()\n            .name(\"Test Example\")\n            .build();\n\n        // Lambda and method reference usage\n        List<String> items = Arrays.asList(\"a\", \"b\", \"c\");\n        example.processItems(\n            items,\n            String::isEmpty,\n            System.out::println\n        );\n\n        // Stream API usage\n        Map<Status, Long> statusCounts = items.stream()\n            .map(s -> Status.PENDING)\n            .collect(Collectors.groupingBy(\n                status -> status,\n                Collectors.counting()\n            ));\n\n        // CompletableFuture with exception handling\n        example.processAsync(\"test\")\n              .thenApply(String::toLowerCase)\n              .exceptionally(throwable -> {\n                  System.err.println(\"Error: \" + throwable.getMessage());\n                  return \"\";\n              });\n\n        // Try-with-resources and Optional usage\n        try (var scanner = new Scanner(System.in)) {\n            Optional.of(scanner.nextLine())\n                    .filter(example::validate)\n                    .ifPresent(example::processAsync);\n        }\n    }\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/javascript_example.js",
    "content": "// ES Module imports\nimport { EventEmitter } from 'events';\nimport { promisify } from 'util';\n\n// Global constants\nconst MAX_RETRIES = 3;\nconst DEFAULT_TIMEOUT = 5000;\n\n// Symbol for private properties\nconst privateState = Symbol('privateState');\n\n// Class using ES6+ features\nclass DataProcessor extends EventEmitter {\n    // Private class field\n    #cache = new Map();\n    \n    // Static class field\n    static version = '1.0.0';\n    \n    // Constructor with parameter destructuring\n    constructor({ maxRetries = MAX_RETRIES, timeout = DEFAULT_TIMEOUT } = {}) {\n        super();\n        this[privateState] = { maxRetries, timeout };\n    }\n    \n    // Async method with error handling\n    async processData(data) {\n        try {\n            const result = await this.#validateAndTransform(data);\n            this.emit('processed', result);\n            return result;\n        } catch (error) {\n            this.emit('error', error);\n            throw error;\n        }\n    }\n    \n    // Private method\n    async #validateAndTransform(data) {\n        if (!data) throw new Error('Data is required');\n        return { ...data, timestamp: Date.now() };\n    }\n    \n    // Generator method\n    *iterateCache() {\n        for (const [key, value] of this.#cache) {\n            yield { key, value };\n        }\n    }\n}\n\n\n// Decorator function (stage 3 proposal)\nfunction deprecated(target, context) {\n    if (context.kind === 'method') {\n        const originalMethod = target;\n        return function(...args) {\n            console.warn(`Warning: ${context.name} is deprecated`);\n            return originalMethod.apply(this, args);\n        };\n    }\n}\n\n// Proxy example\nconst handler = {\n    get(target, prop) {\n        return prop in target ? target[prop] : 'Property not found';\n    }\n};\n\nconst proxy = new Proxy({}, handler);\n\n// Promise-based utility function\nconst delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n\n// Async generator function\nasync function* generateSequence(start, end) {\n    for (let i = start; i <= end; i++) {\n        await delay(100);\n        yield i;\n    }\n}\n\n// Higher-order function\nconst memoize = (fn) => {\n    const cache = new Map();\n    return (...args) => {\n        const key = JSON.stringify(args);\n        if (cache.has(key)) return cache.get(key);\n        const result = fn.apply(this, args);\n        cache.set(key, result);\n        return result;\n    };\n};\n\n// Custom error class\nclass ValidationError extends Error {\n    constructor(message, field) {\n        super(message);\n        this.name = 'ValidationError';\n        this.field = field;\n    }\n}\n\n// Object with getter/setter\nconst config = {\n    _theme: 'light',\n    get theme() {\n        return this._theme;\n    },\n    set theme(value) {\n        if (!['light', 'dark'].includes(value)) {\n            throw new ValidationError('Invalid theme', 'theme');\n        }\n        this._theme = value;\n    }\n};\n\n// Array methods and destructuring\nconst processItems = (items) => {\n    const [first, ...rest] = items;\n    return rest\n        .filter(item => item != null)\n        .map(item => ({ ...item, processed: true }))\n        .reduce((acc, curr) => {\n            acc[curr.id] = curr;\n            return acc;\n        }, {});\n};\n\n// Async/await with Promise.all\nconst fetchData = async (urls) => {\n    try {\n        const responses = await Promise.all(\n            urls.map(url => fetch(url).then(res => res.json()))\n        );\n        return responses;\n    } catch (error) {\n        console.error('Failed to fetch data:', error);\n        throw error;\n    }\n};\n\n// Export statement\nexport {\n    DataProcessor,\n    ValidationError,\n    processItems,\n    fetchData,\n    delay,\n    memoize,\n    config\n};\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/kotlin_example.kt",
    "content": "// Package declaration\npackage example\n\n// Imports\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport java.time.LocalDateTime\nimport kotlin.properties.Delegates\n\n// Interface with generic type parameter\ninterface DataProcessor<T> {\n    suspend fun process(data: T): Result<T>\n    fun validate(data: T): Boolean\n}\n\n// Sealed class for representing states\nsealed class ProcessingState<out T> {\n    object Loading : ProcessingState<Nothing>()\n    data class Success<T>(val data: T) : ProcessingState<T>()\n    data class Error(val exception: Throwable) : ProcessingState<Nothing>()\n}\n\n// Data class with default parameters\ndata class User(\n    val id: String,\n    val name: String,\n    val email: String,\n    val roles: Set<Role> = emptySet(),\n    val createdAt: LocalDateTime = LocalDateTime.now()\n)\n\n// Enum class with properties and function\nenum class Role(val permission: Int) {\n    ADMIN(0xFF) {\n        override fun toString() = \"Administrator\"\n    },\n    USER(0x0F) {\n        override fun toString() = \"Regular User\"\n    },\n    GUEST(0x00) {\n        override fun toString() = \"Guest User\"\n    }\n}\n\n// Object declaration (Singleton)\nobject Configuration {\n    const val API_VERSION = \"1.0.0\"\n    val defaultTimeout = 5000L\n    \n    fun getConfig(key: String) = config[key]\n    \n    private val config = mutableMapOf<String, Any>()\n}\n\n// Class with companion object and delegation\nclass UserProcessor : DataProcessor<User> {\n    // Companion object with factory method\n    companion object {\n        fun create(): UserProcessor = UserProcessor()\n    }\n    \n    // Property delegation\n    private var processingCount: Int by Delegates.observable(0) { _, old, new ->\n        println(\"Processing count changed from $old to $new\")\n    }\n    \n    // Property with custom getter\n    val isActive: Boolean\n        get() = processingCount > 0\n    \n    // Suspending function implementation\n    override suspend fun process(data: User): Result<User> = runCatching {\n        processingCount++\n        validateEmail(data.email)\n        data\n    }.also {\n        processingCount--\n    }\n    \n    // Regular function implementation\n    override fun validate(data: User): Boolean =\n        data.email.isNotBlank() && data.name.isNotBlank()\n    \n    // Extension function\n    private fun String.isValidEmail(): Boolean =\n        matches(Regex(\"^[A-Za-z0-9+_.-]+@(.+)$\"))\n    \n    // Inline function with reified type parameter\n    inline fun <reified T> logType() =\n        println(\"Processing type: ${T::class.simpleName}\")\n    \n    // Private function using extension function\n    private fun validateEmail(email: String) {\n        require(email.isValidEmail()) { \"Invalid email format\" }\n    }\n}\n\n// Higher-order function with function type parameter\nfun <T> withRetry(\n    times: Int = 3,\n    action: suspend () -> T\n): suspend () -> T = {\n    var lastException: Exception? = null\n    repeat(times) { attempt ->\n        try {\n            return@withRetry action()\n        } catch (e: Exception) {\n            lastException = e\n            println(\"Attempt ${attempt + 1} failed: ${e.message}\")\n        }\n    }\n    throw lastException ?: IllegalStateException(\"All attempts failed\")\n}\n\n// Coroutine scope extension\nfun CoroutineScope.processUsers(users: List<User>): Flow<ProcessingState<User>> = flow {\n    val processor = UserProcessor.create()\n    \n    emit(ProcessingState.Loading)\n    \n    users.forEach { user ->\n        processor.process(user)\n            .onSuccess { emit(ProcessingState.Success(it)) }\n            .onFailure { emit(ProcessingState.Error(it)) }\n    }\n}\n\n// Extension property\nval User.displayName: String\n    get() = \"$name (${email})\"\n\n// Main function demonstrating usage\nsuspend fun main() = coroutineScope {\n    val users = listOf(\n        User(\"1\", \"John Doe\", \"john@example.com\"),\n        User(\"2\", \"Jane Smith\", \"jane@example.com\", setOf(Role.ADMIN))\n    )\n    \n    launch {\n        processUsers(users)\n            .collect { state ->\n                when (state) {\n                    is ProcessingState.Loading -> println(\"Processing started\")\n                    is ProcessingState.Success -> println(\"Processed: ${state.data.displayName}\")\n                    is ProcessingState.Error -> println(\"Error: ${state.exception.message}\")\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/lua_example.lua",
    "content": "-- Module definition\nlocal Example = {}\n\n-- Constants\nlocal MAX_RETRIES = 3\nlocal DEFAULT_TIMEOUT = 5000\n\n-- Private functions (local)\nlocal function validateInput(input)\n    if type(input) ~= \"string\" then\n        error(\"Input must be a string\")\n    end\n    return true\nend\n\n-- Metatable for creating classes\nlocal function createClass(name)\n    local cls = {}\n    cls.__index = cls\n    cls.__name = name\n    \n    -- Constructor\n    function cls.new(...)\n        local self = setmetatable({}, cls)\n        if self.init then\n            self:init(...)\n        end\n        return self\n    end\n    \n    return cls\nend\n\n-- Class definition using metatables\nlocal User = createClass(\"User\")\n\nfunction User:init(name, age)\n    self.name = name\n    self.age = age\n    self.created_at = os.time()\nend\n\nfunction User:toString()\n    return string.format(\"User(%s, %d)\", self.name, self.age)\nend\n\n-- Table with custom metamethods\nlocal DataStore = {\n    data = {},\n    __newindex = function(t, k, v)\n        print(\"Setting value:\", k, v)\n        rawset(t.data, k, v)\n    end,\n    __index = function(t, k)\n        return t.data[k]\n    end\n}\nsetmetatable(DataStore, DataStore)\n\n-- Coroutine example\nlocal function producer()\n    return coroutine.create(function()\n        for i = 1, 5 do\n            coroutine.yield(i)\n        end\n    end)\nend\n\n-- Iterator function\nlocal function range(from, to, step)\n    step = step or 1\n    local i = from - step\n    return function()\n        i = i + step\n        if i <= to then\n            return i\n        end\n    end\nend\n\n-- Module functions\nfunction Example.process(input)\n    assert(validateInput(input))\n    \n    local result = {\n        original = input,\n        processed = string.upper(input),\n        timestamp = os.time()\n    }\n    \n    return result\nend\n\n-- Function with multiple returns\nfunction Example.divide(a, b)\n    if b == 0 then\n        return nil, \"Division by zero\"\n    end\n    return a / b\nend\n\n-- Closure example\nfunction Example.counter(initial)\n    local count = initial or 0\n    return function()\n        count = count + 1\n        return count\n    end\nend\n\n-- Table manipulation\nfunction Example.merge(t1, t2)\n    local result = {}\n    for k, v in pairs(t1) do\n        result[k] = v\n    end\n    for k, v in pairs(t2) do\n        result[k] = v\n    end\n    return result\nend\n\n-- Pattern matching example\nfunction Example.extractEmails(text)\n    local emails = {}\n    for email in string.gmatch(text, \"[%w%.%-_]+@[%w%.%-_]+%.%w+\") do\n        table.insert(emails, email)\n    end\n    return emails\nend\n\n-- Event handling system\nlocal EventEmitter = createClass(\"EventEmitter\")\n\nfunction EventEmitter:init()\n    self.handlers = {}\nend\n\nfunction EventEmitter:on(event, handler)\n    self.handlers[event] = self.handlers[event] or {}\n    table.insert(self.handlers[event], handler)\nend\n\nfunction EventEmitter:emit(event, ...)\n    if self.handlers[event] then\n        for _, handler in ipairs(self.handlers[event]) do\n            handler(...)\n        end\n    end\nend\n\n-- Add classes to module\nExample.User = User\nExample.EventEmitter = EventEmitter\n\n-- Module return\nreturn Example\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/markdown_example.md",
    "content": "# SoundScape 🎵\n\nAn AI-powered music visualization and generation platform that creates real-time visual art from audio input.\n\n## Features ✨\n\n- Real-time audio processing using WebAudio API\n- Dynamic visualization generation using Three.js\n- AI-powered music analysis for enhanced visual mapping\n- Multiple visualization styles (geometric, particle, liquid simulation)\n- Audio recording and export capabilities\n- Collaborative mode for live performances\n\n## Getting Started 🚀\n\n### Prerequisites\n\n- Node.js (v18 or higher)\n- GPU with WebGL 2.0 support\n- Microphone access (for live input)\n\n### Installation\n\n1. Clone the repository:\n```bash\ngit clone https://github.com/yourusername/soundscape.git\ncd soundscape\n```\n\n2. Install dependencies:\n```bash\nnpm install\n```\n\n3. Start the development server:\n```bash\nnpm run dev\n```\n\nThe application will be available at `http://localhost:3000`\n\n## Architecture 🏗️\n\nSoundScape uses a modular architecture with the following core components:\n\n- **AudioEngine**: Handles audio input processing and analysis\n- **VisualizationCore**: Manages the 3D rendering pipeline\n- **AIProcessor**: Processes audio features for enhanced visualization\n- **StateManager**: Handles application state and user preferences\n\n## API Reference 📚\n\n### Audio Processing\n\n```typescript\ninterface AudioProcessor {\n  analyze(input: AudioBuffer): AudioFeatures;\n  extractBeat(features: AudioFeatures): BeatPattern;\n  generateVisuals(pattern: BeatPattern): Scene;\n}\n```\n\n### Visualization\n\n```typescript\ninterface VisualizationStyle {\n  name: string;\n  parameters: VisualParameters;\n  render(scene: Scene): void;\n  updateParams(params: Partial<VisualParameters>): void;\n}\n```\n\n## Contributing 🤝\n\nWe welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting a pull request.\n\n### Development Workflow\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n## Performance Optimization Tips 💡\n\n- Use Web Workers for heavy audio processing\n- Implement lazy loading for visualization styles\n- Enable GPU acceleration when available\n- Cache frequently used audio features\n- Optimize render loops for smooth performance\n\n## License 📄\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## Acknowledgments 🙏\n\n- Three.js community for 3D rendering support\n- TensorFlow.js team for machine learning capabilities\n- Web Audio API working group\n- All our amazing contributors\n\n## Contact 📧\n\nProject Lead - [@projectlead](https://twitter.com/projectlead)\n\nProject Link: [https://github.com/yourusername/soundscape](https://github.com/yourusername/soundscape)\n\n---\n\nMade with ❤️ by the SoundScape Team"
  },
  {
    "path": "app/server/syntax/file_map/examples/ocaml_example.ml",
    "content": "(* Module signature *)\nmodule type DataProcessor = sig\n  type 'a t\n  val create : unit -> 'a t\n  val process : 'a t -> 'a -> ('a, string) result\n  val validate : 'a -> bool\nend\n\n(* Module implementation *)\nmodule StringProcessor : DataProcessor with type 'a = string = struct\ntype 'a = string\n  type t = {\n    mutable processed_count: int;\n    created_at: float;\n  }\n\n  let create () = {\n    processed_count = 0;\n    created_at = Unix.time ();\n  }\n\n  let process t input =\n    t.processed_count <- t.processed_count + 1;\n    if String.length input > 0 then\n      Ok (String.uppercase_ascii input)\n    else\n      Error \"Empty input\"\n\n  let validate input =\n    String.length input > 0\nend\n\n(* Type definitions *)\ntype user = {\n  id: int;\n  name: string;\n  email: string;\n  created_at: float;\n}\n\ntype 'a result = \n  | Success of 'a \n  | Error of string\n\n(* Variant type *)\ntype message =\n  | Text of string\n  | Number of int\n  | Tuple of string * int\n  | Record of user\n\n(* Functor definition *)\nmodule type Comparable = sig\n  type t\n  val compare : t -> t -> int\nend\n\nmodule MakeSet (Item : Comparable) = struct\n  type element = Item.t\n  type t = element list\n\n  let empty = []\n  \n  let rec add x = function\n    | [] -> [x]\n    | hd :: tl as l ->\n        match Item.compare x hd with\n        | 0 -> l\n        | n when n < 0 -> x :: l\n        | _ -> hd :: add x tl\n\n  let member x set =\n    List.exists (fun y -> Item.compare x y = 0) set\nend\n\n(* Exception definition *)\nexception ValidationError of string\n\n(* Higher-order function *)\nlet memoize f =\n  let cache = Hashtbl.create 16 in\n  fun x ->\n    try Hashtbl.find cache x\n    with Not_found ->\n      let result = f x in\n      Hashtbl.add cache x result;\n      result\n\n(* Object-oriented features *)\nclass virtual ['a] queue = object(self)\n  val mutable items = []\n  \n  method virtual push : 'a -> unit\n  method virtual pop : 'a option\n  \n  method size = List.length items\n  \n  method is_empty = items = []\n  \n  method protected get_items = items\n  method protected set_items new_items = items <- new_items\nend\n\nclass ['a] fifo_queue = object(self)\n  inherit ['a] queue\n\n  method push item =\n    self#set_items (self#get_items @ [item])\n\n  method pop =\n    match self#get_items with\n    | [] -> None\n    | hd::tl ->\n        self#set_items tl;\n        Some hd\nend\n\n(* Module for handling JSON-like data *)\nmodule Json = struct\n  type t =\n    | Null\n    | Bool of bool\n    | Number of float\n    | String of string\n    | Array of t list\n    | Object of (string * t) list\n\n  let rec to_string = function\n    | Null -> \"null\"\n    | Bool b -> string_of_bool b\n    | Number n -> string_of_float n\n    | String s -> \"\\\"\" ^ String.escaped s ^ \"\\\"\"\n    | Array items ->\n        \"[\" ^ String.concat \", \" (List.map to_string items) ^ \"]\"\n    | Object pairs ->\n        let pair_to_string (k, v) =\n          \"\\\"\" ^ String.escaped k ^ \"\\\": \" ^ to_string v\n        in\n        \"{\" ^ String.concat \", \" (List.map pair_to_string pairs) ^ \"}\"\nend\n\n(* Main execution *)\nlet () =\n  let processor = StringProcessor.create () in\n  let result = StringProcessor.process processor \"hello world\" in\n  match result with\n  | Ok processed -> Printf.printf \"Processed: %s\\n\" processed\n  | Error msg -> Printf.eprintf \"Error: %s\\n\" msg;\n\n  let queue = new fifo_queue in\n  queue#push 1;\n  queue#push 2;\n  queue#push 3;\n  \n  let rec print_queue () =\n    match queue#pop with\n    | Some item -> \n        Printf.printf \"Item: %d\\n\" item;\n        print_queue ()\n    | None -> ()\n  in\n  print_queue ()\n\n\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/php_example.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Example;\n\nuse DateTime;\nuse Exception;\nuse InvalidArgumentException;\nuse JsonSerializable;\nuse Psr\\Log\\LoggerInterface;\n\n// Interface definition\ninterface DataProcessor\n{\n    public function process(mixed $data): mixed;\n    public function validate(mixed $data): bool;\n}\n\n// Trait definition\ntrait Loggable\n{\n    protected ?LoggerInterface $logger = null;\n\n    public function setLogger(LoggerInterface $logger): void\n    {\n        $this->logger = $logger;\n    }\n\n    protected function log(string $message, array $context = []): void\n    {\n        $this->logger?->info($message, $context);\n    }\n}\n\n// Abstract class\nabstract class Entity implements JsonSerializable\n{\n    protected DateTime $createdAt;\n    protected ?DateTime $updatedAt = null;\n\n    public function __construct()\n    {\n        $this->createdAt = new DateTime();\n    }\n\n    abstract public function validate(): bool;\n\n    public function jsonSerialize(): mixed\n    {\n        return [\n            'createdAt' => $this->createdAt->format('c'),\n            'updatedAt' => $this->updatedAt?->format('c'),\n        ];\n    }\n}\n\n// Enum definition (PHP 8.1+)\nenum Status: string\n{\n    case PENDING = 'pending';\n    case ACTIVE = 'active';\n    case COMPLETED = 'completed';\n    case FAILED = 'failed';\n\n    public function label(): string\n    {\n        return match($this) {\n            self::PENDING => 'Pending',\n            self::ACTIVE => 'Active',\n            self::COMPLETED => 'Completed',\n            self::FAILED => 'Failed',\n        };\n    }\n}\n\n// Class implementing interface and using trait\nclass User extends Entity implements DataProcessor\n{\n    use Loggable;\n\n    private static int $instanceCount = 0;\n\n    public function __construct(\n        private string $name,\n        private string $email,\n        private Status $status = Status::PENDING,\n        private array $metadata = []\n    ) {\n        parent::__construct();\n        self::$instanceCount++;\n    }\n\n    public static function getInstanceCount(): int\n    {\n        return self::$instanceCount;\n    }\n\n    // Property getter with validation\n    public function getEmail(): string\n    {\n        return $this->email;\n    }\n\n    // Property setter with validation\n    public function setEmail(string $email): void\n    {\n        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {\n            throw new InvalidArgumentException('Invalid email format');\n        }\n        $this->email = $email;\n        $this->updatedAt = new DateTime();\n    }\n\n    // Magic method implementation\n    public function __get(string $name)\n    {\n        return $this->metadata[$name] ?? null;\n    }\n\n    public function __set(string $name, mixed $value): void\n    {\n        $this->metadata[$name] = $value;\n    }\n\n    // Interface method implementations\n    public function process(mixed $data): mixed\n    {\n        if (!is_array($data)) {\n            throw new InvalidArgumentException('Data must be an array');\n        }\n\n        $this->log('Processing user data', ['user' => $this->name]);\n\n        foreach ($data as $key => $value) {\n            $this->metadata[$key] = $value;\n        }\n\n        $this->status = Status::COMPLETED;\n        return $this;\n    }\n\n    public function validate(mixed $data): bool\n    {\n        return is_array($data) && !empty($data);\n    }\n\n    // Abstract method implementation\n    public function validate(): bool\n    {\n        return !empty($this->name) && !empty($this->email);\n    }\n\n    // Method using arrow functions (PHP 7.4+)\n    public function getMetadataValues(): array\n    {\n        return array_map(\n            fn($value) => is_array($value) ? json_encode($value) : (string)$value,\n            $this->metadata\n        );\n    }\n\n    // Implementation of JsonSerializable\n    public function jsonSerialize(): mixed\n    {\n        return [\n            ...parent::jsonSerialize(),\n            'name' => $this->name,\n            'email' => $this->email,\n            'status' => $this->status->value,\n            'metadata' => $this->metadata,\n        ];\n    }\n}\n\n// Custom exception\nclass ProcessingException extends Exception\n{\n    public function __construct(\n        string $message = \"\",\n        private ?string $errorCode = null,\n        int $code = 0,\n        ?Throwable $previous = null\n    ) {\n        parent::__construct($message, $code, $previous);\n    }\n\n    public function getErrorCode(): ?string\n    {\n        return $this->errorCode;\n    }\n}\n\n// Anonymous class usage\n$validator = new class {\n    public function validateUser(User $user): bool\n    {\n        return $user->validate();\n    }\n};\n\n// Example usage\ntry {\n    $user = new User(\"John Doe\", \"john@example.com\");\n    $user->process(['role' => 'admin', 'preferences' => ['theme' => 'dark']]);\n    \n    // Using magic methods\n    $user->customField = 'custom value';\n    echo $user->customField;\n    \n    // JSON serialization\n    echo json_encode($user, JSON_PRETTY_PRINT);\n    \n} catch (Exception $e) {\n    echo \"Error: \" . $e->getMessage();\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/protobuf_example.proto",
    "content": "syntax = \"proto3\";\n\npackage example;\n\nimport \"google/protobuf/timestamp.proto\";\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/wrappers.proto\";\n\noption go_package = \"example/generated\";\noption java_package = \"com.example.generated\";\noption java_multiple_files = true;\n\n// Enum definition\nenum Status {\n  STATUS_UNSPECIFIED = 0;\n  STATUS_PENDING = 1;\n  STATUS_ACTIVE = 2;\n  STATUS_COMPLETED = 3;\n  STATUS_FAILED = 4;\n}\n\n// Message with nested messages and various field types\nmessage User {\n  // Nested message definition\n  message Address {\n    string street = 1;\n    string city = 2;\n    string state = 3;\n    string country = 4;\n    string postal_code = 5;\n  }\n\n  // Nested enum\n  enum Role {\n    ROLE_UNSPECIFIED = 0;\n    ROLE_ADMIN = 1;\n    ROLE_USER = 2;\n    ROLE_GUEST = 3;\n  }\n\n  // Fields with different types and options\n  string id = 1;\n  string name = 2 [(validate.rules).string = {\n    min_len: 1,\n    max_len: 100\n  }];\n  string email = 3 [(validate.rules).string.email = true];\n  repeated string phone_numbers = 4;\n  Role role = 5;\n  Status status = 6;\n  Address primary_address = 7;\n  repeated Address additional_addresses = 8;\n  map<string, string> metadata = 9;\n  google.protobuf.Timestamp created_at = 10;\n  google.protobuf.Timestamp updated_at = 11;\n  oneof verification {\n    string phone_verification = 12;\n    string email_verification = 13;\n  }\n}\n\n// Message using various repeated and map fields\nmessage UserList {\n  repeated User users = 1;\n  int32 total_count = 2;\n  string next_page_token = 3;\n}\n\n// Request/Response messages\nmessage CreateUserRequest {\n  User user = 1;\n}\n\nmessage UpdateUserRequest {\n  string user_id = 1;\n  User user = 2;\n  google.protobuf.FieldMask update_mask = 3;\n}\n\nmessage GetUserRequest {\n  string user_id = 1;\n}\n\nmessage DeleteUserRequest {\n  string user_id = 1;\n}\n\nmessage ListUsersRequest {\n  int32 page_size = 1;\n  string page_token = 2;\n  string filter = 3;\n}\n\n// Service definition with various RPC patterns\nservice UserService {\n  // Unary RPC\n  rpc CreateUser(CreateUserRequest) returns (User);\n  \n  // Unary RPC with custom error responses\n  rpc GetUser(GetUserRequest) returns (User);\n  \n  // Unary RPC with field mask\n  rpc UpdateUser(UpdateUserRequest) returns (User);\n  \n  // Unary RPC returning empty response\n  rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);\n  \n  // Server streaming RPC\n  rpc ListUsers(ListUsersRequest) returns (stream User);\n  \n  // Client streaming RPC\n  rpc BatchCreateUsers(stream CreateUserRequest) returns (UserList);\n  \n  // Bidirectional streaming RPC\n  rpc ProcessUsers(stream User) returns (stream User);\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/python_example.py",
    "content": "#!/usr/bin/env python3\nfrom __future__ import annotations\n\nimport asyncio\nimport dataclasses\nimport enum\nfrom abc import ABC, abstractmethod\nfrom datetime import datetime\nfrom functools import wraps\nfrom typing import (\n    Any, AsyncIterator, Callable, ClassVar, Dict, Generic, \n    List, Optional, Protocol, TypeVar, Union\n)\n\n# Type variable definitions\nT = TypeVar('T')\nK = TypeVar('K')\nV = TypeVar('V')\n\n# Protocol definition\nclass Processable(Protocol):\n    def process(self) -> None: ...\n    def validate(self) -> bool: ...\n\n# Enum definition\nclass Status(enum.Enum):\n    PENDING = \"pending\"\n    ACTIVE = \"active\"\n    COMPLETED = \"completed\"\n    FAILED = \"failed\"\n\n    def __str__(self) -> str:\n        return self.value\n\n# Dataclass with frozen and slots options\n@dataclasses.dataclass(frozen=True, slots=True)\nclass UserCredentials:\n    username: str\n    email: str\n    created_at: datetime = dataclasses.field(default_factory=datetime.now)\n\n# Abstract base class\nclass BaseProcessor(ABC, Generic[T]):\n    def __init__(self) -> None:\n        self._items: List[T] = []\n        self._processed_count: int = 0\n\n    @abstractmethod\n    async def process_item(self, item: T) -> None:\n        pass\n\n    @property\n    def processed_count(self) -> int:\n        return self._processed_count\n\n# Decorator definition\ndef log_execution(func: Callable) -> Callable:\n    @wraps(func)\n    async def wrapper(*args: Any, **kwargs: Any) -> Any:\n        print(f\"Executing {func.__name__}\")\n        try:\n            result = await func(*args, **kwargs)\n            print(f\"Completed {func.__name__}\")\n            return result\n        except Exception as e:\n            print(f\"Error in {func.__name__}: {e}\")\n            raise\n    return wrapper\n\n# Class implementing abstract base class and protocol\nclass DataProcessor(BaseProcessor[UserCredentials], Processable):\n    # Class variable\n    DEFAULT_BATCH_SIZE: ClassVar[int] = 100\n\n    def __init__(self, batch_size: Optional[int] = None) -> None:\n        super().__init__()\n        self.batch_size = batch_size or self.DEFAULT_BATCH_SIZE\n        self._status = Status.PENDING\n\n    # Property with getter and setter\n    @property\n    def status(self) -> Status:\n        return self._status\n\n    @status.setter\n    def status(self, value: Status) -> None:\n        if not isinstance(value, Status):\n            raise ValueError(\"Status must be a Status enum value\")\n        self._status = value\n\n    # Context manager methods\n    async def __aenter__(self) -> DataProcessor:\n        self.status = Status.ACTIVE\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:\n        self.status = Status.COMPLETED if exc_type is None else Status.FAILED\n\n    # Generator method\n    async def process_batch(self) -> AsyncIterator[List[UserCredentials]]:\n        for i in range(0, len(self._items), self.batch_size):\n            batch = self._items[i:i + self.batch_size]\n            yield batch\n            await asyncio.sleep(0.1)\n\n    # Implementation of abstract method\n    @log_execution\n    async def process_item(self, item: UserCredentials) -> None:\n        if not self.validate():\n            raise ValueError(\"Processor is not in a valid state\")\n        self._items.append(item)\n        self._processed_count += 1\n\n    # Implementation of protocol method\n    def process(self) -> None:\n        if not self._items:\n            raise ValueError(\"No items to process\")\n        self.status = Status.ACTIVE\n\n    def validate(self) -> bool:\n        return self.status != Status.FAILED\n\n# Custom exception\nclass ProcessingError(Exception):\n    def __init__(self, message: str, item: Any) -> None:\n        self.item = item\n        super().__init__(f\"Error processing {item}: {message}\")\n\n# Async main function\nasync def main() -> None:\n    async with DataProcessor(batch_size=10) as processor:\n        # Create test data\n        user = UserCredentials(\n            username=\"test_user\",\n            email=\"test@example.com\"\n        )\n\n        try:\n            await processor.process_item(user)\n            \n            async for batch in processor.process_batch():\n                print(f\"Processing batch of {len(batch)} items\")\n                \n        except ProcessingError as e:\n            print(f\"Processing failed: {e}\")\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/ruby_example.rb",
    "content": "#!/usr/bin/env ruby\n\nglobal_var = \"Hello, World!\"\n\n# Module for mixing in common functionality\nmodule Loggable\n  def log(message)\n    puts \"[#{Time.now}] #{message}\"\n  end\nend\n\n# Module with class methods\nmodule Utils\n  class << self\n    def generate_id\n      SecureRandom.uuid\n    end\n  end\nend\n\n# Abstract base class\nclass BaseProcessor\n  include Loggable\n\n  # Class instance variable\n  @processors = []\n\n  class << self\n    attr_reader :processors\n\n    def register(processor)\n      @processors << processor\n    end\n  end\n\n  # Instance variables with attr accessors\n  attr_reader :id, :created_at\n  attr_accessor :status\n\n  def initialize\n    @id = Utils.generate_id\n    @created_at = Time.now\n    @status = :pending\n    self.class.register(self)\n  end\n\n  # Abstract method\n  def process\n    raise NotImplementedError, \"#{self.class} must implement process\"\n  end\nend\n\n# Custom exception class\nclass ProcessingError < StandardError\n  attr_reader :item\n\n  def initialize(message, item)\n    @item = item\n    super(message)\n  end\nend\n\n# Struct definition\nUser = Struct.new(:name, :email, keyword_init: true) do\n  def valid?\n    name && email && email.include?('@')\n  end\nend\n\n# Enum-like module using freeze\nmodule Status\n  PENDING = 'pending'.freeze\n  ACTIVE = 'active'.freeze\n  COMPLETED = 'completed'.freeze\n  FAILED = 'failed'.freeze\n\n  ALL = [PENDING, ACTIVE, COMPLETED, FAILED].freeze\nend\n\n# Class using inheritance and mixins\nclass DataProcessor < BaseProcessor\n  # Constants\n  MAX_RETRIES = 3\n  DEFAULT_TIMEOUT = 5\n\n  # Class variable\n  @@instance_count = 0\n\n  def self.instance_count\n    @@instance_count\n  end\n\n  def initialize(options = {})\n    super()\n    @options = options\n    @items = []\n    @@instance_count += 1\n  end\n\n  # Method with keyword arguments and default value\n  def add_item(item:, priority: :normal)\n    validate_item(item)\n    @items << [item, priority]\n  end\n\n  # Private methods\n  private\n\n  def validate_item(item)\n    raise ArgumentError, \"Invalid item\" unless item.respond_to?(:valid?)\n    raise ProcessingError.new(\"Invalid item\", item) unless item.valid?\n  end\n\n  # Method using block\n  def with_retry\n    retries = 0\n    begin\n      yield\n    rescue StandardError => e\n      retries += 1\n      retry if retries < MAX_RETRIES\n      raise\n    end\n  end\n\n  # Method using lambda\n  def process_items\n    sorter = ->(a, b) { a[1] <=> b[1] }\n    @items.sort(&sorter).each do |item, _priority|\n      process_item(item)\n    end\n  end\n\n  protected\n\n  def process_item(item)\n    log(\"Processing item: #{item}\")\n    # Processing logic here\n  end\nend\n\n# Singleton class\nrequire 'singleton'\nclass Configuration\n  include Singleton\n\n  def initialize\n    @settings = {}\n  end\n\n  def [](key)\n    @settings[key]\n  end\n\n  def []=(key, value)\n    @settings[key] = value\n  end\nend\n\n# Example usage\nif __FILE__ == $PROGRAM_NAME\n  config = Configuration.instance\n  config[:timeout] = 30\n\n  processor = DataProcessor.new(timeout: config[:timeout])\n  user = User.new(name: \"John Doe\", email: \"john@example.com\")\n\n  begin\n    processor.add_item(item: user, priority: :high)\n    processor.process\n  rescue ProcessingError => e\n    puts \"Failed to process #{e.item}: #{e.message}\"\n  end\nend\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/rust_example.rs",
    "content": "use std::{\n    collections::{HashMap, HashSet},\n    sync::{Arc, Mutex},\n    time::{Duration, SystemTime},\n};\n\nuse tokio::sync::mpsc;\nuse serde::{Deserialize, Serialize};\n\n// Type alias\ntype Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;\n\n// Constants\nconst MAX_RETRIES: u32 = 3;\nconst DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);\n\n// Custom error type\n#[derive(Debug, thiserror::Error)]\npub enum ProcessError {\n    #[error(\"Validation failed: {0}\")]\n    ValidationError(String),\n    \n    #[error(\"Processing failed: {0}\")]\n    ProcessingError(String),\n    \n    #[error(transparent)]\n    Other(#[from] Box<dyn std::error::Error + Send + Sync>),\n}\n\n// Trait definition\n#[async_trait::async_trait]\npub trait DataProcessor<T> {\n    async fn process(&self, data: T) -> Result<T>;\n    fn validate(&self, data: &T) -> bool;\n}\n\n// Struct with lifetime parameter and generic type\n#[derive(Debug)]\npub struct ProcessorState<'a, T> {\n    name: &'a str,\n    data: T,\n    created_at: SystemTime,\n}\n\n// Enum with different variants\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum Status {\n    Pending,\n    Active { started_at: SystemTime },\n    Completed { result: String },\n    Failed { error: String },\n}\n\n// Struct implementing trait\npub struct ItemProcessor {\n    items: Arc<Mutex<HashSet<String>>>,\n    status: Status,\n    tx: mpsc::Sender<String>,\n}\n\n// Trait implementation\n#[async_trait::async_trait]\nimpl DataProcessor<String> for ItemProcessor {\n    async fn process(&self, data: String) -> Result<String> {\n        if !self.validate(&data) {\n            return Err(Box::new(ProcessError::ValidationError(\"Invalid data\".into())));\n        }\n        Ok(data.to_uppercase())\n    }\n\n    fn validate(&self, data: &String) -> bool {\n        !data.is_empty()\n    }\n}\n\n// Struct with derive macros\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct Config {\n    pub name: String,\n    pub max_items: usize,\n    #[serde(default)]\n    pub timeout: Option<Duration>,\n}\n\n\n// Implementation with associated types\npub trait Storage {\n    type Item;\n    type Error;\n\n    fn store(&mut self, item: Self::Item) -> std::result::Result<(), Self::Error>;\n    fn retrieve(&self, id: &str) -> std::result::Result<Option<Self::Item>, Self::Error>;\n}\n\n// Struct implementing trait with associated types\npub struct MemoryStorage {\n    data: HashMap<String, Vec<u8>>,\n}\n\nimpl Storage for MemoryStorage {\n    type Item = Vec<u8>;\n    type Error = std::io::Error;\n\n    fn store(&mut self, item: Self::Item) -> std::result::Result<(), Self::Error> {\n        self.data.insert(String::from(\"default\"), item);\n        Ok(())\n    }\n\n    fn retrieve(&self, id: &str) -> std::result::Result<Option<Self::Item>, Self::Error> {\n        Ok(self.data.get(id).cloned())\n    }\n}\n\n// Async main function\n#[tokio::main]\nasync fn main() -> Result<()> {\n    let (tx, mut rx) = mpsc::channel(100);\n    let mut processor = ItemProcessor::new(tx);\n\n    // Spawn background task\n    tokio::spawn(async move {\n        while let Some(item) = rx.recv().await {\n            println!(\"Received: {}\", item);\n        }\n    });\n\n    // Process items\n    processor.add_item(\"test\".into()).await?;\n    \n    let config = Config {\n        name: \"test\".into(),\n        max_items: 100,\n        timeout: Some(DEFAULT_TIMEOUT),\n    };\n\n    println!(\"Config: {:?}\", config);\n    \n    Ok(())\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/scala_example.scala",
    "content": "package example\n\nimport scala.concurrent.{Future, ExecutionContext}\nimport scala.util.{Try, Success, Failure}\nimport scala.collection.mutable\nimport scala.annotation.tailrec\n\n// Type alias\ntype Result[T] = Either[String, T]\n\n// Trait with type parameter\ntrait DataProcessor[T] {\n  def process(data: T): Result[T]\n  def validate(data: T): Boolean\n}\n\n// Case class with type parameters\ncase class ProcessingState[T](\n  data: T,\n  status: ProcessingState.Status,\n  timestamp: Long = System.currentTimeMillis()\n)\n\n// Companion object with sealed trait\nobject ProcessingState {\n  sealed trait Status\n  case object Pending extends Status\n  case object Active extends Status\n  case object Completed extends Status\n  case class Failed(error: String) extends Status\n}\n\n// Abstract class\nabstract class BaseProcessor[T] extends DataProcessor[T] {\n  protected val logger = new Logger(getClass.getName)\n  \n  // Abstract method\n  protected def transform(data: T): T\n  \n  // Concrete implementation using abstract method\n  override def process(data: T): Result[T] = {\n    if (!validate(data)) {\n      Left(\"Invalid data\")\n    } else {\n      Try(transform(data)).toEither.left.map(_.getMessage)\n    }\n  }\n}\n\n// Class with type bounds\nclass NumberProcessor[T <: Number] extends BaseProcessor[T] {\n  override def validate(data: T): Boolean = data != null\n  \n  protected def transform(data: T): T = data\n}\n\n// Case class with default and optional parameters\ncase class User(\n  id: String,\n  name: String,\n  email: String,\n  roles: Set[String] = Set.empty,\n  metadata: Map[String, String] = Map.empty\n)\n\n// Object for utility functions\nobject Utils {\n  def withRetry[T](times: Int)(f: => T): Try[T] = {\n    @tailrec\n    def attempt(remaining: Int, lastError: Option[Throwable]): Try[T] = {\n      if (remaining == 0) {\n        Failure(lastError.getOrElse(new RuntimeException(\"Max retries exceeded\")))\n      } else {\n        Try(f) match {\n          case success @ Success(_) => success\n          case Failure(error) => attempt(remaining - 1, Some(error))\n        }\n      }\n    }\n    attempt(times, None)\n  }\n}\n\n// Implicit class for extensions\nobject Implicits {\n  implicit class StringOps(val s: String) extends AnyVal {\n    def isValidEmail: Boolean = s.matches(\".+@.+\\\\..+\")\n  }\n}\n\n// Trait with self type annotation\ntrait Logging {\n  self: Logger =>\n  \n  def debug(message: => String): Unit = log(\"DEBUG\", message)\n  def info(message: => String): Unit = log(\"INFO\", message)\n  def error(message: => String): Unit = log(\"ERROR\", message)\n}\n\n// Class using self type trait\nclass Logger(name: String) extends Logging {\n  def log(level: String, message: => String): Unit = {\n    println(s\"[$level] $name: $message\")\n  }\n}\n\n// Class with type constructor and higher-kinded type\ntrait Monad[F[_]] {\n  def pure[A](a: A): F[A]\n  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]\n  \n  def map[A, B](fa: F[A])(f: A => B): F[B] =\n    flatMap(fa)(a => pure(f(a)))\n}\n\n// Implicit conversions\nobject Conversions {\n  implicit def stringToUser(s: String): User = {\n    val parts = s.split(\":\")\n    User(parts(0), parts(1), parts(2))\n  }\n}\n\n// Trait with path-dependent type\ntrait Container {\n  type Content\n  def content: Content\n  def transform(f: Content => Content): Container\n}\n\n// Class implementing path-dependent type\nclass Box[T](initial: T) extends Container {\n  type Content = T\n  def content: T = initial\n  def transform(f: T => T): Box[T] = new Box(f(initial))\n}\n\n// Main object with application\nobject Main extends App {\n  import Implicits._\n  import ExecutionContext.Implicits.global\n  \n  val processor = new NumberProcessor[java.lang.Integer]\n  \n  def processAsync[T](data: T)(implicit ec: ExecutionContext): Future[Result[T]] = {\n    Future {\n      Thread.sleep(100) // Simulate work\n      Right(data)\n    }\n  }\n  \n  // Pattern matching\n  def handleResult[T](result: Result[T]): Unit = result match {\n    case Right(value) => println(s\"Success: $value\")\n    case Left(error) => println(s\"Error: $error\")\n  }\n  \n  // For comprehension\n  val computation = for {\n    a <- Future(1)\n    b <- Future(2)\n    c <- Future(a + b)\n  } yield c\n  \n  // Partial function\n  val handler: PartialFunction[Throwable, Unit] = {\n    case e: IllegalArgumentException => println(s\"Invalid argument: ${e.getMessage}\")\n    case e: Exception => println(s\"Other error: ${e.getMessage}\")\n  }\n  \n  // Using implicit conversion\n  val user: User = \"1:John Doe:john@example.com\"\n  \n  // Using type class\n  val box = new Box(42)\n  val transformed = box.transform(_ * 2)\n  \n  println(s\"Transformed value: ${transformed.content}\")\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/svelte_example.svelte",
    "content": "<script lang=\"ts\">\n  import { onMount, onDestroy } from 'svelte';\n  import { writable, derived } from 'svelte/store';\n  import type { Writable } from 'svelte/store';\n\n  // Props with TypeScript types\n  export let title: string;\n  export let initialCount: number = 0;\n\n  // Reactive declarations\n  $: doubled = count * 2;\n  $: {\n    if (count > 10) {\n      console.log('Count is getting high!');\n    }\n  }\n\n  // Local state\n  let count: number = initialCount;\n  let inputValue: string = '';\n  let mounted: boolean = false;\n\n  // Stores\n  const items: Writable<string[]> = writable([]);\n  const filteredItems = derived(items, $items => \n    $items.filter(item => item.includes(inputValue))\n  );\n\n  // Event handlers\n  function handleClick() {\n    count += 1;\n  }\n\n  function addItem() {\n    if (inputValue.trim()) {\n      items.update(items => [...items, inputValue.trim()]);\n      inputValue = '';\n    }\n  }\n\n  function removeItem(index: number) {\n    items.update(items => items.filter((_, i) => i !== index));\n  }\n\n  // Lifecycle\n  onMount(() => {\n    mounted = true;\n    return () => {\n      mounted = false;\n    };\n  });\n\n  onDestroy(() => {\n    console.log('Component destroyed');\n  });\n</script>\n\n<!-- Markup section -->\n<div class=\"container\">\n  <h1>{title}</h1>\n\n  <!-- Event binding and reactive values -->\n  <div class=\"counter\">\n    <button on:click={handleClick}>\n      Count: {count}\n    </button>\n    <p>Doubled: {doubled}</p>\n  </div>\n\n  <!-- Form with two-way binding -->\n  <div class=\"form\">\n    <input\n      type=\"text\"\n      bind:value={inputValue}\n      placeholder=\"Add item\"\n      on:keydown={e => e.key === 'Enter' && addItem()}\n    />\n    <button on:click={addItem}>Add</button>\n  </div>\n\n  <!-- Conditional rendering -->\n  {#if $items.length > 0}\n    <!-- List with store subscription -->\n    <ul>\n      {#each $filteredItems as item, index (item)}\n        <li class=\"item\">\n          {item}\n          <button on:click={() => removeItem(index)}>×</button>\n        </li>\n      {/each}\n    </ul>\n  {:else}\n    <p>No items added yet</p>\n  {/if}\n\n  <!-- Slots for content projection -->\n  <div class=\"content\">\n    <slot name=\"header\">\n      <h2>Default Header</h2>\n    </slot>\n    \n    <slot>\n      <p>Default content</p>\n    </slot>\n    \n    <slot name=\"footer\" />\n  </div>\n</div>\n\n<style>\n  .container {\n    padding: 1rem;\n    max-width: 600px;\n    margin: 0 auto;\n  }\n\n  .counter {\n    margin: 1rem 0;\n  }\n\n  .form {\n    display: flex;\n    gap: 0.5rem;\n    margin-bottom: 1rem;\n  }\n\n  input {\n    flex: 1;\n    padding: 0.5rem;\n    border: 1px solid #ccc;\n    border-radius: 4px;\n  }\n\n  button {\n    padding: 0.5rem 1rem;\n    background: #4CAF50;\n    color: white;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n  }\n\n  button:hover {\n    background: #45a049;\n  }\n\n  ul {\n    list-style: none;\n    padding: 0;\n  }\n\n  .item {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 0.5rem;\n    margin: 0.25rem 0;\n    background: #f5f5f5;\n    border-radius: 4px;\n  }\n\n  .item button {\n    background: #ff4444;\n    padding: 0.25rem 0.5rem;\n  }\n\n  .item button:hover {\n    background: #cc0000;\n  }\n\n  .content {\n    margin-top: 2rem;\n    padding: 1rem;\n    border: 1px solid #ddd;\n    border-radius: 4px;\n  }\n\n  /* Scoped styles - only apply to this component */\n  :global(.theme-dark) .container {\n    background: #333;\n    color: #fff;\n  }\n</style>\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/swift_example.swift",
    "content": "import Foundation\n\n// Protocol definitions\nprotocol DataProcessor {\n    associatedtype Input\n    associatedtype Output\n    \n    func process(_ input: Input) async throws -> Output\n    func validate(_ input: Input) -> Bool\n}\n\n// Error type\nenum ProcessingError: LocalizedError {\n    case invalidInput(String)\n    case processingFailed(String)\n    \n    var errorDescription: String? {\n        switch self {\n        case .invalidInput(let reason): return \"Invalid input: \\(reason)\"\n        case .processingFailed(let reason): return \"Processing failed: \\(reason)\"\n        }\n    }\n}\n\n// Property wrapper\n@propertyWrapper\nstruct Validated<T> {\n    private var value: T\n    private let validator: (T) -> Bool\n    \n    var wrappedValue: T {\n        get { value }\n        set {\n            guard validator(newValue) else {\n                fatalError(\"Invalid value\")\n            }\n            value = newValue\n        }\n    }\n    \n    init(wrappedValue: T, validator: @escaping (T) -> Bool) {\n        guard validator(wrappedValue) else {\n            fatalError(\"Invalid initial value\")\n        }\n        self.value = wrappedValue\n        self.validator = validator\n    }\n}\n\n// Actor for thread-safe state management\nactor ProcessingState {\n    private(set) var processedCount: Int = 0\n    private var status: Status = .pending\n    \n    enum Status {\n        case pending\n        case processing\n        case completed\n        case failed(Error)\n    }\n    \n    func incrementCount() {\n        processedCount += 1\n    }\n    \n    func updateStatus(_ newStatus: Status) {\n        status = newStatus\n    }\n}\n\n// Generic struct with where clause\nstruct Queue<Element> where Element: Sendable {\n    private var elements: [Element] = []\n    private let lock = NSLock()\n    \n    mutating func enqueue(_ element: Element) {\n        lock.lock()\n        defer { lock.unlock() }\n        elements.append(element)\n    }\n    \n    mutating func dequeue() -> Element? {\n        lock.lock()\n        defer { lock.unlock() }\n        return elements.isEmpty ? nil : elements.removeFirst()\n    }\n}\n\n// Class inheritance and protocol conformance\nclass StringProcessor: DataProcessor {\n    typealias Input = String\n    typealias Output = String\n    \n    private let state = ProcessingState()\n    \n    @Validated(validator: { !$0.isEmpty })\n    private var currentInput: String = \"default\"\n    \n    func process(_ input: String) async throws -> String {\n        guard validate(input) else {\n            throw ProcessingError.invalidInput(\"String is empty\")\n        }\n        \n        await state.updateStatus(.processing)\n        \n        // Simulate processing\n        try await Task.sleep(nanoseconds: 1_000_000_000)\n        let result = input.uppercased()\n        \n        await state.incrementCount()\n        await state.updateStatus(.completed)\n        \n        return result\n    }\n    \n    func validate(_ input: String) -> Bool {\n        !input.isEmpty\n    }\n}\n\n// Extension with async sequence\nextension StringProcessor: AsyncSequence, AsyncIteratorProtocol {\n    typealias Element = String\n    \n    func makeAsyncIterator() -> StringProcessor {\n        self\n    }\n    \n    func next() async throws -> String? {\n        try await process(currentInput)\n    }\n}\n\n// Result builders\n@resultBuilder\nstruct ArrayBuilder<T> {\n    static func buildBlock(_ components: T...) -> [T] {\n        components\n    }\n}\n\n// Function using result builder\nfunc makeArray<T>(@ArrayBuilder<T> content: () -> [T]) -> [T] {\n    content()\n}\n\n// Async main function demonstrating usage\n@main\nstruct Example {\n    static func main() async throws {\n        let processor = StringProcessor()\n        var queue = Queue<String>()\n        \n        // Using result builder\n        let inputs = makeArray {\n            \"Hello\"\n            \"World\"\n            \"Swift\"\n        }\n        \n        // Process inputs\n        for input in inputs {\n            queue.enqueue(input)\n        }\n        \n        // Process queue\n        while let input = queue.dequeue() {\n            do {\n                let result = try await processor.process(input)\n                print(\"Processed: \\(result)\")\n            } catch {\n                print(\"Error: \\(error.localizedDescription)\")\n            }\n        }\n        \n        // Using async sequence\n        for try await result in processor.prefix(3) {\n            print(\"Async sequence result: \\(result)\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/toml_example.toml",
    "content": "# This is a TOML example file demonstrating various TOML syntax elements\n\n# Basic key/value pairs\ntitle = \"TOML Example\"\nversion = \"1.0.0\"\nenabled = true\n\n# String with special characters\ndescription = \"\"\"\nA multi-line string that can contain \"quotes\"\nand other special characters like \\n or \\t\n\"\"\"\n\n# Date and time\ncreated = 1979-05-27T07:32:00Z\nupdated = 1979-05-27T00:32:00-07:00\n\n# Numbers\ninteger = 42\nfloat = 3.14159\nscientific = 1e+12\nhex = 0xDEADBEEF\noctal = 0o755\nbinary = 0b11010110\n\n# Arrays\ncolors = [\"red\", \"yellow\", \"green\"]\nnumbers = [1, 2, 3]\nnested_arrays = [[1, 2], [3, 4, 5]]\nmixed_array = [[1, 2], [\"a\", \"b\", \"c\"]]\n\n# Array of tables\n[[fruits]]\nname = \"apple\"\ncolor = \"red\"\nseason = \"fall\"\n\n[[fruits]]\nname = \"banana\"\ncolor = \"yellow\"\nseason = \"summer\"\n\n# Tables\n[server]\nhost = \"example.com\"\nport = 8080\nenabled = true\n\n[server.options]\ntimeout = 30\nmax_connections = 100\n\n[database]\nurl = \"postgresql://localhost:5432/mydb\"\nmax_connections = 100\n\n[database.replica]\nenabled = true\nhosts = [\n    \"replica1.example.com\",\n    \"replica2.example.com\"\n]\n\n# Nested tables\n[owner]\nname = \"John Doe\"\n[owner.preferences]\ntheme = \"dark\"\nnotifications = true\n[owner.preferences.display]\ncolor_scheme = \"monokai\"\nfont_size = 12\n\n# Table with inline tables\n[endpoints]\nstatus = { url = \"/status\", method = \"GET\" }\nhealth = { url = \"/health\", method = \"GET\", timeout = 5 }\n\n# Complex types\n[types]\nprimitive_array = [ \"red\", \"yellow\", \"green\" ]\narray_of_integers = [ 1, 2, 3 ]\narray_of_floats = [ 1.1, 2.2, 3.3 ]\narray_of_dates = [ 1979-05-27T07:32:00Z, 1979-05-28T07:32:00Z ]\narray_of_tables = [\n    { x = 1, y = 2, z = 3 },\n    { x = 7, y = 8, z = 9 },\n    { x = 2, y = 4, z = 8 }\n]\n\n# Example configuration\n[app]\nname = \"MyApp\"\nenvironment = \"production\"\n\n[app.logging]\nlevel = \"info\"\nformat = \"json\"\noutput = \"stdout\"\n\n[app.features]\nexperimental = false\nbeta_features = [\"feature1\", \"feature2\"]\n\n[app.limits]\nrequests_per_second = 1000\nconcurrent_connections = 50\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/tsx_example.tsx",
    "content": "import React, { useState, useEffect, useCallback, useRef } from 'react';\nimport type { FC, ReactNode, FormEvent } from 'react';\n\n// Type definitions\ninterface User {\n  id: string;\n  name: string;\n  email: string;\n  role: 'admin' | 'user';\n}\n\ntype ValidationResult = {\n  valid: boolean;\n  errors: string[];\n};\n\n// Props interface with generic type\ninterface DataListProps<T> {\n  items: T[];\n  renderItem: (item: T) => ReactNode;\n  onItemSelect?: (item: T) => void;\n}\n\n// Custom hook with TypeScript\nfunction useDebounce<T>(value: T, delay: number): T {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(timer);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n\n// Higher-order component with TypeScript\nfunction withLoading<P extends object>(\n  WrappedComponent: React.ComponentType<P>\n): FC<P & { loading?: boolean }> {\n  return ({ loading = false, ...props }) => (\n    <div className=\"wrapper\">\n      {loading ? (\n        <div className=\"loading-spinner\" />\n      ) : (\n        <WrappedComponent {...(props as P)} />\n      )}\n    </div>\n  );\n}\n\n// Form component with controlled inputs\nconst UserForm: FC<{ onSubmit: (user: Partial<User>) => void }> = ({ onSubmit }) => {\n  const [formData, setFormData] = useState<Partial<User>>({\n    name: '',\n    email: '',\n    role: 'user'\n  });\n\n  const handleSubmit = (e: FormEvent) => {\n    e.preventDefault();\n    onSubmit(formData);\n  };\n\n  return (\n    <form onSubmit={handleSubmit} className=\"user-form\">\n      <div className=\"form-group\">\n        <label htmlFor=\"name\">Name:</label>\n        <input\n          type=\"text\"\n          id=\"name\"\n          value={formData.name}\n          onChange={e => setFormData(prev => ({\n            ...prev,\n            name: e.target.value\n          }))}\n          required\n        />\n      </div>\n\n      <div className=\"form-group\">\n        <label htmlFor=\"email\">Email:</label>\n        <input\n          type=\"email\"\n          id=\"email\"\n          value={formData.email}\n          onChange={e => setFormData(prev => ({\n            ...prev,\n            email: e.target.value\n          }))}\n          required\n        />\n      </div>\n\n      <div className=\"form-group\">\n        <label htmlFor=\"role\">Role:</label>\n        <select\n          id=\"role\"\n          value={formData.role}\n          onChange={e => setFormData(prev => ({\n            ...prev,\n            role: e.target.value as User['role']\n          }))}\n        >\n          <option value=\"user\">User</option>\n          <option value=\"admin\">Admin</option>\n        </select>\n      </div>\n\n      <button type=\"submit\">Submit</button>\n    </form>\n  );\n};\n\n// Generic list component\nconst DataList = <T extends { id: string }>({\n  items,\n  renderItem,\n  onItemSelect\n}: DataListProps<T>): JSX.Element => {\n  return (\n    <ul className=\"data-list\">\n      {items.map(item => (\n        <li\n          key={item.id}\n          onClick={() => onItemSelect?.(item)}\n          role=\"button\"\n          tabIndex={0}\n        >\n          {renderItem(item)}\n        </li>\n      ))}\n    </ul>\n  );\n};\n\n// Main app component using all features\nconst App: FC = () => {\n  const [users, setUsers] = useState<User[]>([]);\n  const [searchTerm, setSearchTerm] = useState('');\n  const debouncedSearch = useDebounce(searchTerm, 300);\n  const formRef = useRef<HTMLFormElement>(null);\n\n  const handleUserSubmit = useCallback((userData: Partial<User>) => {\n    const newUser: User = {\n      id: crypto.randomUUID(),\n      ...userData as Omit<User, 'id'>\n    };\n    setUsers(prev => [...prev, newUser]);\n  }, []);\n\n  const filteredUsers = users.filter(user =>\n    user.name.toLowerCase().includes(debouncedSearch.toLowerCase())\n  );\n\n  return (\n    <div className=\"app\">\n      <header className=\"app-header\">\n        <h1>User Management</h1>\n        <input\n          type=\"search\"\n          placeholder=\"Search users...\"\n          value={searchTerm}\n          onChange={e => setSearchTerm(e.target.value)}\n          className=\"search-input\"\n        />\n      </header>\n\n      <main>\n        <section className=\"user-form-section\">\n          <h2>Add New User</h2>\n          <UserForm onSubmit={handleUserSubmit} />\n        </section>\n\n        <section className=\"user-list-section\">\n          <h2>Users</h2>\n          <DataList\n            items={filteredUsers}\n            renderItem={user => (\n              <div className=\"user-item\">\n                <strong>{user.name}</strong>\n                <span>{user.email}</span>\n                <badge className={`role-badge ${user.role}`}>\n                  {user.role}\n                </badge>\n              </div>\n            )}\n            onItemSelect={user => console.log('Selected user:', user)}\n          />\n        </section>\n      </main>\n    </div>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "app/server/syntax/file_map/examples/typescript_example.ts",
    "content": "// @ts-nocheck\n\n// Type imports\nimport type { Request, Response, NextFunction } from 'express';\n\n// Interface definitions\ninterface DataProcessor<T> {\n    process(data: T): Promise<T>;\n    validate(data: T): boolean;\n}\n\n// Type aliases\ntype Result<T> = {\n    data: T;\n    error?: string;\n    metadata: Record<string, unknown>;\n};\n\n// Enum with string values\nenum Status {\n    PENDING = 'pending',\n    ACTIVE = 'active',\n    COMPLETED = 'completed',\n    FAILED = 'failed'\n}\n\n// Union type\ntype ValidationResult =\n    | { valid: true; data: unknown }\n    | { valid: false; errors: string[] };\n\n// Intersection type\ntype AdminUser = User & {\n    permissions: string[];\n    role: 'admin';\n};\n\n// Mapped type\ntype Readonly<T> = {\n    readonly [P in keyof T]: T[P];\n};\n\n// Utility type\ntype Partial<T> = {\n    [P in keyof T]?: T[P];\n};\n\n// Class with decorators\n\n// Decorator factory\nfunction validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value;\n    descriptor.value = function (...args: any[]) {\n        if (this.validate(args[0])) {\n            return originalMethod.apply(this, args);\n        }\n        throw new Error('Validation failed');\n    };\n    return descriptor;\n}\n\n@logger\nclass User {\n    @required\n    private name: string;\n\n    @email\n    private email: string;\n\n    @format('YYYY-MM-DD')\n    private createdAt: Date;\n\n    constructor(name: string, email: string) {\n        this.name = name;\n        this.email = email;\n        this.createdAt = new Date();\n    }\n\n    // Method decorator\n    @validate\n    public updateEmail(newEmail: string): void {\n        this.email = newEmail;\n    }\n\n    // Getter with type guard\n    public get isAdmin(): this is AdminUser {\n        return 'role' in this && (this as AdminUser).role === 'admin';\n    }\n}\n\n\n// Abstract class\nabstract class BaseProcessor<T> implements DataProcessor<T> {\n    protected status: Status = Status.PENDING;\n\n    abstract process(data: T): Promise<T>;\n\n    validate(data: T): boolean {\n        return data !== null && data !== undefined;\n    }\n}\n\n// Generic class extending abstract class\nclass StringProcessor extends BaseProcessor<string> {\n    async process(data: string): Promise<string> {\n        this.status = Status.ACTIVE;\n        const result = await this.transform(data);\n        this.status = Status.COMPLETED;\n        return result;\n    }\n\n    private async transform(data: string): Promise<string> {\n        return data.toUpperCase();\n    }\n}\n\n// Function overloads\nfunction process(data: string): Promise<string>;\nfunction process(data: number): Promise<number>;\nfunction process(data: string | number): Promise<string | number> {\n    return Promise.resolve(data);\n}\n\n// Generic function with constraints\nasync function validateData<T extends { id: string }>(\n    data: T\n): Promise<ValidationResult> {\n    if (!data.id) {\n        return { valid: false, errors: ['ID is required'] };\n    }\n    return { valid: true, data };\n}\n\n// Higher-order function\nfunction withRetry<T>(\n    fn: () => Promise<T>,\n    retries: number = 3\n): () => Promise<T> {\n    return async () => {\n        let lastError: Error | undefined;\n\n        for (let i = 0; i < retries; i++) {\n            try {\n                return await fn();\n            } catch (error) {\n                lastError = error as Error;\n            }\n        }\n\n        throw lastError;\n    };\n}\n\n// Middleware function type\ntype Middleware = (\n    req: Request,\n    res: Response,\n    next: NextFunction\n) => Promise<void>;\n\n// Utility functions with type inference\nconst createUser = <T extends User>(data: Partial<T>): T => {\n    return { ...data } as T;\n};\n\n// Async generator function\nasync function* generateSequence(\n    start: number,\n    end: number\n): AsyncGenerator<number> {\n    for (let i = start; i <= end; i++) {\n        await new Promise(resolve => setTimeout(resolve, 100));\n        yield i;\n    }\n}\n\n// Example usage\nasync function main() {\n    const processor = new StringProcessor();\n    const result = await processor.process('hello');\n\n    const user = createUser<AdminUser>({\n        name: 'Admin',\n        email: 'admin@example.com',\n        permissions: ['read', 'write'],\n        role: 'admin'\n    });\n\n    const retryableProcess = withRetry(async () => {\n        return await processor.process('retry me');\n    });\n\n    for await (const num of generateSequence(1, 5)) {\n        console.log(num);\n    }\n}\n\n// exported definitions\nexport const exportedFunction = (a: number, b: number) => {\n    return a + b;\n}\nexport const SOME_CONST: number = 123;\nexport var SOME_VAR: string = 'hello';\nexport let SOME_LET: boolean = true;\nexport type SOME_TYPE = string;\nexport interface SOME_INTERFACE {\n    name: string;\n}\nexport class SOME_CLASS {\n    constructor(public name: string) { }\n}\n\nexport function fn() {\n    return 'hello';\n}\n\nexport async function asyncFn() {\n    return 'hello';\n}\n\nexport enum Enum {\n    A = 'a',\n    B = 'b',\n    C = 'c'\n}\n\nexport const oneLineFunc = (a: number, b: number): number => a + b;\n\n\n/* default-export variations (multiple defaults are illegal for the\n   TS checker, but tree-sitter parses them just fine) */\nexport default function makeId<T = string>() { return '' as T }\nexport default class DefaultCls { }\nexport default (x: number) => x * 2\n\n/* re-exports that should be ignored */\nexport { Foo as Renamed } from './foo'\nexport * as utils from './utils'\n\n/* const / ambient enums */\nconst enum Flags { None = 0, Read = 1 }\ndeclare enum Ambient { X }\n\n/* namespace with nested export */\nnamespace Legacy { export function greet() { } }\n\n/* Arrow returning an implicit object literal */\nexport const build = (id: string) => ({ id, ts: Date.now() })\n\n/* Generic arrow function assigned to a const */\nconst identity = <T>(x: T) => x\n\n/* Function expression assigned to a const */\nconst internal = function named() { return 1 }\n\n/* Static field plus getter accessor inside a class */\nclass C {\n    static value = 1\n    static get inc() { return ++this.value }\n}\n\n/* New “accessor” keyword (TS 5.4 Stage-3) */\nclass Modern {\n    accessor score = 0\n}\n\n/* Template-literal & conditional type aliases */\ntype Route<T extends string> = `/api/${T}`\ntype Flatten<T> = T extends (infer U)[] ? U : T\n\n/* Module augmentation / ambient global */\ndeclare global {\n    interface Window { myLib: unknown }\n}"
  },
  {
    "path": "app/server/syntax/file_map/examples/yaml_example.yaml",
    "content": "\n# Document metadata\n# Example YAML file demonstrating various syntax elements\n---\nname: YAML Example\nversion: 1.0.0\ndescription: Example YAML file demonstrating various syntax elements\nupdated_at: 2024-02-20T15:00:00Z\n\n# Scalar types\nstrings:\n  simple: Hello World\n  quoted: \"Hello \\\"quoted\\\" World\"\n  literal: |\n    This is a literal block\n    that preserves newlines\n    and can contain special chars: *&[]\n  folded: >\n    This is a folded block\n    that will be rendered\n    as a single line\n\nnumbers:\n  integer: 42\n  float: 3.14159\n  scientific: 1.23e+4\n  infinity: .inf\n  not_a_number: .nan\n\ndates:\n  simple: 2024-02-20\n  datetime: 2024-02-20T15:00:00Z\n  canonical: 2024-02-20T15:00:00.000Z\n\nbooleans:\n  true_values: [true, True, TRUE, yes, Yes, YES, on, On, ON]\n  false_values: [false, False, FALSE, no, No, NO, off, Off, OFF]\n\n# Complex types\narrays:\n  - simple\n  - items\n  - [nested, array]\n  - \n    - deeply\n    - nested\n    - array\n\n# Mapping types\nobject:\n  key1: value1\n  key2: value2\n  nested:\n    key3: value3\n    key4: value4\n\n# Anchors and aliases\ndefinitions: &defaults\n  timeout: 30\n  retries: 3\n  enabled: true\n\nproduction:\n  <<: *defaults  # Merge defaults\n  environment: production\n  host: prod.example.com\n\nstaging:\n  <<: *defaults\n  environment: staging\n  host: staging.example.com\n  timeout: 60  # Override default\n\n# Complex mapping\nservices:\n  - name: web\n    image: nginx:latest\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    environment:\n      - NODE_ENV=production\n      - DEBUG=false\n    volumes:\n      - type: bind\n        source: /var/www\n        target: /usr/share/nginx/html\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n\n  - name: database\n    image: postgres:13\n    environment:\n      POSTGRES_USER: admin\n      POSTGRES_PASSWORD: !vault |\n        $ANSIBLE_VAULT;1.1;AES256\n        31613262363135363132666363366363\n    volumes:\n      - data:/var/lib/postgresql/data\n\n# Sets\nunique_values: !!set\n  ? item1\n  ? item2\n  ? item3\n\n# Binary data\ncertificate: !!binary |\n  R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOf...\n\n# Custom tags\ndatetime_obj: !!python/object:datetime.datetime\n  year: 2024\n  month: 2\n  day: 20\n  hour: 15\n  minute: 0\n  second: 0\n\n# Explicit typing\nexplicit_strings: !!str 42\nexplicit_int: !!int \"42\"\nexplicit_float: !!float \"3.14159\"\nexplicit_bool: !!bool \"yes\"\n\n\n"
  },
  {
    "path": "app/server/syntax/file_map/map.go",
    "content": "package file_map\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"plandex-server/syntax\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\ttree_sitter \"github.com/smacker/go-tree-sitter\"\n)\n\nvar verboseLogging = os.Getenv(\"VERBOSE_LOGGING\") == \"true\"\n\n// FileMap represents a file's important definitions\ntype FileMap struct {\n\tDefinitions []Definition\n}\n\ntype Definition struct {\n\tType      string       // \"function\", \"class\", \"key\", \"selector\", \"instruction\" etc\n\tSignature string       // The full signature/header without implementation\n\tComments  []string     // Any comments that precede this definition\n\tTagAttrs  []string     // For xml style markup tags, the class and id attributes\n\tTagReps   int          // For tags, the number of times this tag is repeated\n\tLine      int          // Line number where definition starts\n\tChildren  []Definition // For parent types that can contain nested definitions\n}\n\ntype Node struct {\n\tType   string\n\tLang   shared.Language\n\tTsNode *tree_sitter.Node\n\tBytes  []byte\n}\n\nfunc MapFile(ctx context.Context, filename string, content []byte) (*FileMap, error) {\n\tif !shared.HasFileMapSupport(filename) {\n\t\t// return nil, fmt.Errorf(\"unsupported file type: %s\", filename)\n\t\treturn &FileMap{\n\t\t\tDefinitions: []Definition{\n\t\t\t\t{\n\t\t\t\t\tType:      \"no_map\",\n\t\t\t\t\tSignature: \"[NO MAP]\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tlang := syntax.GetLanguageForPath(filename)\n\n\tif lang == \"\" {\n\t\t// return nil, fmt.Errorf(\"unsupported file type: %s\", ext)\n\t\treturn &FileMap{\n\t\t\tDefinitions: []Definition{},\n\t\t}, nil\n\t}\n\n\tif !shared.IsTreeSitterLanguage(lang) {\n\t\tswitch lang {\n\t\tcase shared.LanguageMarkdown:\n\t\t\treturn &FileMap{\n\t\t\t\tDefinitions: mapMarkdownSimple(content),\n\t\t\t}, nil\n\t\tdefault:\n\t\t\t// return nil, fmt.Errorf(\"unsupported file type: %s\", ext)\n\t\t\treturn &FileMap{\n\t\t\t\tDefinitions: []Definition{},\n\t\t\t}, nil\n\t\t}\n\t}\n\n\t// Get appropriate parser\n\tvar parser *tree_sitter.Parser\n\tvar fallbackParser *tree_sitter.Parser\n\tvar fallbackLang shared.Language\n\n\tparser, lang, fallbackParser, fallbackLang = syntax.GetParserForPath(filename)\n\n\tif parser == nil && fallbackParser == nil {\n\t\t// return nil, fmt.Errorf(\"unsupported file type: %s\", ext)\n\t\treturn &FileMap{\n\t\t\tDefinitions: []Definition{},\n\t\t}, nil\n\t}\n\n\tif parser != nil {\n\t\tdefer parser.Close()\n\t}\n\n\tif fallbackParser != nil {\n\t\tdefer fallbackParser.Close()\n\t}\n\n\tvar tree *tree_sitter.Tree\n\tvar err error\n\n\tif parser != nil {\n\t\t// Parse file\n\t\ttree, err = parser.ParseCtx(ctx, nil, content)\n\t\tif err != nil {\n\t\t\t// return nil, fmt.Errorf(\"failed to parse file: %v\", err)\n\t\t\treturn &FileMap{\n\t\t\t\tDefinitions: []Definition{},\n\t\t\t}, nil\n\t\t}\n\t\tdefer tree.Close()\n\t}\n\n\tif tree == nil || tree.RootNode().Type() == \"error\" {\n\t\tfallbackTree, err := fallbackParser.ParseCtx(ctx, nil, content)\n\t\tif err != nil {\n\t\t\t// return nil, fmt.Errorf(\"failed to parse file: %v\", err)\n\t\t\treturn &FileMap{\n\t\t\t\tDefinitions: []Definition{},\n\t\t\t}, nil\n\t\t}\n\t\tdefer fallbackTree.Close()\n\n\t\tif fallbackTree.RootNode().Type() != \"error\" {\n\t\t\treturn &FileMap{\n\t\t\t\tDefinitions: mapNode(fallbackTree.RootNode(), content, fallbackLang),\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn &FileMap{\n\t\tDefinitions: mapNode(tree.RootNode(), content, lang),\n\t}, nil\n\n}\n\nfunc mapNode(node *tree_sitter.Node, content []byte, lang shared.Language) []Definition {\n\tswitch lang {\n\tcase shared.LanguageHtml:\n\t\treturn mapMarkup(content)\n\tcase shared.LanguageSvelte:\n\t\treturn mapSvelte(content)\n\tdefault:\n\t\treturn mapTraditional(Node{\n\t\t\tLang:   lang,\n\t\t\tTsNode: node,\n\t\t\tBytes:  content,\n\t\t}, nil)\n\t}\n}\n\n// For traditional programming languages\nfunc mapTraditional(baseNode Node, parentNode *Node) []Definition {\n\tvar defs []Definition\n\tcursor := tree_sitter.NewTreeCursor(baseNode.TsNode)\n\tdefer cursor.Close()\n\n\t// potentially too much output even for verbose logging — uncomment if you need to see the full tree\n\t// if verboseLogging {\n\t// \tfmt.Println(\"mapTraditional\", baseNode.TsNode)\n\t// }\n\n\tif cursor.GoToFirstChild() {\n\t\tfor {\n\t\t\ttsNode := cursor.CurrentNode()\n\t\t\tnode := Node{\n\t\t\t\tType:   tsNode.Type(),\n\t\t\t\tLang:   baseNode.Lang,\n\t\t\t\tTsNode: tsNode,\n\t\t\t\tBytes:  baseNode.Bytes,\n\t\t\t}\n\n\t\t\tif isIncludeAndContinueNode(node) {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"include and continue node\", cursor.CurrentNode().Type())\n\t\t\t\t}\n\t\t\t\tif !cursor.GoToNextSibling() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println()\n\t\t\t\tfmt.Println(\"node\", node.Type)\n\t\t\t\t// fmt.Println(\"content\", string(node.Content(content)))\n\t\t\t\tfmt.Println()\n\t\t\t}\n\n\t\t\t// Check if this is a definition node\n\t\t\tif isDefinitionNode(node, parentNode) {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"definition node\", node.Type)\n\t\t\t\t}\n\n\t\t\t\tdef := Definition{\n\t\t\t\t\tType: node.Type,\n\t\t\t\t\tLine: int(tsNode.StartPoint().Row) + 1,\n\t\t\t\t}\n\n\t\t\t\tif isAssignmentNode(node) {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Println(\"assignment node\", node.Type)\n\t\t\t\t\t}\n\t\t\t\t\t// Try different field names for identifiers\n\t\t\t\t\t// fmt.Printf(\"assignment node: %s\\n\", node.Type)\n\t\t\t\t\tsig := \"\"\n\n\t\t\t\t\tassignmentBoundary := findAssignmentBoundary(node)\n\n\t\t\t\t\tif assignmentBoundary != nil {\n\t\t\t\t\t\tstart := tsNode.StartByte()\n\t\t\t\t\t\tend := assignmentBoundary.TsNode.StartByte()\n\t\t\t\t\t\tsig = string(node.Bytes[start:end])\n\t\t\t\t\t\tsig = strings.TrimSuffix(strings.TrimSpace(sig), \"=\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tidentifiers := findIdentifier(node)\n\t\t\t\t\t\tif len(identifiers) > 0 {\n\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\tfmt.Println(\"found identifiers\", len(identifiers))\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tstart := tsNode.StartByte()\n\t\t\t\t\t\t\tend := identifiers[len(identifiers)-1].TsNode.EndByte()\n\t\t\t\t\t\t\tsig = string(node.Bytes[start:end])\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\tfmt.Println(\"no identifier found\", node.Type)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsig = string(node.TsNode.Content(node.Bytes)) + \" \"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tdef.Signature = sig\n\t\t\t\t} else if isPassThroughParentNode(node) {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Println(\"pass through parent node\", node.Type)\n\t\t\t\t\t}\n\n\t\t\t\t\tstart := tsNode.StartByte()\n\n\t\t\t\t\tfirstChild := firstDefinitionChild(node)\n\t\t\t\t\tif firstChild != nil {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"firstChild\", firstChild.Type)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tend := firstChild.TsNode.StartByte()\n\n\t\t\t\t\t\tsig := string(node.Bytes[start:end])\n\t\t\t\t\t\tsig = strings.TrimSpace(sig)\n\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"got pass through parent signature\", def.Signature)\n\t\t\t\t\t\t\tfmt.Println(\"recursing into first child\", firstChild.Type)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tchildren := mapTraditional(node, nil)\n\n\t\t\t\t\t\tif sig == \"\" {\n\t\t\t\t\t\t\t// collapse if signature is empty\n\t\t\t\t\t\t\tif len(children) > 0 {\n\t\t\t\t\t\t\t\tsig = children[0].Signature\n\t\t\t\t\t\t\t\tgrandchildren := children[0].Children\n\t\t\t\t\t\t\t\tsibs := children[1:]\n\t\t\t\t\t\t\t\tchildren = append(grandchildren, sibs...)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdef.Signature = sig\n\t\t\t\t\t\tdef.Children = children\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"no first child found\", node.Type)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Println(\"not assignment node\", node.Type)\n\t\t\t\t\t\tfmt.Println(\"looking for implementation boundary\")\n\t\t\t\t\t}\n\t\t\t\t\t// Get signature (up to body)\n\t\t\t\t\tif body := findImplementationBoundary(node); body != nil {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"found implementation boundary\", body.Type)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tstart := tsNode.StartByte()\n\t\t\t\t\t\tvar end uint32\n\t\t\t\t\t\tif tsNode == body.TsNode {\n\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\tfmt.Println(\"node == body\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfirstChild := firstDefinitionChild(*body)\n\t\t\t\t\t\t\tif firstChild != nil {\n\t\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\t\tfmt.Println(\"firstChild != nil\")\n\t\t\t\t\t\t\t\t\tfmt.Println(\"firstChild\", firstChild.Type)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tend = firstChild.TsNode.StartByte()\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\t\tfmt.Println(\"firstChild == nil\")\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tend = body.TsNode.EndByte()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tend = body.TsNode.StartByte()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"start\", start)\n\t\t\t\t\t\t\tfmt.Println(\"end\", end)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdef.Signature = string(node.Bytes[start:end])\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"got signature\", def.Signature)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// If this is a parent type node, recurse into the body\n\t\t\t\t\t\tif isParentNode(node) {\n\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\tfmt.Println(\"isParentNode, recursing into body\", node.Type)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdef.Children = mapTraditional(*body, &node)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"no implementation boundary found\", node.Type)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdef.Signature = string(node.TsNode.Content(node.Bytes))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Get preceding comments\n\t\t\t\t// no comments for now to minimize tokens\n\t\t\t\t// def.Comments = getPrecedingComments(node)\n\n\t\t\t\tdefs = append(defs, def)\n\t\t\t} else {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"not definition node\", node.Type)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !cursor.GoToNextSibling() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn defs\n}\n\n// // Get preceding comments\n// func getPrecedingComments(node Node) []string {\n// \tvar comments []string\n// \tconst maxCommentLength = 1000\n\n// \tprevNode := node.TsNode.PrevSibling()\n// \tfor prevNode != nil {\n// \t\tif !strings.Contains(prevNode.Type(), \"comment\") {\n// \t\t\tbreak\n// \t\t}\n// \t\tcomment := string(prevNode.Content(node.Bytes))\n// \t\tif len(comment) > maxCommentLength {\n// \t\t\tcomment = comment[:maxCommentLength] + \"...\"\n// \t\t}\n// \t\tcomments = append([]string{comment}, comments...)\n// \t\tprevNode = prevNode.PrevSibling()\n// \t}\n// \treturn comments\n// }\n\n// func mapConfig(node *tree_sitter.Node, content []byte) []Definition {\n// \tcursor := tree_sitter.NewTreeCursor(node)\n// \tdefer cursor.Close()\n\n// \tvar walkConfig func(*tree_sitter.Node) []Definition\n// \twalkConfig = func(node *tree_sitter.Node) []Definition {\n// \t\tvar defs []Definition\n\n// \t\t// Handle key-value pairs\n// \t\tswitch node.Type() {\n// \t\tcase \"block_mapping_pair\": // YAML\n// \t\t\tif key := node.ChildByFieldName(\"key\"); key != nil {\n// \t\t\t\tdef := Definition{\n// \t\t\t\t\tType:      \"key\",\n// \t\t\t\t\tSignature: string(key.Content(content)),\n// \t\t\t\t\tLine:      int(key.StartPoint().Row) + 1,\n// \t\t\t\t}\n\n// \t\t\t\t// Handle nested structures\n// \t\t\t\tif val := node.ChildByFieldName(\"value\"); val != nil {\n// \t\t\t\t\tswitch val.Type() {\n// \t\t\t\t\tcase \"block_mapping\": // nested YAML map\n// \t\t\t\t\t\tdef.Children = walkConfig(val)\n// \t\t\t\t\tcase \"block_sequence\": // YAML array\n// \t\t\t\t\t\t// Could track sequences if needed\n// \t\t\t\t\t}\n// \t\t\t\t}\n\n// \t\t\t\tdefs = append(defs, def)\n// \t\t\t}\n// \t\tcase \"pair\": // TOML/JSON\n// \t\t\t// Similar pattern for TOML/JSON\n// \t\tcase \"field\": // CUE/HCL\n// \t\t\t// Similar pattern for CUE/HCL\n// \t\t}\n\n// \t\treturn defs\n// \t}\n\n// \treturn walkConfig(node)\n// }\n\nfunc (m *FileMap) String() string {\n\tvar b strings.Builder\n\n\tvar writeDefinition func(def *Definition, depth int)\n\twriteDefinition = func(def *Definition, depth int) {\n\t\tif def.Type == \"svelte-style\" {\n\t\t\tb.WriteString(\"\\n\")\n\t\t}\n\n\t\t// Indent\n\t\tif depth > 0 {\n\t\t\tb.WriteString(strings.Repeat(\"  \", depth))\n\t\t\tb.WriteString(\"- \")\n\t\t}\n\n\t\t// Write signature (for tags, include attrs)\n\t\tif def.Type == \"tag\" {\n\t\t\t// Extract tag name from signature (it's the first word)\n\t\t\ttagName := strings.Fields(def.Signature)[0]\n\t\t\t// Build full representation with attrs\n\t\t\tif len(def.TagAttrs) > 0 {\n\t\t\t\tif def.TagReps > 1 {\n\t\t\t\t\tb.WriteString(fmt.Sprintf(\"[%dx]\", def.TagReps))\n\t\t\t\t}\n\t\t\t\tb.WriteString(fmt.Sprintf(\"%s%s\", tagName, strings.Join(def.TagAttrs, \"\")))\n\t\t\t} else {\n\t\t\t\tb.WriteString(tagName)\n\t\t\t}\n\t\t} else {\n\t\t\tb.WriteString(strings.TrimSpace(def.Signature))\n\t\t}\n\t\tb.WriteString(\"\\n\")\n\n\t\t// Write children with increased depth\n\t\tfor _, child := range def.Children {\n\t\t\twriteDefinition(&child, depth+1)\n\t\t}\n\n\t\tif def.Type == \"svelte-script\" {\n\t\t\tb.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\t// Write all top-level definitions\n\tfor _, def := range m.Definitions {\n\t\twriteDefinition(&def, 0)\n\t}\n\n\treturn b.String()\n}\n\nfunc mapMarkdownSimple(content []byte) []Definition {\n\tvar defs []Definition\n\tlines := strings.Split(string(content), \"\\n\")\n\n\tfor i, line := range lines {\n\t\ttrimmedLine := strings.TrimSpace(line)\n\n\t\t// Skip empty lines\n\t\tif trimmedLine == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check for ATX headings (# style)\n\t\tif strings.HasPrefix(trimmedLine, \"#\") {\n\t\t\theading := trimmedLine\n\t\t\tlevel := 0\n\t\t\t// Count heading level\n\t\t\tfor strings.HasPrefix(heading, \"#\") {\n\t\t\t\tlevel++\n\t\t\t\theading = strings.TrimPrefix(heading, \"#\")\n\t\t\t}\n\t\t\theading = strings.TrimSpace(heading)\n\n\t\t\t// Only add if there's actual heading content\n\t\t\tif heading != \"\" {\n\t\t\t\tdefs = append(defs, Definition{\n\t\t\t\t\tType:      fmt.Sprintf(\"h%d\", level),\n\t\t\t\t\tSignature: heading,\n\t\t\t\t\tLine:      i + 1,\n\t\t\t\t})\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Check for Setext headings (=== or --- style)\n\t\tif i > 0 && len(trimmedLine) > 0 {\n\t\t\t// Check if line consists entirely of = or -\n\t\t\tisAllEquals := strings.TrimSpace(strings.ReplaceAll(trimmedLine, \"=\", \"\")) == \"\"\n\t\t\tisAllDashes := strings.TrimSpace(strings.ReplaceAll(trimmedLine, \"-\", \"\")) == \"\"\n\n\t\t\t// Must have at least 2 characters and previous line must not be empty\n\t\t\tprevLine := strings.TrimSpace(lines[i-1])\n\t\t\tif len(trimmedLine) >= 2 && prevLine != \"\" {\n\t\t\t\tif isAllEquals {\n\t\t\t\t\t// Level 1 heading\n\t\t\t\t\tdefs = append(defs, Definition{\n\t\t\t\t\t\tType:      \"h1\",\n\t\t\t\t\t\tSignature: prevLine,\n\t\t\t\t\t\tLine:      i, // Use previous line's number\n\t\t\t\t\t})\n\t\t\t\t} else if isAllDashes {\n\t\t\t\t\t// Level 2 heading\n\t\t\t\t\tdefs = append(defs, Definition{\n\t\t\t\t\t\tType:      \"h2\",\n\t\t\t\t\t\tSignature: prevLine,\n\t\t\t\t\t\tLine:      i, // Use previous line's number\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn defs\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/markup.go",
    "content": "package file_map\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org/x/net/html\"\n)\n\nfunc mapMarkup(content []byte) []Definition {\n\treader := bytes.NewReader(content)\n\tdoc, err := html.Parse(reader)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tvar walk func(*html.Node) []Definition\n\twalk = func(n *html.Node) []Definition {\n\t\tvar defs []Definition\n\n\t\tif n.Type == html.ElementNode {\n\t\t\t// Only track semantically significant elements\n\t\t\tif isSignificantTag(n.Data) {\n\t\t\t\tdef := Definition{\n\t\t\t\t\tType:      \"tag\",\n\t\t\t\t\tSignature: n.Data,\n\t\t\t\t}\n\n\t\t\t\t// Only include semantic classes/ids\n\t\t\t\tfor _, attr := range n.Attr {\n\t\t\t\t\tif attr.Key == \"id\" {\n\t\t\t\t\t\tdef.TagAttrs = append(def.TagAttrs, fmt.Sprintf(\"#%s\", attr.Val))\n\t\t\t\t\t} else if attr.Key == \"class\" {\n\t\t\t\t\t\tclasses := strings.Fields(attr.Val)\n\t\t\t\t\t\tif len(classes) > 3 {\n\t\t\t\t\t\t\tclasses = classes[:3]\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdef.TagAttrs = append(def.TagAttrs, fmt.Sprintf(\".%s\", strings.Join(classes, \".\")))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Get children of this element\n\t\t\t\tdef.Children = []Definition{}\n\t\t\t\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\t\t\t\tdef.Children = append(def.Children, walk(c)...)\n\t\t\t\t}\n\n\t\t\t\tdefs = append(defs, def)\n\t\t\t}\n\t\t}\n\n\t\t// Only process siblings for non-significant elements\n\t\tif !isSignificantTag(n.Data) {\n\t\t\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\t\t\tdefs = append(defs, walk(c)...)\n\t\t\t}\n\t\t}\n\n\t\treturn defs\n\t}\n\n\tdefs := walk(doc)\n\tdefs = consolidateRepeatedTags(defs)\n\treturn defs\n}\n\n// Helper function to check if two definitions are equivalent\nfunc areMarkupDefinitionsEqual(a, b Definition) bool {\n\tif a.Type != b.Type || a.Signature != b.Signature || len(a.TagAttrs) != len(b.TagAttrs) {\n\t\treturn false\n\t}\n\n\t// Compare attributes\n\tfor i, attr := range a.TagAttrs {\n\t\tif attr != b.TagAttrs[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// Helper function to consolidate repeated tags\nfunc consolidateRepeatedTags(defs []Definition) []Definition {\n\tvar result []Definition\n\n\tfirstDef := defs[0]\n\tcount := 1\n\tallEqual := true\n\n\t// fmt.Printf(\"consolidateRepeatedTags: checking %d definitions for equality\\n\", len(defs))\n\t// spew.Dump(defs)\n\n\tif len(defs) > 1 {\n\t\tfor i, def := range defs {\n\t\t\tif len(def.Children) > 0 {\n\t\t\t\t// fmt.Printf(\"consolidateRepeatedTags: definition %d has children, cannot consolidate\\n\", i)\n\t\t\t\tallEqual = false\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif !areMarkupDefinitionsEqual(firstDef, def) {\n\t\t\t\t// fmt.Printf(\"consolidateRepeatedTags: definition %d is not equal to first definition\\n\", i)\n\t\t\t\tallEqual = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcount++\n\t\t}\n\t}\n\n\tif allEqual && count > 1 {\n\t\t// fmt.Printf(\"consolidateRepeatedTags: consolidated %d equal definitions\\n\", count)\n\t\tfirstDef.TagReps = count\n\t\tresult = []Definition{firstDef}\n\t} else {\n\t\t// fmt.Printf(\"consolidateRepeatedTags: definitions not equal, keeping original %d definitions\\n\", len(defs))\n\t\tresult = defs\n\t}\n\n\tfor i := range result {\n\t\tdef := &result[i]\n\t\tif len(def.Children) > 0 {\n\t\t\t// fmt.Printf(\"consolidateRepeatedTags: recursively consolidating children of definition %d\\n\", i)\n\t\t\tdef.Children = consolidateRepeatedTags(def.Children)\n\t\t}\n\t}\n\n\treturn result\n}\n\nvar significantHtmlTags = map[string]bool{\n\t\"html\":     true,\n\t\"head\":     true,\n\t\"body\":     true,\n\t\"main\":     true,\n\t\"nav\":      true,\n\t\"header\":   true,\n\t\"footer\":   true,\n\t\"article\":  true,\n\t\"section\":  true,\n\t\"form\":     true,\n\t\"dialog\":   true,\n\t\"template\": true,\n\t\"table\":    true,\n\t\"div\":      true,\n\t\"ul\":       true,\n\t\"aside\":    true,\n}\n\nfunc isSignificantTag(tag string) bool {\n\treturn significantHtmlTags[tag]\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/multi.go",
    "content": "package file_map\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"path/filepath\"\n\t\"plandex-server/syntax\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\tshared \"plandex-shared\"\n\n\ttree_sitter \"github.com/smacker/go-tree-sitter\"\n)\n\n// No longer used - all mapping is done via GetFileMap handler\n// handles concurrent processing of multiple files for mapping\nfunc ProcessMapFiles(ctx context.Context, inputs map[string]string) (shared.FileMapBodies, error) {\n\tlog.Println(\"ProcessMapFiles\")\n\tbodies := make(shared.FileMapBodies, len(inputs))\n\tvar mu sync.Mutex\n\terrCh := make(chan error, len(inputs))\n\n\t// Use half of available CPUs\n\tcpus := runtime.NumCPU()\n\tlog.Printf(\"ProcessMapFiles: Available CPUs: %d\", cpus)\n\tmaxWorkers := cpus / 2\n\tif maxWorkers < 1 {\n\t\tmaxWorkers = 1 // Ensure at least one worker\n\t}\n\tlog.Printf(\"ProcessMapFiles: Max workers: %d\", maxWorkers)\n\tsem := make(chan struct{}, maxWorkers)\n\n\tfor path, content := range inputs {\n\t\tif !shared.HasFileMapSupport(path) {\n\t\t\tmu.Lock()\n\t\t\tbodies[path] = \"[NO MAP]\"\n\t\t\tmu.Unlock()\n\t\t\terrCh <- nil\n\t\t\tcontinue\n\t\t} else if len(content) > shared.MaxContextMapSingleInputSize { // 1MB\n\t\t\tmu.Lock()\n\t\t\tbodies[path] = \"[NO MAP - TOO LARGE]\"\n\t\t\tmu.Unlock()\n\t\t\terrCh <- nil\n\t\t\tcontinue\n\t\t}\n\n\t\tsem <- struct{}{}\n\t\tgo func(path, content string) {\n\t\t\tdefer func() { <-sem }()\n\t\t\tfileMap, err := MapFile(ctx, path, []byte(content))\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"error mapping file %s: %v\", path, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\t\t\tbodies[path] = fileMap.String()\n\t\t\terrCh <- nil\n\t\t}(path, content)\n\t}\n\n\tfor i := 0; i < len(inputs); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn bodies, nil\n}\n\ntype MapTrees map[string]string\n\nfunc ProcessMapTrees(ctx context.Context, inputs map[string]string) (MapTrees, error) {\n\ttrees := make(MapTrees, len(inputs))\n\tvar mu sync.Mutex\n\terrCh := make(chan error, len(inputs))\n\n\tfor path, content := range inputs {\n\t\tgo func(path, content string) {\n\t\t\t// Get appropriate parser\n\t\t\tvar parser *tree_sitter.Parser\n\t\t\tfile := filepath.Base(path)\n\t\t\tif strings.Contains(strings.ToLower(file), \"dockerfile\") {\n\t\t\t\tparser = syntax.GetParserForLanguage(shared.LanguageDockerfile)\n\t\t\t} else {\n\t\t\t\tparser, _, _, _ = syntax.GetParserForPath(path)\n\n\t\t\t\tif parser == nil {\n\t\t\t\t\terrCh <- fmt.Errorf(\"unsupported file type: %s\", path)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontentBytes := []byte(content)\n\n\t\t\t// Parse file\n\t\t\ttree, err := parser.ParseCtx(ctx, nil, contentBytes)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"failed to parse file: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer tree.Close()\n\n\t\t\tmu.Lock()\n\t\t\tdefer mu.Unlock()\n\t\t\ttrees[path] = string(tree.RootNode().String())\n\t\t\terrCh <- nil\n\t\t}(path, content)\n\t}\n\n\tfor i := 0; i < len(inputs); i++ {\n\t\terr := <-errCh\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn trees, nil\n}\n\nfunc (m MapTrees) CombinedTrees() string {\n\tvar combinedMap strings.Builder\n\tpaths := make([]string, 0, len(m))\n\tfor path := range m {\n\t\tpaths = append(paths, path)\n\t}\n\tsort.Strings(paths)\n\tfor _, path := range paths {\n\t\tbody := m[path]\n\t\tbody = strings.TrimSpace(body)\n\t\tfileHeading := fmt.Sprintf(\"\\n### %s\\n\", path)\n\t\tcombinedMap.WriteString(fileHeading)\n\t\tif body == \"\" {\n\t\t\tcombinedMap.WriteString(\"[NO MAP]\\n\")\n\t\t} else {\n\t\t\tcombinedMap.WriteString(body)\n\t\t}\n\t\tcombinedMap.WriteString(\"\\n\")\n\t}\n\treturn combinedMap.String()\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/nodes_config.go",
    "content": "package file_map\n\nimport shared \"plandex-shared\"\n\ntype nodeType string\n\ntype matchType string\n\nconst (\n\tmatchTypeEqual  matchType = \"equal\"\n\tmatchTypePrefix matchType = \"prefix\"\n\tmatchTypeSuffix matchType = \"suffix\"\n)\n\ntype langSet map[shared.Language]bool\n\ntype nodeConfig struct {\n\tnodeMatch    matchType\n\tignore       bool\n\tall          bool\n\texcept       langSet\n\tlanguages    langSet\n\tonlyChildren map[nodeType]bool\n\tskipForward  map[nodeType]bool\n}\n\ntype nodeMap map[nodeType]nodeConfig\n\nvar assignmentNodeMap = nodeMap{\n\t// Common patterns across many languages\n\t\"assignment_expression\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"variable_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"variable_assignment\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\n\t\"const_spec\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo: true,\n\t\t},\n\t},\n\t\"var_spec\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo: true,\n\t\t},\n\t},\n\n\t\"lexical_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t},\n\t},\n\n\t\"field_definition\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t},\n\t},\n\n\t\"interface_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp: true,\n\t\t},\n\t},\n\n\t\"trait_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp: true,\n\t\t},\n\t},\n\n\t\"enum_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp:    true,\n\t\t\tshared.LanguageCsharp: true,\n\t\t},\n\t},\n\n\t\"global_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp: true,\n\t\t},\n\t},\n\t\"static_variable_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp: true,\n\t\t},\n\t},\n\n\t\"type_alias_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageElm:        true,\n\t\t},\n\t},\n\n\t\"assignment_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePython: true,\n\t\t},\n\t},\n\n\t\"class_variable_assignment\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\t\"assignment\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\n\t\"let_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n\t\"const_item\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n\n\t\"static_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageJava:   true,\n\t\t\tshared.LanguageCsharp: true,\n\t\t},\n\t},\n\t\"member_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageJava:   true,\n\t\t\tshared.LanguageCsharp: true,\n\t\t},\n\t},\n\t\"field_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageJava:   true,\n\t\t\tshared.LanguageCsharp: true,\n\t\t},\n\t},\n\n\t\"property_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageKotlin: true,\n\t\t\tshared.LanguageScala:  true,\n\t\t\tshared.LanguagePhp:    true,\n\t\t\tshared.LanguageSwift:  true,\n\t\t},\n\t},\n\n\t\"let_binding\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageOCaml: true,\n\t\t},\n\t},\n\t\"value_binding\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageOCaml: true,\n\t\t},\n\t},\n\n\t\"unary_operator\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElixir: true,\n\t\t},\n\t},\n\n\t\"value_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElm: true,\n\t\t},\n\t},\n\n\t\"declaration_command\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageBash: true,\n\t\t},\n\t},\n\n\t\"type_item\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n\n\t\"val_definition\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageScala: true,\n\t\t},\n\t},\n\n\t\"type_definition\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageScala: true,\n\t\t},\n\t},\n\n\t\"typealias_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageSwift: true,\n\t\t},\n\t},\n\n\t\"_field_definition\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t},\n\t},\n}\n\nvar definitionNodeMap = nodeMap{\n\t// Common patterns across languages\n\t\"_definition\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"_declaration\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"_declarator\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"_spec\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"_binding\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"_signature\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"_specifier\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"interface_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tall:       true,\n\t},\n\n\t// Language-specific definitions\n\t\"def\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\t\"method\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\t\"defmodule\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElixir: true,\n\t\t},\n\t},\n\t\"defmacro\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElixir: true,\n\t\t},\n\t},\n\t\"port\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElm: true,\n\t\t},\n\t},\n\t\"functor\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageOCaml: true,\n\t\t},\n\t},\n\t\"_def\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageC:   true,\n\t\t\tshared.LanguageCpp: true,\n\t\t},\n\t},\n\t\"type_annotation\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElm: true,\n\t\t},\n\t},\n\t\"function_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageLua: true,\n\t\t},\n\t},\n\t\"rule_set\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageCss: true,\n\t\t},\n\t},\n\t\"from_instruction\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageDockerfile: true,\n\t\t},\n\t},\n\t\"entrypoint_instruction\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageDockerfile: true,\n\t\t},\n\t},\n\t\"cmd_instruction\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageDockerfile: true,\n\t\t},\n\t},\n\t\"expose_instruction\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageDockerfile: true,\n\t\t},\n\t},\n\t\"copy_instruction\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageDockerfile: true,\n\t\t},\n\t},\n\t\"env_instruction\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageDockerfile: true,\n\t\t},\n\t},\n\n\t// Ignored patterns\n\t\"import_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tignore:    true,\n\t\tall:       true,\n\t},\n\t\"require_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tignore:    true,\n\t\tall:       true,\n\t},\n\t\"use_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tignore:    true,\n\t\tall:       true,\n\t},\n\t\"namespace_use_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tignore:    true,\n\t\tall:       true,\n\t},\n\t\"using_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tignore:    true,\n\t\tall:       true,\n\t},\n\t\"include_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tignore:    true,\n\t\tall:       true,\n\t},\n\t\"package_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tignore:    true,\n\t\tall:       true,\n\t},\n\t\"_include\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tignore:    true,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageC:   true,\n\t\t\tshared.LanguageCpp: true,\n\t\t},\n\t},\n\t\"_item\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n}\n\nvar parentNodeMap = nodeMap{\n\t// Common patterns\n\t\"class_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tall:       true,\n\t},\n\t\"module_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tall:       true,\n\t},\n\t\"namespace_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tall:       true,\n\t},\n\n\t// Language-specific parents\n\t\"object\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageScala: true,\n\t\t},\n\t},\n\t\"object_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageKotlin: true,\n\t\t},\n\t},\n\t\"protocol\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageSwift: true,\n\t\t},\n\t},\n\t\"protocol_\": {\n\t\tnodeMatch: matchTypePrefix,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElixir: true,\n\t\t},\n\t},\n\t\"extension\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageSwift: true,\n\t\t},\n\t},\n\n\t\"const_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo: true,\n\t\t},\n\t},\n\n\t\"var_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo: true,\n\t\t},\n\t},\n\n\t\"template_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageCpp: true,\n\t\t},\n\t},\n\n\t\"export_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t},\n\t\tonlyChildren: map[nodeType]bool{\n\t\t\t\"lexical_declaration\":    true,\n\t\t\t\"variable_declaration\":   true,\n\t\t\t\"type_alias_declaration\": true,\n\t\t\t\"interface_declaration\":  true,\n\t\t\t\"enum_declaration\":       true,\n\t\t\t\"class_declaration\":      true,\n\t\t\t\"function_declaration\":   true,\n\t\t\t\"arrow_function\":         true,\n\t\t},\n\t},\n\n\t\"module\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\n\t\"class\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\n\t\"singleton_class\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\n\t\"enum_entry\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageKotlin: true,\n\t\t},\n\t},\n\n\t\"closure\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGroovy: true,\n\t\t},\n\t},\n\n\t\"impl_item\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n\n\t\"trait_item\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n\n\t\"trait_definition\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageScala: true,\n\t\t},\n\t},\n\n\t\"object_definition\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageScala: true,\n\t\t},\n\t},\n\n\t\"function_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageLua: true,\n\t\t},\n\t\tonlyChildren: map[nodeType]bool{\n\t\t\t\"function_statement\": true,\n\t\t},\n\t},\n\n\t\"internal_module\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t},\n\t},\n\n\t\"expression_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t},\n\t\tonlyChildren: map[nodeType]bool{\n\t\t\t\"internal_module\": true,\n\t\t},\n\t},\n\n\t\"ambient_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t},\n\t},\n\n\t\"interface_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t},\n\t},\n}\n\nvar implBoundaryNodeMap = nodeMap{\n\t// Common patterns\n\t\"block\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t\texcept: langSet{\n\t\t\tshared.LanguageRuby:   true,\n\t\t\tshared.LanguageElixir: true,\n\t\t},\n\t},\n\t\"body\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"_body\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\t\"compound_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\n\t// Language-specific boundaries\n\t\"do_block\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby:   true,\n\t\t\tshared.LanguageElixir: true,\n\t\t},\n\t},\n\t\"body_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\t\"field_declaration_list\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo:  true,\n\t\t\tshared.LanguageC:   true,\n\t\t\tshared.LanguageCpp: true,\n\t\t},\n\t},\n\t\"property_accessors\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageCsharp: true,\n\t\t\tshared.LanguageSwift:  true,\n\t\t},\n\t},\n\t\"const_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo: true,\n\t\t},\n\t},\n\t\"var_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo: true,\n\t\t},\n\t},\n\t\"method_elem\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageGo: true,\n\t\t},\n\t},\n\n\t\"preproc_arg\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageC:   true,\n\t\t\tshared.LanguageCpp: true,\n\t\t},\n\t},\n\n\t\"statement_block\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t},\n\t},\n\n\t\"declaration_list\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp:    true,\n\t\t\tshared.LanguageCsharp: true,\n\t\t\tshared.LanguageRust:   true,\n\t\t},\n\t},\n\n\t\"_declaration_list\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n\n\t\"enum_variant_list\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRust: true,\n\t\t},\n\t},\n\n\t\"=\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageScala: true,\n\t\t},\n\t},\n}\n\nvar assignmentBoundaryNodeMap = nodeMap{\n\t\"=\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\":=\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"<-\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"expression\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"interface_body\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t},\n\t},\n\t\"declaration_list\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp:    true,\n\t\t\tshared.LanguageCsharp: true,\n\t\t},\n\t},\n\t\"_declaration_list\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp:    true,\n\t\t\tshared.LanguageCsharp: true,\n\t\t},\n\t},\n\t\"lambda_literal\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageKotlin: true,\n\t\t},\n\t},\n\t\"arguments\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElixir: true,\n\t\t},\n\t},\n\t\"eq\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageElm: true,\n\t\t},\n\t},\n\t\"statements\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageSwift: true,\n\t\t},\n\t},\n}\n\nvar assignmentBoundarySkipForwardNodeMap = nodeMap{\n\t\"=\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t},\n\t\tskipForward: map[nodeType]bool{\n\t\t\t\"statement_block\": true,\n\t\t\t\"=>\":              true,\n\t\t},\n\t},\n}\n\nvar identifierNodeMap = nodeMap{\n\t// Common patterns\n\t\"identifier\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tall:       true,\n\t},\n\t\"_identifier\": {\n\t\tnodeMatch: matchTypeSuffix,\n\t\tall:       true,\n\t},\n\n\t// Language-specific identifiers\n\t\"constant\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageRuby: true,\n\t\t},\n\t},\n\t\"name\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguagePhp: true,\n\t\t},\n\t},\n\t\"bare_key\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageToml: true,\n\t\t\tshared.LanguageYaml: true,\n\t\t},\n\t},\n}\n\nvar passThroughParentNodeMap = nodeMap{\n\t\"template_declaration\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageCpp: true,\n\t\t},\n\t},\n\t\"export_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t},\n\t},\n\t\"expression_statement\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageTypescript: true,\n\t\t\tshared.LanguageJavascript: true,\n\t\t\tshared.LanguageTsx:        true,\n\t\t\tshared.LanguageJsx:        true,\n\t\t},\n\t\tonlyChildren: map[nodeType]bool{\n\t\t\t\"internal_module\": true,\n\t\t},\n\t},\n}\n\nvar includeAndContinueNodeMap = nodeMap{\n\t\"template_parameter_list\": {\n\t\tnodeMatch: matchTypeEqual,\n\t\tlanguages: langSet{\n\t\t\tshared.LanguageCpp: true,\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/nodes_find.go",
    "content": "package file_map\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc isDefinitionNode(node Node, parentNode *Node) bool {\n\tsetNodeType(&node)\n\n\tres := false\n\n\tconfig := definitionNodeMap.getConfig(node.Type, node.Lang)\n\n\tvar parentConfig *nodeConfig\n\tif parentNode != nil {\n\t\tparentConfig = parentNodeMap.getConfig(parentNode.Type, parentNode.Lang)\n\t}\n\n\tif config == nil {\n\t\tres = isAssignmentNode(node) || isParentNode(node)\n\t\tif res && parentConfig != nil && parentConfig.onlyChildren != nil {\n\t\t\tres = parentConfig.onlyChildren[nodeType(node.Type)]\n\t\t}\n\t\treturn res\n\t}\n\n\tres = !config.ignore\n\tif res {\n\t\tif parentConfig != nil && parentConfig.onlyChildren != nil {\n\t\t\tres = parentConfig.onlyChildren[nodeType(node.Type)]\n\t\t}\n\t}\n\treturn res\n}\n\nfunc isAssignmentNode(node Node) bool {\n\tsetNodeType(&node)\n\tconfig := assignmentNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn !config.ignore\n}\n\nfunc isParentNode(node Node) bool {\n\tsetNodeType(&node)\n\tconfig := parentNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn !config.ignore\n}\n\nfunc isImplBoundaryNode(node Node) bool {\n\tsetNodeType(&node)\n\tconfig := implBoundaryNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn !config.ignore\n}\n\nfunc isAssignmentBoundaryNode(node Node) bool {\n\tsetNodeType(&node)\n\tconfig := assignmentBoundaryNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn !config.ignore\n}\n\nfunc isIdentifierNode(node Node) bool {\n\tsetNodeType(&node)\n\tconfig := identifierNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn !config.ignore\n}\n\nfunc isPassThroughParentNode(node Node) bool {\n\tsetNodeType(&node)\n\tconfig := passThroughParentNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn !config.ignore\n}\n\nfunc isIncludeAndContinueNode(node Node) bool {\n\tsetNodeType(&node)\n\tconfig := includeAndContinueNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn false\n\t}\n\treturn !config.ignore\n}\n\nfunc (m nodeMap) getConfig(t string, lang shared.Language) *nodeConfig {\n\t// first look for exact match\n\tconfig, ok := m[nodeType(t)]\n\tif ok {\n\t\tif config.all {\n\t\t\tif config.except == nil || !config.except[lang] {\n\t\t\t\treturn &config\n\t\t\t}\n\t\t}\n\n\t\tif config.languages != nil && config.languages[lang] {\n\t\t\treturn &config\n\t\t}\n\t}\n\n\t// then look for prefix or suffix match\n\tvar foundConfig *nodeConfig\n\tfor k, config := range m {\n\t\tvar maybeConfig *nodeConfig\n\t\tif config.nodeMatch == matchTypePrefix && strings.HasPrefix(t, string(k)) {\n\t\t\tmaybeConfig = &config\n\t\t} else if config.nodeMatch == matchTypeSuffix && strings.HasSuffix(t, string(k)) {\n\t\t\tmaybeConfig = &config\n\t\t}\n\n\t\tvar wouldSet *nodeConfig\n\t\tif maybeConfig != nil {\n\t\t\tif maybeConfig.all {\n\t\t\t\tif maybeConfig.except == nil || !maybeConfig.except[lang] {\n\t\t\t\t\twouldSet = maybeConfig\n\t\t\t\t}\n\t\t\t} else if maybeConfig.languages != nil && maybeConfig.languages[lang] {\n\t\t\t\twouldSet = maybeConfig\n\t\t\t}\n\t\t}\n\n\t\tif wouldSet != nil {\n\t\t\tif foundConfig != nil {\n\t\t\t\t// ignore takes precedence\n\t\t\t\tif wouldSet.ignore {\n\t\t\t\t\tfoundConfig = wouldSet\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfoundConfig = wouldSet\n\t\t\t}\n\t\t}\n\t}\n\n\treturn foundConfig\n}\n\nfunc findImplementationBoundary(node Node) *Node {\n\tif isImplBoundaryNode(node) {\n\t\treturn &node\n\t}\n\n\tfor i := 0; i < int(node.TsNode.ChildCount()); i++ {\n\t\tchild := node.TsNode.Child(i)\n\n\t\tif verboseLogging {\n\t\t\tfmt.Println(\"  findImplementationBoundary child\", child.Type())\n\t\t}\n\n\t\tchildNode := Node{\n\t\t\tType:   child.Type(),\n\t\t\tLang:   node.Lang,\n\t\t\tTsNode: child,\n\t\t\tBytes:  node.Bytes,\n\t\t}\n\n\t\tif isImplBoundaryNode(childNode) {\n\t\t\treturn &childNode\n\t\t} else {\n\t\t\tfound := findImplementationBoundary(childNode)\n\t\t\tif found != nil {\n\t\t\t\treturn found\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc findIdentifier(node Node) []Node {\n\tif isIdentifierNode(node) {\n\t\treturn []Node{node}\n\t}\n\n\tnodes := []Node{}\n\tfor i := 0; i < int(node.TsNode.ChildCount()); i++ {\n\t\tchild := node.TsNode.Child(i)\n\t\tchildNode := Node{\n\t\t\tType:   child.Type(),\n\t\t\tLang:   node.Lang,\n\t\t\tTsNode: child,\n\t\t\tBytes:  node.Bytes,\n\t\t}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Println(\"  child\", child.Type())\n\t\t}\n\n\t\tfound := findIdentifier(childNode)\n\t\tif found != nil {\n\t\t\tnodes = append(nodes, found...)\n\t\t}\n\t}\n\treturn nodes\n}\n\nfunc firstDefinitionChild(node Node) *Node {\n\tfor i := 0; i < int(node.TsNode.ChildCount()); i++ {\n\t\tchild := node.TsNode.Child(i)\n\n\t\tif verboseLogging {\n\t\t\tfmt.Println(\"  child\", child.Type())\n\t\t}\n\n\t\tchildNode := Node{\n\t\t\tType:   child.Type(),\n\t\t\tLang:   node.Lang,\n\t\t\tTsNode: child,\n\t\t\tBytes:  node.Bytes,\n\t\t}\n\n\t\tif isDefinitionNode(childNode, &node) && !isIncludeAndContinueNode(childNode) {\n\t\t\treturn &childNode\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc findAssignmentBoundary(node Node) *Node {\n\treturn findAssignmentBoundaryOnly(node, nil)\n}\n\nfunc findAssignmentBoundaryOnly(node Node, only map[nodeType]bool) *Node {\n\tif only != nil {\n\t\tif _, ok := only[nodeType(node.Type)]; ok {\n\t\t\treturn &node\n\t\t}\n\t} else if isAssignmentBoundaryNode(node) {\n\t\treturn skipForwardIfNeeded(node)\n\t}\n\n\tfor i := 0; i < int(node.TsNode.ChildCount()); i++ {\n\t\tchild := node.TsNode.Child(i)\n\n\t\tchildNode := Node{\n\t\t\tType:   child.Type(),\n\t\t\tLang:   node.Lang,\n\t\t\tTsNode: child,\n\t\t\tBytes:  node.Bytes,\n\t\t}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Println(\"  child\", child.Type())\n\t\t}\n\n\t\tfound := findAssignmentBoundaryOnly(childNode, only)\n\t\tif found != nil {\n\t\t\treturn found\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc skipForwardIfNeeded(node Node) *Node {\n\tconfig := assignmentBoundarySkipForwardNodeMap.getConfig(node.Type, node.Lang)\n\tif config == nil {\n\t\treturn &node\n\t}\n\n\tif config.skipForward != nil {\n\t\ttsParent := node.TsNode.Parent()\n\t\tif tsParent == nil {\n\t\t\treturn &node\n\t\t}\n\n\t\tparent := Node{\n\t\t\tType:   tsParent.Type(),\n\t\t\tLang:   node.Lang,\n\t\t\tTsNode: tsParent,\n\t\t\tBytes:  node.Bytes,\n\t\t}\n\n\t\tskipToNode := findAssignmentBoundaryOnly(parent, config.skipForward)\n\t\tif skipToNode != nil {\n\t\t\treturn skipToNode\n\t\t}\n\t}\n\treturn &node\n}\n\nfunc setNodeType(node *Node) {\n\tif node.Lang == shared.LanguageElixir && node.Type == \"call\" {\n\t\tcontent := node.TsNode.Content(node.Bytes)\n\n\t\tswitch {\n\t\tcase strings.HasPrefix(string(content), \"defmodule\"):\n\t\t\tnode.Type = \"module_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defprotocol\"):\n\t\t\tnode.Type = \"protocol_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defimpl\"):\n\t\t\tnode.Type = \"protocol_implementation\"\n\t\tcase strings.HasPrefix(string(content), \"defstruct\"):\n\t\t\tnode.Type = \"struct_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defexception\"):\n\t\t\tnode.Type = \"exception_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defdelegate\"):\n\t\t\tnode.Type = \"delegate_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defoverridable\"):\n\t\t\tnode.Type = \"overridable_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defcallback\"):\n\t\t\tnode.Type = \"callback_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defmacrocallback\"):\n\t\t\tnode.Type = \"macro_callback_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defmacrop\"):\n\t\t\tnode.Type = \"private_macro_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defmacro\"):\n\t\t\tnode.Type = \"macro_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defguardp\"):\n\t\t\tnode.Type = \"private_guard_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defguard\"):\n\t\t\tnode.Type = \"guard_definition\"\n\t\tcase strings.HasPrefix(string(content), \"defp\"):\n\t\t\tnode.Type = \"private_function_definition\"\n\t\tcase strings.HasPrefix(string(content), \"def\"):\n\t\t\tnode.Type = \"function_definition\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "app/server/syntax/file_map/svelte.go",
    "content": "package file_map\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/syntax\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"golang.org/x/net/html\"\n)\n\nfunc mapSvelte(content []byte) []Definition {\n\tscriptContent, scriptLang, styleContent := getSvelteScriptAndStyle(content)\n\tdefs := []Definition{}\n\n\tif scriptContent != \"\" {\n\t\tvar lang shared.Language\n\t\tif scriptLang == \"ts\" {\n\t\t\tlang = shared.LanguageTypescript\n\t\t} else {\n\t\t\tlang = shared.LanguageJavascript\n\t\t}\n\n\t\tparser := syntax.GetParserForLanguage(lang)\n\t\ttree, err := parser.ParseCtx(context.Background(), nil, []byte(scriptContent))\n\t\tif err != nil {\n\t\t\tlog.Printf(\"mapSvelte - error parsing script content: %v\\n\", err)\n\t\t\treturn defs\n\t\t}\n\n\t\tdef := Definition{\n\t\t\tType:      \"svelte-script\",\n\t\t\tSignature: fmt.Sprintf(\"<script lang=%q>\", scriptLang),\n\t\t\tChildren: mapTraditional(Node{\n\t\t\t\tLang:   lang,\n\t\t\t\tTsNode: tree.RootNode(),\n\t\t\t\tBytes:  []byte(scriptContent),\n\t\t\t}, nil),\n\t\t}\n\n\t\tdefs = append(defs, def)\n\t}\n\n\tdefs = append(defs, mapMarkup(content)...)\n\n\tif styleContent != \"\" {\n\t\tparser := syntax.GetParserForLanguage(shared.LanguageCss)\n\t\ttree, err := parser.ParseCtx(context.Background(), nil, []byte(styleContent))\n\t\tif err != nil {\n\t\t\tlog.Printf(\"mapSvelte - error parsing style content: %v\\n\", err)\n\t\t\treturn defs\n\t\t}\n\n\t\tdef := Definition{\n\t\t\tType:      \"svelte-style\",\n\t\t\tSignature: \"<style>\",\n\t\t\tChildren: mapTraditional(Node{\n\t\t\t\tLang:   shared.LanguageCss,\n\t\t\t\tTsNode: tree.RootNode(),\n\t\t\t\tBytes:  []byte(styleContent),\n\t\t\t}, nil),\n\t\t}\n\n\t\tdefs = append(defs, def)\n\t}\n\n\treturn defs\n}\n\nfunc getSvelteScriptAndStyle(content []byte) (scriptContent, scriptLang, styleContent string) {\n\treader := bytes.NewReader(content)\n\tdoc, err := html.Parse(reader)\n\tif err != nil {\n\t\treturn \"\", \"\", \"\"\n\t}\n\n\t// Helper function to extract text content from a node\n\tvar getTextContent func(*html.Node) string\n\tgetTextContent = func(n *html.Node) string {\n\t\tif n.Type == html.TextNode {\n\t\t\treturn n.Data\n\t\t}\n\t\tvar result string\n\t\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\t\tresult += getTextContent(c)\n\t\t}\n\t\treturn result\n\t}\n\n\t// Helper function to find script and style tags\n\tvar findTags func(*html.Node)\n\tfindTags = func(n *html.Node) {\n\t\tif n.Type == html.ElementNode {\n\t\t\tif n.Data == \"script\" {\n\t\t\t\tscriptContent = strings.TrimSpace(getTextContent(n))\n\t\t\t\t// Check for lang attribute\n\t\t\t\tfor _, attr := range n.Attr {\n\t\t\t\t\tif attr.Key == \"lang\" {\n\t\t\t\t\t\tscriptLang = attr.Val\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if n.Data == \"style\" {\n\t\t\t\tstyleContent = strings.TrimSpace(getTextContent(n))\n\t\t\t}\n\t\t}\n\n\t\t// Recursively search children\n\t\tfor c := n.FirstChild; c != nil; c = c.NextSibling {\n\t\t\tfindTags(c)\n\t\t}\n\t}\n\n\tfindTags(doc)\n\treturn scriptContent, scriptLang, styleContent\n}\n"
  },
  {
    "path": "app/server/syntax/map.txt",
    "content": "### ../examples/bash_example.sh\nfunction print_message()\nget_date()\nmain()\n\n### ../examples/c_example.c\n#define MAX_SIZE\n#define SQUARE(x)\ntypedef struct\ntypedef enum {\n    MONDAY,\n    TUESDAY,\n    WEDNESDAY,\n    THURSDAY,\n    FRIDAY\n} Weekday;\nstatic const double PI\nint globalCounter\nvoid printPerson(const Person* p\nint factorial(int n\nunion Data\ntypedef int (*Operation)(int, int);\nint add(int a, int b)\nint subtract(int a, int b)\nvoid printPerson(const Person* p)\nint factorial(int n)\nint main()\n\n### ../examples/cpp_example.cpp\nint globalInteger\nconst double PI\nstatic std::string globalString\nenum Color { RED, GREEN, BLUE }\nconstexpr int MAX_SIZE\ntemplate<typename T>\n  - class Container\n    - public\n    - static T defaultValue;\n    - const T maxCapacity = MAX_SIZE;\n    - void add(T item)\n    - const std::vector<T>& getItems() const\n    - private\n    - std::vector<T> items;\ntemplate<typename T>\n  - T Container<T>::defaultValue\nclass Animal\n  - public\n  - virtual ~Animal() = default;\n  - virtual void makeSound() const = 0;\n  - protected\n  - std::string name;\nclass Dog : virtual public Animal\n  - public\n  - Dog(const std::string& dogName)\n  - void makeSound() const override\nnamespace Utils {\n    // Namespace-level variables\n    inline int counter = 0;\n    const std::string VERSION = \"1.0.0\";\n    \n    // Function template\n    template<typename T>\n    T max(T a, T b)\nclass Resource\n  - public\n  - Resource(const std::string& data) : data_(data)\n  - ~Resource()\n  - Resource(Resource&& other) noexcept : data_(std::move(other.data_))\n  - std::string getData() const\n  - private\n  - std::string data_;\nclass Counter\n  - public\n  - static int getCount()\n  - Counter()\n  - ~Counter()\n  - private\n  - static int count;\nint Counter::count\nclass Box\n  - friend std::ostream& operator<<(std::ostream& os, const Box& box);\n  - public\n  - Box(int w, int h) : width(w), height(h)\n  - private\n  - int width;\n  - int height;\nstd::ostream& operator<<(std::ostream& os, const Box& box)\nint main()\n\n### ../examples/csharp_example.cs\nnamespace ExampleApp\n{\n    // Interface definition\n    public interface IProcessor<T>\n    {\n        Task<T> ProcessAsync(T input);\n        bool Validate(T input);\n    }\n\n    // Enum definition\n    public enum Status\n    {\n        Pending,\n        Active,\n        Completed,\n        Failed\n    }\n\n    // Delegate declaration\n    public delegate void StatusChangedEventHandler(Status oldStatus, Status newStatus);\n\n    // Generic class implementing interface\n    public class DataProcessor<T> : IProcessor<T> where T : class\n    {\n        // Event declaration\n        public event StatusChangedEventHandler StatusChanged;\n\n        // Auto-implemented property\n        public Status CurrentStatus { get; private set; }\n\n        // Static field\n        private static readonly Dictionary<Type, int> _processedItems = new();\n\n        // Constructor\n        public DataProcessor()\n\n### ../examples/elm_example.elm\ntype alias Model =\n    { users : List User\n    , inputText : String\n    , error : Maybe String\n    }\ntype alias User =\n    { id : Int\n    , name : String\n    , email : String\n    }\ninit _ =\n    ( { users = []\n      , inputText = \"\"\n      , error = Nothing\n      }\n    , fetchUsers\n    )\ntype Msg\n    = GotUsers (Result Http.Error (List User))\n    | InputChanged String\n    | AddUser\n    | UserAdded (Result Http.Error User)\nupdate msg model =\n    case msg of\n        GotUsers result ->\n            case result of\n                Ok users ->\n                    ( { model | users = users, error = Nothing }\n                    , Cmd.none\n                    )\n                Err _ ->\n                    ( { model | error = Just \"Failed to fetch users\" }\n                    , Cmd.none\n                    )\n\n        InputChanged text ->\n            ( { model | inputText = text }\n            , Cmd.none\n            )\n\n        AddUser ->\n            ( model\n            , addUser model.inputText\n            )\n\n        UserAdded result ->\n            case result of\n                Ok user ->\n                    ( { model \n                      | users = user :: model.users\n                      , inputText = \"\"\n                      , error = Nothing\n                      }\n                    , Cmd.none\n                    )\n                Err _ ->\n                    ( { model | error = Just \"Failed to add user\" }\n                    , Cmd.none\n                    )\nfetchUsers =\n    Http.get\n        { url = \"/api/users\"\n        , expect = Http.expectJson GotUsers usersDecoder\n        }\naddUser name =\n    Http.post\n        { url = \"/api/users\"\n        , body = Http.jsonBody (userEncoder name)\n        , expect = Http.expectJson UserAdded userDecoder\n        }\nuserEncoder name =\n    Encode.object\n        [ (\"name\", Encode.string name)\n        ]\nuserDecoder =\n    Decode.map3 User\n        (Decode.field \"id\" Decode.int)\n        (Decode.field \"name\" Decode.string)\n        (Decode.field \"email\" Decode.string)\nusersDecoder =\n    Decode.list userDecoder\nview model =\n    div []\n        [ h1 [] [ text \"User Management\" ]\n        , viewError model.error\n        , viewInput model.inputText\n        , viewUsers model.users\n        ]\nviewError maybeError =\n    case maybeError of\n        Just error ->\n            div [ class \"error\" ] [ text error ]\n        Nothing ->\n            text \"\"\nviewInput inputText =\n    div []\n        [ input\n            [ value inputText\n            , onInput InputChanged\n            , placeholder \"Enter user name\"\n            ] []\n        , button [ onClick AddUser ] [ text \"Add User\" ]\n        ]\nviewUsers users =\n    div []\n        [ h2 [] [ text \"Users\" ]\n        , ul [] (List.map viewUser users)\n        ]\nviewUser user =\n    li []\n        [ text (user.name ++ \" (\" ++ user.email ++ \")\")\n        ]\nmain =\n    Browser.element\n        { init = init\n        , update = update\n        , view = view\n        , subscriptions = \\_ -> Sub.none\n        }\n\n### ../examples/go_example.go\ntype DataProcessor interface {\ntype ValidationError struct\nfunc (e *ValidationError) Error() string\ntype User struct\ntype UserID = int64\nconst (\n  - MaxRetries\n  - DefaultLimit\nconst\n  - singleLineConst string\nvar (\n  - defaultTimeout\n  - processor      DataProcessor\nvar\n  - singleLineVar string\ntype Result[T any] struct\ntype UserProcessor struct\nfunc NewUserProcessor() *UserProcessor\nfunc (p *UserProcessor) Process(ctx context.Context, data interface{}) error\nfunc (p *UserProcessor) Validate(data interface{}) bool\nfunc processUsers(ctx context.Context, users <-chan *User) <-chan *Result[*User]\nfunc createUser(name, email string) (user *User, err error)\nfunc main()\n\n### ../examples/groovy_example.groovy\ntrait Loggable\nabstract class\nclass Car extends Vehicle implements Loggable {\n    // Properties with type definitions\n    private BigDecimal price\n    protected Boolean running = false\n    \n    // Static fields\n    static final String MANUFACTURER = \"Generic Motors\"\n    static def carCount = 0\n    \n    // Constructor with default parameters\nCar(String make = 'Unknown', String model = 'Generic', Integer year = 2024) {\n    this.make = make\n    this.model = model\n    this.year = year\n    carCount++\n}\n    \n    // Getter with @Lazy annotation\n    @Lazy\n    String fullName = \"$make $model ($year)\"\n    \n    // Method implementation with closure parameter\n    void start() {\n        running = true\n        log \"Starting ${fullName}\"\n        withLock {\n            // Some synchronized code\n            println \"Engine started\"\n        }\n    }\n    \n    void stop() {\n        running = false\n        log \"Stopping ${fullName}\"\n    }\n    \n    // Operator overloading\n    def plus(Car other) {\n        new Car(\nmake: \"${this.make}-${other.make}\",\nmodel: \"${this.model}-${other.model}\",\nyear: Math.max(this.year, other.year)\n        )\n    }\n    \n    // Property with custom getter/setter\n    private BigDecimal _price\n    void setPrice(BigDecimal price) {\n        if (price < 0) throw new IllegalArgumentException(\"Price cannot be negative\")\n        this._price = price\n    }\n    BigDecimal getPrice() { _price }\n}\nenum Status\nclass StringExtensions {\n    static String truncate(String self, Integer length) {\n        self.size() <= length ? self : self[0..<length] + \"...\"\n    }\n}\nclass EmailBuilder {\n    private def email = [:]\n    \n    EmailBuilder to(String recipient) {\n        email.to = recipient\n        this\n    }\n    \n    EmailBuilder from(String sender) {\n        email.from = sender\n        this\n    }\n    \n    EmailBuilder subject(String subject) {\n        email.subject = subject\n        this\n    }\n    \n    EmailBuilder body(@DelegatesTo(strategy=Closure.DELEGATE_FIRST) Closure body) {\n        def builder = new StringBuilder()\n        body.delegate = builder\n        body.resolveStrategy = Closure.DELEGATE_FIRST\n        body()\n        email.body = builder.toString()\n        this\n    }\n    \n    Map build() {\n        email.clone() as Map\n    }\n}\ndef main() {\n    use(StringExtensions) {\n        def description = \"This is a very long description that needs truncating\"\n        println description.truncate(20)\n    }\n    \n    def car = new Car(make: \"Tesla\", model: \"Model S\")\n    car.start()\ndef email = new EmailBuilder()\n    .to(\"recipient@example.com\")\n    .from(\"sender@example.com\")\n    .subject(\"Test Email\")\n    .body {\n        append \"Hello,\\n\"\n        append \"This is a test email.\\n\"\n        append \"Regards\"\n    }\n    .build()\n    .build()\n    \n    println email\n}\n\n### ../examples/java_example.java\ninterface DataProcessor<T extends Comparable<T>>\n  - CompletableFuture<T> processAsync(T input);\n  - boolean validate(T input);\nenum Status\nabstract class BaseEntity<ID>\n  - protected ID id\n  - protected LocalDateTime createdAt\n  - protected LocalDateTime updatedAt\n  - public abstract void validate();\nrecord UserDTO(\n    String name,\n    String email,\n    Set<String> roles\n)\n@interface Audited\npublic class Example extends BaseEntity<UUID> implements DataProcessor<String>\n  - private static final int MAX_RETRIES\n  - private static final Map<String, Integer> CACHE\n  - private final Queue<String> queue\n  - protected Status status\n  - @Audited\n    public String name\n  - private Example(Builder builder)\n  - public static class Builder\n    - private String name\n    - public Builder name(String name)\n    - public Example build()\n  - @Override\n    public CompletableFuture<String> processAsync(String input)\n  - @Override\n    public boolean validate(String input)\n  - @Override\n    public void validate()\n  - public <T extends Comparable<? super T>> List<T> sort(Collection<T> items)\n  - public void processItems(\n        List<String> items,\n        Predicate<String> filter,\n        Consumer<String> processor\n    )\n  - public static class ProcessingException extends RuntimeException\n    - public ProcessingException(String message)\n  - public static void main(String[] args)\n\n### ../examples/javascript_example.js\nconst MAX_RETRIES\nconst DEFAULT_TIMEOUT\nconst privateState\nclass DataProcessor extends EventEmitter\n  - #cache\n  - static version\n  - constructor({ maxRetries = MAX_RETRIES, timeout = DEFAULT_TIMEOUT } = {})\n  - async processData(data)\n  - async #validateAndTransform(data)\n  - *iterateCache()\nfunction deprecated(target, context)\nconst handler\nconst proxy\nconst delay\nasync function* generateSequence(start, end)\nconst memoize\nclass ValidationError extends Error\n  - constructor(message, field)\nconst config\nconst processItems\nconst fetchData\n\n### ../examples/kotlin_example.kt\ninterface DataProcessor<T>\n  - suspend fun process(data: T): Result<T>\n  - fun validate(data: T): Boolean\nsealed class ProcessingState<out T>\n  - object Loading : ProcessingState<Nothing>()\n  - data class Success<T>(val data: T) : ProcessingState<T>()\n  - data class Error(val exception: Throwable) : ProcessingState<Nothing>()\ndata class User(\n    val id: String,\n    val name: String,\n    val email: String,\n    val roles: Set<Role> = emptySet(),\n    val createdAt: LocalDateTime = LocalDateTime.now()\n)\nenum class Role(val permission: Int)\nobject Configuration\nclass UserProcessor : DataProcessor<User>\n  - private var processingCount: Int by Delegates.observable(0) { _, old, new ->\n        println(\"Processing count changed from $old to $new\n  - val isActive: Boolean\n  - override suspend fun process(data: User): Result<User>\n  - override fun validate(data: User): Boolean\n  - private fun String.isValidEmail(): Boolean\n  - inline fun <reified T> logType()\n  - private fun validateEmail(email: String)\nfun <T> withRetry(\n    times: Int = 3,\n    action: suspend () -> T\n): suspend () -> T\nfun CoroutineScope.processUsers(users: List<User>): Flow<ProcessingState<User>>\nval User.displayName: String\nsuspend fun main()\n\n### ../examples/lua_example.lua\nlocal Example\nlocal MAX_RETRIES\nlocal DEFAULT_TIMEOUT\nlocal User\nlocal DataStore\nlocal EventEmitter\nExample.User\nExample.EventEmitter\n\n### ../examples/ocaml_example.ml\ntype 'a = string\ntype t = {\n    mutable processed_count: int;\n    created_at: float;\n  }\nlet create () = {\n    processed_count = 0;\n    created_at = Unix.time ();\n  }\nlet process t input =\n    t.processed_count <- t.processed_count + 1;\n    if String.length input > 0 then\n      Ok (String.uppercase_ascii input)\n    else\n      Error \"Empty input\"\nlet validate input =\n    String.length input > 0\nend\ntype user = {\n  id: int;\n  name: string;\n  email: string;\n  created_at: float;\n}\ntype 'a result = \n  | Success of 'a \n  | Error of string\ntype message =\n  | Text of string\n  | Number of int\n  | Tuple of string * int\n  | Record of user\nexception ValidationError of string\nlet memoize f =\n  let cache = Hashtbl.create 16 in\n  fun x ->\n    try Hashtbl.find cache x\n    with Not_found ->\n      let result = f x in\n      Hashtbl.add cache x result;\n      result\nclass virtual ['a] queue = object(self)\n  val mutable items = []\n  \n  method virtual push : 'a -> unit\n  method virtual pop : 'a option\n  \n  method size = List.length items\n  \n  method is_empty = items = []\n  \n  method protected get_items = items\n  method protected set_items new_items = items <- new_items\nend\nclass ['a] fifo_queue = object(self)\n  inherit ['a] queue\n\n  method push item =\n    self#set_items (self#get_items @ [item])\n\n  method pop =\n    match self#get_items with\n    | [] -> None\n    | hd::tl ->\n        self#set_items tl;\n        Some hd\nend\nlet () =\n  let processor = StringProcessor.create () in\n  let result = StringProcessor.process processor \"hello world\" in\n  match result with\n  | Ok processed -> Printf.printf \"Processed: %s\\n\" processed\n  | Error msg -> Printf.eprintf \"Error: %s\\n\" msg;\n\n  let queue = new fifo_queue in\n  queue#push 1;\n  queue#push 2;\n  queue#push 3;\n  \n  let rec print_queue () =\n    match queue#pop with\n    | Some item -> \n        Printf.printf \"Item: %d\\n\" item;\n        print_queue ()\n    | None -> ()\n  in\n  print_queue ()\n\n### ../examples/php_example.php\nnamespace Example;\ninterface DataProcessor\ntrait Loggable\nabstract class Entity implements JsonSerializable\n  - protected DateTime $createdAt\n  - protected ?DateTime $updatedAt\n  - public function __construct()\n  - abstract public function validate(): bool;\n  - public function jsonSerialize(): mixed\nenum Status: string\nclass User extends Entity implements DataProcessor\n  - private static int $instanceCount\n  - public function __construct(\n        private string $name,\n        private string $email,\n        private Status $status = Status::PENDING,\n        private array $metadata = []\n    )\n  - public static function getInstanceCount(): int\n  - public function getEmail(): string\n  - public function setEmail(string $email): void\n  - public function __get(string $name)\n  - public function __set(string $name, mixed $value): void\n  - public function process(mixed $data): mixed\n  - public function validate(mixed $data): bool\n  - public function validate(): bool\n  - public function getMetadataValues(): array\n  - public function jsonSerialize(): mixed\nclass ProcessingException extends Exception\n  - public function __construct(\n        string $message = \"\",\n        private ?string $errorCode = null,\n        int $code = 0,\n        ?Throwable $previous = null\n    )\n  - public function getErrorCode(): ?string\n\n### ../examples/python_example.py\nclass Processable(Protocol):\n  - def process(self) -> None:\n  - def validate(self) -> bool:\nclass Status(enum.Enum):\n  - def __str__(self) -> str:\n@dataclasses.dataclass(frozen=True, slots=True)\nclass UserCredentials:\nclass BaseProcessor(ABC, Generic[T]):\n  - def __init__(self) -> None:\n  - @abstractmethod\n    async def process_item(self, item: T) -> None:\n  - @property\n    def processed_count(self) -> int:\ndef log_execution(func: Callable) -> Callable:\nclass DataProcessor(BaseProcessor[UserCredentials], Processable):\n    # Class variable\n  - def __init__(self, batch_size: Optional[int] = None) -> None:\n  - @property\n    def status(self) -> Status:\n  - @status.setter\n    def status(self, value: Status) -> None:\n  - async def __aenter__(self) -> DataProcessor:\n  - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:\n  - async def process_batch(self) -> AsyncIterator[List[UserCredentials]]:\n  - @log_execution\n    async def process_item(self, item: UserCredentials) -> None:\n  - def process(self) -> None:\n  - def validate(self) -> bool:\nclass ProcessingError(Exception):\n  - def __init__(self, message: str, item: Any) -> None:\nasync def main() -> None:\n\n### ../examples/ruby_example.rb\nglobal_var\nmodule Loggable\n  - def log(message)\nmodule Utils\n  - class << self\n    - def generate_id\nclass BaseProcessor\n  - @processors\n  - class << self\n    - def register(processor)\n  - def initialize\n  - def process\nclass ProcessingError < StandardError\n  - def initialize(message, item)\nUser\nmodule Status\n  - PENDING\n  - ACTIVE\n  - COMPLETED\n  - FAILED\n  - ALL\nclass DataProcessor < BaseProcessor\n  # Constants\n  - MAX_RETRIES\n  - DEFAULT_TIMEOUT\n  - @@instance_count\n  - def initialize(options = {})\n  - def add_item(item:, priority: :normal)\n  - def validate_item(item)\n  - def with_retry\n  - def process_items\n  - def process_item(item)\nclass Configuration\n  - def initialize\n  - def [](key)\n  - def []=(key, value)\n\n### ../examples/rust_example.rs\nconst MAX_RETRIES: u32\nconst DEFAULT_TIMEOUT: Duration\n\n### ../examples/scala_example.scala\ntype Result[T] = Either[String, T]\ntrait DataProcessor[T]\ncase class ProcessingState[T](\n  data: T,\n  status: ProcessingState.Status,\n  timestamp: Long = System.currentTimeMillis()\n)\nobject ProcessingState\nabstract class BaseProcessor[T] extends DataProcessor[T]\n  - protected val logger = new Logger(getClass.getName)\n  - protected def transform(data: T): T\n  - override def process(data: T): Result[T] =\nclass NumberProcessor[T <: Number] extends BaseProcessor[T]\n  - override def validate(data: T): Boolean = data != null\n  - protected def transform(data: T): T = data\ncase class User(\n  id: String,\n  name: String,\n  email: String,\n  roles: Set[String] = Set.empty,\n  metadata: Map[String, String] = Map.empty\n)\nobject Utils\nobject Implicits\ntrait Logging\nclass Logger(name: String) extends Logging\n  - def log(level: String, message: => String): Unit =\ntrait Monad[F[_]]\nobject Conversions\ntrait Container\nclass Box[T](initial: T) extends Container\n  - type Content = T\n  - def content: T = initial\n  - def transform(f: T => T): Box[T] = new Box(f(initial))\nobject Main extends App\n\n### ../examples/swift_example.swift\nprotocol DataProcessor\nenum ProcessingError: LocalizedError\n  - var errorDescription: String? {\n        switch self {\n        case .invalidInput(let reason): return \"Invalid input: \\(reason)\"\n        case .processingFailed(let reason): return \"Processing failed: \\(reason)\"\n        }\n    }\n@propertyWrapper\nstruct Validated<T>\n  - private var value: T\n  - private let validator: (T) -> Bool\n  - var wrappedValue: T {\n        get { value }\n        set {\n            guard validator(newValue) else {\n                fatalError(\"Invalid value\")\n            }\n            value = newValue\n        }\n    }\n  - init(wrappedValue: T, validator: @escaping (T) -> Bool)\nactor ProcessingState\n  - private(set) var processedCount: Int = 0\n  - private var status: Status = .pending\n  - enum Status\n  - func incrementCount()\n  - func updateStatus(_ newStatus: Status)\nstruct Queue<Element> where Element: Sendable\n  - private var elements: [Element] = []\n  - private let lock = NSLock()\n  - mutating func enqueue(_ element: Element)\n  - mutating func dequeue() -> Element?\nclass StringProcessor: DataProcessor\n  - typealias Input = String\n  - typealias Output = String\n  - private let state = ProcessingState()\n  - @Validated(validator: { !$0.isEmpty })\n    private var currentInput: String = \"default\"\n  - func process(_ input: String) async throws -> String\n  - func validate(_ input: String) -> Bool\nextension StringProcessor: AsyncSequence, AsyncIteratorProtocol\n  - typealias Element = String\n  - func makeAsyncIterator() -> StringProcessor\n  - func next() async throws -> String?\n@resultBuilder\nstruct ArrayBuilder<T>\n  - static func buildBlock(_ components: T...) -> [T]\nfunc makeArray<T>(@ArrayBuilder<T> content: () -> [T]) -> [T]\n@main\nstruct Example\n  - static func main() async throws\n\n### ../examples/tsx_example.tsx\ninterface User\ntype ValidationResult\ninterface DataListProps<T>\nfunction useDebounce<T>(value: T, delay: number): T\nfunction withLoading<P extends object>(\n  WrappedComponent: React.ComponentType<P>\n): FC<P & { loading?: boolean }>\nconst UserForm: FC<{ onSubmit: (user: Partial<User>) => void }>\nconst DataList\nconst App: FC\n\n### ../examples/typescript_example.ts\ninterface DataProcessor<T>\ntype Result<T>\nenum Status\ntype ValidationResult\ntype AdminUser\ntype Readonly<T>\ntype Partial<T>\nfunction validate(target: any, propertyKey: string, descriptor: PropertyDescriptor)\n@logger\nclass User\n  - @required\n    private name: string\n  - @email\n    private email: string\n  - @format('YYYY-MM-DD')\n    private createdAt: Date\n  - constructor(name: string, email: string)\n  - public updateEmail(newEmail: string): void\n  - public get isAdmin(): this is AdminUser\nabstract class BaseProcessor<T> implements DataProcessor<T>\nclass StringProcessor extends BaseProcessor<string>\n  - async process(data: string): Promise<string>\n  - private async transform(data: string): Promise<string>\nfunction process(data: string): Promise<string>;\nfunction process(data: number): Promise<number>;\nfunction process(data: string | number): Promise<string | number>\nasync function validateData<T extends { id: string }>(\n    data: T\n): Promise<ValidationResult>\nfunction withRetry<T>(\n    fn: () => Promise<T>,\n    retries: number = 3\n): () => Promise<T>\ntype Middleware\nconst createUser\nasync function* generateSequence(\n    start: number,\n    end: number\n): AsyncGenerator<number>\nasync function main()\n\n"
  },
  {
    "path": "app/server/syntax/parsers.go",
    "content": "package syntax\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\n\ttree_sitter \"github.com/smacker/go-tree-sitter\"\n\t\"github.com/smacker/go-tree-sitter/bash\"\n\t\"github.com/smacker/go-tree-sitter/c\"\n\t\"github.com/smacker/go-tree-sitter/cpp\"\n\t\"github.com/smacker/go-tree-sitter/csharp\"\n\t\"github.com/smacker/go-tree-sitter/css\"\n\t\"github.com/smacker/go-tree-sitter/cue\"\n\t\"github.com/smacker/go-tree-sitter/dockerfile\"\n\t\"github.com/smacker/go-tree-sitter/elixir\"\n\t\"github.com/smacker/go-tree-sitter/elm\"\n\t\"github.com/smacker/go-tree-sitter/golang\"\n\t\"github.com/smacker/go-tree-sitter/groovy\"\n\t\"github.com/smacker/go-tree-sitter/hcl\"\n\t\"github.com/smacker/go-tree-sitter/html\"\n\t\"github.com/smacker/go-tree-sitter/java\"\n\t\"github.com/smacker/go-tree-sitter/javascript\"\n\t\"github.com/smacker/go-tree-sitter/kotlin\"\n\t\"github.com/smacker/go-tree-sitter/lua\"\n\t\"github.com/smacker/go-tree-sitter/ocaml\"\n\t\"github.com/smacker/go-tree-sitter/php\"\n\t\"github.com/smacker/go-tree-sitter/protobuf\"\n\t\"github.com/smacker/go-tree-sitter/python\"\n\t\"github.com/smacker/go-tree-sitter/ruby\"\n\t\"github.com/smacker/go-tree-sitter/rust\"\n\t\"github.com/smacker/go-tree-sitter/scala\"\n\t\"github.com/smacker/go-tree-sitter/svelte\"\n\t\"github.com/smacker/go-tree-sitter/swift\"\n\t\"github.com/smacker/go-tree-sitter/toml\"\n\t\"github.com/smacker/go-tree-sitter/typescript/tsx\"\n\t\"github.com/smacker/go-tree-sitter/typescript/typescript\"\n\t\"github.com/smacker/go-tree-sitter/yaml\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc GetLanguageForPath(path string) shared.Language {\n\text := filepath.Ext(path)\n\tlang, ok := shared.LanguageByExtension[ext]\n\tif !ok {\n\t\tif strings.Contains(strings.ToLower(path), \"dockerfile\") {\n\t\t\treturn shared.LanguageDockerfile\n\t\t}\n\t\tif strings.Contains(strings.ToLower(path), \"rakefile\") {\n\t\t\treturn shared.LanguageRuby\n\t\t}\n\n\t\tif strings.Contains(strings.ToLower(path), \"gemfile\") {\n\t\t\treturn shared.LanguageRuby\n\t\t}\n\n\t\tif strings.Contains(strings.ToLower(path), \"gemfile.lock\") {\n\t\t\treturn shared.LanguageRuby\n\t\t}\n\n\t\tif strings.Contains(strings.ToLower(path), \"gemspec\") {\n\t\t\treturn shared.LanguageRuby\n\t\t}\n\n\t\tif strings.Contains(strings.ToLower(path), \"guardfile\") {\n\t\t\treturn shared.LanguageRuby\n\t\t}\n\n\t\treturn \"\"\n\t}\n\treturn lang\n}\n\nfunc GetParserForPath(path string) (*tree_sitter.Parser, shared.Language, *tree_sitter.Parser, shared.Language) {\n\tlang := GetLanguageForPath(path)\n\tif lang == \"\" {\n\t\treturn nil, \"\", nil, \"\"\n\t}\n\n\tparser := GetParserForLanguage(lang)\n\n\text := filepath.Ext(path)\n\tfallback := shared.LanguageFallbackByExtension[ext]\n\tvar fallbackParser *tree_sitter.Parser\n\tif fallback != \"\" {\n\t\tfallbackParser = GetParserForLanguage(fallback)\n\t}\n\n\treturn parser, lang, fallbackParser, fallback\n}\n\nfunc GetParserForLanguage(lang shared.Language) *tree_sitter.Parser {\n\tparser := tree_sitter.NewParser()\n\tswitch lang {\n\tcase shared.LanguageBash:\n\t\tparser.SetLanguage(bash.GetLanguage())\n\tcase shared.LanguageC:\n\t\tparser.SetLanguage(c.GetLanguage())\n\tcase shared.LanguageCpp:\n\t\tparser.SetLanguage(cpp.GetLanguage())\n\tcase shared.LanguageCsharp:\n\t\tparser.SetLanguage(csharp.GetLanguage())\n\tcase shared.LanguageCss:\n\t\tparser.SetLanguage(css.GetLanguage())\n\tcase shared.LanguageCue:\n\t\tparser.SetLanguage(cue.GetLanguage())\n\tcase shared.LanguageDockerfile:\n\t\tparser.SetLanguage(dockerfile.GetLanguage())\n\tcase shared.LanguageElixir:\n\t\tparser.SetLanguage(elixir.GetLanguage())\n\tcase shared.LanguageElm:\n\t\tparser.SetLanguage(elm.GetLanguage())\n\tcase shared.LanguageGo:\n\t\tparser.SetLanguage(golang.GetLanguage())\n\tcase shared.LanguageGroovy:\n\t\tparser.SetLanguage(groovy.GetLanguage())\n\tcase shared.LanguageHcl:\n\t\tparser.SetLanguage(hcl.GetLanguage())\n\tcase shared.LanguageHtml:\n\t\tparser.SetLanguage(html.GetLanguage())\n\tcase shared.LanguageJava:\n\t\tparser.SetLanguage(java.GetLanguage())\n\tcase shared.LanguageJavascript, shared.LanguageJson:\n\t\tparser.SetLanguage(javascript.GetLanguage())\n\tcase shared.LanguageKotlin:\n\t\tparser.SetLanguage(kotlin.GetLanguage())\n\tcase shared.LanguageLua:\n\t\tparser.SetLanguage(lua.GetLanguage())\n\tcase shared.LanguageOCaml:\n\t\tparser.SetLanguage(ocaml.GetLanguage())\n\tcase shared.LanguagePhp:\n\t\tparser.SetLanguage(php.GetLanguage())\n\tcase shared.LanguageProtobuf:\n\t\tparser.SetLanguage(protobuf.GetLanguage())\n\tcase shared.LanguagePython:\n\t\tparser.SetLanguage(python.GetLanguage())\n\tcase shared.LanguageRuby:\n\t\tparser.SetLanguage(ruby.GetLanguage())\n\tcase shared.LanguageRust:\n\t\tparser.SetLanguage(rust.GetLanguage())\n\tcase shared.LanguageScala:\n\t\tparser.SetLanguage(scala.GetLanguage())\n\tcase shared.LanguageSvelte:\n\t\tparser.SetLanguage(svelte.GetLanguage())\n\tcase shared.LanguageSwift:\n\t\tparser.SetLanguage(swift.GetLanguage())\n\tcase shared.LanguageToml:\n\t\tparser.SetLanguage(toml.GetLanguage())\n\tcase shared.LanguageTypescript:\n\t\tparser.SetLanguage(typescript.GetLanguage())\n\tcase shared.LanguageJsx, shared.LanguageTsx:\n\t\tparser.SetLanguage(tsx.GetLanguage())\n\tcase shared.LanguageYaml:\n\t\tparser.SetLanguage(yaml.GetLanguage())\n\tdefault:\n\t\treturn nil\n\t}\n\treturn parser\n}\n"
  },
  {
    "path": "app/server/syntax/structured_edits_apply.go",
    "content": "package syntax\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\tsitter \"github.com/smacker/go-tree-sitter\"\n)\n\n// const duplicationThreshold = 20\n\ntype Reference int\ntype Removal int\n\ntype Anchor int\n\ntype NeedsVerifyReason string\n\nconst (\n\tNeedsVerifyReasonCodeRemoved       NeedsVerifyReason = \"code_removed\"\n\tNeedsVerifyReasonCodeDuplicated    NeedsVerifyReason = \"code_duplicated\"\n\tNeedsVerifyReasonAmbiguousLocation NeedsVerifyReason = \"ambiguous_location\"\n)\n\ntype ApplyChangesResult struct {\n\tNewFile            string\n\tProposed           string\n\tNeedsVerifyReasons []NeedsVerifyReason\n\tBlocksRemoved      []struct {\n\t\tStart   int\n\t\tEnd     int\n\t\tContent string\n\t}\n\t// Comments           []Comment\n}\n\ntype AnchorMap = map[int]int\n\ntype ReferenceBlock struct {\n\tstart int // Start line number in original file (inclusive)\n\tend   int // End line number in original file (inclusive)\n}\n\ntype Comment struct {\n\tTxt   string\n\tIsRef bool\n}\n\ntype RemovalRange struct {\n\tStart int\n\tEnd   int\n}\n\nfunc (r RemovalRange) Overlaps(other RemovalRange) bool {\n\treturn (other.Start <= r.End && other.Start >= r.Start) ||\n\t\t(other.End <= r.End && other.End >= r.Start) ||\n\t\t(other.Start <= r.Start && other.End >= r.End)\n}\n\nconst verboseLogging = false\n\nfunc isRef(content string) bool {\n\ttrimmedLower := strings.ToLower(strings.TrimSpace(content))\n\tif strings.Contains(trimmedLower, \"... existing code ...\") {\n\t\treturn true\n\t}\n\tregex := regexp.MustCompile(`(\\.\\.\\.)?.*?(existing|rest of|start of|end of).*?\\.\\.\\.$`)\n\treturn regex.MatchString(trimmedLower)\n}\n\nfunc isRemoval(content string) bool {\n\treturn strings.Contains(strings.ToLower(content), \"plandex: removed\")\n}\n\ntype ApplyChangesParams struct {\n\tOriginal               string\n\tProposed               string\n\tDesc                   string\n\tAddMissingStartEndRefs bool\n\tParser                 *sitter.Parser\n\tLanguage               shared.Language\n}\n\nfunc ApplyChanges(\n\tctx context.Context,\n\tparams ApplyChangesParams,\n) *ApplyChangesResult {\n\toriginal := params.Original\n\tproposed := params.Proposed\n\tdesc := params.Desc\n\taddMissingStartEndRefs := params.AddMissingStartEndRefs\n\tparser := params.Parser\n\tlanguage := params.Language\n\n\tproposedInitial := proposed\n\n\tproposedLines := strings.Split(proposed, \"\\n\")\n\toriginalLines := strings.Split(original, \"\\n\")\n\n\tvar references []Reference\n\thasRefByLine := map[int]bool{}\n\n\tvar removals []Removal\n\thasRemovalByLine := map[int]bool{}\n\n\tfor i, line := range proposedLines {\n\t\tlineNum := i + 1\n\t\tcontent := strings.TrimSpace(line)\n\t\tfound := false\n\t\tif isRef(content) {\n\t\t\tif !hasRefByLine[lineNum] {\n\t\t\t\treferences = append(references, Reference(lineNum))\n\t\t\t\thasRefByLine[lineNum] = true\n\t\t\t}\n\t\t\tfound = true\n\t\t} else if isRemoval(content) {\n\t\t\tif !hasRemovalByLine[lineNum] {\n\t\t\t\tremovals = append(removals, Removal(lineNum))\n\t\t\t\thasRemovalByLine[lineNum] = true\n\t\t\t}\n\t\t\tfound = true\n\t\t}\n\n\t\tif found {\n\t\t\tproposedLines[i] = strings.Replace(proposedLines[i], content, \"\", 1)\n\t\t}\n\t}\n\n\tdesc = strings.ToLower(desc)\n\tdesc = strings.TrimSpace(desc)\n\tdesc = strings.ReplaceAll(desc, \"*\", \"\")\n\tdesc = strings.ReplaceAll(desc, \"`\", \"\")\n\tdesc = strings.ReplaceAll(desc, \"'\", \"\")\n\tdesc = strings.ReplaceAll(desc, `\"`, \"\")\n\n\tisEntireFileUpdate := strings.Contains(desc, \"type: overwrite\")\n\tisReplace := strings.Contains(desc, \"type: replace\")\n\tisRemove := strings.Contains(desc, \"type: remove\")\n\tisAdd := strings.Contains(desc, \"type: add\")\n\tisPrepend := strings.Contains(desc, \"type: prepend\")\n\tisAppend := strings.Contains(desc, \"type: append\")\n\n\tvar removalRanges []RemovalRange\n\n\t// Parse line ranges from summary field\n\t// Matches formats like:\n\t// Replace: line 10\n\t// Remove: line 10\n\t// Replace: lines 10-20\n\t// Remove: lines 10-20\n\tsingleLineRegex := regexp.MustCompile(`(?i)(Replace|Replaces|Remove|Removes).*?(\\d+)`)\n\tlineRangeRegex := regexp.MustCompile(`(?i)(Replace|Replaces|Remove|Removes).*?(\\d+)-(\\d+)`)\n\n\tdescLines := strings.Split(desc, \"\\n\")\n\n\tfor _, line := range descLines {\n\t\tline = strings.TrimSpace(line)\n\n\t\tmatchesRemaining := true\n\t\tfor matchesRemaining {\n\t\t\t// first see if there's a multi-line match\n\t\t\tif lineRangeMatch := lineRangeRegex.FindStringSubmatch(strings.ToLower(line)); len(lineRangeMatch) > 3 {\n\t\t\t\t// we have a multi-line match\n\t\t\t\tstart, startErr := strconv.Atoi(lineRangeMatch[2])\n\t\t\t\tend, endErr := strconv.Atoi(lineRangeMatch[3])\n\t\t\t\tif startErr == nil && endErr == nil {\n\t\t\t\t\tremovalRanges = append(removalRanges, RemovalRange{\n\t\t\t\t\t\tStart: start,\n\t\t\t\t\t\tEnd:   end,\n\t\t\t\t\t})\n\t\t\t\t\tline = strings.Replace(line, fmt.Sprintf(\"%d-%d\", start, end), \"\", 1)\n\t\t\t\t} else {\n\t\t\t\t\tlog.Println(\"ApplyChanges - multi-line match error\")\n\t\t\t\t\tlog.Println(spew.Sdump(startErr))\n\t\t\t\t\tlog.Println(spew.Sdump(endErr))\n\t\t\t\t\tmatchesRemaining = false\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// try single-line match\n\t\t\t\tif singleLineMatch := singleLineRegex.FindStringSubmatch(strings.ToLower(line)); len(singleLineMatch) > 2 {\n\t\t\t\t\tif lineNum, err := strconv.Atoi(singleLineMatch[2]); err == nil {\n\t\t\t\t\t\tremovalRanges = append(removalRanges, RemovalRange{\n\t\t\t\t\t\t\tStart: lineNum,\n\t\t\t\t\t\t\tEnd:   lineNum,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tline = strings.Replace(line, singleLineMatch[2], \"\", 1)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Println(\"ApplyChanges - single-line match error\")\n\t\t\t\t\t\tlog.Println(spew.Sdump(err))\n\t\t\t\t\t\tmatchesRemaining = false\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tmatchesRemaining = false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif verboseLogging {\n\t\tlog.Println(\"ApplyChanges - removal ranges:\")\n\t\tlog.Println(spew.Sdump(removalRanges))\n\t}\n\n\tvar res *ApplyChangesResult\n\n\tif isEntireFileUpdate {\n\t\thasRefsOrRemovals := len(references) > 0 || len(removals) > 0\n\n\t\tif !hasRefsOrRemovals {\n\t\t\t// shortcut to just return the full updated file and skip verification\n\t\t\t// if any references were included or the first and last lines of the proposed file don't match the first and last lines of the original file\n\t\t\tres = &ApplyChangesResult{\n\t\t\t\tNewFile:  proposed,\n\t\t\t\tProposed: proposed,\n\t\t\t}\n\t\t}\n\t}\n\n\tif res == nil {\n\t\tif addMissingStartEndRefs {\n\t\t\tvar beginsWithRef bool = false\n\t\t\tvar endsWithRef bool = false\n\t\t\tvar foundNonRefLine bool = false\n\n\t\t\tfor i, line := range proposedLines {\n\t\t\t\thasRef := hasRefByLine[i+1] || hasRemovalByLine[i+1]\n\n\t\t\t\tif hasRef {\n\t\t\t\t\tif !foundNonRefLine {\n\t\t\t\t\t\tbeginsWithRef = true\n\t\t\t\t\t}\n\t\t\t\t\tendsWithRef = true\n\t\t\t\t} else if line != \"\" {\n\t\t\t\t\tfoundNonRefLine = true\n\t\t\t\t\tendsWithRef = false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !beginsWithRef &&\n\t\t\t\t!isEntireFileUpdate &&\n\t\t\t\t!((isReplace || isRemove) &&\n\t\t\t\t\tstrings.Contains(desc, \"start of the file\")) {\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"adding ... existing code ... to start of file\")\n\t\t\t\t}\n\n\t\t\t\tproposedLines = append([]string{\"\"}, proposedLines...)\n\n\t\t\t\t// bump all existing references up by 1\n\t\t\t\tfor i, ref := range references {\n\t\t\t\t\treferences[i] = Reference(int(ref) + 1)\n\t\t\t\t}\n\t\t\t\treferences = append([]Reference{Reference(1)}, references...)\n\t\t\t}\n\n\t\t\tif !endsWithRef &&\n\t\t\t\t!isEntireFileUpdate &&\n\t\t\t\t!((isReplace || isRemove) &&\n\t\t\t\t\tstrings.Contains(desc, \"end of the file\")) {\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"adding ... existing code ... to end of file\")\n\t\t\t\t}\n\n\t\t\t\tproposedLines = append(proposedLines, \"\")\n\t\t\t\treferences = append(references, Reference(len(proposedLines)))\n\t\t\t}\n\t\t}\n\n\t\tproposed = strings.Join(proposedLines, \"\\n\")\n\n\t\tisPureInsert := !isEntireFileUpdate && !isReplace && !isRemove && (isAdd || isPrepend || isAppend)\n\n\t\tif isPureInsert {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println(\"isPureInsert\")\n\t\t\t}\n\t\t}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Println(\"proposed:\")\n\t\t\tfmt.Println(proposed)\n\t\t\tlog.Println(\"ApplyChanges - references:\")\n\t\t\tspew.Dump(references)\n\t\t\tlog.Println(\"ApplyChanges - removals:\")\n\t\t\tspew.Dump(removals)\n\t\t}\n\n\t\tif !isPureInsert {\n\t\t\tlog.Println(\"ApplyChanges - removal ranges:\")\n\t\t\tlog.Println(spew.Sdump(removalRanges))\n\t\t}\n\n\t\tres = ExecApplyGeneric(\n\t\t\texecApplyGenericParams{\n\t\t\t\toriginal:      original,\n\t\t\t\tproposed:      proposed,\n\t\t\t\toriginalLines: originalLines,\n\t\t\t\tproposedLines: proposedLines,\n\t\t\t\treferences:    references,\n\t\t\t\tremovals:      removals,\n\t\t\t\tisInsert:      isPureInsert,\n\t\t\t\tremovalRanges: removalRanges,\n\t\t\t},\n\t\t)\n\n\t\tres.Proposed = proposed\n\n\t\tif len(res.NeedsVerifyReasons) > 0 {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"ApplyChanges - needs verify reasons:\")\n\t\t\t\tlog.Println(spew.Sdump(res.NeedsVerifyReasons))\n\n\t\t\t\tlog.Println(\"ApplyChanges - proposed:\")\n\t\t\t\tlog.Println(proposedInitial)\n\t\t\t\tlog.Println(\"--------------------------------\")\n\n\t\t\t\t// log.Println(\"ApplyChanges - original:\")\n\t\t\t\t// log.Println(original)\n\t\t\t\t// log.Println(\"--------------------------------\")\n\t\t\t}\n\n\t\t\tif len(res.NeedsVerifyReasons) == 1 && res.NeedsVerifyReasons[0] == NeedsVerifyReasonAmbiguousLocation && parser != nil {\n\t\t\t\tvar err error\n\t\t\t\tprevRes := res\n\t\t\t\tres, err = ExecApplyTreeSitter(\n\t\t\t\t\texecApplyTreeSitterParams{\n\t\t\t\t\t\toriginal:   original,\n\t\t\t\t\t\tproposed:   proposed,\n\t\t\t\t\t\treferences: references,\n\t\t\t\t\t\tremovals:   removals,\n\t\t\t\t\t\tlanguage:   language,\n\t\t\t\t\t\tparser:     parser,\n\t\t\t\t\t\tctx:        ctx,\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"ApplyChanges - error applying tree-sitter: %v\", err)\n\t\t\t\t\t// since we got an error, give up and go back to the previous result\n\t\t\t\t\tres = prevRes\n\t\t\t\t} else if len(res.NeedsVerifyReasons) > 0 {\n\t\t\t\t\treturn res\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// we want to verify if any code was removed/replaced based on length comparison or description\n\t// check if code was removed based on length comparison\n\tif len(res.NewFile) < len(original) {\n\t\tlog.Println(\"ApplyChanges - code removed based on length comparison - needs verification\")\n\t\tres.NeedsVerifyReasons = append(res.NeedsVerifyReasons, NeedsVerifyReasonCodeRemoved)\n\t} else if isRemove || isReplace || isEntireFileUpdate {\n\t\tlog.Println(\"ApplyChanges - code removed based on description - needs verification\")\n\t\tres.NeedsVerifyReasons = append(res.NeedsVerifyReasons, NeedsVerifyReasonCodeRemoved)\n\t} else {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"ApplyChanges - checking for removed lines\")\n\t\t}\n\n\t\toriginalLineMap := make(map[string]bool)\n\t\tfor _, line := range originalLines {\n\t\t\toriginalLineMap[strings.TrimSpace(line)] = true\n\t\t}\n\n\t\t// log.Println(\"NewFile:\")\n\t\t// log.Println(strconv.Quote(res.NewFile))\n\n\t\tnewLines := strings.Split(res.NewFile, \"\\n\")\n\t\tnewLineMap := make(map[string]bool)\n\t\tfor _, line := range newLines {\n\t\t\tnewLineMap[strings.TrimSpace(line)] = true\n\t\t}\n\n\t\tlog.Println(\"ApplyChanges - originalLineMap:\")\n\t\tspew.Dump(originalLineMap)\n\t\tlog.Println(\"ApplyChanges - newLineMap:\")\n\t\tspew.Dump(newLineMap)\n\n\t\t// Check for removed lines (lines in original that are not in new)\n\t\tfor line := range originalLineMap {\n\t\t\tif !newLineMap[line] {\n\t\t\t\tlog.Println(\"ApplyChanges - code removed based on line comparison - needs verification\")\n\t\t\t\tlog.Println(\"line:\")\n\t\t\t\tlog.Println(line)\n\n\t\t\t\tres.NeedsVerifyReasons = append(res.NeedsVerifyReasons, NeedsVerifyReasonCodeRemoved)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// Duplication check is now redundant since we verify all replacements\n\t// if strings.Contains(desc, \" replace \") {\n\t// \tif verboseLogging {\n\t// \t\tlog.Println(\"ApplyChanges - checking for duplicated lines\")\n\t// \t}\n\t//\n\t// \t// Check for lines in proposed updates that are duplicated in new file\n\t// \tnewLineFreq := make(map[string]int)\n\t// \toriginalLineFreq := make(map[string]int)\n\t// \tproposedLineFreq := make(map[string]int)\n\t//\n\t// \t// First count frequencies in original file\n\t// \tfor _, line := range originalLines {\n\t// \t\ttrimmed := strings.TrimSpace(line)\n\t// \t\tif len(trimmed) > duplicationThreshold {\n\t// \t\t\toriginalLineFreq[line]++\n\t// \t\t}\n\t// \t}\n\t//\n\t// \t// Count frequencies in proposed file\n\t// \tfor _, line := range proposedLines {\n\t// \t\ttrimmed := strings.TrimSpace(line)\n\t// \t\tif len(trimmed) > duplicationThreshold {\n\t// \t\t\tproposedLineFreq[line]++\n\t// \t\t}\n\t// \t}\n\t//\n\t// \t// Count frequencies in new file\n\t// \tfor _, line := range newLines {\n\t// \t\ttrimmed := strings.TrimSpace(line)\n\t// \t\tif len(trimmed) > duplicationThreshold {\n\t// \t\t\tnewLineFreq[line]++\n\t// \t\t}\n\t// \t}\n\t//\n\t// \t// Check proposed lines against new frequencies, accounting for original duplicates\n\t// \tfor _, line := range proposedLines {\n\t// \t\ttrimmed := strings.TrimSpace(line)\n\t// \t\tif len(trimmed) > duplicationThreshold {\n\t// \t\t\toriginalCount := originalLineFreq[line]\n\t// \t\t\tproposedCount := proposedLineFreq[line]\n\t// \t\t\tnewCount := newLineFreq[line]\n\t// \t\t\tif newCount > originalCount && newCount > proposedCount {\n\t// \t\t\t\tif verboseLogging {\n\t// \t\t\t\t\tlog.Println(\"ApplyChanges - code duplicated\")\n\t// \t\t\t\t\tlog.Println(\"line:\")\n\t// \t\t\t\t\tlog.Println(line)\n\t// \t\t\t\t\tlog.Printf(\"original occurrences: %d, new occurrences: %d\", originalCount, newLineFreq[line])\n\t// \t\t\t\t}\n\t// \t\t\t\tres.NeedsVerifyReasons = append(res.NeedsVerifyReasons, NeedsVerifyReasonCodeDuplicated)\n\t// \t\t\t\tbreak\n\t// \t\t\t}\n\t// \t\t}\n\t// \t}\n\t// }\n\n\t// if parser != nil {\n\t// \tcomments, err := FindComments(ctx, parser, proposed)\n\t// \tif err != nil {\n\t// \t\tlog.Printf(\"ApplyChanges - error finding comments: %v\", err)\n\t// \t}\n\t// \tres.Comments = comments\n\t// }\n\n\tif verboseLogging && len(res.BlocksRemoved) > 0 {\n\t\tlog.Printf(\"ApplyChanges - detected %d removed code blocks\", len(res.BlocksRemoved))\n\t\tfor i, block := range res.BlocksRemoved {\n\t\t\tlog.Printf(\"Block %d: lines %d-%d\", i+1, block.Start, block.End)\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Printf(\"Content: %s\", block.Content)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "app/server/syntax/structured_edits_generic.go",
    "content": "package syntax\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\ntype execApplyGenericParams struct {\n\toriginal,\n\tproposed string\n\toriginalLines,\n\tproposedLines []string\n\treferences    []Reference\n\tremovals      []Removal\n\tisInsert      bool\n\tremovalRanges []RemovalRange\n}\n\nfunc ExecApplyGeneric(\n\tparams execApplyGenericParams,\n) *ApplyChangesResult {\n\toriginalLines := params.originalLines\n\tproposedLines := params.proposedLines\n\treferences := params.references\n\tremovals := params.removals\n\tisInsert := params.isInsert\n\tremovalRanges := params.removalRanges\n\n\tres := &ApplyChangesResult{}\n\n\tvar b strings.Builder\n\n\twrite := func(s string, newline bool) {\n\t\tif verboseLogging {\n\t\t\ttoLog := s\n\n\t\t\tif len(toLog) > 200 {\n\t\t\t\ttoLog = toLog[:100] + \"\\n...\\n\" + toLog[len(toLog)-100:]\n\t\t\t}\n\n\t\t\tfmt.Printf(\"writing: %s\\n\", toLog)\n\t\t\tfmt.Printf(\"newline: %v\\n\", newline)\n\t\t}\n\n\t\tb.WriteString(s)\n\t\tif newline {\n\t\t\tb.WriteByte('\\n')\n\t\t}\n\t}\n\n\trefsByLine := map[Reference]bool{}\n\tremovalsByLine := map[Removal]bool{}\n\n\tfor _, ref := range references {\n\t\trefsByLine[ref] = true\n\t}\n\n\tfor _, removal := range removals {\n\t\tremovalsByLine[removal] = true\n\t}\n\n\tanchorMap := buildAnchorMap(\n\t\toriginalLines,\n\t\tproposedLines,\n\t\trefsByLine,\n\t\tremovalsByLine,\n\t)\n\n\tif verboseLogging {\n\t\tfmt.Printf(\"anchorMap:\\n%v\\n\", spew.Sdump(anchorMap))\n\t}\n\n\tfindAnchor := func(pLineNum int) *Anchor {\n\t\toLineNum, ok := anchorMap[pLineNum]\n\t\tif ok {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"found anchor in anchorLines: oLineNum: %d, pLineNum: %d\\n\", oLineNum, pLineNum)\n\t\t\t}\n\n\t\t\toLine := originalLines[oLineNum-1]\n\t\t\tif strings.TrimSpace(oLine) == \"\" {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"skipping anchor because oLine is blank: %q\\n\", oLine)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tanchor := Anchor(oLineNum)\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"found anchor: %d\\n\", anchor)\n\t\t\t}\n\t\t\treturn &anchor\n\t\t} else {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"no anchor found in anchorMap: pLineNum: %d\\n\", pLineNum)\n\t\t\t\tfmt.Printf(\"anchorMap: %v\\n\", spew.Sdump(anchorMap))\n\n\t\t\t\t// for i, line := range originalLines {\n\t\t\t\t// \tfmt.Printf(\"originalLines[%d]: %q\\n\", i, line)\n\t\t\t\t// }\n\n\t\t\t\t// for i, line := range proposedLines {\n\t\t\t\t// \tfmt.Printf(\"proposedLines[%d]: %q\\n\", i, line)\n\t\t\t\t// }\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tvar oLineNum int = 0\n\tvar refOpen bool\n\tvar refStart int\n\tvar postRefBuffers []strings.Builder\n\n\tlastLineMatched := true\n\n\tsetOLineNum := func(n int) {\n\t\tif n < 1 {\n\t\t\tn = 1\n\t\t}\n\t\tif n > len(originalLines) {\n\t\t\tn = len(originalLines)\n\t\t}\n\t\toLineNum = n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"setting oLineNum: %d\\n\", oLineNum)\n\t\t}\n\t}\n\n\twriteToLatestPostRefBuffer := func(s string) {\n\t\tlatestBuffer := &postRefBuffers[len(postRefBuffers)-1]\n\t\tlatestBuffer.WriteString(s)\n\t\tlatestBuffer.WriteByte('\\n')\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"writing to latest postRefBuffer: %q\\n\", s)\n\t\t}\n\t}\n\n\taddNewPostRefBuffer := func() {\n\t\tpostRefBuffers = append(postRefBuffers, strings.Builder{})\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"adding new postRefBuffer\\n\")\n\t\t}\n\t}\n\n\tresetPostRefBuffers := func() {\n\t\tpostRefBuffers = []strings.Builder{}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"resetting postRefBuffers\\n\")\n\t\t}\n\t}\n\n\twriteRefs := func(eof bool) bool {\n\t\tvar fullRef []string\n\t\tif eof {\n\t\t\tstart := refStart - 1\n\t\t\tif start < 0 {\n\t\t\t\tstart = 0\n\t\t\t}\n\t\t\tif start >= len(originalLines) {\n\t\t\t\tstart = len(originalLines) - 1\n\t\t\t}\n\t\t\tfullRef = originalLines[start:]\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println(\"eof\")\n\t\t\t\tfmt.Printf(\"fullRef refStart: %d\\n\", refStart)\n\t\t\t\tfmt.Printf(\"originalLines[refStart]: %q\\n\", originalLines[refStart])\n\t\t\t\tfmt.Printf(\"writing eof fullRef: %q\\n\", strings.Join(fullRef, \"\\n\"))\n\t\t\t}\n\t\t} else {\n\t\t\tstart := refStart - 1\n\t\t\tif start < 0 {\n\t\t\t\tstart = 0\n\t\t\t}\n\t\t\tif start >= len(originalLines) {\n\t\t\t\tstart = len(originalLines) - 1\n\t\t\t}\n\t\t\tend := oLineNum - 1\n\t\t\tif end < 1 {\n\t\t\t\tend = 0\n\t\t\t}\n\t\t\tif end >= len(originalLines) {\n\t\t\t\tend = len(originalLines) - 1\n\t\t\t}\n\n\t\t\t// Add detailed diagnostic logging for invalid slice bounds\n\t\t\tif start > end {\n\t\t\t\tfmt.Printf(\"\\n=== INVALID SLICE BOUNDS DIAGNOSTIC INFO ===\\n\")\n\t\t\t\tfmt.Printf(\"start: %d, end: %d\\n\", start, end)\n\t\t\t\tfmt.Printf(\"refStart: %d, oLineNum: %d\\n\", refStart, oLineNum)\n\n\t\t\t\t// Log relevant lines for context\n\t\t\t\tfmt.Printf(\"\\nOriginal lines context:\\n\")\n\t\t\t\tstartContext := max(0, start-2)\n\t\t\t\tendContext := min(len(originalLines), end+3)\n\t\t\t\tfor i := startContext; i < endContext; i++ {\n\t\t\t\t\tfmt.Printf(\"line %d: %q\\n\", i+1, originalLines[i])\n\t\t\t\t}\n\n\t\t\t\tfmt.Printf(\"\\nProposed lines context:\\n\")\n\t\t\t\tfmt.Printf(\"=====================================\\n\\n\")\n\n\t\t\t\tres.NeedsVerifyReasons = append(res.NeedsVerifyReasons, NeedsVerifyReasonAmbiguousLocation)\n\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"writing fullRef\\n\")\n\t\t\t\tfmt.Printf(\"refStart: %d, oLineNum: %d\\n\", refStart, oLineNum)\n\t\t\t\tfmt.Printf(\"start: %d, end: %d\\n\", start, end)\n\t\t\t}\n\t\t\tfullRef = originalLines[start:end]\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"fullRef refStart: %d, oLineNum-1: %d\\n\", refStart, oLineNum-1)\n\t\t\t\tfmt.Printf(\"originalLines[start]: %q\\n\", originalLines[start])\n\t\t\t\tfmt.Printf(\"originalLines[end]: %q\\n\", originalLines[end])\n\t\t\t}\n\t\t}\n\n\t\tnumRefs := len(postRefBuffers)\n\t\tif numRefs == 1 {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println(\"writeRefs\")\n\t\t\t\tfmt.Printf(\"numRefs == 1, refStart: %d, oLineNum: %d\\n\", refStart, oLineNum)\n\t\t\t}\n\n\t\t\twrite(strings.Join(fullRef, \"\\n\"), !eof)\n\n\t\t\tpostRefContent := postRefBuffers[0].String()\n\n\t\t\tif strings.TrimSpace(postRefContent) != \"\" {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"writing postRefBuffer\")\n\t\t\t\t}\n\t\t\t\twrite(postRefBuffers[0].String(), false)\n\t\t\t}\n\t\t} else {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"writeRefs - ambiguous location - numRefs: %d\\n\", numRefs)\n\t\t\t}\n\n\t\t\tres.NeedsVerifyReasons = append(res.NeedsVerifyReasons, NeedsVerifyReasonAmbiguousLocation)\n\n\t\t\treturn true\n\t\t}\n\n\t\treturn false\n\t}\n\n\treachedEndOfOriginal := false\n\n\tfor idx, pLine := range proposedLines {\n\t\tfinalLine := idx == len(proposedLines)-1\n\t\tpLineNum := idx + 1\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"\\n\\ni: %d, num: %d, pLine: %q, refOpen: %v\\n\", idx, pLineNum, pLine, refOpen)\n\t\t}\n\n\t\tvar isRef, isRemoval, nextPLineIsRef bool\n\n\t\tif !reachedEndOfOriginal {\n\t\t\tisRef = refsByLine[Reference(pLineNum)]\n\t\t\tisRemoval = removalsByLine[Removal(pLineNum)]\n\t\t\tnextPLineIsRef = refsByLine[Reference(pLineNum+1)]\n\t\t}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"isRef: %v\\n\", isRef)\n\t\t\tfmt.Printf(\"isRemoval: %v\\n\", isRemoval)\n\t\t\tfmt.Printf(\"nextPLineIsRef: %v\\n\", nextPLineIsRef)\n\t\t\tfmt.Printf(\"oLineNum before: %d\\n\", oLineNum)\n\t\t\tif oLineNum > 0 && oLineNum < len(originalLines) {\n\t\t\t\tfmt.Printf(\"oLine before: %q\\n\", originalLines[oLineNum-1])\n\t\t\t}\n\t\t\tfmt.Printf(\"lastLineMatched: %v\\n\", lastLineMatched)\n\t\t}\n\n\t\tif isRemoval {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println(\"isRemoval - skip line\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif isRef {\n\t\t\tif !refOpen {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"isRef - opening ref\")\n\t\t\t\t}\n\n\t\t\t\trefOpen = true\n\t\t\t\tsetOLineNum(oLineNum + 1)\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"setting refStart: %d\\n\", refStart)\n\t\t\t\t}\n\n\t\t\t\trefStart = oLineNum\n\t\t\t}\n\n\t\t\taddNewPostRefBuffer()\n\n\t\t\tif oLineNum == len(originalLines) {\n\t\t\t\treachedEndOfOriginal = true\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif !reachedEndOfOriginal && !refOpen && lastLineMatched {\n\t\t\tif strings.TrimSpace(pLine) == \"\" {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"pLine is blank\\n\")\n\t\t\t\t\tif oLineNum < len(originalLines) {\n\t\t\t\t\t\tfmt.Printf(\"nextOLine: %q\\n\", originalLines[oLineNum])\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tnextOLineIsBlank := oLineNum > 0 && oLineNum < len(originalLines) && strings.TrimSpace(originalLines[oLineNum]) == \"\"\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"nextPLineIsRef: %v\\n\", nextPLineIsRef)\n\t\t\t\t\tfmt.Printf(\"nextOLineIsBlank: %v\\n\", nextOLineIsBlank)\n\t\t\t\t}\n\n\t\t\t\tif !nextPLineIsRef || nextOLineIsBlank {\n\t\t\t\t\twrite(pLine, !finalLine)\n\t\t\t\t}\n\n\t\t\t\t// Check if next line in original is also blank\n\t\t\t\tif nextOLineIsBlank {\n\t\t\t\t\tsetOLineNum(oLineNum + 1)\n\t\t\t\t}\n\n\t\t\t\tif oLineNum == len(originalLines) {\n\t\t\t\t\treachedEndOfOriginal = true\n\t\t\t\t}\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tvar matching bool\n\n\t\tprevOLineNum := oLineNum\n\n\t\tif !reachedEndOfOriginal {\n\t\t\tanchor := findAnchor(pLineNum)\n\t\t\tif anchor != nil {\n\t\t\t\tmatching = true\n\t\t\t\tsetOLineNum(int(*anchor))\n\t\t\t}\n\t\t}\n\n\t\twroteRefs := false\n\t\tif matching {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"matching line: %s, oLineNum: %d\\n\", pLine, oLineNum)\n\t\t\t}\n\n\t\t\tif refOpen {\n\t\t\t\t// we found the end of the current reference\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"closing ref, oLineNum: %d\\n\", oLineNum)\n\t\t\t\t}\n\t\t\t\trefOpen = false\n\t\t\t\twillAbort := writeRefs(false)\n\t\t\t\twrite(pLine, !finalLine)\n\t\t\t\twroteRefs = true\n\t\t\t\tif willAbort {\n\t\t\t\t\treturn res\n\t\t\t\t}\n\t\t\t} else if oLineNum != prevOLineNum+1 {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"\\nExecApplyChanges - found non-adjacent anchor jump:\\n\")\n\t\t\t\t\tfmt.Printf(\"prevOLineNum: %d ('%s')\\n\", prevOLineNum, originalLines[prevOLineNum])\n\t\t\t\t\tfmt.Printf(\"oLineNum: %d ('%s')\\n\", oLineNum, originalLines[oLineNum-1])\n\t\t\t\t\tfmt.Printf(\"Lines that would be removed:\\n\")\n\t\t\t\t\tfor i := prevOLineNum; i < oLineNum-1; i++ {\n\t\t\t\t\t\tfmt.Printf(\"Line %d: '%s'\\n\", i, originalLines[i])\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tremovalRange := RemovalRange{\n\t\t\t\t\tStart: prevOLineNum,\n\t\t\t\t\tEnd:   oLineNum - 1,\n\t\t\t\t}\n\n\t\t\t\tif isInsert {\n\t\t\t\t\t// Write any lines that would have been removed\n\t\t\t\t\tfor i := prevOLineNum; i < oLineNum-1; i++ {\n\t\t\t\t\t\twrite(originalLines[i], true)\n\t\t\t\t\t}\n\t\t\t\t} else if len(removalRanges) > 0 {\n\t\t\t\t\toverlapsAny := false\n\t\t\t\t\tfor _, r := range removalRanges {\n\t\t\t\t\t\tif removalRange.Overlaps(r) {\n\t\t\t\t\t\t\toverlapsAny = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// if the removal doesn't overlap with any of the listed removal ranges, we can deterministically catch a mistake and write the lines that would have been removed\n\t\t\t\t\tif !overlapsAny {\n\t\t\t\t\t\t// Write any lines that would have been removed\n\t\t\t\t\t\tfor i := prevOLineNum; i < oLineNum-1; i++ {\n\t\t\t\t\t\t\twrite(originalLines[i], true)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"no matching line\\n\")\n\t\t\t}\n\t\t}\n\n\t\tif wroteRefs {\n\t\t\t// reset buffers\n\t\t\tresetPostRefBuffers()\n\t\t} else {\n\t\t\tif refOpen {\n\t\t\t\twriteToLatestPostRefBuffer(pLine)\n\t\t\t} else {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"writing pLine: %s\\n\", pLine)\n\t\t\t\t}\n\t\t\t\twrite(pLine, !finalLine)\n\t\t\t}\n\n\t\t}\n\n\t\tif oLineNum == len(originalLines) {\n\t\t\treachedEndOfOriginal = true\n\t\t}\n\t}\n\n\tif refOpen {\n\t\twillAbort := writeRefs(true)\n\t\tif willAbort {\n\t\t\treturn res\n\t\t}\n\t}\n\n\tif verboseLogging {\n\t\t// fmt.Printf(\"final result:\\n%s\\n\", b.String())\n\t}\n\n\tres.NewFile = b.String()\n\n\treturn res\n}\n\nfunc buildAnchorMap(\n\toriginalLines []string,\n\tproposedLines []string,\n\trefsByLine map[Reference]bool,\n\tremovalsByLine map[Removal]bool,\n) AnchorMap {\n\tresult := AnchorMap{}\n\n\tsetAnchor := func(pLine, oLine int) {\n\t\tresult[pLine] = oLine\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"setAnchor - pLine: %d, oLine: %d\\n\", pLine, oLine)\n\t\t}\n\t}\n\n\treferenceBlocks := []ReferenceBlock{}\n\n\t// Helper to check if a line is in a reference block\n\tisInReference := func(lineNum int) bool {\n\t\tfor _, block := range referenceBlocks {\n\t\t\tif lineNum >= block.start && lineNum <= block.end {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\tallRefsByLine := map[int]bool{}\n\tfor ref := range refsByLine {\n\t\tallRefsByLine[int(ref)] = true\n\t}\n\tfor removal := range removalsByLine {\n\t\tallRefsByLine[int(removal)] = true\n\t}\n\n\t// Keep track of definitely new code lines\n\tnewCodeLines := make(map[int]bool)\n\toriginalLinesSet := map[string]bool{}\n\tfor _, line := range originalLines {\n\t\toriginalLinesSet[line] = true\n\t}\n\tfor idx, line := range proposedLines {\n\t\tif _, inOriginal := originalLinesSet[line]; !inOriginal {\n\t\t\tnewCodeLines[idx+1] = true\n\t\t}\n\t}\n\n\tfoundRefBounds := map[int]bool{}\n\n\t// When we establish an anchor match, check if we can determine reference bounds\n\ttryEstablishReferenceBounds := func() {\n\t\tif verboseLogging {\n\t\t\tfmt.Println(\"\\n\\ntryEstablishReferenceBounds\")\n\t\t}\n\t\tfor lineNum := range allRefsByLine {\n\t\t\tif !foundRefBounds[lineNum] {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"tryEstablishReferenceBounds - lineNum: %d\\n\", lineNum)\n\t\t\t\t}\n\n\t\t\t\tprevSignificantLineNum := lineNum - 1\n\t\t\t\tlinesBack := 1\n\t\t\t\tfor prevSignificantLineNum > 0 && proposedLines[prevSignificantLineNum-1] == \"\" {\n\t\t\t\t\tprevSignificantLineNum--\n\t\t\t\t\tlinesBack++\n\t\t\t\t}\n\n\t\t\t\tnextSignificantLineNum := lineNum + 1\n\t\t\t\tlinesForward := 1\n\t\t\t\tfor nextSignificantLineNum <= len(proposedLines) && proposedLines[nextSignificantLineNum-1] == \"\" {\n\t\t\t\t\tnextSignificantLineNum++\n\t\t\t\t\tlinesForward++\n\t\t\t\t}\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"prevSignificantLineNum: %d, nextSignificantLineNum: %d\\n\", prevSignificantLineNum, nextSignificantLineNum)\n\t\t\t\t}\n\n\t\t\t\tvar top, bottom int\n\n\t\t\t\tif prevSignificantLineNum <= 1 {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"prevSignificantLineNum <= 1 - setting top to 1\\n\")\n\t\t\t\t\t}\n\t\t\t\t\ttop = 1\n\t\t\t\t} else if _, isAnchor := result[prevSignificantLineNum]; isAnchor {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"prevSignificantLineNum is anchor - setting top to %d\\n\", result[prevSignificantLineNum])\n\t\t\t\t\t}\n\t\t\t\t\ttop = result[prevSignificantLineNum] + linesBack\n\t\t\t\t}\n\n\t\t\t\tif nextSignificantLineNum >= len(proposedLines) {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"nextSignificantLineNum >= len(proposedLines) - setting bottom to len(originalLines)\\n\")\n\t\t\t\t\t}\n\t\t\t\t\tbottom = len(originalLines)\n\t\t\t\t} else if _, isAnchor := result[nextSignificantLineNum]; isAnchor {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"nextSignificantLineNum is anchor - setting bottom to %d\\n\", result[nextSignificantLineNum])\n\t\t\t\t\t}\n\t\t\t\t\tbottom = result[nextSignificantLineNum] - linesForward\n\t\t\t\t} else if newCodeLines[nextSignificantLineNum] {\n\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"nextSignificantLineNum is new code - finding next anchor\\n\")\n\t\t\t\t\t}\n\n\t\t\t\t\t// go forward from here to find the next anchor (or eof)\n\t\t\t\t\tfoundAnchor := false\n\t\t\t\t\tfor i := nextSignificantLineNum; i < len(proposedLines)+1; i++ {\n\t\t\t\t\t\tif _, isAnchor := result[i]; isAnchor {\n\t\t\t\t\t\t\tbottom = result[i] - 1\n\t\t\t\t\t\t\tfoundAnchor = true\n\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\tfmt.Printf(\"found anchor at %d\\n\", i)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif !foundAnchor {\n\t\t\t\t\t\tbottom = len(originalLines)\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Printf(\"no anchor found - setting bottom to len(originalLines)\\n\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif top != 0 && bottom != 0 {\n\t\t\t\t\tfoundRefBounds[lineNum] = true\n\t\t\t\t\treferenceBlocks = append(referenceBlocks, ReferenceBlock{start: top, end: bottom})\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"found reference bounds: %d-%d\\n\", top, bottom)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"no reference bounds found for lineNum: %d\\n\", lineNum)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif verboseLogging {\n\t\tfmt.Printf(\"\\n=== Building Anchor Map ===\\n\")\n\t}\n\n\tvar matchSection func(pStart, pEnd, oStart, oEnd int)\n\tmatchSection = func(pStart, pEnd, oStart, oEnd int) {\n\t\tif pEnd <= pStart || oEnd <= oStart {\n\t\t\treturn\n\t\t}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"\\n--- Processing Section ---\\n\")\n\t\t\tfmt.Printf(\"Proposed lines %d-%d, Original lines %d-%d\\n\", pStart, pEnd, oStart, oEnd)\n\t\t}\n\n\t\t// First find unique matches in this section\n\t\tsectionOriginal := make(map[string][]int)\n\t\tsectionProposed := make(map[string][]int)\n\n\t\t// Build frequency maps for this section\n\t\tfor i := oStart; i < oEnd; i++ {\n\t\t\tcontent := originalLines[i]\n\t\t\tsectionOriginal[content] = append(sectionOriginal[content], i)\n\t\t}\n\n\t\tfor i := pStart; i < pEnd; i++ {\n\t\t\tcontent := proposedLines[i]\n\t\t\tsectionProposed[content] = append(sectionProposed[content], i)\n\t\t}\n\n\t\t// Handle unique matches first\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"\\nProcessing unique matches...\\n\")\n\t\t}\n\t\tfor content, pIndices := range sectionProposed {\n\t\t\tif oIndices, exists := sectionOriginal[content]; exists && len(oIndices) == 1 && len(pIndices) == 1 {\n\t\t\t\tpIdx, oIdx := pIndices[0], oIndices[0]\n\t\t\t\tif _, exists := result[pIdx+1]; !exists {\n\t\t\t\t\tsetAnchor(pIdx+1, oIdx+1)\n\t\t\t\t}\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"Found unique match: %q\\n\", content)\n\t\t\t\t\tfmt.Printf(\"Mapping proposed line %d (%q) -> original line %d (%q)\\n\",\n\t\t\t\t\t\tpIdx+1, proposedLines[pIdx], oIdx+1, originalLines[oIdx])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// after finding unique anchors, try to establish reference bounds\n\t\ttryEstablishReferenceBounds()\n\n\t\t// Get ordered anchors to establish subsections\n\t\tvar orderedAnchors []struct {\n\t\t\tpLine int\n\t\t\toLine int\n\t\t}\n\t\tfor pLine := pStart; pLine < pEnd; pLine++ {\n\t\t\tif anchor, exists := result[pLine+1]; exists {\n\t\t\t\torderedAnchors = append(orderedAnchors, struct {\n\t\t\t\t\tpLine int\n\t\t\t\t\toLine int\n\t\t\t\t}{pLine, anchor - 1})\n\t\t\t}\n\t\t}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"\\nOrdered anchors in section: %v\\n\", orderedAnchors)\n\t\t}\n\n\t\t// Sort anchors by proposed line number\n\t\tsort.Slice(orderedAnchors, func(i, j int) bool {\n\t\t\treturn orderedAnchors[i].pLine < orderedAnchors[j].pLine\n\t\t})\n\n\t\t// Process each subsection between anchors\n\t\tlastPLine := pStart\n\t\tlastOLine := oStart\n\n\t\tfor i := 0; i <= len(orderedAnchors); i++ {\n\t\t\tvar nextPLine, nextOLine int\n\t\t\tif i < len(orderedAnchors) {\n\t\t\t\tnextPLine = orderedAnchors[i].pLine\n\t\t\t\tnextOLine = orderedAnchors[i].oLine\n\t\t\t} else {\n\t\t\t\tnextPLine = pEnd\n\t\t\t\tnextOLine = oEnd\n\t\t\t}\n\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"\\nProcessing subsection %d\\n\", i)\n\t\t\t\tfmt.Printf(\"Proposed lines %d-%d, Original lines %d-%d\\n\", lastPLine, nextPLine, lastOLine, nextOLine)\n\t\t\t}\n\n\t\t\t// Handle duplicates in this subsection using outside-in matching\n\t\t\tsubSectionOriginal := make(map[string][]int)\n\t\t\tsubSectionProposed := make(map[string][]int)\n\n\t\t\t// Build frequency maps for this subsection\n\t\t\tfor i := lastOLine; i < nextOLine; i++ {\n\t\t\t\tcontent := originalLines[i]\n\t\t\t\tsubSectionOriginal[content] = append(subSectionOriginal[content], i)\n\t\t\t}\n\n\t\t\tfor i := lastPLine; i < nextPLine; i++ {\n\t\t\t\tcontent := proposedLines[i]\n\t\t\t\tsubSectionProposed[content] = append(subSectionProposed[content], i)\n\t\t\t}\n\n\t\t\t// Match duplicates from outside-in\n\t\t\tfor content, pIndices := range subSectionProposed {\n\t\t\t\toIndices, exists := subSectionOriginal[content]\n\t\t\t\tif !exists {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"New code found: %q at proposed lines %v\\n\", content, pIndices)\n\t\t\t\t\t}\n\n\t\t\t\t\tcontinue // This is new code to be inserted\n\t\t\t\t}\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"\\nMatching duplicates for content: %q\\n\", content)\n\t\t\t\t\tfmt.Printf(\"Found in proposed lines: %v\\n\", pIndices)\n\t\t\t\t\tfmt.Printf(\"Found in original lines: %v\\n\", oIndices)\n\t\t\t\t}\n\n\t\t\t\t// Filter oIndices to only include matches within subsection boundaries\n\t\t\t\tvar validOIndices []int\n\t\t\t\tfor _, idx := range oIndices {\n\t\t\t\t\tif idx >= lastOLine && idx < nextOLine {\n\t\t\t\t\t\tvalidOIndices = append(validOIndices, idx)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\toIndices = validOIndices\n\n\t\t\t\t// Match from outside in until we run out of original occurrences\n\t\t\t\tmatched := 0\n\t\t\t\tfor matched < len(oIndices) && matched*2 < len(pIndices) {\n\n\t\t\t\t\tif isInReference(oIndices[matched] + 1) {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Printf(\"Skipping reference line %d\\n\", oIndices[matched]+1)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatched++\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\t// Match first unmatched occurrence\n\t\t\t\t\tif _, exists := result[pIndices[matched]+1]; !exists {\n\t\t\t\t\t\tsetAnchor(pIndices[matched]+1, oIndices[matched]+1)\n\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Printf(\"Matched first occurrence: proposed line %d (%q) -> original line %d (%q)\\n\",\n\t\t\t\t\t\t\t\tpIndices[matched]+1, proposedLines[pIndices[matched]],\n\t\t\t\t\t\t\t\toIndices[matched]+1, originalLines[oIndices[matched]])\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// after finding anchor, try to establish reference bounds\n\t\t\t\t\t\ttryEstablishReferenceBounds()\n\t\t\t\t\t}\n\n\t\t\t\t\t// Match last unmatched occurrence if we have more to match\n\t\t\t\t\tif matched*2+1 < len(pIndices) {\n\t\t\t\t\t\tlastOrigIdx := oIndices[len(oIndices)-1-matched]\n\t\t\t\t\t\tlastPropIdx := pIndices[len(pIndices)-1-matched]\n\t\t\t\t\t\tif isInReference(lastOrigIdx + 1) {\n\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\tfmt.Printf(\"Skipping reference line %d\\n\", lastOrigIdx+1)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmatched++\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif _, exists := result[lastPropIdx+1]; !exists {\n\t\t\t\t\t\t\tsetAnchor(lastPropIdx+1, lastOrigIdx+1)\n\t\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\t\tfmt.Printf(\"Matched last occurrence: proposed line %d (%q) -> original line %d (%q)\\n\",\n\t\t\t\t\t\t\t\t\tlastPropIdx+1, proposedLines[lastPropIdx],\n\t\t\t\t\t\t\t\t\tlastOrigIdx+1, originalLines[lastOrigIdx])\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// after finding anchor, try to establish reference bounds\n\t\t\t\t\t\t\ttryEstablishReferenceBounds()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t}\n\t\t\t\t\tmatched++\n\t\t\t\t}\n\t\t\t\t// Any remaining occurrences in pIndices are new code to be inserted\n\t\t\t}\n\n\t\t\t// Recursively process the next subsection\n\t\t\tif i < len(orderedAnchors) {\n\t\t\t\tlastPLine = nextPLine\n\t\t\t\tlastOLine = nextOLine\n\t\t\t}\n\t\t}\n\t}\n\n\t// Start recursive matching with full file\n\tmatchSection(0, len(proposedLines), 0, len(originalLines))\n\n\tif verboseLogging {\n\t\tfmt.Printf(\"\\n=== Final Anchor Map ===\\n\")\n\t\tfmt.Printf(\"Result: %v\\n\", result)\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "app/server/syntax/structured_edits_sections.go",
    "content": "package syntax\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\ttree_sitter \"github.com/smacker/go-tree-sitter\"\n)\n\ntype TreeSitterSection []*tree_sitter.Node\n\ntype NodeIndex struct {\n\tnodesByLine   map[int]*tree_sitter.Node\n\tparentsByLine map[int]*tree_sitter.Node\n}\n\nfunc getSections(parent *tree_sitter.Node, bytes []byte, numSections, fromLine, upToLine int, foundAnyAnchor bool) []TreeSitterSection {\n\tsections := make([]TreeSitterSection, numSections)\n\tstructures := [][]*tree_sitter.Node{}\n\tlatestStructure := []*tree_sitter.Node{}\n\n\tcursor := tree_sitter.NewTreeCursor(parent)\n\tdefer cursor.Close()\n\n\tparentFirstLineNum := int(parent.StartPoint().Row) + 1\n\tparentEndLineNum := int(parent.EndPoint().Row) + 1\n\tif verboseLogging {\n\t\tfmt.Printf(\"parent.Type(): %s\\n\", parent.Type())\n\t\tfmt.Printf(\"parent.Content(bytes):\\n%q\\n\", parent.Content(bytes))\n\t\tfmt.Printf(\"parentFirstLineNum: %d\\n\", parentFirstLineNum)\n\t\tfmt.Printf(\"parentEndLineNum: %d\\n\", parentEndLineNum)\n\t}\n\n\tif cursor.GoToFirstChild() {\n\t\tfor {\n\t\t\tnode := cursor.CurrentNode()\n\n\t\t\tstartLineNum := int(node.StartPoint().Row) + 1\n\t\t\tendLineNum := int(node.EndPoint().Row) + 1\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"startLineNum: %d, endLineNum: %d\\n\", startLineNum, endLineNum)\n\t\t\t\tfmt.Println(node.Type())\n\t\t\t\tfmt.Printf(\"node.Content(bytes):\\n%q\\n\", node.Content(bytes))\n\t\t\t}\n\n\t\t\tif startLineNum < fromLine {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"startLineNum < fromLine, skipping\")\n\t\t\t\t\tfmt.Printf(\"skipping lineNum: %d | before fromLine: %d\\n\", startLineNum, fromLine)\n\t\t\t\t}\n\t\t\t\tif !cursor.GoToNextSibling() {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Println(\"no next sibling, breaking\")\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif startLineNum == parentFirstLineNum && foundAnyAnchor {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"skipping first line\\n\")\n\t\t\t\t}\n\t\t\t\tif !cursor.GoToNextSibling() {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Println(\"no next sibling, breaking\")\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif endLineNum > upToLine {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"endLineNum > upToLine, breaking\")\n\t\t\t\t\tfmt.Printf(\"upToLine: %d, endLineNum: %d, node.Type(): %s\\n\", upToLine, endLineNum, node.Type())\n\n\t\t\t\t\ttoLog := node.Content(bytes)\n\t\t\t\t\tif len(toLog) > 200 {\n\t\t\t\t\t\ttoLog = toLog[:100] + \"\\n...\\n\" + toLog[len(toLog)-100:]\n\t\t\t\t\t}\n\t\t\t\t\tfmt.Println(toLog)\n\t\t\t\t}\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif endLineNum == parentEndLineNum && !isStructuralNode(node) {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"endLineNum == parentEndLineNum && !isStructuralNode(node), breaking\")\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif isStructuralNode(node) {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"found structural node: %s\\n\", node.Type())\n\t\t\t\t\tfmt.Printf(\"starting new group\\n\")\n\t\t\t\t}\n\t\t\t\tif len(latestStructure) > 0 {\n\t\t\t\t\tstructures = append(structures, latestStructure)\n\t\t\t\t}\n\t\t\t\tlatestStructure = []*tree_sitter.Node{node}\n\t\t\t} else {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"not structural: %s\\n\", node.Type())\n\t\t\t\t}\n\t\t\t\tlatestStructure = append(latestStructure, node)\n\t\t\t}\n\n\t\t\tif !cursor.GoToNextSibling() {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"no next sibling, breaking\")\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif len(latestStructure) > 0 {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println(\"appending latestStructure to structures\")\n\t\t\t}\n\t\t\tstructures = append(structures, latestStructure)\n\t\t}\n\t}\n\n\tif verboseLogging {\n\t\tfmt.Printf(\"structures:\\n\")\n\t\tlog.Println(spew.Sdump(structures))\n\t}\n\n\tnumStructural := len(structures)\n\tbaseSize := numStructural / numSections\n\tremainder := numStructural % numSections\n\n\tif verboseLogging {\n\t\tfmt.Printf(\"baseSize: %d, remainder: %d\\n\", baseSize, remainder)\n\t}\n\n\tstartIndex := 0\n\n\tfor i := 0; i < numSections; i++ {\n\t\tsize := baseSize\n\t\tif i < remainder {\n\t\t\tsize++\n\t\t}\n\n\t\tendIndex := startIndex + size\n\t\tif endIndex > len(structures) {\n\t\t\tendIndex = len(structures)\n\t\t}\n\n\t\tvar section TreeSitterSection\n\t\tgroup := structures[startIndex:endIndex]\n\n\t\tfor _, s := range group {\n\t\t\tsection = append(section, s...)\n\t\t}\n\t\tsections[i] = section\n\t\tstartIndex = endIndex\n\t}\n\n\tif verboseLogging {\n\t\tfmt.Printf(\"sections:\\n\")\n\t\tlog.Println(spew.Sdump(sections))\n\n\t\tfmt.Println(\"Sections content:\")\n\t\tfor i, section := range sections {\n\t\t\tfmt.Printf(\"section %d:\\n\", i)\n\t\t\tfor j, node := range section {\n\t\t\t\tfmt.Printf(\"node %d:\\n\", j)\n\t\t\t\ttoLog := node.Content(bytes)\n\t\t\t\tif len(toLog) > 200 {\n\t\t\t\t\ttoLog = toLog[:100] + \"\\n...\\n\" + toLog[len(toLog)-100:]\n\t\t\t\t}\n\t\t\t\tfmt.Println(toLog)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn sections\n}\n\nfunc isStructuralNode(node *tree_sitter.Node) bool {\n\tnodeType := node.Type()\n\n\tif strings.Contains(nodeType, \"comment\") {\n\t\treturn false\n\t}\n\n\tif strings.HasSuffix(nodeType, \"space\") {\n\t\treturn false\n\t}\n\n\tswitch nodeType {\n\tcase \"ws\", \"newline\", \"indent\", \"dedent\":\n\t\treturn false\n\t}\n\n\tif node.IsNamed() {\n\t\treturn true\n\t}\n\n\tif node.ChildCount() > 0 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc BuildNodeIndex(tree *tree_sitter.Tree) *NodeIndex {\n\tnodesByLine := make(map[int]*tree_sitter.Node)\n\tparentsByLine := make(map[int]*tree_sitter.Node)\n\n\troot := tree.RootNode()\n\n\t// First pass - index direct nodes\n\tvar indexNodes func(node *tree_sitter.Node, depth int)\n\tindexNodes = func(node *tree_sitter.Node, depth int) {\n\t\t// if verboseLogging {\n\t\t// \tfmt.Printf(\"node: %s, depth: %d, childCount: %d\\n\", node.Type(), depth, node.ChildCount())\n\t\t// \t// spew.Dump(node.StartPoint())\n\t\t// }\n\n\t\tif node.Type() != root.Type() {\n\t\t\tline := int(node.StartPoint().Row)\n\t\t\texisting, exists := nodesByLine[line]\n\t\t\tif !exists ||\n\t\t\t\tnode.StartPoint().Column < existing.StartPoint().Column ||\n\t\t\t\t(node.StartPoint().Column == existing.StartPoint().Column && depth < getNodeDepth(existing)) {\n\t\t\t\tnodesByLine[line] = node\n\t\t\t\tparentsByLine[line] = node.Parent()\n\t\t\t}\n\t\t}\n\n\t\tfor i := 0; i < int(node.ChildCount()); i++ {\n\t\t\tchild := node.Child(i)\n\t\t\tindexNodes(child, depth+1)\n\t\t}\n\t}\n\n\tindexNodes(root, 0)\n\n\t// Second pass - fill in missing lines with parent nodes\n\tendLine := int(root.EndPoint().Row)\n\tfor line := 0; line <= endLine; line++ {\n\t\tif _, exists := nodesByLine[line]; !exists {\n\t\t\t// Find containing parent node\n\t\t\tvar findParent func(node *tree_sitter.Node) *tree_sitter.Node\n\t\t\tfindParent = func(node *tree_sitter.Node) *tree_sitter.Node {\n\t\t\t\tnodeStart := int(node.StartPoint().Row)\n\t\t\t\tnodeEnd := int(node.EndPoint().Row)\n\n\t\t\t\tif nodeStart <= line && line <= nodeEnd {\n\t\t\t\t\t// Check children first for more specific containment\n\t\t\t\t\tfor i := 0; i < int(node.ChildCount()); i++ {\n\t\t\t\t\t\tchild := node.Child(i)\n\t\t\t\t\t\tif found := findParent(child); found != nil {\n\t\t\t\t\t\t\treturn found\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn node\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tparent := findParent(root)\n\n\t\t\tif parent == nil {\n\t\t\t\tnodesByLine[line] = root\n\t\t\t} else {\n\t\t\t\tnodesByLine[line] = parent\n\t\t\t\tparentsByLine[line] = parent\n\t\t\t}\n\t\t}\n\t}\n\n\t// spew.Dump(nodesByLine)\n\treturn &NodeIndex{\n\t\tnodesByLine:   nodesByLine,\n\t\tparentsByLine: parentsByLine,\n\t}\n}\n\nfunc getNodeDepth(node *tree_sitter.Node) int {\n\tdepth := 0\n\tcurrent := node\n\tfor current.Parent() != nil {\n\t\tdepth++\n\t\tcurrent = current.Parent()\n\t}\n\treturn depth\n}\n\nfunc (s TreeSitterSection) String(sourceLines []string, bytes []byte) string {\n\tif len(s) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Find first structural node\n\tvar firstNode *tree_sitter.Node\n\tfor _, n := range s {\n\t\tif isStructuralNode(n) {\n\t\t\tfirstNode = n\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Find last structural node\n\tvar lastNode *tree_sitter.Node\n\tfor i := len(s) - 1; i >= 0; i-- {\n\t\tif isStructuralNode(s[i]) {\n\t\t\tlastNode = s[i]\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif firstNode == nil || lastNode == nil {\n\t\treturn \"\"\n\t}\n\n\tstartIdx := int(firstNode.StartPoint().Row)\n\tendIdx := int(lastNode.EndPoint().Row)\n\n\tif verboseLogging {\n\t\tfor _, node := range s {\n\t\t\tfmt.Printf(\"node.Type(): %s\\n\", node.Type())\n\t\t\tfmt.Printf(\"StartPoint().Row: %d\\n\", node.StartPoint().Row)\n\t\t\tfmt.Printf(\"EndPoint().Row: %d\\n\", node.EndPoint().Row)\n\t\t}\n\n\t\tfmt.Printf(\"section.String startIdx: %d, endIdx: %d\\n\", startIdx, endIdx)\n\t}\n\n\tparent := lastNode.Parent()\n\tparentEndNode := parent.Child(int(parent.ChildCount()) - 1)\n\tlastLine := sourceLines[endIdx]\n\tif lastLine == parentEndNode.Content(bytes) {\n\t\tendIdx--\n\t}\n\n\tresult := strings.Join(sourceLines[startIdx:endIdx+1], \"\\n\") + \"\\n\"\n\n\tif verboseLogging {\n\t\ttoLog := result\n\t\tif len(toLog) > 200 {\n\t\t\ttoLog = toLog[:100] + \"\\n...\\n\" + toLog[len(toLog)-100:]\n\t\t}\n\t\tfmt.Printf(\"section.String result: %s\\n\", toLog)\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "app/server/syntax/structured_edits_test.go",
    "content": "package syntax\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestStructuredReplacements(t *testing.T) {\n\ttests := []struct {\n\t\tonly        bool\n\t\tname        string\n\t\toriginal    string\n\t\tproposed    string\n\t\twant        string\n\t\text         string\n\t\tisInsert    bool\n\t\tlaxNewlines bool\n\t\tdesc        string\n\t}{\n\t\t{\n\t\t\tname: \"single reference in function\",\n\t\t\toriginal: `\n    func processUser(id int) error {\n        validate(id)\n        startTx()\n        updateUser(id)\n        commit()\n        return nil\n    }`,\n\t\t\tproposed: `\n    func processUser(id int) error {\n        // ... existing code ...\n        log.Info(\"processing user\")\n        return nil\n    }`,\n\t\t\twant: `\n    func processUser(id int) error {\n        validate(id)\n        startTx()\n        updateUser(id)\n        commit()\n        log.Info(\"processing user\")\n        return nil\n    }`,\n\t\t\text: \"go\",\n\t\t},\n\t\t{\n\t\t\tname: \"bad formatting\",\n\t\t\toriginal: `\n    func processUser(id int) error {\n    validate(id)\n    validateAgain(id)\n    prepForUpdate(id)\n    return update(id)\n    }`,\n\t\t\tproposed: `\n    func processUser(id int) error {\n    // ... existing code ...\n    if force {\n    log.Warn(\"will force update\")\n    }\n    return update(id)\n    }`,\n\t\t\twant: `\n    func processUser(id int) error {\n    validate(id)\n    validateAgain(id)\n    prepForUpdate(id)\n    if force {\n    log.Warn(\"will force update\")\n    }\n    return update(id)\n    }`,\n\t\t\text: \"go\",\n\t\t},\n\t\t{\n\t\t\tname:        \"multiple refs in class/nested structures\",\n\t\t\tlaxNewlines: true,\n\t\t\toriginal: `\n\t\tpackage main\n\n\t\timport \"log\"\n\n\t\tfunc init() {\n\t\t  log.Println(\"init\")\n\t\t}\n\n\t\ttype UserService struct {\n\t\t    db *DB\n\t\t    cache *Cache\n\t\t}\n\n\t\tfunc (s *UserService) Process() {\n\t\t    s.validate()\n\t\t    s.update()\n\t\t    s.notify()\n\t\t}\n\n\t\tfunc (s *UserService) Update() {\n\t\t    s.db.begin()\n\t\t    s.db.exec()\n\t\t    s.db.commit()\n\t\t}\n\n\t\tfunc (s *UserService) Record() {\n\t\t  log.Println(\"record\")\n\t\t}\n\t\t`,\n\t\t\tproposed: `\n\t\t// ... existing code ...\n\n\t\ttype UserService struct {\n\t\t    // ... existing code ...\n\t\t    metrics *Metrics\n\t\t}\n\n\t\tfunc (s *UserService) Process() {\n\t\t    // ... existing code ...\n\t\t    s.metrics.Record()\n\t\t    // ... existing code ...\n\t\t}\n\n\t\tfunc (s *UserService) Update() {\n\t\t    // ... existing code ...\n\t\t}\n\n\t\t// ... existing code ...\n\t\t`,\n\t\t\twant: `\n\t\tpackage main\n\n\t\timport \"log\"\n\n\t\tfunc init() {\n\t\t  log.Println(\"init\")\n\t\t}\n\t\ttype UserService struct {\n\t\t    db *DB\n\t\t    cache *Cache\n\t\t    metrics *Metrics\n\t\t}\n\n\t\tfunc (s *UserService) Process() {\n\t\t    s.validate()\n\t\t    s.update()\n\t\t    s.metrics.Record()\n\t\t    s.notify()\n\t\t}\n\n\t\tfunc (s *UserService) Update() {\n\t\t    s.db.begin()\n\t\t    s.db.exec()\n\t\t    s.db.commit()\n\t\t}\n\n\t\tfunc (s *UserService) Record() {\n\t\t  log.Println(\"record\")\n\t\t}\n\t\t`,\n\t\t\text: \"go\",\n\t\t},\n\t\t{\n\t\t\tname: \"code removal comment\",\n\t\t\toriginal: `\n    func processUser(id int) error {\n        validate(id)\n        startTx()\n        logTransaction()\n        validateAgain(id)\n        updateUser(id)\n        commit()\n        return nil\n    }`,\n\t\t\tproposed: `\n    func processUser(id int) error {\n        validate(id)\n        // Plandex: removed code\n        validateAgain(id)\n        updateUser(id)\n        commit()\n        return nil\n    }`,\n\t\t\twant: `\n    func processUser(id int) error {\n        validate(id)\n        validateAgain(id)\n        updateUser(id)\n        commit()\n        return nil\n    }`,\n\t\t\text: \"go\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple code removal comments\",\n\t\t\toriginal: `\n    func processUser(id int) error {\n        validate(id)\n        startTx()\n        logTransaction()\n        validateAgain(id)\n        logValidation()\n        updateUser(id)\n        commit()\n        return nil\n    }`,\n\t\t\tproposed: `\n    func processUser(id int) error {\n        validate(id)\n        // Plandex: removed code\n        validateAgain(id)\n        // Plandex: removed code\n        updateUser(id)\n        commit()\n        return nil\n    }`,\n\t\t\twant: `\n    func processUser(id int) error {\n        validate(id)\n        validateAgain(id)\n        updateUser(id)\n        commit()\n        return nil\n    }`,\n\t\t\text: \"go\",\n\t\t},\n\t\t{\n\t\t\tname: \"json update with reference comments\",\n\t\t\toriginal: `{\n        \"name\": \"test-app\",\n        \"version\": \"1.0.0\",\n        \"dependencies\": {\n            \"express\": \"^4.17.1\",\n            \"body-parser\": \"^1.19.0\",\n            \"cors\": \"^2.8.5\"\n        },\n        \"scripts\": {\n            \"start\": \"node index.js\",\n            \"test\": \"jest\",\n            \"build\": \"webpack\"\n        }\n    }`,\n\t\t\tproposed: `{\n        // ... existing code ...\n        \"dependencies\": {\n            \"express\": \"^4.17.1\",\n            \"body-parser\": \"^1.19.0\",\n            \"cors\": \"^2.8.5\",\n            \"dotenv\": \"^16.0.0\",\n            \"mongoose\": \"^6.0.0\"\n        },\n        // ... existing code ...\n    }`,\n\t\t\twant: `{\n        \"name\": \"test-app\",\n        \"version\": \"1.0.0\",\n        \"dependencies\": {\n            \"express\": \"^4.17.1\",\n            \"body-parser\": \"^1.19.0\",\n            \"cors\": \"^2.8.5\",\n            \"dotenv\": \"^16.0.0\",\n            \"mongoose\": \"^6.0.0\"\n        },\n        \"scripts\": {\n            \"start\": \"node index.js\",\n            \"test\": \"jest\",\n            \"build\": \"webpack\"\n        }\n    }`,\n\t\t\text: \"json\",\n\t\t},\n\t\t{\n\t\t\tname: \"method replacement with context\",\n\t\t\toriginal: `\n    class UserService {\n        constructor() {\n            this.cache = new Cache()\n        }\n\n        async getUser(id) {\n            const user = await db.find(id)\n            return user\n        }\n\n        async updateUser(id, data) {\n            await db.update(id, data)\n            this.cache.clear()\n        }\n\n        async deleteUser(id) {\n            await db.delete(id)\n            this.cache.clear()\n        }\n    }`,\n\t\t\tproposed: `\n    class UserService {\n        // ... existing code ...\n\n        async getUser(id) {\n            const cached = await this.cache.get(id)\n            if (cached) return cached\n            \n            const user = await db.find(id)\n            await this.cache.set(id, user)\n            return user\n        }\n\n        // ... existing code ...\n    }`,\n\t\t\twant: `\n    class UserService {\n        constructor() {\n            this.cache = new Cache()\n        }\n\n        async getUser(id) {\n            const cached = await this.cache.get(id)\n            if (cached) return cached\n            \n            const user = await db.find(id)\n            await this.cache.set(id, user)\n            return user\n        }\n\n        async updateUser(id, data) {\n            await db.update(id, data)\n            this.cache.clear()\n        }\n\n        async deleteUser(id) {\n            await db.delete(id)\n            this.cache.clear()\n        }\n    }`,\n\t\t\text: \"js\",\n\t\t},\n\t\t{\n\t\t\tname: \"nested class methods update\",\n\t\t\toriginal: `\n    namespace Database {\n        class Transaction {\n            begin() {\n                log.Info(\"beginning transaction\")\n                startTx()\n            }\n\n            commit() {\n                log.Info(\"committing transaction\")\n                commitTx()\n            }\n\n            rollback() {\n                log.Info(\"rolling back transaction\")\n                rollbackTx()\n            }\n        }\n    }`,\n\t\t\tproposed: `\n    namespace Database {\n        class Transaction {\n            begin() {\n                // ... existing code ...\n            }\n\n            commit() {\n                log.Info(\"committing transaction\")\n                validateTx()\n                commitTx()\n                notifyCommit()\n            }\n\n            // ... existing code ...\n        }\n    }`,\n\t\t\twant: `\n    namespace Database {\n        class Transaction {\n            begin() {\n                log.Info(\"beginning transaction\")\n                startTx()\n            }\n\n            commit() {\n                log.Info(\"committing transaction\")\n                validateTx()\n                commitTx()\n                notifyCommit()\n            }\n\n            rollback() {\n                log.Info(\"rolling back transaction\")\n                rollbackTx()\n            }\n        }\n    }`,\n\t\t\text: \"ts\",\n\t\t},\n\t\t{\n\t\t\tname: \"update with trailing commas\",\n\t\t\toriginal: `\n    const handlers = {\n        onStart: () => {\n            console.log(\"starting\")\n        },\n        onProcess: () => {\n            console.log(\"processing\")\n        },\n        onFinish: () => {\n            console.log(\"finishing\")\n        },\n    }`,\n\t\t\tproposed: `\n    const handlers = {\n        // ... existing code ...\n        onProcess: () => {\n            console.log(\"processing\")\n            emitEvent(\"process\"),\n        },\n        // ... existing code ...\n    }`,\n\t\t\twant: `\n    const handlers = {\n        onStart: () => {\n            console.log(\"starting\")\n        },\n        onProcess: () => {\n            console.log(\"processing\")\n            emitEvent(\"process\"),\n        },\n        onFinish: () => {\n            console.log(\"finishing\")\n        },\n    }`,\n\t\t\text: \"js\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple structural updates\",\n\t\t\toriginal: `\n    class Logger {\n        info(msg) {\n            console.log(\"[INFO]\", msg)\n        }\n\n        warn(msg) {\n            console.log(\"[WARN]\", msg)\n        }\n\n        error(msg) {\n            console.log(\"[ERROR]\", msg)\n        }\n\n        debug(msg) {\n            console.log(\"[DEBUG]\", msg)\n        }\n    }`,\n\t\t\tproposed: `\n    class Logger {\n        constructor(prefix) {\n            this.prefix = prefix\n        }\n\n        info(msg) {\n            console.log(this.prefix, \"[INFO]\", msg)\n        }\n\n        // ... existing code ...\n\n        error(msg) {\n            console.error(this.prefix, \"[ERROR]\", msg)\n            notify(\"error\", msg)\n        }\n\n        // ... existing code ...\n\n        fatal(msg) {\n            console.error(this.prefix, \"[FATAL]\", msg)\n            process.exit(1)\n        }\n    }`,\n\t\t\twant: `\n    class Logger {\n        constructor(prefix) {\n            this.prefix = prefix\n        }\n\n        info(msg) {\n            console.log(this.prefix, \"[INFO]\", msg)\n        }\n\n        warn(msg) {\n            console.log(\"[WARN]\", msg)\n        }\n\n        error(msg) {\n            console.error(this.prefix, \"[ERROR]\", msg)\n            notify(\"error\", msg)\n        }\n\n        debug(msg) {\n            console.log(\"[DEBUG]\", msg)\n        }\n\n        fatal(msg) {\n            console.error(this.prefix, \"[FATAL]\", msg)\n            process.exit(1)\n        }\n    }`,\n\t\t\text: \"js\",\n\t\t},\n\t\t{\n\t\t\tname: \"json multi-level update\",\n\t\t\toriginal: `\n{\n  \"name\": \"vscode-plandex\",\n  \"displayName\": \"Plandex\",\n  \"description\": \"VSCode extension for Plandex integration\",\n  \"version\": \"0.1.0\",\n  \"engines\": {\n    \"vscode\": \"^1.80.0\"\n  },\n  \"categories\": [\n    \"Other\"\n  ],\n  \"activationEvents\": [\n    \"onLanguage:plandex\"\n  ],\n  \"main\": \"./dist/extension.js\",\n  \"contributes\": {\n    \"languages\": [{\n      \"id\": \"plandex\",\n      \"aliases\": [\"Plandex\", \"plandex\"],\n      \"extensions\": [\".pd\"]\n    }],\n    \"commands\": [\n      {\n        \"command\": \"plandex.tellPlandex\",\n        \"title\": \"Tell Plandex\"\n      }\n    ],\n    \"keybindings\": [{\n      \"command\": \"plandex.showFilePicker\",\n      \"key\": \"@\",\n      \"when\": \"editorTextFocus && editorLangId == plandex\"\n    }]\n  },\n  \"scripts\": {\n    \"vscode:prepublish\": \"npm run package\",\n    \"compile\": \"webpack\",\n    \"watch\": \"webpack --watch\",\n    \"package\": \"webpack --mode production --devtool hidden-source-map\",\n    \"compile-tests\": \"tsc -p . --outDir out\",\n    \"watch-tests\": \"tsc -p . -w --outDir out\",\n    \"test\": \"node ./out/test/runTest.js\"\n  },\n  \"devDependencies\": {\n    \"@types/vscode\": \"^1.80.0\",\n    \"@types/glob\": \"^8.1.0\",\n    \"@types/mocha\": \"^10.0.1\",\n    \"@types/node\": \"20.2.5\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.59.8\",\n    \"@typescript-eslint/parser\": \"^5.59.8\",\n    \"eslint\": \"^8.41.0\",\n    \"glob\": \"^8.1.0\",\n    \"mocha\": \"^10.2.0\",\n    \"ts-loader\": \"^9.4.3\",\n    \"typescript\": \"^5.1.3\",\n    \"webpack\": \"^5.85.0\",\n    \"webpack-cli\": \"^5.1.1\",\n    \"@vscode/test-electron\": \"^2.3.2\"\n  }\n}\n`,\n\t\t\tproposed: `\n{\n  // ... existing code ...  \n  \"contributes\": {\n    \"languages\": [{\n      \"id\": \"plandex\",\n      \"aliases\": [\"Plandex\", \"plandex\"],\n      \"extensions\": [\".pd\"],\n      \"configuration\": \"./language-configuration.json\",\n      \"icon\": {\n          \"light\": \"./icons/plandex-light.png\",\n          \"dark\": \"./icons/plandex-dark.png\"\n      }\n    }],\n    \"grammars\": [{\n      \"language\": \"plandex\",\n      \"scopeName\": \"text.plandex\",\n      \"path\": \"./syntaxes/plandex.tmLanguage.json\",\n      \"embeddedLanguages\": {\n          \"meta.embedded.block.yaml\": \"yaml\",\n          \"text.html.markdown\": \"markdown\"\n      }\n    }],\n    // ... existing code ...\n  },\n  // ... existing code ...\n}\n`,\n\t\t\twant: `\n{\n  \"name\": \"vscode-plandex\",\n  \"displayName\": \"Plandex\",\n  \"description\": \"VSCode extension for Plandex integration\",\n  \"version\": \"0.1.0\",\n  \"engines\": {\n    \"vscode\": \"^1.80.0\"\n  },\n  \"categories\": [\n    \"Other\"\n  ],\n  \"activationEvents\": [\n    \"onLanguage:plandex\"\n  ],\n  \"main\": \"./dist/extension.js\",\n  \"contributes\": {\n    \"languages\": [{\n      \"id\": \"plandex\",\n      \"aliases\": [\"Plandex\", \"plandex\"],\n      \"extensions\": [\".pd\"],\n      \"configuration\": \"./language-configuration.json\",\n      \"icon\": {\n          \"light\": \"./icons/plandex-light.png\",\n          \"dark\": \"./icons/plandex-dark.png\"\n      }\n    }],\n    \"grammars\": [{\n      \"language\": \"plandex\",\n      \"scopeName\": \"text.plandex\",\n      \"path\": \"./syntaxes/plandex.tmLanguage.json\",\n      \"embeddedLanguages\": {\n          \"meta.embedded.block.yaml\": \"yaml\",\n          \"text.html.markdown\": \"markdown\"\n      }\n    }],\n    \"commands\": [\n      {\n        \"command\": \"plandex.tellPlandex\",\n        \"title\": \"Tell Plandex\"\n      }\n    ],\n    \"keybindings\": [{\n      \"command\": \"plandex.showFilePicker\",\n      \"key\": \"@\",\n      \"when\": \"editorTextFocus && editorLangId == plandex\"\n    }]\n  },\n  \"scripts\": {\n    \"vscode:prepublish\": \"npm run package\",\n    \"compile\": \"webpack\",\n    \"watch\": \"webpack --watch\",\n    \"package\": \"webpack --mode production --devtool hidden-source-map\",\n    \"compile-tests\": \"tsc -p . --outDir out\",\n    \"watch-tests\": \"tsc -p . -w --outDir out\",\n    \"test\": \"node ./out/test/runTest.js\"\n  },\n  \"devDependencies\": {\n    \"@types/vscode\": \"^1.80.0\",\n    \"@types/glob\": \"^8.1.0\",\n    \"@types/mocha\": \"^10.0.1\",\n    \"@types/node\": \"20.2.5\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.59.8\",\n    \"@typescript-eslint/parser\": \"^5.59.8\",\n    \"eslint\": \"^8.41.0\",\n    \"glob\": \"^8.1.0\",\n    \"mocha\": \"^10.2.0\",\n    \"ts-loader\": \"^9.4.3\",\n    \"typescript\": \"^5.1.3\",\n    \"webpack\": \"^5.85.0\",\n    \"webpack-cli\": \"^5.1.1\",\n    \"@vscode/test-electron\": \"^2.3.2\"\n  }\n}\n`,\n\t\t\text: \"json\",\n\t\t},\n\t\t{\n\t\t\tname: \"json multi-level update 2\",\n\t\t\toriginal: `\n{\n  \"name\": \"vscode-plandex\",\n  \"displayName\": \"Plandex\",\n  \"description\": \"VSCode extension for Plandex integration\",\n  \"version\": \"0.1.0\",\n  \"publisher\": \"plandex\",\n  \"engines\": {\n    \"vscode\": \"^1.80.0\"\n  },\n  \"categories\": [\n    \"Other\"\n  ],\n  \"activationEvents\": [\n    \"onLanguage:pdx\"\n  ],\n  \"main\": \"./dist/extension.js\",\n  \"contributes\": {\n    \"languages\": [\n      {\n        \"id\": \"pdx\",\n        \"aliases\": [\"Plandex\", \"pdx\"],\n        \"extensions\": [\".pdx\"],\n        \"configuration\": \"./language-configuration.json\"\n      }\n    ],\n    \"grammars\": [\n      {\n        \"language\": \"pdx\",\n        \"scopeName\": \"source.mdx\",\n        \"path\": \"./syntaxes/mdx.tmLanguage.json\",\n        \"embeddedLanguages\": {\n          \"meta.embedded.block.yaml\": \"yaml\",\n          \"meta.embedded.block.markdown\": \"markdown\"\n        }\n      }\n    ],\n    \"commands\": [\n      {\n        \"command\": \"plandex.tellPlandex\",\n        \"title\": \"Tell Plandex\",\n        \"icon\": \"$(play)\"\n      }\n    ],\n    \"keybindings\": [\n      {\n        \"command\": \"plandex.showFilePicker\",\n        \"key\": \"@\",\n        \"when\": \"editorTextFocus && editorLangId == pdx\"\n      }\n    ],\n    \"menus\": {\n      \"editor/title\": [\n        {\n          \"command\": \"plandex.tellPlandex\",\n          \"group\": \"navigation\",\n          \"when\": \"editorLangId == pdx\"\n        }\n      ]\n    }\n  },\n  \"scripts\": {\n    \"vscode:prepublish\": \"npm run package\",\n    \"compile\": \"webpack\",\n    \"watch\": \"webpack --watch\",\n    \"package\": \"webpack --mode production --devtool hidden-source-map\",\n    \"compile-tests\": \"tsc -p . --outDir out\",\n    \"watch-tests\": \"tsc -p . -w --outDir out\",\n    \"pretest\": \"npm run compile-tests && npm run compile && npm run lint\",\n    \"lint\": \"eslint src --ext ts\",\n    \"test\": \"node ./out/test/runTest.js\"\n  },\n  \"devDependencies\": {\n    \"@types/glob\": \"^8.1.0\",\n    \"@types/mocha\": \"^10.0.1\",\n    \"@types/node\": \"^20.2.5\",\n    \"@types/vscode\": \"^1.80.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.59.8\",\n    \"@typescript-eslint/parser\": \"^5.59.8\",\n    \"eslint\": \"^8.41.0\",\n    \"glob\": \"^8.1.0\",\n    \"mocha\": \"^10.2.0\",\n    \"ts-loader\": \"^9.4.3\",\n    \"typescript\": \"^5.1.3\",\n    \"webpack\": \"^5.85.0\",\n    \"webpack-cli\": \"^5.1.1\"\n  },\n  \"dependencies\": {\n    \"yaml\": \"^2.3.1\"\n  }\n}\n`,\n\t\t\tproposed: `\n{\n  // ... existing code ...\n\n  \"contributes\": {\n    // ... existing code ...\n\n    \"commands\": [\n      {\n        \"command\": \"plandex.tellPlandex\",\n        \"title\": \"Tell Plandex\",\n        \"icon\": {\n          \"light\": \"resources/light/play.svg\",\n          \"dark\": \"resources/dark/play.svg\"\n        }\n      }\n    ],\n\n    // ... existing code ...\n  },\n\n  // ... existing code ...\n}\n`,\n\t\t\twant: `\n{\n  \"name\": \"vscode-plandex\",\n  \"displayName\": \"Plandex\",\n  \"description\": \"VSCode extension for Plandex integration\",\n  \"version\": \"0.1.0\",\n  \"publisher\": \"plandex\",\n  \"engines\": {\n    \"vscode\": \"^1.80.0\"\n  },\n  \"categories\": [\n    \"Other\"\n  ],\n  \"activationEvents\": [\n    \"onLanguage:pdx\"\n  ],\n  \"main\": \"./dist/extension.js\",\n  \"contributes\": {\n    \"languages\": [\n      {\n        \"id\": \"pdx\",\n        \"aliases\": [\"Plandex\", \"pdx\"],\n        \"extensions\": [\".pdx\"],\n        \"configuration\": \"./language-configuration.json\"\n      }\n    ],\n    \"grammars\": [\n      {\n        \"language\": \"pdx\",\n        \"scopeName\": \"source.mdx\",\n        \"path\": \"./syntaxes/mdx.tmLanguage.json\",\n        \"embeddedLanguages\": {\n          \"meta.embedded.block.yaml\": \"yaml\",\n          \"meta.embedded.block.markdown\": \"markdown\"\n        }\n      }\n    ],\n    \"commands\": [\n      {\n        \"command\": \"plandex.tellPlandex\",\n        \"title\": \"Tell Plandex\",\n        \"icon\": {\n          \"light\": \"resources/light/play.svg\",\n          \"dark\": \"resources/dark/play.svg\"\n        }\n      }\n    ],\n    \"keybindings\": [\n      {\n        \"command\": \"plandex.showFilePicker\",\n        \"key\": \"@\",\n        \"when\": \"editorTextFocus && editorLangId == pdx\"\n      }\n    ],\n    \"menus\": {\n      \"editor/title\": [\n        {\n          \"command\": \"plandex.tellPlandex\",\n          \"group\": \"navigation\",\n          \"when\": \"editorLangId == pdx\"\n        }\n      ]\n    }\n  },\n  \"scripts\": {\n    \"vscode:prepublish\": \"npm run package\",\n    \"compile\": \"webpack\",\n    \"watch\": \"webpack --watch\",\n    \"package\": \"webpack --mode production --devtool hidden-source-map\",\n    \"compile-tests\": \"tsc -p . --outDir out\",\n    \"watch-tests\": \"tsc -p . -w --outDir out\",\n    \"pretest\": \"npm run compile-tests && npm run compile && npm run lint\",\n    \"lint\": \"eslint src --ext ts\",\n    \"test\": \"node ./out/test/runTest.js\"\n  },\n  \"devDependencies\": {\n    \"@types/glob\": \"^8.1.0\",\n    \"@types/mocha\": \"^10.0.1\",\n    \"@types/node\": \"^20.2.5\",\n    \"@types/vscode\": \"^1.80.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.59.8\",\n    \"@typescript-eslint/parser\": \"^5.59.8\",\n    \"eslint\": \"^8.41.0\",\n    \"glob\": \"^8.1.0\",\n    \"mocha\": \"^10.2.0\",\n    \"ts-loader\": \"^9.4.3\",\n    \"typescript\": \"^5.1.3\",\n    \"webpack\": \"^5.85.0\",\n    \"webpack-cli\": \"^5.1.1\"\n  },\n  \"dependencies\": {\n    \"yaml\": \"^2.3.1\"\n  }\n}\n`,\n\t\t\text: \"json\",\n\t\t},\n\t\t{\n\t\t\tname:        \"scala complex structures\",\n\t\t\tlaxNewlines: true,\n\t\t\toriginal: `\n\t\tpackage domain.service\n\n\t\timport java.time.format.DateTimeFormatter\n\n\t\tclass MetricsService(\n\t\t    client: Client,\n\t\t    service: Service,\n\t\t    automation: Automation\n\t\t)(\n\t\t    implicit context: Context\n\t\t) extends LazyLogging\n\t\t  with BaseImplicits {\n\n\t\t    def metrics(\n\t\t        ids: Seq[Id],\n\t\t        channels: Option[Seq[Channel]>,\n\t\t    ): Future[Metrics] = {\n\n\t\t      getMetrics(\n\t\t        ids,\n\t\t        channels,\n\t\t        Endpoint.Metrics\n\t\t      )\n\t\t    }\n\n\t\t    def metrics2(\n\t\t        ids: Seq[Id],\n\t\t        channels: Option[Seq[Channel]>,\n\t\t    ): Future[Metrics] = {\n\n\t\t      getMetrics2(\n\t\t        ids,\n\t\t        channels,\n\t\t        Endpoint.Metrics\n\t\t      )\n\t\t    }\n\t\t  }\n\t\t`,\n\t\t\tproposed: `\n\t\tpackage domain.service\n\n\t\t// ... existing code ...\n\n\t\tclass MetricsService(\n\t\t  // ... existing code ...\n\t\t)(\n\t\t    implicit context: Context\n\t\t) extends LazyLogging\n\t\t  with BaseImplicits {\n\n\t\t    // ... existing code ...\n\n\t\t    def update(authContext: AuthContext, id: String): Future[Done] = {\n\t\t      fallbacks.stub\n\t\t        .update(\n\t\t          updateRequest(\n\t\t            authContext = Some(authContext),\n\t\t            id = id\n\t\t          )\n\t\t        )\n\t\t        .map(_ => Done)\n\t\t    }\n\n\t\t    // ... existing code ...\n\t\t  }\n\t\t`,\n\t\t\twant: `\n\t\tpackage domain.service\n\n\t\timport java.time.format.DateTimeFormatter\n\n\t\tclass MetricsService(\n\t\t    client: Client,\n\t\t    service: Service,\n\t\t    automation: Automation\n\t\t)(\n\t\t    implicit context: Context\n\t\t) extends LazyLogging\n\t\t  with BaseImplicits {\n\n\t\t    def metrics(\n\t\t        ids: Seq[Id],\n\t\t        channels: Option[Seq[Channel]>,\n\t\t    ): Future[Metrics] = {\n\n\t\t      getMetrics(\n\t\t        ids,\n\t\t        channels,\n\t\t        Endpoint.Metrics\n\t\t      )\n\t\t    }\n\n\t\t    def update(authContext: AuthContext, id: String): Future[Done] = {\n\t\t      fallbacks.stub\n\t\t        .update(\n\t\t          updateRequest(\n\t\t            authContext = Some(authContext),\n\t\t            id = id\n\t\t          )\n\t\t        )\n\t\t        .map(_ => Done)\n\t\t    }\n\n\t\t    def metrics2(\n\t\t        ids: Seq[Id],\n\t\t        channels: Option[Seq[Channel]>,\n\t\t    ): Future[Metrics] = {\n\n\t\t      getMetrics2(\n\t\t        ids,\n\t\t        channels,\n\t\t        Endpoint.Metrics\n\t\t      )\n\t\t    }\n\t\t  }\n\t\t`,\n\t\t\text: \"scala\",\n\t\t},\n\t\t{\n\t\t\tname:        \"top-level ambiguous\",\n\t\t\tlaxNewlines: true,\n\t\t\toriginal: `\n\t\tfunction someFunction() {\n\t\t  console.log(\"someFunction\")\n\t\t  const res = await fetch(\"https://example.com\")\n\t\t  processResponse(res)\n\t\t  return res\n\t\t}\n\n\t\tfunction processResponse(res) {\n\t\t  console.log(\"processing response\")\n\t\t  callSomeOtherFunction(res)\n\t\t  return res\n\t\t}\n\n\t\tfunction yetAnotherFunction() {\n\t\t  console.log(\"yetAnotherFunction\")\n\t\t}\n\n\t\tfunction callSomething() {\n\t\t  console.log(\"callSomething\")\n\t\t  await logSomething()\n\t\t  return \"something\"\n\t\t}\n\t\t`,\n\t\t\tproposed: `\n\t\t// ... existing code ...\n\n\t\tfunction newFunction() {\n\t\t  console.log(\"newFunction\")\n\t\t  const res = await callSomething()\n\t\t  return res\n\t\t}\n\n\t\t// ... existing code ...\n\t\t`,\n\t\t\twant: `\n\t\tfunction someFunction() {\n\t\t  console.log(\"someFunction\")\n\t\t  const res = await fetch(\"https://example.com\")\n\t\t  processResponse(res)\n\t\t  return res\n\t\t}\n\n\t\tfunction processResponse(res) {\n\t\t  console.log(\"processing response\")\n\t\t  callSomeOtherFunction(res)\n\t\t  return res\n\t\t}\n\n\t\tfunction newFunction() {\n\t\t  console.log(\"newFunction\")\n\t\t  const res = await callSomething()\n\t\t  return res\n\t\t}\n\n\t\tfunction yetAnotherFunction() {\n\t\t  console.log(\"yetAnotherFunction\")\n\t\t}\n\n\t\tfunction callSomething() {\n\t\t  console.log(\"callSomething\")\n\t\t  await logSomething()\n\t\t  return \"something\"\n\t\t}\n\t\t`,\n\t\t\text: \"js\",\n\t\t},\n\t\t{\n\t\t\tname: \"top-level with anchors\",\n\t\t\toriginal: `\n    function someFunction() {\n      console.log(\"someFunction\")\n      const res = await fetch(\"https://example.com\")\n      processResponse(res)\n      return res\n    }\n\n    function processResponse(res) {\n      console.log(\"processing response\")\n      callSomeOtherFunction(res)\n      return res\n    }\n\n    function yetAnotherFunction() {\n      console.log(\"yetAnotherFunction\")\n      doStuff()\n    }\n\n    function callSomething() {\n      console.log(\"callSomething\")\n      await logSomething()\n      return \"something\"\n    }\n    `,\n\t\t\tproposed: `\n    // ... existing code ...\n\n    function processResponse(res) {\n      // ... existing code ...\n    }\n\n    function newFunction() {\n      console.log(\"newFunction\")\n      const res = await callSomething()\n      return res\n    }\n\n    function yetAnotherFunction() {\n      // ... existing code ...\n    }\n\n    // ... existing code ...\n    `,\n\t\t\twant: `\n    function someFunction() {\n      console.log(\"someFunction\")\n      const res = await fetch(\"https://example.com\")\n      processResponse(res)\n      return res\n    }\n\n    function processResponse(res) {\n      console.log(\"processing response\")\n      callSomeOtherFunction(res)\n      return res\n    }\n\n    function newFunction() {\n      console.log(\"newFunction\")\n      const res = await callSomething()\n      return res\n    }\n\n    function yetAnotherFunction() {\n      console.log(\"yetAnotherFunction\")\n      doStuff()\n    }\n\n    function callSomething() {\n      console.log(\"callSomething\")\n      await logSomething()\n      return \"something\"\n    }\n    `,\n\t\t\text: \"js\",\n\t\t},\n\t\t{\n\t\t\tname: \"clean up extraneous newlines\",\n\t\t\toriginal: `\n      func func1 () {\n        fmt.Println(\"func1\")\n      }\n\n      func func2 () {\n        fmt.Println(\"log something\")\n        fmt.Println(\"func2\")\n      }\n\n      func func3 () {\n        fmt.Println(\"func3\")\n\n        fmt.Println(\"func3\")\n      }\n      `,\n\t\t\tproposed: `\n      // ... existing code ...\n\n      func func2 () {\n        // ... existing code ...\n      }\n\n      func newFunc () {\n        console.log(\"newFunc\")\n      }\n\n      func func3 () {\n\n      // ... existing code ...\n      `,\n\t\t\twant: `\n      func func1 () {\n        fmt.Println(\"func1\")\n      }\n\n      func func2 () {\n        fmt.Println(\"log something\")\n        fmt.Println(\"func2\")\n      }\n\n      func newFunc () {\n        console.log(\"newFunc\")\n      }\n\n      func func3 () {\n        fmt.Println(\"func3\")\n\n        fmt.Println(\"func3\")\n      }\n      `,\n\t\t\text: \"go\",\n\t\t},\n\t\t{\n\t\t\tname: \"insert between non-adjacent anchors\",\n\t\t\toriginal: `func main() {\n  fmt.Println(\"start\")\n  doSomething()\n  fmt.Println(\"middle\")\n  someOtherThing()\n  fmt.Println(\"end\")\n}`,\n\t\t\tproposed: `func main() {\n  fmt.Println(\"start\")\n  doSomething()\n  log.Info(\"new log\")\n  fmt.Println(\"end\")\n}`,\n\t\t\twant: `func main() {\n  fmt.Println(\"start\")\n  doSomething()\n  log.Info(\"new log\")\n  fmt.Println(\"middle\")\n  someOtherThing()\n  fmt.Println(\"end\")\n}`,\n\t\t\tisInsert: true,\n\t\t},\n\t\t{\n\t\t\tname: \"insert with reference and non-adjacent anchors\",\n\t\t\toriginal: `func processRequest(req *Request) error {\n  validateRequest(req)\n  startTransaction()\n\n  err := updateData(req)\n  if err != nil {\n      return err\n  }\n\n  notifyUpdate()\n  commitTransaction()\n  return nil\n}`,\n\t\t\tproposed: `func processRequest(req *Request) error {\n  // ... existing code ...\n  startTransaction()\n\n  log.Info(\"processing request\", req.ID)\n\n  commitTransaction()\n  return nil\n}`,\n\t\t\twant: `func processRequest(req *Request) error {\n  validateRequest(req)\n  startTransaction()\n\n  log.Info(\"processing request\", req.ID)\n\n  err := updateData(req)\n  if err != nil {\n      return err\n  }\n\n  notifyUpdate()\n  commitTransaction()\n  return nil\n}`,\n\t\t\tisInsert: true,\n\t\t},\n\n\t\t{\n\t\t\tname: \"replacement with removal outside of single line range\",\n\t\t\tdesc: `\n\t\t      Type: replace\n\t\t      Replace: line 11\n\t\t      `,\n\t\t\toriginal: `func processRequest(req *Request) error {\n\t\t  validateRequest(req)\n\t\t  someOtherThing()\n\t\t  startTransaction()\n\n\t\t  err := updateData(req)\n\t\t  if err != nil {\n\t\t      return err\n\t\t  }\n\n\t\t  notifyUpdate()\n\t\t  commitTransaction()\n\t\t  return nil\n\t\t}`,\n\t\t\tproposed: `func processRequest(req *Request) error {\n\t\t  startTransaction()\n\n\t\t  err := updateData(req)\n\t\t  if err != nil {\n\t\t      return err\n\t\t  }\n\n\t\t  commitTransaction()\n\t\t  log.Info(\"processed request\", req.ID)\n\t\t  return nil\n\t\t}`,\n\t\t\twant: `func processRequest(req *Request) error {\n\t\t  validateRequest(req)\n\t\t  someOtherThing()\n\t\t  startTransaction()\n\n\t\t  err := updateData(req)\n\t\t  if err != nil {\n\t\t      return err\n\t\t  }\n\n\t\t  commitTransaction()\n\t\t  log.Info(\"processed request\", req.ID)\n\t\t  return nil\n\t\t}`,\n\t\t},\n\n\t\t{\n\t\t\tname: \"replacement with removal inside of multi line range\",\n\t\t\tdesc: `\n\t\t      Type: replace\n\t\t      Replace: lines 2-3, line 11\n\t\t      `,\n\t\t\toriginal: `func processRequest(req *Request) error {\n\t\t  validateRequest(req)\n\t\t  someOtherThing()\n\t\t  startTransaction()\n\n\t\t  err := updateData(req)\n\t\t  if err != nil {\n\t\t      return err\n\t\t  }\n\n\t\t  notifyUpdate()\n\t\t  commitTransaction()\n\t\t  return nil\n\t\t}`,\n\t\t\tproposed: `func processRequest(req *Request) error {\n\t\t  startTransaction()\n\n\t\t  err := updateData(req)\n\t\t  if err != nil {\n\t\t      return err\n\t\t  }\n\n\t\t  commitTransaction()\n\t\t  log.Info(\"processed request\", req.ID)\n\t\t  return nil\n\t\t}`,\n\t\t\twant: `func processRequest(req *Request) error {\n\t\t  startTransaction()\n\n\t\t  err := updateData(req)\n\t\t  if err != nil {\n\t\t      return err\n\t\t  }\n\n\t\t  commitTransaction()\n\t\t  log.Info(\"processed request\", req.ID)\n\t\t  return nil\n\t\t}`,\n\t\t},\n\n\t\t{\n\t\t\tname: \"add to end with full file included\",\n\t\t\tdesc: `\n      Type: add\n      Summary: Add Tailwind CSS typography plugin\n      `,\n\t\t\tisInsert: true,\n\t\t\toriginal: `# Install dependencies for markdown processing                            \necho \"Installing dependencies for markdown processing...\"                 \nnpm install next-mdx-remote gray-matter --save                            \necho \"Dependencies installed successfully!\"`,\n\t\t\tproposed: `# Install dependencies for markdown processing                            \necho \"Installing dependencies for markdown processing...\"                 \nnpm install next-mdx-remote gray-matter --save                            \necho \"Dependencies installed successfully!\"\n\n# Install Tailwind CSS typography plugin                                  \necho \"Installing Tailwind CSS typography plugin...\"                       \nnpm install @tailwindcss/typography --save-dev                            \necho \"Tailwind CSS typography plugin installed successfully!\"`,\n\n\t\t\twant: `# Install dependencies for markdown processing                            \necho \"Installing dependencies for markdown processing...\"                 \nnpm install next-mdx-remote gray-matter --save                            \necho \"Dependencies installed successfully!\"\n\n# Install Tailwind CSS typography plugin                                  \necho \"Installing Tailwind CSS typography plugin...\"                       \nnpm install @tailwindcss/typography --save-dev                            \necho \"Tailwind CSS typography plugin installed successfully!\"`,\n\t\t},\n\n\t\t{\n\t\t\tname: \"add to beginning with full file included\",\n\t\t\tdesc: `\n      Type: add\n      Summary: Add Tailwind CSS typography plugin\n      `,\n\t\t\tisInsert: true,\n\t\t\toriginal: `# Install dependencies for markdown processing                            \necho \"Installing dependencies for markdown processing...\"                 \nnpm install next-mdx-remote gray-matter --save                            \necho \"Dependencies installed successfully!\"`,\n\t\t\tproposed: `# Install Tailwind CSS typography plugin                                  \necho \"Installing Tailwind CSS typography plugin...\"                       \nnpm install @tailwindcss/typography --save-dev                            \necho \"Tailwind CSS typography plugin installed successfully!\"\n      \n# Install dependencies for markdown processing                            \necho \"Installing dependencies for markdown processing...\"                 \nnpm install next-mdx-remote gray-matter --save                            \necho \"Dependencies installed successfully!\"`,\n\n\t\t\twant: `# Install Tailwind CSS typography plugin                                  \necho \"Installing Tailwind CSS typography plugin...\"                       \nnpm install @tailwindcss/typography --save-dev                            \necho \"Tailwind CSS typography plugin installed successfully!\"\n      \n# Install dependencies for markdown processing                            \necho \"Installing dependencies for markdown processing...\"                 \nnpm install next-mdx-remote gray-matter --save                            \necho \"Dependencies installed successfully!\"`,\n\t\t},\n\t}\n\n\tonlyTests := map[int]bool{}\n\n\tfor i, tt := range tests {\n\t\tif tt.only {\n\t\t\tonlyTests[i] = true\n\t\t}\n\t}\n\n\tfor i, tt := range tests {\n\t\tif len(onlyTests) > 0 {\n\t\t\tif _, ok := onlyTests[i]; !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// got, err := ApplyChanges(\n\t\t\t// \tcontext.Background(),\n\t\t\t// \ttt.language,\n\t\t\t// \ttt.parser,\n\t\t\t// \ttt.original,\n\t\t\t// \ttt.proposed,\n\t\t\t// \ttt.references,\n\t\t\t// \ttt.removals,\n\t\t\t// \tanchorLines,\n\t\t\t// )\n\n\t\t\t// originalLines := strings.Split(tt.original, \"\\n\")\n\t\t\t// proposedLines := strings.Split(tt.proposed, \"\\n\")\n\n\t\t\tdesc := \"\"\n\t\t\tif tt.desc != \"\" {\n\t\t\t\tdesc = tt.desc\n\t\t\t} else if tt.isInsert {\n\t\t\t\tdesc = \"Type: add\"\n\t\t\t} else {\n\t\t\t\tdesc = \"Type: replace\"\n\t\t\t}\n\n\t\t\tparser, lang, _, _ := GetParserForPath(\"file.\" + tt.ext)\n\n\t\t\tres := ApplyChanges(\n\t\t\t\tcontext.Background(),\n\t\t\t\tApplyChangesParams{\n\t\t\t\t\tOriginal:               tt.original,\n\t\t\t\t\tProposed:               tt.proposed,\n\t\t\t\t\tDesc:                   desc,\n\t\t\t\t\tAddMissingStartEndRefs: false,\n\t\t\t\t\tParser:                 parser,\n\t\t\t\t\tLanguage:               lang,\n\t\t\t\t},\n\t\t\t)\n\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"NAME:\", tt.name)\n\t\t\tfmt.Println(res.NewFile)\n\t\t\tfmt.Println()\n\n\t\t\t// assert.Empty(t, res.NeedsVerifyReasons)\n\n\t\t\tif tt.laxNewlines {\n\t\t\t\tassert.Equal(t, strings.ReplaceAll(tt.want, \"\\n\", \"\"), strings.ReplaceAll(res.NewFile, \"\\n\", \"\"))\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.want, res.NewFile)\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/server/syntax/structured_edits_tree_sitter.go",
    "content": "package syntax\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n\n\ttree_sitter \"github.com/smacker/go-tree-sitter\"\n)\n\ntype tsAnchor struct {\n\topen  int\n\tclose int\n}\n\ntype execApplyTreeSitterParams struct {\n\toriginal,\n\tproposed string\n\treferences  []Reference\n\tremovals    []Removal\n\tanchorLines map[int]int\n\tlanguage    shared.Language\n\tparser      *tree_sitter.Parser\n\tctx         context.Context\n}\n\nfunc ExecApplyTreeSitter(\n\tparams execApplyTreeSitterParams,\n) (*ApplyChangesResult, error) {\n\toriginal := params.original\n\tproposed := params.proposed\n\treferences := params.references\n\tremovals := params.removals\n\tanchorLines := params.anchorLines\n\tlanguage := params.language\n\tparser := params.parser\n\tctx := params.ctx\n\tres := &ApplyChangesResult{}\n\n\tvar b strings.Builder\n\n\twrite := func(s string, newline bool) {\n\t\tif verboseLogging {\n\t\t\ttoLog := s\n\n\t\t\tif len(toLog) > 200 {\n\t\t\t\ttoLog = toLog[:100] + \"\\n...\\n\" + toLog[len(toLog)-100:]\n\t\t\t}\n\n\t\t\tfmt.Printf(\"writing: %s\\n\", toLog)\n\t\t\tfmt.Printf(\"newline: %v\\n\", newline)\n\t\t}\n\n\t\tb.WriteString(s)\n\t\tif newline {\n\t\t\tb.WriteByte('\\n')\n\t\t}\n\t}\n\n\trefsByLine := map[Reference]bool{}\n\tremovalsByLine := map[Removal]bool{}\n\n\tfor _, ref := range references {\n\t\trefsByLine[ref] = true\n\t}\n\n\tfor _, removal := range removals {\n\t\tremovalsByLine[removal] = true\n\t}\n\n\toriginalLines := strings.Split(original, \"\\n\")\n\tproposedLines := strings.Split(proposed, \"\\n\")\n\n\t// normalize comments in case the wrong comment symbols were used\n\topeningCommentSymbol, closingCommentSymbol := GetCommentSymbols(language)\n\tif openingCommentSymbol != \"\" {\n\t\tfor i, line := range proposedLines {\n\t\t\t// keep indentation for syntax parsing\n\t\t\tcontent := strings.TrimSpace(line)\n\n\t\t\tif removalsByLine[Removal(i+1)] || refsByLine[Reference(i+1)] {\n\t\t\t\tcomment := openingCommentSymbol + \" ref \" + closingCommentSymbol\n\t\t\t\tproposedLines[i] = strings.Replace(line, content, comment, 1)\n\t\t\t}\n\t\t}\n\t}\n\n\tproposedWithNormalizedComments := strings.Join(proposedLines, \"\\n\")\n\tres.Proposed = proposedWithNormalizedComments\n\n\toriginalBytes := []byte(original)\n\tproposedBytes := []byte(proposedWithNormalizedComments)\n\n\toriginalTree, err := parser.ParseCtx(ctx, nil, originalBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse the original content: %v\", err)\n\t}\n\tdefer originalTree.Close()\n\n\tproposedTree, err := parser.ParseCtx(ctx, nil, proposedBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse the proposed content: %v\", err)\n\t}\n\tdefer proposedTree.Close()\n\n\tif verboseLogging {\n\t\tfmt.Printf(\"anchorLines: %v\\n\", anchorLines)\n\t}\n\n\toRes := BuildNodeIndex(originalTree)\n\tpRes := BuildNodeIndex(proposedTree)\n\n\toriginalNodesByLineIndex := oRes.nodesByLine\n\tproposedNodesByLineIndex := pRes.nodesByLine\n\toriginalParentsByLineIndex := oRes.parentsByLine\n\n\tfindNextAnchor := func(s string, pLineNum int, pNode *tree_sitter.Node, fromLine int) *tsAnchor {\n\t\toLineNum, ok := anchorLines[pLineNum]\n\t\tif ok {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"found anchor in anchorLines: oLineNum: %d, pLineNum: %d\\n\", oLineNum, pLineNum)\n\t\t\t}\n\n\t\t\toNode := originalNodesByLineIndex[oLineNum-1]\n\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"oNode.Type(): %s\\n\", oNode.Type())\n\t\t\t}\n\n\t\t\tanchor := &tsAnchor{open: oLineNum, close: int(oNode.EndPoint().Row) + 1}\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"found anchor: %v\\n\", anchor)\n\t\t\t}\n\t\t\treturn anchor\n\t\t} else {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"no anchor found in anchorLines: pLineNum: %d\\n\", pLineNum)\n\t\t\t}\n\t\t}\n\n\t\tfor idx, line := range originalLines {\n\t\t\tif idx < fromLine {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif strings.TrimSpace(line) == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// if verboseLogging {\n\t\t\t// \tfmt.Printf(\"line: %s, idx: %d\\n\", line, idx)\n\t\t\t// }\n\n\t\t\toNode := originalNodesByLineIndex[idx]\n\n\t\t\tif verboseLogging {\n\t\t\t\t// fmt.Println(\"node:\")\n\t\t\t\t// fmt.Println(oNode.Type())\n\t\t\t\t// fmt.Println(oNode)\n\t\t\t\t// fmt.Println(oNode.Content(originalBytes))\n\t\t\t}\n\n\t\t\t// just using string matching for now since there's too much ambiguity in node-based matching\n\t\t\tstringMatch := line == s\n\t\t\t// nodeMatch := oNode != nil && oNode.IsNamed() && nodesMatch(oNode, pNode, originalBytes, proposedBytes)\n\n\t\t\tif stringMatch {\n\t\t\t\tvar endLineNum int\n\t\t\t\tif oNode != nil {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"oNode.Type(): %s\\n\", oNode.Type())\n\t\t\t\t\t\tfmt.Printf(\"oNode.EndPoint().Row: %d\\n\", oNode.EndPoint().Row)\n\t\t\t\t\t\t// fmt.Printf(\"%v\\n\", oNode)\n\t\t\t\t\t\t// fmt.Printf(\"oNode.Content(originalBytes):\\n%q\\n\", oNode.Content(originalBytes))\n\t\t\t\t\t}\n\t\t\t\t\tendLineNum = int(oNode.EndPoint().Row) + 1\n\t\t\t\t}\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"found match: num: %d, endLineNum: %d\\n\", idx+1, endLineNum)\n\t\t\t\t\t//   fmt.Println(oNode.Content(originalBytes))\n\t\t\t\t}\n\n\t\t\t\treturn &tsAnchor{open: idx + 1, close: endLineNum}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tproposedUpdatesHaveLine := func(line string, afterLine int) bool {\n\t\tfor idx, pLine := range proposedLines {\n\t\t\tpLineNum := idx + 1\n\n\t\t\t// if verboseLogging {\n\t\t\t// \tfmt.Printf(\"proposedUpdatesHaveLine - lineNum: %d, line: %s, pLine: %s, afterLine: %d\\n\", pLineNum, line, pLine, afterLine)\n\t\t\t// }\n\n\t\t\tif pLineNum > afterLine && pLine == line {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\tvar oLineNum int = 0\n\tvar refOpen bool\n\tvar refStart int\n\tvar refOriginalParent *tree_sitter.Node\n\tvar postRefBuffers []strings.Builder\n\n\tclosingLinesByPLineNum := map[int]int{}\n\tvar noMatchUntilStructureClose string\n\tdepth := 0\n\n\tvar currentPNode *tree_sitter.Node\n\tvar currentPNodeEndsAtIdx int\n\tvar currentPNodeMatches bool\n\n\tlastLineMatched := true\n\tfoundAnyAnchor := false\n\n\tsetOLineNum := func(n int) {\n\t\toLineNum = n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"setting oLineNum: %d\\n\", oLineNum)\n\t\t}\n\t}\n\n\twriteToLatestPostRefBuffer := func(s string) {\n\t\tlatestBuffer := &postRefBuffers[len(postRefBuffers)-1]\n\t\tlatestBuffer.WriteString(s)\n\t\tlatestBuffer.WriteByte('\\n')\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"writing to latest postRefBuffer: %q\\n\", s)\n\t\t}\n\t}\n\n\taddNewPostRefBuffer := func() {\n\t\tpostRefBuffers = append(postRefBuffers, strings.Builder{})\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"adding new postRefBuffer\\n\")\n\t\t}\n\t}\n\n\tresetPostRefBuffers := func() {\n\t\tpostRefBuffers = []strings.Builder{}\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"resetting postRefBuffers\\n\")\n\t\t}\n\t}\n\n\tincDepth := func() {\n\t\tdepth++\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"incrementing depth: %d\\n\", depth)\n\t\t}\n\t}\n\n\tdecDepth := func() {\n\t\tdepth--\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"decrementing depth: %d\\n\", depth)\n\t\t}\n\t}\n\n\twriteRefs := func(eof bool) bool {\n\t\tnumRefs := len(postRefBuffers)\n\t\tif numRefs == 1 {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println(\"writeRefs\")\n\t\t\t\tfmt.Printf(\"numRefs == 1, refStart: %d, oLineNum: %d\\n\", refStart, oLineNum)\n\t\t\t}\n\n\t\t\tvar fullRef []string\n\t\t\tif eof {\n\t\t\t\tstart := refStart - 1\n\t\t\t\tfullRef = originalLines[start:]\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"eof\")\n\t\t\t\t\tfmt.Printf(\"fullRef refStart: %d\\n\", refStart)\n\t\t\t\t\tfmt.Printf(\"originalLines[refStart]: %q\\n\", originalLines[refStart])\n\t\t\t\t\tfmt.Printf(\"writing eof fullRef: %q\\n\", strings.Join(fullRef, \"\\n\"))\n\t\t\t\t\tfmt.Printf(\"depth: %d\\n\", depth)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tstart := refStart - 1\n\t\t\t\tif start < 0 {\n\t\t\t\t\tstart = 0\n\t\t\t\t}\n\t\t\t\tend := oLineNum - 1\n\t\t\t\tif end < 1 {\n\t\t\t\t\tend = 0\n\t\t\t\t}\n\n\t\t\t\t// Add detailed diagnostic logging for invalid slice bounds\n\t\t\t\tif start > end {\n\t\t\t\t\tfmt.Printf(\"\\n=== INVALID SLICE BOUNDS DIAGNOSTIC INFO ===\\n\")\n\t\t\t\t\tfmt.Printf(\"start: %d, end: %d\\n\", start, end)\n\t\t\t\t\tfmt.Printf(\"refStart: %d, oLineNum: %d\\n\", refStart, oLineNum)\n\t\t\t\t\tfmt.Printf(\"depth: %d, refOpen: %v\\n\", depth, refOpen)\n\n\t\t\t\t\t// Log relevant lines for context\n\t\t\t\t\tfmt.Printf(\"\\nOriginal lines context:\\n\")\n\t\t\t\t\tstartContext := max(0, start-2)\n\t\t\t\t\tendContext := min(len(originalLines), end+3)\n\t\t\t\t\tfor i := startContext; i < endContext; i++ {\n\t\t\t\t\t\tfmt.Printf(\"line %d: %q\\n\", i+1, originalLines[i])\n\t\t\t\t\t}\n\n\t\t\t\t\tfmt.Printf(\"\\nProposed lines context:\\n\")\n\t\t\t\t\tfmt.Printf(\"=====================================\\n\\n\")\n\n\t\t\t\t\tres.NeedsVerifyReasons = append(res.NeedsVerifyReasons, NeedsVerifyReasonAmbiguousLocation)\n\n\t\t\t\t\treturn false\n\t\t\t\t}\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"writing fullRef\\n\")\n\t\t\t\t\tfmt.Printf(\"refStart: %d, oLineNum: %d\\n\", refStart, oLineNum)\n\t\t\t\t\tfmt.Printf(\"start: %d, end: %d\\n\", start, end)\n\t\t\t\t}\n\t\t\t\tfullRef = originalLines[start:end]\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"fullRef refStart: %d, oLineNum-1: %d\\n\", refStart, oLineNum-1)\n\t\t\t\t\tfmt.Printf(\"originalLines[start]: %q\\n\", originalLines[start])\n\t\t\t\t\tfmt.Printf(\"originalLines[end]: %q\\n\", originalLines[end])\n\t\t\t\t\tfmt.Printf(\"depth: %d\\n\", depth)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\twrite(strings.Join(fullRef, \"\\n\"), !eof)\n\n\t\t\tpostRefContent := postRefBuffers[0].String()\n\n\t\t\tif strings.TrimSpace(postRefContent) != \"\" {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"writing postRefBuffer\")\n\t\t\t\t}\n\t\t\t\twrite(postRefBuffers[0].String(), false)\n\t\t\t}\n\t\t} else {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"numRefs > 1, refOriginalParent: %s, eof: %v\\n\", refOriginalParent.Type(), eof)\n\t\t\t\tfmt.Printf(\"refOriginalParent.Content(originalBytes):\\n%q\\n\", refOriginalParent.Content(originalBytes))\n\t\t\t\tfmt.Printf(\"numRefs: %d, oLineNum: %d\\n\", numRefs, oLineNum)\n\t\t\t}\n\n\t\t\tvar upToLine int\n\t\t\tif eof {\n\t\t\t\tupToLine = len(originalLines)\n\t\t\t} else {\n\t\t\t\tupToLine = oLineNum\n\t\t\t}\n\n\t\t\tsections := getSections(\n\t\t\t\trefOriginalParent,\n\t\t\t\toriginalBytes,\n\t\t\t\tnumRefs,\n\t\t\t\trefStart,\n\t\t\t\tupToLine,\n\t\t\t\tfoundAnyAnchor,\n\t\t\t)\n\n\t\t\tfor i, section := range sections {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"writing section i: %d\\n\", i)\n\t\t\t\t\t// fmt.Printf(\"writing i: %d, section:\\n%q\\n\t\", i, section.String(originalLines, originalBytes))\n\t\t\t\t}\n\t\t\t\twrite(section.String(originalLines, originalBytes), false)\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"writing postRefBuffer\")\n\t\t\t\t}\n\t\t\t\twrite(postRefBuffers[i].String(), false)\n\t\t\t}\n\t\t}\n\n\t\treturn true\n\t}\n\n\tfor idx, pLine := range proposedLines {\n\t\tfinalLine := idx == len(proposedLines)-1\n\t\tpLineNum := idx + 1\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"\\n\\ni: %d, num: %d, pLine: %q, refOpen: %v\\n\", idx, pLineNum, pLine, refOpen)\n\t\t}\n\n\t\tisRef := refsByLine[Reference(pLineNum)]\n\t\tisRemoval := removalsByLine[Removal(pLineNum)]\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"isRef: %v\\n\", isRef)\n\t\t\tfmt.Printf(\"isRemoval: %v\\n\", isRemoval)\n\t\t\tfmt.Printf(\"oLineNum: %d\\n\", oLineNum)\n\t\t\tfmt.Printf(\"currentPNode set: %v\\n\", currentPNode != nil)\n\t\t\tfmt.Printf(\"currentPNodeEndsAtIdx: %d\\n\", currentPNodeEndsAtIdx)\n\t\t\tfmt.Printf(\"currentPNodeMatches: %v\\n\", currentPNodeMatches)\n\t\t\tfmt.Printf(\"lastLineMatched: %v\\n\", lastLineMatched)\n\t\t\tfmt.Printf(\"depth: %d\\n\", depth)\n\t\t}\n\n\t\tif isRemoval {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Println(\"isRemoval - skip line\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif isRef {\n\t\t\tif !refOpen {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"isRef - opening ref\")\n\t\t\t\t\tpnode := proposedNodesByLineIndex[idx]\n\t\t\t\t\tfmt.Printf(\"pnode.Type(): %s\\n\", pnode.Type())\n\t\t\t\t\t// fmt.Printf(\"pnode.Content(proposedBytes):\\n%q\\n\", pnode.Content(proposedBytes))\n\t\t\t\t}\n\n\t\t\t\trefOpen = true\n\t\t\t\tsetOLineNum(oLineNum + 1)\n\t\t\t\trefStart = oLineNum\n\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"setting refStart: %d\\n\", refStart)\n\t\t\t\t}\n\n\t\t\t\tif depth > 0 {\n\t\t\t\t\trefNode := originalNodesByLineIndex[refStart-1]\n\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"refNode.Type(): %s\\n\", refNode.Type())\n\t\t\t\t\t\t// fmt.Printf(\"refNode.Content(originalBytes): %q\\n\", refNode.Content(originalBytes))\n\n\t\t\t\t\t\tcurrent := refNode\n\t\t\t\t\t\tdepth := 0\n\t\t\t\t\t\tfor current != nil {\n\t\t\t\t\t\t\tfmt.Printf(\"parent depth %d type: %s content: %q\\n\",\n\t\t\t\t\t\t\t\tdepth,\n\t\t\t\t\t\t\t\tcurrent.Type(),\n\t\t\t\t\t\t\t\tcurrent.Content(originalBytes))\n\t\t\t\t\t\t\tcurrent = current.Parent()\n\t\t\t\t\t\t\tdepth++\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\trefOriginalParent = originalParentsByLineIndex[refStart-1]\n\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"setting refOriginalParent | refNode.Parent() | Type(): %s\\n\", refOriginalParent.Type())\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\trefOriginalParent = originalTree.RootNode()\n\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"setting refOriginalParent | originalTree.RootNode() | Type(): %s\\n\", refOriginalParent.Type())\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\taddNewPostRefBuffer()\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif !refOpen && lastLineMatched && !currentPNodeMatches {\n\t\t\tif strings.TrimSpace(pLine) == \"\" {\n\t\t\t\twrite(pLine, !finalLine)\n\t\t\t\t// Check if next line in original is also blank\n\t\t\t\tif oLineNum+1 < len(originalLines) && strings.TrimSpace(originalLines[oLineNum+1]) == \"\" {\n\t\t\t\t\tsetOLineNum(oLineNum + 1)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tpNode := proposedNodesByLineIndex[idx]\n\t\tpNodeStartsThisLine := pNode.StartPoint().Row == uint32(idx)\n\t\tpNodeEndsAtIdx := int(pNode.EndPoint().Row)\n\t\tpNodeMultiline := pNodeEndsAtIdx > idx\n\n\t\tvar matching bool\n\t\tisClosingAnchor := closingLinesByPLineNum[pLineNum] != 0\n\n\t\tif verboseLogging {\n\t\t\tfmt.Printf(\"currentPNode != nil: %v\\n\", currentPNode != nil)\n\t\t\tfmt.Printf(\"currentPNodeMatches: %v\\n\", currentPNodeMatches)\n\t\t}\n\n\t\tif isClosingAnchor {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"isClosingAnchor: %v\\n\", isClosingAnchor)\n\t\t\t}\n\t\t\tmatching = true\n\t\t\tsetOLineNum(closingLinesByPLineNum[pLineNum])\n\t\t\tnoMatchUntilStructureClose = \"\"\n\t\t} else if noMatchUntilStructureClose != pLine && !(currentPNode != nil && !currentPNodeMatches) {\n\t\t\t// find next line in original that matches\n\t\t\tanchor := findNextAnchor(pLine, pLineNum, pNode, oLineNum-1)\n\t\t\tif anchor != nil {\n\t\t\t\tfoundAnyAnchor = true\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Println(\"anchor found\")\n\t\t\t\t\tfmt.Printf(\"anchor.close: %d, anchor.open: %d\\n\", anchor.close, anchor.open)\n\t\t\t\t}\n\t\t\t\tmatching = true\n\t\t\t\tsetOLineNum(anchor.open)\n\n\t\t\t\tif pNodeStartsThisLine && pNodeMultiline && currentPNode != nil {\n\t\t\t\t\tcurrentPNodeMatches = true\n\t\t\t\t}\n\n\t\t\t\tif anchor.close != 0 && anchor.close != anchor.open {\n\n\t\t\t\t\toriginalClosingLine := originalLines[anchor.close-1]\n\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tfmt.Printf(\"originalClosingLine: %s\\n\", originalClosingLine)\n\t\t\t\t\t}\n\n\t\t\t\t\tif proposedUpdatesHaveLine(originalClosingLine, idx) {\n\t\t\t\t\t\t// if verboseLogging {\n\t\t\t\t\t\t// \tfmt.Printf(\"proposedUpdatesHaveLine: %v\\n\", proposedUpdatesHaveLine(originalClosingLine, anchor.open))\n\t\t\t\t\t\t// }\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Printf(\"proposedUpdatesHaveLine: %v\\n\", proposedUpdatesHaveLine(originalClosingLine, anchor.open))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tclosingPLineNum := int(pNode.EndPoint().Row) + 1\n\t\t\t\t\t\tclosingLinesByPLineNum[closingPLineNum] = anchor.close\n\t\t\t\t\t\tnoMatchUntilStructureClose = originalClosingLine\n\t\t\t\t\t\tincDepth()\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Printf(\"anchor.close: %d\\n\", anchor.close)\n\t\t\t\t\t\t\tfmt.Printf(\"closingPLineNum: %d\\n\", closingPLineNum)\n\t\t\t\t\t\t\tfmt.Printf(\"noMatchUntilStructureClose: %s\\n\", noMatchUntilStructureClose)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\t\tfmt.Println(\"proposed updates do not have originalClosingLine\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif pNodeStartsThisLine && pNodeMultiline && (currentPNode == nil || matching != currentPNodeMatches) {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"setting currentPNode: %s\\n\", pNode.Type())\n\t\t\t\tfmt.Printf(\"pNodeEndsAtIdx: %d\\n\", pNodeEndsAtIdx)\n\t\t\t\t// fmt.Printf(\"content: %q\\n\", pNode.Content(proposedBytes))\n\t\t\t\t// fmt.Println(pNode)\n\t\t\t}\n\t\t\tcurrentPNode = pNode\n\t\t\tcurrentPNodeEndsAtIdx = pNodeEndsAtIdx\n\t\t\tcurrentPNodeMatches = matching\n\n\t\t}\n\n\t\twroteRefs := false\n\t\tif matching {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"matching line: %s, oLineNum: %d\\n\", pLine, oLineNum)\n\t\t\t}\n\n\t\t\tif refOpen {\n\t\t\t\t// we found the end of the current reference\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"closing ref, oLineNum: %d\\n\", oLineNum)\n\t\t\t\t}\n\t\t\t\trefOpen = false\n\t\t\t\tok := writeRefs(false)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn res, nil\n\t\t\t\t}\n\t\t\t\twrite(pLine, !finalLine)\n\t\t\t\twroteRefs = true\n\t\t\t}\n\t\t} else {\n\t\t\tif verboseLogging {\n\t\t\t\tfmt.Printf(\"no matching line\\n\")\n\t\t\t}\n\t\t}\n\n\t\tlastLineMatched = matching\n\n\t\tif currentPNodeEndsAtIdx == idx {\n\t\t\tcurrentPNode = nil\n\t\t\tcurrentPNodeEndsAtIdx = 0\n\t\t\tcurrentPNodeMatches = false\n\t\t}\n\n\t\tif isClosingAnchor {\n\t\t\tdecDepth()\n\t\t}\n\n\t\tif wroteRefs {\n\t\t\t// reset buffers\n\t\t\tresetPostRefBuffers()\n\t\t} else {\n\t\t\tif refOpen {\n\t\t\t\twriteToLatestPostRefBuffer(pLine)\n\t\t\t} else {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tfmt.Printf(\"writing pLine: %s\\n\", pLine)\n\t\t\t\t}\n\t\t\t\twrite(pLine, !finalLine)\n\t\t\t}\n\n\t\t}\n\n\t}\n\n\tif refOpen {\n\t\tok := writeRefs(true)\n\t\tif !ok {\n\t\t\treturn res, nil\n\t\t}\n\t}\n\n\tif verboseLogging {\n\t\t// fmt.Printf(\"final result:\\n%s\\n\", b.String())\n\t}\n\n\tres.NewFile = b.String()\n\n\treturn res, nil\n}\n"
  },
  {
    "path": "app/server/syntax/unique_replacement.go",
    "content": "package syntax\n\nimport \"strings\"\n\nfunc FindUniqueReplacement(originalFile, old string) string {\n\toldCount := strings.Count(originalFile, old)\n\n\tif oldCount == 1 {\n\t\t// perfect match\n\t\treturn old\n\t}\n\n\t// try to find a unique match. we're forgiving of errors in the middle if we can still identify the block uniquely by checking from both ends\n\tstartMatch := \"\"\n\tendMatch := \"\"\n\tn := 0\n\toldLength := len(old)\n\n\tfor {\n\t\tn++\n\t\tif n > oldLength {\n\t\t\t// Prevent slice bounds error\n\t\t\treturn \"\"\n\t\t}\n\n\t\tstartMatch = old[0:n]\n\t\tendMatch = old[oldLength-n : oldLength]\n\n\t\tstartOccurrences := strings.Count(originalFile, startMatch)\n\n\t\tif startOccurrences == 0 {\n\t\t\t// lost the match from the start\n\t\t\treturn \"\"\n\t\t}\n\n\t\tvar endOccurrences int\n\t\tvar afterStart string\n\t\tif startOccurrences == 1 {\n\t\t\tstartSplit := strings.Split(originalFile, startMatch)\n\t\t\tafterStart = startSplit[1]\n\n\t\t\tendOccurrences = strings.Count(afterStart, endMatch)\n\t\t} else {\n\t\t\tendOccurrences = strings.Count(originalFile, endMatch)\n\t\t}\n\n\t\tif endOccurrences == 0 {\n\t\t\t// lost the match from the end\n\t\t\treturn \"\"\n\t\t}\n\n\t\tvar beforeEnd string\n\t\tif endOccurrences == 1 {\n\t\t\tendSplit := strings.Split(originalFile, endMatch)\n\t\t\tbeforeEnd = endSplit[0]\n\t\t\tif startOccurrences > 1 {\n\t\t\t\tstartOccurrences = strings.Count(beforeEnd, startMatch)\n\t\t\t}\n\t\t}\n\n\t\tif startOccurrences == 1 && endOccurrences == 1 {\n\t\t\tafterStartIndex := strings.Index(originalFile, afterStart)\n\t\t\tbeforeEndIndex := strings.Index(originalFile, beforeEnd)\n\n\t\t\tstartIndex := beforeEndIndex + strings.Index(beforeEnd, startMatch)\n\t\t\tendIndex := afterStartIndex + strings.Index(afterStart, endMatch) + len(endMatch)\n\t\t\treturn originalFile[startIndex:endIndex]\n\t\t} else {\n\t\t\t// couldn't get a unique match on both ends, keep narrowing down\n\t\t\tcontinue\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "app/server/syntax/unique_replacement_test.go",
    "content": "package syntax\n\nimport (\n\t\"testing\"\n)\n\nfunc TestFindUniqueReplacement(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\toriginalFile string\n\t\told          string\n\t\twant         string\n\t}{\n\t\t{\n\t\t\tname:         \"perfect single match\",\n\t\t\toriginalFile: \"prefix ABC123DEF suffix\",\n\t\t\told:          \"ABC123DEF\",\n\t\t\twant:         \"ABC123DEF\",\n\t\t},\n\t\t{\n\t\t\tname:         \"match with error in middle\",\n\t\t\toriginalFile: \"prefix ABC999DEF suffix\",\n\t\t\told:          \"ABC123DEF\",\n\t\t\twant:         \"ABC999DEF\",\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple instances but unique boundaries\",\n\t\t\toriginalFile: \"ABC123XYZ ABC456XYZ ABC789DEF\",\n\t\t\told:          \"ABC789DEF\",\n\t\t\twant:         \"ABC789DEF\",\n\t\t},\n\t\t{\n\t\t\tname:         \"no match at all\",\n\t\t\toriginalFile: \"completely different text\",\n\t\t\told:          \"ABC123DEF\",\n\t\t\twant:         \"\",\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple complete matches\",\n\t\t\toriginalFile: \"ABC123DEF ABC123DEF\",\n\t\t\told:          \"ABC123DEF\",\n\t\t\twant:         \"\", // should fail because not unique\n\t\t},\n\t\t{\n\t\t\tname:         \"ambiguous boundaries\",\n\t\t\toriginalFile: \"ABC123DEF ABC456DEF\",\n\t\t\told:          \"ABC789DEF\",\n\t\t\twant:         \"\", // should fail because multiple possible matches\n\t\t},\n\t\t{\n\t\t\tname:         \"match with very different middle\",\n\t\t\toriginalFile: \"prefix ABCCOMPLETELY_DIFFERENT_TEXTDEF suffix\",\n\t\t\told:          \"ABC123DEF\",\n\t\t\twant:         \"ABCCOMPLETELY_DIFFERENT_TEXTDEF\",\n\t\t},\n\t\t{\n\t\t\tname:         \"unique match near identical text\",\n\t\t\toriginalFile: \"ABCDEF ABC123DEF ABCXEF\",\n\t\t\told:          \"ABC123DEF\",\n\t\t\twant:         \"ABC123DEF\",\n\t\t},\n\t\t{\n\t\t\tname:         \"identical start/end patterns\",\n\t\t\toriginalFile: \"AAA123AAA AAA456AAA\",\n\t\t\told:          \"AAA789AAA\",\n\t\t\twant:         \"\", // should fail because boundaries are ambiguous\n\t\t},\n\t\t{\n\t\t\tname:         \"overlapping patterns\",\n\t\t\toriginalFile: \"ABCABCDEF\",\n\t\t\told:          \"ABCDEF\",\n\t\t\twant:         \"ABCDEF\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := FindUniqueReplacement(tt.originalFile, tt.old)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"FindUniqueReplacement(%q, %q) = %q, want %q\",\n\t\t\t\t\ttt.originalFile, tt.old, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/server/syntax/validate.go",
    "content": "package syntax\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\ttree_sitter \"github.com/smacker/go-tree-sitter\"\n\n\t\"context\"\n\t\"fmt\"\n)\n\nconst parserTimeout = 500 * time.Millisecond\n\ntype ValidationRes = struct {\n\tLang     shared.Language\n\tParser   *tree_sitter.Parser\n\tTimedOut bool\n\tValid    bool\n\tErrors   []string\n}\n\nfunc ValidateFile(ctx context.Context, path string, file string) (*ValidationRes, error) {\n\tparser, lang, fallbackParser, fallbackLang := GetParserForPath(path)\n\n\tif parser == nil {\n\t\treturn &ValidationRes{Lang: lang, Parser: nil}, nil\n\t}\n\n\treturn ValidateWithParsers(ctx, lang, parser, fallbackLang, fallbackParser, file)\n}\n\nfunc ValidateWithParsers(ctx context.Context, lang shared.Language, parser *tree_sitter.Parser, fallbackLang shared.Language, fallbackParser *tree_sitter.Parser, file string) (*ValidationRes, error) {\n\tif file == \"\" {\n\t\treturn &ValidationRes{Lang: lang, Parser: parser, Valid: true}, nil\n\t}\n\n\t// Set a timeout duration for the parsing operations\n\tctx, cancel := context.WithTimeout(ctx, parserTimeout)\n\tdefer cancel()\n\n\t// Parse the content\n\ttree, err := parser.ParseCtx(ctx, nil, []byte(file))\n\n\tif err != nil || tree == nil {\n\t\tif err != nil && err.Error() == \"operation limit was hit\" {\n\t\t\treturn &ValidationRes{Lang: lang, Parser: parser, TimedOut: true}, nil\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to parse the content: %v\", err)\n\t}\n\tdefer tree.Close()\n\n\t// Get the root node of the syntax tree and check for errors\n\troot := tree.RootNode()\n\n\tif root.HasError() {\n\t\tif fallbackParser != nil {\n\t\t\tfallbackTree, err := fallbackParser.ParseCtx(ctx, nil, []byte(file))\n\t\t\tif err != nil || fallbackTree == nil {\n\n\t\t\t\tif err != nil && strings.Contains(err.Error(), \"timeout\") {\n\t\t\t\t\treturn &ValidationRes{Lang: lang, Parser: parser, TimedOut: true}, nil\n\t\t\t\t}\n\n\t\t\t\treturn nil, fmt.Errorf(\"failed to parse the content with fallback parser: %v\", err)\n\t\t\t}\n\t\t\tdefer fallbackTree.Close()\n\n\t\t\troot = fallbackTree.RootNode()\n\n\t\t\tif !root.HasError() {\n\t\t\t\treturn &ValidationRes{Lang: fallbackLang, Parser: fallbackParser, Valid: true}, nil\n\t\t\t}\n\t\t}\n\n\t\terrorMarkers := insertErrorMarkers(file, root)\n\n\t\treturn &ValidationRes{\n\t\t\tLang:   lang,\n\t\t\tParser: parser,\n\t\t\tValid:  false,\n\t\t\tErrors: errorMarkers,\n\t\t}, nil\n\n\t}\n\n\treturn &ValidationRes{Lang: lang, Parser: parser, Valid: true}, nil\n}\n\nfunc insertErrorMarkers(source string, node *tree_sitter.Node) []string {\n\tif source == \"\" {\n\t\treturn []string{}\n\t}\n\n\tvar markers []string\n\tvar uniqueMarkers = map[string]bool{}\n\n\t// Function to calculate line numbers\n\tcalculateLineNumber := func(position int) int {\n\t\treturn strings.Count(source[:position], \"\\n\") + 1\n\t}\n\n\thasChildError := func(n *tree_sitter.Node) bool {\n\t\tfor i := 0; i < int(n.ChildCount()); i++ {\n\t\t\tif n.Child(i).HasError() {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\tvisitNodes(node, func(n *tree_sitter.Node) {\n\t\tif n.HasError() && !hasChildError(n) {\n\t\t\tstartPosition := int(n.StartByte())\n\t\t\tendPosition := int(n.EndByte())\n\t\t\tstartLineNumber := calculateLineNumber(startPosition)\n\t\t\tendLineNumber := calculateLineNumber(endPosition)\n\n\t\t\tif startLineNumber == endLineNumber {\n\t\t\t\tuniqueMarkers[fmt.Sprintf(\"Invalid syntax on line %d\", startLineNumber)] = true\n\t\t\t} else {\n\t\t\t\tuniqueMarkers[fmt.Sprintf(\"Invalid syntax on lines %d to %d\", startLineNumber, endLineNumber)] = true\n\t\t\t}\n\n\t\t}\n\t})\n\n\tfor marker := range uniqueMarkers {\n\t\tmarkers = append(markers, marker)\n\t}\n\n\treturn markers\n}\n\n// visitNodes recursively visits nodes in the syntax tree\nfunc visitNodes(n *tree_sitter.Node, f func(node *tree_sitter.Node)) {\n\tf(n)\n\tfor i := 0; i < int(n.ChildCount()); i++ {\n\t\tchild := n.Child(i)\n\t\tvisitNodes(child, f)\n\t}\n}\n"
  },
  {
    "path": "app/server/types/active_plan.go",
    "content": "package types\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"plandex-server/db\"\n\t\"plandex-server/notify\"\n\t\"plandex-server/shutdown\"\n\t\"sync\"\n\t\"time\"\n\n\tshared \"plandex-shared\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/google/uuid\"\n)\n\nconst MaxStreamRate = 70 * time.Millisecond\nconst ActivePlanTimeout = 2 * time.Hour\n\ntype ActiveBuild struct {\n\tReplyId           string\n\tFileDescription   string\n\tFileContent       string\n\tFileContentTokens int\n\tCurrentFileTokens int\n\tPath              string\n\tSuccess           bool\n\tError             error\n\tIsMoveOp          bool\n\tMoveDestination   string\n\tIsRemoveOp        bool\n\tIsResetOp         bool\n}\n\ntype subscription struct {\n\tch           chan string\n\tctx          context.Context\n\tcancelFn     context.CancelFunc\n\tmu           sync.Mutex // Protects the messageQueue\n\tmessageQueue []string\n\tcond         *sync.Cond // Used to wait for and signal new messages\n}\n\ntype ActivePlan struct {\n\tId                      string\n\tUserId                  string\n\tOrgId                   string\n\tCurrentStreamingReplyId string\n\tCurrentReplyDoneCh      chan bool\n\tBranch                  string\n\tPrompt                  string\n\tBuildOnly               bool\n\tCtx                     context.Context\n\tCancelFn                context.CancelFunc\n\tModelStreamCtx          context.Context\n\tCancelModelStreamFn     context.CancelFunc\n\tSummaryCtx              context.Context\n\tSummaryCancelFn         context.CancelFunc\n\t// LatestSummaryCh         chan *db.ConvoSummary\n\tContexts              []*db.Context\n\tContextsByPath        map[string]*db.Context\n\tOperations            []*shared.Operation\n\tBuiltFiles            map[string]bool\n\tIsBuildingByPath      map[string]bool\n\tCurrentReplyContent   string\n\tNumTokens             int\n\tMessageNum            int\n\tBuildQueuesByPath     map[string][]*ActiveBuild\n\tRepliesFinished       bool\n\tStreamDoneCh          chan *shared.ApiError\n\tModelStreamId         string\n\tMissingFilePath       string\n\tMissingFileResponseCh chan shared.RespondMissingFileChoice\n\tAutoContext           bool\n\tAutoLoadContextCh     chan struct{}\n\tAllowOverwritePaths   map[string]bool\n\tSkippedPaths          map[string]bool\n\tStoredReplyIds        []string\n\tDidEditFiles          bool\n\tSessionId             string\n\n\tsubscriptions  map[string]*subscription\n\tsubscriptionMu sync.Mutex\n\n\tstreamCh              chan string\n\tstreamMu              sync.Mutex\n\tlastStreamMessageSent time.Time\n\tstreamMessageBuffer   []shared.StreamMessage\n}\n\nfunc NewActivePlan(orgId, userId, planId, branch, prompt string, buildOnly, autoContext bool, sessionId string) *ActivePlan {\n\tctx, cancel := context.WithTimeout(shutdown.ShutdownCtx, ActivePlanTimeout)\n\t// child context for model stream so we can cancel it separately if needed\n\tmodelStreamCtx, cancelModelStream := context.WithCancel(ctx)\n\n\t// we don't want to cancel summaries unless the whole plan is stopped or there's an error -- if the active plan finishes, we want summaries to continue -- so they get their own context\n\tsummaryCtx, cancelSummary := context.WithCancel(shutdown.ShutdownCtx)\n\n\tactive := ActivePlan{\n\t\tId:                    planId,\n\t\tOrgId:                 orgId,\n\t\tUserId:                userId,\n\t\tBuildOnly:             buildOnly,\n\t\tBranch:                branch,\n\t\tPrompt:                prompt,\n\t\tCtx:                   ctx,\n\t\tCancelFn:              cancel,\n\t\tModelStreamCtx:        modelStreamCtx,\n\t\tCancelModelStreamFn:   cancelModelStream,\n\t\tSummaryCtx:            summaryCtx,\n\t\tSummaryCancelFn:       cancelSummary,\n\t\tBuildQueuesByPath:     map[string][]*ActiveBuild{},\n\t\tContexts:              []*db.Context{},\n\t\tContextsByPath:        map[string]*db.Context{},\n\t\tOperations:            []*shared.Operation{},\n\t\tBuiltFiles:            map[string]bool{},\n\t\tIsBuildingByPath:      map[string]bool{},\n\t\tStreamDoneCh:          make(chan *shared.ApiError),\n\t\tMissingFileResponseCh: make(chan shared.RespondMissingFileChoice),\n\t\tAutoContext:           autoContext,\n\t\tAutoLoadContextCh:     make(chan struct{}),\n\t\tAllowOverwritePaths:   map[string]bool{},\n\t\tSkippedPaths:          map[string]bool{},\n\t\tSessionId:             sessionId,\n\t\tstreamCh:              make(chan string),\n\t\tsubscriptions:         map[string]*subscription{},\n\t\tsubscriptionMu:        sync.Mutex{},\n\t}\n\n\tgo func() {\n\t\tdefer func() {\n\t\t\tlog.Println(\"ActivePlan stream manager returned\")\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"Recovered in send to subscriber: %v\\n\", r)\n\t\t\t}\n\t\t}()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-active.Ctx.Done():\n\t\t\t\treturn\n\t\t\tcase msg := <-active.streamCh:\n\t\t\t\tvar subscriptions map[string]*subscription\n\t\t\t\tactive.subscriptionMu.Lock()\n\t\t\t\tsubscriptions = active.subscriptions\n\t\t\t\tactive.subscriptionMu.Unlock()\n\t\t\t\tfor _, sub := range subscriptions {\n\t\t\t\t\tsub.enqueueMessage(msg)\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn &active\n}\n\nfunc (ap *ActivePlan) FlushStreamBuffer() {\n\tap.streamMu.Lock()\n\tif len(ap.streamMessageBuffer) == 0 {\n\t\tap.streamMu.Unlock()\n\t\treturn\n\t}\n\n\tbufferToFlush := ap.streamMessageBuffer\n\tap.streamMessageBuffer = []shared.StreamMessage{}\n\tap.streamMu.Unlock()\n\n\tif len(bufferToFlush) == 1 {\n\t\tlog.Println(\"ActivePlan.FlushStreamBuffer: flushing single message\")\n\t\tap.Stream(bufferToFlush[0])\n\t} else {\n\t\tlog.Println(\"ActivePlan.FlushStreamBuffer: flushing multi-message\")\n\t\tap.Stream(shared.StreamMessage{\n\t\t\tType:           shared.StreamMessageMulti,\n\t\t\tStreamMessages: bufferToFlush,\n\t\t})\n\t}\n}\n\nconst verboseStreamLogging = false\n\nfunc (ap *ActivePlan) Stream(msg shared.StreamMessage) {\n\tif verboseStreamLogging {\n\t\tlog.Println(\"ActivePlan.Stream:\")\n\t\tlog.Println(msg)\n\t}\n\n\tap.streamMu.Lock()\n\n\tskipBuffer := false\n\tif msg.Type == shared.StreamMessagePromptMissingFile || msg.Type == shared.StreamMessageLoadContext || msg.Type == shared.StreamMessageFinished || msg.Type == shared.StreamMessageError {\n\t\tskipBuffer = true\n\n\t\tlog.Println(\"ActivePlan.Stream: skipping buffer for special message\")\n\t\tlog.Println(spew.Sdump(msg))\n\t}\n\n\t// Special messages bypass buffering\n\tif !skipBuffer {\n\t\tif verboseStreamLogging {\n\t\t\tlog.Println(\"ActivePlan.Stream: time since last message sent:\", time.Since(ap.lastStreamMessageSent))\n\t\t}\n\n\t\tif time.Since(ap.lastStreamMessageSent) < MaxStreamRate {\n\t\t\tif verboseStreamLogging {\n\t\t\t\tlog.Println(\"ActivePlan.Stream: buffering message\")\n\t\t\t}\n\n\t\t\t// Buffer the message\n\t\t\tap.streamMessageBuffer = append(ap.streamMessageBuffer, msg)\n\t\t\tap.streamMu.Unlock()\n\t\t\treturn\n\t\t} else if len(ap.streamMessageBuffer) > 0 {\n\t\t\tif verboseStreamLogging {\n\t\t\t\tlog.Println(\"ActivePlan.Stream: flushing buffer\")\n\t\t\t}\n\n\t\t\t// Need to flush buffer first\n\t\t\tap.streamMessageBuffer = append(ap.streamMessageBuffer, msg)\n\t\t\tbufferToFlush := ap.streamMessageBuffer\n\t\t\tap.streamMessageBuffer = []shared.StreamMessage{}\n\t\t\tap.streamMu.Unlock()\n\n\t\t\tif verboseStreamLogging {\n\t\t\t\tlog.Println(\"ActivePlan.Stream: sending multi-message:\")\n\t\t\t\tlog.Println(bufferToFlush)\n\t\t\t}\n\t\t\t// Send as multi-message\n\t\t\tap.Stream(shared.StreamMessage{\n\t\t\t\tType:           shared.StreamMessageMulti,\n\t\t\t\tStreamMessages: bufferToFlush,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Direct send path\n\tmsgJson, err := json.Marshal(msg)\n\tif err != nil {\n\t\tap.streamMu.Unlock()\n\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"error marshalling stream message: %v\", err))\n\n\t\tap.StreamDoneCh <- &shared.ApiError{\n\t\t\tType:   shared.ApiErrorTypeOther,\n\t\t\tStatus: http.StatusInternalServerError,\n\t\t\tMsg:    \"Error marshalling stream message: \" + err.Error(),\n\t\t}\n\t\treturn\n\t}\n\n\tif skipBuffer && len(ap.streamMessageBuffer) > 0 {\n\t\t// Handle any remaining buffered messages before sending the message\n\t\t// log.Println(\"ActivePlan.Stream: message is a skip buffer type and there are buffered messages\")\n\t\t// log.Println(\"ActivePlan.Stream: flushing remaining buffered messages before skip buffer message is sent\")\n\t\tbufferToFlush := ap.streamMessageBuffer\n\t\tap.streamMessageBuffer = []shared.StreamMessage{}\n\t\tap.streamMu.Unlock()\n\n\t\tlog.Println(\"Flushing buffered messages before finishing\")\n\t\t// log.Println(\"ActivePlan.Stream: sending multi-message:\")\n\t\t// log.Println(bufferToFlush)\n\t\tap.Stream(shared.StreamMessage{\n\t\t\tType:           shared.StreamMessageMulti,\n\t\t\tStreamMessages: bufferToFlush,\n\t\t})\n\t\tlog.Println(\"ActivePlan.Stream: finished flushing buffered messages. waiting 50ms before sending skip buffer type message\")\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\tlog.Println(\"ActivePlan.Stream: sending finish message\")\n\t\tap.Stream(msg) // send the skip buffer type message\n\n\t\tap.streamMu.Lock()\n\t\tnow := time.Now()\n\t\tif now.After(ap.lastStreamMessageSent) {\n\t\t\tap.lastStreamMessageSent = now\n\t\t}\n\n\t\tif msg.Type == shared.StreamMessageFinished {\n\t\t\tap.streamMu.Unlock()\n\t\t\t// wait for the finish message to be sent then send the done signal\n\t\t\tlog.Println(\"ActivePlan.Stream: waiting 50ms before sending done signal\")\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\tlog.Println(\"ActivePlan.Stream: sending done signal\")\n\t\t\tap.StreamDoneCh <- nil\n\t\t\treturn\n\t\t}\n\t}\n\n\tif verboseStreamLogging {\n\t\tlog.Println(\"ActivePlan.Stream: sending direct message\")\n\t\tlog.Println(string(msgJson))\n\t}\n\n\tap.streamCh <- string(msgJson)\n\n\tnow := time.Now()\n\tif now.After(ap.lastStreamMessageSent) {\n\t\tap.lastStreamMessageSent = now\n\t}\n\tap.streamMu.Unlock()\n\n\tif msg.Type == shared.StreamMessageFinished {\n\t\tlog.Println(\"ActivePlan.Stream: waiting 50ms before sending done signal\")\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\tlog.Println(\"ActivePlan.Stream: sending done signal\")\n\t\tap.StreamDoneCh <- nil\n\t}\n}\n\nfunc (ap *ActivePlan) ResetModelCtx() {\n\tap.ModelStreamCtx, ap.CancelModelStreamFn = context.WithCancel(ap.Ctx)\n}\n\nfunc (ap *ActivePlan) BuildFinished() bool {\n\tfor path := range ap.BuildQueuesByPath {\n\t\tif ap.IsBuildingByPath[path] || !ap.PathQueueEmpty(path) {\n\t\t\tlog.Printf(\"BuildFinished - %s - is building %t - path queue not empty %t\\n\", path, ap.IsBuildingByPath[path], !ap.PathQueueEmpty(path))\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (ap *ActivePlan) PathQueueEmpty(path string) bool {\n\t// log.Printf(\"PathQueueEmpty - %s\\n\", path)\n\t// log.Println(spew.Sdump(ap.BuildQueuesByPath[path]))\n\tfor _, build := range ap.BuildQueuesByPath[path] {\n\t\tif !build.BuildFinished() {\n\t\t\t// log.Printf(\"PathQueueEmpty - %s - build not finished\\n\", path)\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (ap *ActivePlan) Subscribe(reqCtx context.Context) (string, chan string) {\n\tap.subscriptionMu.Lock()\n\tdefer ap.subscriptionMu.Unlock()\n\tid := uuid.New().String()\n\n\tplanCtx := ap.Ctx // from the plan\n\n\t// Make a subscription context that will end if EITHER the request ends\n\t// OR the plan’s context ends:\n\tsubCtx, subCancel := context.WithCancel(shutdown.ShutdownCtx)\n\n\t// Use a small goroutine to combine the two cancels:\n\tgo func() {\n\t\tselect {\n\t\tcase <-reqCtx.Done():\n\t\t\t// client disconnected\n\t\tcase <-planCtx.Done():\n\t\t\t// plan ended\n\t\t}\n\t\tsubCancel()\n\t}()\n\n\tsub := newSubscription(subCtx)\n\n\tap.subscriptions[id] = sub\n\treturn id, sub.ch\n}\n\nfunc (ap *ActivePlan) Unsubscribe(id string) {\n\tap.subscriptionMu.Lock()\n\tdefer ap.subscriptionMu.Unlock()\n\n\tsub, ok := ap.subscriptions[id]\n\n\tif ok {\n\t\tsub.cancelFn()\n\t\tsub.cond.Signal()\n\t\tdelete(ap.subscriptions, id)\n\t}\n}\n\nfunc (ap *ActivePlan) NumSubscribers() int {\n\tap.subscriptionMu.Lock()\n\tdefer ap.subscriptionMu.Unlock()\n\treturn len(ap.subscriptions)\n}\n\nfunc (b *ActiveBuild) BuildFinished() bool {\n\treturn b.Success || b.Error != nil\n}\n\nfunc newSubscription(ctx context.Context) *subscription {\n\tctx, cancel := context.WithCancel(ctx)\n\tsub := &subscription{\n\t\tch:           make(chan string),\n\t\tctx:          ctx,\n\t\tcancelFn:     cancel,\n\t\tmessageQueue: make([]string, 0),\n\t}\n\tsub.mu = sync.Mutex{}\n\tsub.cond = sync.NewCond(&sub.mu)\n\tgo sub.processMessages()\n\treturn sub\n}\n\nfunc (sub *subscription) processMessages() {\n\tfor {\n\t\tsub.mu.Lock()\n\t\tfor len(sub.messageQueue) == 0 {\n\t\t\tsub.cond.Wait()           // Automatically unlocks sub.mu and waits; re-locks sub.mu upon waking.\n\t\t\tif sub.ctx.Err() != nil { // Check if context is cancelled after waking up.\n\t\t\t\tsub.mu.Unlock()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// At this point, there is at least one message in the queue\n\t\tmsg := sub.messageQueue[0]\n\t\tsub.messageQueue = sub.messageQueue[1:]\n\t\tsub.mu.Unlock()\n\n\t\tselect {\n\t\tcase <-sub.ctx.Done():\n\t\t\tlog.Println(\"ActivePlan: subscription context done, aborting send\")\n\t\t\treturn\n\t\tcase sub.ch <- msg:\n\t\t\t// Message sent, proceed to next\n\t\t}\n\t}\n}\n\n// Adding a message to the subscription's queue\nfunc (sub *subscription) enqueueMessage(msg string) {\n\t// log.Printf(\"ActivePlan: enqueueing message: %s\\n\", msg)\n\tsub.mu.Lock()\n\tsub.messageQueue = append(sub.messageQueue, msg)\n\tsub.mu.Unlock()\n\tsub.cond.Signal() // Signal the waiting goroutine that a new message is available\n}\n\nfunc (ap *ActivePlan) Finish() {\n\tap.Stream(shared.StreamMessage{\n\t\tType: shared.StreamMessageFinished,\n\t})\n}\n\nfunc (ab *ActiveBuild) IsFileOperation() bool {\n\treturn ab.IsMoveOp || ab.IsRemoveOp || ab.IsResetOp\n}\n"
  },
  {
    "path": "app/server/types/active_plan_pending_builds.go",
    "content": "package types\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc (ap *ActivePlan) PendingBuildsByPath(orgId, userId string, convoMessagesArg []*db.ConvoMessage) (map[string][]*ActiveBuild, error) {\n\tplanDescs, err := db.GetConvoMessageDescriptions(orgId, ap.Id)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting pending build descriptions: %v\", err)\n\t}\n\n\tif !HasPendingBuilds(planDescs) {\n\t\treturn map[string][]*ActiveBuild{}, nil\n\t}\n\n\tvar convoMessages []*db.ConvoMessage\n\tif convoMessagesArg == nil {\n\t\tvar err error\n\t\tconvoMessages, err = db.GetPlanConvo(orgId, ap.Id)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting plan convo: %v\", err)\n\t\t}\n\t} else {\n\t\tconvoMessages = convoMessagesArg\n\t}\n\n\tconvoMessagesById := map[string]*db.ConvoMessage{}\n\tfor _, msg := range convoMessages {\n\t\tconvoMessagesById[msg.Id] = msg\n\t}\n\n\tactiveBuildsByPath := map[string][]*ActiveBuild{}\n\n\tfor _, desc := range planDescs {\n\t\tif (!desc.DidBuild && len(desc.Operations) > 0) || len(desc.BuildPathsInvalidated) > 0 {\n\t\t\tif desc.ConvoMessageId == \"\" {\n\t\t\t\tlog.Printf(\"No convo message ID for description: %v\\n\", desc)\n\t\t\t\treturn nil, fmt.Errorf(\"no convo message ID for description: %v\", desc)\n\t\t\t}\n\n\t\t\tif convoMessagesById[desc.ConvoMessageId] == nil {\n\t\t\t\tlog.Printf(\"No convo message for ID: %s\\n\", desc.ConvoMessageId)\n\t\t\t\treturn nil, fmt.Errorf(\"no convo message for ID: %s\", desc.ConvoMessageId)\n\t\t\t}\n\n\t\t\t// convoMessage := convoMessagesById[desc.ConvoMessageId]\n\n\t\t\t// replyParser := NewReplyParser()\n\t\t\t// replyParser.AddChunk(convoMessage.Message, false)\n\t\t\t// parserRes := replyParser.FinishAndRead()\n\n\t\t\tnumAdded := 0\n\t\t\tfor _, op := range desc.Operations {\n\n\t\t\t\tif desc.DidBuild && !desc.BuildPathsInvalidated[op.Path] {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif activeBuildsByPath[op.Path] == nil {\n\t\t\t\t\tactiveBuildsByPath[op.Path] = []*ActiveBuild{}\n\t\t\t\t}\n\n\t\t\t\tnumTokens := shared.GetNumTokensEstimate(op.Content)\n\n\t\t\t\tactiveBuildsByPath[op.Path] = append(activeBuildsByPath[op.Path], &ActiveBuild{\n\t\t\t\t\tReplyId:           desc.ConvoMessageId,\n\t\t\t\t\tFileContent:       op.Content,\n\t\t\t\t\tFileContentTokens: numTokens,\n\t\t\t\t\tPath:              op.Path,\n\t\t\t\t\tFileDescription:   op.Description,\n\t\t\t\t})\n\t\t\t\tnumAdded++\n\t\t\t}\n\n\t\t}\n\t}\n\n\t// log.Println(\"activeBuildsByPath:\")\n\t// spew.Dump(activeBuildsByPath)\n\n\treturn activeBuildsByPath, nil\n}\n"
  },
  {
    "path": "app/server/types/auth.go",
    "content": "package types\n\nimport (\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n)\n\ntype ServerAuth struct {\n\tAuthToken   *db.AuthToken\n\tUser        *db.User\n\tOrgId       string\n\tPermissions shared.Permissions\n}\n\nfunc (a *ServerAuth) HasPermission(permission shared.Permission) bool {\n\treturn a.Permissions.HasPermission(permission)\n}\n\nfunc (a *ServerAuth) HasPermissionForResource(permission shared.Permission, resourceId string) bool {\n\treturn a.Permissions.HasPermissionForResource(permission, resourceId)\n}\n"
  },
  {
    "path": "app/server/types/convo_message_desc.go",
    "content": "package types\n\nimport (\n\t\"plandex-server/db\"\n\n\tshared \"plandex-shared\"\n)\n\nfunc HasPendingBuilds(planDescs []*db.ConvoMessageDescription) bool {\n\tapiDescs := make([]*shared.ConvoMessageDescription, len(planDescs))\n\tfor i, desc := range planDescs {\n\t\tapiDescs[i] = desc.ToApi()\n\t}\n\n\treturn shared.HasPendingBuilds(apiDescs)\n}\n"
  },
  {
    "path": "app/server/types/exec_status.go",
    "content": "package types\n\ntype ExecStatusResponse struct {\n\tReasoning       string `json:\"reasoning\"`\n\tSubtaskFinished bool   `json:\"subtaskFinished\"`\n}\n"
  },
  {
    "path": "app/server/types/message.go",
    "content": "package types\n\nimport (\n\tshared \"plandex-shared\"\n\t\"time\"\n\n\t\"strings\"\n\n\t\"github.com/sashabaranov/go-openai\"\n)\n\ntype CacheControlType string\n\nconst (\n\tCacheControlTypeEphemeral CacheControlType = \"ephemeral\"\n)\n\ntype CacheControlSpec struct {\n\tType CacheControlType `json:\"type\"`\n}\n\ntype ExtendedChatMessagePart struct {\n\tType         openai.ChatMessagePartType  `json:\"type\"`\n\tText         string                      `json:\"text,omitempty\"`\n\tImageURL     *openai.ChatMessageImageURL `json:\"image_url,omitempty\"`\n\tCacheControl *CacheControlSpec           `json:\"cache_control,omitempty\"`\n}\n\ntype ExtendedChatMessage struct {\n\tRole    string                    `json:\"role\"`\n\tContent []ExtendedChatMessagePart `json:\"content\"`\n}\n\nfunc (msg *ExtendedChatMessage) ToOpenAI() *openai.ChatCompletionMessage {\n\t// If there's only one part and it's text, use simple Content field\n\tif len(msg.Content) == 1 && msg.Content[0].Type == \"text\" {\n\t\treturn &openai.ChatCompletionMessage{\n\t\t\tRole:    msg.Role,\n\t\t\tContent: msg.Content[0].Text,\n\t\t}\n\t}\n\n\t// Otherwise, use MultiContent for multiple parts or non-text content\n\tparts := make([]openai.ChatMessagePart, len(msg.Content))\n\tfor i, part := range msg.Content {\n\t\tparts[i] = openai.ChatMessagePart{\n\t\t\tType:     part.Type,\n\t\t\tText:     part.Text,\n\t\t\tImageURL: part.ImageURL,\n\t\t}\n\t}\n\n\treturn &openai.ChatCompletionMessage{\n\t\tRole:         msg.Role,\n\t\tMultiContent: parts,\n\t}\n}\n\ntype OpenAIPrediction struct {\n\tType    string `json:\"type\"`\n\tContent string `json:\"content\"`\n}\n\ntype ReasoningConfig struct {\n\tEffort    shared.ReasoningEffort `json:\"effort,omitempty\"`     // \"high\" | \"medium\" | \"low\"\n\tMaxTokens int                    `json:\"max_tokens,omitempty\"` // Anthropic-style budget\n\tExclude   bool                   `json:\"exclude,omitempty\"`    // don’t echo reasoning in the response\n}\n\ntype OpenRouterProviderConfig struct {\n\tOrder            []string `json:\"order\"`\n\tAllowFallbacks   bool     `json:\"allow_fallbacks\"`\n\tRequireParamters bool     `json:\"require_paramters\"`\n\tDataCollection   bool     `json:\"data_collection\"`\n\tOnly             []string `json:\"only\"`\n\tIgnore           []string `json:\"ignore\"`\n}\n\ntype ExtendedChatCompletionRequest struct {\n\t// copied from openai.ChatCompletionRequest\n\tModel    shared.ModelName      `json:\"model\"`\n\tMessages []ExtendedChatMessage `json:\"messages\"`\n\t// MaxTokens The maximum number of tokens that can be generated in the chat completion.\n\t// This value can be used to control costs for text generated via API.\n\t// This value is now deprecated in favor of max_completion_tokens, and is not compatible with o1 series models.\n\t// refs: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_tokens\n\tMaxTokens int `json:\"max_tokens,omitempty\"`\n\t// MaxCompletionTokens An upper bound for the number of tokens that can be generated for a completion,\n\t// including visible output tokens and reasoning tokens https://platform.openai.com/docs/guides/reasoning\n\tMaxCompletionTokens int                                  `json:\"max_completion_tokens,omitempty\"`\n\tTemperature         float32                              `json:\"temperature,omitempty\"`\n\tTopP                float32                              `json:\"top_p,omitempty\"`\n\tN                   int                                  `json:\"n,omitempty\"`\n\tStream              bool                                 `json:\"stream,omitempty\"`\n\tStop                []string                             `json:\"stop,omitempty\"`\n\tPresencePenalty     float32                              `json:\"presence_penalty,omitempty\"`\n\tResponseFormat      *openai.ChatCompletionResponseFormat `json:\"response_format,omitempty\"`\n\tSeed                *int                                 `json:\"seed,omitempty\"`\n\tFrequencyPenalty    float32                              `json:\"frequency_penalty,omitempty\"`\n\t// LogitBias is must be a token id string (specified by their token ID in the tokenizer), not a word string.\n\t// incorrect: `\"logit_bias\":{\"You\": 6}`, correct: `\"logit_bias\":{\"1639\": 6}`\n\t// refs: https://platform.openai.com/docs/api-reference/chat/create#chat/create-logit_bias\n\tLogitBias map[string]int `json:\"logit_bias,omitempty\"`\n\t// LogProbs indicates whether to return log probabilities of the output tokens or not.\n\t// If true, returns the log probabilities of each output token returned in the content of message.\n\t// This option is currently not available on the gpt-4-vision-preview model.\n\tLogProbs bool `json:\"logprobs,omitempty\"`\n\t// TopLogProbs is an integer between 0 and 5 specifying the number of most likely tokens to return at each\n\t// token position, each with an associated log probability.\n\t// logprobs must be set to true if this parameter is used.\n\tTopLogProbs int    `json:\"top_logprobs,omitempty\"`\n\tUser        string `json:\"user,omitempty\"`\n\t// Deprecated: use Tools instead.\n\tFunctions []openai.FunctionDefinition `json:\"functions,omitempty\"`\n\t// Deprecated: use ToolChoice instead.\n\tFunctionCall any           `json:\"function_call,omitempty\"`\n\tTools        []openai.Tool `json:\"tools,omitempty\"`\n\t// This can be either a string or an ToolChoice object.\n\tToolChoice any `json:\"tool_choice,omitempty\"`\n\t// Options for streaming response. Only set this when you set stream: true.\n\tStreamOptions *openai.StreamOptions `json:\"stream_options,omitempty\"`\n\t// Disable the default behavior of parallel tool calls by setting it: false.\n\tParallelToolCalls any `json:\"parallel_tool_calls,omitempty\"`\n\t// Store can be set to true to store the output of this completion request for use in distillations and evals.\n\t// https://platform.openai.com/docs/api-reference/chat/create#chat-create-store\n\tStore bool `json:\"store,omitempty\"`\n\t// Metadata to store with the completion.\n\tMetadata map[string]string `json:\"metadata,omitempty\"`\n\n\tPrediction *OpenAIPrediction         `json:\"prediction,omitempty\"`\n\tProvider   *OpenRouterProviderConfig `json:\"provider,omitempty\"`\n\n\t// LiteLLM api base\n\tLiteLLMApiBase           string `json:\"api_base,omitempty\"`\n\tLiteLLMBaseUrl           string `json:\"base_url,omitempty\"`\n\tLiteLLMCustomLLMProvider string `json:\"custom_llm_provider,omitempty\"`\n\n\t// Openrouter/LiteLLM reasoning\n\tReasoningConfig *ReasoningConfig `json:\"reasoning,omitempty\"`\n\n\t// Headers that pass through to LiteLLM proxy\n\tExtraHeaders map[string]string `json:\"extra_headers,omitempty\"`\n\n\t// Vertex request vars\n\tVertexProject     string `json:\"vertex_project,omitempty\"`\n\tVertexLocation    string `json:\"vertex_location,omitempty\"`\n\tVertexCredentials string `json:\"vertex_credentials,omitempty\"`\n\n\t// Azure OpenAI request vars\n\tAzureApiVersion      string                 `json:\"api_version,omitempty\"`\n\tAzureReasoningEffort shared.ReasoningEffort `json:\"reasoning_effort,omitempty\"`\n\n\t// AWS Bedrock request vars\n\tBedrockAccessKeyId         string `json:\"aws_access_key_id,omitempty\"`\n\tBedrockSecretAccessKey     string `json:\"aws_secret_access_key,omitempty\"`\n\tBedrockSessionToken        string `json:\"aws_session_token,omitempty\"`\n\tBedrockRegion              string `json:\"aws_region_name,omitempty\"`\n\tBedrockInferenceProfileArn string `json:\"aws_inference_profile_arn,omitempty\"`\n}\n\n// for properties that OpenAI direct api calls support but aren't included in https://github.com/sashabaranov/go-openai\ntype ExtendedOpenAIChatCompletionRequest struct {\n\topenai.ChatCompletionRequest\n\tPrediction      *OpenAIPrediction       `json:\"prediction,omitempty\"`\n\tReasoningEffort *shared.ReasoningEffort `json:\"reasoning_effort,omitempty\"`\n}\n\n// strips out properties that direct OpenAI api calls don't support\nfunc (req *ExtendedChatCompletionRequest) ToOpenAI() *ExtendedOpenAIChatCompletionRequest {\n\topenaiMessages := make([]openai.ChatCompletionMessage, len(req.Messages))\n\tfor i, msg := range req.Messages {\n\t\topenaiMessages[i] = *msg.ToOpenAI()\n\t}\n\n\tvar reasoningEffort *shared.ReasoningEffort\n\tif req.ReasoningConfig != nil && req.ReasoningConfig.Effort != \"\" {\n\t\treasoningEffort = &req.ReasoningConfig.Effort\n\t}\n\n\treturn &ExtendedOpenAIChatCompletionRequest{\n\t\tChatCompletionRequest: openai.ChatCompletionRequest{\n\t\t\tModel:               string(req.Model),\n\t\t\tMessages:            openaiMessages,\n\t\t\tMaxTokens:           req.MaxTokens,\n\t\t\tMaxCompletionTokens: req.MaxCompletionTokens,\n\t\t\tTemperature:         req.Temperature,\n\t\t\tTopP:                req.TopP,\n\t\t\tN:                   req.N,\n\t\t\tStream:              req.Stream,\n\t\t\tStop:                req.Stop,\n\t\t\tPresencePenalty:     req.PresencePenalty,\n\t\t\tResponseFormat:      req.ResponseFormat,\n\t\t\tSeed:                req.Seed,\n\t\t\tFrequencyPenalty:    req.FrequencyPenalty,\n\t\t\tLogitBias:           req.LogitBias,\n\t\t\tLogProbs:            req.LogProbs,\n\t\t\tTopLogProbs:         req.TopLogProbs,\n\t\t\tUser:                req.User,\n\t\t\tFunctions:           req.Functions,\n\t\t\tFunctionCall:        req.FunctionCall,\n\t\t\tTools:               req.Tools,\n\t\t\tToolChoice:          req.ToolChoice,\n\t\t\tStreamOptions:       req.StreamOptions,\n\t\t\tParallelToolCalls:   req.ParallelToolCalls,\n\t\t\tStore:               req.Store,\n\t\t\tMetadata:            req.Metadata,\n\t\t},\n\t\tPrediction:      req.Prediction,\n\t\tReasoningEffort: reasoningEffort,\n\t}\n}\n\ntype ExtendedChatCompletionStreamChoiceDelta struct {\n\tContent      string               `json:\"content,omitempty\"`\n\tReasoning    string               `json:\"reasoning,omitempty\"`\n\tRole         string               `json:\"role,omitempty\"`\n\tFunctionCall *openai.FunctionCall `json:\"function_call,omitempty\"`\n\tToolCalls    []openai.ToolCall    `json:\"tool_calls,omitempty\"`\n\tRefusal      string               `json:\"refusal,omitempty\"`\n}\n\ntype ExtendedChatCompletionStreamChoice struct {\n\tIndex                int                                        `json:\"index\"`\n\tDelta                ExtendedChatCompletionStreamChoiceDelta    `json:\"delta\"`\n\tLogprobs             *openai.ChatCompletionStreamChoiceLogprobs `json:\"logprobs,omitempty\"`\n\tFinishReason         openai.FinishReason                        `json:\"finish_reason\"`\n\tContentFilterResults openai.ContentFilterResults                `json:\"content_filter_results,omitempty\"`\n}\n\ntype ExtendedChatCompletionStreamError struct {\n\tMessage  string `json:\"message\"`\n\tCode     int    `json:\"code\"`\n\tMetadata struct {\n\t\tHeaders      map[string]string `json:\"headers,omitempty\"`\n\t\tProviderName string            `json:\"provider_name,omitempty\"`\n\t} `json:\"metadata,omitempty\"`\n\tUserId string `json:\"user_id,omitempty\"`\n}\ntype ExtendedChatCompletionStreamResponse struct {\n\tID                  string                               `json:\"id\"`\n\tObject              string                               `json:\"object\"`\n\tCreated             int64                                `json:\"created\"`\n\tModel               string                               `json:\"model\"`\n\tChoices             []ExtendedChatCompletionStreamChoice `json:\"choices\"`\n\tSystemFingerprint   string                               `json:\"system_fingerprint\"`\n\tPromptAnnotations   []openai.PromptAnnotation            `json:\"prompt_annotations,omitempty\"`\n\tPromptFilterResults []openai.PromptFilterResult          `json:\"prompt_filter_results,omitempty\"`\n\t// An optional field that will only be present when you set stream_options: {\"include_usage\": true} in your request.\n\t// When present, it contains a null value except for the last chunk which contains the token usage statistics\n\t// for the entire request.\n\tUsage *openai.Usage                      `json:\"usage,omitempty\"`\n\tError *ExtendedChatCompletionStreamError `json:\"error,omitempty\"`\n}\n\n// ModelResponse holds both the accumulated content and final usage information from a streaming completion request\ntype ModelResponse struct {\n\tContent      string        `json:\"content\"`\n\tUsage        *openai.Usage `json:\"usage,omitempty\"`\n\tStopped      bool          `json:\"stopped,omitempty\"`\n\tError        string        `json:\"error,omitempty\"`\n\tGenerationId string        `json:\"generation_id,omitempty\"`\n\tFirstTokenAt time.Time     `json:\"first_token_at,omitempty\"`\n}\n\n// StreamCompletionAccumulator accumulates content and tracks usage from streaming chunks\ntype StreamCompletionAccumulator struct {\n\tcontent      strings.Builder\n\tusage        *openai.Usage\n\tgenerationId string\n\tfirstTokenAt time.Time\n}\n\n// NewStreamCompletionAccumulator creates a new StreamCompletionAccumulator\nfunc NewStreamCompletionAccumulator() *StreamCompletionAccumulator {\n\treturn &StreamCompletionAccumulator{\n\t\tcontent: strings.Builder{},\n\t\tusage:   nil,\n\t}\n}\n\n// AddContent appends new content from a streaming chunk\nfunc (a *StreamCompletionAccumulator) AddContent(content string) {\n\ta.content.WriteString(content)\n}\n\n// SetUsage sets the usage information, typically from the final chunk\nfunc (a *StreamCompletionAccumulator) SetUsage(usage *openai.Usage) {\n\ta.usage = usage\n}\n\nfunc (a *StreamCompletionAccumulator) SetGenerationId(generationId string) {\n\ta.generationId = generationId\n}\n\nfunc (a *StreamCompletionAccumulator) SetFirstTokenAt(firstTokenAt time.Time) {\n\ta.firstTokenAt = firstTokenAt\n}\n\nfunc (a *StreamCompletionAccumulator) Content() string {\n\treturn a.content.String()\n}\n\n// Result creates a StreamCompletionResult from the accumulated content and usage\nfunc (a *StreamCompletionAccumulator) Result(stopped bool, err error) *ModelResponse {\n\terrStr := \"\"\n\tif err != nil {\n\t\terrStr = err.Error()\n\t}\n\n\treturn &ModelResponse{\n\t\tContent:      a.content.String(),\n\t\tUsage:        a.usage,\n\t\tStopped:      stopped,\n\t\tError:        errStr,\n\t\tGenerationId: a.generationId,\n\t\tFirstTokenAt: a.firstTokenAt,\n\t}\n}\n"
  },
  {
    "path": "app/server/types/model.go",
    "content": "package types\n\nimport (\n\tshared \"plandex-shared\"\n)\n\ntype ChangesWithLineNums struct {\n\tComments []struct {\n\t\tTxt       string `json:\"txt\"`\n\t\tReference bool   `json:\"reference\"`\n\t}\n\tChanges []*shared.StreamedChangeWithLineNums `json:\"changes\"`\n}\n"
  },
  {
    "path": "app/server/types/reply.go",
    "content": "package types\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\tshared \"plandex-shared\"\n)\n\nconst verboseLogging = false\n\ntype ReplyParserRes struct {\n\tMaybeFilePath   string\n\tCurrentFilePath string\n\tIsInMoveBlock   bool\n\tIsInRemoveBlock bool\n\tIsInResetBlock  bool\n\tOperations      []*shared.Operation\n\tTotalTokens     int\n}\n\ntype ReplyParser struct {\n\tlines                     []string\n\tcurrentFileLines          []string\n\tlineIndex                 int\n\tmaybeFilePath             string\n\tcurrentFilePath           string\n\tcurrentDescriptionLines   []string\n\tcurrentDescriptionLineIdx int\n\tnumTokens                 int\n\toperations                []*shared.Operation\n\tcurrentFileOperation      *shared.Operation\n\tpendingOperations         []*shared.Operation\n\tpendingPaths              map[string]bool\n\tisInMoveBlock             bool\n\tisInRemoveBlock           bool\n\tisInResetBlock            bool\n}\n\nfunc NewReplyParser() *ReplyParser {\n\tinfo := &ReplyParser{\n\t\tlines:                   []string{\"\"},\n\t\tcurrentFileLines:        []string{},\n\t\tcurrentDescriptionLines: []string{\"\"},\n\t\toperations:              []*shared.Operation{},\n\t\tpendingPaths:            map[string]bool{},\n\t}\n\treturn info\n}\n\nfunc (r *ReplyParser) AddChunk(chunk string, addToTotal bool) {\n\tif verboseLogging {\n\t\tlog.Println(\"Adding chunk:\", strconv.Quote(chunk)) // Logging the chunk that's being processed\n\t}\n\n\thasNewLine := false\n\tnextChunk := \"\"\n\n\tif addToTotal {\n\t\tr.numTokens++\n\t\t// log.Println(\"Total tokens:\", r.numTokens)\n\t\t// log.Println(\"Tokens by file path:\", r.numTokensByFile)\n\t}\n\n\tif r.currentFilePath != \"\" && r.currentFileOperation != nil {\n\t\tr.currentFileOperation.NumTokens++\n\t}\n\n\tif chunk == \"\\n\" {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"Chunk is \\\\n, adding new line\")\n\t\t}\n\t\tr.lines = append(r.lines, \"\")\n\t\thasNewLine = true\n\t\tr.lineIndex++\n\n\t\tif r.currentFileOperation == nil {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Current file operation is empty--adding new description line...\")\n\t\t\t}\n\t\t\tr.currentDescriptionLines = append(r.currentDescriptionLines, \"\")\n\t\t\tr.currentDescriptionLineIdx++\n\t\t}\n\n\t} else {\n\t\tchunkLines := strings.Split(chunk, \"\\n\")\n\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"Chunk lines:\", len(chunkLines))\n\t\t}\n\n\t\tcurrentLine := r.lines[r.lineIndex]\n\t\tcurrentLine += chunkLines[0]\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"Current line:\", strconv.Quote(currentLine))\n\t\t}\n\t\tr.lines[r.lineIndex] = currentLine\n\n\t\tif r.currentFileOperation == nil {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Current file operation is empty--adding to current description...\")\n\t\t\t\tlog.Println(\"Current description lines:\", r.currentDescriptionLines)\n\t\t\t\tlog.Printf(\"Current description line index: %d\\n\", r.currentDescriptionLineIdx)\n\t\t\t}\n\n\t\t\tcurrentDescLine := r.currentDescriptionLines[r.currentDescriptionLineIdx]\n\t\t\tcurrentDescLine += chunkLines[0]\n\t\t\tr.currentDescriptionLines[r.currentDescriptionLineIdx] = currentDescLine\n\t\t}\n\n\t\tif len(chunkLines) > 1 {\n\t\t\tr.lines = append(r.lines, chunkLines[1])\n\t\t\tr.lineIndex++\n\n\t\t\tif r.currentFileOperation == nil {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"Current file operation is empty--adding to current description...\")\n\t\t\t\t\tlog.Println(\"Current description lines:\", r.currentDescriptionLines)\n\t\t\t\t\tlog.Printf(\"Current description line index: %d\\n\", r.currentDescriptionLineIdx)\n\t\t\t\t}\n\n\t\t\t\tr.currentDescriptionLines = append(r.currentDescriptionLines, chunkLines[1])\n\t\t\t\tr.currentDescriptionLineIdx++\n\t\t\t}\n\n\t\t\thasNewLine = true\n\n\t\t\tif len(chunkLines) > 2 {\n\t\t\t\ttail := chunkLines[2:]\n\t\t\t\tnextChunk = \"\\n\" + strings.Join(tail, \"\\n\")\n\t\t\t\tdefer func() {\n\t\t\t\t\tif verboseLogging {\n\t\t\t\t\t\tlog.Println(\"Recursive add next queued chunk:\", strconv.Quote(nextChunk))\n\t\t\t\t\t}\n\t\t\t\t\tr.AddChunk(nextChunk, false)\n\t\t\t\t}()\n\t\t\t}\n\t\t}\n\t}\n\n\tif r.lineIndex == 0 || !hasNewLine {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"No new line detected--returning\")\n\t\t}\n\t\treturn\n\t}\n\n\tprevFullLine := r.lines[r.lineIndex-1]\n\tif verboseLogging {\n\t\tlog.Println(\"Previous full line:\", strconv.Quote(prevFullLine)) // Logging the full line that's being checked\n\t}\n\n\tprevFullLineTrimmed := strings.TrimSpace(prevFullLine)\n\n\tsetCurrentFile := func(path string, noLabel bool) {\n\t\tr.currentFilePath = path\n\t\tr.currentFileOperation = &shared.Operation{\n\t\t\tType: shared.OperationTypeFile,\n\t\t\tPath: path,\n\t\t}\n\t\tr.maybeFilePath = \"\"\n\t\tr.currentFileLines = []string{}\n\n\t\tvar fileDescription string\n\t\tskipNumLines := 4\n\t\tif noLabel {\n\t\t\tskipNumLines = 2\n\t\t}\n\t\tif len(r.currentDescriptionLines) > skipNumLines {\n\t\t\tfileDescription = strings.TrimSpace(strings.Join(r.currentDescriptionLines[0:len(r.currentDescriptionLines)-skipNumLines], \"\\n\"))\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"File description:\", fileDescription)\n\t\t\t}\n\t\t\tif fileDescription != \"\" {\n\t\t\t\tr.currentFileOperation.Description = fileDescription\n\t\t\t}\n\t\t} else {\n\t\t\tr.currentFileOperation.Description = \"\"\n\t\t}\n\n\t\tr.currentDescriptionLines = []string{\"\"}\n\t\tr.currentDescriptionLineIdx = 0\n\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"Confirmed file path:\", r.currentFilePath) // Logging the confirmed file path\n\t\t}\n\n\t}\n\n\tif r.maybeFilePath != \"\" && !r.isInMoveBlock && !r.isInRemoveBlock && !r.isInResetBlock {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"Maybe file path is:\", r.maybeFilePath) // Logging the maybeFilePath\n\t\t}\n\t\tif strings.HasPrefix(prevFullLineTrimmed, \"<PlandexBlock\") {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Found opening tag--confirming file path...\") // Logging the confirmed file path\n\t\t\t}\n\n\t\t\tsetCurrentFile(r.maybeFilePath, false)\n\t\t\treturn\n\t\t} else if prevFullLineTrimmed != \"\" {\n\t\t\t// turns out previous maybeFilePath was not a file path since there's a non-empty line before finding opening ticks\n\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Previous maybeFilePath was not a file path--resetting maybeFilePath\")\n\t\t\t}\n\n\t\t\tr.maybeFilePath = \"\"\n\t\t}\n\t}\n\n\tif r.currentFilePath == \"\" && !r.isInMoveBlock && !r.isInRemoveBlock && !r.isInResetBlock {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"Current file path is empty--checking for possible file path...\")\n\t\t}\n\n\t\tif LineHasXmlPath(prevFullLineTrimmed) {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Line has XML-style PlandexBlock tag\")\n\t\t\t}\n\t\t\tpath := extractFilePath(prevFullLineTrimmed)\n\t\t\tif path != \"\" {\n\t\t\t\tsetCurrentFile(path, true)\n\t\t\t}\n\t\t}\n\n\t\tvar gotPath string\n\t\tif LineMaybeHasFilePath(prevFullLineTrimmed) {\n\t\t\tgotPath = extractFilePath(prevFullLineTrimmed)\n\t\t} else if prevFullLineTrimmed == \"### Move Files\" {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Found move block\")\n\t\t\t}\n\t\t\tr.isInMoveBlock = true\n\t\t} else if prevFullLineTrimmed == \"### Remove Files\" {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Found remove block\")\n\t\t\t}\n\t\t\tr.isInRemoveBlock = true\n\t\t} else if prevFullLineTrimmed == \"### Reset Changes\" {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Found reset block\")\n\t\t\t}\n\t\t\tr.isInResetBlock = true\n\t\t}\n\n\t\tif gotPath != \"\" {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Detected possible file path:\", gotPath) // Logging the possible file path\n\t\t\t}\n\t\t\tr.maybeFilePath = gotPath\n\t\t}\n\t} else if r.currentFilePath != \"\" {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"Current file path is not empty--adding to current file...\")\n\t\t}\n\t\tif prevFullLineTrimmed == \"</PlandexBlock>\" {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Found closing tag--adding file to files and resetting current file...\")\n\t\t\t}\n\t\t\tr.operations = append(r.operations, r.currentFileOperation)\n\t\t\tr.currentFilePath = \"\"\n\t\t\tr.currentFileOperation = nil\n\n\t\t} else {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Adding tokens to current file...\") // Logging token addition\n\t\t\t}\n\n\t\t\tr.currentFileOperation.Content += prevFullLine + \"\\n\"\n\t\t\tr.currentFileLines = append(r.currentFileLines, prevFullLine)\n\n\t\t}\n\t} else if r.isInMoveBlock || r.isInRemoveBlock || r.isInResetBlock {\n\t\tif verboseLogging {\n\t\t\tlog.Println(\"In move, remove, or reset block\")\n\t\t}\n\t\tif prevFullLineTrimmed == \"<EndPlandexFileOps/>\" {\n\t\t\tif verboseLogging {\n\t\t\t\tlog.Println(\"Found closing tag--adding operations to operations and resetting pending operations...\")\n\t\t\t}\n\t\t\tr.isInMoveBlock = false\n\t\t\tr.isInRemoveBlock = false\n\t\t\tr.isInResetBlock = false\n\t\t\tr.operations = append(r.operations, r.pendingOperations...)\n\t\t\tr.pendingOperations = []*shared.Operation{}\n\t\t\tr.pendingPaths = map[string]bool{}\n\t\t} else if r.isInMoveBlock {\n\t\t\top := extractMoveFile(prevFullLineTrimmed)\n\t\t\tif op != nil && !r.pendingPaths[op.Path] {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"Found move operation\")\n\t\t\t\t}\n\t\t\t\tr.pendingOperations = append(r.pendingOperations, op)\n\t\t\t\tr.pendingPaths[op.Path] = true\n\t\t\t}\n\t\t} else if r.isInRemoveBlock {\n\t\t\top := extractRemoveOrResetFile(shared.OperationTypeRemove, prevFullLineTrimmed)\n\t\t\tif op != nil && !r.pendingPaths[op.Path] {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"Found remove operation\")\n\t\t\t\t}\n\t\t\t\tr.pendingOperations = append(r.pendingOperations, op)\n\t\t\t\tr.pendingPaths[op.Path] = true\n\t\t\t}\n\t\t} else if r.isInResetBlock {\n\t\t\top := extractRemoveOrResetFile(shared.OperationTypeReset, prevFullLineTrimmed)\n\t\t\tif op != nil && !r.pendingPaths[op.Path] {\n\t\t\t\tif verboseLogging {\n\t\t\t\t\tlog.Println(\"Found reset operation\")\n\t\t\t\t}\n\t\t\t\tr.pendingOperations = append(r.pendingOperations, op)\n\t\t\t\tr.pendingPaths[op.Path] = true\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (r *ReplyParser) Read() ReplyParserRes {\n\treturn ReplyParserRes{\n\t\tMaybeFilePath:   r.maybeFilePath,\n\t\tCurrentFilePath: r.currentFilePath,\n\t\tOperations:      r.operations,\n\t\tIsInMoveBlock:   r.isInMoveBlock,\n\t\tIsInRemoveBlock: r.isInRemoveBlock,\n\t\tIsInResetBlock:  r.isInResetBlock,\n\t\tTotalTokens:     r.numTokens,\n\t}\n}\n\nfunc (r *ReplyParser) FinishAndRead() ReplyParserRes {\n\tr.AddChunk(\"\\n\", false)\n\treturn r.Read()\n}\n\nfunc (r *ReplyParser) GetReplyBeforeCurrentPath() string {\n\treturn r.GetReplyBeforePath(r.currentFilePath)\n}\n\nfunc (r *ReplyParser) GetReplyBeforePath(path string) string {\n\tif path == \"\" {\n\t\treturn strings.Join(r.lines, \"\\n\")\n\t}\n\n\tvar idx int\n\tfor i := len(r.lines) - 1; i >= 0; i-- {\n\t\tline := r.lines[i]\n\t\tif LineMaybeHasFilePath(line) && path == extractFilePath(line) {\n\t\t\tidx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn strings.Join(r.lines[:idx], \"\\n\")\n}\n\nfunc (r *ReplyParser) GetReplyForMissingFile() string {\n\tpath := r.currentFilePath\n\n\tvar idx int\n\tfor i := len(r.lines) - 1; i >= 0; i-- {\n\t\tline := r.lines[i]\n\t\tif LineMaybeHasFilePath(line) && path == extractFilePath(line) {\n\t\t\tidx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif idx == -1 {\n\t\treturn strings.Join(r.lines, \"\\n\")\n\t}\n\n\tidx = idx + 2\n\n\tif idx > len(r.lines)-1 {\n\t\treturn strings.Join(r.lines, \"\\n\")\n\t}\n\n\treturn strings.Join(r.lines[:idx], \"\\n\") + \"\\n\"\n}\n\nfunc (r *ReplyParserRes) FileOperationBlockOpen() bool {\n\treturn r.IsInMoveBlock || r.IsInRemoveBlock || r.IsInResetBlock\n}\n\nfunc LineHasXmlPath(line string) bool {\n\treturn strings.HasPrefix(line, \"<PlandexBlock\") && strings.Contains(line, `path=\"`)\n}\n\nfunc LineMaybeHasFilePath(line string) bool {\n\tcouldBe := (strings.HasPrefix(line, \"-\")) || strings.HasPrefix(line, \"-file:\") || strings.HasPrefix(line, \"- file:\") || (strings.HasPrefix(line, \"**\") && strings.HasSuffix(line, \"**\")) || (strings.HasPrefix(line, \"#\") && strings.HasSuffix(line, \":\"))\n\n\tif couldBe {\n\t\textracted := extractFilePath(line)\n\n\t\textSplit := strings.Split(extracted, \".\")\n\t\thasExt := len(extSplit) > 1 && !strings.Contains(extSplit[len(extSplit)-1], \" \")\n\t\thasFileSep := strings.Contains(extracted, string(os.PathSeparator))\n\t\thasSpaces := strings.Contains(extracted, \" \")\n\n\t\treturn !(!hasExt && !hasFileSep && hasSpaces)\n\t}\n\n\treturn couldBe\n}\n\nvar re = regexp.MustCompile(`path=\"([^\"]+)\"`)\n\nfunc extractFilePath(line string) string {\n\t// Handle XML-style PlandexBlock tag\n\tif strings.HasPrefix(line, \"<PlandexBlock\") {\n\t\tmatch := re.FindStringSubmatch(line)\n\t\tif len(match) > 1 {\n\t\t\treturn match[1]\n\t\t}\n\t\treturn \"\"\n\t}\n\n\tp := strings.ReplaceAll(line, \"**\", \"\")\n\tp = strings.ReplaceAll(p, \"`\", \"\")\n\tp = strings.ReplaceAll(p, \"'\", \"\")\n\tp = strings.ReplaceAll(p, `\"`, \"\")\n\tp = strings.TrimPrefix(p, \"-\")\n\tp = strings.TrimPrefix(p, \"####\")\n\tp = strings.TrimPrefix(p, \"###\")\n\tp = strings.TrimPrefix(p, \"##\")\n\tp = strings.TrimPrefix(p, \"#\")\n\tp = strings.TrimSpace(p)\n\tp = strings.TrimPrefix(p, \"file:\")\n\tp = strings.TrimPrefix(p, \"file path:\")\n\tp = strings.TrimPrefix(p, \"filepath:\")\n\tp = strings.TrimPrefix(p, \"File path:\")\n\tp = strings.TrimPrefix(p, \"File Path:\")\n\tp = strings.TrimSuffix(p, \":\")\n\tp = strings.TrimSpace(p)\n\n\t// split := strings.Split(p, \" \")\n\t// if len(split) > 1 {\n\t// \tp = split[0]\n\t// }\n\n\tsplit := strings.Split(p, \": \")\n\tif len(split) > 1 {\n\t\tp = split[len(split)-1]\n\t}\n\n\tsplit = strings.Split(p, \" (\")\n\tif len(split) > 1 {\n\t\tp = split[0]\n\t}\n\n\treturn p\n}\n\nfunc extractMoveFile(line string) *shared.Operation {\n\tline = strings.TrimSpace(line)\n\tif !strings.HasPrefix(line, \"-\") {\n\t\treturn nil\n\t}\n\n\t// Remove the leading dash and trim\n\tline = strings.TrimPrefix(line, \"-\")\n\tline = strings.TrimSpace(line)\n\n\tparts := strings.Split(line, \"→\")\n\tif len(parts) != 2 {\n\t\treturn nil\n\t}\n\n\tsrc := strings.TrimSpace(parts[0])\n\tdst := strings.TrimSpace(parts[1])\n\n\t// Remove backticks\n\tsrc = strings.Trim(src, \"`\")\n\tdst = strings.Trim(dst, \"`\")\n\n\treturn &shared.Operation{\n\t\tType:        shared.OperationTypeMove,\n\t\tPath:        src,\n\t\tDestination: dst,\n\t}\n}\n\nfunc extractRemoveOrResetFile(opType shared.OperationType, line string) *shared.Operation {\n\tline = strings.TrimSpace(line)\n\tif !strings.HasPrefix(line, \"-\") {\n\t\treturn nil\n\t}\n\n\t// Remove the leading dash and trim\n\tline = strings.TrimPrefix(line, \"-\")\n\tline = strings.TrimSpace(line)\n\n\tpath := strings.Trim(line, \"`\")\n\n\treturn &shared.Operation{\n\t\tType: opType,\n\t\tPath: path,\n\t}\n}\n"
  },
  {
    "path": "app/server/types/reply_test.go",
    "content": "package types\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\tshared \"plandex-shared\"\n)\n\ntype TestExample struct {\n\tOnly       bool\n\tOperations []shared.Operation\n}\n\n// These aren't the real number of tokens\n// We're just splitting the file into chunks of 5 characters to simulate tokens\nvar examples = []TestExample{\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"cmd/apply.go\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"cmd/checkout.go\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"cmd/context_rm.go\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"cmd/context_update.go\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"cmd/context_rm.go\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"cmd/context_update.go\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"server/types/section.go\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"shared/types.go\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"cli/lib/conversation.go\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"server/model/proposal/create.go\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"file_map/map.go\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"Makefile\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"_apply.sh\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType:        shared.OperationTypeMove,\n\t\t\t\tPath:        \"src/game.c\",\n\t\t\t\tDestination: \"src/game/game.c\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:        shared.OperationTypeMove,\n\t\t\t\tPath:        \"src/game.h\",\n\t\t\t\tDestination: \"src/game/game.h\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeRemove,\n\t\t\t\tPath: \"src/README.md\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeReset,\n\t\t\t\tPath: \"Makefile\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"Makefile\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tOnly: true,\n\t\tOperations: []shared.Operation{\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"server/model/prompts/describe.go\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType: shared.OperationTypeFile,\n\t\t\t\tPath: \"server/model/plan/commit_msg.go\",\n\t\t\t\tDescription: `Now let's update the commit message handling in commit_msg.go:\n\n**Updating ` + \"`server/model/plan/commit_msg.go`\" + `:** I'll update the genPlanDescription method to handle XML output instead of JSON.`,\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc TestReplyParser(t *testing.T) {\n\tonly := map[int]bool{}\n\tfor i, example := range examples {\n\t\tif example.Only {\n\t\t\tonly[i] = true\n\t\t}\n\t}\n\n\tfor i, example := range examples {\n\n\t\tif len(only) > 0 && !only[i] {\n\t\t\tcontinue\n\t\t}\n\n\t\tt.Run(fmt.Sprintf(\"Example_%d\", i+1), func(t *testing.T) {\n\t\t\tfilePath := fmt.Sprintf(\"reply_test_examples/%d.md\", i+1)\n\t\t\tfmt.Println(filePath)\n\n\t\t\tbytes, err := os.ReadFile(filePath)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tcontent := string(bytes)\n\t\t\ttokenSize := 5\n\n\t\t\tparser := NewReplyParser()\n\n\t\t\tfor i := 0; i < len(content); {\n\t\t\t\tend := i + tokenSize\n\t\t\t\tif end > len(content) {\n\t\t\t\t\tend = len(content)\n\t\t\t\t}\n\t\t\t\tchunk := content[i:end]\n\t\t\t\tparser.AddChunk(chunk, true)\n\t\t\t\ti = end\n\t\t\t}\n\n\t\t\tres := parser.FinishAndRead()\n\n\t\t\toperations := res.Operations\n\n\t\t\tif len(operations) != len(example.Operations) {\n\t\t\t\tt.Errorf(\"Example %d: Expected %d operations, got %d\",\n\t\t\t\t\ti+1, len(example.Operations), len(operations))\n\t\t\t}\n\n\t\t\tfor j, operation := range operations {\n\t\t\t\tif operation.Name() != example.Operations[j].Name() {\n\t\t\t\t\tt.Errorf(\"Example %d: Expected operation %s, got %s\",\n\t\t\t\t\t\ti+1, example.Operations[j].Name(), operation.Name())\n\t\t\t\t}\n\n\t\t\t\tif example.Operations[j].Description != \"\" {\n\t\t\t\t\tif operation.Description != example.Operations[j].Description {\n\t\t\t\t\t\tt.Errorf(\"Example %d: Expected description %s, got %s\",\n\t\t\t\t\t\t\ti+1, strconv.Quote(example.Operations[j].Description), strconv.Quote(operation.Description))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/server/types/reply_test_examples/1.md",
    "content": "Thank you for the update. In order to handle errors from the apply command execution, we need to modify the 'checkout' command and the 'apply' command and function.\n\nFollow the steps below:\n\n1. Modify the 'apply' function to return an error.\n2. Change 'apply' command invocation in 'checkout' function to handle the returned error.\n\nThe code changes are as follows:\n\n- file: cmd/apply.go\n\n<PlandexBlock lang=\"go\" path=\"cmd/apply.go\">\npackage cmd\n\n// ... other imports ...\n\nfunc init() {\n\tRootCmd.AddCommand(applyCmd)\n}\n\nvar applyCmd = &cobra.Command{\n\tUse:   \"apply [name]\",\n\tShort: \"Apply a plan to the project\",\n\tArgs:  cobra.MaximumNArgs(1),\n\tRunE:  apply,   // We use RunE instead of Run, to allow us to return an error from the apply function\n}\n\nfunc apply(cmd *cobra.Command, args []string) error {\n\n\t// ... rest of the code ...\n\n\tif err != nil {\n\t\t// Return error to the caller instead of directly logging it\n\t\treturn fmt.Errorf(\"Error processing files: %v\", err)\n\t}\n\n\t// ... rest of the code ...\n\n\tif err != nil {\n\t\t// Return error to the caller instead of directly logging it\n\t\treturn fmt.Errorf(\"Error executing plan: %v\", err)\n\t}\n\n\tif copiedAny || didExec {\n\t\tfmt.Println(\"Plan applied successfully!\")\n\t} else {\n\t\treturn fmt.Errorf(\"This plan has no changes to apply.\")\n\t}\n\n\treturn nil\n}\n\n// ... rest of the code ...\n</PlandexBlock>\n\n- file: cmd/checkout.go\n\n<PlandexBlock lang=\"go\" path=\"cmd/checkout.go\">\npackage cmd\n\n// ... other imports ...\n\nfunc checkout(cmd *cobra.Command, args []string) {\n\n\t// ... rest of the code ...\n\n\terr = apply(cmd, args)\n\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Error committing plan: \", err)\n\t\treturn\n\t}\n\n\t// ... rest of the code ...\n}\n</PlandexBlock>\n\nNext, compile and run your application to verify that any error from the 'apply' command would be gracefully handled by the 'checkout' command. Changes have been made in such a way that 'apply' command can still work standalone, besides working in sync with 'checkout' command.\n"
  },
  {
    "path": "app/server/types/reply_test_examples/10.md",
    "content": "I'll work on the current subtask \"Update commit message prompts and handling\". This involves updating both the prompt in describe.go and the handling in commit_msg.go to use XML instead of JSON.\n\nFirst, let's update the prompt in describe.go:\n\n**Updating `server/model/prompts/describe.go`:** I'll replace the function definition and update the system prompt to use XML output instead of JSON.\n\n<PlandexBlock lang=\"go\" path=\"server/model/prompts/describe.go\">\npackage prompts\n\nimport (\n\t\"fmt\"\n\n\t\"plandex-shared\"\n)\n\nconst SysDescribe = `You are an AI parser. You turn an AI's plan for a programming task into a structured description. You must output a commit message for the changes proposed using XML tags.\n\nOutput your response using a <commitMsg> tag. Do not use any XML attributes, just the tag content.\n\nExample:\n<commitMsg>Add user authentication system with JWT support</commitMsg>\n\nYou must ALWAYS output a commit message using the XML format shown above. The commit message should be good and succinct.`\n\nvar SysDescribeNumTokens int\n\nconst 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.\"\n\nvar SysPendingResultsNumTokens int\n\nfunc init() {\n\tvar err error\n\tSysDescribeNumTokens, err = shared.GetNumTokensEstimate(SysDescribe)\n\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Error getting num tokens for describe plan prompt: %v\\n\", err))\n\t}\n\n\tSysPendingResultsNumTokens, err = shared.GetNumTokensEstimate(SysPendingResults)\n\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"Error getting num tokens for pending results prompt: %v\\n\", err))\n\t}\n}\n</PlandexBlock>\n\nNow let's update the commit message handling in commit_msg.go:\n\n**Updating `server/model/plan/commit_msg.go`:** I'll update the genPlanDescription method to handle XML output instead of JSON.\n\n<PlandexBlock lang=\"go\" path=\"server/model/plan/commit_msg.go\">\n// ... existing code ...\n\nfunc (state *activeTellStreamState) genPlanDescription() (*db.ConvoMessageDescription, error) {\n\tauth := state.auth\n\tplan := state.plan\n\tplanId := plan.Id\n\tbranch := state.branch\n\tsettings := state.settings\n\tclients := state.clients\n\tconfig := settings.ModelPack.CommitMsg\n\tenvVar := config.BaseModelConfig.ApiKeyEnvVar\n\tclient := clients[envVar]\n\n\tactivePlan := GetActivePlan(planId, branch)\n\tif activePlan == nil {\n\t\treturn nil, fmt.Errorf(\"active plan not found\")\n\t}\n\n\tnumTokens := prompts.ExtraTokensPerRequest + (prompts.ExtraTokensPerMessage * 2) + prompts.SysDescribeNumTokens + activePlan.NumTokens\n\n\t_, apiErr := hooks.ExecHook(hooks.WillSendModelRequest, hooks.HookParams{\n\t\tAuth: auth,\n\t\tPlan: plan,\n\t\tWillSendModelRequestParams: &hooks.WillSendModelRequestParams{\n\t\t\tInputTokens:  numTokens,\n\t\t\tOutputTokens: config.BaseModelConfig.MaxOutputTokens,\n\t\t\tModelName:    config.BaseModelConfig.ModelName,\n\t\t},\n\t})\n\tif apiErr != nil {\n\t\treturn nil, errors.New(apiErr.Msg)\n\t}\n\n\tlog.Println(\"Sending plan description model request\")\n\n\tdescResp, err := model.CreateChatCompletionWithRetries(\n\t\tclient,\n\t\tactivePlan.Ctx,\n\t\topenai.ChatCompletionRequest{\n\t\t\tModel: config.BaseModelConfig.ModelName,\n\t\t\tMessages: []openai.ChatCompletionMessage{\n\t\t\t\t{\n\t\t\t\t\tRole:    openai.ChatMessageRoleSystem,\n\t\t\t\t\tContent: prompts.SysDescribe,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tRole:    openai.ChatMessageRoleAssistant,\n\t\t\t\t\tContent: activePlan.CurrentReplyContent,\n\t\t\t\t},\n\t\t\t},\n\t\t\tTemperature: config.Temperature,\n\t\t\tTopP:        config.TopP,\n\t\t},\n\t)\n\n\tif err != nil {\n\t\tfmt.Printf(\"Error during plan description model call: %v\\n\", err)\n\t\treturn nil, err\n\t}\n\n\tlog.Println(\"Plan description model call complete\")\n\n\tif len(descResp.Choices) == 0 {\n\t\treturn nil, fmt.Errorf(\"No choices in model response\")\n\t}\n\n\tcontent := descResp.Choices[0].Message.Content\n\tcommitMsg := GetXMLContent(content, \"commitMsg\")\n\n\tif commitMsg == \"\" {\n\t\treturn nil, fmt.Errorf(\"No commit message found in response\")\n\t}\n\n\tvar inputTokens int\n\tvar outputTokens int\n\tif descResp.Usage.CompletionTokens > 0 {\n\t\tinputTokens = descResp.Usage.PromptTokens\n\t\toutputTokens = descResp.Usage.CompletionTokens\n\t} else {\n\t\tinputTokens = numTokens\n\t\toutputTokens, err = shared.GetNumTokensEstimate(content)\n\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error getting num tokens for content: %v\", err)\n\t\t}\n\t}\n\n\tlog.Println(\"Sending DidSendModelRequest hook\")\n\n\t_, apiErr = hooks.ExecHook(hooks.DidSendModelRequest, hooks.HookParams{\n\t\tAuth: auth,\n\t\tPlan: plan,\n\t\tDidSendModelRequestParams: &hooks.DidSendModelRequestParams{\n\t\t\tInputTokens:   inputTokens,\n\t\t\tOutputTokens:  outputTokens,\n\t\t\tModelName:     config.BaseModelConfig.ModelName,\n\t\t\tModelProvider: config.BaseModelConfig.Provider,\n\t\t\tModelPackName: settings.ModelPack.Name,\n\t\t\tModelRole:     shared.ModelRoleCommitMsg,\n\t\t\tPurpose:       \"Generated commit message for suggested changes\",\n\t\t},\n\t})\n\n\tif apiErr != nil {\n\t\treturn nil, errors.New(apiErr.Msg)\n\t}\n\n\tlog.Println(\"DidSendModelRequest hook complete\")\n\n\treturn &db.ConvoMessageDescription{\n\t\tPlanId:    planId,\n\t\tCommitMsg: commitMsg,\n\t}, nil\n}\n\n// ... existing code ...\n</PlandexBlock>\n\n**Update commit message prompts and handling** has been completed."
  },
  {
    "path": "app/server/types/reply_test_examples/2.md",
    "content": "Sure, here's how you could structure your 'context rm' command and 'context update' command using placeholders for the 'lib' functions that will handle the actual removal and updating of contexts:\n\n**cmd/context_rm.go**\n\n<PlandexBlock lang=\"go\" path=\"cmd/context_rm.go\">\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// contextRmCmd represents the delete command\nvar contextRmCmd = &cobra.Command{\n\tUse:     \"rm [context_name]\",\n\tAliases: []string{\"delete\"},\n\tShort:   \"\",\n\tLong:    ``,\n\tRun:     contextRm,\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tall, err := cmd.Flags().GetBool(\"all\")\n\n\tif err != nil {\n\t\tlog.Fatal(\"Error parsing all flag: \", err)\n\t}\n\n\tif all {\n\t    \t// Placeholder for lib function\n\t\terr = lib.RemoveAllContexts()\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"Error removing all contexts: \", err)\n\t\t}\n\t} else if len(args) > 0 {\n\t\tfor _, contextName := range args {\n\t    \t\t// Placeholder for lib function\n\t\t\terr = lib.RemoveContext(contextName)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error removing context '%s': %v\\n\", contextName, err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfmt.Println(\"Please provide a context name or use the --all flag to remove all contexts.\")\n\t}\n}\n\nfunc init() {\n\tcontextCmd.AddCommand(contextRmCmd)\n\tcontextRmCmd.Flags().BoolP(\"all\", \"a\", false, \"remove all contexts\")\n}\n</PlandexBlock>\n\n**cmd/context_update.go**\n\n<PlandexBlock lang=\"go\" path=\"cmd/context_update.go\">\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// contextUpdateCmd represents the context command\nvar contextUpdateCmd = &cobra.Command{\n\tUse:   \"update [file-or-urls...]\",\n\tShort: \"A brief description of your command\",\n\tLong: `A longer description that spans multiple lines and likely contains examples\nand usage of using your command. For example:\n\nCobra is a CLI library for Go that empowers applications.\nThis application is a tool to generate the needed files\nto quickly create a Cobra application.`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif len(args) == 0 {\n\t\t\tfmt.Println(\"Please provide one or more files or URLs to update.\")\n\t\t\treturn\n\t\t}\n\n\t\tfor _, arg := range args {\n\t    \t\t// Placeholder for lib function\n\t\t\terr := lib.UpdateContext(arg)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error updating context for '%s': %v\\n\", arg, err)\n\t\t\t}\n\t\t}\n\t},\n}\n\nfunc init() {\n\tcontextCmd.AddCommand(contextUpdateCmd)\n}\n</PlandexBlock>\n\nIn your next iteration, you could implement the functions in 'lib' package such as `RemoveAllContexts`, `RemoveContext`, and `UpdateContext`.\n"
  },
  {
    "path": "app/server/types/reply_test_examples/3.md",
    "content": "Sure, here's how you could structure your 'context rm' command and 'context update' command using placeholders for the 'lib' functions that will handle the actual removal and updating of contexts:\n\n**cmd/context_rm.go**\n\n<PlandexBlock lang=\"go\" path=\"cmd/context_rm.go\">\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// contextRmCmd represents the delete command\nvar contextRmCmd = &cobra.Command{\n\tUse:     \"rm [context_name]\",\n\tAliases: []string{\"delete\"},\n\tShort:   \"\",\n\tLong:    ``,\n\tRun:     contextRm,\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tall, err := cmd.Flags().GetBool(\"all\")\n\n\tif err != nil {\n\t\tlog.Fatal(\"Error parsing all flag: \", err)\n\t}\n\n\tif all {\n\t    \t// Placeholder for lib function\n\t\terr = lib.RemoveAllContexts()\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"Error removing all contexts: \", err)\n\t\t}\n\t} else if len(args) > 0 {\n\t\tfor _, contextName := range args {\n\t    \t\t// Placeholder for lib function\n\t\t\terr = lib.RemoveContext(contextName)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error removing context '%s': %v\\n\", contextName, err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfmt.Println(\"Please provide a context name or use the --all flag to remove all contexts.\")\n\t}\n}\n\nfunc init() {\n\tcontextCmd.AddCommand(contextRmCmd)\n\tcontextRmCmd.Flags().BoolP(\"all\", \"a\", false, \"remove all contexts\")\n}\n</PlandexBlock>\n\n**cmd/context_update.go**\n\n<PlandexBlock lang=\"go\" path=\"cmd/context_update.go\">\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// contextUpdateCmd represents the context command\nvar contextUpdateCmd = &cobra.Command{\n\tUse:   \"update [file-or-urls...]\",\n\tShort: \"A brief description of your command\",\n\tLong: `A longer description that spans multiple lines and likely contains examples\nand usage of using your command. For example:\n\nCobra is a CLI library for Go that empowers applications.\nThis application is a tool to generate the needed files\nto quickly create a Cobra application.`,\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tif len(args) == 0 {\n\t\t\tfmt.Println(\"Please provide one or more files or URLs to update.\")\n\t\t\treturn\n\t\t}\n\n\t\tfor _, arg := range args {\n\t    \t\t// Placeholder for lib function\n\t\t\terr := lib.UpdateContext(arg)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"Error updating context for '%s': %v\\n\", arg, err)\n\t\t\t}\n\t\t}\n\t},\n}\n\nfunc init() {\n\tcontextCmd.AddCommand(contextUpdateCmd)\n}\n</PlandexBlock>\n\nIn your next iteration, you could implement the functions in 'lib' package such as `RemoveAllContexts`, `RemoveContext`, and `UpdateContext`.\n"
  },
  {
    "path": "app/server/types/reply_test_examples/4.md",
    "content": "Yes, it does. Let's start by defining the types for `Section` and `SectionizeResponse`. For the Section type, we would need `name`, `content`, and an optional `subSections` field (to store any potentially nested sections).\n\nHere is the structure we can follow:\n\n1. A `Section` struct which contains a `Name` for the section, `Content` of the section, and a slice of `Subsection` for possible subsections in that section.\n2. A `SectionizeResponse` struct which contains a slice of `Section`.\n\nIn `server/types/` directory, create a new `section.go` file.\n\n- server/types/section.go\n\n<PlandexBlock lang=\"go\" path=\"types/section.go\">\npackage types\n\ntype Section struct {\n\tName       string\n\tContent    string\n\tSubsection []Section\n}\n\ntype SectionizeResponse struct {\n\tSections []Section\n}\n</PlandexBlock>\n\nOnce you have checked and confirmed this task, I will proceed to the next task which is creating a sectionize function in the server model.\n"
  },
  {
    "path": "app/server/types/reply_test_examples/5.md",
    "content": "Given that a summarization utility exists, the plan can be further simplified to utilize that. Here is the updated plan:\n\n1. Update the shared/types.go file to introduce a new boolean field indicating if a message has been summarized.\n\n   - shared/types.go:\n\n   <PlandexBlock lang=\"go\">\n   type ChatCompletionMessage struct {\n   \t// existing fields\n   \t...\n   \tSummarized bool\n   }\n   </PlandexBlock>\n\n2. Modify the loadConversation function in cli/lib/conversation.go to take advantage of this new field.\n\n   - cli/lib/conversation.go:\n     <PlandexBlock lang=\"go\">\n     // modified part of loadConversation\n     ...\n     \tmessages = append(messages, openai.ChatCompletionMessage{\n     \t\tRole:       currentRole,\n     \t\tContent:    loadContent(filePath, currentRole),\n     \t\tSummarized: checkIfMsgSummarized(filePath, currentRole)\n     \t})\n     ...\n     </PlandexBlock>\n   - Add the helper functions, checkIfMsgSummarized to check if a given message has already been summarized.\n\n3. In the server/model/proposal/create.go, handle the token limit error by introducing a summarization step.\n\n   - Before calling the CreateChatCompletionStream, check the number of tokens in the request if they exceed the limit.\n   - If they do, find the pairs of conversation and attempt to summarize starting from the second.\n   - Use the model.Summarize() function provided by the system in step 2 above to handle this summarization.\n\n4. Lastly, handle the superfluous WriteHeader call.\n   - For the superfluous WriteHeader call, ensure that all response modifications (like setting headers) are made before calling WriteHeader() and before writing to the response body.\n\nNote: It's still necessary to track which parts of the conversation have already been summarized and avoid summarizing them again if the token limit is still exceeded. This can be achieved by storing a \"summarized\" flag in each chat message (ChatCompletionMessage).\n"
  },
  {
    "path": "app/server/types/reply_test_examples/6.md",
    "content": "In the following implementation for error handling and conversation summarization in `server/model/proposal/create.go`, the changes will help anticipate any token limit errors. After collecting and formatting all the necessary context, we will check if the token number exceeds the permitted limit. If so, we will initialize a process for summarizing conversation pairs.\n\nChanges will include:\n\n- A check for the token limit before calling `CreateChatCompletionStream`\n- A mechanism to iterate through conversation pairs for summarization\n- Recursive calls of `CreateProposal` with summarized conversation, if necessary\n\n-server/model/proposal/create.go:\n\n<PlandexBlock lang=\"go\">\n// Add this at the start of the CreateProposal function\nconst maxTokens = 8192  // The maximum tokens allowed by the model\n\n...\n\n// After preparation of the main conversation context, we add checks for tokens\ntokenNumbers := GetTokenNumber(messages)  // Assuming you have a function for this\n\nif tokenNumbers > maxTokens {\n\n    // Iterate the Conversation pairs\n    for i := 1; i < len(req.Conversation)-1; i++ {\n\n        // Check if the assistant's part has been summarized before\n        if req.Conversation[i].Role == openai.ChatMessageRoleAssistant && !req.Conversation[i].Summarized {\n            // Summarize the conversation, assuming we have a function Summarize() returning summarized text\n            summarizedContent := Summarize(req.Conversation[i].Content)\n            req.Conversation[i].Content = summarizedContent\n            req.Conversation[i].Summarized = true  // Mark the message as summarized\n            break\n        }\n    }\n    // Recursive call to CreateProposal after summarizing\n    err := CreateProposal(req, onStream)\n    if err != nil {\n        return fmt.Errorf(\"Error recreating proposal: %s\\n\", err)\n    }\n}\n\n...\n\n// Proceed the rest of the function as usual\n</PlandexBlock>\n\nThis block of code handles the error arising from exceeding the maximum tokens supported by the model. If the tokens exceeded are detected, we start the process of summarizing conversation pairs till we are within allowed token limits. This is implemented via a recursive call to `CreateProposal`.\n\nLastly, you should address the \"superfluous response.WriteHeader call\" error. This issue is usually due to an unnecessary double setting of headers in HTTP responses - HTTP headers should only be written once. The fix would involve a careful review of your HTTP response handling process ensuring that the response's headers aren't set after `WriteHeader()` has been called or body written into.\n\nPlease integrate these changes into your respective function to handle the errors as described. Additional iterations might be required to fine-tune and resolve adjacent issues arising from these changes. Please also ensure your token counting and text summarization functions are accurate and performant as they play a crucial role in this error handling process.\n"
  },
  {
    "path": "app/server/types/reply_test_examples/7.md",
    "content": "**Updating `file_map/map.go`**: I'll add debug logging to the mapMarkup function between the function declaration and the end of the file.\n\n- file_map/map.go:\n<PlandexBlock lang=\"go\">\n// ... existing code ...\n\nfunc mapMarkup(node *tree_sitter.Node, content []byte) []Definition {\n    if verboseLogging {\n        fmt.Println(\"mapMarkup called with node type:\", node.Type())\n    }\n    \n    var defs []Definition\n    cursor := tree_sitter.NewTreeCursor(node)\n    defer cursor.Close()\n\n    if cursor.GoToFirstChild() {\n        if verboseLogging {\n            fmt.Println(\"cursor went to first child\")\n        }\n        \n        for {\n            node := cursor.CurrentNode()\n            if verboseLogging {\n                fmt.Println(\"processing node type:\", node.Type())\n            }\n            \n            if node.Type() == \"element\" {\n                if verboseLogging {\n                    fmt.Println(\"found element node\")\n                }\n                \n                // Get tag name from start_tag\n                if startTag := node.ChildByFieldName(\"start_tag\"); startTag != nil {\n                    if verboseLogging {\n                        fmt.Println(\"found start_tag:\", startTag.Type())\n                    }\n                    \n                    if nameNode := startTag.ChildByFieldName(\"name\"); nameNode != nil {\n                        if verboseLogging {\n                            fmt.Printf(\"found name node: %s with content: %s\\n\", \n                                nameNode.Type(), string(nameNode.Content(content)))\n                        }\n                        \n                        def := Definition{\n                            Type:      \"tag\",\n                            Line:      int(node.StartPoint().Row) + 1,\n                            Signature: string(nameNode.Content(content)),\n                        }\n\n                        // Get class and id attributes\n                        var attrs []string\n                        for i := 0; i < int(startTag.ChildCount()); i++ {\n                            child := startTag.Child(i)\n                            if verboseLogging {\n                                fmt.Printf(\"checking child %d type: %s\\n\", i, child.Type())\n                            }\n                            \n                            if child.Type() == \"attribute\" {\n                                if verboseLogging {\n                                    fmt.Println(\"found attribute node\")\n                                }\n                                \n                                if attrName := child.ChildByFieldName(\"name\"); attrName != nil {\n                                    name := string(attrName.Content(content))\n                                    if verboseLogging {\n                                        fmt.Printf(\"found attribute name: %s\\n\", name)\n                                    }\n                                    \n                                    if name == \"class\" || name == \"id\" {\n                                        if attrValue := child.ChildByFieldName(\"value\"); attrValue != nil {\n                                            if verboseLogging {\n                                                fmt.Printf(\"found attribute value: %s\\n\", \n                                                    string(attrValue.Content(content)))\n                                            }\n                                            attrs = append(attrs, fmt.Sprintf(\"%s=%q\", \n                                                name, attrValue.Content(content)))\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        def.TagAttrs = attrs\n\n                        // Recursively process children\n                        if verboseLogging {\n                            fmt.Println(\"recursively processing children\")\n                        }\n                        def.Children = mapMarkup(node, content)\n                        defs = append(defs, def)\n                    } else if verboseLogging {\n                        fmt.Println(\"no name node found\")\n                    }\n                } else if verboseLogging {\n                    fmt.Println(\"no start_tag found\")\n                }\n            }\n\n            if !cursor.GoToNextSibling() {\n                if verboseLogging {\n                    fmt.Println(\"no more siblings\")\n                }\n                break\n            }\n        }\n    } else if verboseLogging {\n        fmt.Println(\"cursor could not go to first child\")\n    }\n\n    if verboseLogging {\n        fmt.Printf(\"returning %d definitions\\n\", len(defs))\n    }\n    return defs\n}\n\n// ... existing code ...\n</PlandexBlock>"
  },
  {
    "path": "app/server/types/reply_test_examples/8.md",
    "content": "**Creating the project structure and Makefile**\n\nFirst, I'll create a basic project structure with a Makefile that will compile our game.\n\n- Makefile:\n<PlandexBlock lang=\"makefile\">\nCC = gcc\nCFLAGS = -Wall -Wextra -I./include\nLDFLAGS = -framework OpenGL -framework GLUT\nSRC_DIR = src\nOBJ_DIR = obj\nBIN_DIR = bin\n\nSRCS = $(wildcard $(SRC_DIR)/*.c)\nOBJS = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)\nEXECUTABLE = $(BIN_DIR)/pong\n\n.PHONY: all clean directories\n\nall: directories $(EXECUTABLE)\n\ndirectories:\n\t@mkdir -p $(OBJ_DIR) $(BIN_DIR)\n\n$(EXECUTABLE): $(OBJS)\n\t$(CC) $(OBJS) -o $@ $(LDFLAGS)\n\n$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c\n\t$(CC) $(CFLAGS) -c $< -o $@\n\nclean:\n\trm -rf $(OBJ_DIR) $(BIN_DIR)\n</PlandexBlock>\n\n- _apply.sh:\n<PlandexBlock lang=\"bash\">\n# Create necessary directories if they don't exist\nmkdir -p src include obj bin\n\n# Check if Homebrew is installed\nif ! command -v brew &> /dev/null; then\n    echo \"Homebrew is not installed. Please install Homebrew first.\"\n    exit 1\nfi\n\n# Install GLUT if not already installed\nif ! brew list | grep -q freeglut; then\n    echo \"Installing GLUT...\"\n    brew install freeglut\nfi\n\n# Compile the project\nmake clean && make\n\n# Run the game\n./bin/pong\n</PlandexBlock>\n\n**Creating the project structure and Makefile** has been completed.\n\nNext, I'll implement the header files for game structures and constants.\n"
  },
  {
    "path": "app/server/types/reply_test_examples/9.md",
    "content": "I'll move the game-related files to the game directory.\n\n### Move Files\n\n- src/game.c → src/game/game.c\n- src/game.h → src/game/game.h\n\n<EndPlandexFileOps/>\n\nNow I'll remove the README.md file as requested.\n\n### Remove Files\n\n- src/README.md\n\n<EndPlandexFileOps/>\n\nThen I'll reset the changes to the Makefile.\n\n### Reset Changes\n\n- Makefile\n\n<EndPlandexFileOps/>\n\nNow I'll write a new Makefile.\n\n- Makefile:\n\n<PlandexBlock lang=\"bash\">\n# Create necessary directories if they don't exist\nmkdir -p src include obj bin\n\n# Check if Homebrew is installed\nif ! command -v brew &> /dev/null; then\n    echo \"Homebrew is not installed. Please install Homebrew first.\"\n    exit 1\nfi\n</PlandexBlock>"
  },
  {
    "path": "app/server/types/safe_map.go",
    "content": "package types\n\nimport \"sync\"\n\ntype SafeMap[V any] struct {\n\titems map[string]V\n\tmu    sync.Mutex\n}\n\nfunc NewSafeMap[V any]() *SafeMap[V] {\n\treturn &SafeMap[V]{items: make(map[string]V)}\n}\n\nfunc (sm *SafeMap[V]) Get(key string) V {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\treturn sm.items[key]\n}\n\nfunc (sm *SafeMap[V]) Set(key string, value V) {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\tsm.items[key] = value\n}\n\nfunc (sm *SafeMap[V]) Delete(key string) {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\tdelete(sm.items, key)\n}\n\nfunc (sm *SafeMap[V]) Update(key string, fn func(V)) {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\tif item, ok := sm.items[key]; ok {\n\t\tfn(item)\n\t\tsm.items[key] = item\n\t}\n}\n\nfunc (sm *SafeMap[V]) Items() map[string]V {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\treturn sm.items\n}\n\nfunc (sm *SafeMap[V]) Keys() []string {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\tkeys := make([]string, len(sm.items))\n\ti := 0\n\tfor k := range sm.items {\n\t\tkeys[i] = k\n\t\ti++\n\t}\n\treturn keys\n}\n\nfunc (sm *SafeMap[V]) Len() int {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\treturn len(sm.items)\n}\n"
  },
  {
    "path": "app/server/types/trial.go",
    "content": "package types\n\nconst TrialMaxReplies = 20\nconst TrialMaxPlans = 10\n"
  },
  {
    "path": "app/server/utils/whitespace.go",
    "content": "package utils\n\nimport \"strings\"\n\nfunc StripAddedBlankLines(orig, upd string) string {\n\torigLines := strings.Split(orig, \"\\n\")\n\tupdLines := strings.Split(upd, \"\\n\")\n\n\tleadingOrig := 0\n\tfor leadingOrig < len(origLines) && strings.TrimSpace(origLines[leadingOrig]) == \"\" {\n\t\tleadingOrig++\n\t}\n\n\tleadingUpd := 0\n\tfor leadingUpd < len(updLines) && strings.TrimSpace(updLines[leadingUpd]) == \"\" {\n\t\tleadingUpd++\n\t}\n\n\tif leadingUpd > leadingOrig {\n\t\tupdLines = updLines[leadingUpd-leadingOrig:] // trim surplus\n\t}\n\n\ttrailingOrig := 0\n\tfor trailingOrig < len(origLines) && strings.TrimSpace(origLines[len(origLines)-1-trailingOrig]) == \"\" {\n\t\ttrailingOrig++\n\t}\n\n\ttrailingUpd := 0\n\tfor trailingUpd < len(updLines) && strings.TrimSpace(updLines[len(updLines)-1-trailingUpd]) == \"\" {\n\t\ttrailingUpd++\n\t}\n\n\tif trailingUpd > trailingOrig {\n\t\tupdLines = updLines[:len(updLines)-(trailingUpd-trailingOrig)]\n\t}\n\n\treturn strings.Join(updLines, \"\\n\")\n}\n"
  },
  {
    "path": "app/server/utils/whitespace_test.go",
    "content": "package utils\n\nimport \"testing\"\n\nfunc TestStripAddedBlankLines(t *testing.T) {\n\ttcs := []struct {\n\t\tname string\n\t\torig string\n\t\tupd  string\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"no change\",\n\t\t\torig: \"a\\nb\\nc\\n\",\n\t\t\tupd:  \"a\\nb\\nc\\n\",\n\t\t\twant: \"a\\nb\\nc\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"leading newline added\",\n\t\t\torig: \"a\\nb\\n\",\n\t\t\tupd:  \"\\n\\na\\nb\\n\",\n\t\t\twant: \"a\\nb\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"trailing newline added\",\n\t\t\torig: \"a\\nb\\n\",\n\t\t\tupd:  \"a\\nb\\n\\n\",\n\t\t\twant: \"a\\nb\\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"both ends, keep original padding\",\n\t\t\torig: \"\\nfoo\\nbar\\n\\n\",\n\t\t\tupd:  \"\\n\\nfoo\\nbar\\n\\n\\n\",\n\t\t\twant: \"\\nfoo\\nbar\\n\\n\",\n\t\t},\n\t}\n\n\tfor _, tc := range tcs {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := StripAddedBlankLines(tc.orig, tc.upd)\n\t\t\tif got != tc.want {\n\t\t\t\tt.Fatalf(\"\\norig:\\n%q\\nupd:\\n%q\\nwant:\\n%q\\ngot:\\n%q\",\n\t\t\t\t\ttc.orig, tc.upd, tc.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "app/server/utils/xml.go",
    "content": "package utils\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc EscapeInvalidXMLAttributeCharacters(xmlString string) string {\n\t// Regular expression to match content inside double quotes, but not the quotes themselves\n\tre := regexp.MustCompile(`\"([^\"]*?)\"`)\n\treturn re.ReplaceAllStringFunc(xmlString, func(attrValue string) string {\n\t\t// Extract the content inside the quotes (removing the enclosing quotes)\n\t\tcontent := attrValue[1 : len(attrValue)-1]\n\n\t\t// Escape the content inside the quotes\n\t\tescaped := strings.ReplaceAll(content, \"&\", \"&amp;\")\n\t\tescaped = strings.ReplaceAll(escaped, \"<\", \"&lt;\")\n\t\tescaped = strings.ReplaceAll(escaped, \">\", \"&gt;\")\n\t\tescaped = strings.ReplaceAll(escaped, `\"`, \"&quot;\")\n\t\tescaped = strings.ReplaceAll(escaped, \"'\", \"&apos;\")\n\n\t\t// Re-wrap the escaped content in quotes\n\t\treturn `\"` + escaped + `\"`\n\t})\n}\n\nfunc EscapeCdata(xmlString string) string {\n\tescaped := strings.ReplaceAll(xmlString, \"]]>\", \"PDX_ESCAPED_CDATA_END\")\n\treturn escaped\n}\n\nfunc UnescapeCdata(xmlString string) string {\n\tescaped := strings.ReplaceAll(xmlString, \"PDX_ESCAPED_CDATA_END\", \"]]>\")\n\treturn escaped\n}\n\nfunc StripCdata(xmlString, tagName string) string {\n\topenTag := \"<\" + tagName + \">\"\n\tcloseTag := \"</\" + tagName + \">\"\n\txmlString = regexp.MustCompile(openTag+`\\s*<!\\[CDATA\\[`).ReplaceAllString(xmlString, openTag)\n\txmlString = regexp.MustCompile(`]]>\\s*`+closeTag).ReplaceAllString(xmlString, closeTag)\n\treturn xmlString\n}\n\nfunc WrapCdata(xmlString, tagName string) string {\n\topenTag := \"<\" + tagName + \">\"\n\tcloseTag := \"</\" + tagName + \">\"\n\txmlString = StripCdata(xmlString, tagName)\n\n\txmlString = strings.ReplaceAll(xmlString, openTag, openTag+\"<![CDATA[\")\n\txmlString = strings.ReplaceAll(xmlString, closeTag, \"]]>\"+closeTag)\n\n\treturn xmlString\n}\n\nfunc GetXMLTag(xmlString, tagName string, wrapCdata bool) string {\n\topenTag := \"<\" + tagName + \">\"\n\tcloseTag := \"</\" + tagName + \">\"\n\n\t// Get everything after the last opening tag\n\tsplit := strings.Split(xmlString, openTag)\n\tif len(split) < 2 {\n\t\treturn \"\"\n\t}\n\tafterOpenTag := split[len(split)-1]\n\n\t// Get everything before the first closing tag\n\tsplit2 := strings.Split(afterOpenTag, closeTag)\n\tif len(split2) < 1 {\n\t\treturn \"\"\n\t}\n\n\tprocessedXml := openTag + EscapeInvalidXMLAttributeCharacters(split2[0]) + closeTag\n\n\tif wrapCdata {\n\t\tprocessedXml = WrapCdata(processedXml, tagName)\n\t}\n\n\treturn processedXml\n}\n\nfunc GetXMLContent(xmlString, tagName string) string {\n\topenTag := \"<\" + tagName + \">\"\n\tcloseTag := \"</\" + tagName + \">\"\n\n\t// Get everything after the last opening tag\n\tsplit := strings.Split(xmlString, openTag)\n\tif len(split) < 2 {\n\t\treturn \"\"\n\t}\n\tafterOpenTag := split[len(split)-1]\n\n\t// Get everything before the first closing tag\n\tsplit2 := strings.Split(afterOpenTag, closeTag)\n\tif len(split2) < 1 {\n\t\treturn \"\"\n\t}\n\n\treturn split2[0]\n}\n\n// GetAllXMLContent returns all occurrences of content between the specified XML tags\n// as an array of strings. Returns an empty array if no matches are found.\nfunc GetAllXMLContent(xmlString, tagName string) []string {\n\tvar results []string\n\topenTag := \"<\" + tagName + \">\"\n\tcloseTag := \"</\" + tagName + \">\"\n\n\t// Split by opening tag\n\tparts := strings.Split(xmlString, openTag)\n\n\t// Skip the first part (it's before any opening tag)\n\tfor i := 1; i < len(parts); i++ {\n\t\t// Split by closing tag\n\t\tsubParts := strings.Split(parts[i], closeTag)\n\t\tif len(subParts) > 0 {\n\t\t\t// The content is before the first closing tag\n\t\t\tresults = append(results, subParts[0])\n\t\t}\n\t}\n\n\treturn results\n}\n"
  },
  {
    "path": "app/server/version.txt",
    "content": "2.2.1\n"
  },
  {
    "path": "app/shared/ai_models_available.go",
    "content": "package shared\n\nimport (\n\t\"strings\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\n/*\n'MaxTokens' is the absolute input limit for the provider.\n\n'MaxOutputTokens' is the absolute output limit for the provider.\n\n'ReservedOutputTokens' is how much we set aside in context for the model to use in its output. It's more of a realistic output limit, since for some models, the hard maximum 'MaxTokens' is actually equal to the input limit, which would leave no room for input.\n\nThe effective input limit is 'MaxTokens' - 'ReservedOutputTokens'.\n\nFor example, OpenAI o3-mini has 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. However, 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.\n\nFor models with a low output limit, we just set ReservedOutputTokens to the MaxOutputTokens.\n\nWhen checking for sufficient credits on Plandex Cloud, we use MaxOutputTokens-InputTokens, since this is the maximum that could hypothetically be used.\n\n'DefaultMaxConvoTokens' is the default maximum number of conversation tokens that are allowed before we start using gradual summarization to shorten the conversation.\n\n'ModelName' is the name of the model on the provider's side.\n\n'ModelId' is the identifier for the model on the Plandex side—it must be unique per provider. We have this so that models with the same name and provider, but different settings can be differentiated.\n\n'ModelCompatibility' is used to check for feature support (like image support).\n\n'BaseUrl' is the base URL for the provider.\n\n'PreferredOutputFormat' is the preferred output format for the model—currently either 'ModelOutputFormatToolCallJson' or 'ModelOutputFormatXml' — OpenAI models like JSON (and benefit from strict JSON schemas), while most other providers are unreliable for JSON generation and do better with XML, even if they claim to support JSON.\n\n'RoleParamsDisabled' is used to disable role-based parameters like temperature, top_p, etc. for the model—OpenAI early releases often don't allow changes to these.\n\n'SystemPromptDisabled' is used to disable the system prompt for the model—OpenAI early releases sometimes don't allow system prompts.\n\n'ReasoningEffortEnabled' is used to enable reasoning effort for the model (like OpenAI's o3-mini).\n\n'ReasoningEffort' is the reasoning effort for the model, when 'ReasoningEffortEnabled' is true.\n\n'PredictedOutputEnabled' is used to enable predicted output for the model (currently only supported by gpt-4o).\n\n'ApiKeyEnvVar' is the environment variable that contains the API key for the model.\n*/\n\nvar BuiltInModels = []*BaseModelConfigSchema{\n\t{\n\t\tModelTag:    \"openai/o3\",\n\t\tPublisher:   ModelPublisherOpenAI,\n\t\tDescription: \"OpenAI o3\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 200000, MaxOutputTokens: 100000,\n\t\t\tReservedOutputTokens: 40000, ModelCompatibility: FullCompatibility,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml, SystemPromptDisabled: true,\n\t\t\tRoleParamsDisabled: true, ReasoningEffortEnabled: true, StopDisabled: true,\n\t\t},\n\t\tRequiresVariantOverrides: []string{\"ReasoningEffort\"},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{VariantTag: \"high\", Description: \"high\", Overrides: BaseModelShared{ReasoningEffort: ReasoningEffortHigh}},\n\t\t\t{VariantTag: \"medium\", Description: \"medium\", Overrides: BaseModelShared{ReasoningEffort: ReasoningEffortMedium}},\n\t\t\t{VariantTag: \"low\", Description: \"low\", Overrides: BaseModelShared{ReasoningEffort: ReasoningEffortLow}},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenAI, ModelName: \"o3\"},\n\t\t\t{Provider: ModelProviderAzureOpenAI, ModelName: \"azure/o3\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"openai/o3\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"openai/o4-mini\",\n\t\tPublisher:   ModelPublisherOpenAI,\n\t\tDescription: \"OpenAI o4-mini\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 10000, MaxTokens: 200000, MaxOutputTokens: 100000,\n\t\t\tReservedOutputTokens: 40000, ModelCompatibility: FullCompatibility,\n\t\t\tPreferredOutputFormat: ModelOutputFormatToolCallJson, SystemPromptDisabled: true,\n\t\t\tRoleParamsDisabled: true, ReasoningEffortEnabled: true, ReasoningEffort: ReasoningEffortHigh,\n\t\t\tStopDisabled: true,\n\t\t},\n\t\tRequiresVariantOverrides: []string{\"ReasoningEffort\"},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{VariantTag: \"high\", Description: \"high\", Overrides: BaseModelShared{ReasoningEffort: ReasoningEffortHigh}},\n\t\t\t{VariantTag: \"medium\", Description: \"medium\", Overrides: BaseModelShared{ReasoningEffort: ReasoningEffortMedium, ReservedOutputTokens: 30000}},\n\t\t\t{VariantTag: \"low\", Description: \"low\", Overrides: BaseModelShared{ReasoningEffort: ReasoningEffortLow, ReservedOutputTokens: 20000}},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenAI, ModelName: \"o4-mini\"},\n\t\t\t{Provider: ModelProviderAzureOpenAI, ModelName: \"azure/o4-mini\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"openai/o4-mini\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"openai/gpt-4.1\",\n\t\tPublisher:   ModelPublisherOpenAI,\n\t\tDescription: \"OpenAI gpt-4.1\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 75000, MaxTokens: 1047576,\n\t\t\tMaxOutputTokens: 32768, ReservedOutputTokens: 32768,\n\t\t\tModelCompatibility: FullCompatibility, PreferredOutputFormat: ModelOutputFormatToolCallJson,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenAI, ModelName: \"gpt-4.1\"},\n\t\t\t{Provider: ModelProviderAzureOpenAI, ModelName: \"azure/gpt-4.1\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"openai/gpt-4.1\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"openai/gpt-4.1-mini\",\n\t\tPublisher:   ModelPublisherOpenAI,\n\t\tDescription: \"OpenAI gpt-4.1-mini\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 75000, MaxTokens: 1047576,\n\t\t\tMaxOutputTokens: 32768, ReservedOutputTokens: 32768,\n\t\t\tModelCompatibility: FullCompatibility, PreferredOutputFormat: ModelOutputFormatToolCallJson,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenAI, ModelName: \"gpt-4.1-mini\"},\n\t\t\t{Provider: ModelProviderAzureOpenAI, ModelName: \"azure/gpt-4.1-mini\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"openai/gpt-4.1-mini\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"openai/gpt-4.1-nano\",\n\t\tPublisher:   ModelPublisherOpenAI,\n\t\tDescription: \"OpenAI gpt-4.1-nano\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 75000, MaxTokens: 1047576,\n\t\t\tMaxOutputTokens: 32768, ReservedOutputTokens: 32768,\n\t\t\tModelCompatibility: FullCompatibility, PreferredOutputFormat: ModelOutputFormatToolCallJson,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenAI, ModelName: \"gpt-4.1-nano\"},\n\t\t\t{Provider: ModelProviderAzureOpenAI, ModelName: \"azure/gpt-4.1-nano\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"openai/gpt-4.1-nano\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"anthropic/claude-opus-4\",\n\t\tPublisher:   ModelPublisherAnthropic,\n\t\tDescription: \"Anthropic Claude Opus 4\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 200000, MaxOutputTokens: 128000,\n\t\t\tReservedOutputTokens: 20000, SupportsCacheControl: true,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml, SingleMessageNoSystemPrompt: true,\n\t\t\tTokenEstimatePaddingPct: 0.10,\n\t\t},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{IsBaseVariant: true},\n\t\t\t// Opus thinking variant not working yet—might need a specific cap set\n\t\t\t// {\n\t\t\t// \tVariantTag: \"thinking\", Description: \"thinking\",\n\t\t\t// \tOverrides: BaseModelShared{ReasoningBudget: AnthropicMaxReasoningBudget},\n\t\t\t// \tVariants: []BaseModelConfigVariant{\n\t\t\t// \t\t{VariantTag: \"visible\", IsDefaultVariant: true, Description: \"visible\", Overrides: BaseModelShared{IncludeReasoning: true}},\n\t\t\t// \t\t{VariantTag: \"hidden\", Description: \"hidden\", Overrides: BaseModelShared{IncludeReasoning: false}},\n\t\t\t// \t},\n\t\t\t// },\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderAnthropic, ModelName: \"anthropic/claude-opus-4-0\"},\n\t\t\t{Provider: ModelProviderAmazonBedrock, ModelName: \"bedrock/anthropic.claude-opus-4-20250514-v1:0\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/claude-opus-4@20250514\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"anthropic/claude-opus-4\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"anthropic/claude-sonnet-4\",\n\t\tPublisher:   ModelPublisherAnthropic,\n\t\tDescription: \"Anthropic Claude Sonnet 4\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 200000, MaxOutputTokens: 128000,\n\t\t\tReservedOutputTokens: 40000, SupportsCacheControl: true,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml, SingleMessageNoSystemPrompt: true,\n\t\t\tTokenEstimatePaddingPct: 0.10,\n\t\t},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{IsBaseVariant: true},\n\t\t\t{\n\t\t\t\tVariantTag: \"thinking\", Description: \"thinking\",\n\t\t\t\tOverrides: BaseModelShared{ReasoningBudget: AnthropicMaxReasoningBudget},\n\t\t\t\tVariants: []BaseModelConfigVariant{\n\t\t\t\t\t{VariantTag: \"visible\", IsDefaultVariant: true, Description: \"visible\", Overrides: BaseModelShared{IncludeReasoning: true}},\n\t\t\t\t\t{VariantTag: \"hidden\", Description: \"hidden\", Overrides: BaseModelShared{IncludeReasoning: false}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderAnthropic, ModelName: \"anthropic/claude-sonnet-4-0\"},\n\t\t\t{Provider: ModelProviderAmazonBedrock, ModelName: \"anthropic.claude-sonnet-4-20250514-v1:0\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/claude-sonnet-4@20250514\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"anthropic/claude-sonnet-4\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"anthropic/claude-3.7-sonnet\",\n\t\tPublisher:   ModelPublisherAnthropic,\n\t\tDescription: \"Anthropic Claude 3.7 Sonnet\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 200000, MaxOutputTokens: 128000,\n\t\t\tReservedOutputTokens: 20000, SupportsCacheControl: true,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml, SingleMessageNoSystemPrompt: true,\n\t\t\tTokenEstimatePaddingPct: 0.10,\n\t\t},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{IsBaseVariant: true},\n\t\t\t{\n\t\t\t\tVariantTag: \"thinking\", Description: \"thinking\",\n\t\t\t\tOverrides: BaseModelShared{ReasoningBudget: AnthropicMaxReasoningBudget},\n\t\t\t\tVariants: []BaseModelConfigVariant{\n\t\t\t\t\t{VariantTag: \"visible\", IsDefaultVariant: true, Description: \"visible\", Overrides: BaseModelShared{IncludeReasoning: true}},\n\t\t\t\t\t{VariantTag: \"hidden\", Description: \"hidden\", Overrides: BaseModelShared{IncludeReasoning: false}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderAnthropic, ModelName: \"anthropic/claude-3-7-sonnet-latest\"},\n\t\t\t{Provider: ModelProviderAmazonBedrock, ModelName: \"bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/claude-3-7-sonnet@20250219\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"anthropic/claude-3.7-sonnet\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"anthropic/claude-3.5-sonnet\",\n\t\tPublisher:   ModelPublisherAnthropic,\n\t\tDescription: \"Anthropic Claude 3.5 Sonnet\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 200000, MaxOutputTokens: 128000,\n\t\t\tReservedOutputTokens: 20000, SupportsCacheControl: true,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml, SingleMessageNoSystemPrompt: true,\n\t\t\tTokenEstimatePaddingPct: 0.10,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderAnthropic, ModelName: \"anthropic/claude-3-5-sonnet-latest\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/claude-3-5-sonnet-v2@20241022\"},\n\t\t\t{Provider: ModelProviderAmazonBedrock, ModelName: \"bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"anthropic/claude-3.5-sonnet\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"anthropic/claude-3.5-haiku\",\n\t\tPublisher:   ModelPublisherAnthropic,\n\t\tDescription: \"Anthropic Claude 3.5 Haiku\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 200000, MaxOutputTokens: 8192,\n\t\t\tReservedOutputTokens: 8192, SupportsCacheControl: true,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml, SingleMessageNoSystemPrompt: true,\n\t\t\tTokenEstimatePaddingPct: 0.10,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderAnthropic, ModelName: \"anthropic/claude-3-5-haiku-latest\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/claude-3-5-haiku@20241022\"},\n\t\t\t{Provider: ModelProviderAmazonBedrock, ModelName: \"bedrock/anthropic.claude-3-5-haiku-20241022-v1:0\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"anthropic/claude-3.5-haiku\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"google/gemini-pro-1.5\",\n\t\tPublisher:   ModelPublisherGoogle,\n\t\tDescription: \"Google Gemini 1.5 Pro\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 75000, MaxTokens: 2000000,\n\t\t\tMaxOutputTokens: 8192, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderGoogleAIStudio, ModelName: \"gemini/gemini-1.5-pro\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/gemini-1.5-pro\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"google/gemini-pro-1.5\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"google/gemini-2.5-pro\",\n\t\tPublisher:   ModelPublisherGoogle,\n\t\tDescription: \"Google Gemini 2.5 Pro\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 75000, MaxTokens: 1048576,\n\t\t\tMaxOutputTokens: 65535, ReservedOutputTokens: 65535,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderGoogleAIStudio, ModelName: \"gemini/gemini-2.5-pro\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/gemini-2.5-pro\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"google/gemini-2.5-pro\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"google/gemini-2.5-flash\",\n\t\tPublisher:   ModelPublisherGoogle,\n\t\tDescription: \"Google Gemini 2.5 Flash\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 75000, MaxTokens: 1048576,\n\t\t\tMaxOutputTokens: 65535, ReservedOutputTokens: 65535,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{IsBaseVariant: true},\n\t\t\t{\n\t\t\t\tVariantTag:  \"thinking\",\n\t\t\t\tDescription: \"thinking\",\n\t\t\t\tOverrides:   BaseModelShared{IncludeReasoning: true},\n\t\t\t\tVariants: []BaseModelConfigVariant{\n\t\t\t\t\t{VariantTag: \"visible\", IsDefaultVariant: true, Description: \"visible\"},\n\t\t\t\t\t{VariantTag: \"hidden\", Description: \"hidden\", Overrides: BaseModelShared{HideReasoning: true}},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderGoogleAIStudio, ModelName: \"gemini/gemini-2.5-flash\"},\n\t\t\t{Provider: ModelProviderGoogleVertex, ModelName: \"vertex_ai/gemini-2.5-flash\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"google/gemini-2.5-flash\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"deepseek/v3\",\n\t\tPublisher:   ModelPublisherDeepSeek,\n\t\tDescription: \"DeepSeek V3\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 7500, MaxTokens: 64000,\n\t\t\tMaxOutputTokens: 8192, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderDeepSeek, ModelName: \"deepseek/deepseek-chat\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"deepseek/deepseek-chat-v3\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"deepseek/r1\",\n\t\tPublisher:   ModelPublisherDeepSeek,\n\t\tDescription: \"DeepSeek R1\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 7500, MaxTokens: 164000,\n\t\t\tMaxOutputTokens: 33000, ReservedOutputTokens: 20000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{VariantTag: \"visible\", IsDefaultVariant: true, Description: \"(reasoning visible)\", Overrides: BaseModelShared{IncludeReasoning: true}},\n\t\t\t{VariantTag: \"hidden\", Description: \"(reasoning hidden)\", Overrides: BaseModelShared{IncludeReasoning: false}},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderDeepSeek, ModelName: \"deepseek/deepseek-reasoner\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"deepseek/deepseek-r1-0528\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"deepseek/r1-70b\",\n\t\tPublisher:   ModelPublisherDeepSeek,\n\t\tDescription: \"DeepSeek R1 70B\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 10000, MaxTokens: 131072,\n\t\t\tMaxOutputTokens: 131072, ReservedOutputTokens: 20000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/deepseek-r1:70b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"deepseek/r1-32b\",\n\t\tPublisher:   ModelPublisherDeepSeek,\n\t\tDescription: \"DeepSeek R1 32B\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 10000, MaxTokens: 131072,\n\t\t\tMaxOutputTokens: 131072, ReservedOutputTokens: 20000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/deepseek-r1:32b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"deepseek/r1-14b\",\n\t\tPublisher:   ModelPublisherDeepSeek,\n\t\tDescription: \"DeepSeek R1 14B\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 10000, MaxTokens: 131072,\n\t\t\tMaxOutputTokens: 131072, ReservedOutputTokens: 20000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/deepseek-r1:14b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"deepseek/r1-8b\",\n\t\tPublisher:   ModelPublisherDeepSeek,\n\t\tDescription: \"DeepSeek R1 8B\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 10000, MaxTokens: 131072,\n\t\t\tMaxOutputTokens: 131072, ReservedOutputTokens: 20000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/deepseek-r1:8b\"},\n\t\t},\n\t},\n\n\t{\n\t\tModelTag:    \"qwen/qwen-2.5-coder-32b-instruct\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 2.5 Coder 32B (Instruct)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 10000, MaxTokens: 128000,\n\t\t\tMaxOutputTokens: 8192, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"qwen/qwen-2.5-coder-32b-instruct\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"qwen/qwen3-235b-local\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-235B (Local)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 5000, MaxTokens: 40960,\n\t\t\tMaxOutputTokens: 40960, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/qwen3:235b\"},\n\t\t},\n\t},\n\n\t{\n\t\tModelTag:    \"qwen/qwen3-235b-a22b-cloud\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-235B A22B (Cloud)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 5000, MaxTokens: 40960,\n\t\t\tMaxOutputTokens: 40960, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"qwen/qwen3-235b-a22b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"qwen/qwen3-32b-local\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-32B (Local)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 5000, MaxTokens: 40960,\n\t\t\tMaxOutputTokens: 40960, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/qwen3:32b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"qwen/qwen3-32b-cloud\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-32B (Cloud)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 5000, MaxTokens: 40960,\n\t\t\tMaxOutputTokens: 40960, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"qwen/qwen3-32b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"qwen/qwen3-14b-local\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-14B (Local)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 5000, MaxTokens: 40960,\n\t\t\tMaxOutputTokens: 40960, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/qwen3:14b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"qwen/qwen3-14b-cloud\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-14B (Cloud)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 5000, MaxTokens: 40960,\n\t\t\tMaxOutputTokens: 40960, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"qwen/qwen3-14b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"qwen/qwen3-8b-local\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-8B (Local)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 5000, MaxTokens: 32768,\n\t\t\tMaxOutputTokens: 32768, ReservedOutputTokens: 8192,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/qwen3:8b\"},\n\t\t},\n\t},\n\t{\n\t\tModelTag:    \"qwen/qwen3-8b-cloud\",\n\t\tPublisher:   ModelPublisherQwen,\n\t\tDescription: \"Qwen 3-8B (Cloud)\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 128000,\n\t\t\tMaxOutputTokens: 20000, ReservedOutputTokens: 20000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"qwen/qwen3-8b\"},\n\t\t},\n\t},\n\n\t{\n\t\tModelTag:    \"mistral/devstral-small\",\n\t\tPublisher:   ModelPublisherMistral,\n\t\tDescription: \"Mistral Devstral Small\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 15000, MaxTokens: 128000,\n\t\t\tMaxOutputTokens: 128000, ReservedOutputTokens: 16384,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/devstral:24b\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"mistral/devstral-small\"},\n\t\t},\n\t},\n\n\t{\n\t\tModelTag:    \"perplexity/r1-1776\",\n\t\tPublisher:   ModelPublisherPerplexity,\n\t\tDescription: \"Perplexity R1-1776\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 7500, MaxTokens: 128000,\n\t\t\tMaxOutputTokens: 128000, ReservedOutputTokens: 30000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{VariantTag: \"visible\", IsDefaultVariant: true, Description: \"(reasoning visible)\", Overrides: BaseModelShared{IncludeReasoning: true}},\n\t\t\t{VariantTag: \"hidden\", Description: \"(reasoning hidden)\", Overrides: BaseModelShared{IncludeReasoning: false}},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderPerplexity, ModelName: \"r1-1776-online\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"perplexity/r1-1776\"},\n\t\t},\n\t},\n\n\t{\n\t\tModelTag:    \"perplexity/r1-1776-70b\",\n\t\tPublisher:   ModelPublisherPerplexity,\n\t\tDescription: \"Perplexity R1-1776 70B\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 10000, MaxTokens: 131072,\n\t\t\tMaxOutputTokens: 131072, ReservedOutputTokens: 20000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderOllama, ModelName: \"ollama_chat/r1-1776:70b\"},\n\t\t},\n\t},\n\n\t{\n\t\tModelTag:    \"perplexity/sonar-reasoning\",\n\t\tPublisher:   ModelPublisherPerplexity,\n\t\tDescription: \"Perplexity Sonar Reasoning\",\n\t\tBaseModelShared: BaseModelShared{\n\t\t\tDefaultMaxConvoTokens: 7500, MaxTokens: 127000,\n\t\t\tMaxOutputTokens: 127000, ReservedOutputTokens: 30000,\n\t\t\tPreferredOutputFormat: ModelOutputFormatXml,\n\t\t},\n\t\tVariants: []BaseModelConfigVariant{\n\t\t\t{VariantTag: \"visible\", IsDefaultVariant: true, Description: \"(reasoning visible)\", Overrides: BaseModelShared{IncludeReasoning: true}},\n\t\t\t{VariantTag: \"hidden\", Description: \"(reasoning hidden)\", Overrides: BaseModelShared{IncludeReasoning: false}},\n\t\t},\n\t\tProviders: []BaseModelUsesProvider{\n\t\t\t{Provider: ModelProviderPerplexity, ModelName: \"sonar-reasoning-online\"},\n\t\t\t{Provider: ModelProviderOpenRouter, ModelName: \"perplexity/sonar-reasoning\"},\n\t\t},\n\t},\n}\n\nvar BuiltInBaseModelsById = map[ModelId]*BaseModelConfigSchema{}\n\nvar BuiltInModelProvidersByModelId = map[ModelId][]BaseModelUsesProvider{}\nvar BuiltInBaseModels = []*BaseModelConfigSchema{}\n\nvar AvailableModels = []*AvailableModel{}\n\nvar AvailableModelsByComposite = map[string]*AvailableModel{}\n\nfunc init() {\n\tfor _, model := range BuiltInModels {\n\t\t// if the model has an anthropic provider, insert claude max provider before it\n\t\tvar usesAnthropicProvider *BaseModelUsesProvider\n\t\tfor _, provider := range model.Providers {\n\t\t\tif provider.Provider == ModelProviderAnthropic {\n\t\t\t\tcopy := provider\n\t\t\t\tlatestModelName, ok := AnthropicLatestModelNameMap[provider.ModelName]\n\t\t\t\tif ok {\n\t\t\t\t\tcopy.ModelName = latestModelName\n\t\t\t\t}\n\t\t\t\tusesAnthropicProvider = &copy\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif usesAnthropicProvider != nil {\n\t\t\tusesAnthropicProvider.Provider = ModelProviderAnthropicClaudeMax\n\t\t\tmodel.Providers = append([]BaseModelUsesProvider{*usesAnthropicProvider}, model.Providers...)\n\t\t}\n\n\t\tAvailableModels = append(AvailableModels, model.ToAvailableModels()...)\n\n\t\tvar addVariants func(variants []BaseModelConfigVariant, baseId ModelId)\n\t\taddVariants = func(variants []BaseModelConfigVariant, baseId ModelId) {\n\t\t\tfor _, variant := range variants {\n\t\t\t\tvar modelId ModelId\n\t\t\t\tif variant.IsBaseVariant || variant.IsDefaultVariant {\n\t\t\t\t\tmodelId = baseId\n\t\t\t\t} else {\n\t\t\t\t\tmodelId = ModelId(strings.Join([]string{string(baseId), string(variant.VariantTag)}, \"-\"))\n\t\t\t\t}\n\n\t\t\t\tif _, ok := BuiltInBaseModelsById[modelId]; !ok {\n\t\t\t\t\tcloned := *model\n\t\t\t\t\tcloned.ModelId = modelId\n\t\t\t\t\tmerged := Merge(model.BaseModelShared, variant.Overrides)\n\t\t\t\t\tcloned.BaseModelShared = merged\n\n\t\t\t\t\tBuiltInModelProvidersByModelId[modelId] = cloned.Providers\n\t\t\t\t\tBuiltInBaseModelsById[modelId] = &cloned\n\t\t\t\t\tBuiltInBaseModels = append(BuiltInBaseModels, &cloned)\n\t\t\t\t}\n\n\t\t\t\tif len(variant.Variants) > 0 {\n\t\t\t\t\taddVariants(variant.Variants, modelId)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\n\t\tif len(model.Variants) > 0 {\n\t\t\taddVariants(model.Variants, ModelId(string(model.ModelTag)))\n\t\t} else {\n\t\t\tmodelId := ModelId(string(model.ModelTag))\n\t\t\tmodel.ModelId = modelId\n\t\t\tBuiltInModelProvidersByModelId[modelId] = model.Providers\n\t\t\tBuiltInBaseModelsById[modelId] = model\n\t\t\tBuiltInBaseModels = append(BuiltInBaseModels, model)\n\t\t}\n\t}\n\n\t// fmt.Println(\"AvailableModels\")\n\t// for _, model := range AvailableModels {\n\t// \tfmt.Println(model.ModelString())\n\t// }\n\n\tfor _, model := range AvailableModels {\n\t\tif model.Description == \"\" {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"description is not set\")\n\t\t}\n\n\t\tif model.Provider == \"\" {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"model provider is not set\")\n\t\t}\n\t\tif model.ModelId == \"\" {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"model id is not set\")\n\t\t}\n\n\t\tif model.DefaultMaxConvoTokens == 0 {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"default max convo tokens is not set\")\n\t\t}\n\n\t\tif model.MaxTokens == 0 {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"max tokens is not set\")\n\t\t}\n\n\t\tif model.MaxOutputTokens == 0 {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"max output tokens is not set\")\n\t\t}\n\n\t\tif model.ReservedOutputTokens == 0 {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"reserved output tokens is not set\")\n\t\t}\n\n\t\tif model.ApiKeyEnvVar == \"\" && len(model.ExtraAuthVars) == 0 && !model.SkipAuth && !model.HasAWSAuth && !model.HasClaudeMaxAuth {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"api key or auth settings are not set\")\n\t\t}\n\n\t\tif model.BaseUrl == \"\" {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"base url is not set\")\n\t\t}\n\n\t\tif model.PreferredOutputFormat == \"\" {\n\t\t\tspew.Dump(model)\n\t\t\tpanic(\"preferred model output format is not set\")\n\t\t}\n\n\t\tcompositeKey := string(model.Provider) + \"/\" + string(model.ModelId)\n\t\tAvailableModelsByComposite[compositeKey] = model\n\t}\n}\n\nfunc GetAvailableModel(provider ModelProvider, modelId ModelId) *AvailableModel {\n\tcompositeKey := string(provider) + \"/\" + string(modelId)\n\treturn AvailableModelsByComposite[compositeKey]\n}\n\nvar AnthropicLatestModelNameMap = map[ModelName]ModelName{\n\t\"anthropic/claude-sonnet-4-0\":        \"anthropic/claude-sonnet-4-20250514\",\n\t\"anthropic/claude-opus-4-0\":          \"anthropic/claude-opus-4-20250514\",\n\t\"anthropic/claude-3-7-sonnet-latest\": \"anthropic/claude-3-7-sonnet-20250219\",\n\t\"anthropic/claude-3-5-haiku-latest\":  \"anthropic/claude-3-5-haiku-20241022\",\n\t\"anthropic/claude-3-5-sonnet-latest\": \"anthropic/claude-3-5-sonnet-20241022\",\n}\n"
  },
  {
    "path": "app/shared/ai_models_compatibility.go",
    "content": "package shared\n\nvar FullCompatibility = ModelCompatibility{\n\tHasImageSupport: true,\n}\n\nvar RequiredCompatibilityByRole = map[ModelRole]ModelCompatibility{\n\tModelRolePlanner:          {},\n\tModelRolePlanSummary:      {},\n\tModelRoleBuilder:          {},\n\tModelRoleName:             {},\n\tModelRoleCommitMsg:        {},\n\tModelRoleExecStatus:       {},\n\tModelRoleArchitect:        {},\n\tModelRoleCoder:            {},\n\tModelRoleWholeFileBuilder: {},\n}\n\nfunc FilterBuiltInCompatibleModels(models []*BaseModelConfigSchema, role ModelRole) []*BaseModelConfigSchema {\n\t// required := RequiredCompatibilityByRole[role]\n\tvar compatibleModels []*BaseModelConfigSchema\n\n\tfor _, model := range models {\n\t\t// no compatibility checks are needed in v2, but keeping this here in case compatibility checks are needed in the future\n\n\t\tcompatibleModels = append(compatibleModels, model)\n\t}\n\n\treturn compatibleModels\n}\n\nfunc FilterCustomCompatibleModels(models []*CustomModel, role ModelRole) []*CustomModel {\n\t// required := RequiredCompatibilityByRole[role]\n\tvar compatibleModels []*CustomModel\n\n\tfor _, model := range models {\n\t\t// no compatibility checks are needed in v2, but keeping this here in case compatibility checks are needed in the future\n\n\t\tcompatibleModels = append(compatibleModels, model)\n\t}\n\n\treturn compatibleModels\n}\n"
  },
  {
    "path": "app/shared/ai_models_config.go",
    "content": "package shared\n\nvar DefaultConfigByRole = map[ModelRole]ModelRoleConfig{\n\tModelRolePlanner: {\n\t\tTemperature: 0.3,\n\t\tTopP:        0.3,\n\t},\n\tModelRoleCoder: {\n\t\tTemperature: 0.3,\n\t\tTopP:        0.3,\n\t},\n\tModelRoleArchitect: {\n\t\tTemperature: 0.3,\n\t\tTopP:        0.3,\n\t},\n\tModelRolePlanSummary: {\n\t\tTemperature: 0.2,\n\t\tTopP:        0.2,\n\t},\n\tModelRoleBuilder: {\n\t\tTemperature: 0.1,\n\t\tTopP:        0.1,\n\t},\n\tModelRoleWholeFileBuilder: {\n\t\tTemperature: 0.1,\n\t\tTopP:        0.1,\n\t},\n\tModelRoleName: {\n\t\tTemperature: 0.8,\n\t\tTopP:        0.5,\n\t},\n\tModelRoleCommitMsg: {\n\t\tTemperature: 0.8,\n\t\tTopP:        0.5,\n\t},\n\tModelRoleExecStatus: {\n\t\tTemperature: 0.1,\n\t\tTopP:        0.1,\n\t},\n}\n"
  },
  {
    "path": "app/shared/ai_models_credentials.go",
    "content": "package shared\n\ntype ModelProviderOption struct {\n\tPublishers map[ModelPublisher]bool\n\tConfig     *ModelProviderConfigSchema\n\tPriority   int\n}\n\ntype ModelProviderOptions map[string]ModelProviderOption\n\nfunc (m ModelRoleConfig) GetModelProviderOptions(settings *PlanSettings) ModelProviderOptions {\n\topts := ModelProviderOptions{}\n\n\tbuiltInUsesProviders := BuiltInModelProvidersByModelId[m.ModelId]\n\n\tvar customUsesProviders []BaseModelUsesProvider\n\tif settings != nil {\n\t\tcustomUsesProviders = settings.UsesCustomProviderByModelId[m.ModelId]\n\t}\n\n\tusesProviders := append(builtInUsesProviders, customUsesProviders...)\n\tif len(usesProviders) == 0 {\n\t\treturn opts\n\t}\n\n\tfor i, usesProvider := range usesProviders {\n\t\tcomposite := usesProvider.ToComposite()\n\n\t\tfoundProvider := false\n\t\tconfig, ok := BuiltInModelProviderConfigs[usesProvider.Provider]\n\t\tif ok {\n\t\t\t// built-in provider\n\t\t\tfoundProvider = true\n\t\t} else if settings != nil && settings.CustomProviders != nil {\n\t\t\t// no built-in provider, check custom providers\n\t\t\tfor _, customProvider := range settings.CustomProviders {\n\t\t\t\tif usesProvider.CustomProvider != nil && customProvider.Name == *usesProvider.CustomProvider {\n\t\t\t\t\tconfig = customProvider.ToModelProviderConfigSchema()\n\t\t\t\t\tfoundProvider = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif !foundProvider {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar publisher ModelPublisher\n\n\t\tbaseModel, ok := BuiltInBaseModelsById[m.ModelId]\n\t\tif ok {\n\t\t\tpublisher = baseModel.Publisher\n\t\t} else if settings != nil && settings.CustomModelsById != nil {\n\t\t\tcustomModel, ok := settings.CustomModelsById[m.ModelId]\n\t\t\tif ok {\n\t\t\t\tpublisher = customModel.Publisher\n\t\t\t}\n\t\t}\n\n\t\tif publisher == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\topts[composite] = ModelProviderOption{\n\t\t\tPublishers: map[ModelPublisher]bool{\n\t\t\t\tpublisher: true,\n\t\t\t},\n\t\t\tConfig:   &config,\n\t\t\tPriority: i,\n\t\t}\n\t}\n\n\tif m.ErrorFallback != nil {\n\t\topts = opts.Condense(m.ErrorFallback.GetModelProviderOptions(settings))\n\t}\n\n\tif m.LargeContextFallback != nil {\n\t\topts = opts.Condense(m.LargeContextFallback.GetModelProviderOptions(settings))\n\t}\n\n\tif m.LargeOutputFallback != nil {\n\t\topts = opts.Condense(m.LargeOutputFallback.GetModelProviderOptions(settings))\n\t}\n\n\tif m.StrongModel != nil {\n\t\topts = opts.Condense(m.StrongModel.GetModelProviderOptions(settings))\n\t}\n\n\treturn opts\n}\n\nfunc (m ModelProviderOptions) Condense(opts ...ModelProviderOptions) ModelProviderOptions {\n\tfor _, opt := range opts {\n\t\tfor composite, option := range opt {\n\t\t\texistingOption, exists := m[composite]\n\t\t\tif !exists {\n\t\t\t\t// first time seeing this composite, add directly\n\t\t\t\tm[composite] = ModelProviderOption{\n\t\t\t\t\tPublishers: copyPublishersMap(option.Publishers),\n\t\t\t\t\tConfig:     option.Config,\n\t\t\t\t\tPriority:   option.Priority,\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif option.Priority < existingOption.Priority {\n\t\t\t\texistingOption.Priority = option.Priority\n\t\t\t}\n\n\t\t\t// composite already exists, merge Publishers\n\t\t\tfor pub := range option.Publishers {\n\t\t\t\texistingOption.Publishers[pub] = true\n\t\t\t}\n\n\t\t\t// no need to overwrite Config, as it should be identical\n\t\t\tm[composite] = existingOption\n\t\t}\n\t}\n\treturn m\n}\n\nfunc copyPublishersMap(src map[ModelPublisher]bool) map[ModelPublisher]bool {\n\tcpy := make(map[ModelPublisher]bool, len(src))\n\tfor k, v := range src {\n\t\tcpy[k] = v\n\t}\n\treturn cpy\n}\n"
  },
  {
    "path": "app/shared/ai_models_custom.go",
    "content": "package shared\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n)\n\ntype SchemaUrl string\n\nconst (\n\tSchemaUrlInputConfig     SchemaUrl = \"https://plandex.ai/schemas/models-input.schema.json\"\n\tSchemaUrlPlanConfig      SchemaUrl = \"https://plandex.ai/schemas/plan-config.schema.json\"\n\tSchemaUrlInlineModelPack SchemaUrl = \"https://plandex.ai/schemas/model-pack-inline.schema.json\"\n)\n\n// Note that none of the custom model structs should have maps anywhere in the hierarchy, since it will break deterministic hashing. Use structs or slices instead.\n\ntype CustomModel struct {\n\tId          string         `json:\"id,omitempty\"`\n\tModelId     ModelId        `json:\"modelId\"`\n\tPublisher   ModelPublisher `json:\"publisher\"`\n\tDescription string         `json:\"description\"`\n\n\tBaseModelShared\n\n\tProviders []BaseModelUsesProvider `json:\"providers\"`\n\n\tCreatedAt *time.Time `json:\"createdAt,omitempty\"`\n\tUpdatedAt *time.Time `json:\"updatedAt,omitempty\"`\n}\n\ntype CustomProvider struct {\n\tId      string `json:\"id,omitempty\"`\n\tName    string `json:\"name\"`\n\tBaseUrl string `json:\"baseUrl\"`\n\n\t// for AWS Bedrock models\n\tHasAWSAuth bool `json:\"hasAWSAuth,omitempty\"`\n\n\t// for local models that don't require auth (ollama, etc.)\n\tSkipAuth bool `json:\"skipAuth,omitempty\"`\n\n\tApiKeyEnvVar  string                       `json:\"apiKeyEnvVar,omitempty\"`\n\tExtraAuthVars []ModelProviderExtraAuthVars `json:\"extraAuthVars,omitempty\"`\n\n\tCreatedAt *time.Time `json:\"createdAt,omitempty\"`\n\tUpdatedAt *time.Time `json:\"updatedAt,omitempty\"`\n}\n\ntype ModelsInput struct {\n\tCustomModels     []*CustomModel     `json:\"models,omitempty\"`\n\tCustomProviders  []*CustomProvider  `json:\"providers,omitempty\"`\n\tCustomModelPacks []*ModelPackSchema `json:\"modelPacks,omitempty\"`\n}\n\nfunc (input ModelsInput) FilterUnchanged(existing *ModelsInput) ModelsInput {\n\tfiltered := ModelsInput{}\n\n\texistingProvidersById := map[string]*CustomProvider{}\n\tfor _, provider := range existing.CustomProviders {\n\t\texistingProvidersById[provider.Name] = provider\n\t}\n\texistingModelsById := map[string]*CustomModel{}\n\tfor _, model := range existing.CustomModels {\n\t\texistingModelsById[string(model.ModelId)] = model\n\t}\n\texistingPacksById := map[string]*ModelPackSchema{}\n\tfor _, pack := range existing.CustomModelPacks {\n\t\texistingPacksById[pack.Name] = pack\n\t}\n\n\tfor _, model := range input.CustomModels {\n\t\tif existingModel, ok := existingModelsById[string(model.ModelId)]; !ok || !modelsEqual(model, existingModel) {\n\t\t\tfiltered.CustomModels = append(filtered.CustomModels, model)\n\t\t}\n\t}\n\n\tfor _, provider := range input.CustomProviders {\n\t\tif existingProvider, ok := existingProvidersById[provider.Name]; !ok || !providersEqual(provider, existingProvider) {\n\t\t\tfiltered.CustomProviders = append(filtered.CustomProviders, provider)\n\t\t}\n\t}\n\n\tfor _, pack := range input.CustomModelPacks {\n\t\tif existingPack, ok := existingPacksById[pack.Name]; !ok || !packsEqual(pack, existingPack) {\n\t\t\tfiltered.CustomModelPacks = append(filtered.CustomModelPacks, pack)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\nfunc (input ModelsInput) Equals(other ModelsInput) bool {\n\tleft := input.FilterUnchanged(&other)\n\tright := other.FilterUnchanged(&input)\n\n\treturn left.IsEmpty() && right.IsEmpty()\n}\n\nfunc (input ModelsInput) CheckNoDuplicates() (bool, string) {\n\tsawModelIds := map[ModelId]bool{}\n\tsawProviderNames := map[string]bool{}\n\tsawPackNames := map[string]bool{}\n\n\tbuilder := strings.Builder{}\n\n\tfor _, provider := range input.CustomProviders {\n\t\tif _, ok := sawProviderNames[provider.Name]; ok {\n\t\t\tbuilder.WriteString(fmt.Sprintf(\"• Provider %s is duplicated\\n\", provider.Name))\n\t\t}\n\t\tsawProviderNames[provider.Name] = true\n\t}\n\n\tfor _, model := range input.CustomModels {\n\t\tif _, ok := sawModelIds[model.ModelId]; ok {\n\t\t\tbuilder.WriteString(fmt.Sprintf(\"• Model %s is duplicated\\n\", model.ModelId))\n\t\t}\n\t\tsawModelIds[model.ModelId] = true\n\t}\n\n\tfor _, pack := range input.CustomModelPacks {\n\t\tif _, ok := sawPackNames[pack.Name]; ok {\n\t\t\tbuilder.WriteString(fmt.Sprintf(\"• Model pack %s is duplicated\\n\", pack.Name))\n\t\t}\n\t\tsawPackNames[pack.Name] = true\n\t}\n\n\tres := builder.String()\n\n\treturn len(res) == 0, res\n}\n\nfunc (input ModelsInput) IsEmpty() bool {\n\treturn len(input.CustomModels) == 0 && len(input.CustomProviders) == 0 && len(input.CustomModelPacks) == 0\n}\n\nfunc modelsEqual(a, b *CustomModel) bool {\n\treturn cmp.Equal(\n\t\ta, b,\n\t\tcmpopts.EquateEmpty(), // treat nil == empty slice/map\n\t\tcmpopts.IgnoreFields(CustomModel{}, \"CreatedAt\", \"UpdatedAt\", \"Id\"),\n\t)\n}\n\nfunc providersEqual(a, b *CustomProvider) bool {\n\treturn cmp.Equal(\n\t\ta,\n\t\tb,\n\t\tcmpopts.EquateEmpty(),\n\t\tcmpopts.IgnoreFields(CustomProvider{}, \"CreatedAt\", \"UpdatedAt\", \"Id\"),\n\t)\n}\n\nfunc packsEqual(a, b *ModelPackSchema) bool {\n\tres := cmp.Equal(\n\t\ta,\n\t\tb,\n\t\tcmpopts.EquateEmpty(),\n\t)\n\treturn res\n}\n\nfunc (s *ModelPackSchema) Equals(other *ModelPackSchema) bool {\n\treturn packsEqual(s, other)\n}\n\nfunc (mp *ModelPack) Equals(other *ModelPack) bool {\n\treturn mp.ToModelPackSchema().Equals(other.ToModelPackSchema())\n}\n\n// Hash returns a deterministic hash of the ModelsInput.\n// WARNING: This relies on json.Marshal being deterministic for our struct types.\n// Do not add map fields to these structs or the hash will become non-deterministic.\nfunc (input ModelsInput) Hash() (string, error) {\n\tdata, err := json.Marshal(input)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thash := sha256.Sum256(data)\n\treturn hex.EncodeToString(hash[:]), nil\n}\n\ntype ClientModelPackSchema struct {\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\n\tClientModelPackSchemaRoles\n}\n\nfunc (input *ClientModelPackSchema) ToModelPackSchema() *ModelPackSchema {\n\treturn &ModelPackSchema{\n\t\tName:                 input.Name,\n\t\tDescription:          input.Description,\n\t\tModelPackSchemaRoles: input.ClientModelPackSchemaRoles.ToModelPackSchemaRoles(),\n\t}\n}\n\nfunc (input *ModelPackSchema) ToClientModelPackSchema() *ClientModelPackSchema {\n\treturn &ClientModelPackSchema{\n\t\tName:                       input.Name,\n\t\tDescription:                input.Description,\n\t\tClientModelPackSchemaRoles: input.ToClientModelPackSchemaRoles(),\n\t}\n}\n\ntype ClientModelsInput struct {\n\tSchemaUrl SchemaUrl `json:\"$schema\"`\n\n\tCustomModels     []*CustomModel           `json:\"models,omitempty\"`\n\tCustomProviders  []*CustomProvider        `json:\"providers,omitempty\"`\n\tCustomModelPacks []*ClientModelPackSchema `json:\"modelPacks,omitempty\"`\n}\n\nfunc (input ClientModelsInput) ToModelsInput() ModelsInput {\n\tmodelPacks := []*ModelPackSchema{}\n\tfor _, pack := range input.CustomModelPacks {\n\t\tmodelPacks = append(modelPacks, pack.ToModelPackSchema())\n\t}\n\n\treturn ModelsInput{\n\t\tCustomModels:     input.CustomModels,\n\t\tCustomProviders:  input.CustomProviders,\n\t\tCustomModelPacks: modelPacks,\n\t}\n}\n\nfunc (input *ClientModelsInput) PrepareUpdate() {\n\tfor _, model := range input.CustomModels {\n\t\tmodel.Id = \"\"\n\t\tmodel.CreatedAt = nil\n\t\tmodel.UpdatedAt = nil\n\t}\n\n\tfor _, provider := range input.CustomProviders {\n\t\tprovider.Id = \"\"\n\t\tprovider.CreatedAt = nil\n\t\tprovider.UpdatedAt = nil\n\t}\n}\n\nfunc (input ModelsInput) ToClientModelsInput() ClientModelsInput {\n\tclientModelPacks := []*ClientModelPackSchema{}\n\tfor _, pack := range input.CustomModelPacks {\n\t\tclientModelPacks = append(clientModelPacks, pack.ToClientModelPackSchema())\n\t}\n\n\treturn ClientModelsInput{\n\t\tSchemaUrl:        SchemaUrlInputConfig,\n\t\tCustomModels:     input.CustomModels,\n\t\tCustomProviders:  input.CustomProviders,\n\t\tCustomModelPacks: clientModelPacks,\n\t}\n}\n\nfunc (cp *CustomProvider) ToModelProviderConfigSchema() ModelProviderConfigSchema {\n\treturn ModelProviderConfigSchema{\n\t\tProvider:       ModelProviderCustom,\n\t\tCustomProvider: &cp.Name,\n\t\tBaseUrl:        cp.BaseUrl,\n\t\tHasAWSAuth:     cp.HasAWSAuth,\n\t\tSkipAuth:       cp.SkipAuth,\n\t\tApiKeyEnvVar:   cp.ApiKeyEnvVar,\n\t\tExtraAuthVars:  cp.ExtraAuthVars,\n\t}\n}\n\nfunc (input *CustomModel) ToBaseModelConfig(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) *BaseModelConfig {\n\tproviders := GetProvidersForAuthVarsWithModelId(authVars, settings, input.ModelId, orgUserConfig)\n\n\tif len(providers) == 0 {\n\t\treturn nil\n\t}\n\n\tproviderSchema := providers[0]\n\treturn input.ToBaseModelConfigForProvider(authVars, settings, &providerSchema)\n}\n\nfunc (input *CustomModel) ToBaseModelConfigForProvider(authVars map[string]string, settings *PlanSettings, providerSchema *ModelProviderConfigSchema) *BaseModelConfig {\n\tvar modelName ModelName\n\tfor _, usesProvider := range input.Providers {\n\t\tif usesProvider.Provider == providerSchema.Provider {\n\t\t\tmodelName = usesProvider.ModelName\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn &BaseModelConfig{\n\t\tModelTag:        ModelTag(input.ModelId),\n\t\tModelId:         input.ModelId,\n\t\tPublisher:       input.Publisher,\n\t\tBaseModelShared: input.BaseModelShared,\n\t\tBaseModelProviderConfig: BaseModelProviderConfig{\n\t\t\tModelProviderConfigSchema: *providerSchema,\n\t\t\tModelName:                 modelName,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "app/shared/ai_models_data_models.go",
    "content": "package shared\n\nimport (\n\t\"crypto/sha256\"\n\t\"database/sql/driver\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype ModelCompatibility struct {\n\tHasImageSupport bool `json:\"hasImageSupport\"`\n}\n\ntype ModelOutputFormat string\n\nconst (\n\tModelOutputFormatToolCallJson ModelOutputFormat = \"tool-call-json\"\n\tModelOutputFormatXml          ModelOutputFormat = \"xml\"\n)\n\n// to help avoid confusion between model tag, price id, model name, and the model id\ntype ModelName string\ntype ModelId string\ntype ModelTag string\ntype VariantTag string\n\ntype BaseModelShared struct {\n\tDefaultMaxConvoTokens       int               `json:\"defaultMaxConvoTokens\"`\n\tMaxTokens                   int               `json:\"maxTokens\"`\n\tMaxOutputTokens             int               `json:\"maxOutputTokens\"`\n\tReservedOutputTokens        int               `json:\"reservedOutputTokens\"`\n\tPreferredOutputFormat       ModelOutputFormat `json:\"preferredOutputFormat\"`\n\tSystemPromptDisabled        bool              `json:\"systemPromptDisabled,omitempty\"`\n\tRoleParamsDisabled          bool              `json:\"roleParamsDisabled,omitempty\"`\n\tStopDisabled                bool              `json:\"stopDisabled,omitempty\"`\n\tPredictedOutputEnabled      bool              `json:\"predictedOutputEnabled,omitempty\"`\n\tReasoningEffortEnabled      bool              `json:\"reasoningEffortEnabled,omitempty\"`\n\tReasoningEffort             ReasoningEffort   `json:\"reasoningEffort,omitempty\"`\n\tIncludeReasoning            bool              `json:\"includeReasoning,omitempty\"`\n\tHideReasoning               bool              `json:\"hideReasoning,omitempty\"`\n\tReasoningBudget             int               `json:\"reasoningBudget,omitempty\"`\n\tSupportsCacheControl        bool              `json:\"supportsCacheControl,omitempty\"`\n\tSingleMessageNoSystemPrompt bool              `json:\"singleMessageNoSystemPrompt,omitempty\"`\n\tTokenEstimatePaddingPct     float64           `json:\"tokenEstimatePaddingPct,omitempty\"`\n\tModelCompatibility\n}\n\ntype BaseModelProviderConfig struct {\n\tModelProviderConfigSchema\n\tModelName ModelName `json:\"modelName\"`\n}\n\ntype BaseModelConfig struct {\n\tModelTag  ModelTag       `json:\"modelTag\"`\n\tModelId   ModelId        `json:\"modelId\"`\n\tPublisher ModelPublisher `json:\"publisher,omitempty\"`\n\tBaseModelShared\n\tBaseModelProviderConfig\n}\n\ntype BaseModelUsesProvider struct {\n\tProvider       ModelProvider `json:\"provider\"`\n\tCustomProvider *string       `json:\"customProvider,omitempty\"`\n\tModelName      ModelName     `json:\"modelName\"`\n}\n\nfunc (b BaseModelUsesProvider) ToComposite() string {\n\tif b.CustomProvider != nil {\n\t\treturn fmt.Sprintf(\"%s|%s\", b.Provider, *b.CustomProvider)\n\t}\n\treturn string(b.Provider)\n}\n\ntype BaseModelConfigSchema struct {\n\tModelTag    ModelTag       `json:\"modelTag\"`\n\tModelId     ModelId        `json:\"modelId\"`\n\tPublisher   ModelPublisher `json:\"publisher\"`\n\tDescription string         `json:\"description\"`\n\n\tBaseModelShared\n\n\tRequiresVariantOverrides []string `json:\"requiresVariantOverrides\"`\n\n\tVariants  []BaseModelConfigVariant `json:\"variants\"`\n\tProviders []BaseModelUsesProvider  `json:\"providers\"`\n}\n\ntype BaseModelConfigVariant struct {\n\tIsBaseVariant            bool                     `json:\"isBaseVariant\"`\n\tVariantTag               VariantTag               `json:\"variantTag\"`\n\tDescription              string                   `json:\"description\"`\n\tOverrides                BaseModelShared          `json:\"overrides\"`\n\tVariants                 []BaseModelConfigVariant `json:\"variants\"`\n\tRequiresVariantOverrides []string                 `json:\"requiresVariantOverrides\"`\n\tIsDefaultVariant         bool                     `json:\"isDefaultVariant\"`\n}\n\nfunc (b *BaseModelConfigSchema) IsLocalOnly() bool {\n\tif len(b.Providers) == 0 {\n\t\treturn false\n\t}\n\n\tfor _, provider := range b.Providers {\n\t\tbuiltIn, ok := BuiltInModelProviderConfigs[provider.Provider]\n\t\tif !ok {\n\t\t\t// has a custom provider—assume not local only\n\t\t\treturn false\n\t\t}\n\t\tif !builtIn.LocalOnly {\n\t\t\t// has a built-in provider that is not local only\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (b *BaseModelConfigSchema) ToAvailableModels() []*AvailableModel {\n\tavail := []*AvailableModel{}\n\tfor _, provider := range b.Providers {\n\n\t\tproviderConfig, ok := BuiltInModelProviderConfigs[provider.Provider]\n\t\tif !ok {\n\t\t\tpanic(fmt.Sprintf(\"provider %s not found\", provider.Provider))\n\t\t}\n\n\t\taddBase := func() {\n\t\t\tavail = append(avail, &AvailableModel{\n\t\t\t\tDescription:           b.Description,\n\t\t\t\tDefaultMaxConvoTokens: b.DefaultMaxConvoTokens,\n\t\t\t\tBaseModelConfig: BaseModelConfig{\n\t\t\t\t\tModelTag:        b.ModelTag,\n\t\t\t\t\tModelId:         ModelId(string(b.ModelTag)),\n\t\t\t\t\tBaseModelShared: b.BaseModelShared,\n\t\t\t\t\tBaseModelProviderConfig: BaseModelProviderConfig{\n\t\t\t\t\t\tModelProviderConfigSchema: providerConfig,\n\t\t\t\t\t\tModelName:                 provider.ModelName,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\ttype variantParams struct {\n\t\t\tBaseVariant              *BaseModelConfigVariant\n\t\t\tBaseId                   ModelId\n\t\t\tBaseDescription          string\n\t\t\tCumulativeOverrides      BaseModelShared\n\t\t\tRequiresVariantOverrides []string\n\t\t}\n\n\t\taddBaseVariant := func(params variantParams) {\n\t\t\tif params.BaseVariant == nil {\n\t\t\t\taddBase()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbaseDescription := params.BaseVariant.Description\n\t\t\tbaseId := params.BaseId\n\t\t\tmergedOverrides := params.CumulativeOverrides\n\n\t\t\tavail = append(avail, &AvailableModel{\n\t\t\t\tDescription:           baseDescription,\n\t\t\t\tDefaultMaxConvoTokens: b.DefaultMaxConvoTokens,\n\t\t\t\tBaseModelConfig: BaseModelConfig{\n\t\t\t\t\tModelTag:        b.ModelTag,\n\t\t\t\t\tModelId:         baseId,\n\t\t\t\t\tPublisher:       b.Publisher,\n\t\t\t\t\tBaseModelShared: mergedOverrides,\n\t\t\t\t\tBaseModelProviderConfig: BaseModelProviderConfig{\n\t\t\t\t\t\tModelProviderConfigSchema: providerConfig,\n\t\t\t\t\t\tModelName:                 provider.ModelName,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\tif len(b.Variants) == 0 {\n\t\t\taddBase()\n\t\t} else {\n\n\t\t\tvar addVariants func(variants []BaseModelConfigVariant, baseParams variantParams)\n\t\t\taddVariants = func(variants []BaseModelConfigVariant, baseParams variantParams) {\n\t\t\t\tfor _, variant := range variants {\n\t\t\t\t\tif variant.IsBaseVariant {\n\t\t\t\t\t\taddBaseVariant(baseParams)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(baseParams.RequiresVariantOverrides) > 0 {\n\t\t\t\t\t\tok, missing := FieldsDefined(variant.Overrides, baseParams.RequiresVariantOverrides)\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\tpanic(fmt.Sprintf(\"variant %s is missing required field %s\", variant.VariantTag, missing))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tvar baseId ModelId\n\t\t\t\t\tvar baseDescription string\n\t\t\t\t\tif baseParams.BaseId != \"\" {\n\t\t\t\t\t\tbaseId = baseParams.BaseId\n\t\t\t\t\t\tbaseDescription = baseParams.BaseDescription\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbaseId = ModelId(string(b.ModelTag))\n\t\t\t\t\t\tbaseDescription = b.Description\n\t\t\t\t\t}\n\n\t\t\t\t\tvar modelId ModelId\n\t\t\t\t\tif variant.IsDefaultVariant {\n\t\t\t\t\t\tmodelId = baseId\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmodelId = ModelId(strings.Join([]string{string(baseId), string(variant.VariantTag)}, \"-\"))\n\t\t\t\t\t}\n\n\t\t\t\t\tdescription := strings.Join([]string{baseDescription, variant.Description}, \" \")\n\n\t\t\t\t\tmerged := Merge(baseParams.CumulativeOverrides, variant.Overrides)\n\n\t\t\t\t\tif len(variant.Variants) > 0 {\n\t\t\t\t\t\taddVariants(variant.Variants, variantParams{\n\t\t\t\t\t\t\tBaseVariant:              &variant,\n\t\t\t\t\t\t\tBaseId:                   modelId,\n\t\t\t\t\t\t\tBaseDescription:          description,\n\t\t\t\t\t\t\tCumulativeOverrides:      merged,\n\t\t\t\t\t\t\tRequiresVariantOverrides: variant.RequiresVariantOverrides,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tavail = append(avail, &AvailableModel{\n\t\t\t\t\t\tDescription:           description,\n\t\t\t\t\t\tDefaultMaxConvoTokens: b.DefaultMaxConvoTokens,\n\t\t\t\t\t\tBaseModelConfig: BaseModelConfig{\n\t\t\t\t\t\t\tModelTag:        b.ModelTag,\n\t\t\t\t\t\t\tModelId:         modelId,\n\t\t\t\t\t\t\tBaseModelShared: merged,\n\t\t\t\t\t\t\tBaseModelProviderConfig: BaseModelProviderConfig{\n\t\t\t\t\t\t\t\tModelProviderConfigSchema: providerConfig,\n\t\t\t\t\t\t\t\tModelName:                 provider.ModelName,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\taddVariants(b.Variants, variantParams{\n\t\t\t\tBaseId:                   ModelId(string(b.ModelTag)),\n\t\t\t\tBaseDescription:          b.Description,\n\t\t\t\tCumulativeOverrides:      b.BaseModelShared,\n\t\t\t\tRequiresVariantOverrides: b.RequiresVariantOverrides,\n\t\t\t})\n\t\t}\n\t}\n\treturn avail\n}\n\ntype AvailableModel struct {\n\tId string `json:\"id\"`\n\tBaseModelConfig\n\tDescription           string    `json:\"description\"`\n\tDefaultMaxConvoTokens int       `json:\"defaultMaxConvoTokens\"`\n\tCreatedAt             time.Time `json:\"createdAt\"`\n\tUpdatedAt             time.Time `json:\"updatedAt\"`\n}\n\nfunc (m *AvailableModel) ModelString() string {\n\ts := \"\"\n\tif m.Provider != \"\" && m.Provider != ModelProviderOpenAI {\n\t\ts += string(m.Provider) + \"/\"\n\t}\n\ts += string(m.ModelId)\n\treturn s\n}\n\ntype PlannerModelConfig struct {\n\tMaxConvoTokens int `json:\"maxConvoTokens\"`\n}\n\ntype ReasoningEffort string\n\nconst (\n\tReasoningEffortLow    ReasoningEffort = \"low\"\n\tReasoningEffortMedium ReasoningEffort = \"medium\"\n\tReasoningEffortHigh   ReasoningEffort = \"high\"\n)\n\ntype ModelRoleConfig struct {\n\tRole ModelRole `json:\"role\"`\n\n\tModelId ModelId `json:\"modelId\"` // new in 2.2.0 refactor — uses provider lookup instead of BaseModelConfig and MissingKeyFallback\n\n\tBaseModelConfig      *BaseModelConfig `json:\"baseModelConfig,omitempty\"`\n\tTemperature          float32          `json:\"temperature\"`\n\tTopP                 float32          `json:\"topP\"`\n\tReservedOutputTokens int              `json:\"reservedOutputTokens\"`\n\n\tLargeContextFallback *ModelRoleConfig `json:\"largeContextFallback\"`\n\tLargeOutputFallback  *ModelRoleConfig `json:\"largeOutputFallback\"`\n\tErrorFallback        *ModelRoleConfig `json:\"errorFallback\"`\n\t// MissingKeyFallback   *ModelRoleConfig `json:\"missingKeyFallback\"` // removed in 2.2.0 refactor —\n\tStrongModel *ModelRoleConfig `json:\"strongModel\"`\n\n\tLocalProvider ModelProvider `json:\"localProvider,omitempty\"`\n}\n\ntype ModelRoleModelConfig struct {\n\tProvider       ModelProvider `json:\"provider\"`\n\tCustomProvider *string       `json:\"customProvider,omitempty\"`\n\tModelTag       ModelTag      `json:\"modelTag\"`\n}\n\ntype ModelRoleConfigSchema struct {\n\tModelId ModelId `json:\"modelId\"`\n\n\tTemperature          *float32 `json:\"temperature,omitempty\"`\n\tTopP                 *float32 `json:\"topP,omitempty\"`\n\tReservedOutputTokens *int     `json:\"reservedOutputTokens,omitempty\"`\n\tMaxConvoTokens       *int     `json:\"maxConvoTokens,omitempty\"`\n\n\tLargeContextFallback *ModelRoleConfigSchema `json:\"largeContextFallback,omitempty\"`\n\tLargeOutputFallback  *ModelRoleConfigSchema `json:\"largeOutputFallback,omitempty\"`\n\tErrorFallback        *ModelRoleConfigSchema `json:\"errorFallback,omitempty\"`\n\tStrongModel          *ModelRoleConfigSchema `json:\"strongModel,omitempty\"`\n}\n\n// ToClientVal returns either:\n//   - string  – when the value (or any nested fallback) is just a bare role\n//   - map[string]any – when additional fields are set, with all fallbacks\n//     processed recursively.\nfunc (m *ModelRoleConfigSchema) ToClientVal() RoleJSON {\n\tif m == nil {\n\t\treturn nil\n\t}\n\tif m.bareRole() {\n\t\treturn string(m.ModelId)\n\t}\n\n\tout := map[string]any{\n\t\t\"modelId\": string(m.ModelId),\n\t}\n\n\t// simple optional scalars\n\tif m.Temperature != nil {\n\t\tout[\"temperature\"] = *m.Temperature\n\t}\n\tif m.TopP != nil {\n\t\tout[\"topP\"] = *m.TopP\n\t}\n\tif m.ReservedOutputTokens != nil {\n\t\tout[\"reservedOutputTokens\"] = *m.ReservedOutputTokens\n\t}\n\tif m.MaxConvoTokens != nil {\n\t\tout[\"maxConvoTokens\"] = *m.MaxConvoTokens\n\t}\n\n\t// recurse on each fallback, collapsing to string when bare\n\tif m.LargeContextFallback != nil {\n\t\tout[\"largeContextFallback\"] = m.LargeContextFallback.ToClientVal()\n\t}\n\tif m.LargeOutputFallback != nil {\n\t\tout[\"largeOutputFallback\"] = m.LargeOutputFallback.ToClientVal()\n\t}\n\tif m.ErrorFallback != nil {\n\t\tout[\"errorFallback\"] = m.ErrorFallback.ToClientVal()\n\t}\n\tif m.StrongModel != nil {\n\t\tout[\"strongModel\"] = m.StrongModel.ToClientVal()\n\t}\n\n\treturn out\n}\n\n// bareRole returns true if *every* field except ModelId is nil / zero.\nfunc (m *ModelRoleConfigSchema) bareRole() bool {\n\tif m == nil {\n\t\treturn true\n\t}\n\n\tv := reflect.ValueOf(*m)\n\tt := v.Type()\n\n\tfor i := 0; i < v.NumField(); i++ {\n\t\tf := t.Field(i)\n\t\tif f.Name == \"ModelId\" { // skip the sentinel field\n\t\t\tcontinue\n\t\t}\n\n\t\tfv := v.Field(i)\n\n\t\tswitch fv.Kind() {\n\t\tcase reflect.Pointer, reflect.Interface, reflect.Map, reflect.Slice:\n\t\t\tif !fv.IsNil() {\n\t\t\t\treturn false\n\t\t\t}\n\t\tdefault:\n\t\t\tif !fv.IsZero() {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (m *ModelRoleConfigSchema) AllModelIds() []ModelId {\n\tids := []ModelId{}\n\n\tif m.ModelId != \"\" {\n\t\tids = append(ids, m.ModelId)\n\t}\n\n\tif m.LargeContextFallback != nil {\n\t\tids = append(ids, m.LargeContextFallback.AllModelIds()...)\n\t}\n\n\tif m.LargeOutputFallback != nil {\n\t\tids = append(ids, m.LargeOutputFallback.AllModelIds()...)\n\t}\n\n\tif m.ErrorFallback != nil {\n\t\tids = append(ids, m.ErrorFallback.AllModelIds()...)\n\t}\n\n\tif m.StrongModel != nil {\n\t\tids = append(ids, m.StrongModel.AllModelIds()...)\n\t}\n\n\treturn ids\n}\n\nfunc (m *ModelRoleConfigSchema) ToModelRoleConfig(role ModelRole) ModelRoleConfig {\n\treturn m.toModelRoleConfig(role)\n}\n\nfunc (m *ModelRoleConfigSchema) toModelRoleConfig(role ModelRole) ModelRoleConfig {\n\tvar largeContextFallback *ModelRoleConfig\n\tif m.LargeContextFallback != nil {\n\t\tc := m.LargeContextFallback.ToModelRoleConfig(role)\n\t\tlargeContextFallback = &c\n\t}\n\tvar largeOutputFallback *ModelRoleConfig\n\tif m.LargeOutputFallback != nil {\n\t\tc := m.LargeOutputFallback.ToModelRoleConfig(role)\n\t\tlargeOutputFallback = &c\n\t}\n\tvar errorFallback *ModelRoleConfig\n\tif m.ErrorFallback != nil {\n\t\tc := m.ErrorFallback.ToModelRoleConfig(role)\n\t\terrorFallback = &c\n\t}\n\tvar strongModel *ModelRoleConfig\n\tif m.StrongModel != nil {\n\t\tc := m.StrongModel.ToModelRoleConfig(role)\n\t\tstrongModel = &c\n\t}\n\n\ttemperature := m.Temperature\n\ttopP := m.TopP\n\n\tconfig := DefaultConfigByRole[role]\n\n\tif temperature == nil {\n\t\ttemperature = &config.Temperature\n\t}\n\tif topP == nil {\n\t\ttopP = &config.TopP\n\t}\n\n\tvar reservedOutputTokens int\n\tif m.ReservedOutputTokens != nil {\n\t\treservedOutputTokens = *m.ReservedOutputTokens\n\t}\n\n\treturn ModelRoleConfig{\n\t\tRole: role,\n\n\t\tModelId: m.ModelId,\n\n\t\tTemperature:          *temperature,\n\t\tTopP:                 *topP,\n\t\tReservedOutputTokens: reservedOutputTokens,\n\n\t\tLargeContextFallback: largeContextFallback,\n\t\tLargeOutputFallback:  largeOutputFallback,\n\t\tErrorFallback:        errorFallback,\n\t\tStrongModel:          strongModel,\n\t}\n}\n\nfunc (m *ModelRoleConfigSchema) MarshalJSON() ([]byte, error) {\n\tif m == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\tif m.bareRole() {\n\t\tif m.ModelId == \"\" {\n\t\t\treturn []byte(\"null\"), nil\n\t\t}\n\n\t\treturn json.Marshal(string(m.ModelId)) // compact form\n\t}\n\ttype alias ModelRoleConfigSchema\n\treturn json.Marshal((*alias)(m)) // full object\n}\n\nfunc (m *ModelRoleConfigSchema) UnmarshalJSON(data []byte) error {\n\t// attempt the short string first\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err == nil {\n\t\t*m = ModelRoleConfigSchema{ModelId: ModelId(s)}\n\t\treturn nil\n\t}\n\t// fallback to full object\n\ttype alias ModelRoleConfigSchema\n\treturn json.Unmarshal(data, (*alias)(m))\n}\n\nfunc (m *ModelRoleConfig) ToModelRoleConfigSchema() ModelRoleConfigSchema {\n\tvar largeContextFallback *ModelRoleConfigSchema\n\tif m.LargeContextFallback != nil {\n\t\tc := m.LargeContextFallback.ToModelRoleConfigSchema()\n\t\tlargeContextFallback = &c\n\t}\n\tvar largeOutputFallback *ModelRoleConfigSchema\n\tif m.LargeOutputFallback != nil {\n\t\tc := m.LargeOutputFallback.ToModelRoleConfigSchema()\n\t\tlargeOutputFallback = &c\n\t}\n\tvar errorFallback *ModelRoleConfigSchema\n\tif m.ErrorFallback != nil {\n\t\tc := m.ErrorFallback.ToModelRoleConfigSchema()\n\t\terrorFallback = &c\n\t}\n\tvar strongModel *ModelRoleConfigSchema\n\tif m.StrongModel != nil {\n\t\tc := m.StrongModel.ToModelRoleConfigSchema()\n\t\tstrongModel = &c\n\t}\n\n\tdefaultConfig := DefaultConfigByRole[m.Role]\n\n\tvar temperature *float32\n\tvar topP *float32\n\tvar reservedOutputTokens *int\n\n\tif m.Temperature != defaultConfig.Temperature {\n\t\ttemperature = &m.Temperature\n\t}\n\tif m.TopP != defaultConfig.TopP {\n\t\ttopP = &m.TopP\n\t}\n\n\tif m.ReservedOutputTokens != 0 {\n\t\treservedOutputTokens = &m.ReservedOutputTokens\n\t}\n\n\treturn ModelRoleConfigSchema{\n\t\tModelId:              m.GetModelId(),\n\t\tTemperature:          temperature,\n\t\tTopP:                 topP,\n\t\tReservedOutputTokens: reservedOutputTokens,\n\t\tLargeContextFallback: largeContextFallback,\n\t\tLargeOutputFallback:  largeOutputFallback,\n\t\tErrorFallback:        errorFallback,\n\t\tStrongModel:          strongModel,\n\t}\n}\n\nfunc (p PlannerRoleConfig) ToModelRoleConfigSchema() ModelRoleConfigSchema {\n\ts := p.ModelRoleConfig.ToModelRoleConfigSchema()\n\n\tvar maxConvoTokens *int\n\tif p.MaxConvoTokens != 0 {\n\t\tmaxConvoTokens = &p.MaxConvoTokens\n\t}\n\n\ts.MaxConvoTokens = maxConvoTokens\n\treturn s\n}\n\nfunc (m ModelRoleConfig) GetModelId() ModelId {\n\tif m.BaseModelConfig != nil {\n\t\treturn m.BaseModelConfig.ModelId\n\t}\n\n\treturn m.ModelId\n}\n\nfunc (m ModelRoleConfig) GetBaseModelConfig(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) *BaseModelConfig {\n\tfoundProvider := m.GetFirstProviderForAuthVars(authVars, settings, orgUserConfig)\n\tif foundProvider == nil {\n\t\treturn nil\n\t}\n\n\treturn m.GetBaseModelConfigForProvider(authVars, settings, foundProvider)\n}\n\nfunc (m ModelRoleConfig) GetBaseModelConfigForProvider(authVars map[string]string, settings *PlanSettings, providerSchema *ModelProviderConfigSchema) *BaseModelConfig {\n\tif m.BaseModelConfig != nil {\n\t\treturn m.BaseModelConfig\n\t}\n\n\tavailableModel := GetAvailableModel(providerSchema.Provider, m.ModelId)\n\tif availableModel != nil {\n\t\tc := availableModel.BaseModelConfig\n\t\treturn &c\n\t}\n\n\tvar customModel *CustomModel\n\tif settings != nil {\n\t\tcustomModel = settings.CustomModelsById[m.ModelId]\n\t}\n\tif customModel != nil {\n\t\tc := customModel.ToBaseModelConfigForProvider(authVars, settings, providerSchema)\n\t\treturn c\n\t}\n\n\treturn nil\n}\n\nfunc (m ModelRoleConfig) GetProviderComposite(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) string {\n\tbaseModelConfig := m.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\n\tif baseModelConfig == nil {\n\t\treturn \"\"\n\t}\n\n\treturn baseModelConfig.ToComposite()\n}\n\nfunc (m ModelRoleConfig) GetReservedOutputTokens(customModelsById map[ModelId]*CustomModel) int {\n\tif m.ReservedOutputTokens > 0 {\n\t\treturn m.ReservedOutputTokens\n\t}\n\n\tsharedBaseConfig := m.GetSharedBaseConfigWithCustomModels(customModelsById)\n\treturn sharedBaseConfig.ReservedOutputTokens\n}\n\nfunc (m ModelRoleConfig) GetSharedBaseConfig(settings *PlanSettings) *BaseModelShared {\n\treturn m.GetSharedBaseConfigWithCustomModels(settings.CustomModelsById)\n}\n\nfunc (m ModelRoleConfig) GetSharedBaseConfigWithCustomModels(customModels map[ModelId]*CustomModel) *BaseModelShared {\n\tif m.BaseModelConfig != nil {\n\t\treturn &m.BaseModelConfig.BaseModelShared\n\t}\n\n\tbuiltInModel := BuiltInBaseModelsById[m.ModelId]\n\tif builtInModel != nil {\n\t\treturn &builtInModel.BaseModelShared\n\t}\n\n\tcustomModel := customModels[m.ModelId]\n\tif customModel != nil {\n\t\treturn &customModel.BaseModelShared\n\t}\n\n\treturn nil\n}\n\nfunc (m *ModelRoleConfig) Scan(src interface{}) error {\n\tif src == nil {\n\t\treturn nil\n\t}\n\tswitch s := src.(type) {\n\tcase []byte:\n\t\treturn json.Unmarshal(s, m)\n\tcase string:\n\t\treturn json.Unmarshal([]byte(s), m)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported data type: %T\", src)\n\t}\n}\n\nfunc (m ModelRoleConfig) Value() (driver.Value, error) {\n\treturn json.Marshal(m)\n}\n\ntype PlannerRoleConfig struct {\n\tModelRoleConfig\n\tPlannerModelConfig\n}\n\nfunc (p *PlannerRoleConfig) Scan(src interface{}) error {\n\tif src == nil {\n\t\treturn nil\n\t}\n\tswitch s := src.(type) {\n\tcase []byte:\n\t\treturn json.Unmarshal(s, p)\n\tcase string:\n\t\treturn json.Unmarshal([]byte(s), p)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported data type: %T\", src)\n\t}\n}\n\nfunc (p PlannerRoleConfig) Value() (driver.Value, error) {\n\treturn json.Marshal(p)\n}\n\nfunc (p PlannerRoleConfig) GetMaxConvoTokens(settings *PlanSettings) int {\n\tif p.MaxConvoTokens > 0 {\n\t\treturn p.MaxConvoTokens\n\t}\n\n\treturn p.ModelRoleConfig.GetSharedBaseConfig(settings).DefaultMaxConvoTokens\n}\n\ntype RoleJSON any\n\ntype ClientModelPackSchemaRoles struct {\n\tSchemaUrl SchemaUrl `json:\"$schema,omitempty\"`\n\n\tLocalProvider ModelProvider `json:\"localProvider,omitempty\"`\n\n\t// in the JSON, these can either be a role as a string or a ModelRoleConfigSchema object for more complex config\n\tPlanner          RoleJSON `json:\"planner\"`\n\tArchitect        RoleJSON `json:\"architect,omitempty\"`\n\tCoder            RoleJSON `json:\"coder,omitempty\"`\n\tPlanSummary      RoleJSON `json:\"summarizer\"`\n\tBuilder          RoleJSON `json:\"builder\"`\n\tWholeFileBuilder RoleJSON `json:\"wholeFileBuilder,omitempty\"`\n\tNamer            RoleJSON `json:\"names\"`\n\tCommitMsg        RoleJSON `json:\"commitMessages\"`\n\tExecStatus       RoleJSON `json:\"autoContinue\"`\n}\n\nfunc (c *ClientModelPackSchemaRoles) ToModelPackSchemaRoles() ModelPackSchemaRoles {\n\tres := ModelPackSchemaRoles{\n\t\tLocalProvider: c.LocalProvider,\n\t}\n\n\tvar convertField func(field interface{}) *ModelRoleConfigSchema\n\tconvertField = func(field interface{}) *ModelRoleConfigSchema {\n\t\tif field == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch v := field.(type) {\n\t\tcase string:\n\t\t\t// It's a string, handle accordingly\n\t\t\treturn &ModelRoleConfigSchema{\n\t\t\t\tModelId: ModelId(v),\n\t\t\t}\n\t\tcase map[string]any:\n\t\t\t// re-marshal then unmarshal into the right struct\n\t\t\tb, _ := json.Marshal(v)\n\t\t\tvar m ModelRoleConfigSchema\n\t\t\tif err := json.Unmarshal(b, &m); err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Now handle the fallback fields recursively\n\t\t\tif fallback, ok := v[\"largeContextFallback\"]; ok && fallback != nil {\n\t\t\t\tm.LargeContextFallback = convertField(fallback)\n\t\t\t}\n\t\t\tif fallback, ok := v[\"largeOutputFallback\"]; ok && fallback != nil {\n\t\t\t\tm.LargeOutputFallback = convertField(fallback)\n\t\t\t}\n\t\t\tif fallback, ok := v[\"errorFallback\"]; ok && fallback != nil {\n\t\t\t\tm.ErrorFallback = convertField(fallback)\n\t\t\t}\n\t\t\tif fallback, ok := v[\"strongModel\"]; ok && fallback != nil {\n\t\t\t\tm.StrongModel = convertField(fallback)\n\t\t\t}\n\n\t\t\treturn &m\n\t\tdefault:\n\t\t\t// Handle unexpected type - you might want to log or panic\n\t\t\t// Or try to convert from a map if it's coming from JSON\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Convert each field\n\tres.Planner = *convertField(c.Planner)\n\tif c.Coder != nil {\n\t\tconverted := convertField(c.Coder)\n\t\tres.Coder = converted\n\t}\n\tres.PlanSummary = *convertField(c.PlanSummary)\n\tres.Builder = *convertField(c.Builder)\n\tif c.WholeFileBuilder != nil {\n\t\tconverted := convertField(c.WholeFileBuilder)\n\t\tres.WholeFileBuilder = converted\n\t}\n\tres.Namer = *convertField(c.Namer)\n\tres.CommitMsg = *convertField(c.CommitMsg)\n\tres.ExecStatus = *convertField(c.ExecStatus)\n\tif c.Architect != nil {\n\t\tconverted := convertField(c.Architect)\n\t\tres.Architect = converted\n\t}\n\n\treturn res\n}\n\ntype ModelPackSchemaRoles struct {\n\tLocalProvider    ModelProvider          `json:\"localProvider,omitempty\"`\n\tPlanner          ModelRoleConfigSchema  `json:\"planner\"`\n\tCoder            *ModelRoleConfigSchema `json:\"coder,omitempty\"`\n\tPlanSummary      ModelRoleConfigSchema  `json:\"planSummary\"`\n\tBuilder          ModelRoleConfigSchema  `json:\"builder\"`\n\tWholeFileBuilder *ModelRoleConfigSchema `json:\"wholeFileBuilder,omitempty\"` // optional, defaults to builder model — access via GetWholeFileBuilder()\n\tNamer            ModelRoleConfigSchema  `json:\"namer\"`\n\tCommitMsg        ModelRoleConfigSchema  `json:\"commitMsg\"`\n\tExecStatus       ModelRoleConfigSchema  `json:\"execStatus\"`\n\tArchitect        *ModelRoleConfigSchema `json:\"contextLoader,omitempty\"`\n}\n\nfunc (m *ModelPackSchemaRoles) ToClientModelPackSchemaRoles() ClientModelPackSchemaRoles {\n\tres := ClientModelPackSchemaRoles{\n\t\tSchemaUrl:     SchemaUrlInlineModelPack,\n\t\tLocalProvider: m.LocalProvider,\n\t}\n\n\tres.Planner = m.Planner.ToClientVal()\n\tif m.Coder != nil {\n\t\tval := m.Coder.ToClientVal()\n\t\tres.Coder = &val\n\t}\n\tres.PlanSummary = m.PlanSummary.ToClientVal()\n\tres.Builder = m.Builder.ToClientVal()\n\tif m.WholeFileBuilder != nil {\n\t\tval := m.WholeFileBuilder.ToClientVal()\n\t\tres.WholeFileBuilder = &val\n\t}\n\tres.Namer = m.Namer.ToClientVal()\n\tres.CommitMsg = m.CommitMsg.ToClientVal()\n\tres.ExecStatus = m.ExecStatus.ToClientVal()\n\tif m.Architect != nil {\n\t\tval := m.Architect.ToClientVal()\n\t\tres.Architect = &val\n\t}\n\n\treturn res\n}\n\ntype ModelPackSchema struct {\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n\n\tModelPackSchemaRoles\n}\n\nfunc (m *ModelPackSchema) AllModelIds() []ModelId {\n\tids := []ModelId{}\n\n\tids = append(ids, m.Planner.AllModelIds()...)\n\n\tif m.Coder != nil {\n\t\tids = append(ids, m.Coder.AllModelIds()...)\n\t}\n\n\tids = append(ids, m.PlanSummary.AllModelIds()...)\n\tids = append(ids, m.Builder.AllModelIds()...)\n\n\tif m.WholeFileBuilder != nil {\n\t\tids = append(ids, m.WholeFileBuilder.AllModelIds()...)\n\t}\n\n\tids = append(ids, m.Namer.AllModelIds()...)\n\tids = append(ids, m.CommitMsg.AllModelIds()...)\n\tids = append(ids, m.ExecStatus.AllModelIds()...)\n\n\tif m.Architect != nil {\n\t\tids = append(ids, m.Architect.AllModelIds()...)\n\t}\n\n\treturn ids\n}\n\nfunc (m *ModelPackSchema) ToModelPack() ModelPack {\n\tvar (\n\t\tcoder            *ModelRoleConfig\n\t\twholeFileBuilder *ModelRoleConfig\n\t\tarchitect        *ModelRoleConfig\n\t)\n\n\tif m.Coder != nil {\n\t\tc := m.Coder.ToModelRoleConfig(ModelRoleCoder)\n\t\tcoder = &c\n\t}\n\n\tif m.WholeFileBuilder != nil {\n\t\tc := m.WholeFileBuilder.ToModelRoleConfig(ModelRoleWholeFileBuilder)\n\t\twholeFileBuilder = &c\n\t}\n\n\tif m.Architect != nil {\n\t\tc := m.Architect.ToModelRoleConfig(ModelRoleArchitect)\n\t\tarchitect = &c\n\t}\n\n\tvar maxConvoTokens int\n\tif m.Planner.MaxConvoTokens != nil {\n\t\tmaxConvoTokens = *m.Planner.MaxConvoTokens\n\t}\n\n\treturn ModelPack{\n\t\tName:          m.Name,\n\t\tDescription:   m.Description,\n\t\tLocalProvider: m.LocalProvider,\n\t\tPlanner: PlannerRoleConfig{\n\t\t\tModelRoleConfig: m.Planner.ToModelRoleConfig(ModelRolePlanner),\n\t\t\tPlannerModelConfig: PlannerModelConfig{\n\t\t\t\tMaxConvoTokens: maxConvoTokens,\n\t\t\t},\n\t\t},\n\t\tCoder:            coder,\n\t\tPlanSummary:      m.PlanSummary.ToModelRoleConfig(ModelRolePlanSummary),\n\t\tBuilder:          m.Builder.ToModelRoleConfig(ModelRoleBuilder),\n\t\tWholeFileBuilder: wholeFileBuilder,\n\t\tNamer:            m.Namer.ToModelRoleConfig(ModelRoleName),\n\t\tCommitMsg:        m.CommitMsg.ToModelRoleConfig(ModelRoleCommitMsg),\n\t\tExecStatus:       m.ExecStatus.ToModelRoleConfig(ModelRoleExecStatus),\n\t\tArchitect:        architect,\n\t}\n}\n\ntype ModelPack struct {\n\tId               string            `json:\"id\"`\n\tName             string            `json:\"name\"`\n\tLocalProvider    ModelProvider     `json:\"localProvider,omitempty\"`\n\tDescription      string            `json:\"description\"`\n\tPlanner          PlannerRoleConfig `json:\"planner\"`\n\tCoder            *ModelRoleConfig  `json:\"coder\"`\n\tPlanSummary      ModelRoleConfig   `json:\"planSummary\"`\n\tBuilder          ModelRoleConfig   `json:\"builder\"`\n\tWholeFileBuilder *ModelRoleConfig  `json:\"wholeFileBuilder\"` // optional, defaults to builder model — access via GetWholeFileBuilder()\n\tNamer            ModelRoleConfig   `json:\"namer\"`\n\tCommitMsg        ModelRoleConfig   `json:\"commitMsg\"`\n\tExecStatus       ModelRoleConfig   `json:\"execStatus\"`\n\tArchitect        *ModelRoleConfig  `json:\"contextLoader\"`\n}\n\nfunc (m *ModelPack) GetCoder() ModelRoleConfig {\n\tif m.Coder == nil {\n\t\treturn m.Planner.ModelRoleConfig\n\t}\n\treturn *m.Coder\n}\n\nfunc (m *ModelPack) GetWholeFileBuilder() ModelRoleConfig {\n\tif m.WholeFileBuilder == nil {\n\t\treturn m.Builder\n\t}\n\treturn *m.WholeFileBuilder\n}\n\nfunc (m *ModelPack) GetArchitect() ModelRoleConfig {\n\tif m.Architect == nil {\n\t\treturn m.Planner.ModelRoleConfig\n\t}\n\treturn *m.Architect\n}\n\nfunc (m *ModelPack) ToModelPackSchema() *ModelPackSchema {\n\tvar coder *ModelRoleConfigSchema\n\tif m.Coder != nil {\n\t\tc := m.Coder.ToModelRoleConfigSchema()\n\t\tcoder = &c\n\t}\n\tvar wholeFileBuilder *ModelRoleConfigSchema\n\tif m.WholeFileBuilder != nil {\n\t\tc := m.WholeFileBuilder.ToModelRoleConfigSchema()\n\t\twholeFileBuilder = &c\n\t}\n\tvar architect *ModelRoleConfigSchema\n\tif m.Architect != nil {\n\t\tc := m.Architect.ToModelRoleConfigSchema()\n\t\tarchitect = &c\n\t}\n\n\treturn &ModelPackSchema{\n\t\tName:        m.Name,\n\t\tDescription: m.Description,\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tLocalProvider:    m.LocalProvider,\n\t\t\tPlanner:          m.Planner.ToModelRoleConfigSchema(),\n\t\t\tCoder:            coder,\n\t\t\tArchitect:        architect,\n\t\t\tPlanSummary:      m.PlanSummary.ToModelRoleConfigSchema(),\n\t\t\tBuilder:          m.Builder.ToModelRoleConfigSchema(),\n\t\t\tWholeFileBuilder: wholeFileBuilder,\n\t\t\tNamer:            m.Namer.ToModelRoleConfigSchema(),\n\t\t\tCommitMsg:        m.CommitMsg.ToModelRoleConfigSchema(),\n\t\t\tExecStatus:       m.ExecStatus.ToModelRoleConfigSchema(),\n\t\t},\n\t}\n}\n\nfunc (m ModelPackSchemaRoles) Hash() (string, error) {\n\tbytes, err := json.Marshal(m)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\thash := sha256.Sum256(bytes)\n\treturn hex.EncodeToString(hash[:]), nil\n}\n"
  },
  {
    "path": "app/shared/ai_models_errors.go",
    "content": "package shared\n\nimport (\n\t\"log\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n\t\"github.com/jinzhu/copier\"\n)\n\ntype ModelErrKind string\n\nconst (\n\tErrOverloaded                 ModelErrKind = \"ErrOverloaded\"\n\tErrContextTooLong             ModelErrKind = \"ErrContextTooLong\"\n\tErrRateLimited                ModelErrKind = \"ErrRateLimited\"\n\tErrSubscriptionQuotaExhausted ModelErrKind = \"ErrSubscriptionQuotaExhausted\"\n\tErrOther                      ModelErrKind = \"ErrOther\"\n\tErrCacheSupport               ModelErrKind = \"ErrCacheSupport\"\n)\n\ntype ModelError struct {\n\tKind              ModelErrKind\n\tRetriable         bool\n\tRetryAfterSeconds int\n}\n\nfunc (m ModelError) ShouldIncrementRetry() bool {\n\treturn m.Kind != ErrSubscriptionQuotaExhausted && m.Kind != ErrCacheSupport\n}\n\n// if fallback is defined, retry with main model, then remaining tries use error fallback\ntype FallbackType string\n\nconst (\n\tFallbackTypeError    FallbackType = \"error\"\n\tFallbackTypeContext  FallbackType = \"context\"\n\tFallbackTypeProvider FallbackType = \"provider\"\n)\n\ntype FallbackResult struct {\n\tModelRoleConfig *ModelRoleConfig\n\tIsFallback      bool\n\tFallbackType    FallbackType\n\tBaseModelConfig *BaseModelConfig\n}\n\nconst MAX_RETRIES_BEFORE_FALLBACK = 1\n\nfunc (m *ModelRoleConfig) GetFallbackForModelError(\n\tnumTotalRetry int,\n\tdidProviderFallback bool,\n\tmodelErr *ModelError,\n\tauthVars map[string]string,\n\tsettings *PlanSettings,\n\torgUserConfig *OrgUserConfig,\n) FallbackResult {\n\tif m == nil || modelErr == nil {\n\t\treturn FallbackResult{\n\t\t\tModelRoleConfig: m,\n\t\t\tBaseModelConfig: m.GetBaseModelConfig(authVars, settings, orgUserConfig),\n\t\t\tIsFallback:      false,\n\t\t}\n\t}\n\tif modelErr.Kind == ErrContextTooLong {\n\t\tif m.LargeContextFallback != nil {\n\t\t\treturn FallbackResult{\n\t\t\t\tModelRoleConfig: m.LargeContextFallback,\n\t\t\t\tBaseModelConfig: m.LargeContextFallback.GetBaseModelConfig(authVars, settings, orgUserConfig),\n\t\t\t\tFallbackType:    FallbackTypeContext,\n\t\t\t\tIsFallback:      true,\n\t\t\t}\n\t\t}\n\t} else if !modelErr.Retriable || numTotalRetry > MAX_RETRIES_BEFORE_FALLBACK {\n\t\tif m.ErrorFallback != nil {\n\t\t\treturn FallbackResult{\n\t\t\t\tModelRoleConfig: m.ErrorFallback,\n\t\t\t\tBaseModelConfig: m.ErrorFallback.GetBaseModelConfig(authVars, settings, orgUserConfig),\n\t\t\t\tFallbackType:    FallbackTypeError,\n\t\t\t\tIsFallback:      true,\n\t\t\t}\n\t\t} else if !didProviderFallback {\n\t\t\tlog.Println(\"no error fallback, trying provider fallback\")\n\n\t\t\tproviderFallback := m.GetProviderFallback(authVars, settings, orgUserConfig)\n\n\t\t\tlog.Println(spew.Sdump(map[string]interface{}{\n\t\t\t\t\"providerFallback\": providerFallback,\n\t\t\t}))\n\n\t\t\tif providerFallback != nil {\n\t\t\t\treturn FallbackResult{\n\t\t\t\t\tModelRoleConfig: providerFallback,\n\t\t\t\t\tBaseModelConfig: providerFallback.GetBaseModelConfig(authVars, settings, orgUserConfig),\n\t\t\t\t\tFallbackType:    FallbackTypeProvider,\n\t\t\t\t\tIsFallback:      true,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn FallbackResult{\n\t\tModelRoleConfig: m,\n\t\tIsFallback:      false,\n\t}\n}\n\n// we just try a single provider fallback if all defined fallbacks are exhausted\n// if we've got openrouter credentials in the stack, we always use OpenRouter as the fallback since it has its own routing/fallback routing to maximize resilience\n// otherwise we just use the second provider in the stack\n// if we're using the claude subscription, we also go to second provider in the stack rather than openrouter\nfunc (m ModelRoleConfig) GetProviderFallback(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) *ModelRoleConfig {\n\tproviders := m.GetProvidersForAuthVars(authVars, settings, orgUserConfig)\n\n\tif len(providers) < 2 {\n\t\treturn nil\n\t}\n\n\tfirstProvider := providers[0]\n\n\tres := ModelRoleConfig{}\n\tcopier.Copy(&res, m)\n\n\tvar provider ModelProvider\n\n\tif !firstProvider.HasClaudeMaxAuth {\n\t\tfor _, p := range providers {\n\t\t\tif p.Provider == ModelProviderOpenRouter {\n\t\t\t\tprovider = p.Provider\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif provider == \"\" {\n\t\tprovider = providers[1].Provider\n\t}\n\n\tavailableModel := GetAvailableModel(provider, m.ModelId)\n\n\tif availableModel != nil {\n\t\tc := availableModel.BaseModelConfig\n\t\tres.BaseModelConfig = &c\n\t} else {\n\t\tc := m.GetBaseModelConfig(authVars, settings, orgUserConfig)\n\t\tres.BaseModelConfig = c\n\t}\n\n\treturn &res\n}\n"
  },
  {
    "path": "app/shared/ai_models_large_context.go",
    "content": "package shared\n\nconst maxFallbackDepth = 10 // max fallback depth for large context fallback - should never be reached in real scenarios, but protects against infinite loops in case of circular references etc.\n\nfunc (m ModelRoleConfig) GetFinalLargeContextFallback() ModelRoleConfig {\n\tvar currentConfig ModelRoleConfig = m\n\tvar n int = 0\n\n\tfor {\n\t\tif currentConfig.LargeContextFallback == nil {\n\t\t\treturn currentConfig\n\t\t} else {\n\t\t\tcurrentConfig = *currentConfig.LargeContextFallback\n\t\t}\n\t\tn++\n\t\tif n > maxFallbackDepth {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn currentConfig\n}\n\nfunc (m ModelRoleConfig) GetFinalLargeOutputFallback() ModelRoleConfig {\n\tvar currentConfig ModelRoleConfig = m\n\tvar n int = 0\n\n\tif currentConfig.LargeOutputFallback == nil {\n\t\treturn currentConfig.GetFinalLargeContextFallback()\n\t}\n\n\tfor {\n\t\tif currentConfig.LargeOutputFallback == nil {\n\t\t\treturn currentConfig\n\t\t} else {\n\t\t\tcurrentConfig = *currentConfig.LargeOutputFallback\n\t\t}\n\t\tn++\n\t\tif n > maxFallbackDepth {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn currentConfig\n}\n\n// note that if the token number exeeds all the fallback models, it will return the last fallback model\n\nfunc (m ModelRoleConfig) GetRoleForInputTokens(inputTokens int, settings *PlanSettings) ModelRoleConfig {\n\tvar maxTokens int\n\tvar paddingPct float64\n\n\tsharedBaseConfig := m.GetSharedBaseConfig(settings)\n\tmaxTokens = sharedBaseConfig.MaxTokens\n\tpaddingPct = sharedBaseConfig.TokenEstimatePaddingPct\n\n\tinputTokens = int(float64(inputTokens) * (1 + paddingPct))\n\tvar currentConfig ModelRoleConfig = m\n\tvar n int = 0\n\tfor {\n\t\tif maxTokens >= inputTokens {\n\t\t\treturn currentConfig\n\t\t}\n\n\t\tif currentConfig.LargeContextFallback == nil {\n\t\t\treturn currentConfig\n\t\t} else {\n\t\t\tcurrentConfig = *currentConfig.LargeContextFallback\n\t\t}\n\t\tn++\n\t\tif n > maxFallbackDepth {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn currentConfig\n}\n\nfunc (m ModelRoleConfig) GetRoleForOutputTokens(outputTokens int, settings *PlanSettings) ModelRoleConfig {\n\tsharedBaseConfig := m.GetSharedBaseConfig(settings)\n\n\toutputTokens = int(float64(outputTokens) * (1 + sharedBaseConfig.TokenEstimatePaddingPct))\n\tvar currentConfig ModelRoleConfig = m\n\n\tcustomModelsById := map[ModelId]*CustomModel{}\n\tif settings != nil {\n\t\tcustomModelsById = settings.CustomModelsById\n\t}\n\n\tvar n int = 0\n\tfor {\n\t\tif currentConfig.GetReservedOutputTokens(customModelsById) >= outputTokens {\n\t\t\treturn currentConfig\n\t\t}\n\n\t\tif currentConfig.LargeOutputFallback == nil {\n\t\t\tif currentConfig.LargeContextFallback == nil {\n\t\t\t\treturn currentConfig\n\t\t\t} else {\n\t\t\t\tcurrentConfig = *currentConfig.LargeContextFallback\n\t\t\t}\n\t\t} else {\n\t\t\tcurrentConfig = *currentConfig.LargeOutputFallback\n\t\t}\n\t\tn++\n\t\tif n > maxFallbackDepth {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn currentConfig\n}\n"
  },
  {
    "path": "app/shared/ai_models_openrouter.go",
    "content": "package shared\n\ntype OpenRouterFamily string\n\nconst (\n\tOpenRouterFamilyAnthropic OpenRouterFamily = \"anthropic\"\n\tOpenRouterFamilyGoogle    OpenRouterFamily = \"google\"\n\tOpenRouterFamilyOpenAI    OpenRouterFamily = \"openai\"\n\tOpenRouterFamilyQwen      OpenRouterFamily = \"qwen\"\n\tOpenRouterFamilyDeepSeek  OpenRouterFamily = \"deepseek\"\n)\n\n// type OpenRouterProvider string\n\n// const (\n// \tOpenRouterProviderAnthropic OpenRouterProvider = \"Anthropic\"\n// \tOpenRouterProviderGoogle    OpenRouterProvider = \"Google Vertex\"\n// \tOpenRouterProviderOpenAI    OpenRouterProvider = \"OpenAI\"\n// \tOpenRouterProviderDeepSeek  OpenRouterProvider = \"DeepSeek\"\n// \tOpenRouterProviderQwen      OpenRouterProvider = \"Hyperbolic\"\n// \tOpenRouterProviderDeepInfra OpenRouterProvider = \"DeepInfra\"\n// \tOpenRouterProviderFireworks OpenRouterProvider = \"Fireworks\"\n// )\n\n// var DefaultOpenRouterProvidersByFamily = map[OpenRouterFamily][]OpenRouterProvider{\n// \tOpenRouterFamilyAnthropic: {OpenRouterProviderAnthropic},\n// \tOpenRouterFamilyGoogle:    {OpenRouterProviderGoogle},\n// \tOpenRouterFamilyOpenAI:    {OpenRouterProviderOpenAI},\n// \tOpenRouterFamilyQwen:      {OpenRouterProviderQwen},\n// \tOpenRouterFamilyDeepSeek:  {OpenRouterProviderDeepSeek},\n// }\n\n// // open source models don't have fallbacks enabled for now because pricing and context limits aren't predictable across providers\n// var DefaultOpenRouterAllowFallbacksByFamily = map[OpenRouterFamily]bool{\n// \tOpenRouterFamilyAnthropic: true,\n// \tOpenRouterFamilyGoogle:    true,\n// \tOpenRouterFamilyOpenAI:    false,\n// }\n"
  },
  {
    "path": "app/shared/ai_models_packs.go",
    "content": "package shared\n\nvar DailyDriverModelPack ModelPack\nvar ReasoningModelPack ModelPack\nvar StrongModelPack ModelPack\nvar OSSModelPack ModelPack\nvar CheapModelPack ModelPack\n\nvar OpusPlannerModelPack ModelPack\n\nvar AnthropicModelPack ModelPack\nvar OpenAIModelPack ModelPack\nvar GoogleModelPack ModelPack\n\nvar GeminiPlannerModelPack ModelPack\nvar O3PlannerModelPack ModelPack\nvar R1PlannerModelPack ModelPack\nvar PerplexityPlannerModelPack ModelPack\n\nvar OllamaExperimentalModelPack ModelPack\nvar OllamaAdaptiveOssModelPack ModelPack\nvar OllamaAdaptiveDailyModelPack ModelPack\n\nvar BuiltInModelPacks = []*ModelPack{\n\t&DailyDriverModelPack,\n\t&ReasoningModelPack,\n\t&StrongModelPack,\n\t&CheapModelPack,\n\t&OSSModelPack,\n\t&OllamaExperimentalModelPack,\n\t&OllamaAdaptiveOssModelPack,\n\t&OllamaAdaptiveDailyModelPack,\n\t&AnthropicModelPack,\n\t&OpenAIModelPack,\n\t&GoogleModelPack,\n\t&GeminiPlannerModelPack,\n\t&OpusPlannerModelPack,\n\t&O3PlannerModelPack,\n\t&R1PlannerModelPack,\n\t&PerplexityPlannerModelPack,\n}\n\nvar BuiltInModelPacksByName = make(map[string]*ModelPack)\n\nvar DefaultModelPack *ModelPack = &DailyDriverModelPack\n\nfunc getModelRoleConfig(role ModelRole, modelId ModelId, fns ...func(*ModelRoleConfigSchema)) ModelRoleConfigSchema {\n\tc := ModelRoleConfigSchema{\n\t\tModelId: modelId,\n\t}\n\tfor _, f := range fns {\n\t\tf(&c)\n\t}\n\treturn c\n}\n\nfunc getLargeContextFallback(role ModelRole, modelId ModelId, fns ...func(*ModelRoleConfigSchema)) func(*ModelRoleConfigSchema) {\n\treturn func(c *ModelRoleConfigSchema) {\n\t\tn := getModelRoleConfig(role, modelId)\n\t\tfor _, f := range fns {\n\t\t\tf(&n)\n\t\t}\n\t\tc.LargeContextFallback = &n\n\t}\n}\n\nfunc getErrorFallback(role ModelRole, modelId ModelId, fns ...func(*ModelRoleConfigSchema)) func(*ModelRoleConfigSchema) {\n\treturn func(c *ModelRoleConfigSchema) {\n\t\tn := getModelRoleConfig(role, modelId)\n\t\tfor _, f := range fns {\n\t\t\tf(&n)\n\t\t}\n\t\tc.ErrorFallback = &n\n\t}\n}\n\nfunc getStrongModelFallback(role ModelRole, modelId ModelId, fns ...func(*ModelRoleConfigSchema)) func(*ModelRoleConfigSchema) {\n\treturn func(c *ModelRoleConfigSchema) {\n\t\tn := getModelRoleConfig(role, modelId)\n\t\tfor _, f := range fns {\n\t\t\tf(&n)\n\t\t}\n\t\tc.StrongModel = &n\n\t}\n}\n\nvar (\n\tDailyDriverSchema         ModelPackSchema\n\tReasoningSchema           ModelPackSchema\n\tStrongSchema              ModelPackSchema\n\tOssSchema                 ModelPackSchema\n\tCheapSchema               ModelPackSchema\n\tOllamaExperimentalSchema  ModelPackSchema\n\tOllamaAdaptiveOssSchema   ModelPackSchema\n\tOllamaAdaptiveDailySchema ModelPackSchema\n\tAnthropicSchema           ModelPackSchema\n\tOpenAISchema              ModelPackSchema\n\tGoogleSchema              ModelPackSchema\n\tGeminiPlannerSchema       ModelPackSchema\n\tOpusPlannerSchema         ModelPackSchema\n\tR1PlannerSchema           ModelPackSchema\n\tPerplexityPlannerSchema   ModelPackSchema\n\tO3PlannerSchema           ModelPackSchema\n)\n\nvar BuiltInModelPackSchemas = []*ModelPackSchema{\n\t&DailyDriverSchema,\n\t&ReasoningSchema,\n\t&StrongSchema,\n\t&CheapSchema,\n\t&OssSchema,\n\t&OllamaExperimentalSchema,\n\t&OllamaAdaptiveOssSchema,\n\t&OllamaAdaptiveDailySchema,\n\t&AnthropicSchema,\n\t&OpenAISchema,\n\t&GeminiPlannerSchema,\n\t&OpusPlannerSchema,\n\t&O3PlannerSchema,\n\t&R1PlannerSchema,\n\t&PerplexityPlannerSchema,\n}\n\nfunc init() {\n\tdefaultBuilder := getModelRoleConfig(ModelRoleBuilder, \"openai/o4-mini-medium\",\n\t\tgetStrongModelFallback(ModelRoleBuilder, \"openai/o4-mini-high\"),\n\t)\n\n\tDailyDriverSchema = ModelPackSchema{\n\t\tName:        \"daily-driver\",\n\t\tDescription: \"A mix of models from Anthropic, OpenAI, and Google that balances speed, quality, and cost. Supports up to 2M context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner: getModelRoleConfig(ModelRolePlanner, \"anthropic/claude-sonnet-4\",\n\t\t\t\tgetLargeContextFallback(ModelRolePlanner, \"google/gemini-2.5-pro\",\n\t\t\t\t\tgetLargeContextFallback(ModelRolePlanner, \"google/gemini-pro-1.5\"),\n\t\t\t\t),\n\t\t\t),\n\t\t\tArchitect: Pointer(getModelRoleConfig(ModelRoleArchitect, \"anthropic/claude-sonnet-4\",\n\t\t\t\tgetLargeContextFallback(ModelRoleArchitect, \"google/gemini-2.5-pro\",\n\t\t\t\t\tgetLargeContextFallback(ModelRoleArchitect, \"google/gemini-pro-1.5\"),\n\t\t\t\t),\n\t\t\t)),\n\t\t\tCoder: Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\",\n\t\t\t\tgetLargeContextFallback(ModelRoleCoder, \"openai/gpt-4.1\"),\n\t\t\t)),\n\t\t\tPlanSummary:      getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:          defaultBuilder,\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder, \"openai/o4-mini-medium\")),\n\t\t\tNamer:            getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:        getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus:       getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tReasoningSchema = ModelPackSchema{\n\t\tName:        \"reasoning\",\n\t\tDescription: \"Like the daily driver, but uses sonnet-4-thinking with reasoning enabled for planning and coding. Supports up to 160k input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"anthropic/claude-sonnet-4-thinking-hidden\"),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4-thinking-hidden\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     defaultBuilder,\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-medium\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tStrongSchema = ModelPackSchema{\n\t\tName:        \"strong\",\n\t\tDescription: \"For difficult tasks where slower responses and builds are ok. Uses o3-high for architecture and planning, claude-sonnet-4 thinking for implementation. Supports up to 160k input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"openai/o3-high\"),\n\t\t\tArchitect:   Pointer(getModelRoleConfig(ModelRoleArchitect, \"openai/o3-high\")),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4-thinking-hidden\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     getModelRoleConfig(ModelRoleBuilder, \"openai/o4-mini-high\"),\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-high\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-medium\"),\n\t\t},\n\t}\n\n\tCheapSchema = ModelPackSchema{\n\t\tName:        \"cheap\",\n\t\tDescription: \"Cost-effective models that can still get the job done for easier tasks. Supports up to 160k context. Uses OpenAI's o4-mini model for planning, GPT-4.1 for coding, and GPT-4.1 Mini for lighter tasks.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"openai/o4-mini-medium\"),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"openai/gpt-4.1\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/gpt-4.1-mini\"),\n\t\t\tBuilder:     getModelRoleConfig(ModelRoleBuilder, \"openai/o4-mini-low\"),\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-low\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tOssSchema = ModelPackSchema{\n\t\tName:        \"oss\",\n\t\tDescription: \"An experimental mix of the best open source models for coding. Supports up to 144k context, 33k per file.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"deepseek/r1\"),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"deepseek/v3\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"deepseek/r1-hidden\"),\n\t\t\tBuilder:     getModelRoleConfig(ModelRoleBuilder, \"deepseek/r1-hidden\"),\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"deepseek/r1-hidden\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"qwen/qwen3-8b-cloud\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"qwen/qwen3-8b-cloud\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"deepseek/r1-hidden\"),\n\t\t},\n\t}\n\n\tOllamaExperimentalSchema = ModelPackSchema{\n\t\tName:        \"ollama\",\n\t\tDescription: \"Ollama experimental local blend. Supports up to 110k context. For now, more for experimentation and benchmarking than getting work done.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tLocalProvider: ModelProviderOllama,\n\t\t\tPlanner:       getModelRoleConfig(ModelRolePlanner, \"qwen/qwen3-32b-local\"),\n\t\t\tPlanSummary:   getModelRoleConfig(ModelRolePlanSummary, \"mistral/devstral-small\"),\n\t\t\tBuilder:       getModelRoleConfig(ModelRoleBuilder, \"mistral/devstral-small\"),\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"mistral/devstral-small\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"qwen/qwen3-8b-local\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"qwen/qwen3-8b-local\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"mistral/devstral-small\"),\n\t\t},\n\t}\n\n\t// Copy daily driver schema and modify it to use ollama for lighter tasks\n\tOllamaAdaptiveDailySchema = cloneSchema(DailyDriverSchema)\n\tOllamaAdaptiveDailySchema.Name = \"ollama-daily\"\n\tOllamaAdaptiveDailySchema.Description = \"Ollama adaptive/daily-driver blend. Uses 'daily-driver' for heavy lifting, local models for lighter tasks.\"\n\tOllamaAdaptiveDailySchema.LocalProvider = ModelProviderOllama\n\tOllamaAdaptiveDailySchema.PlanSummary = getModelRoleConfig(ModelRolePlanSummary, \"mistral/devstral-small\")\n\tOllamaAdaptiveDailySchema.CommitMsg = getModelRoleConfig(ModelRoleCommitMsg, \"qwen/qwen3-8b-local\")\n\tOllamaAdaptiveDailySchema.Namer = getModelRoleConfig(ModelRoleName, \"qwen/qwen3-8b-local\")\n\n\t// Copy oss schema and modify it to use ollama for lighter tasks\n\tOllamaAdaptiveOssSchema = cloneSchema(OssSchema)\n\tOllamaAdaptiveOssSchema.Name = \"ollama-oss\"\n\tOllamaAdaptiveOssSchema.Description = \"Ollama adaptive/oss blend. Uses local models for planning and context selection, open source cloud models for implementation and file edits. Supports up to 110k context.\"\n\tOllamaAdaptiveOssSchema.LocalProvider = ModelProviderOllama\n\tOllamaAdaptiveOssSchema.PlanSummary = getModelRoleConfig(ModelRolePlanSummary, \"mistral/devstral-small\")\n\tOllamaAdaptiveOssSchema.CommitMsg = getModelRoleConfig(ModelRoleCommitMsg, \"qwen/qwen3-8b-local\")\n\tOllamaAdaptiveOssSchema.Namer = getModelRoleConfig(ModelRoleName, \"qwen/qwen3-8b-local\")\n\n\tOpenAISchema = ModelPackSchema{\n\t\tName:        \"openai\",\n\t\tDescription: \"OpenAI blend. Supports up to 1M context. Uses OpenAI's GPT-4.1 model for heavy lifting, GPT-4.1 Mini for lighter tasks.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"openai/gpt-4.1\"),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     defaultBuilder,\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-medium\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tAnthropicSchema = ModelPackSchema{\n\t\tName:        \"anthropic\",\n\t\tDescription: \"Anthropic blend. Supports up to 180k context. Uses Claude Sonnet 4 for heavy lifting, Claude 3 Haiku for lighter tasks.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"anthropic/claude-sonnet-4\"),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"anthropic/claude-3.5-haiku\"),\n\t\t\tBuilder:     getModelRoleConfig(ModelRoleBuilder, \"anthropic/claude-sonnet-4\"),\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"anthropic/claude-sonnet-4\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"anthropic/claude-3.5-haiku\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"anthropic/claude-3.5-haiku\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"anthropic/claude-sonnet-4\"),\n\t\t},\n\t}\n\n\tGoogleSchema = ModelPackSchema{\n\t\tName:        \"google\",\n\t\tDescription: \"Uses Gemini 2.5 Pro for heavy lifting, 2.5 Flash for light tasks. Supports up to 1M input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"google/gemini-2.5-pro\"),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"google/gemini-2.5-flash\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"google/gemini-2.5-flash\"),\n\t\t\tBuilder:     getModelRoleConfig(ModelRoleBuilder, \"google/gemini-2.5-pro\"),\n\t\t\tNamer:       getModelRoleConfig(ModelRoleName, \"google/gemini-2.5-flash\"),\n\t\t\tCommitMsg:   getModelRoleConfig(ModelRoleCommitMsg, \"google/gemini-2.5-flash\"),\n\t\t\tExecStatus:  getModelRoleConfig(ModelRoleExecStatus, \"google/gemini-2.5-pro\"),\n\t\t},\n\t}\n\n\tGeminiPlannerSchema = ModelPackSchema{\n\t\tName:        \"gemini-planner\",\n\t\tDescription: \"Uses Gemini 2.5 Pro for planning, default models for other roles. Supports up to 1M input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner: getModelRoleConfig(ModelRolePlanner, \"google/gemini-2.5-pro\"),\n\t\t\tCoder: Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\",\n\t\t\t\tgetLargeContextFallback(ModelRoleCoder, \"openai/gpt-4.1\"),\n\t\t\t)),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     defaultBuilder,\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-medium\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tOpusPlannerSchema = ModelPackSchema{\n\t\tName:        \"opus-planner\",\n\t\tDescription: \"Uses Claude Opus 4 for planning, default models for other roles. Supports up to 180k input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner: getModelRoleConfig(ModelRolePlanner, \"anthropic/claude-opus-4\"),\n\t\t\tCoder: Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\",\n\t\t\t\tgetLargeContextFallback(ModelRoleCoder, \"openai/gpt-4.1\"),\n\t\t\t)),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     defaultBuilder,\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-medium\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tO3PlannerSchema = ModelPackSchema{\n\t\tName:        \"o3-planner\",\n\t\tDescription: \"Uses Claude Opus 4 for planning, default models for other roles. Supports up to 180k input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner: getModelRoleConfig(ModelRolePlanner, \"anthropic/opus-4\"),\n\t\t\tCoder: Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\",\n\t\t\t\tgetLargeContextFallback(ModelRoleCoder, \"openai/gpt-4.1\"),\n\t\t\t)),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     defaultBuilder,\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-medium\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tO3PlannerSchema = ModelPackSchema{\n\t\tName:        \"o3-planner\",\n\t\tDescription: \"Uses OpenAI o3-medium for planning, default models for other roles. Supports up to 160k input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner: getModelRoleConfig(ModelRolePlanner, \"openai/o3-medium\"),\n\t\t\tCoder: Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\",\n\t\t\t\tgetLargeContextFallback(ModelRoleCoder, \"openai/gpt-4.1\"),\n\t\t\t)),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     defaultBuilder,\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-medium\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-low\"),\n\t\t},\n\t}\n\n\tR1PlannerSchema = ModelPackSchema{\n\t\tName:        \"r1-planner\",\n\t\tDescription: \"Uses DeepSeek R1 for planning, Qwen for light tasks, and default models for implementation. Supports up to 56k input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"deepseek/r1\"),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     getModelRoleConfig(ModelRoleBuilder, \"openai/o4-mini-medium\"),\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-low\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-medium\"),\n\t\t},\n\t}\n\n\tPerplexityPlannerSchema = ModelPackSchema{\n\t\tName:        \"perplexity-planner\",\n\t\tDescription: \"Uses Perplexity Sonar for planning, Qwen for light tasks, and default models for implementation. Supports up to 97k input context.\",\n\t\tModelPackSchemaRoles: ModelPackSchemaRoles{\n\t\t\tPlanner:     getModelRoleConfig(ModelRolePlanner, \"perplexity/sonar-reasoning\"),\n\t\t\tCoder:       Pointer(getModelRoleConfig(ModelRoleCoder, \"anthropic/claude-sonnet-4\")),\n\t\t\tPlanSummary: getModelRoleConfig(ModelRolePlanSummary, \"openai/o4-mini-low\"),\n\t\t\tBuilder:     getModelRoleConfig(ModelRoleBuilder, \"openai/o4-mini-medium\"),\n\t\t\tWholeFileBuilder: Pointer(getModelRoleConfig(ModelRoleWholeFileBuilder,\n\t\t\t\t\"openai/o4-mini-low\")),\n\t\t\tNamer:      getModelRoleConfig(ModelRoleName, \"openai/gpt-4.1-mini\"),\n\t\t\tCommitMsg:  getModelRoleConfig(ModelRoleCommitMsg, \"openai/gpt-4.1-mini\"),\n\t\t\tExecStatus: getModelRoleConfig(ModelRoleExecStatus, \"openai/o4-mini-medium\"),\n\t\t},\n\t}\n\n\tDailyDriverModelPack = DailyDriverSchema.ToModelPack()\n\tReasoningModelPack = ReasoningSchema.ToModelPack()\n\tStrongModelPack = StrongSchema.ToModelPack()\n\tCheapModelPack = CheapSchema.ToModelPack()\n\tOSSModelPack = OssSchema.ToModelPack()\n\tOllamaExperimentalModelPack = OllamaExperimentalSchema.ToModelPack()\n\tOllamaAdaptiveOssModelPack = OllamaAdaptiveOssSchema.ToModelPack()\n\tOllamaAdaptiveDailyModelPack = OllamaAdaptiveDailySchema.ToModelPack()\n\tAnthropicModelPack = AnthropicSchema.ToModelPack()\n\tOpenAIModelPack = OpenAISchema.ToModelPack()\n\tGoogleModelPack = GoogleSchema.ToModelPack()\n\tGeminiPlannerModelPack = GeminiPlannerSchema.ToModelPack()\n\tOpusPlannerModelPack = OpusPlannerSchema.ToModelPack()\n\tR1PlannerModelPack = R1PlannerSchema.ToModelPack()\n\tPerplexityPlannerModelPack = PerplexityPlannerSchema.ToModelPack()\n\tO3PlannerModelPack = O3PlannerSchema.ToModelPack()\n\n\tBuiltInModelPacks = []*ModelPack{\n\t\t&DailyDriverModelPack,\n\t\t&ReasoningModelPack,\n\t\t&StrongModelPack,\n\t\t&CheapModelPack,\n\t\t&OSSModelPack,\n\t\t&OllamaExperimentalModelPack,\n\t\t&OllamaAdaptiveOssModelPack,\n\t\t&OllamaAdaptiveDailyModelPack,\n\t\t&AnthropicModelPack,\n\t\t&OpenAIModelPack,\n\t\t&GoogleModelPack,\n\t\t&GeminiPlannerModelPack,\n\t\t&OpusPlannerModelPack,\n\t\t&O3PlannerModelPack,\n\t\t&R1PlannerModelPack,\n\t\t&PerplexityPlannerModelPack,\n\t}\n\n\tDefaultModelPack = &DailyDriverModelPack\n\n\tfor _, mp := range BuiltInModelPacks {\n\t\tBuiltInModelPacksByName[mp.Name] = mp\n\n\t\tfor _, id := range mp.ToModelPackSchema().AllModelIds() {\n\t\t\tif BuiltInBaseModelsById[id] == nil {\n\t\t\t\tpanic(\"missing base model: \" + id)\n\t\t\t}\n\t\t}\n\t}\n\n}\n\n// pointer fields need to be cloned to avoid modifying the original schema\nfunc cloneSchema(schema ModelPackSchema) ModelPackSchema {\n\tres := schema\n\n\tif schema.Architect != nil {\n\t\ttmp := *schema.Architect\n\t\tres.Architect = &tmp\n\t}\n\n\tif schema.Coder != nil {\n\t\ttmp := *schema.Coder\n\t\tres.Coder = &tmp\n\t}\n\tif schema.WholeFileBuilder != nil {\n\t\ttmp := *schema.WholeFileBuilder\n\t\tres.WholeFileBuilder = &tmp\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "app/shared/ai_models_providers.go",
    "content": "package shared\n\nimport (\n\t\"fmt\"\n)\n\nconst OpenAIV1BaseUrl = \"https://api.openai.com/v1\"\nconst OpenRouterBaseUrl = \"https://openrouter.ai/api/v1\"\nconst LiteLLMBaseUrl = \"http://localhost:4000/v1\" // runs in the same container alongside the plandex server\n\nconst OpenAIEnvVar = \"OPENAI_API_KEY\"\nconst OpenRouterApiKeyEnvVar = \"OPENROUTER_API_KEY\"\nconst AnthropicApiKeyEnvVar = \"ANTHROPIC_API_KEY\"\nconst GoogleAIStudioApiKeyEnvVar = \"GEMINI_API_KEY\"\nconst AzureOpenAIEnvVar = \"AZURE_OPENAI_API_KEY\"\nconst DeepSeekApiKeyEnvVar = \"DEEPSEEK_API_KEY\"\nconst PerplexityApiKeyEnvVar = \"PERPLEXITY_API_KEY\"\n\n// not set directly via env vars, but used for auth var resolution\nconst AnthropicClaudeMaxTokenEnvVar = \"ANTHROPIC_CLAUDE_MAX_TOKEN\"\nconst AnthropicClaudeMaxBetaHeader = \"oauth-2025-04-20\"\nconst LiteLLMAnthropicBaseUrl = \"https://api.anthropic.com\"\n\ntype ModelPublisher string\n\nconst (\n\tModelPublisherOpenAI     ModelPublisher = \"OpenAI\"\n\tModelPublisherAnthropic  ModelPublisher = \"Anthropic\"\n\tModelPublisherGoogle     ModelPublisher = \"Google\"\n\tModelPublisherDeepSeek   ModelPublisher = \"DeepSeek\"\n\tModelPublisherPerplexity ModelPublisher = \"Perplexity\"\n\tModelPublisherQwen       ModelPublisher = \"Qwen\"\n\tModelPublisherMistral    ModelPublisher = \"Mistral\"\n)\n\ntype ModelProvider string\n\nconst (\n\tModelProviderOpenRouter ModelProvider = \"openrouter\"\n\tModelProviderOpenAI     ModelProvider = \"openai\"\n\n\tModelProviderAnthropic          ModelProvider = \"anthropic\"\n\tModelProviderAnthropicClaudeMax ModelProvider = \"anthropic-pro\"\n\tModelProviderGoogleAIStudio     ModelProvider = \"google-ai-studio\"\n\tModelProviderGoogleVertex       ModelProvider = \"google-vertex\"\n\tModelProviderAzureOpenAI        ModelProvider = \"azure-openai\"\n\tModelProviderDeepSeek           ModelProvider = \"deepseek\"\n\tModelProviderPerplexity         ModelProvider = \"perplexity\"\n\n\tModelProviderAmazonBedrock ModelProvider = \"aws-bedrock\"\n\n\tModelProviderOllama ModelProvider = \"ollama\"\n\n\tModelProviderCustom ModelProvider = \"custom\"\n)\n\nvar ModelProviderToLiteLLMId = map[ModelProvider]string{\n\tModelProviderGoogleAIStudio: \"gemini\",\n\tModelProviderGoogleVertex:   \"vertex_ai\",\n\tModelProviderAzureOpenAI:    \"azure\",\n\tModelProviderAmazonBedrock:  \"bedrock\",\n}\n\nvar AllModelProviders = []ModelProvider{\n\tModelProviderOpenRouter,\n\tModelProviderOpenAI,\n\tModelProviderAnthropic,\n\tModelProviderAnthropicClaudeMax,\n\tModelProviderGoogleAIStudio,\n\tModelProviderGoogleVertex,\n\tModelProviderAzureOpenAI,\n\tModelProviderAmazonBedrock,\n\tModelProviderDeepSeek,\n\tModelProviderPerplexity,\n\tModelProviderOllama,\n\tModelProviderCustom,\n}\n\ntype ModelProviderExtraAuthVars struct {\n\tVar               string `json:\"var\"`\n\tMaybeJSONFilePath bool   `json:\"maybeJSONFilePath,omitempty\"`\n\tRequired          bool   `json:\"required,omitempty\"`\n\tDefault           string `json:\"default,omitempty\"`\n}\n\ntype ModelProviderConfigSchema struct {\n\tProvider       ModelProvider `json:\"provider\"`\n\tCustomProvider *string       `json:\"customProvider,omitempty\"`\n\tBaseUrl        string        `json:\"baseUrl\"`\n\n\t// for AWS Bedrock models\n\tHasAWSAuth bool `json:\"hasAWSAuth,omitempty\"`\n\n\t// for Claude Max integration\n\tHasClaudeMaxAuth bool `json:\"hasClaudeMaxAuth,omitempty\"`\n\n\t// for local models that don't require auth (ollama, etc.)\n\tSkipAuth  bool `json:\"skipAuth,omitempty\"`\n\tLocalOnly bool `json:\"localOnly,omitempty\"`\n\n\tApiKeyEnvVar  string                       `json:\"apiKeyEnvVar,omitempty\"`\n\tExtraAuthVars []ModelProviderExtraAuthVars `json:\"extraAuthVars,omitempty\"`\n}\n\nfunc (m *ModelProviderConfigSchema) ToComposite() string {\n\tif m.CustomProvider != nil {\n\t\treturn fmt.Sprintf(\"%s|%s\", m.Provider, *m.CustomProvider)\n\t}\n\treturn string(m.Provider)\n}\n\nconst DefaultAzureApiVersion = \"2025-04-01-preview\"\nconst AnthropicMaxReasoningBudget = 32000\nconst GoogleMaxReasoningBudget = 32000\n\nvar BuiltInModelProviderConfigs = map[ModelProvider]ModelProviderConfigSchema{\n\tModelProviderOpenAI: {\n\t\tProvider:     ModelProviderOpenAI,\n\t\tBaseUrl:      OpenAIV1BaseUrl,\n\t\tApiKeyEnvVar: OpenAIEnvVar,\n\t\tExtraAuthVars: []ModelProviderExtraAuthVars{\n\t\t\t{\n\t\t\t\tVar:      \"OPENAI_ORG_ID\",\n\t\t\t\tRequired: false,\n\t\t\t},\n\t\t},\n\t},\n\tModelProviderOpenRouter: {\n\t\tProvider:     ModelProviderOpenRouter,\n\t\tBaseUrl:      OpenRouterBaseUrl,\n\t\tApiKeyEnvVar: OpenRouterApiKeyEnvVar,\n\t},\n\tModelProviderAnthropic: {\n\t\tProvider:     ModelProviderAnthropic,\n\t\tBaseUrl:      LiteLLMBaseUrl,\n\t\tApiKeyEnvVar: AnthropicApiKeyEnvVar,\n\t},\n\tModelProviderAnthropicClaudeMax: {\n\t\tProvider:         ModelProviderAnthropicClaudeMax,\n\t\tBaseUrl:          LiteLLMBaseUrl,\n\t\tHasClaudeMaxAuth: true,\n\t},\n\tModelProviderGoogleAIStudio: {\n\t\tProvider:     ModelProviderGoogleAIStudio,\n\t\tBaseUrl:      LiteLLMBaseUrl,\n\t\tApiKeyEnvVar: GoogleAIStudioApiKeyEnvVar,\n\t},\n\tModelProviderGoogleVertex: {\n\t\tProvider: ModelProviderGoogleVertex,\n\t\tBaseUrl:  LiteLLMBaseUrl,\n\t\tExtraAuthVars: []ModelProviderExtraAuthVars{\n\t\t\t{\n\t\t\t\t// this is a file path, but client-side it will be read and then passed along as an auth var just as if it were an env var\n\t\t\t\tVar:               \"GOOGLE_APPLICATION_CREDENTIALS\",\n\t\t\t\tMaybeJSONFilePath: true,\n\t\t\t\tRequired:          true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tVar:      \"VERTEXAI_PROJECT\",\n\t\t\t\tRequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tVar:      \"VERTEXAI_LOCATION\",\n\t\t\t\tRequired: true,\n\t\t\t},\n\t\t},\n\t},\n\tModelProviderAzureOpenAI: {\n\t\tProvider:     ModelProviderAzureOpenAI,\n\t\tBaseUrl:      LiteLLMBaseUrl,\n\t\tApiKeyEnvVar: AzureOpenAIEnvVar,\n\t\tExtraAuthVars: []ModelProviderExtraAuthVars{\n\t\t\t{\n\t\t\t\tVar:      \"AZURE_API_BASE\",\n\t\t\t\tRequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tVar:      \"AZURE_API_VERSION\",\n\t\t\t\tRequired: false,\n\t\t\t\tDefault:  DefaultAzureApiVersion,\n\t\t\t},\n\t\t\t{\n\t\t\t\tVar:               \"AZURE_DEPLOYMENTS_MAP\",\n\t\t\t\tRequired:          false,\n\t\t\t\tMaybeJSONFilePath: true,\n\t\t\t},\n\t\t},\n\t},\n\tModelProviderDeepSeek: {\n\t\tProvider:     ModelProviderDeepSeek,\n\t\tBaseUrl:      LiteLLMBaseUrl,\n\t\tApiKeyEnvVar: DeepSeekApiKeyEnvVar,\n\t},\n\tModelProviderPerplexity: {\n\t\tProvider:     ModelProviderPerplexity,\n\t\tBaseUrl:      LiteLLMBaseUrl,\n\t\tApiKeyEnvVar: PerplexityApiKeyEnvVar,\n\t},\n\tModelProviderAmazonBedrock: {\n\t\tProvider:   ModelProviderAmazonBedrock,\n\t\tBaseUrl:    LiteLLMBaseUrl,\n\t\tHasAWSAuth: true,\n\n\t\t// these aren't required as env vars—but if found in the credentials file, they are passed along as auth vars just as if they were env vars\n\t\tExtraAuthVars: []ModelProviderExtraAuthVars{\n\t\t\t{Var: \"AWS_ACCESS_KEY_ID\", Required: true},\n\t\t\t{Var: \"AWS_SECRET_ACCESS_KEY\", Required: true},\n\t\t\t{Var: \"AWS_REGION\", Required: true},\n\t\t\t{Var: \"AWS_SESSION_TOKEN\", Required: false},\n\t\t\t{Var: \"AWS_INFERENCE_PROFILE_ARN\", Required: false},\n\t\t},\n\t},\n\tModelProviderOllama: {\n\t\tProvider:  ModelProviderOllama,\n\t\tBaseUrl:   LiteLLMBaseUrl,\n\t\tSkipAuth:  true,\n\t\tLocalOnly: true,\n\t},\n}\n\nvar BuiltInModelProviderConfigsByComposite = map[string]ModelProviderConfigSchema{}\n\nfunc init() {\n\tfor _, providerConfig := range BuiltInModelProviderConfigs {\n\t\tBuiltInModelProviderConfigsByComposite[providerConfig.ToComposite()] = providerConfig\n\t}\n}\n\nfunc GetProvidersForAuthVars(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) []ModelProviderConfigSchema {\n\tvar claudeSubscriptionCooldownActive bool\n\tif orgUserConfig != nil {\n\t\tclaudeSubscriptionCooldownActive = orgUserConfig.IsClaudeSubscriptionCooldownActive()\n\t}\n\n\tvar foundProviders []ModelProviderConfigSchema\n\n\tallProviders := []ModelProviderConfigSchema{}\n\n\tfor _, providerConfig := range BuiltInModelProviderConfigs {\n\t\tallProviders = append(allProviders, providerConfig)\n\t}\n\n\tif settings != nil {\n\t\tfor _, customProvider := range settings.CustomProviders {\n\t\t\tallProviders = append(allProviders, customProvider.ToModelProviderConfigSchema())\n\t\t}\n\t}\n\n\tfor _, providerConfig := range allProviders {\n\t\t// filter out claude max if the cooldown is active\n\t\tif claudeSubscriptionCooldownActive && providerConfig.HasClaudeMaxAuth {\n\t\t\tcontinue\n\t\t}\n\n\t\tif providerConfig.SkipAuth {\n\t\t\tfoundProviders = append(foundProviders, providerConfig)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar checkVars []string\n\t\tif providerConfig.ApiKeyEnvVar != \"\" {\n\t\t\tcheckVars = append(checkVars, providerConfig.ApiKeyEnvVar)\n\t\t}\n\t\tfor _, extraAuthVar := range providerConfig.ExtraAuthVars {\n\t\t\tif extraAuthVar.Required {\n\t\t\t\tcheckVars = append(checkVars, extraAuthVar.Var)\n\t\t\t}\n\t\t}\n\t\tif providerConfig.HasClaudeMaxAuth {\n\t\t\tcheckVars = append(checkVars, AnthropicClaudeMaxTokenEnvVar)\n\t\t}\n\n\t\tmissingAny := false\n\t\tfor _, checkVar := range checkVars {\n\t\t\tif _, ok := authVars[checkVar]; !ok {\n\t\t\t\tmissingAny = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif missingAny {\n\t\t\tcontinue\n\t\t}\n\n\t\tfoundProviders = append(foundProviders, providerConfig)\n\t}\n\n\treturn foundProviders\n}\n\nfunc GetProvidersForAuthVarsWithModelId(authVars map[string]string, settings *PlanSettings, modelId ModelId, orgUserConfig *OrgUserConfig) []ModelProviderConfigSchema {\n\tvar localProvider ModelProvider\n\tif settings != nil {\n\t\tmodelPack := settings.GetModelPack()\n\t\tif modelPack != nil {\n\t\t\tlocalProvider = modelPack.LocalProvider\n\t\t}\n\t}\n\n\tbuiltInUsesProviders := BuiltInModelProvidersByModelId[modelId]\n\n\tvar customUsesProviders []BaseModelUsesProvider\n\tif settings != nil {\n\t\tcustomUsesProviders = settings.UsesCustomProviderByModelId[modelId]\n\t}\n\n\tusesProviders := append(builtInUsesProviders, customUsesProviders...)\n\tif len(usesProviders) == 0 {\n\t\treturn []ModelProviderConfigSchema{}\n\t}\n\n\tproviders := GetProvidersForAuthVars(authVars, settings, orgUserConfig)\n\tprovidersByComposite := map[string]ModelProviderConfigSchema{}\n\tfor _, provider := range providers {\n\t\tprovidersByComposite[provider.ToComposite()] = provider\n\t}\n\n\tres := []ModelProviderConfigSchema{}\n\tfor _, usesProvider := range usesProviders {\n\t\tcomposite := usesProvider.ToComposite()\n\t\tprovider, ok := providersByComposite[composite]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// if the model pack has a local provider, the provider is local only, and the provider is not the local provider, skip it\n\t\tif localProvider != \"\" && provider.LocalOnly && provider.Provider != localProvider {\n\t\t\tcontinue\n\t\t}\n\n\t\tres = append(res, provider)\n\t}\n\n\treturn res\n\n}\n\nfunc (m ModelRoleConfig) GetProvidersForAuthVars(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) []ModelProviderConfigSchema {\n\treturn GetProvidersForAuthVarsWithModelId(authVars, settings, m.ModelId, orgUserConfig)\n}\n\nfunc (m ModelRoleConfig) GetFirstProviderForAuthVars(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) *ModelProviderConfigSchema {\n\tproviders := m.GetProvidersForAuthVars(authVars, settings, orgUserConfig)\n\tif len(providers) == 0 {\n\t\treturn nil\n\t}\n\treturn &providers[0]\n}\n"
  },
  {
    "path": "app/shared/ai_models_roles.go",
    "content": "package shared\n\ntype ModelRole string\n\nconst (\n\tModelRolePlanner          ModelRole = \"planner\"\n\tModelRoleCoder            ModelRole = \"coder\"\n\tModelRoleArchitect        ModelRole = \"architect\"\n\tModelRolePlanSummary      ModelRole = \"summarizer\"\n\tModelRoleBuilder          ModelRole = \"builder\"\n\tModelRoleWholeFileBuilder ModelRole = \"whole-file-builder\"\n\tModelRoleName             ModelRole = \"names\"\n\tModelRoleCommitMsg        ModelRole = \"commit-messages\"\n\tModelRoleExecStatus       ModelRole = \"auto-continue\"\n)\n\nvar AllModelRoles = []ModelRole{ModelRolePlanner, ModelRoleCoder, ModelRoleArchitect, ModelRolePlanSummary, ModelRoleBuilder, ModelRoleWholeFileBuilder, ModelRoleName, ModelRoleCommitMsg, ModelRoleExecStatus}\n\nvar ModelRoleDescriptions = map[ModelRole]string{\n\tModelRolePlanner:          \"replies to prompts and makes plans\",\n\tModelRoleCoder:            \"writes code to implement a plan\",\n\tModelRolePlanSummary:      \"summarizes conversations exceeding max-convo-tokens\",\n\tModelRoleBuilder:          \"builds a plan into file diffs\",\n\tModelRoleWholeFileBuilder: \"builds a plan into file diffs by writing the entire file\",\n\tModelRoleName:             \"names plans\",\n\tModelRoleCommitMsg:        \"writes commit messages\",\n\tModelRoleExecStatus:       \"determines whether to auto-continue\",\n\tModelRoleArchitect:        \"makes high level plan and decides what context to load using codebase map\",\n}\n"
  },
  {
    "path": "app/shared/auth.go",
    "content": "package shared\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype AuthHeader struct {\n\tToken string `json:\"token\"`\n\tOrgId string `json:\"orgId\"`\n\tHash  string `json:\"hash\"`\n}\n\ntype ApiErrorType string\n\nconst (\n\tApiErrorTypeInvalidToken          ApiErrorType = \"invalid_token\"\n\tApiErrorTypeAuthOutdated          ApiErrorType = \"auth_outdated\"\n\tApiErrorTypeTrialPlansExceeded    ApiErrorType = \"trial_plans_exceeded\"\n\tApiErrorTypeTrialMessagesExceeded ApiErrorType = \"trial_messages_exceeded\"\n\tApiErrorTypeTrialActionNotAllowed ApiErrorType = \"trial_action_not_allowed\"\n\n\tApiErrorTypeContinueNoMessages ApiErrorType = \"continue_no_messages\"\n\n\tApiErrorTypeCloudInsufficientCredits ApiErrorType = \"cloud_insufficient_credits\"\n\tApiErrorTypeCloudMonthlyMaxReached   ApiErrorType = \"cloud_monthly_max_reached\"\n\tApiErrorTypeCloudSubscriptionPaused  ApiErrorType = \"cloud_subscription_paused\"\n\tApiErrorTypeCloudSubscriptionOverdue ApiErrorType = \"cloud_subscription_overdue\"\n\n\tApiErrorTypeOther ApiErrorType = \"other\"\n)\n\ntype TrialPlansExceededError struct {\n\tMaxPlans int `json:\"maxPlans\"`\n}\n\ntype TrialMessagesExceededError struct {\n\tMaxReplies int `json:\"maxMessages\"`\n}\n\ntype BillingError struct {\n\tHasBillingPermission bool `json:\"hasBillingPermission\"`\n\tIsTrial              bool `json:\"isTrial\"`\n}\n\ntype ApiError struct {\n\tType   ApiErrorType `json:\"type\"`\n\tStatus int          `json:\"status\"`\n\tMsg    string       `json:\"msg\"`\n\n\t// only used for trial plans exceeded error\n\tTrialPlansExceededError *TrialPlansExceededError `json:\"trialPlansExceededError,omitempty\"`\n\n\t// only used for trial messages exceeded error\n\tTrialMessagesExceededError *TrialMessagesExceededError `json:\"trialMessagesExceededError,omitempty\"`\n\n\t// only used for billing errors\n\tBillingError *BillingError `json:\"billingError,omitempty\"`\n}\n\nfunc (e *ApiError) Error() string {\n\treturn fmt.Sprintf(\"%d Error: %s\", e.Status, e.Msg)\n}\n\ntype ClientAccount struct {\n\tIsCloud     bool   `json:\"isCloud\"`\n\tHost        string `json:\"host\"`\n\tEmail       string `json:\"email\"`\n\tUserName    string `json:\"userName\"`\n\tUserId      string `json:\"userId\"`\n\tToken       string `json:\"token\"`\n\tIsLocalMode bool   `json:\"isLocalMode\"`\n\n\tIsTrial bool `json:\"isTrial\"` // legacy field\n}\n\ntype ClientAuth struct {\n\tClientAccount\n\tOrgId                string `json:\"orgId\"`\n\tOrgName              string `json:\"orgName\"`\n\tOrgIsTrial           bool   `json:\"orgIsTrial\"`\n\tIntegratedModelsMode bool   `json:\"integratedModelsMode\"`\n}\n\n// Helps the client refresh auth if any of the org fields change\nfunc (c *ClientAuth) ToHash() string {\n\ts := strings.Join([]string{\n\t\tc.OrgName,\n\t\tstrconv.FormatBool(c.OrgIsTrial),\n\t\tstrconv.FormatBool(c.IntegratedModelsMode),\n\t}, \"||\")\n\n\thash := sha256.Sum256([]byte(s))\n\treturn hex.EncodeToString(hash[:])\n}\n"
  },
  {
    "path": "app/shared/context.go",
    "content": "package shared\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/olekukonko/tablewriter\"\n)\n\nconst (\n\tMaxContextBodySize           = 25 * 1024 * 1024 // 25MB\n\tMaxContextCount              = 1000\n\tMaxContextMapPaths           = 3000\n\tMaxContextMapSingleInputSize = 500 * 1024             // 500KB\n\tMaxContextMapTotalInputSize  = 250 * 1024 * 1024      // 250MB\n\tMaxTotalContextSize          = 1 * 1024 * 1024 * 1024 // 1GB\n\n\tContextMapMaxBatchBytes = 10 * 1024 * 1024 // 10MB\n\tContextMapMaxBatchSize  = 500\n)\n\ntype ContextUpdateResult struct {\n\tUpdatedContexts []*Context\n\tTokenDiffsById  map[string]int\n\tTokensDiff      int\n\tTotalTokens     int\n\tNumFiles        int\n\tNumUrls         int\n\tNumImages       int\n\tNumTrees        int\n\tNumMaps         int\n\tMaxTokens       int\n}\n\nfunc (c *Context) TypeAndIcon() (string, string) {\n\tvar icon string\n\tvar t string\n\tswitch c.ContextType {\n\tcase ContextFileType:\n\t\ticon = \"📄\"\n\t\tt = \"file\"\n\tcase ContextURLType:\n\t\ticon = \"🌎\"\n\t\tt = \"url\"\n\tcase ContextDirectoryTreeType:\n\t\ticon = \"🗂 \"\n\t\tt = \"tree\"\n\tcase ContextNoteType:\n\t\ticon = \"✏️ \"\n\t\tt = \"note\"\n\tcase ContextPipedDataType:\n\t\ticon = \"↔️ \"\n\t\tt = \"piped\"\n\tcase ContextImageType:\n\t\ticon = \"🖼️ \"\n\t\tt = \"image\"\n\tcase ContextMapType:\n\t\ticon = \"🗺️ \"\n\t\tt = \"map\"\n\t}\n\n\treturn t, icon\n}\n\nfunc TableForLoadContext(contexts []*Context, plaintext bool) string {\n\ttableString := &strings.Builder{}\n\ttable := tablewriter.NewWriter(tableString)\n\ttable.SetHeader([]string{\"Name\", \"Type\", \"🪙\"})\n\ttable.SetAutoWrapText(false)\n\n\tfor _, context := range contexts {\n\t\tt, icon := context.TypeAndIcon()\n\t\trow := []string{\n\t\t\t\" \" + icon + \" \" + context.Name,\n\t\t\tt,\n\t\t\t\"+\" + strconv.Itoa(context.NumTokens),\n\t\t}\n\n\t\tif !plaintext {\n\t\t\ttable.Rich(row, []tablewriter.Colors{\n\t\t\t\t{tablewriter.FgHiGreenColor, tablewriter.Bold},\n\t\t\t\t{tablewriter.FgHiGreenColor},\n\t\t\t\t{tablewriter.FgHiGreenColor},\n\t\t\t})\n\t\t} else {\n\t\t\ttable.Append(row)\n\t\t}\n\t}\n\n\ttable.Render()\n\n\treturn strings.TrimSpace(tableString.String())\n}\n\nfunc MarkdownTableForLoadContext(contexts []*Context) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"| Name | Type | 🪙 |\\n\")\n\tsb.WriteString(\"|------|------|----|\\n\")\n\n\tfor _, context := range contexts {\n\t\tt, icon := context.TypeAndIcon()\n\t\tsb.WriteString(fmt.Sprintf(\"| %s %s | %s | +%d |\\n\",\n\t\t\ticon, context.Name, t, context.NumTokens))\n\t}\n\n\treturn sb.String()\n}\n\nfunc SummaryForLoadContext(contexts []*Context, tokensAdded, totalTokens int) string {\n\n\tvar hasNote bool\n\tvar hasPiped bool\n\n\tvar numFiles int\n\tvar numTrees int\n\tvar numUrls int\n\tvar numMaps int\n\n\tfor _, context := range contexts {\n\t\tswitch context.ContextType {\n\t\tcase ContextFileType:\n\t\t\tnumFiles++\n\t\tcase ContextURLType:\n\t\t\tnumUrls++\n\t\tcase ContextDirectoryTreeType:\n\t\t\tnumTrees++\n\t\tcase ContextNoteType:\n\t\t\thasNote = true\n\t\tcase ContextPipedDataType:\n\t\t\thasPiped = true\n\t\tcase ContextMapType:\n\t\t\tnumMaps++\n\t\t}\n\t}\n\n\tvar added []string\n\n\tif hasNote {\n\t\tadded = append(added, \"a note\")\n\t}\n\tif hasPiped {\n\t\tadded = append(added, \"piped data\")\n\t}\n\tif numFiles > 0 {\n\t\tlabel := \"file\"\n\t\tif numFiles > 1 {\n\t\t\tlabel = \"files\"\n\t\t}\n\t\tadded = append(added, fmt.Sprintf(\"%d %s\", numFiles, label))\n\t}\n\tif numTrees > 0 {\n\t\tlabel := \"directory tree\"\n\t\tif numTrees > 1 {\n\t\t\tlabel = \"directory trees\"\n\t\t}\n\t\tadded = append(added, fmt.Sprintf(\"%d %s\", numTrees, label))\n\t}\n\tif numUrls > 0 {\n\t\tlabel := \"url\"\n\t\tif numUrls > 1 {\n\t\t\tlabel = \"urls\"\n\t\t}\n\t\tadded = append(added, fmt.Sprintf(\"%d %s\", numUrls, label))\n\t}\n\tif numMaps > 0 {\n\t\tlabel := \"map\"\n\t\tif numMaps > 1 {\n\t\t\tlabel = \"maps\"\n\t\t}\n\t\tadded = append(added, fmt.Sprintf(\"%d %s\", numMaps, label))\n\t}\n\n\tmsg := \"Loaded \"\n\n\tif len(added) <= 2 {\n\t\tmsg += strings.Join(added, \" and \")\n\t} else {\n\t\tfor i, add := range added {\n\t\t\tif i == len(added)-1 {\n\t\t\t\tmsg += \", and \" + add\n\t\t\t} else {\n\t\t\t\tmsg += \", \" + add\n\t\t\t}\n\t\t}\n\t}\n\n\tmsg += fmt.Sprintf(\" into context | added → %d 🪙 |  total → %d 🪙\", tokensAdded, totalTokens)\n\n\treturn msg\n}\n\nfunc TableForRemoveContext(contexts []*Context) string {\n\ttableString := &strings.Builder{}\n\ttable := tablewriter.NewWriter(tableString)\n\ttable.SetHeader([]string{\"Name\", \"Type\", \"🪙\"})\n\ttable.SetAutoWrapText(false)\n\n\tfor _, context := range contexts {\n\t\tt, icon := context.TypeAndIcon()\n\t\trow := []string{\n\t\t\t\" \" + icon + \" \" + context.Name,\n\t\t\tt,\n\t\t\t\"-\" + strconv.Itoa(context.NumTokens),\n\t\t}\n\n\t\ttable.Rich(row, []tablewriter.Colors{\n\t\t\t{tablewriter.FgHiRedColor, tablewriter.Bold},\n\t\t\t{tablewriter.FgHiRedColor},\n\t\t\t{tablewriter.FgHiRedColor},\n\t\t})\n\t}\n\n\ttable.Render()\n\n\treturn tableString.String()\n}\n\nfunc SummaryForRemoveContext(contexts []*Context, previousTotalTokens int) string {\n\tremovedTokens := 0\n\n\tfor _, context := range contexts {\n\t\tremovedTokens += context.NumTokens\n\t}\n\n\ttotalTokens := previousTotalTokens - removedTokens\n\n\tsuffix := \"\"\n\tif len(contexts) > 1 {\n\t\tsuffix = \"s\"\n\t}\n\n\treturn fmt.Sprintf(\"Removed %d piece%s of context | removed → %d 🪙 | total → %d 🪙\", len(contexts), suffix, removedTokens, totalTokens)\n}\n\ntype SummaryForUpdateContextParams struct {\n\tNumFiles    int\n\tNumTrees    int\n\tNumUrls     int\n\tNumMaps     int\n\tTokensDiff  int\n\tTotalTokens int\n}\n\nfunc SummaryForUpdateContext(params SummaryForUpdateContextParams) string {\n\tnumFiles := params.NumFiles\n\tnumTrees := params.NumTrees\n\tnumUrls := params.NumUrls\n\tnumMaps := params.NumMaps\n\ttokensDiff := params.TokensDiff\n\ttotalTokens := params.TotalTokens\n\n\tmsg := \"Updated\"\n\tvar toAdd []string\n\tif numFiles > 0 {\n\t\tpostfix := \"s\"\n\t\tif numFiles == 1 {\n\t\t\tpostfix = \"\"\n\t\t}\n\t\ttoAdd = append(toAdd, fmt.Sprintf(\"%d file%s\", numFiles, postfix))\n\t}\n\tif numTrees > 0 {\n\t\tpostfix := \"s\"\n\t\tif numTrees == 1 {\n\t\t\tpostfix = \"\"\n\t\t}\n\t\ttoAdd = append(toAdd, fmt.Sprintf(\"%d tree%s\", numTrees, postfix))\n\t}\n\tif numUrls > 0 {\n\t\tpostfix := \"s\"\n\t\tif numUrls == 1 {\n\t\t\tpostfix = \"\"\n\t\t}\n\t\ttoAdd = append(toAdd, fmt.Sprintf(\"%d url%s\", numUrls, postfix))\n\t}\n\tif numMaps > 0 {\n\t\tpostfix := \"s\"\n\t\tif numMaps == 1 {\n\t\t\tpostfix = \"\"\n\t\t}\n\t\ttoAdd = append(toAdd, fmt.Sprintf(\"%d map%s\", numMaps, postfix))\n\t}\n\n\tif len(toAdd) <= 2 {\n\t\tmsg += \" \" + strings.Join(toAdd, \" and \")\n\t} else {\n\t\tfor i, add := range toAdd {\n\t\t\tif i == len(toAdd)-1 {\n\t\t\t\tmsg += \", and \" + add\n\t\t\t} else {\n\t\t\t\tmsg += \", \" + add\n\t\t\t}\n\t\t}\n\t}\n\n\tmsg += \" in context\"\n\n\taction := \"added\"\n\tif tokensDiff < 0 {\n\t\taction = \"removed\"\n\t}\n\tabsTokenDiff := int(math.Abs(float64(tokensDiff)))\n\tmsg += fmt.Sprintf(\" | %s → %d 🪙 | total → %d 🪙\", action, absTokenDiff, totalTokens)\n\n\treturn msg\n}\n\nfunc TableForContextUpdate(updateRes *ContextUpdateResult) string {\n\tcontexts := updateRes.UpdatedContexts\n\ttokenDiffsById := updateRes.TokenDiffsById\n\n\tif len(contexts) == 0 {\n\t\treturn \"\"\n\t}\n\n\ttableString := &strings.Builder{}\n\ttable := tablewriter.NewWriter(tableString)\n\ttable.SetHeader([]string{\"Name\", \"Type\", \"🪙\"})\n\ttable.SetAutoWrapText(false)\n\n\tfor _, context := range contexts {\n\t\tt, icon := context.TypeAndIcon()\n\t\tdiff := tokenDiffsById[context.Id]\n\n\t\tdiffStr := \"+\" + strconv.Itoa(diff)\n\t\ttableColor := tablewriter.FgHiGreenColor\n\n\t\tif diff < 0 {\n\t\t\tdiffStr = strconv.Itoa(diff)\n\t\t\ttableColor = tablewriter.FgHiRedColor\n\t\t}\n\n\t\trow := []string{\n\t\t\t\" \" + icon + \" \" + context.Name,\n\t\t\tt,\n\t\t\tdiffStr,\n\t\t}\n\n\t\ttable.Rich(row, []tablewriter.Colors{\n\t\t\t{tableColor, tablewriter.Bold},\n\t\t\t{tableColor},\n\t\t\t{tableColor},\n\t\t})\n\t}\n\n\ttable.Render()\n\n\treturn tableString.String()\n}\n"
  },
  {
    "path": "app/shared/convo_message.go",
    "content": "package shared\n\nfunc (f *ConvoMessageFlags) GetReplyTags() []string {\n\tvar replyTags []string\n\tif f.DidLoadContext {\n\t\treplyTags = append(replyTags, \"📥 Loaded Context\")\n\t}\n\tif f.DidMakePlan {\n\t\tif f.DidMakeDebuggingPlan {\n\t\t\treplyTags = append(replyTags, \"🐞 Made Debug Plan\")\n\t\t} else if f.DidRemoveTasks {\n\t\t\treplyTags = append(replyTags, \"🔄 Revised Plan\")\n\t\t} else {\n\t\t\treplyTags = append(replyTags, \"📋 Made Plan\")\n\t\t}\n\t}\n\tif f.DidWriteCode {\n\t\treplyTags = append(replyTags, \"👨‍💻 Wrote Code\")\n\t}\n\t// if f.DidCompleteTask {\n\t// \treplyTags = append(replyTags, \"✅\")\n\t// }\n\tif f.DidCompletePlan {\n\t\treplyTags = append(replyTags, \"🏁\")\n\t}\n\n\tif f.HasError {\n\t\treplyTags = append(replyTags, \"🚨 Error\")\n\t}\n\n\treturn replyTags\n}\n"
  },
  {
    "path": "app/shared/data_models.go",
    "content": "package shared\n\nimport (\n\t\"time\"\n\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/shopspring/decimal\"\n)\n\ntype Org struct {\n\tId                 string `json:\"id\"`\n\tName               string `json:\"name\"`\n\tIsTrial            bool   `json:\"isTrial\"`\n\tAutoAddDomainUsers bool   `json:\"autoAddDomainUsers\"`\n\n\t// optional cloud attributes\n\tIntegratedModelsMode bool                `json:\"integratedModelsMode,omitempty\"`\n\tCloudBillingFields   *CloudBillingFields `json:\"cloudBillingFields,omitempty\"`\n}\n\ntype User struct {\n\tId               string `json:\"id\"`\n\tName             string `json:\"name\"`\n\tEmail            string `json:\"email\"`\n\tIsTrial          bool   `json:\"isTrial\"`\n\tNumNonDraftPlans int    `json:\"numNonDraftPlans\"`\n\n\tDefaultPlanConfig *PlanConfig `json:\"defaultPlanConfig,omitempty\"`\n}\n\ntype OrgUser struct {\n\tOrgId     string `json:\"orgId\"`\n\tUserId    string `json:\"userId\"`\n\tOrgRoleId string `json:\"orgRoleId\"`\n\n\tConfig *OrgUserConfig `json:\"config,omitempty\"`\n}\n\ntype Invite struct {\n\tId         string     `json:\"id\"`\n\tOrgId      string     `json:\"orgId\"`\n\tEmail      string     `json:\"email\"`\n\tName       string     `json:\"name\"`\n\tOrgRoleId  string     `json:\"orgRoleId\"`\n\tInviterId  string     `json:\"inviterId\"`\n\tInviteeId  *string    `json:\"inviteeId\"`\n\tAcceptedAt *time.Time `json:\"acceptedAt\"`\n\tCreatedAt  time.Time  `json:\"createdAt\"`\n}\n\ntype Project struct {\n\tId   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype Plan struct {\n\tId              string      `json:\"id\"`\n\tOwnerId         string      `json:\"ownerId\"`\n\tProjectId       string      `json:\"projectId\"`\n\tName            string      `json:\"name\"`\n\tSharedWithOrgAt *time.Time  `json:\"sharedWithOrgAt,omitempty\"`\n\tTotalReplies    int         `json:\"totalReplies\"`\n\tActiveBranches  int         `json:\"activeBranches\"`\n\tPlanConfig      *PlanConfig `json:\"planConfig,omitempty\"`\n\tArchivedAt      *time.Time  `json:\"archivedAt,omitempty\"`\n\tCreatedAt       time.Time   `json:\"createdAt\"`\n\tUpdatedAt       time.Time   `json:\"updatedAt\"`\n}\n\ntype Branch struct {\n\tId              string     `json:\"id\"`\n\tPlanId          string     `json:\"planId\"`\n\tOwnerId         string     `json:\"ownerId\"`\n\tParentBranchId  *string    `json:\"parentBranchId\"`\n\tName            string     `json:\"name\"`\n\tStatus          PlanStatus `json:\"status\"`\n\tContextTokens   int        `json:\"contextTokens\"`\n\tConvoTokens     int        `json:\"convoTokens\"`\n\tSharedWithOrgAt *time.Time `json:\"sharedWithOrgAt,omitempty\"`\n\tArchivedAt      *time.Time `json:\"archivedAt,omitempty\"`\n\tCreatedAt       time.Time  `json:\"createdAt\"`\n\tUpdatedAt       time.Time  `json:\"updatedAt\"`\n}\n\ntype ContextType string\n\nconst (\n\tContextFileType          ContextType = \"file\"\n\tContextURLType           ContextType = \"url\"\n\tContextNoteType          ContextType = \"note\"\n\tContextDirectoryTreeType ContextType = \"directory tree\"\n\tContextPipedDataType     ContextType = \"piped data\"\n\tContextImageType         ContextType = \"image\"\n\tContextMapType           ContextType = \"map\"\n)\n\ntype FileMapBodies map[string]string\n\ntype Context struct {\n\tId              string                `json:\"id\"`\n\tOwnerId         string                `json:\"ownerId\"`\n\tContextType     ContextType           `json:\"contextType\"`\n\tName            string                `json:\"name\"`\n\tUrl             string                `json:\"url\"`\n\tFilePath        string                `json:\"file_path\"`\n\tSha             string                `json:\"sha\"`\n\tNumTokens       int                   `json:\"numTokens\"`\n\tBody            string                `json:\"body,omitempty\"`\n\tBodySize        int64                 `json:\"bodySize,omitempty\"`\n\tForceSkipIgnore bool                  `json:\"forceSkipIgnore\"`\n\tImageDetail     openai.ImageURLDetail `json:\"imageDetail,omitempty\"`\n\tMapParts        FileMapBodies         `json:\"mapParts,omitempty\"`\n\tMapShas         map[string]string     `json:\"mapShas,omitempty\"`\n\tMapTokens       map[string]int        `json:\"mapTokens,omitempty\"`\n\tMapSizes        map[string]int64      `json:\"mapSizes,omitempty\"`\n\tAutoLoaded      bool                  `json:\"autoLoaded\"`\n\tCreatedAt       time.Time             `json:\"createdAt\"`\n\tUpdatedAt       time.Time             `json:\"updatedAt\"`\n}\n\ntype TellStage string\n\nconst (\n\tTellStagePlanning       TellStage = \"planning\"\n\tTellStageImplementation TellStage = \"implementation\"\n)\n\ntype PlanningPhase string\n\nconst (\n\tPlanningPhaseContext PlanningPhase = \"context\"\n\tPlanningPhaseTasks   PlanningPhase = \"tasks\"\n)\n\ntype CurrentStage struct {\n\tTellStage     TellStage\n\tPlanningPhase PlanningPhase\n}\n\ntype ConvoMessageFlags struct {\n\tDidMakePlan           bool `json:\"didMakePlan\"`\n\tDidRemoveTasks        bool `json:\"didRemoveTasks\"`\n\tDidMakeDebuggingPlan  bool `json:\"didMakeDebuggingPlan\"`\n\tDidLoadContext        bool `json:\"didLoadContext\"`\n\tCurrentStage          CurrentStage\n\tIsChat                bool `json:\"isChat\"`\n\tDidWriteCode          bool `json:\"didWriteCode\"`\n\tDidCompleteTask       bool `json:\"didCompleteTask\"`\n\tDidCompletePlan       bool `json:\"didCompletePlan\"`\n\tHasUnfinishedSubtasks bool `json:\"hasUnfinishedSubtasks\"`\n\tIsApplyDebug          bool `json:\"isApplyDebug\"`\n\tIsUserDebug           bool `json:\"isUserDebug\"`\n\tHasError              bool `json:\"hasError\"`\n}\n\ntype Subtask struct {\n\tTitle       string   `json:\"title\"`\n\tDescription string   `json:\"description\"`\n\tUsesFiles   []string `json:\"usesFiles\"`\n\tIsFinished  bool     `json:\"isFinished\"`\n}\n\ntype ConvoMessage struct {\n\tId               string            `json:\"id\"`\n\tUserId           string            `json:\"userId\"`\n\tRole             string            `json:\"role\"`\n\tTokens           int               `json:\"tokens\"`\n\tNum              int               `json:\"num\"`\n\tMessage          string            `json:\"message\"`\n\tStopped          bool              `json:\"stopped\"`\n\tFlags            ConvoMessageFlags `json:\"flags\"`\n\tSubtask          *Subtask          `json:\"subtask,omitempty\"`\n\tAddedSubtasks    []*Subtask        `json:\"addedSubtasks,omitempty\"`\n\tRemovedSubtasks  []string          `json:\"removedSubtasks,omitempty\"`\n\tActiveContextIds []string          `json:\"activeContextIds\"`\n\tCreatedAt        time.Time         `json:\"createdAt\"`\n}\n\ntype ConvoSummary struct {\n\tId                          string    `json:\"id\"`\n\tLatestConvoMessageCreatedAt time.Time `json:\"latestConvoMessageCreatedAt\"`\n\tLatestConvoMessageId        string    `json:\"lastestConvoMessageId\"`\n\tSummary                     string    `json:\"summary\"`\n\tTokens                      int       `json:\"tokens\"`\n\tNumMessages                 int       `json:\"numMessages\"`\n\tCreatedAt                   time.Time `json:\"createdAt\"`\n}\n\ntype OperationType string\n\nconst (\n\tOperationTypeFile   OperationType = \"file\"\n\tOperationTypeMove   OperationType = \"move\"\n\tOperationTypeRemove OperationType = \"remove\"\n\tOperationTypeReset  OperationType = \"reset\"\n)\n\ntype Operation struct {\n\tType        OperationType\n\tPath        string\n\tDestination string\n\tContent     string\n\tDescription string\n\tReplyBefore string\n\tNumTokens   int\n}\n\nfunc (o *Operation) Name() string {\n\tres := string(o.Type) + \" | \" + o.Path\n\tif o.Destination != \"\" {\n\t\tres += \" → \" + o.Destination\n\t}\n\treturn res\n}\n\ntype ConvoMessageDescription struct {\n\tId                    string `json:\"id\"`\n\tConvoMessageId        string `json:\"convoMessageId\"`\n\tSummarizedToMessageId string `json:\"summarizedToMessageId\"`\n\tWroteFiles            bool   `json:\"wroteFiles\"`\n\tCommitMsg             string `json:\"commitMsg\"`\n\t// Files                 []string        `json:\"files\"`\n\tOperations            []*Operation    `json:\"operations\"`\n\tDidBuild              bool            `json:\"didBuild\"`\n\tBuildPathsInvalidated map[string]bool `json:\"buildPathsInvalidated\"`\n\tError                 string          `json:\"error\"`\n\tAppliedAt             *time.Time      `json:\"appliedAt,omitempty\"`\n\tCreatedAt             time.Time       `json:\"createdAt\"`\n\tUpdatedAt             time.Time       `json:\"updatedAt\"`\n}\n\ntype PlanBuild struct {\n\tId             string    `json:\"id\"`\n\tConvoMessageId string    `json:\"convoMessageId\"`\n\tFilePath       string    `json:\"filePath\"`\n\tError          string    `json:\"error\"`\n\tCreatedAt      time.Time `json:\"createdAt\"`\n\tUpdatedAt      time.Time `json:\"updatedAt\"`\n}\n\ntype Replacement struct {\n\tId             string                      `json:\"id\"`\n\tOld            string                      `json:\"old\"`\n\tSummary        string                      `json:\"summary\"`\n\tEntireFile     bool                        `json:\"entireFile\"`\n\tNew            string                      `json:\"new\"`\n\tFailed         bool                        `json:\"failed\"`\n\tRejectedAt     *time.Time                  `json:\"rejectedAt,omitempty\"`\n\tStreamedChange *StreamedChangeWithLineNums `json:\"streamedChange\"`\n}\n\ntype PlanFileResult struct {\n\tId                  string         `json:\"id\"`\n\tTypeVersion         int            `json:\"typeVersion\"`\n\tReplaceWithLineNums bool           `json:\"replaceWithLineNums\"`\n\tConvoMessageId      string         `json:\"convoMessageId\"`\n\tPlanBuildId         string         `json:\"planBuildId\"`\n\tPath                string         `json:\"path\"`\n\tContent             string         `json:\"content\"`\n\tAnyFailed           bool           `json:\"anyFailed\"`\n\tAppliedAt           *time.Time     `json:\"appliedAt,omitempty\"`\n\tRejectedAt          *time.Time     `json:\"rejectedAt,omitempty\"`\n\tReplacements        []*Replacement `json:\"replacements\"`\n\n\tRemovedFile bool `json:\"removedFile\"`\n\n\tCreatedAt time.Time `json:\"createdAt\"`\n\tUpdatedAt time.Time `json:\"updatedAt\"`\n}\n\ntype CurrentPlanFiles struct {\n\tFiles           map[string]string    `json:\"files\"`\n\tRemoved         map[string]bool      `json:\"removedByPath\"`\n\tUpdatedAtByPath map[string]time.Time `json:\"updatedAtByPath\"`\n}\n\ntype PlanFileResultsByPath map[string][]*PlanFileResult\ntype PlanResult struct {\n\tSortedPaths        []string                  `json:\"sortedPaths\"`\n\tFileResultsByPath  PlanFileResultsByPath     `json:\"fileResultsByPath\"`\n\tResults            []*PlanFileResult         `json:\"results\"`\n\tReplacementsByPath map[string][]*Replacement `json:\"replacementsByPath\"`\n}\n\ntype PlanApply struct {\n\tId                         string    `json:\"id\"`\n\tUserId                     string    `json:\"userId\"`\n\tConvoMessageIds            []string  `json:\"convoMessageIds\"`\n\tConvoMessageDescriptionIds []string  `json:\"convoMessageDescriptionIds\"`\n\tPlanFileResultIds          []string  `json:\"planFileResultIds\"`\n\tCommitMsg                  string    `json:\"commitMsg\"`\n\tCreatedAt                  time.Time `json:\"createdAt\"`\n}\n\ntype CurrentPlanState struct {\n\tPlanResult               *PlanResult                `json:\"planResult\"`\n\tCurrentPlanFiles         *CurrentPlanFiles          `json:\"currentPlanFiles\"`\n\tConvoMessageDescriptions []*ConvoMessageDescription `json:\"convoMessageDescriptions\"`\n\tPlanApplies              []*PlanApply               `json:\"planApplies\"`\n\tContextsByPath           map[string]*Context        `json:\"contextsByPath\"`\n}\n\ntype OrgRole struct {\n\tId          string `json:\"id\"`\n\tIsDefault   bool   `json:\"isDefault\"`\n\tLabel       string `json:\"label\"`\n\tDescription string `json:\"description\"`\n}\n\ntype CloudBillingFields struct {\n\tCreditsBalance        decimal.Decimal `json:\"creditsBalance\"`\n\tMonthlyGrant          decimal.Decimal `json:\"monthlyGrant\"`\n\tAutoRebuyEnabled      bool            `json:\"autoRebuyEnabled\"`\n\tAutoRebuyMinThreshold decimal.Decimal `json:\"autoRebuyMinThreshold\"`\n\tAutoRebuyToBalance    decimal.Decimal `json:\"autoRebuyToBalance\"`\n\tNotifyThreshold       decimal.Decimal `json:\"notifyThreshold\"`\n\tMaxThresholdPerMonth  decimal.Decimal `json:\"maxThresholdPerMonth\"`\n\tBillingCycleStartedAt time.Time       `json:\"billingCycleStartedAt\"`\n\n\tChangedBillingMode bool `json:\"changedBillingMode\"`\n\tTrialPaid          bool `json:\"trialPaid\"`\n\n\tStripeSubscriptionId                 *string    `json:\"stripeSubscriptionId\"`\n\tSubscriptionStatus                   *string    `json:\"subscriptionStatus\"`\n\tSubscriptionPausedAt                 *time.Time `json:\"subscriptionPausedAt\"`\n\tStripePaymentMethod                  *string    `json:\"stripePaymentMethod\"`\n\tSubscriptionActionRequired           bool       `json:\"subscriptionActionRequired\"` // for 3ds/sca approvals\n\tSubscriptionActionRequiredInvoiceUrl *string    `json:\"subscriptionActionRequiredInvoiceUrl\"`\n}\n\ntype CreditsTransactionType string\n\nconst (\n\tCreditsTransactionTypeCredit CreditsTransactionType = \"credit\"\n\tCreditsTransactionTypeDebit  CreditsTransactionType = \"debit\"\n)\n\ntype CreditType string\n\nconst (\n\tCreditTypeTrial      CreditType = \"trial\"\n\tCreditTypeGrant      CreditType = \"grant\"\n\tCreditTypeAdminGrant CreditType = \"admin_grant\"\n\tCreditTypePurchase   CreditType = \"purchase\"\n\tCreditTypeSwitch     CreditType = \"switch\"\n)\n\ntype CreditsTransaction struct {\n\tId              string                 `json:\"id\"`\n\tOrgId           string                 `json:\"orgId\"`\n\tOrgName         string                 `json:\"orgName\"`\n\tUserId          *string                `json:\"userId\"`\n\tUserEmail       *string                `json:\"userEmail\"`\n\tUserName        *string                `json:\"userName\"`\n\tTransactionType CreditsTransactionType `json:\"transactionType\"`\n\tAmount          decimal.Decimal        `json:\"amount\"`\n\tStartBalance    decimal.Decimal        `json:\"startBalance\"`\n\tEndBalance      decimal.Decimal        `json:\"endBalance\"`\n\n\tCreditType                  *CreditType      `json:\"creditType,omitempty\"`\n\tCreditIsAutoRebuy           bool             `json:\"creditIsAutoRebuy\"`\n\tCreditAutoRebuyMinThreshold *decimal.Decimal `json:\"creditAutoRebuyMinThreshold,omitempty\"`\n\tCreditAutoRebuyToBalance    *decimal.Decimal `json:\"creditAutoRebuyToBalance,omitempty\"`\n\n\tDebitInputTokens              *int             `json:\"debitInputTokens,omitempty\"`\n\tDebitOutputTokens             *int             `json:\"debitOutputTokens,omitempty\"`\n\tDebitModelInputPricePerToken  *decimal.Decimal `json:\"debitModelInputPricePerToken,omitempty\"`\n\tDebitModelOutputPricePerToken *decimal.Decimal `json:\"debitModelOutputPricePerToken,omitempty\"`\n\n\tDebitBaseAmount *decimal.Decimal `json:\"debitBaseAmount,omitempty\"`\n\tDebitSurcharge  *decimal.Decimal `json:\"debitSurcharge,omitempty\"`\n\n\tDebitModelProvider *ModelProvider `json:\"debitModelProvider,omitempty\"`\n\tDebitModelName     *string        `json:\"debitModelName,omitempty\"`\n\tDebitModelPackName *string        `json:\"debitModelPackName,omitempty\"`\n\tDebitModelRole     *ModelRole     `json:\"debitModelRole,omitempty\"`\n\n\tDebitPurpose  *string `json:\"debitPurpose,omitempty\"`\n\tDebitPlanId   *string `json:\"debitPlanId,omitempty\"`\n\tDebitPlanName *string `json:\"debitPlanName,omitempty\"`\n\tDebitId       *string `json:\"debitId,omitempty\"`\n\n\tDebitCacheDiscount *decimal.Decimal `json:\"debitCacheDiscount,omitempty\"`\n\n\tDebitSessionId *string `json:\"debitSessionId,omitempty\"`\n\n\tCreatedAt time.Time `json:\"createdAt\"`\n}\n\nfunc (t *CreditsTransaction) ModelString() string {\n\ts := \"\"\n\tif t.DebitModelProvider != nil && *t.DebitModelProvider != ModelProviderOpenAI {\n\t\ts += string(*t.DebitModelProvider) + \"/\"\n\t}\n\tif t.DebitModelName != nil {\n\t\ts += *t.DebitModelName\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "app/shared/email.go",
    "content": "package shared\n\nfunc IsEmailServiceDomain(domain string) bool {\n\t_, ok := emailServiceDomains[domain]\n\treturn ok\n}\n\nvar emailServiceDomains = map[string]bool{\n\t\"gmail.com\":        true,\n\t\"yahoo.com\":        true,\n\t\"outlook.com\":      true,\n\t\"hotmail.com\":      true,\n\t\"icloud.com\":       true,\n\t\"zoho.com\":         true,\n\t\"protonmail.com\":   true,\n\t\"mail.com\":         true,\n\t\"yandex.com\":       true,\n\t\"fastmail.com\":     true,\n\t\"tutanota.com\":     true,\n\t\"hey.com\":          true,\n\t\"hushmail.com\":     true,\n\t\"runbox.com\":       true,\n\t\"mailfence.com\":    true,\n\t\"disroot.org\":      true,\n\t\"posteo.de\":        true,\n\t\"mailinator.com\":   true,\n\t\"aol.com\":          true,\n\t\"hotmail.co.uk\":    true,\n\t\"hotmail.fr\":       true,\n\t\"msn.com\":          true,\n\t\"yahoo.fr\":         true,\n\t\"wanadoo.fr\":       true,\n\t\"orange.fr\":        true,\n\t\"comcast.net\":      true,\n\t\"yahoo.co.uk\":      true,\n\t\"yahoo.com.br\":     true,\n\t\"yahoo.co.in\":      true,\n\t\"live.com\":         true,\n\t\"rediffmail.com\":   true,\n\t\"free.fr\":          true,\n\t\"gmx.de\":           true,\n\t\"web.de\":           true,\n\t\"yandex.ru\":        true,\n\t\"ymail.com\":        true,\n\t\"libero.it\":        true,\n\t\"uol.com.br\":       true,\n\t\"bol.com.br\":       true,\n\t\"mail.ru\":          true,\n\t\"cox.net\":          true,\n\t\"hotmail.it\":       true,\n\t\"sbcglobal.net\":    true,\n\t\"sfr.fr\":           true,\n\t\"live.fr\":          true,\n\t\"verizon.net\":      true,\n\t\"live.co.uk\":       true,\n\t\"googlemail.com\":   true,\n\t\"yahoo.es\":         true,\n\t\"ig.com.br\":        true,\n\t\"live.nl\":          true,\n\t\"bigpond.com\":      true,\n\t\"terra.com.br\":     true,\n\t\"yahoo.it\":         true,\n\t\"neuf.fr\":          true,\n\t\"yahoo.de\":         true,\n\t\"alice.it\":         true,\n\t\"rocketmail.com\":   true,\n\t\"att.net\":          true,\n\t\"laposte.net\":      true,\n\t\"facebook.com\":     true,\n\t\"bellsouth.net\":    true,\n\t\"yahoo.in\":         true,\n\t\"hotmail.es\":       true,\n\t\"charter.net\":      true,\n\t\"yahoo.ca\":         true,\n\t\"yahoo.com.au\":     true,\n\t\"rambler.ru\":       true,\n\t\"hotmail.de\":       true,\n\t\"tiscali.it\":       true,\n\t\"shaw.ca\":          true,\n\t\"yahoo.co.jp\":      true,\n\t\"sky.com\":          true,\n\t\"earthlink.net\":    true,\n\t\"optonline.net\":    true,\n\t\"freenet.de\":       true,\n\t\"t-online.de\":      true,\n\t\"aliceadsl.fr\":     true,\n\t\"virgilio.it\":      true,\n\t\"home.nl\":          true,\n\t\"qq.com\":           true,\n\t\"telenet.be\":       true,\n\t\"me.com\":           true,\n\t\"yahoo.com.ar\":     true,\n\t\"tiscali.co.uk\":    true,\n\t\"yahoo.com.mx\":     true,\n\t\"voila.fr\":         true,\n\t\"gmx.net\":          true,\n\t\"planet.nl\":        true,\n\t\"tin.it\":           true,\n\t\"live.it\":          true,\n\t\"ntlworld.com\":     true,\n\t\"arcor.de\":         true,\n\t\"yahoo.co.id\":      true,\n\t\"frontiernet.net\":  true,\n\t\"hetnet.nl\":        true,\n\t\"live.com.au\":      true,\n\t\"yahoo.com.sg\":     true,\n\t\"zonnet.nl\":        true,\n\t\"club-internet.fr\": true,\n\t\"juno.com\":         true,\n\t\"optusnet.com.au\":  true,\n\t\"blueyonder.co.uk\": true,\n\t\"bluewin.ch\":       true,\n\t\"skynet.be\":        true,\n\t\"sympatico.ca\":     true,\n\t\"windstream.net\":   true,\n\t\"mac.com\":          true,\n\t\"centurytel.net\":   true,\n\t\"chello.nl\":        true,\n\t\"live.ca\":          true,\n\t\"aim.com\":          true,\n\t\"bigpond.net.au\":   true,\n\t\"163.com\":          true,\n\t\"126.com\":          true,\n\t\"daum.net\":         true,\n\t\"naver.com\":        true,\n\t\"sina.com\":         true,\n\t\"sohu.com\":         true,\n\t\"gmx.com\":          true,\n\t\"yopmail.com\":      true,\n\t\"bk.ru\":            true,\n\t\"list.ru\":          true,\n\t\"seznam.cz\":        true,\n\t\"nextmail.ru\":      true,\n\t\"kpnmail.nl\":       true,\n}\n"
  },
  {
    "path": "app/shared/file_maps.go",
    "content": "package shared\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n)\n\nfunc (m FileMapBodies) CombinedMap(tokensByPath map[string]int) string {\n\tvar combinedMap strings.Builder\n\tpaths := make([]string, 0, len(m))\n\tfor path := range m {\n\t\tpaths = append(paths, path)\n\t}\n\tsort.Strings(paths)\n\tfor _, path := range paths {\n\t\tbody := m[path]\n\t\tbody = strings.TrimSpace(body)\n\t\tfileHeading := MapFileHeading(path, tokensByPath[path])\n\t\tcombinedMap.WriteString(fileHeading)\n\t\tif body == \"\" {\n\t\t\tcombinedMap.WriteString(\"[NO MAP]\")\n\t\t} else {\n\t\t\tcombinedMap.WriteString(body)\n\t\t}\n\t\tcombinedMap.WriteString(\"\\n\")\n\t}\n\treturn combinedMap.String()\n}\n\nfunc MapFileHeading(path string, tokens int) string {\n\treturn fmt.Sprintf(\"\\n### %s (%d 🪙)\\n\\n\", path, tokens)\n}\n"
  },
  {
    "path": "app/shared/go.mod",
    "content": "module plandex-shared\n\ngo 1.23.3\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/jinzhu/copier v0.4.0\n\tgithub.com/pkoukk/tiktoken-go v0.1.7\n)\n\nrequire (\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n)\n\nrequire (\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/olekukonko/tablewriter v0.0.5\n\tgithub.com/sashabaranov/go-openai v1.38.1\n\tgithub.com/shopspring/decimal v1.4.0\n\tgolang.org/x/image v0.25.0\n)\n"
  },
  {
    "path": "app/shared/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=\ngithub.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=\ngithub.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw=\ngithub.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=\ngithub.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngolang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=\ngolang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "app/shared/images.go",
    "content": "package shared\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"image\"\n\t\"io\"\n\t\"log\"\n\t\"math\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sashabaranov/go-openai\"\n\n\t_ \"image/gif\"\n\t_ \"image/jpeg\"\n\t_ \"image/png\"\n\n\t_ \"golang.org/x/image/webp\"\n)\n\nfunc GetImageTokens(base64Image string, detail openai.ImageURLDetail) (int, error) {\n\timageData, err := base64.StdEncoding.DecodeString(base64Image)\n\tif err != nil {\n\t\tlog.Println(\"failed to decode base64 image data:\", err)\n\t\treturn 0, fmt.Errorf(\"failed to decode base64 image data: %w\", err)\n\t}\n\n\treturn GetImageTokensFromHeader(bytes.NewReader(imageData), detail, int64(len(imageData)))\n}\n\nfunc GetImageTokensFromHeader(reader io.Reader, detail openai.ImageURLDetail, maxBytes int64) (int, error) {\n\treader = io.LimitReader(reader, maxBytes)\n\timg, _, err := image.DecodeConfig(reader)\n\tif err != nil {\n\t\tlog.Println(\"failed to decode image config:\", err)\n\t\treturn 0, fmt.Errorf(\"failed to decode image config: %w\", err)\n\t}\n\n\twidth, height := img.Width, img.Height\n\n\tanthropicTokens := getAnthropicImageTokens(width, height)\n\tgoogleTokens := getGoogleImageTokens(width, height)\n\topenaiTokens := getOpenAIImageTokens(width, height, detail)\n\n\t// log.Printf(\"GetImageTokens - width: %d, height: %d\\n\", width, height)\n\t// log.Printf(\"GetImageTokens - anthropicTokens: %d\\n\", anthropicTokens)\n\t// log.Printf(\"GetImageTokens - googleTokens: %d\\n\", googleTokens)\n\t// log.Printf(\"GetImageTokens - openaiTokens: %d\\n\", openaiTokens)\n\n\t// get max of the three\n\treturn int(math.Max(\n\t\tfloat64(anthropicTokens),\n\t\tmath.Max(\n\t\t\tfloat64(googleTokens),\n\t\t\tfloat64(openaiTokens),\n\t\t),\n\t)), nil\n}\n\nfunc GetImageTokensEstimateFromBytes(l int64) int {\n\treturn int(l) / 750\n}\n\nfunc getAnthropicImageTokens(width, height int) int {\n\t// Anthropic uses a simple area-based calculation (1 token per ~750 px²)\n\tarea := width * height\n\treturn int(math.Ceil(float64(area) / 750.0))\n}\n\nfunc getGoogleImageTokens(width, height int) int {\n\t// Google Gemini uses 768px tiles at 258 tokens per tile\n\tconst tileSize = 768\n\tconst tokensPerTile = 258\n\n\thorizontalTiles := int(math.Ceil(float64(width) / float64(tileSize)))\n\tverticalTiles := int(math.Ceil(float64(height) / float64(tileSize)))\n\n\tnumTiles := horizontalTiles * verticalTiles\n\treturn numTiles * tokensPerTile\n}\n\nfunc getOpenAIImageTokens(width, height int, detail openai.ImageURLDetail) int {\n\tconst (\n\t\tlowDetailTokens  = 85\n\t\thighDetailBase   = 85\n\t\thighDetailFactor = 170\n\t)\n\n\tif detail == openai.ImageURLDetailLow {\n\t\treturn lowDetailTokens\n\t}\n\n\t// Scale to fit within 2048px square\n\tif width > 2048 || height > 2048 {\n\t\tscaleFactor := math.Min(2048.0/float64(width), 2048.0/float64(height))\n\t\twidth = int(float64(width) * scaleFactor)\n\t\theight = int(float64(height) * scaleFactor)\n\t}\n\n\t// Scale shortest side to 768px\n\tif width < height {\n\t\tscaleFactor := 768.0 / float64(width)\n\t\twidth = 768\n\t\theight = int(float64(height) * scaleFactor)\n\t} else {\n\t\tscaleFactor := 768.0 / float64(height)\n\t\theight = 768\n\t\twidth = int(float64(width) * scaleFactor)\n\t}\n\n\t// Calculate 512px tiles\n\thorizontalTiles := int(math.Ceil(float64(width) / 512.0))\n\tverticalTiles := int(math.Ceil(float64(height) / 512.0))\n\n\tnumTiles := horizontalTiles * verticalTiles\n\treturn highDetailBase + numTiles*highDetailFactor\n}\n\nfunc GetImageDataURI(base64Image, path string) string {\n\tmimeType := ImageMimeType(path)\n\treturn fmt.Sprintf(\"data:%s;base64,%s\", mimeType, base64Image)\n}\n\nfunc IsImageFile(filePath string) bool {\n\text := strings.ToLower(filepath.Ext(filePath))\n\treturn ext == \".jpg\" || ext == \".jpeg\" || ext == \".png\" || ext == \".webp\" || ext == \".gif\"\n}\n\nfunc ImageMimeType(filePath string) string {\n\text := strings.ToLower(filepath.Ext(filePath))\n\tswitch ext {\n\tcase \".jpg\", \".jpeg\":\n\t\treturn \"image/jpeg\"\n\tcase \".png\":\n\t\treturn \"image/png\"\n\tcase \".webp\":\n\t\treturn \"image/webp\"\n\tcase \".gif\":\n\t\treturn \"image/gif\"\n\t}\n\treturn \"application/octet-stream\"\n}\n"
  },
  {
    "path": "app/shared/org_user_config.go",
    "content": "package shared\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n)\n\n// Claude pro and max use time-based cooldowns (4 hours, 8 hours, etc.) after reaching quota\n// However, we still use a relatively short cooldown so that we find out fairly quickly if the quota has reset\nconst claudeSubscriptionCooldownDuration = 10 * time.Minute\n\ntype OrgUserConfig struct {\n\tPromptedClaudeMax                   bool      `json:\"promptedClaudeMax\"`\n\tUseClaudeSubscription               bool      `json:\"useClaudeSubscription\"`\n\tClaudeSubscriptionCooldownStartedAt time.Time `json:\"claudeSubscriptionCooldownStartedAt\"`\n}\n\nfunc (p *OrgUserConfig) IsClaudeSubscriptionCooldownActive() bool {\n\tif p == nil || p.ClaudeSubscriptionCooldownStartedAt.IsZero() {\n\t\treturn false // never started\n\t}\n\treturn time.Since(p.ClaudeSubscriptionCooldownStartedAt) < claudeSubscriptionCooldownDuration\n}\n\nfunc (p *OrgUserConfig) Scan(src interface{}) error {\n\tif src == nil {\n\t\t*p = OrgUserConfig{}\n\t\treturn nil\n\t}\n\tswitch s := src.(type) {\n\tcase []byte:\n\t\tif len(s) == 0 {\n\t\t\t*p = OrgUserConfig{}\n\t\t\treturn nil\n\t\t}\n\t\treturn json.Unmarshal(s, p)\n\tcase string:\n\t\tif s == \"\" {\n\t\t\t*p = OrgUserConfig{}\n\t\t\treturn nil\n\t\t}\n\t\treturn json.Unmarshal([]byte(s), p)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported data type: %T\", src)\n\t}\n}\n\nfunc (p *OrgUserConfig) Value() (driver.Value, error) {\n\tif p == nil {\n\t\treturn nil, nil\n\t}\n\treturn json.Marshal(p)\n}\n"
  },
  {
    "path": "app/shared/plan_config.go",
    "content": "package shared\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst defaultAutoDebugTries = 5\n\nconst (\n\tEditorTypeVim  string = \"vim\"\n\tEditorTypeNano string = \"nano\"\n)\n\nconst defaultEditor = EditorTypeVim\n\ntype AutoModeType string\n\nconst (\n\tAutoModeFull   AutoModeType = \"full\"\n\tAutoModeSemi   AutoModeType = \"semi\"\n\tAutoModePlus   AutoModeType = \"plus\"\n\tAutoModeBasic  AutoModeType = \"basic\"\n\tAutoModeNone   AutoModeType = \"none\"\n\tAutoModeCustom AutoModeType = \"custom\"\n)\n\nvar AutoModeDescriptions = map[AutoModeType]string{\n\tAutoModeFull:   \"Fully automated: context, apply, execution and debugging\",\n\tAutoModeSemi:   \"Auto context, manual apply and execution\",\n\tAutoModePlus:   \"Manual context with auto updates and smart loading, manual apply and execution\",\n\tAutoModeBasic:  \"Manual context, manual apply and execution\",\n\tAutoModeNone:   \"Fully manual and step-by-step, one response at a time, manual builds\",\n\tAutoModeCustom: \"Choose settings individually with set-config command\",\n}\n\nvar AutoModeOptions = [][3]string{\n\t{string(AutoModeFull), \"Full Auto\", AutoModeDescriptions[AutoModeFull]},\n\t{string(AutoModeSemi), \"Semi Auto\", AutoModeDescriptions[AutoModeSemi]},\n\t{string(AutoModePlus), \"Basic Plus\", AutoModeDescriptions[AutoModePlus]},\n\t{string(AutoModeBasic), \"Basic\", AutoModeDescriptions[AutoModeBasic]},\n\t{string(AutoModeNone), \"None\", AutoModeDescriptions[AutoModeNone]},\n\t{string(AutoModeCustom), \"Custom\", AutoModeDescriptions[AutoModeCustom]},\n}\n\nvar AutoModeLabels = map[AutoModeType]string{}\n\n// populated in init()\nvar AutoModeChoices []string\n\ntype PlanConfig struct {\n\tAutoMode AutoModeType `json:\"autoMode\"`\n\t// QuietMode bool         `json:\"quietMode\"`\n\n\tEditor             string   `json:\"editor\"`\n\tEditorCommand      string   `json:\"editorCommand\"`\n\tEditorArgs         []string `json:\"editorArgs\"`\n\tEditorOpenManually bool     `json:\"editorOpenManually\"`\n\n\tAutoContinue bool `json:\"autoContinue\"`\n\tAutoBuild    bool `json:\"autoBuild\"`\n\n\tAutoUpdateContext bool `json:\"autoUpdateContext\"`\n\tAutoLoadContext   bool `json:\"autoContext\"`\n\tSmartContext      bool `json:\"smartContext\"`\n\n\t// AutoApproveContext bool `json:\"autoApproveContext\"`\n\t// QuietContext       bool `json:\"quietContext\"`\n\n\t// AutoApprovePlan bool `json:\"autoApprovePlan\"`\n\n\t// QuietCoding    bool `json:\"quietCoding\"`\n\t// ParallelCoding bool `json:\"parallelCoding\"`\n\n\tAutoApply  bool `json:\"autoApply\"`\n\tAutoCommit bool `json:\"autoCommit\"`\n\tSkipCommit bool `json:\"skipCommit\"`\n\n\tCanExec        bool `json:\"canExec\"`\n\tAutoExec       bool `json:\"autoExec\"`\n\tAutoDebug      bool `json:\"autoDebug\"`\n\tAutoDebugTries int  `json:\"autoDebugTries\"`\n\n\tAutoRevertOnRewind bool `json:\"autoRevertOnRewind\"`\n\n\tSkipChangesMenu bool `json:\"skipChangesMenu\"`\n\n\t// ReplMode    bool     `json:\"replMode\"`\n\t// DefaultRepl ReplType `json:\"defaultRepl\"`\n\n\t// PlainTextMode     bool `json:\"plainTextMode\"`\n\t// PlainTextCommands bool `json:\"plainTextCommands\"`\n\t// PlainTextStream   bool `json:\"plainTextStream\"`\n\n}\n\nvar DefaultPlanConfig = PlanConfig{}\n\nfunc (p *PlanConfig) Scan(src interface{}) error {\n\tif src == nil {\n\t\t*p = DefaultPlanConfig\n\t\treturn nil\n\t}\n\tswitch s := src.(type) {\n\tcase []byte:\n\t\tif len(s) == 0 {\n\t\t\t*p = DefaultPlanConfig\n\t\t\treturn nil\n\t\t}\n\t\treturn json.Unmarshal(s, p)\n\tcase string:\n\t\tif s == \"\" {\n\t\t\t*p = DefaultPlanConfig\n\t\t\treturn nil\n\t\t}\n\t\treturn json.Unmarshal([]byte(s), p)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported data type: %T\", src)\n\t}\n}\n\nfunc (p PlanConfig) Value() (driver.Value, error) {\n\treturn json.Marshal(p)\n}\n\nfunc (p *PlanConfig) SetAutoMode(mode AutoModeType) {\n\tp.AutoMode = mode\n\n\tswitch p.AutoMode {\n\tcase AutoModeFull:\n\t\tp.AutoContinue = true\n\t\tp.AutoBuild = true\n\t\tp.AutoUpdateContext = true\n\t\tp.AutoLoadContext = true\n\t\tp.SmartContext = true\n\t\tp.AutoApply = true\n\t\tp.AutoCommit = true\n\t\tp.CanExec = true\n\t\tp.AutoExec = true\n\t\tp.AutoDebug = true\n\t\tp.AutoDebugTries = defaultAutoDebugTries\n\t\tp.AutoRevertOnRewind = true\n\t\tp.SkipChangesMenu = false\n\n\tcase AutoModeSemi:\n\t\tp.AutoContinue = true\n\t\tp.AutoBuild = true\n\t\tp.AutoUpdateContext = true\n\t\tp.AutoLoadContext = true\n\t\tp.SmartContext = true\n\t\tp.AutoApply = false\n\t\tp.AutoCommit = true\n\t\tp.CanExec = true\n\t\tp.AutoExec = false\n\t\tp.AutoDebug = false\n\t\tp.AutoRevertOnRewind = true\n\t\tp.SkipChangesMenu = false\n\n\tcase AutoModePlus:\n\t\tp.AutoContinue = true\n\t\tp.AutoBuild = true\n\t\tp.AutoUpdateContext = true\n\t\tp.AutoLoadContext = false\n\t\tp.SmartContext = true\n\t\tp.AutoApply = false\n\t\tp.AutoCommit = true\n\t\tp.CanExec = true\n\t\tp.AutoExec = false\n\t\tp.AutoDebug = false\n\t\tp.AutoRevertOnRewind = true\n\t\tp.SkipChangesMenu = false\n\n\tcase AutoModeBasic:\n\t\tp.AutoContinue = true\n\t\tp.AutoBuild = true\n\t\tp.AutoUpdateContext = false\n\t\tp.AutoLoadContext = false\n\t\tp.SmartContext = false\n\t\tp.AutoApply = false\n\t\tp.AutoCommit = false\n\t\tp.CanExec = false\n\t\tp.AutoExec = false\n\t\tp.AutoDebug = false\n\t\tp.AutoRevertOnRewind = true\n\t\tp.SkipChangesMenu = false\n\n\tcase AutoModeNone:\n\t\tp.AutoContinue = false\n\t\tp.AutoBuild = false\n\t\tp.AutoUpdateContext = false\n\t\tp.AutoLoadContext = false\n\t\tp.SmartContext = false\n\t\tp.AutoApply = false\n\t\tp.AutoCommit = false\n\t\tp.CanExec = false\n\t\tp.AutoExec = false\n\t\tp.AutoDebug = false\n\t\tp.AutoRevertOnRewind = true\n\t\tp.SkipChangesMenu = false\n\t}\n}\n\ntype ConfigSetting struct {\n\tName            string\n\tDesc            string\n\tVisible         func(p *PlanConfig) bool\n\tBoolSetter      func(p *PlanConfig, enabled bool)\n\tIntSetter       func(p *PlanConfig, value int)\n\tStringSetter    func(p *PlanConfig, value string)\n\tEditorSetter    func(p *PlanConfig, label, command string, args []string)\n\tGetter          func(p *PlanConfig) string\n\tChoices         *[]string\n\tHasCustomChoice bool\n\tChoiceToKey     func(choice string) string\n\tSortKey         string\n\tKeyToLabel      func(key string) string\n}\n\nvar ConfigSettingsByKey = map[string]ConfigSetting{\n\n\t\"automode\": {\n\t\tName: \"auto-mode\",\n\t\tDesc: \"Use preset config based on desired level of autonomy\",\n\t\tStringSetter: func(p *PlanConfig, value string) {\n\t\t\tp.SetAutoMode(AutoModeType(value))\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn string(p.AutoMode)\n\t\t},\n\t\tChoices: &AutoModeChoices,\n\t\tChoiceToKey: func(choice string) string {\n\t\t\tfor _, option := range AutoModeOptions {\n\t\t\t\tif strings.HasPrefix(choice, option[1]) {\n\t\t\t\t\treturn option[0]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn \"\"\n\t\t},\n\t\tKeyToLabel: func(key string) string {\n\t\t\tfor _, option := range AutoModeOptions {\n\t\t\t\tif option[0] == key {\n\t\t\t\t\treturn option[1]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn \"\"\n\t\t},\n\t\tSortKey: \"0\",\n\t},\n\n\t\"editor\": {\n\t\tName: \"editor\",\n\t\tDesc: \"Preferred editor\",\n\t\tEditorSetter: func(p *PlanConfig, label, command string, args []string) {\n\t\t\tp.Editor = label\n\t\t\tp.EditorCommand = command\n\t\t\tp.EditorArgs = args\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn p.Editor\n\t\t},\n\t},\n\n\t\"autocontinue\": {\n\t\tName: \"auto-continue\",\n\t\tDesc: \"Continue iterating until plan is complete\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoContinue {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\n\t\t\tp.AutoContinue = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoContinue)\n\t\t},\n\t},\n\n\t\"autobuild\": {\n\t\tName: \"auto-build\",\n\t\tDesc: \"Automatically generate pending file edits\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoBuild {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\n\t\t\tp.AutoBuild = enabled\n\n\t\t\tif !enabled {\n\t\t\t\tp.AutoApply = false\n\t\t\t}\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoBuild)\n\t\t},\n\t},\n\t\"autoupdatecontext\": {\n\t\tName: \"auto-update-context\",\n\t\tDesc: \"Automatically update context after changes\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoUpdateContext {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoUpdateContext = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoUpdateContext)\n\t\t},\n\t},\n\t\"autoloadcontext\": {\n\t\tName: \"auto-load-context\",\n\t\tDesc: \"Find and load context automatically\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoLoadContext {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoLoadContext = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoLoadContext)\n\t\t},\n\t},\n\t\"smartcontext\": {\n\t\tName: \"smart-context\",\n\t\tDesc: \"Load only necessary context for each task in the plan\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.SmartContext {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.SmartContext = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.SmartContext)\n\t\t},\n\t},\n\t\"autocommit\": {\n\t\tName: \"auto-commit\",\n\t\tDesc: \"Automatically commit changes to git after apply\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoCommit {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoCommit = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoCommit)\n\t\t},\n\t},\n\t\"skipcommit\": {\n\t\tName: \"skip-commit\",\n\t\tDesc: \"Skip committing changes to git after apply\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tp.SkipCommit = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.SkipCommit)\n\t\t},\n\t},\n\t\"autoapply\": {\n\t\tName: \"auto-apply\",\n\t\tDesc: \"Automatically apply changes after plan finishes\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoApply {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoApply = enabled\n\n\t\t\tif enabled {\n\t\t\t\tp.AutoBuild = true\n\t\t\t}\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoApply)\n\t\t},\n\t},\n\t\"canexec\": {\n\t\tName: \"can-exec\",\n\t\tDesc: \"Allow execution of commands\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.CanExec {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.CanExec = enabled\n\n\t\t\tif !enabled {\n\t\t\t\tp.AutoExec = false\n\t\t\t\tp.AutoDebug = false\n\t\t\t}\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.CanExec)\n\t\t},\n\t},\n\t\"autoexec\": {\n\t\tName: \"auto-exec\",\n\t\tDesc: \"Automatically execute commands after plan is applied\",\n\t\tVisible: func(p *PlanConfig) bool {\n\t\t\treturn p.CanExec\n\t\t},\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoExec {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoExec = enabled\n\n\t\t\tif enabled {\n\t\t\t\tp.CanExec = true\n\t\t\t} else {\n\t\t\t\tp.AutoDebug = false\n\t\t\t}\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoExec)\n\t\t},\n\t},\n\t\"autodebug\": {\n\t\tName: \"auto-debug\",\n\t\tDesc: \"Automatically debug failed commands\",\n\t\tVisible: func(p *PlanConfig) bool {\n\t\t\treturn p.AutoExec\n\t\t},\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoDebug {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoDebug = enabled\n\n\t\t\tif enabled {\n\t\t\t\tp.CanExec = true\n\t\t\t\tp.AutoExec = true\n\n\t\t\t\tif p.AutoDebugTries == 0 {\n\t\t\t\t\tp.AutoDebugTries = defaultAutoDebugTries\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoDebug)\n\t\t},\n\t},\n\t\"autodebugtries\": {\n\t\tName: \"auto-debug-tries\",\n\t\tDesc: \"Number of auto-debug attempts\",\n\t\tVisible: func(p *PlanConfig) bool {\n\t\t\treturn p.AutoDebug\n\t\t},\n\t\tIntSetter: func(p *PlanConfig, value int) {\n\t\t\tif value != p.AutoDebugTries {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoDebugTries = value\n\n\t\t\tif p.AutoDebugTries == 0 {\n\t\t\t\tp.AutoDebug = false\n\t\t\t} else {\n\t\t\t\tp.CanExec = true\n\t\t\t\tp.AutoExec = true\n\t\t\t\tp.AutoDebug = true\n\t\t\t}\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%d\", p.AutoDebugTries)\n\t\t},\n\t},\n\t\"autorevert\": {\n\t\tName: \"auto-revert\",\n\t\tDesc: \"Automatically update project files when rewinding plan\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tif enabled != p.AutoRevertOnRewind {\n\t\t\t\tp.AutoMode = AutoModeCustom\n\t\t\t}\n\t\t\tp.AutoRevertOnRewind = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.AutoRevertOnRewind)\n\t\t},\n\t},\n\t\"skipchangesmenu\": {\n\t\tName: \"skip-changes-menu\",\n\t\tDesc: \"Skip interactive menu when response finishes and changes are pending\",\n\t\tBoolSetter: func(p *PlanConfig, enabled bool) {\n\t\t\tp.SkipChangesMenu = enabled\n\t\t},\n\t\tGetter: func(p *PlanConfig) string {\n\t\t\treturn fmt.Sprintf(\"%t\", p.SkipChangesMenu)\n\t\t},\n\t},\n}\n\nfunc init() {\n\tDefaultPlanConfig.SetAutoMode(AutoModeSemi)\n\n\tfor _, choice := range AutoModeOptions {\n\t\tAutoModeChoices = append(AutoModeChoices, fmt.Sprintf(\"%s → %s\", choice[1], choice[2]))\n\t\tAutoModeLabels[AutoModeType(choice[0])] = choice[1]\n\t}\n}\n"
  },
  {
    "path": "app/shared/plan_model_settings.go",
    "content": "package shared\n\nimport (\n\t\"database/sql/driver\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n)\n\ntype PlanSettings struct {\n\tModelPackName               string                              `json:\"modelPackName\"`\n\tModelPack                   *ModelPack                          `json:\"modelPack\"`\n\tCustomModelPacks            []*ModelPack                        `json:\"customModelPacks\"`\n\tCustomModels                []*CustomModel                      `json:\"customModels\"`\n\tCustomModelsById            map[ModelId]*CustomModel            `json:\"customModelsById\"`\n\tCustomProviders             []*CustomProvider                   `json:\"customProviders\"`\n\tUsesCustomProviderByModelId map[ModelId][]BaseModelUsesProvider `json:\"usesCustomProviderByModelId\"`\n\tIsCloud                     bool                                `json:\"isCloud\"`\n\tConfigured                  bool                                `json:\"configured\"`\n\tUpdatedAt                   time.Time                           `json:\"updatedAt\"`\n}\n\nfunc (p *PlanSettings) Configure(customModelPacks []*ModelPack, customModels []*CustomModel, customProviders []*CustomProvider, isCloud bool) {\n\tp.CustomModelPacks = customModelPacks\n\tp.CustomModels = customModels\n\tp.CustomProviders = customProviders\n\tp.IsCloud = isCloud\n\tp.Configured = true\n\tp.CustomModelsById = map[ModelId]*CustomModel{}\n\tp.UsesCustomProviderByModelId = map[ModelId][]BaseModelUsesProvider{}\n\n\tfor _, customModel := range customModels {\n\t\tp.CustomModelsById[customModel.ModelId] = customModel\n\t\tp.UsesCustomProviderByModelId[customModel.ModelId] = customModel.Providers\n\t}\n\n}\n\nfunc (p PlanSettings) GetModelPack() *ModelPack {\n\tif !p.Configured {\n\t\tpanic(\"PlanSettings not configured\")\n\t}\n\n\tcustomModelPacks := p.CustomModelPacks\n\tisCloud := p.IsCloud\n\n\tfillDefault := true // seems best to make this the default behavior, but keeping the switch just in case\n\n\tif p.ModelPack != nil {\n\t\treturn p.ModelPack\n\t}\n\n\tfor _, builtInModelPack := range BuiltInModelPacks {\n\t\tif isCloud && builtInModelPack.LocalProvider != \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif builtInModelPack.Name == p.ModelPackName {\n\t\t\treturn builtInModelPack\n\t\t}\n\t}\n\n\tfor _, customModelPack := range customModelPacks {\n\t\tif customModelPack.Name == p.ModelPackName {\n\t\t\treturn customModelPack\n\t\t}\n\t}\n\n\tif fillDefault {\n\t\treturn DefaultModelPack\n\t}\n\n\treturn nil\n}\n\nfunc (p *PlanSettings) SetModelPackByName(modelPackName string) {\n\tp.ModelPackName = modelPackName\n\tp.ModelPack = nil\n}\n\nfunc (p *PlanSettings) SetCustomModelPack(modelPack *ModelPack) {\n\tp.ModelPackName = \"\"\n\tp.ModelPack = modelPack\n}\n\nfunc (p *PlanSettings) Scan(src interface{}) error {\n\tif src == nil {\n\t\treturn nil\n\t}\n\tswitch s := src.(type) {\n\tcase []byte:\n\t\treturn json.Unmarshal(s, p)\n\tcase string:\n\t\treturn json.Unmarshal([]byte(s), p)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported data type: %T\", src)\n\t}\n}\n\nfunc (p PlanSettings) Value() (driver.Value, error) {\n\treturn json.Marshal(p)\n}\n\nfunc (ps PlanSettings) GetPlannerMaxTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tplanner := modelPack.Planner\n\tfallback := planner.GetFinalLargeContextFallback()\n\tbaseConfig := fallback.GetSharedBaseConfig(&ps)\n\treturn baseConfig.MaxTokens\n}\n\nfunc (ps PlanSettings) GetPlannerMaxReservedOutputTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tplanner := modelPack.Planner\n\treturn planner.GetFinalLargeContextFallback().GetReservedOutputTokens(ps.CustomModelsById)\n}\n\nfunc (ps PlanSettings) GetArchitectMaxTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tarchitect := modelPack.GetArchitect()\n\tfallback := architect.GetFinalLargeContextFallback()\n\treturn fallback.GetSharedBaseConfig(&ps).MaxTokens\n}\n\nfunc (ps PlanSettings) GetArchitectMaxReservedOutputTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tarchitect := modelPack.GetArchitect()\n\tfallback := architect.GetFinalLargeContextFallback()\n\treturn fallback.GetReservedOutputTokens(ps.CustomModelsById)\n}\n\nfunc (ps PlanSettings) GetCoderMaxTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tcoder := modelPack.GetCoder()\n\tfallback := coder.GetFinalLargeContextFallback()\n\treturn fallback.GetSharedBaseConfig(&ps).MaxTokens\n}\n\nfunc (ps PlanSettings) GetCoderMaxReservedOutputTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tcoder := modelPack.GetCoder()\n\tfallback := coder.GetFinalLargeContextFallback()\n\treturn fallback.GetReservedOutputTokens(ps.CustomModelsById)\n}\n\nfunc (ps PlanSettings) GetWholeFileBuilderMaxTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tbuilder := modelPack.GetWholeFileBuilder()\n\tfallback := builder.GetFinalLargeContextFallback()\n\treturn fallback.GetSharedBaseConfig(&ps).MaxTokens\n}\n\nfunc (ps PlanSettings) GetWholeFileBuilderMaxReservedOutputTokens() int {\n\tmodelPack := ps.GetModelPack()\n\tbuilder := modelPack.GetWholeFileBuilder()\n\tfallback := builder.GetFinalLargeOutputFallback()\n\treturn fallback.GetReservedOutputTokens(ps.CustomModelsById)\n}\n\nfunc (ps PlanSettings) GetPlannerMaxConvoTokens() int {\n\tmodelPack := ps.GetModelPack()\n\n\t// for max convo tokens, we use the planner's default max convo tokens, *not* the fallback, so that we don't end up switching to the fallback just based on the conversation length\n\tplanner := modelPack.Planner\n\tif planner.MaxConvoTokens != 0 {\n\t\treturn planner.MaxConvoTokens\n\t}\n\n\treturn planner.GetSharedBaseConfig(&ps).DefaultMaxConvoTokens\n}\n\nfunc (ps PlanSettings) GetPlannerEffectiveMaxTokens() int {\n\tmaxPlannerTokens := ps.GetPlannerMaxTokens()\n\tmaxReservedOutputTokens := ps.GetPlannerMaxReservedOutputTokens()\n\n\treturn maxPlannerTokens - maxReservedOutputTokens\n}\n\nfunc (ps PlanSettings) GetArchitectEffectiveMaxTokens() int {\n\tmaxArchitectTokens := ps.GetArchitectMaxTokens()\n\tmaxReservedOutputTokens := ps.GetArchitectMaxReservedOutputTokens()\n\n\treturn maxArchitectTokens - maxReservedOutputTokens\n}\n\nfunc (ps PlanSettings) GetCoderEffectiveMaxTokens() int {\n\tmaxCoderTokens := ps.GetCoderMaxTokens()\n\tmaxReservedOutputTokens := ps.GetCoderMaxReservedOutputTokens()\n\n\treturn maxCoderTokens - maxReservedOutputTokens\n}\n\nfunc (ps PlanSettings) GetWholeFileBuilderEffectiveMaxTokens() int {\n\tmaxWholeFileBuilderTokens := ps.GetWholeFileBuilderMaxTokens()\n\tmaxReservedOutputTokens := ps.GetWholeFileBuilderMaxReservedOutputTokens()\n\n\treturn maxWholeFileBuilderTokens - maxReservedOutputTokens\n}\n\nfunc (ps PlanSettings) GetModelProviderOptions() ModelProviderOptions {\n\topts := ModelProviderOptions{}\n\n\tms := ps.GetModelPack()\n\tif ms == nil {\n\t\tms = DefaultModelPack\n\t}\n\n\topts = opts.Condense(\n\t\tms.Planner.GetModelProviderOptions(&ps),\n\t\tms.Builder.GetModelProviderOptions(&ps),\n\t\tms.PlanSummary.GetModelProviderOptions(&ps),\n\t\tms.Namer.GetModelProviderOptions(&ps),\n\t\tms.CommitMsg.GetModelProviderOptions(&ps),\n\t\tms.ExecStatus.GetModelProviderOptions(&ps),\n\t\t// optional roles\n\t\tgetOptionalModelProviderOptions(&ps, ms.WholeFileBuilder),\n\t\tgetOptionalModelProviderOptions(&ps, ms.Architect),\n\t\tgetOptionalModelProviderOptions(&ps, ms.Coder),\n\t)\n\n\treturn opts\n}\n\nfunc (ps *PlanSettings) Equals(other *PlanSettings) bool {\n\treturn ps.GetModelPack().Equals(other.GetModelPack())\n}\n\nfunc (ps PlanSettings) ForCompare() PlanSettings {\n\tps.UpdatedAt = time.Time{}\n\tps.CustomModelPacks = nil\n\tps.CustomModels = nil\n\tps.CustomProviders = nil\n\tps.IsCloud = false\n\tps.Configured = false\n\treturn ps\n}\n\nfunc (ps PlanSettings) DeepCopy() (*PlanSettings, error) {\n\tbytes, err := json.Marshal(ps)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshalling plan settings: %v\", err)\n\t}\n\tvar copy PlanSettings\n\terr = json.Unmarshal(bytes, &copy)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling plan settings: %v\", err)\n\t}\n\treturn &copy, nil\n}\n\nfunc getOptionalModelProviderOptions(settings *PlanSettings, cfg *ModelRoleConfig) ModelProviderOptions {\n\tif cfg == nil {\n\t\treturn ModelProviderOptions{}\n\t}\n\treturn cfg.GetModelProviderOptions(settings)\n}\n"
  },
  {
    "path": "app/shared/plan_result.go",
    "content": "package shared\n\nimport (\n\t\"time\"\n)\n\nfunc (rep *Replacement) IsPending() bool {\n\treturn !rep.Failed && rep.RejectedAt == nil\n}\n\nfunc (rep *Replacement) SetRejected(t time.Time) {\n\trep.RejectedAt = &t\n}\n\nfunc (res *PlanFileResult) NumPendingReplacements() int {\n\tnumPending := 0\n\tfor _, rep := range res.Replacements {\n\t\tif rep.IsPending() {\n\t\t\tnumPending++\n\t\t}\n\t}\n\treturn numPending\n}\n\nfunc (res *PlanFileResult) IsPending() bool {\n\treturn res.AppliedAt == nil && res.RejectedAt == nil && (res.Content != \"\" || res.NumPendingReplacements() > 0 || res.RemovedFile)\n}\n\nfunc (p PlanFileResultsByPath) SetApplied(t time.Time) {\n\tfor _, planResults := range p {\n\t\tfor _, planResult := range planResults {\n\t\t\tif !planResult.IsPending() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tplanResult.AppliedAt = &t\n\t\t}\n\t}\n}\n\nfunc (p PlanFileResultsByPath) SetRejected(t time.Time) int {\n\tnumRejected := 0\n\tfor _, planResults := range p {\n\t\tfor _, planResult := range planResults {\n\t\t\tif !planResult.IsPending() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tplanResult.RejectedAt = &t\n\t\t\tnumRejected++\n\n\t\t\tfor _, rep := range planResult.Replacements {\n\t\t\t\trep.SetRejected(t)\n\t\t\t}\n\t\t}\n\t}\n\treturn numRejected\n}\n\nfunc (p PlanFileResultsByPath) NumPending() int {\n\tnumPending := 0\n\tfor _, planResults := range p {\n\t\tfor _, planResult := range planResults {\n\t\t\tif planResult.IsPending() {\n\t\t\t\tnumPending++\n\t\t\t}\n\t\t}\n\t}\n\treturn numPending\n}\n\nfunc (p PlanFileResultsByPath) ConflictedPaths(filesByPath map[string]string) map[string]bool {\n\tconflictedPaths := map[string]bool{}\n\n\tfor path, body := range filesByPath {\n\t\tplanRes := p[path]\n\n\t\t// log.Println(\"Checking for conflicts in path:\", path)\n\t\t// log.Println(\"Body:\")\n\t\t// log.Println(body)\n\n\t\tif planRes == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tupdated := body\n\n\t\tnoConflicts := true\n\t\tfor _, res := range planRes {\n\n\t\t\t// log.Println(\"res:\", res.Id)\n\t\t\tif len(res.Replacements) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmaybeWithLineNums := updated\n\t\t\tif res.ReplaceWithLineNums {\n\t\t\t\tmaybeWithLineNums = string(AddLineNums(updated))\n\t\t\t}\n\n\t\t\tvar succeeded bool\n\t\t\tupdated, succeeded = ApplyReplacements(maybeWithLineNums, res.Replacements, false)\n\n\t\t\tupdated = RemoveLineNums(LineNumberedTextType(updated))\n\n\t\t\t// log.Println(\"updated:\", updated)\n\t\t\t// log.Println(\"succeeded:\", succeeded)\n\n\t\t\tif !succeeded {\n\t\t\t\tnoConflicts = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// log.Println(\"No conflicts:\", noConflicts)\n\n\t\tif !noConflicts {\n\t\t\tconflictedPaths[path] = true\n\t\t}\n\t}\n\n\treturn conflictedPaths\n}\n\nfunc (r PlanResult) NumPendingForPath(path string) int {\n\tres := 0\n\tresults := r.FileResultsByPath[path]\n\tfor _, result := range results {\n\t\tif result.IsPending() {\n\t\t\tres += result.NumPendingReplacements()\n\t\t}\n\t}\n\treturn res\n}\n\nfunc (desc *ConvoMessageDescription) NumBuildsPendingByPath() map[string]int {\n\tres := map[string]int{}\n\tif (!desc.DidBuild && len(desc.Operations) > 0) || len(desc.BuildPathsInvalidated) > 0 {\n\t\tfor _, op := range desc.Operations {\n\t\t\tres[op.Path]++\n\t\t}\n\t}\n\treturn res\n}\n\nfunc (desc *ConvoMessageDescription) HasPendingBuilds() bool {\n\treturn len(desc.NumBuildsPendingByPath()) > 0\n}\n\nfunc NumBuildsPendingByPath(planDescs []*ConvoMessageDescription) map[string]int {\n\tres := map[string]int{}\n\tfor _, desc := range planDescs {\n\t\tfor file, num := range desc.NumBuildsPendingByPath() {\n\t\t\tres[file] += num\n\t\t}\n\t}\n\treturn res\n}\n\nfunc HasPendingBuilds(planDescs []*ConvoMessageDescription) bool {\n\treturn len(NumBuildsPendingByPath(planDescs)) > 0\n}\n\nfunc (c *CurrentPlanState) NumBuildsPendingByPath() map[string]int {\n\treturn NumBuildsPendingByPath(c.ConvoMessageDescriptions)\n}\n\nfunc (c *CurrentPlanState) HasPendingBuilds() bool {\n\treturn len(c.NumBuildsPendingByPath()) > 0\n}\n"
  },
  {
    "path": "app/shared/plan_result_exec_history.go",
    "content": "package shared\n\nfunc (state *CurrentPlanState) ExecHistory() string {\n\texecHistory := \"\"\n\n\tif state.PlanResult == nil {\n\t\treturn execHistory\n\t}\n\n\tfor _, result := range state.PlanResult.Results {\n\t\tif result.Path == \"_apply.sh\" && result.AppliedAt != nil {\n\t\t\texecHistory += \"Previously executed _apply.sh:\\n\\n```\\n\" + result.Content + \"\\n```\\n\\n\"\n\t\t}\n\t}\n\n\treturn execHistory\n}\n"
  },
  {
    "path": "app/shared/plan_result_pending_summary.go",
    "content": "package shared\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"sort\"\n\t\"strings\"\n)\n\nfunc (state *CurrentPlanState) PendingChangesSummaryForBuild() string {\n\treturn state.pendingChangesSummary(false, \"\")\n}\n\nfunc (state *CurrentPlanState) PendingChangesSummaryForApply(commitSummary string) string {\n\treturn state.pendingChangesSummary(true, commitSummary)\n}\n\nfunc (state *CurrentPlanState) pendingChangesSummary(forApply bool, commitSummary string) string {\n\tvar msgs []string\n\n\tdescByConvoMessageId := make(map[string]*ConvoMessageDescription)\n\n\tfor _, desc := range state.ConvoMessageDescriptions {\n\t\tif desc.ConvoMessageId == \"\" {\n\t\t\tlog.Println(\"Warning: ConvoMessageId is empty for description:\", desc)\n\t\t\tcontinue\n\t\t}\n\n\t\tdescByConvoMessageId[desc.ConvoMessageId] = desc\n\t}\n\n\ttype changeset struct {\n\t\tdescsSet map[string]bool\n\t\tdescs    []*ConvoMessageDescription\n\t\tresults  []*PlanFileResult\n\t}\n\tbyDescs := map[string]*changeset{}\n\n\tfor _, result := range state.PlanResult.Results {\n\t\t// log.Println(\"result:\")\n\t\t// spew.Dump(result)\n\n\t\tconvoIds := map[string]bool{}\n\t\tif descByConvoMessageId[result.ConvoMessageId] != nil {\n\t\t\tconvoIds[result.ConvoMessageId] = true\n\t\t}\n\t\tvar uniqueConvoIds []string\n\t\tfor convoId := range convoIds {\n\t\t\tuniqueConvoIds = append(uniqueConvoIds, convoId)\n\t\t}\n\n\t\tcomposite := strings.Join(uniqueConvoIds, \"|\")\n\t\tif _, ok := byDescs[composite]; !ok {\n\t\t\tbyDescs[composite] = &changeset{\n\t\t\t\tdescsSet: make(map[string]bool),\n\t\t\t}\n\t\t}\n\n\t\tch := byDescs[composite]\n\t\tch.results = append(byDescs[composite].results, result)\n\n\t\t// log.Println(\"uniqueConvoIds:\", uniqueConvoIds)\n\n\t\tfor _, convoMessageId := range uniqueConvoIds {\n\t\t\tif desc, ok := descByConvoMessageId[convoMessageId]; ok {\n\t\t\t\tif !ch.descsSet[convoMessageId] && (!(desc.DidBuild && !forApply) || len(desc.BuildPathsInvalidated) > 0) {\n\t\t\t\t\tch.descs = append(ch.descs, desc)\n\t\t\t\t\tch.descsSet[convoMessageId] = true\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Println(\"Warning: no description for convo message id:\", convoMessageId)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar sortedChangesets []*changeset\n\tfor _, ch := range byDescs {\n\t\tsortedChangesets = append(sortedChangesets, ch)\n\t}\n\n\tsort.Slice(sortedChangesets, func(i, j int) bool {\n\t\t// put changesets with no descriptions last, otherwise sort by date\n\t\tif len(sortedChangesets[i].descs) == 0 {\n\t\t\treturn false\n\t\t}\n\t\tif len(sortedChangesets[j].descs) == 0 {\n\t\t\treturn true\n\t\t}\n\t\treturn sortedChangesets[i].descs[0].CreatedAt.Before(sortedChangesets[j].descs[0].CreatedAt)\n\t})\n\n\tisRebuild := true\n\trebuildPathsSet := make(map[string]bool)\n\n\tif forApply {\n\t\tmsgs = append(msgs, \"🤖 Plandex → \"+commitSummary)\n\t} else {\n\t\tfor _, ch := range sortedChangesets {\n\t\t\tallRebuild := true\n\t\t\tfor _, desc := range ch.descs {\n\t\t\t\tif len(desc.BuildPathsInvalidated) == 0 {\n\t\t\t\t\tallRebuild = false\n\t\t\t\t\tbreak\n\t\t\t\t} else {\n\t\t\t\t\tfor path := range desc.BuildPathsInvalidated {\n\t\t\t\t\t\trebuildPathsSet[path] = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !allRebuild {\n\t\t\t\tisRebuild = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif isRebuild {\n\t\t\tmsgs = append(msgs, \"🏗️  Rebuild paths invalidated by context update\")\n\t\t\tfor path := range rebuildPathsSet {\n\t\t\t\tmsgs = append(msgs, fmt.Sprintf(\"  • rebuild → %s\", path))\n\t\t\t}\n\t\t\treturn strings.Join(msgs, \"\\n\")\n\t\t}\n\n\t\tmsgs = append(msgs, \"🏗️  Build pending changes\")\n\t\tcurrentFiles := state.CurrentPlanFiles.Files\n\t\tvar sortedFiles []string\n\t\tfor path := range currentFiles {\n\t\t\tsortedFiles = append(sortedFiles, path)\n\t\t}\n\t\tsort.Strings(sortedFiles)\n\t\tfor _, path := range sortedFiles {\n\t\t\tmsgs = append(msgs, fmt.Sprintf(\" • 📄 %s\", path))\n\t\t}\n\t\treturn strings.Join(msgs, \"\\n\")\n\t}\n\n\tfor _, ch := range sortedChangesets {\n\t\tvar descMsgs []string\n\n\t\tif len(ch.descs) <= 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, desc := range ch.descs {\n\t\t\tdescMsgs = append(descMsgs, fmt.Sprintf(\"  ✏️  %s\", desc.CommitMsg))\n\t\t}\n\n\t\tmsgs = append(msgs, descMsgs...)\n\n\t\t// for an apply commit message, we don't need to list file updates\n\t\tif forApply {\n\t\t\tcontinue\n\t\t}\n\n\t\tpendingNewFilesSet := make(map[string]bool)\n\t\tpendingReplacementPathsSet := make(map[string]bool)\n\t\tpendingReplacementsByPath := make(map[string][]*Replacement)\n\n\t\tfor _, result := range ch.results {\n\n\t\t\tif result.IsPending() {\n\t\t\t\tif len(result.Replacements) == 0 && result.Content != \"\" {\n\t\t\t\t\tpendingNewFilesSet[result.Path] = true\n\t\t\t\t} else {\n\t\t\t\t\tpendingReplacementPathsSet[result.Path] = true\n\t\t\t\t\tpendingReplacementsByPath[result.Path] = append(pendingReplacementsByPath[result.Path], result.Replacements...)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(pendingNewFilesSet) == 0 && len(pendingReplacementPathsSet) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar pendingNewFiles []string\n\t\tvar pendingReplacementPaths []string\n\n\t\tfor path := range pendingNewFilesSet {\n\t\t\tpendingNewFiles = append(pendingNewFiles, path)\n\t\t}\n\n\t\tfor path := range pendingReplacementPathsSet {\n\t\t\tpendingReplacementPaths = append(pendingReplacementPaths, path)\n\t\t}\n\n\t\tsort.Slice(pendingReplacementPaths, func(i, j int) bool {\n\t\t\treturn pendingReplacementPaths[i] < pendingReplacementPaths[j]\n\t\t})\n\n\t\tsort.Slice(pendingNewFiles, func(i, j int) bool {\n\t\t\treturn pendingNewFiles[i] < pendingNewFiles[j]\n\t\t})\n\n\t\tif len(pendingNewFiles) > 0 {\n\t\t\tfor _, path := range pendingNewFiles {\n\t\t\t\tmsgs = append(msgs, fmt.Sprintf(\"    • new file → %s\", path))\n\t\t\t}\n\t\t}\n\n\t\tif len(pendingReplacementPaths) > 0 {\n\t\t\tfor _, path := range pendingReplacementPaths {\n\t\t\t\tmsgs = append(msgs, fmt.Sprintf(\"    • edit → %s\", path))\n\t\t\t}\n\n\t\t}\n\n\t}\n\treturn strings.Join(msgs, \"\\n\")\n}\n"
  },
  {
    "path": "app/shared/plan_result_replacements.go",
    "content": "package shared\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/davecgh/go-spew/spew\"\n)\n\nfunc ApplyReplacements(content string, replacements []*Replacement, setFailed bool) (string, bool) {\n\treturn applyReplacements(content, replacements, setFailed, false)\n}\n\nfunc ApplyReplacementsVerbose(content string, replacements []*Replacement, setFailed bool) (string, bool) {\n\treturn applyReplacements(content, replacements, setFailed, true)\n}\n\nfunc applyReplacements(content string, replacements []*Replacement, setFailed, verbose bool) (string, bool) {\n\tapply := func(replacements []*Replacement) (string, int) {\n\t\tif verbose {\n\t\t\tlog.Println(\"Applying replacements\")\n\t\t\tlog.Println(\"original content:\\n\", content)\n\t\t}\n\n\t\tupdated := content\n\n\t\tlastInsertedIdx := 0\n\n\t\tfor i, replacement := range replacements {\n\t\t\tif verbose {\n\t\t\t\tlog.Println(\"replacement.Old:\\n\", replacement.Old)\n\t\t\t\tlog.Println(\"updated:\\n\", updated)\n\t\t\t\tlog.Println(\"lastInsertedIdx:\", lastInsertedIdx)\n\t\t\t}\n\n\t\t\tpre := updated[:lastInsertedIdx]\n\t\t\tsub := updated[lastInsertedIdx:]\n\n\t\t\tvar originalIdx int\n\n\t\t\tif replacement.EntireFile {\n\t\t\t\toriginalIdx = 0\n\t\t\t} else {\n\t\t\t\toriginalIdx = strings.Index(sub, replacement.Old)\n\t\t\t}\n\n\t\t\tif verbose {\n\t\t\t\tlog.Println(\"originalIdx:\", originalIdx)\n\t\t\t}\n\n\t\t\t// only for use with full replacements, which we aren't using now\n\t\t\t// if originalIdx == -1 {\n\t\t\t// \toriginalIdx = getUniqueFuzzyIndex(updated, replacement.Old)\n\t\t\t// }\n\n\t\t\tif originalIdx == -1 {\n\t\t\t\tif setFailed {\n\t\t\t\t\treplacement.Failed = true\n\t\t\t\t}\n\n\t\t\t\tlog.Println(\"Replacement failed at index:\", i)\n\t\t\t\tlog.Println(\"replacement.Old:\")\n\t\t\t\tlog.Println(replacement.Old)\n\n\t\t\t\tlog.Println(\"Updated:\")\n\t\t\t\tlog.Println(updated)\n\n\t\t\t\tif verbose {\n\t\t\t\t\tlog.Println(\"All replacements:\")\n\t\t\t\t\tlog.Println(spew.Sdump(replacements))\n\t\t\t\t}\n\n\t\t\t\treturn updated, i\n\n\t\t\t} else if replacement.EntireFile {\n\t\t\t\tupdated = replacement.New\n\t\t\t\tlastInsertedIdx = 0\n\t\t\t} else {\n\t\t\t\tif verbose {\n\t\t\t\t\tlog.Printf(\"originalIdx: %d, len(replacement.Old): %d\\n\", originalIdx, len(replacement.Old))\n\t\t\t\t\tlog.Println(\"Old: \", replacement.Old)\n\t\t\t\t\tlog.Println(\"New: \", replacement.New)\n\t\t\t\t}\n\t\t\t\treplaced := strings.Replace(sub, replacement.Old, replacement.New, 1)\n\n\t\t\t\tif verbose {\n\t\t\t\t\tlog.Println(\"replaced:\")\n\t\t\t\t\tlog.Println(replaced)\n\t\t\t\t}\n\n\t\t\t\tupdated = pre + replaced\n\n\t\t\t\tif verbose {\n\t\t\t\t\tlog.Printf(\"lastInsertedIdx: %d, originalIdx: %d, len(replacement.New): %d\\n\", lastInsertedIdx, originalIdx, len(replacement.New))\n\t\t\t\t\tlog.Println(\"updated after replacement:\")\n\t\t\t\t\tlog.Println(updated)\n\t\t\t\t}\n\n\t\t\t\tlastInsertedIdx = lastInsertedIdx + originalIdx + len(replacement.New)\n\t\t\t}\n\t\t}\n\n\t\treturn updated, -1\n\t}\n\n\tres, failedAtIndex := apply(replacements)\n\n\treturn res, failedAtIndex == -1\n\n\t// for {\n\n\t// \tif failedAtIndex == 0 {\n\t// \t\treturn res, false\n\t// \t} else if failedAtIndex > 0 {\n\t// \t\t// check if there's overlap between the failed replacement and the previous replacement\n\t// \t\t// if there is, remove the previous one and try again\n\t// \t\tfailed := replacements[failedAtIndex]\n\t// \t\tprev := replacements[failedAtIndex-1]\n\n\t// \t\thasOverlap := failed.StreamedChange.Old.StartLine <= prev.StreamedChange.Old.EndLine\n\n\t// \t\tif hasOverlap {\n\t// \t\t\treplacements = append(replacements[:failedAtIndex-1], replacements[failedAtIndex:]...)\n\n\t// \t\t\tcontinue\n\t// \t\t} else {\n\t// \t\t\treturn res, false\n\t// \t\t}\n\n\t// \t} else {\n\t// \t\treturn res, true\n\t// \t}\n\t// }\n\n}\n\nfunc (planState *CurrentPlanState) GetFiles() (*CurrentPlanFiles, error) {\n\treturn planState.GetFilesBeforeReplacement(\"\")\n}\n\nfunc (planState *CurrentPlanState) GetFilesBeforeReplacement(\n\treplacementId string,\n) (*CurrentPlanFiles, error) {\n\t// log.Println(\"GetFilesBeforeReplacement\")\n\n\tplanRes := planState.PlanResult\n\n\tfiles := make(map[string]string)\n\tshas := make(map[string]string)\n\tupdatedAtByPath := make(map[string]time.Time)\n\tremovedByPath := make(map[string]bool)\n\n\tfor path, planResults := range planRes.FileResultsByPath {\n\t\tupdated := files[path]\n\t\t// log.Println(\"path: \", path)\n\n\t\t// spew.Dump(planResults)\n\t\t// log.Println(\"before PlanResLoop updated:\")\n\t\t// log.Println(updated)\n\n\tPlanResLoop:\n\t\tfor _, planRes := range planResults {\n\n\t\t\t// log.Println(\"planRes: \", planRes.Id)\n\t\t\t// log.Println(spew.Sdump(planRes))\n\n\t\t\tif !planRes.IsPending() {\n\t\t\t\t// log.Println(\"Plan result is not pending -- continuing loop\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif planRes.RemovedFile {\n\t\t\t\tupdated = \"\"\n\t\t\t\tdelete(files, path)\n\t\t\t\tdelete(shas, path)\n\t\t\t\tdelete(updatedAtByPath, path)\n\t\t\t\tremovedByPath[path] = true\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif len(planRes.Replacements) == 0 {\n\t\t\t\tif updated != \"\" {\n\t\t\t\t\tlog.Println(\"plan updates out of order:\", path)\n\t\t\t\t\tlog.Println(\"updated:\")\n\t\t\t\t\tlog.Println(updated)\n\t\t\t\t\tlog.Println(\"planRes.Content:\")\n\t\t\t\t\tlog.Println(planRes.Content)\n\t\t\t\t\treturn nil, fmt.Errorf(\"plan updates out of order: %s\", path)\n\t\t\t\t}\n\n\t\t\t\tupdated = planRes.Content\n\t\t\t\tfiles[path] = updated\n\t\t\t\tupdatedAtByPath[path] = planRes.CreatedAt\n\t\t\t\tdelete(removedByPath, path)\n\n\t\t\t\tcontinue\n\t\t\t} else if updated == \"\" {\n\t\t\t\tcontext := planState.ContextsByPath[path]\n\n\t\t\t\tif context == nil {\n\t\t\t\t\t// spew.Dump(planRes)\n\n\t\t\t\t\treturn nil, fmt.Errorf(\"no context for path: %s\", path)\n\t\t\t\t}\n\n\t\t\t\t// log.Println(\"No updated content -- setting to context body\")\n\n\t\t\t\tupdated = context.Body\n\t\t\t\tshas[path] = context.Sha\n\n\t\t\t\t// log.Println(\"setting updated content to context body\")\n\t\t\t\t// log.Println(updated)\n\t\t\t}\n\n\t\t\treplacements := []*Replacement{}\n\t\t\tfoundTarget := false\n\t\t\tfor _, replacement := range planRes.Replacements {\n\t\t\t\tif replacement.Id == replacementId {\n\t\t\t\t\t// log.Println(\"Found target replacement\")\n\t\t\t\t\tfoundTarget = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treplacements = append(replacements, replacement)\n\t\t\t}\n\n\t\t\tif len(replacements) > 0 {\n\n\t\t\t\tvar allSucceeded bool\n\n\t\t\t\tmaybeWithLineNums := updated\n\t\t\t\tif planRes.ReplaceWithLineNums {\n\t\t\t\t\tmaybeWithLineNums = string(AddLineNums(maybeWithLineNums))\n\t\t\t\t}\n\n\t\t\t\t// log.Println(\"Before replacements. updated:\")\n\t\t\t\t// log.Println(updated)\n\n\t\t\t\tupdated, allSucceeded = ApplyReplacements(maybeWithLineNums, replacements, false)\n\n\t\t\t\tupdated = string(RemoveLineNums(LineNumberedTextType(updated)))\n\n\t\t\t\tif !allSucceeded {\n\t\t\t\t\tlog.Println(\"Failed to apply replacements\")\n\n\t\t\t\t\t// log.Println(\"replacements:\")\n\t\t\t\t\t// log.Println(spew.Sdump(replacements))\n\n\t\t\t\t\t// log.Println(\"updated:\")\n\t\t\t\t\t// log.Println(updated)\n\n\t\t\t\t\treturn nil, fmt.Errorf(\"plan replacement failed - %s\", path)\n\t\t\t\t}\n\n\t\t\t\t// log.Println(\"Updated content: \")\n\t\t\t\t// log.Println(updated)\n\n\t\t\t\tupdatedAtByPath[path] = planRes.CreatedAt\n\t\t\t}\n\n\t\t\tif foundTarget {\n\t\t\t\tbreak PlanResLoop\n\t\t\t}\n\t\t}\n\n\t\t// log.Println(\"Setting updated content for path: \", path)\n\n\t\tfiles[path] = updated\n\t}\n\n\treturn &CurrentPlanFiles{Files: files, UpdatedAtByPath: updatedAtByPath, Removed: removedByPath}, nil\n}\n"
  },
  {
    "path": "app/shared/plan_status.go",
    "content": "package shared\n\ntype PlanStatus string\n\nconst (\n\tPlanStatusDraft       PlanStatus = \"draft\"\n\tPlanStatusReplying    PlanStatus = \"replying\"\n\tPlanStatusDescribing  PlanStatus = \"describing\"\n\tPlanStatusBuilding    PlanStatus = \"building\"\n\tPlanStatusMissingFile PlanStatus = \"missingFile\"\n\tPlanStatusFinished    PlanStatus = \"finished\"\n\tPlanStatusStopped     PlanStatus = \"stopped\"\n\tPlanStatusError       PlanStatus = \"error\"\n)\n"
  },
  {
    "path": "app/shared/rbac.go",
    "content": "package shared\n\nimport (\n\t\"strings\"\n)\n\ntype Permission string\n\nconst (\n\tPermissionDeleteOrg             Permission = \"delete_org\"\n\tPermissionManageEmailDomainAuth Permission = \"manage_email_domain_auth\"\n\tPermissionManageBilling         Permission = \"manage_billing\"\n\tPermissionInviteUser            Permission = \"invite_user\"\n\tPermissionRemoveUser            Permission = \"remove_user\"\n\tPermissionSetUserRole           Permission = \"set_user_role\"\n\tPermissionListOrgRoles          Permission = \"list_org_roles\"\n\tPermissionCreateProject         Permission = \"create_project\"\n\tPermissionRenameAnyProject      Permission = \"rename_any_project\"\n\tPermissionDeleteAnyProject      Permission = \"delete_any_project\"\n\tPermissionCreatePlan            Permission = \"create_plan\"\n\tPermissionManageAnyPlanShares   Permission = \"manage_any_plan_shares\"\n\tPermissionRenameAnyPlan         Permission = \"rename_any_plan\"\n\tPermissionDeleteAnyPlan         Permission = \"delete_any_plan\"\n\tPermissionUpdateAnyPlan         Permission = \"update_any_plan\"\n\tPermissionArchiveAnyPlan        Permission = \"archive_any_plan\"\n)\n\ntype Permissions map[string]bool\n\nfunc (perms Permissions) HasPermission(permission Permission) bool {\n\tfor p := range perms {\n\t\tsplit := strings.Split(p, \"|\")\n\t\tperm := Permission(split[0])\n\n\t\tif perm == permission {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (perms Permissions) HasPermissionForResource(permission Permission, resourceId string) bool {\n\tfor p := range perms {\n\t\tsplit := strings.Split(p, \"|\")\n\t\tperm := Permission(split[0])\n\t\tresId := split[1]\n\n\t\tif perm == permission && resId == resourceId {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "app/shared/req_res.go",
    "content": "package shared\n\nimport (\n\t\"time\"\n\n\t\"github.com/sashabaranov/go-openai\"\n\t\"github.com/shopspring/decimal\"\n)\n\ntype CreateEmailVerificationRequest struct {\n\tEmail         string `json:\"email\"`\n\tUserId        string `json:\"userId\"`\n\tRequireUser   bool   `json:\"requireUser\"`\n\tRequireNoUser bool   `json:\"requireNoUser\"`\n}\n\ntype CreateEmailVerificationResponse struct {\n\tHasAccount  bool `json:\"hasAccount\"`\n\tIsLocalMode bool `json:\"isLocalMode\"`\n}\n\ntype VerifyEmailPinRequest struct {\n\tEmail string `json:\"email\"`\n\tPin   string `json:\"pin\"`\n}\n\ntype SignInRequest struct {\n\tEmail        string `json:\"email\"`\n\tPin          string `json:\"pin\"`\n\tIsSignInCode bool   `json:\"isSignInCode\"`\n}\n\ntype UiSignInToken struct {\n\tPin        string `json:\"pin\"`\n\tRedirectTo string `json:\"redirectTo\"`\n}\n\ntype CreateAccountRequest struct {\n\tEmail    string `json:\"email\"`\n\tPin      string `json:\"pin\"`\n\tUserName string `json:\"userName\"`\n}\n\ntype SessionResponse struct {\n\tUserId      string `json:\"userId\"`\n\tToken       string `json:\"token\"`\n\tEmail       string `json:\"email\"`\n\tUserName    string `json:\"userName\"`\n\tOrgs        []*Org `json:\"orgs\"`\n\tIsLocalMode bool   `json:\"isLocalMode\"`\n}\n\ntype CreateOrgRequest struct {\n\tName               string `json:\"name\"`\n\tAutoAddDomainUsers bool   `json:\"autoAddDomainUsers\"`\n}\n\ntype ConvertTrialRequest struct {\n\tEmail                 string `json:\"email\"`\n\tPin                   string `json:\"pin\"`\n\tUserName              string `json:\"userName\"`\n\tOrgName               string `json:\"orgName\"`\n\tOrgAutoAddDomainUsers bool   `json:\"orgAutoAddDomainUsers\"`\n}\n\ntype CreateOrgResponse struct {\n\tId string `json:\"id\"`\n}\n\ntype InviteRequest struct {\n\tEmail     string `json:\"email\"`\n\tName      string `json:\"name\"`\n\tOrgRoleId string `json:\"orgRoleId\"`\n}\n\ntype CreateProjectRequest struct {\n\tName string `json:\"name\"`\n}\n\ntype CreateProjectResponse struct {\n\tId string `json:\"id\"`\n}\n\ntype SetProjectPlanRequest struct {\n\tPlanId string `json:\"planId\"`\n}\n\ntype RenameProjectRequest struct {\n\tName string `json:\"name\"`\n}\n\ntype CreatePlanRequest struct {\n\tName string `json:\"name\"`\n}\n\ntype CreatePlanResponse struct {\n\tId   string `json:\"id\"`\n\tName string `json:\"name\"`\n}\n\ntype GetCurrentBranchByPlanIdRequest struct {\n\tCurrentBranchByPlanId map[string]string `json:\"currentBranchByPlanId\"`\n}\n\ntype ListPlansRunningResponse struct {\n\tBranches                   []*Branch            `json:\"branches\"`\n\tStreamStartedAtByBranchId  map[string]time.Time `json:\"streamStartedAtByBranchId\"`\n\tStreamFinishedAtByBranchId map[string]time.Time `json:\"streamFinishedAtByBranchId\"`\n\tStreamIdByBranchId         map[string]string    `json:\"streamIdByBranchId\"`\n\tPlansById                  map[string]*Plan     `json:\"plansById\"`\n}\n\ntype BuildMode string\n\nconst (\n\tBuildModeAuto BuildMode = \"auto\"\n\tBuildModeNone BuildMode = \"none\"\n)\n\ntype TellPlanRequest struct {\n\tPrompt         string    `json:\"prompt\"`\n\tBuildMode      BuildMode `json:\"buildMode\"`\n\tConnectStream  bool      `json:\"connectStream\"`\n\tAutoContinue   bool      `json:\"autoContinue\"`\n\tIsUserContinue bool      `json:\"isUserContinue\"`\n\tIsUserDebug    bool      `json:\"isUserDebug\"`\n\tIsApplyDebug   bool      `json:\"isApplyDebug\"`\n\tIsChatOnly     bool      `json:\"isChatOnly\"`\n\tAutoContext    bool      `json:\"autoContext\"`\n\tSmartContext   bool      `json:\"smartContext\"`\n\tExecEnabled    bool      `json:\"execEnabled\"`\n\tOsDetails      string    `json:\"osDetails\"`\n\n\tApiKeys     map[string]string `json:\"apiKeys\"`     // deprecated\n\tOpenAIOrgId string            `json:\"openAIOrgId\"` // deprecated\n\n\tAuthVars map[string]string `json:\"authVars\"`\n\n\tProjectPaths           map[string]bool `json:\"projectPaths\"`\n\tIsImplementationOfChat bool            `json:\"isImplementationOfChat\"`\n\tIsGitRepo              bool            `json:\"isGitRepo\"`\n\tSessionId              string          `json:\"sessionId\"`\n}\n\ntype BuildPlanRequest struct {\n\tConnectStream bool `json:\"connectStream\"`\n\n\tApiKeys     map[string]string `json:\"apiKeys\"`     // deprecated\n\tOpenAIOrgId string            `json:\"openAIOrgId\"` // deprecated\n\n\tAuthVars map[string]string `json:\"authVars\"`\n\n\tProjectPaths map[string]bool `json:\"projectPaths\"`\n\tSessionId    string          `json:\"sessionId\"`\n}\n\nconst NoBuildsErr string = \"No builds\"\n\ntype RespondMissingFileChoice string\n\nconst (\n\tRespondMissingFileChoiceLoad      RespondMissingFileChoice = \"load\"\n\tRespondMissingFileChoiceSkip      RespondMissingFileChoice = \"skip\"\n\tRespondMissingFileChoiceOverwrite RespondMissingFileChoice = \"overwrite\"\n)\n\ntype RespondMissingFileRequest struct {\n\tChoice   RespondMissingFileChoice `json:\"choice\"`\n\tFilePath string                   `json:\"filePath\"`\n\tBody     string                   `json:\"body\"`\n}\n\ntype FileMapInputs map[string]string\n\nfunc (f FileMapInputs) NumFiles() int {\n\treturn len(f)\n}\n\nfunc (f FileMapInputs) TotalSize() int64 {\n\tvar totalSize int64\n\tfor _, body := range f {\n\t\ttotalSize += int64(len(body))\n\t}\n\treturn totalSize\n}\n\ntype LoadContextParams struct {\n\tContextType     ContextType           `json:\"contextType\"`\n\tName            string                `json:\"name\"`\n\tUrl             string                `json:\"url\"`\n\tFilePath        string                `json:\"file_path\"`\n\tBody            string                `json:\"body\"`\n\tForceSkipIgnore bool                  `json:\"forceSkipIgnore\"`\n\tImageDetail     openai.ImageURLDetail `json:\"imageDetail\"`\n\tAutoLoaded      bool                  `json:\"autoLoaded\"`\n\n\tInputShas   map[string]string `json:\"inputShas\"`\n\tInputTokens map[string]int    `json:\"inputTokens\"`\n\tInputSizes  map[string]int64  `json:\"inputSizes\"`\n\tMapBodies   FileMapBodies     `json:\"mapBodies\"`\n\n\t// For naming piped data\n\tApiKeys     map[string]string `json:\"apiKeys\"`     // deprecated\n\tOpenAIBase  string            `json:\"openAIBase\"`  // deprecated\n\tOpenAIOrgId string            `json:\"openAIOrgId\"` // deprecated\n\n\tAuthVars map[string]string `json:\"authVars\"`\n\n\tSessionId string `json:\"sessionId\"`\n}\n\ntype LoadContextRequest []*LoadContextParams\n\ntype LoadContextResponse struct {\n\tTokensAdded       int    `json:\"tokensAdded\"`\n\tTotalTokens       int    `json:\"totalTokens\"`\n\tMaxTokensExceeded bool   `json:\"maxTokensExceeded\"`\n\tMaxTokens         int    `json:\"maxTokens\"`\n\tMsg               string `json:\"msg\"`\n}\n\ntype UpdateContextParams struct {\n\tBody            string            `json:\"body\"`\n\tInputShas       map[string]string `json:\"inputShas\"`\n\tInputTokens     map[string]int    `json:\"inputTokens\"`\n\tInputSizes      map[string]int64  `json:\"inputSizes\"`\n\tMapBodies       FileMapBodies     `json:\"mapBodies\"`\n\tRemovedMapPaths []string          `json:\"removedMapPaths\"`\n}\n\ntype GetFileMapRequest struct {\n\tMapInputs FileMapInputs `json:\"mapInputs\"`\n}\n\ntype GetFileMapResponse struct {\n\tMapBodies FileMapBodies `json:\"mapBodies\"`\n}\n\ntype LoadCachedFileMapRequest struct {\n\tFilePaths []string `json:\"filePaths\"`\n}\n\ntype LoadCachedFileMapResponse struct {\n\tLoadRes      *LoadContextResponse `json:\"loadRes\"`\n\tCachedByPath map[string]bool      `json:\"cachedByPath\"`\n}\n\ntype GetContextBodyRequest struct {\n\tContextId string `json:\"contextId\"`\n}\n\ntype GetContextBodyResponse struct {\n\tBody string `json:\"body\"`\n}\n\ntype UpdateContextRequest map[string]*UpdateContextParams\n\ntype UpdateContextResponse = LoadContextResponse\n\ntype DeleteContextRequest struct {\n\tIds map[string]bool `json:\"ids\"`\n}\n\ntype DeleteContextResponse struct {\n\tTokensRemoved int    `json:\"tokensRemoved\"`\n\tTotalTokens   int    `json:\"totalTokens\"`\n\tMsg           string `json:\"msg\"`\n}\n\ntype RejectFileRequest struct {\n\tFilePath string `json:\"filePath\"`\n}\n\ntype RejectFilesRequest struct {\n\tPaths []string `json:\"paths\"`\n}\n\ntype RewindPlanRequest struct {\n\tSha string `json:\"sha\"`\n}\n\ntype RewindPlanResponse struct {\n\tLatestSha    string `json:\"latestSha\"`\n\tLatestCommit string `json:\"latestCommit\"`\n}\n\ntype LogResponse struct {\n\tShas []string `json:\"shas\"`\n\tBody string   `json:\"body\"`\n}\n\ntype CreateBranchRequest struct {\n\tName string `json:\"name\"`\n}\n\ntype UpdateSettingsRequest struct {\n\tModelPackName string     `json:\"modelPackName\"`\n\tModelPack     *ModelPack `json:\"modelPack\"`\n}\n\ntype UpdateSettingsResponse struct {\n\tMsg string `json:\"msg\"`\n}\n\ntype UpdatePlanConfigRequest struct {\n\tConfig *PlanConfig `json:\"config\"`\n}\n\ntype UpdateDefaultPlanConfigRequest struct {\n\tConfig *PlanConfig `json:\"config\"`\n}\n\ntype GetPlanConfigResponse struct {\n\tConfig *PlanConfig `json:\"config\"`\n}\n\ntype GetDefaultPlanConfigResponse struct {\n\tConfig *PlanConfig `json:\"config\"`\n}\n\ntype ListUsersResponse struct {\n\tUsers            []*User             `json:\"users\"`\n\tOrgUsersByUserId map[string]*OrgUser `json:\"orgUsersByUserId\"`\n}\n\ntype ApplyPlanRequest struct {\n\tApiKeys     map[string]string `json:\"apiKeys\"`     // deprecated\n\tOpenAIBase  string            `json:\"openAIBase\"`  // deprecated\n\tOpenAIOrgId string            `json:\"openAIOrgId\"` // deprecated\n\n\tAuthVars map[string]string `json:\"authVars\"`\n\n\tSessionId string `json:\"sessionId\"`\n}\n\ntype RenamePlanRequest struct {\n\tName string `json:\"name\"`\n}\n\ntype GetBuildStatusResponse struct {\n\tBuiltFiles       map[string]bool `json:\"builtFiles\"`\n\tIsBuildingByPath map[string]bool `json:\"isBuildingByPath\"`\n}\n\n// Cloud requests and responses\ntype CreditsLogRequest struct {\n\tTransactionType CreditsTransactionType `json:\"transactionType\"`\n\tPlanId          string                 `json:\"planId\"`\n\tSessionId       string                 `json:\"sessionId\"`\n\tDayStart        *time.Time             `json:\"dayStart\"`\n\tMonth           bool                   `json:\"month\"`\n}\n\ntype CreditsLogResponse struct {\n\tTransactions  []*CreditsTransaction `json:\"transactions\"`\n\tNumPages      int                   `json:\"numPages\"`\n\tNumPagesMax   bool                  `json:\"numPagesMax\"`\n\tMonthStart    time.Time             `json:\"monthStart\"`\n\tPlanNamesById map[string]string     `json:\"planNamesById\"`\n}\n\ntype CreditsSummaryResponse struct {\n\tBalance decimal.Decimal `json:\"balance\"`\n\n\tTotalSpend decimal.Decimal `json:\"totalSpend\"`\n\n\tMonthStart time.Time `json:\"monthStart\"`\n\n\tByPlanId      map[string]decimal.Decimal `json:\"byPlanId\"`\n\tPlanNamesById map[string]string          `json:\"planNamesById\"`\n\n\tByModelName map[string]decimal.Decimal `json:\"byModelName\"`\n\tByPurpose   map[string]decimal.Decimal `json:\"byPurpose\"`\n\n\tCacheSavings decimal.Decimal `json:\"cacheSavings\"`\n}\n\ntype GetBalanceResponse struct {\n\tBalance decimal.Decimal `json:\"balance\"`\n}\n"
  },
  {
    "path": "app/shared/stream.go",
    "content": "package shared\n\nconst STREAM_MESSAGE_SEPARATOR = \"@@PX@@\"\n\ntype BuildInfo struct {\n\tPath      string `json:\"path\"`\n\tNumTokens int    `json:\"numTokens\"`\n\tFinished  bool   `json:\"finished\"`\n\tRemoved   bool   `json:\"removed,omitempty\"`\n}\n\ntype StreamMessageType string\n\nconst (\n\tStreamMessageStart             StreamMessageType = \"start\"\n\tStreamMessageConnectActive     StreamMessageType = \"connectActive\"\n\tStreamMessageHeartbeat         StreamMessageType = \"heartbeat\"\n\tStreamMessageReply             StreamMessageType = \"reply\"\n\tStreamMessageDescribing        StreamMessageType = \"describing\"\n\tStreamMessageRepliesFinished   StreamMessageType = \"repliesFinished\"\n\tStreamMessageBuildInfo         StreamMessageType = \"buildInfo\"\n\tStreamMessagePromptMissingFile StreamMessageType = \"promptMissingFile\"\n\tStreamMessageLoadContext       StreamMessageType = \"loadContext\"\n\tStreamMessageAborted           StreamMessageType = \"aborted\"\n\tStreamMessageFinished          StreamMessageType = \"finished\"\n\tStreamMessageError             StreamMessageType = \"error\"\n\n\tStreamMessageMulti StreamMessageType = \"multi\"\n)\n\ntype StreamMessage struct {\n\tType StreamMessageType `json:\"type\"`\n\n\tReplyChunk string `json:\"replyChunk,omitempty\"`\n\n\tBuildInfo              *BuildInfo               `json:\"buildInfo,omitempty\"`\n\tDescription            *ConvoMessageDescription `json:\"description,omitempty\"`\n\tError                  *ApiError                `json:\"error,omitempty\"`\n\tMissingFilePath        string                   `json:\"missingFilePath,omitempty\"`\n\tMissingFileAutoContext bool                     `json:\"missingFileAutoContext,omitempty\"`\n\tModelStreamId          string                   `json:\"modelStreamId,omitempty\"`\n\tLoadContextFiles       []string                 `json:\"loadContextFiles,omitempty\"`\n\tInitPrompt             string                   `json:\"initPrompt,omitempty\"`\n\tInitReplies            []string                 `json:\"initReplies,omitempty\"`\n\tInitBuildOnly          bool                     `json:\"initBuildOnly,omitempty\"`\n\n\tStreamMessages []StreamMessage `json:\"streamMessages,omitempty\"`\n}\n"
  },
  {
    "path": "app/shared/streamed_change.go",
    "content": "package shared\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype StreamedChangeSection struct {\n\tStartLine       int    `json:\"startLine\"`\n\tEndLine         int    `json:\"endLine\"`\n\tStartLineString string `json:\"startLineString\"`\n\tEndLineString   string `json:\"endLineString\"`\n}\n\ntype StreamedChangeWithLineNums struct {\n\tOld               StreamedChangeSection `json:\"old\"`\n\tStartLineIncluded bool                  `json:\"startLineIncluded\"`\n\tEndLineIncluded   bool                  `json:\"endLineIncluded\"`\n\tNew               string                `json:\"new\"`\n}\n\nfunc (streamedChangeSection StreamedChangeSection) GetLines() (int, int, error) {\n\treturn streamedChangeSection.GetLinesWithPrefix(\"pdx-\")\n}\n\nfunc (streamedChangeSection StreamedChangeSection) GetLinesWithPrefix(prefix string) (int, int, error) {\n\tvar startLine, endLine int\n\tvar err error\n\n\tif streamedChangeSection.StartLineString == \"\" {\n\t\tlog.Printf(\"StartLineString is empty\\n\")\n\t\t// spew.Dump(streamedChangeSection)\n\t\tstartLine = streamedChangeSection.StartLine\n\t} else {\n\t\tstartLine, err = ExtractLineNumberWithPrefix(streamedChangeSection.StartLineString, prefix)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error extracting start line number: %v\\n\", err)\n\t\t\treturn 0, 0, fmt.Errorf(\"error extracting start line number: %v\", err)\n\t\t}\n\t}\n\n\tif streamedChangeSection.EndLineString == \"\" {\n\t\tlog.Printf(\"EndLineString is empty\\n\")\n\t\t// spew.Dump(streamedChangeSection)\n\t\tif streamedChangeSection.EndLine > 0 {\n\t\t\tendLine = streamedChangeSection.EndLine\n\t\t} else {\n\t\t\tendLine = startLine\n\t\t}\n\t} else {\n\t\tendLine, err = ExtractLineNumberWithPrefix(streamedChangeSection.EndLineString, prefix)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error extracting end line number: %v\\n\", err)\n\t\t\treturn 0, 0, fmt.Errorf(\"error extracting end line number: %v\", err)\n\t\t}\n\t}\n\n\tlog.Printf(\"StartLine: %d, EndLine: %d\\n\", startLine, endLine)\n\n\tif startLine > endLine {\n\t\tlog.Printf(\"Start line is greater than end line: %d > %d\\n\", startLine, endLine)\n\t\treturn 0, 0, fmt.Errorf(\"start line is greater than end line: %d > %d\", startLine, endLine)\n\t}\n\n\tif startLine < 1 {\n\t\tlog.Printf(\"Start line is less than 1: %d\\n\", startLine)\n\t\treturn 0, 0, fmt.Errorf(\"start line is less than 1: %d\", startLine)\n\t}\n\n\treturn startLine, endLine, nil\n}\n\nfunc ExtractLineNumber(line string) (int, error) {\n\treturn ExtractLineNumberWithPrefix(line, \"pdx-\")\n}\n\nfunc ExtractLineNumberWithPrefix(line, prefix string) (int, error) {\n\t// Split the line at the first space to isolate the line number\n\tparts := strings.SplitN(line, \" \", 2)\n\n\t// Remove the colon from the line number part\n\tlineNumberStr := strings.TrimSuffix(parts[0], \":\")\n\tlineNumberStr = strings.TrimPrefix(lineNumberStr, prefix)\n\tif lineNumberStr == \"\" {\n\t\treturn 0, fmt.Errorf(\"no line number found\")\n\t}\n\n\t// Convert the line number part to an integer\n\tlineNumber, err := strconv.Atoi(lineNumberStr)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"invalid line number: %v\", err)\n\t}\n\n\treturn lineNumber, nil\n}\n"
  },
  {
    "path": "app/shared/syntax.go",
    "content": "package shared\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n)\n\ntype Language string\n\nconst (\n\tLanguageBash       Language = \"bash\"\n\tLanguageC          Language = \"c\"\n\tLanguageCpp        Language = \"cpp\"\n\tLanguageCsharp     Language = \"csharp\"\n\tLanguageCss        Language = \"css\"\n\tLanguageCue        Language = \"cue\"\n\tLanguageDockerfile Language = \"dockerfile\"\n\tLanguageElixir     Language = \"elixir\"\n\tLanguageElm        Language = \"elm\"\n\tLanguageGo         Language = \"go\"\n\tLanguageGroovy     Language = \"groovy\"\n\tLanguageHcl        Language = \"hcl\"\n\tLanguageHtml       Language = \"html\"\n\tLanguageJava       Language = \"java\"\n\tLanguageJavascript Language = \"javascript\"\n\tLanguageJson       Language = \"json\"\n\tLanguageKotlin     Language = \"kotlin\"\n\tLanguageLua        Language = \"lua\"\n\tLanguageOCaml      Language = \"ocaml\"\n\tLanguagePhp        Language = \"php\"\n\tLanguageProtobuf   Language = \"protobuf\"\n\tLanguagePython     Language = \"python\"\n\tLanguageRuby       Language = \"ruby\"\n\tLanguageRust       Language = \"rust\"\n\tLanguageScala      Language = \"scala\"\n\tLanguageSvelte     Language = \"svelte\"\n\tLanguageSwift      Language = \"swift\"\n\tLanguageToml       Language = \"toml\"\n\tLanguageTypescript Language = \"typescript\"\n\tLanguageJsx        Language = \"jsx\"\n\tLanguageTsx        Language = \"tsx\"\n\tLanguageYaml       Language = \"yaml\"\n\tLanguageMarkdown   Language = \"markdown\"\n)\n\nvar Languages = []Language{\n\tLanguageBash,\n\tLanguageC,\n\tLanguageCpp,\n\tLanguageCsharp,\n\tLanguageCss,\n\tLanguageCue,\n\tLanguageDockerfile,\n\tLanguageElixir,\n\tLanguageElm,\n\tLanguageGo,\n\tLanguageGroovy,\n\tLanguageHcl,\n\tLanguageHtml,\n\tLanguageJava,\n\tLanguageJavascript,\n\tLanguageJson,\n\tLanguageKotlin,\n\tLanguageLua,\n\tLanguageMarkdown,\n\tLanguageOCaml,\n\tLanguagePhp,\n\tLanguageProtobuf,\n\tLanguagePython,\n\tLanguageRuby,\n\tLanguageRust,\n\tLanguageScala,\n\tLanguageSvelte,\n\tLanguageSwift,\n\tLanguageToml,\n\tLanguageTypescript,\n\tLanguageJsx,\n\tLanguageTsx,\n\tLanguageYaml,\n}\n\nvar lacksFileMapSupport = []Language{\n\t// config languages aren't mapped, model decides whether to load them based on file name\n\tLanguageHcl,\n\tLanguageYaml,\n\tLanguageToml,\n\tLanguageCue,\n\tLanguageJson,\n\tLanguageProtobuf,\n\n\t// these just need more work for mapping\n\tLanguageGroovy,\n\tLanguageOCaml,\n}\n\nvar SkipTreeSitter = map[Language]bool{\n\tLanguageMarkdown: true,\n}\n\nvar LanguageSet = map[Language]bool{}\n\nvar FileMapSupportSet = map[Language]bool{}\n\nfunc init() {\n\tfor _, lang := range Languages {\n\t\tLanguageSet[lang] = true\n\t\tFileMapSupportSet[lang] = true\n\t}\n\tfor _, lang := range lacksFileMapSupport {\n\t\tFileMapSupportSet[lang] = false\n\t}\n}\n\nfunc IsTreeSitterLanguage(lang Language) bool {\n\treturn LanguageSet[lang] && !SkipTreeSitter[lang]\n}\n\nfunc HasTreeSitterSupport(path string) bool {\n\tbase := filepath.Base(path)\n\text := filepath.Ext(base)\n\tisDockerfile := strings.Contains(strings.ToLower(base), \"dockerfile\")\n\treturn (isDockerfile || LanguageByExtension[ext] != \"\") && IsTreeSitterLanguage(LanguageByExtension[ext])\n}\n\nfunc HasFileMapSupport(path string) bool {\n\tbase := filepath.Base(path)\n\text := filepath.Ext(base)\n\tisDockerfile := strings.Contains(strings.ToLower(base), \"dockerfile\")\n\tlang := LanguageByExtension[ext]\n\treturn isDockerfile || (lang != \"\" && FileMapSupportSet[lang])\n}\n\nvar LanguageByExtension = map[string]Language{\n\t\".sh\":     LanguageBash,\n\t\".bash\":   LanguageBash,\n\t\".c\":      LanguageC,\n\t\".h\":      LanguageC,\n\t\".cpp\":    LanguageCpp,\n\t\".cc\":     LanguageCpp,\n\t\".cs\":     LanguageCsharp,\n\t\".css\":    LanguageCss,\n\t\".cue\":    LanguageCue,\n\t\".ex\":     LanguageElixir,\n\t\".exs\":    LanguageElixir,\n\t\".elm\":    LanguageElm,\n\t\".go\":     LanguageGo,\n\t\".groovy\": LanguageGroovy,\n\t\".hcl\":    LanguageHcl,\n\t\".html\":   LanguageHtml,\n\t\".java\":   LanguageJava,\n\t\".js\":     LanguageJavascript,\n\t\".json\":   LanguageJson,\n\t\".jsx\":    LanguageTsx,\n\t\".kt\":     LanguageKotlin,\n\t\".lua\":    LanguageLua,\n\t\".ml\":     LanguageOCaml,\n\t\".php\":    LanguagePhp,\n\t\".proto\":  LanguageProtobuf,\n\t\".py\":     LanguagePython,\n\t\".rb\":     LanguageRuby,\n\t\".rs\":     LanguageRust,\n\t\".scala\":  LanguageScala,\n\t\".svelte\": LanguageSvelte,\n\t\".swift\":  LanguageSwift,\n\t\".toml\":   LanguageToml,\n\t\".ts\":     LanguageTypescript,\n\t\".tsx\":    LanguageTsx,\n\t\".yaml\":   LanguageYaml,\n\t\".yml\":    LanguageYaml,\n\t\".md\":     LanguageMarkdown,\n}\n\nvar LanguageFallbackByExtension = map[string]Language{\n\t\".ts\": LanguageTsx,\n\t\".js\": LanguageTsx,\n}\n"
  },
  {
    "path": "app/shared/tokens.go",
    "content": "package shared\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkoukk/tiktoken-go\"\n)\n\nvar tkm *tiktoken.Tiktoken\n\nconst EstimatedBytesPerToken = 4\n\nfunc init() {\n\tvar err error\n\ttkm, err = tiktoken.EncodingForModel(\"gpt-4o\")\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"error getting encoding for model: %v\", err))\n\t}\n}\n\nfunc GetNumTokensEstimate(text string) int {\n\treturn len(tkm.Encode(text, nil, nil))\n}\n\nfunc GetFastNumTokensEstimate(text string) int {\n\treturn GetBytesToTokensEstimate(int64(len(text)))\n}\n\nfunc GetBytesToTokensEstimate(bytes int64) int {\n\treturn int(bytes / EstimatedBytesPerToken)\n}\n"
  },
  {
    "path": "app/shared/tygo.yaml",
    "content": "# You can specify more than one package\npackages:\n  # The package path just like you would import it in Go\n  - path: \"shared\"\n\n    # Where this output should be written to.\n    # If you specify a folder it will be written to a file `index.ts` within that folder. By default it is written into the Golang package folder.\n    output_path: \"../server/cloud/ui/node/src/types/generated.ts\"\n    include_files:\n      - \"data_models.go\"\n      - \"plan_settings.go\"\n      - \"streamed_change.go\"\n      - \"plan_status.go\"\n\n    type_mappings:\n      time.Time: \"string /* RFC3339 */\"\n      decimal.Decimal: \"string /* decimal.Decimal */\"\n      openai.ImageURLDetail: '\"high\" | \"low\" | \"auto\"'\n"
  },
  {
    "path": "app/shared/utils.go",
    "content": "package shared\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n)\n\nfunc Pointer[T any](v T) *T {\n\treturn &v\n}\n\nconst TsFormat = \"2006-01-02T15:04:05.999Z\"\n\nfunc StringTs() string {\n\treturn time.Now().UTC().Format(TsFormat)\n}\n\nvar letters = []byte(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n\nfunc GetRandomAlphanumeric(n int) ([]byte, error) {\n\tbytes := make([]byte, n)\n\tif _, err := rand.Read(bytes); err != nil {\n\t\treturn nil, err\n\t}\n\tfor i, b := range bytes {\n\t\tbytes[i] = letters[int(b)%len(letters)]\n\t}\n\treturn bytes, nil\n}\n\nfunc Dasherize(s string) string {\n\tregex := regexp.MustCompile(\"([A-Z][a-z0-9]*)\")\n\tindexes := regex.FindAllStringIndex(s, -1)\n\tif indexes == nil {\n\t\treturn strings.ToLower(s)\n\t}\n\n\tvar parts []string\n\tlastStart := 0\n\tfor _, loc := range indexes {\n\t\tif lastStart != loc[0] {\n\t\t\tparts = append(parts, s[lastStart:loc[0]])\n\t\t}\n\t\tparts = append(parts, s[loc[0]:loc[1]])\n\t\tlastStart = loc[1]\n\t}\n\tif lastStart < len(s) {\n\t\tparts = append(parts, s[lastStart:])\n\t}\n\n\ts = strings.ToLower(strings.Join(parts, \"-\"))\n\ts = strings.ReplaceAll(s, \" \", \"-\")\n\ts = strings.ReplaceAll(s, \"_\", \"-\")\n\n\treturn s\n}\n\nfunc Compact(s string) string {\n\treturn strings.ReplaceAll(Dasherize(s), \"-\", \"\")\n}\n\nfunc Capitalize(s string) string {\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn strings.ToUpper(s[:1]) + s[1:]\n}\n\ntype LineNumberedTextType string\n\nfunc AddLineNums(s string) LineNumberedTextType {\n\treturn LineNumberedTextType(AddLineNumsWithPrefix(s, \"pdx-\"))\n}\n\nfunc AddLineNumsWithPrefix(s, prefix string) LineNumberedTextType {\n\tvar res string\n\tfor i, line := range strings.Split(s, \"\\n\") {\n\t\tres += fmt.Sprintf(\"%s%d: %s\\n\", prefix, i+1, line)\n\t}\n\treturn LineNumberedTextType(res)\n}\n\nfunc RemoveLineNums(s LineNumberedTextType) string {\n\treturn RemoveLineNumsWithPrefix(s, \"pdx-\")\n}\n\nfunc RemoveLineNumsWithPrefix(s LineNumberedTextType, prefix string) string {\n\treturn regexp.MustCompile(fmt.Sprintf(`(?m)^%s\\d+: `, prefix)).ReplaceAllString(string(s), \"\")\n}\n\n// indexRunes searches for the slice of runes `needle` in the slice of runes `haystack`\n// and returns the index of the first rune of `needle` in `haystack`, or -1 if `needle` is not present.\nfunc IndexRunes(haystack []rune, needle []rune) int {\n\tif len(needle) == 0 {\n\t\treturn 0\n\t}\n\tif len(haystack) == 0 {\n\t\treturn -1\n\t}\n\n\t// Search for the needle\n\tfor i := 0; i <= len(haystack)-len(needle); i++ {\n\t\tfound := true\n\t\tfor j := 0; j < len(needle); j++ {\n\t\t\tif haystack[i+j] != needle[j] {\n\t\t\t\tfound = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif found {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc ReplaceReverse(s, old, new string, n int) string {\n\t// If n is negative, there is no limit to the number of replacements\n\tif n == 0 {\n\t\treturn s\n\t}\n\n\tif n < 0 {\n\t\treturn strings.Replace(s, old, new, -1)\n\t}\n\n\t// If n is positive, replace the last n occurrences of old with new\n\tvar res string\n\tfor i := 0; i < n; i++ {\n\t\tidx := strings.LastIndex(s, old)\n\t\tif idx == -1 {\n\t\t\tbreak\n\t\t}\n\t\tres = s[:idx] + new + s[idx+len(old):]\n\t\ts = res\n\t}\n\treturn res\n}\n\nfunc NormalizeEOL(data []byte) []byte {\n\tif !looksTextish(data) {\n\t\treturn data\n\t}\n\n\t// CRLF -> LF\n\tn := bytes.ReplaceAll(data, []byte{'\\r', '\\n'}, []byte{'\\n'})\n\n\t// treat stray CR as newline as well\n\tn = bytes.ReplaceAll(n, []byte{'\\r'}, []byte{'\\n'})\n\treturn n\n}\n\n// looksTextish checks some very cheap heuristics:\n//  1. no NUL bytes      → probably not binary\n//  2. valid UTF-8       → BOMs are OK\n//  3. printable ratio   → ≥ 90 % of runes are >= 0x20 or common whitespace\nfunc looksTextish(b []byte) bool {\n\tif bytes.IndexByte(b, 0x00) != -1 { // 1\n\t\treturn false\n\t}\n\tif !utf8.Valid(b) { // 2\n\t\treturn false\n\t}\n\n\tprintable := 0\n\tfor len(b) > 0 {\n\t\tr, size := utf8.DecodeRune(b)\n\t\tb = b[size:]\n\t\tswitch {\n\t\tcase r == '\\n', r == '\\r', r == '\\t':\n\t\t\tprintable++\n\t\tcase r >= 0x20 && r != 0x7f:\n\t\t\tprintable++\n\t\t}\n\t}\n\treturn float64(printable)/float64(len(b)) > 0.90 // 3\n}\n"
  },
  {
    "path": "app/shared/utils_struct.go",
    "content": "package shared\n\nimport \"reflect\"\n\nfunc Merge[T any](base T, ov T) T {\n\trvBase := reflect.ValueOf(&base).Elem() // addressable\n\trvOv := reflect.ValueOf(ov)\n\n\tfor i := 0; i < rvBase.NumField(); i++ {\n\t\tfOv := rvOv.Field(i)\n\t\tif !fOv.IsZero() { // ← built-in zero test\n\t\t\trvBase.Field(i).Set(fOv)\n\t\t}\n\t}\n\treturn base\n}\n\n// FieldsDefined reports whether every name in fields is present on the\n// (possibly-pointer) struct v. It returns the first missing field name\n// (empty string means all present).\nfunc FieldsDefined(v any, fields []string) (ok bool, missing string) {\n\trv := reflect.ValueOf(v)\n\tif rv.Kind() == reflect.Pointer {\n\t\trv = rv.Elem()\n\t}\n\tif rv.Kind() != reflect.Struct {\n\t\tpanic(\"FieldsDefined: value must be a struct or *struct\")\n\t}\n\n\trt := rv.Type() // a reflect.Type is a bit faster for look-ups\n\n\tfor _, name := range fields {\n\t\tif _, found := rt.FieldByName(name); !found {\n\t\t\treturn false, name\n\t\t}\n\t}\n\treturn true, \"\"\n}\n"
  },
  {
    "path": "app/start_local.sh",
    "content": "#!/usr/bin/env bash\n\n# Get the absolute path to the script's directory, regardless of where it's run from\nSCRIPT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\"\n\n# Change to the app directory if we're not already there\ncd \"$SCRIPT_DIR\"\n\necho \"Checking dependencies...\"\n\nif ! [ -x \"$(command -v git)\" ]; then\n    echo 'Error: git is not installed.' >&2\n    echo 'Please install git before running this setup script.' >&2\n    exit 1\nfi\n\nif ! [ -x \"$(command -v docker)\" ]; then\n    echo 'Error: docker is not installed.' >&2\n    echo 'Please install docker before running this setup script.' >&2\n    exit 1\nfi\n\nif ! [ -x \"$(command -v docker-compose)\" ]; then\n    docker compose 2>&1 > /dev/null\n    if [[ $? -ne 0 ]]; then\n        echo 'Error: docker-compose is not installed.' >&2\n        echo 'Please install docker-compose before running this setup script.' >&2\n        exit 1\n    fi\nfi\n\necho \"Starting the local Plandex server and database...\"\n\ndocker compose pull plandex-server\ndocker compose up\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Website\n\nThis website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.\n\n### Installation\n\n```\n$ yarn\n```\n\n### Local Development\n\n```\n$ yarn start\n```\n\nThis command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.\n\n### Build\n\n```\n$ yarn build\n```\n\nThis command generates static content into the `build` directory and can be served using any static contents hosting service.\n\n### Deployment\n\nUsing SSH:\n\n```\n$ USE_SSH=true yarn deploy\n```\n\nNot using SSH:\n\n```\n$ GIT_USER=<Your GitHub username> yarn deploy\n```\n\nIf you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.\n"
  },
  {
    "path": "docs/babel.config.js",
    "content": "module.exports = {\n  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
  },
  {
    "path": "docs/blog/2019-05-28-first-blog-post.md",
    "content": "---\nslug: first-blog-post\ntitle: First Blog Post\nauthors:\n  name: Gao Wei\n  title: Docusaurus Core Team\n  url: https://github.com/wgao19\n  image_url: https://github.com/wgao19.png\ntags: [hola, docusaurus]\n---\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n"
  },
  {
    "path": "docs/blog/2019-05-29-long-blog-post.md",
    "content": "---\nslug: long-blog-post\ntitle: Long Blog Post\nauthors: endi\ntags: [hello, docusaurus]\n---\n\nThis is the summary of a very long blog post,\n\nUse a `<!--` `truncate` `-->` comment to limit blog post size in the list view.\n\n<!--truncate-->\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\n"
  },
  {
    "path": "docs/blog/2021-08-01-mdx-blog-post.mdx",
    "content": "---\nslug: mdx-blog-post\ntitle: MDX Blog Post\nauthors: [slorber]\ntags: [docusaurus]\n---\n\nBlog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).\n\n:::tip\n\nUse the power of React to create interactive blog posts.\n\n```js\n<button onClick={() => alert('button clicked!')}>Click me!</button>\n```\n\n<button onClick={() => alert('button clicked!')}>Click me!</button>\n\n:::\n"
  },
  {
    "path": "docs/blog/2021-08-26-welcome/index.md",
    "content": "---\nslug: welcome\ntitle: Welcome\nauthors: [slorber, yangshun]\ntags: [facebook, hello, docusaurus]\n---\n\n[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).\n\nSimply add Markdown files (or folders) to the `blog` directory.\n\nRegular blog authors can be added to `authors.yml`.\n\nThe blog post date can be extracted from filenames, such as:\n\n- `2019-05-30-welcome.md`\n- `2019-05-30-welcome/index.md`\n\nA blog post folder can be convenient to co-locate blog post images:\n\n![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)\n\nThe blog supports tags as well!\n\n**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.\n"
  },
  {
    "path": "docs/blog/authors.yml",
    "content": "endi:\n  name: Endilie Yacop Sucipto\n  title: Maintainer of Docusaurus\n  url: https://github.com/endiliey\n  image_url: https://github.com/endiliey.png\n\nyangshun:\n  name: Yangshun Tay\n  title: Front End Engineer @ Facebook\n  url: https://github.com/yangshun\n  image_url: https://github.com/yangshun.png\n\nslorber:\n  name: Sébastien Lorber\n  title: Docusaurus maintainer\n  url: https://sebastienlorber.com\n  image_url: https://github.com/slorber.png\n"
  },
  {
    "path": "docs/blog/tags.yml",
    "content": "facebook:\n  label: Facebook\n  permalink: /facebook\n  description: Facebook tag description\nhello:\n  label: Hello\n  permalink: /hello\n  description: Hello tag description\ndocusaurus:\n  label: Docusaurus\n  permalink: /docusaurus\n  description: Docusaurus tag description\nhola:\n  label: Hola\n  permalink: /hola\n  description: Hola tag description\n"
  },
  {
    "path": "docs/docs/cli-reference.md",
    "content": "---\nsidebar_position: 8\nsidebar_label: CLI Reference\n---\n\n# CLI Reference\n\nAll Plandex CLI commands and their options.\n\n## Usage\n\n```bash\nplandex [command] [flags]\npdx [command] [flags] # 'pdx' is an alias for 'plandex'\n```\n\n## Help\n\nBuilt-in help.\n\n```bash\nplandex help\npdx h # alias\n```\n\n`--all/-a`: List all commands.\n\nFor help on a specific command, use:\n\n```bash\nplandex [command] --help\n```\n\n## REPL\n\nThe easiest way to use Plandex is through the REPL. Start it in your project directory with:\n\n```bash\nplandex\n```\n\nor for short:\n\n```bash\npdx\n```\n\n### Flags\n\nThe REPL has a few convenient flags you can use to start it with different modes, autonomy settings, and model packs. You can pass any of these to `plandex` or `pdx` when starting the REPL.\n\n```\n  Mode\n    --chat, -c     Start in chat mode (for conversation without making changes)\n    --tell, -t     Start in tell mode (for implementation)\n\n  Autonomy\n    --no-auto      None → step-by-step, no automation\n    --basic        Basic → auto-continue plans, no other automation\n    --plus         Plus → auto-update context, smart context, auto-commit changes\n    --semi         Semi-Auto → auto-load context\n    --full         Full-Auto → auto-apply, auto-exec, auto-debug\n\n  Models\n    --daily        Daily driver pack (default models, balanced capability, cost, and speed)\n    --reasoning    Similar to daily driver, but uses reasoning model for planning\n    --strong       Strong pack (more capable models, higher cost and slower)\n    --cheap        Cheap pack (less capable models, lower cost and faster)\n    --oss          Open source pack (open source models)\n    \n    --gemini-planner       Gemini pack (Gemini 2.5 Pro for planning, default models for other roles)\n    --o3-planner           OpenAI o3-medium for planning, default models for other roles\n    --r1-planner           DeepSeek R1 for planning, default models for other roles\n    --perplexity-planner   Perplexity for planning, default models for other roles\n    --opus-planner         Anthropic Opus 4 for planning, default models for other roles\n```\n\nAll commands listed below can be run in the REPL by prefixing them with a backslash (`\\`), e.g. `\\new`.\n\n## Plans\n\n### new\n\nStart a new plan.\n\n```bash\nplandex new\nplandex new -n new-plan # with name\n```\n\n`--name/-n`: Name of the new plan. The name is generated automatically after first prompt if no name is specified on creation.\n\n`--context-dir/-d`: Base directory to load context from when auto-loading context is enabled. Defaults to `.` (current directory). Set a different directoy if you don't want all files to be included in the project map.\n\n`--no-auto`: Start the plan with auto-mode 'None' (step-by-step, no automation).\n\n`--basic`: Start the plan with auto-mode 'Basic' (auto-continue plans, no other automation).\n\n`--plus`: Start the plan with auto-mode 'Plus' (auto-update context, smart context, auto-commit changes).\n\n`--semi`: Start the plan with auto-mode 'Semi-Auto' (auto-load context).\n\n`--full`: Start the plan with auto-mode 'Full-Auto' (auto-apply, auto-exec, auto-debug).\n\n`--daily`: Start the plan with the daily driver model pack.\n\n`--reasoning`: Start the plan with the reasoning model pack.\n\n`--strong`: Start the plan with the strong model pack.\n\n`--cheap`: Start the plan with the cheap model pack.\n\n`--oss`: Start the plan with the open source model pack.\n\n`--gemini-planner`: Start the plan with the Gemini planner model pack.\n\n`--o3-planner`: Start the plan with the OpenAI o3-medium planner model pack.\n\n`--r1-planner`: Start the plan with the DeepSeek R1 planner model pack.\n\n`--perplexity-planner`: Start the plan with the Perplexity planner model pack.\n\n`--opus-planner`: Start the plan with the Anthropic Opus 4 planner model pack.\n\n### plans\n\nList plans. Output includes index, when each plan was last updated, the current branch of each plan, the number of tokens in context, and the number of tokens in the conversation (prior to summarization).\n\nIncludes full details on plans in current directory. Also includes names of plans in parent directories and child directories.\n\n```bash\nplandex plans\nplandex plans --archived # list archived plans only\n\npdx pl # alias\n```\n\n`--archived/-a`: List archived plans only.\n\n### current\n\nShow current plan. Output includes when the plan was last updated and created, the current branch, the number of tokens in context, and the number of tokens in the conversation (prior to summarization).\n\n```bash\nplandex current\npdx cu # alias\n```\n\n### cd\n\nSet current plan by name or index.\n\n```bash\nplandex cd # select from a list of plans\nplandex cd some-plan # by name\nplandex cd 4 # by index in `plandex plans`\n```\n\nWith no arguments, Plandex prompts you with a list of plans to select from.\n\nWith one argument, Plandex selects a plan by name or by index in the `plandex plans` list.\n\n### delete-plan\n\nDelete a plan by name, index, range, pattern, or select from a list.\n\n```bash\nplandex delete-plan # select from a list of plans\nplandex delete-plan some-plan # by name\nplandex delete-plan 4 # by index in `plandex plans`\nplandex delete-plan 2-4 # by range of indices\nplandex delete-plan 'docs-*' # by pattern\nplandex delete-plan --all # delete all plans\npdx dp # alias\n```\n\n`--all/-a`: Delete all plans.\n\n### rename\n\nRename the current plan.\n\n```bash\nplandex rename # prompt for new name\nplandex rename new-name # set new name\n```\n\n### archive\n\nArchive a plan.\n\n```bash\nplandex archive # select from a list of plans\nplandex archive some-plan # by name\nplandex archive 4 # by index in `plandex plans`\n\npdx arc # alias\n```\n\n### unarchive\n\nUnarchive a plan.\n\n```bash\nplandex unarchive # select from a list of archived plans\nplandex unarchive some-plan # by name\nplandex unarchive 4 # by index in `plandex plans --archived`\npdx unarc # alias\n```\n\n## Context\n\n### load\n\nLoad files, directories, directory layouts, URLs, notes, images, or piped data into context.\n\n```bash\nplandex load component.ts # single file\nplandex load component.ts action.ts reducer.ts # multiple files\nplandex load lib -r # loads lib and all its subdirectories\nplandex load tests/**/*.ts # loads all .ts files in tests and its subdirectories\nplandex load . --tree # loads the layout of the current directory and its subdirectories (file names only)\nplandex load https://redux.js.org/usage/writing-tests # loads the text-only content of the url\nnpm test | plandex load # loads the output of `npm test`\nplandex load -n 'add logging statements to all the code you generate.' # load a note into context\nplandex load ui-mockup.png # load an image into context\n\npdx l component.ts # alias\n```\n\n`--recursive/-r`: Load an entire directory and all its subdirectories.\n\n`--tree`: Load directory tree layout with file names only.\n\n`--map`: Load file map of the given directory (function/method/class signatures, variable names, types, etc.)\n\n`--note/-n`: Load a note into context.\n\n`--force/-f`: Load files even when ignored by .gitignore or .plandexignore.\n\n`--detail/-d`: Image detail level when loading an image (high or low)—default is high. See https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding for more info.\n\n### ls\n\nList everything in the current plan's context. Output includes index, name, type, token size, when the context added, and when the context was last updated.\n\n```bash\nplandex ls\n\nplandex list-context # longer alias\n```\n\n### rm\n\nRemove context by index, range, name, or glob.\n\n```bash\nplandex rm some-file.ts # by name\nplandex rm app/**/*.ts # by glob pattern\nplandex rm 4 # by index in `plandex ls`\nplandex rm 2-4 # by range of indices\n\nplandex remove # longer alias\nplandex unload # longer alias\n```\n\n### show\n\nOutput context by name or index.\n\n```bash\nplandex show some-file.ts # by name\nplandex show 4 # by index in `plandex ls`\n```\n\n### update\n\nUpdate any outdated context.\n\n```bash\nplandex update\npdx u # alias\n```\n\n### clear\n\nRemove all context.\n\n```bash\nplandex clear\n```\n\n## Control\n\n### tell\n\nDescribe a task.\n\n```bash\nplandex tell -f prompt.txt # from file\nplandex tell # open vim to write prompt\nplandex tell \"add a cancel button to the left of the submit button\" # inline\n\npdx t # alias\n```\n\n`--file/-f`: File path containing prompt.\n\n`--stop/-s`: Stop after a single model response (don't auto-continue). Defaults to opposite of config value `auto-continue`.\n\n`--no-build/-n`: Don't build proposed changes into pending file updates. Defaults to opposite of config value `auto-build`.\n\n`--bg`: Run task in the background. Only allowed if `--auto-load-context` and `--apply/-a` are not enabled. Not allowed with the default [autonomy level](./core-concepts/autonomy.md) in Plandex v2.\n\n`--auto-update-context`: Automatically confirm context updates. Defaults to config value `auto-update-context`.\n\n`--auto-load-context`: Automatically load context using project map. Defaults to config value `auto-load-context`.\n\n`--smart-context`: Use smart context to only load the necessary file(s) for each step during implementation. Defaults to config value `smart-context`.\n\n`--no-exec`: Don't execute commands after successful apply. Defaults to opposite of config value `can-exec`.\n\n`--auto-exec`: Automatically execute commands after successful apply without confirmation. Defaults to config value `auto-exec`.\n\n`--skip-menu`: Skip interactive menu when response finishes and changes are pending. Defaults to config value `skip-changes-menu`.\n\n`--debug`: Automatically execute and debug failing commands (optionally specify number of tries—default is 5). Defaults to config values of `auto-debug` and `auto-debug-tries`.\n\n`--apply/-a`: Automatically apply changes (and confirm context updates). Defaults to config value `auto-apply`.\n\n`--commit/-c`: Commit changes to git when `--apply/-a` is passed. Defaults to config value `auto-commit`.\n\n`--skip-commit`: Don't commit changes to git. Defaults to opposite of config value `auto-commit`.\n\n### continue\n\nContinue the plan.\n\n```bash\nplandex continue\n\npdx c # alias\n```\n\n`--stop/-s`: Stop after a single model response (don't auto-continue). Defaults to opposite of config value `auto-continue`.\n\n`--no-build/-n`: Don't build proposed changes into pending file updates. Defaults to opposite of config value `auto-build`.\n\n`--bg`: Run task in the background. Only allowed if `--auto-load-context` and `--apply/-a` are not enabled. Not allowed with the default [autonomy level](./core-concepts/autonomy.md) in Plandex v2.\n\n`--auto-update-context`: Automatically confirm context updates. Defaults to config value `auto-update-context`.\n\n`--auto-load-context`: Automatically load context using project map. Defaults to config value `auto-load-context`.\n\n`--smart-context`: Use smart context to only load the necessary file(s) for each step during implementation. Defaults to config value `smart-context`.\n\n`--no-exec`: Don't execute commands after successful apply. Defaults to opposite of config value `can-exec`.\n\n`--auto-exec`: Automatically execute commands after successful apply without confirmation. Defaults to config value `auto-exec`.\n\n`--skip-menu`: Skip interactive menu when response finishes and changes are pending. Defaults to config value `skip-changes-menu`.\n\n`--debug`: Automatically execute and debug failing commands (optionally specify number of tries—default is 5). Defaults to config values of `auto-debug` and `auto-debug-tries`.\n\n`--apply/-a`: Automatically apply changes (and confirm context updates). Defaults to config value `auto-apply`.\n\n`--commit/-c`: Commit changes to git when `--apply/-a` is passed. Defaults to config value `auto-commit`.\n\n`--skip-commit`: Don't commit changes to git. Defaults to opposite of config value `auto-commit`.\n\n### build\n\nBuild any unbuilt pending changes from the plan conversation.\n\n```bash\nplandex build\npdx b # alias\n```\n\n`--bg`: Build in the background. Not allowed if `--apply/-a` is enabled.\n\n`--stop/-s`: Stop after a single model response (don't auto-continue). Defaults to opposite of config value `auto-continue`.\n\n`--no-build/-n`: Don't build proposed changes into pending file updates. Defaults to opposite of config value `auto-build`.\n\n`--auto-update-context`: Automatically confirm context updates. Defaults to config value `auto-update-context`.\n\n`--no-exec`: Don't execute commands after successful apply. Defaults to opposite of config value `can-exec`.\n\n`--auto-exec`: Automatically execute commands after successful apply without confirmation. Defaults to config value `auto-exec`.\n\n`--skip-menu`: Skip interactive menu when response finishes and changes are pending. Defaults to config value `skip-changes-menu`.\n\n`--debug`: Automatically execute and debug failing commands (optionally specify number of tries—default is 5). Defaults to config values of `auto-debug` and `auto-debug-tries`.\n\n`--apply/-a`: Automatically apply changes (and confirm context updates). Defaults to config value `auto-apply`.\n\n`--commit/-c`: Commit changes to git when `--apply/-a` is passed. Defaults to config value `auto-commit`.\n\n`--skip-commit`: Don't commit changes to git. Defaults to opposite of config value `auto-commit`.\n\n### chat\n\nAsk a question or chat without making any changes.\n\n```bash\nplandex chat \"is it clear from the context how to add a new line chart?\"\npdx ch # alias\n```\n\n`--file/-f`: File path containing prompt.\n\n`--bg`: Run task in the background. Not allowed if `--auto-load-context` is enabled. Not allowed with the default [autonomy level](./core-concepts/autonomy.md) in Plandex v2.\n\n`--auto-update-context`: Automatically confirm context updates. Defaults to config value `auto-update-context`.\n\n`--auto-load-context`: Automatically load context using project map. Defaults to config value `auto-load-context`.\n\n### debug\n\nRepeatedly run a command and automatically attempt fixes until it succeeds, rolling back changes on failure. Defaults to 5 tries before giving up.\n\n```bash\nplandex debug 'npm test' # try 5 times or until it succeeds\nplandex debug 10 'npm test' # try 10 times or until it succeeds\npdx db 'npm test' # alias\n```\n\n`--commit/-c`: Commit changes to git when `--apply/-a` is passed. Defaults to config value `auto-commit`.\n\n`--skip-commit`: Don't commit changes to git. Defaults to opposite of config value `auto-commit`.\n\n## Changes\n\n### diff\n\nReview pending changes in 'git diff' format or in a local browser UI.\n\n```bash\nplandex diff\nplandex diff --ui\n```\n\n`--plain/-p`: Output diffs in plain text with no ANSI codes.\n\n`--ui/-u`: Review pending changes in a local browser UI.\n\n`--side-by-side/-s`: Show diffs UI in side-by-side view\n\n`--line-by-line/-l`: Show diffs UI in line-by-line view\n\n### apply\n\nApply pending changes to project files.\n\n```bash\nplandex apply\npdx ap # alias\n```\n\n`--auto-update-context`: Automatically confirm context updates. Defaults to config value `auto-update-context`.\n\n`--no-exec`: Don't execute commands after successful apply. Defaults to opposite of config value `can-exec`.\n\n`--auto-exec`: Automatically execute commands after successful apply without confirmation. Defaults to config value `auto-exec`.\n\n`--debug`: Automatically execute and debug failing commands (optionally specify number of tries—default is 5). Defaults to config values of `auto-debug` and `auto-debug-tries`.\n\n`--commit/-c`: Commit changes to git when `--apply/-a` is passed. Defaults to config value `auto-commit`.\n\n`--skip-commit`: Don't commit changes to git. Defaults to opposite of config value `auto-commit`.\n\n`--full`: Apply the plan and debug in full auto mode.\n\n### reject\n\nReject pending changes to one or more project files.\n\n```bash\nplandex reject # select from a list of pending files to reject\nplandex reject file.ts # one file\nplandex reject file.ts another-file.ts # multiple files\nplandex reject --all # all pending files\n\npdx rj file.ts # alias\n```\n\n`--all/-a`: Reject all pending files.\n\n## History\n\n### log\n\nShow plan history.\n\n```bash\nplandex log\n\nplandex history # alias\nplandex logs # alias\n```\n\n### rewind\n\nRewind to a previous state.\n\n```bash\nplandex rewind # select from a list of previous states to rewind to\nplandex rewind 3 # rewind 3 steps\nplandex rewind a7c8d66 # rewind to a specific step from `plandex log`\n```\n\n### convo\n\nShow the current plan's conversation.\n\n```bash\nplandex convo\nplandex convo 1 # show a specific message\nplandex convo 1-5 # show a range of messages\nplandex convo 3- # show all messages from 3 to the end\n```\n\n`--plain/-p`: Output conversation in plain text with no ANSI codes.\n\n### summary\n\nShow the latest summary of the current plan.\n\n```bash\nplandex summary\n```\n\n`--plain/-p`: Output summary in plain text with no ANSI codes.\n\n## Branches\n\n### branches\n\nList plan branches. Output includes index, name, when the branch was last updated, the number of tokens in context, and the number of tokens in the conversation (prior to summarization).\n\n```bash\nplandex branches\npdx br # alias\n```\n\n### checkout\n\nCheckout or create a branch.\n\n```bash\nplandex checkout # select from a list of branches or prompt to create a new branch\nplandex checkout some-branch # checkout by name or create a new branch with that name\nplandex checkout some-branch -y # checkout by name or create a new branch with that name, auto-confirming branch creation\n\npdx co # alias\n```\n\n`--yes/-y`: Auto-confirm creating a new branch if it doesn't exist.\n\n### delete-branch\n\nDelete a branch by name or index.\n\n```bash\nplandex delete-branch # select from a list of branches\nplandex delete-branch some-branch # by name\nplandex delete-branch 4 # by index in `plandex branches`\n\npdx dlb # alias\n```\n\n## Background Tasks / Streams\n\n### ps\n\nList active and recently finished plan streams. Output includes stream ID, plan name, branch name, when the stream was started, and the stream's status (active, finished, stopped, errored, or waiting for a missing file to be selected).\n\n```bash\nplandex ps\n```\n\n### connect\n\nConnect to an active plan stream.\n\n```bash\nplandex connect # select from a list of active streams\nplandex connect a4de # by stream ID in `plandex ps`\nplandex connect some-plan main # by plan name and branch name\npdx conn # alias\n```\n\n### stop\n\nStop an active plan stream.\n\n```bash\nplandex stop # select from a list of active streams\nplandex stop a4de # by stream ID in `plandex ps`\nplandex stop some-plan main # by plan name and branch name\n```\n\n## Configuration\n\n### config\n\nShow current plan config. Output includes configuration settings for the plan, such as autonomy level, model settings, and other plan-specific options.\n\n```bash\nplandex config\n```\n\n### config default\n\nShow the default config used for new plans. Output includes the default configuration settings that will be applied to newly created plans.\n\n```bash\nplandex config default\n```\n\n### set-config\n\nUpdate configuration settings for the current plan.\n\n```bash\nplandex set-config # select from a list of config options\nplandex set-config auto-context true # set a specific config option\n```\n\nWith no arguments, Plandex prompts you to select from a list of config options.\n\nWith arguments, allows you to directly set specific configuration options for the current plan.\n\n### set-config default\n\nUpdate the default configuration settings for new plans.\n\n```bash\nplandex set-config default # select from a list of config options\nplandex set-config default auto-mode basic # set a specific default config option\n```\n\nWorks exactly the same as set-config above, but sets the default configuration for all new plans instead of only the current plan.\n\n### set-auto\n\nUpdate the auto-mode (autonomy level) for the current plan.\n\n```bash\nplandex set-auto # select from a list of auto-modes\nplandex set-auto full # set to full automation\nplandex set-auto semi # set to semi-auto mode\nplandex set-auto plus # set to plus mode\nplandex set-auto basic # set to basic mode\nplandex set-auto none # set to none (step-by-step, no automation)\n```\n\nWith no arguments, Plandex prompts you to select from a list of automation levels.\n\nWith one argument, Plandex sets the automation level directly to the specified value.\n\n### set-auto default\n\nSet the default auto-mode for new plans.\n\n```bash\nplandex set-auto default # select from a list of auto-modes\nplandex set-auto default basic # set default to basic mode\n```\n\nWorks exactly the same as set-auto above, but sets the default automation level for all new plans instead of only the current plan.\n\n## Models\n\n### models\n\nShow current plan models and model settings.\n\n```bash\nplandex models\n```\n\n### models default\n\nShow org-wide default models and model settings for new plans.\n\n```bash\nplandex models default\n```\n\n### models custom\n\nManage custom models, providers, and model packs via JSON file.\n\n```bash\nplandex models custom\nplandex models custom --save # save changes from the default custom-models.json file to the server\nplandex models custom --file /path/to/models.json # import custom models/providers/model-packs from a non-default JSON file\n```\n\n`--save`: Save changes from the JSON file to the server.\n\n`--file/-f`: Path to non-default custom models JSON file.\n\nWithout `--save`, this command will:\n\n- Create an example models file if none exists, or sync current server state to the file\n- Open the file in your configured editor\n- Prompt you to save changes when you return\n\nWith `--save`, it will skip opening the editor and sync changes from the JSON file to the server.\n\n### models available\n\nShow available models.\n\n```bash\nplandex models available # show all available models\nplandex models available --custom # show available custom models only\n```\n\n`--custom`: Show available custom models only.\n\n### providers\n\nShow all available model providers.\n\n```bash\nplandex providers\nplandex providers --custom # show custom providers only (not supported on Plandex Cloud)\n```\n\n`--custom/-c`: Show custom providers only (not supported on Plandex Cloud).\n\n### set-model\n\nUpdate current plan models or model settings.\n\n```bash\nplandex set-model # select from a list of model packs or edit via JSON\nplandex set-model daily # set model pack by name\nplandex set-model --json # edit plan's model settings via JSON file at default path\nplandex set-model --save # save changes from the plan's model settings JSON file to the server\nplandex set-model --json --file /path/to/settings.json # set plan's model settings from a JSON file at a non-default path\n```\n\n`--json`: Edit plan's model settings via JSON file at default path.\n\n`--save`: Save changes from the plan's model settings JSON file to the server.\n\n`--file`: Set plan's model settings from a JSON file at a non-default path.\n\nWith no arguments, Plandex prompts you to either select a model pack or edit settings via JSON.\n\nWhen using JSON mode without `--save`, Plandex will:\n\n- Write current settings to a JSON file\n- Open it in your configured editor\n- Apply the changes when you save and return\n\nWith `--save`, it will skip opening the editor and sync changes from the JSON file to the server.\n\nModel pack shortcuts are still available:\n\n```bash\nplandex set-model daily\nplandex set-model reasoning\nplandex set-model strong\nplandex set-model cheap\nplandex set-model oss\nplandex set-model gemini\n```\n\n### set-model default\n\nUpdate org-wide default model settings for new plans.\n\n```bash\nplandex set-model default # select from a list of model packs or edit via JSON\nplandex set-model default daily # set default model pack by name\nplandex set-model default --json # edit default settings via JSON file at default path\nplandex set-model default --save # save changes from the default model settings JSON file to the server\nplandex set-model default --json --file /path/to/settings.json # set default model settings from a JSON file at a non-default path\n```\n\nWorks exactly the same as `set-model` above, but sets the default model settings for all new plans instead of only the current plan.\n\n### model-packs\n\nShow all available model packs.\n\n```bash\nplandex model-packs # list built-in and custom model packs\nplandex model-packs --custom # list custom model packs only\n```\n\n`--custom`: Show available custom (user-created) model packs only.\n\n\n### model-packs show\n\nShow a built-in or custom model pack's settings.\n\n```bash\nplandex model-packs show # select from a list of built-in and custom model packs\nplandex model-packs show some-model-pack # by name\n```\n\n## Account Management\n\n### sign-in\n\nSign in, accept an invite, or create an account.\n\n```bash\nplandex sign-in\n```\n\n`--pin`: Sign in with a pin from the Plandex Cloud web UI.\n\nUnless you pass `--pin` (from the Plandex Cloud web UI), Plandex will prompt you for all required information to sign in, accept an invite, or create an account.\n\n### invite\n\nInvite a user to join your org.\n\n```bash\nplandex invite # prompt for email, name, and role\nplandex invite name@domain.com 'Full Name' member # invite with email, name, and role\n```\n\nUsers can be invited as `member`, `admin`, or `owner`.\n\n### revoke\n\nRevoke an invite or remove a user from your org.\n\n```bash\nplandex revoke # select from a list of users and invites\nplandex revoke name@domain.com # by email\n```\n\n### users\n\nList users and pending invites in your org.\n\n```bash\nplandex users\n```\n\n## Integrations\n\n### connect-claude\n\nConnect a Claude Pro or Max subscription. When Plandex calls Anthropic models, it will use your Claude subscription up to its quota.\n\n```bash\nplandex connect-claude\n```\n\n### disconnect-claude\n\n```bash\nplandex disconnect-claude\n```\n\nDisconnect your Claude Pro or Max subscription and clear credentials from your device. \n\n### claude-status\n\n```bash\nplandex claude-status\n```\n\nShows whether a Claude Pro or Max subscription is connected, and whether the quota has been exceeded.\n\n## Plandex Cloud\n\n### billing\n\nShow the billing settings page.\n\n```bash\nplandex billing\n```\n\n### usage\n\nShow Plandex Cloud current balance and usage report. Includes recent spend, amount saved by input caching, a breakdown of spend by plan, category, and model, and a log of individual transactions with the `--log` flag.\n\nDefaults to showing usage for the current session if you're using the REPL. Otherwise, defaults to showing usage for the day so far.\n\nRequires **Integrated Models** mode.\n\n```bash\nplandex usage\n```\n\n`--today`: Show usage for the day so far.\n\n`--month`: Show usage for the current billing month.\n\n`--plan`: Show usage for the current plan.\n\n`--log`: Show a log of individual transactions. Defaults to showing the log for the current session if you're using the REPL. Otherwise, defaults to showing the log for the day so far. Works with `--today`, `--month`, and `--plan` flags.\n\nFlags for `usage --log`:\n\n`--debits`: Show only debits in the log.\n\n`--purchases`: Show only purchases in the log.\n\n`--page-size/-s`: Number of transactions to display per page.\n\n`--page/-p`: Page number to display.\n\n\n\n\n\n\n"
  },
  {
    "path": "docs/docs/core-concepts/_category_.json",
    "content": "{\n  \"label\": \"Core Concepts\",\n  \"position\": 5,\n  \"collapsible\": true,\n  \"collapsed\": false\n}"
  },
  {
    "path": "docs/docs/core-concepts/autonomy.md",
    "content": "---\nsidebar_position: 10\nsidebar_label: Autonomy Levels\n---\n\n# Autonomy\n\nPlandex v2 offers multiple levels of autonomy with pre-set config. Each autonomy level controls:\n\n- Context loading and management\n- Plan continuation through multiple steps\n- Building of changes into pending updates\n- Application of changes to project files\n- Command execution and debugging\n- Git commits after changes are applied successfully\n\n## Autonomy Matrix\n\n| Feature               | None | Basic | Plus | Semi | Full |\n| --------------------- | ---- | ----- | ---- | ---- | ---- |\n| `auto-continue`       | ❌   | ✅    | ✅   | ✅   | ✅   |\n| `auto-build`          | ❌   | ✅    | ✅   | ✅   | ✅   |\n| `auto-load-context`   | ❌   | ❌    | ❌   | ✅   | ✅   |\n| `smart-context`       | ❌   | ❌    | ✅   | ✅   | ✅   |\n| `auto-update-context` | ❌   | ❌    | ✅   | ✅   | ✅   |\n| `auto-apply`          | ❌   | ❌    | ❌   | ❌   | ✅   |\n| `can-exec`            | ❌   | ❌    | ✅   | ✅   | ✅   |\n| `auto-exec`           | ❌   | ❌    | ❌   | ❌   | ✅   |\n| `auto-debug`          | ❌   | ❌    | ❌   | ❌   | ✅   |\n| `auto-commit`         | ❌   | ❌    | ✅   | ✅   | ✅   |\n\n## Setting Autonomy Levels\n\n### Using the CLI\n\n```bash\n# For current plan\nplandex set-auto none    # No automation\nplandex set-auto basic   # Auto-continue only\nplandex set-auto plus    # Smart context management\nplandex set-auto semi    # Auto-load context\nplandex set-auto full    # Full automation\n\n# For default settings on new plans\nplandex set-auto default basic   # Set default to basic\n```\n\n### When Starting the REPL\n\n```bash\nplandex --no-auto    # Start with 'None'\nplandex --basic      # Start with 'Basic'\nplandex --plus       # Start with 'Plus'\nplandex --semi       # Start with 'Semi'\nplandex --full       # Start with 'Full'\n```\n\n### When Creating a New Plan\n\n```bash\nplandex new --no-auto    # Create with 'None'\nplandex new --basic      # Create with 'Basic'\nplandex new --plus       # Create with 'Plus'\nplandex new --semi       # Create with 'Semi'\nplandex new --full       # Create with 'Full'\n```\n\n### Using the REPL\n\n```\n\\set-auto none    # Set to None\n\\set-auto basic   # Set to Basic\n\\set-auto plus    # Set to Plus\n\\set-auto semi    # Set to Semi-Auto\n\\set-auto full    # Set to Full-Auto\n```\n\n## Autonomy Levels\n\n### None\n\nComplete manual control with no automation:\n\n- Manual context loading\n- Context updates require approval\n- Manual plan continuation\n- Manual building of changes\n- Manually apply changes\n- Command execution disabled\n\n### Basic\n\n_Equivalent to Plandex v1 autonomy level_\n\nMinimal automation:\n\n- Manual context loading\n- Context updates require approval\n- Auto-continue plans until completion\n- Auto-build changes into pending updates\n- Manually apply changes\n- Command execution disabled\n\n### Plus\n\nSmart context management and manual command execution:\n\n- Manual context loading\n- Auto-update context when files change\n- Auto-continue plans until completion\n- Smart context management (only loads necessary files during implementation steps)\n- Auto-build changes into pending updates\n- Manually apply changes\n- Manual command execution\n- Auto-commit changes to git when applied\n\n### Semi\n\n_Default autonomy level for a fresh Plandex v2 install_\n\nAutomatic context loading:\n\n- Auto-load context using project map\n- Auto-update context when files change\n- Auto-continue plans until completion\n- Smart context management\n- Auto-build changes into pending updates\n- Manually apply changes\n- Manual command execution\n- Auto-commit changes to git when applied\n\n### Full\n\nComplete automation:\n\n- Auto-load context using project map\n- Auto-continue plans until completion\n- Smart context management\n- Auto-update context when files change\n- Auto-build changes into pending updates\n- Auto-apply changes to project files\n- Auto-execute commands after successful apply\n- Auto-debug failing commands\n- Auto-commit changes to git when applied\n\n### Custom\n\nYou can give a plan custom autonomy settings by setting config values directly:\n\n```bash\nplandex set-config auto-continue true\nplandex set-config auto-build true\nplandex set-config auto-load-context true\n```\n\n[More details on configuration](./configuration.md)\n\n## Safety\n\nBe extremely careful with full auto mode! It can make many changes quickly without any prompting or review, and can run commands that could potentially be destructive to your system.\n\nIt's a good idea to make sure your git state is clean, and to check out an isolated branch before running these commands.\n"
  },
  {
    "path": "docs/docs/core-concepts/background-tasks.md",
    "content": "---\nsidebar_position: 12\nsidebar_label: Background Tasks\n---\n\n# Background Tasks\n\nPlandex allows you to run tasks in the background, helping you work on multiple tasks in parallel.\n\n**Note:** in Plandex v2, sending tasks to the background is disabled by default, because it's not compatible with automatic context loading. If you set a lower [autonomy level](./autonomy.md), you can use background tasks.\n\n## Running a Task in the Background\n\nTo run a task in the background, use the `--bg` flag with `plandex tell` or `plandex continue`.\n\n```bash\nplandex tell --bg \"Add an update credit card form to 'src/components/billing'\"\nplandex continue --bg\n```\n\nThe plandex stream TUI also has a `b` hotkey that allows you to send a streaming plan to the background.\n\n## Listing Background Tasks\n\nTo list active and recently finished background tasks, use the `plandex ps` command:\n\n```bash\nplandex ps\n```\n\n## Connecting to a Background Task\n\nTo connect to a running background task and view its stream in the plan stream TUI, use the `plandex connect` command:\n\n```bash\nplandex connect\n```\n\n## Stopping a Background Task\n\nTo stop a running background task, use the `plandex stop` command:\n\n```bash\nplandex stop\n```\n"
  },
  {
    "path": "docs/docs/core-concepts/branches.md",
    "content": "---\nsidebar_position: 8\nsidebar_label: Branches\n---\n\n# Branches\n\nBranches in Plandex allow you to easily try out multiple approaches to a task and see which gives you the best results. They work in conjunction with [version control](./version-control.md). Use cases include:\n\n- Comparing different prompting strategies.\n- Comparing results with different files in context.\n- Comparing results with different models or model-settings.\n- Using `plandex rewind` without losing history (first check out a new branch, then rewind).\n\n## Creating a Branch\n\nTo create a new branch, use the `plandex checkout` command:\n\n```bash\nplandex checkout new-branch\npdxd new-branch # alias\n```\n\n## Switching Branches\n\nTo switch to a different branch, also use the `plandex checkout` command:\n\n```bash\nplandex checkout existing-branch\n```\n\n## Listing Branches\n\nTo list all branches, use the `plandex branches` command:\n\n```bash\nplandex branches\n```\n\n## Deleting a Branch\n\nTo delete a branch, use the `plandex delete-branch` command:\n\n```bash\nplandex delete-branch branch-name\n```\n"
  },
  {
    "path": "docs/docs/core-concepts/configuration.md",
    "content": "---\nsidebar_position: 11\nsidebar_label: Configuration\n---\n\n# Configuration\n\nPlandex provides a flexible configuration system that lets you customize its behavior based on the task you're working on and your preferences.\n\n## Viewing Config\n\n```bash\nplandex config           # View current plan's configuration\nplandex config default   # View default configuration for new plans\n```\n\n## Modifying Config\n\n```bash\nplandex set-config                       # Select from a list of options\nplandex set-config auto-apply true       # Set a specific option\nplandex set-config default auto-mode basic   # Set default for new plans\n```\n\n## Key Settings\n\n### Autonomy Level\n\nAutonomy settings control the overall level of automation Plandex will use. See the [Autonomy](./autonomy.md) page for more details and shortcuts for setting autonomy levels.\n\n| Setting               | Description                                                      | Default |\n| --------------------- | ---------------------------------------------------------------- | ------- |\n| `auto-mode`           | Overall autonomy level (`none`, `basic`, `plus`, `semi`, `full`) | `semi` |\n\n### Plan Control\n\n| Setting               | Description                                                      | Default |\n| --------------------- | ---------------------------------------------------------------- | ------- |\n| `auto-continue`       | Continue plans until completion                                  | `true`  |\n| `auto-build`          | Build changes into pending updates                               | `true`  |\n| `auto-apply`          | Apply changes to project files                                   | `false` |\n\n### Context Management\n\n| Setting                 | Description                              | Default |\n| ----------------------- | ---------------------------------------- | ------- |\n| `auto-update-context` | Update context when files change           | `true`  |\n| `auto-load-context`     | Load context using project map           | `true`  |\n| `smart-context`         | Load only necessary files for each step  | `true`  |\n\n### Execution\n\n| Setting                 | Description                              | Default |\n| ----------------------- | ---------------------------------------- | ------- |\n| `can-exec`              | Allow command execution (safety setting) | `true`  |\n| `auto-exec`             | Automatically execute commands           | `true` |\n| `auto-debug`            | Automatically debug commands             | `false` |\n| `auto-debug-tries`      | Number of tries for automatic debugging  | `5`     |\n\n### Version Control\n\n| Setting                 | Description                              | Default |\n| ----------------------- | ---------------------------------------- | ------- |\n| `auto-commit`           | Commit changes to git when applied       | `true` |\n| `auto-revert-on-rewind` | Revert project files when rewinding      | `true`  |\n\n### Changes Menu\n\n| Setting                 | Description                              | Default |\n| ----------------------- | ---------------------------------------- | ------- |\n| `skip-changes-menu`     | Skip interactive menu when response finishes and changes are pending | `false` |\n\n\n\n### Editor\n\n| Setting                 | Description                              | Default |\n| ----------------------- | ---------------------------------------- | ------- |\n| `editor`                | Editor to use for editing files          |   |\n\n\n\n\n## Command Line Overrides\n\nMany settings can be overridden with command line flags:\n\n```bash\n# this will apply changes, automatically execute commands, and automatically debug regardless of your autonomy level and config settings\nplandex tell \"add a feature\" --apply --auto-exec --debug\n```\n\nThese overrides apply only to the current command execution and don't change the saved configuration.\n\nSee the [CLI Reference](../cli-reference.md) for a full list of command line flags for each command.\n\n## REPL Commands\n\n```\n\\config                # View current plan config\n\\config default        # View default config\n\\set-config            # Modify current plan config\n\\set-config default    # Modify default config for new plans\n\\set-auto              # Set autonomy level\n\\set-auto default      # Set default autonomy level for new plans\n```\n"
  },
  {
    "path": "docs/docs/core-concepts/context-management.md",
    "content": "---\nsidebar_position: 3\nsidebar_label: Context\n---\n\n# Context Management\n\nContext in Plandex refers to files, directories, URLs, images, notes, or piped in data that the LLM uses to understand and work on your project. Context is always associated with a [plan](./plans.md).\n\nChanges to context are [version controlled](./version-control.md) and can be [branched](./branches.md).\n\n## Automatic vs. Manual\n\nAs of v2, Plandex loads context automatically by default. When a new plan is created, a [project map](#loading-project-maps) is generated and loaded into context. The LLM then uses this map to select relevant context before planning a task or responding to a message.\n\n### Tradeoffs\n\nAutomatic context loading makes Plandex more powerful and easier to work with, but there are tradeoffs in terms of cost, focus, and output speed. If you're trying to minimize costs or you know for sure that only one or two files are relevant to a task, you might prefer to load context manually.\n\n### Setting Manual Mode\n\nYou can use manual context loading by:\n\n- Using `set-auto` to choose a lower [autonomy level](./autonomy.md) that has auto-load-context disabled (like `plus` or `basic`).\n\n```bash\nplandex set-auto plus\nplandex set-auto basic\nplandex set-auto default plus # set the default value for all new plans\n```\n\n- Starting a new REPL or a new plan with the `--plus` or `--basic` flags, which will automatically set the config option to the chosen autonomy level.\n\n```bash\nplandex --plus\nplandex new --basic\n```\n\n- Setting the `auto-load-context` [config option](./configuration.md) to `false`:\n\n```bash\nplandex set-config auto-load-context false\nplandex set-config default auto-load-context false # set the default value for all new plans\n```\n\n### Smart Context Window Management\n\nAnother new context management feature in v2 is smart context window management. When making a plan with multiple steps, Plandex will determine which files are relevant to each step. Only those files will be loaded into context during implementation.\n\nWhen combined with automatic context loading, this effectively creates a sliding context window that grows and shrinks as needed throughout the plan.\n\nSmart context can also be used when you're managing context manually. To give an example: say you've manually loaded a directory with 10 files in it, and you need to make some updates to each one of them. Without smart context, each step of the implementation will load all 10 files into context. But if you use smart context, only the one or two files that are edited in each step will be loaded.\n\nSmart context is enabled in the `plus` autonomy level and above. You can also toggle it with `set-config`:\n\n```bash\nplandex set-config smart-context true\nplandex set-config smart-context false\nplandex set-config default smart-context false # set the default value for all new plans\n```\n\n### Automatic Context Updates\n\nWhen you make your own changes to files in context separately from Plandex, those files need to be updated before the plan can continue. Previously, Plandex would prompt you to update context every time a file was changed. This is now automatic by default.\n\nAutomatic updates are enabled in the `plus` autonomy level and above. You can also toggle them with `set-config`:\n\n```bash\nplandex set-config auto-update-context true\nplandex set-config auto-update-context false\nplandex set-config default auto-update-context false # set the default value for all new plans\n```\n\n### Autonomy Matrix\n\nHere are the different autonomy levels as they relate to context management config options:\n\n|                       | `none` | `basic` | `plus` | `semi` | `full` |\n| --------------------- | ------ | ------- | ------ | ------ | ------ |\n| `auto-load-context`   | ❌     | ❌      | ❌     | ✅     | ✅     |\n| `smart-context`       | ❌     | ❌      | ✅     | ✅     | ✅     |\n| `auto-update-context` | ❌     | ❌      | ✅     | ✅     | ✅     |\n\n### Mixing Automatic and Manual Context\n\nYou can manually load additional context even if automatic loading is enabled. The way this additional context is handled works somewhat differently.\n\nFirst, consider how automatic context loading works across each stage of a plan:\n\n#### Automatic context loading (no manual context added)\n\n1. **Context loading:** Only the project map is initially loaded. The map, along with your prompt, is used to select relevant context.\n2. **Planning:** Only context selected in step 1 is loaded.\n3. **Implementation:** Smart context (if enabled) filters context again, loading only what's directly relevant to each step.\n\nHere's how it changes when you load manual context on top:\n\n#### Automatic loading + manual context\n\n1. **Context loading:** Your manually loaded context is **always included** alongside the project map.\n2. **Planning:** Manually loaded context is always loaded, whether or not it's selected by the map-based context selection step.\n3. **Implementation:** Smart context (if enabled) filters all context again (both manual and automatic), loading only what's directly relevant to each implementation step.\n\nLoading files manually when using automatic context loading can sometimes be useful when you **know** certain files are relevant and don't want to risk the LLM leaving them out, or when the LLM is struggling to select the right context. If there are files that can help the LLM select the right context, like READMEs or documentation that describes the structure of the project, those can also be good candidates for manual loading.\n\nAnother use for manual context loading is for context types that can't be loaded automatically, like URLs, notes, or piped data (for now Plandex can only automatically load project files and images within the project).\n\n## Manually Loading Context\n\nTo load files, directories, directory layouts, urls, images, notes, or piped data into a plan's context, use the `plandex load` command.\n\n### Loading Files\n\nYou can pass `load` one or more file paths. File paths are relative to the current directory in your terminal.\n\n```bash\nplandex load component.ts # single file\nplandex load component.ts action.ts reducer.ts # multiple files\npdx l component.ts # alias\n```\n\nYou can also load multiple files using glob patterns:\n\n```bash\nplandex load tests/**/*.ts # loads all .ts files in 'tests' and its subdirectories\nplandex load * # loads all files in the current directory\n```\n\nYou can load context from parent or sibling directories if needed by using `..` in your load paths.\n\n```bash\nplandex load ../file.go # loads file.go from parent directory\nplandex load ../sibling-dir/test.go # loads test.go from sibling directory\n```\n\n### Loading Directories\n\nYou can load an entire directory with the `--recursive/-r` flag:\n\n```bash\nplandex load lib -r # loads lib, all its files and all its subdirectories\nplandex load * -r # loads all files in the current directory and all its subdirectories\n```\n\n### Loading Files and Directories in the REPL\n\nIn the [Plandex REPL](../repl.md), you can use the shortcut `@` plus a relative file path to load a file or directory into context.\n\n```bash\n@component.ts # loads component.ts\n@lib # loads lib directory, and all its files and subdirectories\n```\n\n### Loading Directory Layouts\n\nThere are tasks where it's helpful for the LLM to the know the structure of your project or sections of your project, but it doesn't necessarily need to the see the content of every file. In that case, you can pass in a directory with the `--tree` flag to load in the directory layout. It will include just the names of all included files and subdirectories (and each subdirectory's files and subdirectories, and so on).\n\n```bash\nplandex load . --tree # loads the layout of the current directory and its subdirectories (file names only)\nplandex load src/components --tree # loads the layout of the src/components directory\n```\n\n### Loading Project Maps\n\nPlandex can create a **project map** for any directory using [tree-sitter](https://tree-sitter.github.io/tree-sitter). This shows all the top-level symbols, like variables, functions, classes, etc. in each file. 30+ languages are supported. For non-supported languages, files are still listed without symbols so that the model is aware of their existence.\n\nMaps are mainly used for selecting context during automatic context loading, but can also be used with manual context management in order to improve output. Maps make it much more likely that an LLM will, for example, use an existing function in your project (and call it correctly) rather than generating a new one that does the same thing.\n\n```bash\nplandex load . --map\n```\n\n### Loading URLs\n\nPlandex can load the text content of URLs, which can be useful for adding relevant documentation, blog posts, discussions, and the like.\n\n```bash\nplandex load https://redux.js.org/usage/writing-tests # loads the text-only content of the url\n```\n\n### Loading Images\n\nPlandex can load images into context.\n\n```bash\nplandex load ui-mockup.png\n```\n\nFor most models that support images, png, jpeg, non-animated gif, and webp formats are supported. Some models may support fewer or additional formats.\n\n### Loading Notes\n\nYou can add notes to context, which are just simple strings.\n\n```bash\nplandex load -n 'add logging statements to all the code you generate.' # load a note into context\n```\n\nNotes can be useful as 'sticky' explanations or instructions that will tend to have more prominence throughout a long conversation than normal prompts. That's because long conversations are summarized to stay below a token limit, which can cause some details from your prompts to be dropped along the way. This doesn't happen if you use notes.\n\n### Piping Into Context\n\nYou can pipe the results of other commands into context:\n\n```bash\nnpm test | plandex load # loads the output of `npm test`\n```\n\n### Ignoring files\n\nIf you're in a git repo, Plandex respects `.gitignore` and won't load any files that you're ignoring. You can also add a `.plandexignore` file with ignore patterns to any directory.\n\nYou can force Plandex to load ignored files with the `--force/-f` flag:\n\n```bash\nplandex load .env --force # loads the .env file even if it's in .gitignore or .plandexignore\n```\n\n## Viewing Context\n\nTo list everything in context, use the `plandex ls` command:\n\n```bash\nplandex ls\n```\n\nYou can also see the content of any context item with the `plandex show` command:\n\n```bash\nplandex show component.ts # show the content of component.ts\nplandex show 2 # show the content of the second item in the `plandex ls` list\n```\n\n## Removing Context\n\nTo remove selectively remove context, use the `plandex rm` command:\n\n```bash\nplandex rm component.ts # remove by name\nplandex rm 2 # remove by number in the `plandex ls` list\nplandex rm 2-5 # remove a range of indices\nplandex rm lib/**/*.js # remove by glob pattern\nplandex rm lib # remove whole directory\n```\n\n## Clearing Context\n\nTo clear all context, use the `plandex clear` command:\n\n```bash\nplandex clear\n```\n\n## Updating Context\n\nIf files, directory layouts, or URLs in context are modified outside of Plandex, they will need to be updated next time you send a prompt.\n\nWhether they'll be updated automatically or you'll be prompted to update them depends on the `auto-update-context` config option.\n\nYou can also update any outdated files with the `update` command.\n\n```bash\nplandex update # update files in context\n```\n"
  },
  {
    "path": "docs/docs/core-concepts/conversations.md",
    "content": "---\nsidebar_position: 9\nsidebar_label: Conversations\n---\n\n# Conversations\n\nEach time you send a prompt to Plandex or Plandex responds, the plan's **conversation** is updated. Conversations are [version controlled](./version-control.md) and can be [branched](./branches.md).\n\n## Conversation History\n\nYou can see the full conversation history with the `convo` command.\n\n```bash\nplandex convo # show the full conversation history\n```\n\nYou can output the conversation in plain text with no ANSI codes with the `--plain` or `-p` flag.\n\n```bash\nplandex convo --plain\n```\n\nYou can also show a specific message number or range of messages.\n\n```bash\nplandex convo 1 # show the initial prompt\nplandex convo 1-5 # show messages 1 through 5\nplandex convo 2- # show messages 2 through the end of the conversation\n```\n\n## Conversation Summaries\n\nEvery time the AI model replies, Plandex will summarize the conversation so far in the background and store the summary in case it's needed later. When the conversation size in tokens exceeds the model's limit, Plandex will automatically replace some number of older messages with the corresponding summary. It will summarize as many messages as necessary to keep the conversation size under the limit.\n\nSummaries are also used by the model as a form of working memory to keep track of the state of the plan—what's been implemented and what remains to be done.\n\nYou can see the latest summary with the `summary` command.\n\n```bash\nplandex summary # show the latest conversation summary\n```\n\nAs with the `convo` command, you can output the summary in plain text with no ANSI codes with the `--plain` or `-p` flag.\n\n```bash\nplandex summary --plain\n```\n"
  },
  {
    "path": "docs/docs/core-concepts/execution-and-debugging.md",
    "content": "---\nsidebar_position: 6\nsidebar_label: Execution and Debugging\n---\n\n# Execution and Debugging\n\nPlandex includes command execution and automated debugging capabilities that aim to balance power, control, and safety.\n\n## Command Execution\n\nDuring a plan, apart from making changes to files, Plandex can write to a special path, `_apply.sh`, with any commands required to complete the plan. This commonly includes installing dependencies, running tests, building and running code, starting servers, and so on.\n\nCommands accumulate in the sandbox just like [pending changes to files](./reviewing-changes.md). If execution fails, you can roll back changes and optionally send the output to the model for automated debugging and retries.\n\nWhile Plandex will attempt to automatically infer relevant commands to run, it also tries not to overdo it. It generally won't, for example, run a test suite unless you've specifically asked it to, since it may not be desirable after every change. It tries to only run what's strictly necessary, to make local project-level changes instead of global system-wide changes, to check existing dependencies before installing, to write idempotent commands, to avoid hiding output or asking for user input, and to recover gracefully from failures.\n\nIf you want specific commands to run and Plandex isn't including automatically them because of it's somewhat conservative approach, you can either mention them in the prompt, or you can use the `plandex debug` command to automatically debug based on the output of any command you choose.\n\n### Debugging Browser Applications\n\nIf Chrome is installed, Plandex can automatically debug browser applications by catching errors and reading the console logs.\n\nIf a plan calls for starting a browser, Plandex will automatically include a `plandex browser` call to start Chrome and monitor for errors.\n   \n### Execution Config\n\nControl whether Plandex can execute commands:\n\n```bash\nplandex set-config can-exec true  # Allow command execution (default)\nplandex set-config can-exec false # Disable command execution\n```\n\nIf you toggle `can-exec` to `false`, Plandex will completely skip writing any commands to `_apply.sh`.\n\nControl whether commands are executed automatically after applying changes (be careful with this):\n\n```bash\nplandex set-config auto-exec true  # Auto-execute commands\nplandex set-config auto-exec false # Prompt before executing (default)\n```\n\n## Automated Debugging\n\nThe `plandex debug` command repeatedly runs a terminal command, making fixes until it succeeds:\n\n```bash\nplandex debug 'npm test'  # Try up to 5 times (default)\nplandex debug 10 'npm test'  # Try up to 10 times\n```\n\nThis will:\n\n1. Run the command and check for success/failure\n2. If it fails, send the output to the LLM\n3. Tentatively apply suggested fixes to your project files\n4. If command is succesful after fixes, commit changes (if auto-commit is enabled). Otherwise, roll back changes and return to step 2.\n5. Repeat until success or max tries reached\n\nYou can configure the default number of tries:\n\n```bash\nplandex set-config auto-debug-tries 10  # Set default to 10 tries\n```\n\n## Common Debugging Workflows\n\n### Fixing Failing Tests\n\n```bash\nplandex debug 'npm test'\nplandex debug 'go test ./...'\nplandex debug 'pytest'\n```\n\n### Fixing Build Errors\n\n```bash\nplandex debug 'npm run build'\nplandex debug 'go build'\nplandex debug 'cargo build'\n```\n\n### Fixing Linting Errors\n\n```bash\nplandex debug 'npm run lint'\nplandex debug 'golangci-lint run'\n```\n\n### Fixing Type Errors\n\n```bash\nplandex debug 'npm run typecheck'\nplandex debug 'tsc --noEmit'\n```\n\n## A Manual Alternative\n\nFor a less automated approach, you can pipe the output of a command into `plandex chat` or `plandex tell`.\n\n```bash\nnpm test | plandex tell 'npm test output'\ngo build | plandex chat 'what could be causing these type errors?'\n```\n\nThis works similarly to `plandex debug` but without automatically applying changes and retrying.\n\nNote that piping output into a prompt requires using the CLI directly in the terminal. You can't do it from inside the [REPL](../repl.md).\n\n## Autonomy Matrix\n\nExecution and debugging behavior is affected by your [autonomy level](./autonomy.md):\n\n| Setting      | None | Basic | Plus | Semi | Full |\n| ------------ | ---- | ----- | ---- | ---- | ---- |\n| `can-exec`   | ❌   | ❌    | ✅   | ✅   | ✅   |\n| `auto-exec`  | ❌   | ❌    | ❌   | ❌   | ✅   |\n| `auto-debug` | ❌   | ❌    | ❌   | ❌   | ✅   |\n\n\nWith `full` autonomy, commands are automatically executed and debugged after changes are applied. For other levels, you'll be prompted to approve execution and debugging steps.\n\n## Safety\n\nNeedless to say, you should be extremely careful when using full auto mode, `auto-exec`, `auto-debug`, and the `debug` command. They can make many changes quickly without any prompting or review, and can run commands that could potentially be destructive to your system. While the best LLMs are quite trustworthy when it comes to running commands and are unlikely to cause harm, it still pays to be cautious.\n\nIt's a good idea to make sure your git state is clean, and to check out an isolated branch before using these features.\n"
  },
  {
    "path": "docs/docs/core-concepts/orgs.md",
    "content": "---\nsidebar_position: 13\nsidebar_label: Collaboration / Orgs\n---\n\n# Collaboration and Orgs\n\nWhile so far Plandex is mainly focused on a single-user experience, we plan to add features for sharing, collaboration, and team management in the future, and some groundwork has already been done. **Orgs** are the basis for collaboration in Plandex.\n\n## Multiple Users\n\nOrgs are helpful already if you have multiple users using Plandex in the same project. Because Plandex outputs a `.plandex` file containing a bit of non-sensitive config data in each directory a plan is created in, you'll have problems with multiple users unless you either get each user into the same org or put `.plandex` in your `.gitignore` file. Otherwise, each user will overwrite other users' `.plandex` files on every push, and no one will be happy.\n\n## Domain Access\n\nWhen starting out with Plandex and creating a new org, you have the option of automatically granting access to anyone with an email address on your domain.\n\n## Invitations\n\nIf you choose not to grant access to your whole domain, or you want to invite someone from outside your email domain, you can use `plandex invite`:\n\n```bash\nplandex invite\n```\n\n## Joining an Org\n\nTo join an org you've been invited to, use `plandex sign-in`:\n\n```bash\nplandex sign-in\n```\n\n## Listing Users and Invites\n\nTo list users and pending invites, use `plandex users`:\n\n```bash\nplandex users\n```\n\n## Revoking Users and Invites\n\nTo revoke an invite or remove a user, use `plandex revoke`:\n\n```bash\nplandex revoke\n```\n"
  },
  {
    "path": "docs/docs/core-concepts/plans.md",
    "content": "---\nsidebar_position: 1\nsidebar_label: Plans\n---\n\n# Plans\n\nA **plan** in Plandex is similar to a conversation in ChatGPT or Claude. It might only include a single prompt and model response that executes one small task, or it could represent a long back and forth with the model that generates dozens of files and builds a whole feature or an entire project.\n\nA plan includes:\n\n- Any [context](./context-management.md) that you or the model have loaded.\n- Your [conversation](./conversations.md) with the model.\n- Any [pending changes](./reviewing-changes.md) that have been accumulated during the course of the conversation.\n\nPlans support [version control](./version-control.md) and [branches](./branches.md).\n\n## Creating a New Plan\n\nFirst `cd` into your **project's directory.** Make a new directory first with `mkdir your-project-dir` if you're starting on a new project.\n\n```bash\ncd your-project-dir\n```\n\n### REPL\n\nTo start a new plan with the REPL, just run:\n\n```bash\nplandex\n```\n\nIf you haven't created a new plan in this directory previously, a plan will automatically be created for you when the REPL starts.\n\nIf already have a plan loaded in the REPL (you can check with `\\current`), you can start a new plan with `\\new`.\n\n### CLI\n\nYou can create a new plan through the CLI with `plandex new`.\n\n```bash\nplandex new\n```\n\n## Plan Names and Drafts\n\nWhen you create a plan, Plandex will automatically name your plan after you send the first prompt, but you can also give it a name up front.\n\n```bash\nplandex new -n foo-adapters-component\n```\n\nIf you don't give your plan a name up front, it will be named `draft` until you send an initial prompt. To keep things tidy, you can only have one active plan named `draft`. If you create a new draft plan, any existing draft plan will be removed.\n\n## Listing Plans\n\nWhen you have multiple plans, you can list them with the `plans` command.\n\n```bash\nplandex plans\n```\n\n## The Current Plan\n\nIt's important to know what the **current plan** is for any given directory, since most Plandex commands are executed against that plan.\n\nTo check the current plan:\n\n```bash\nplandex current\n```\n\nYou can change the current plan with the `cd` command:\n\n```\nplandex cd # select from a list of plans\nplandex cd some-other-plan # cd to a plan by name\nplandex cd 2 # cd to a plan by number in the `plandex plans` list\n```\n\n## Deleting Plans\n\nYou can delete a plan with the `delete-plan` command:\n\n```bash\nplandex delete-plan # select from a list of plans to delete\nplandex delete-plan some-plan # delete a plan by name\nplandex delete-plan 4 # delete a plan by number in the `plandex plans` list\n```\n\n## Archiving Plans\n\nYou can archive plans you want to keep around but aren't currently working on with the `archive` command. You can see archived plans in the current directory with `plans --archived`. You can unarchive a plan with the `unarchive` command.\n\n```bash\nplandex archive # select from a list of plans to archive\nplandex archive some-plan # archive a plan by name\nplandex archive 2 # archive a plan by number in the `plandex plans` list\n\nplandex unarchive # select from a list of archived plans to unarchive\nplandex unarchive some-plan # unarchive a plan by name\nplandex unarchive 2 # unarchive a plan by number in the `plandex plans --archived` list\n```\n\n## .plandex Directory\n\nWhen you run `plandex` (for a REPL) or `plandex new` for the first time in any directory, Plandex will create a `.plandex` directory there for light project-level config.\n\nIf multiple people are using Plandex with the same project, you should either:\n\n- **Commit** the `.plandex` directory and get everyone into the same [org](./orgs.md) in Plandex.\n- Put `.plandex/` in `.gitignore`\n\n## Project Directories\n\nSo far, we've assumed you're running `plandex` or `plandex new` to create plans in your project's root directory. While that is the most common use case, it can be useful to create plans in subdirectories of your project too.\n\nThat's because context file paths in Plandex are specified relative to the directory where the plan was created. So if you're working on a plan for just one part of your project, you might want to create the plan in a subdirectory in order to shorten paths when loading context or referencing files in your prompts.\n\nStarting a plan (or REPL) in a subdirectory is also helpful when using [automatic context loading](./context-management.md#automatic-vs-manual) to limit the size of the project map and what files are available for the LLM to load.\n\nIt can also help with plan organization if you have a lot of plans.\n\nWhen you run `plandex plans`, in addition to showing you plans in the current directory, Plandex will also show you plans in nearby parent directories or subdirectories. This helps you keep track of what plans you're working on and where they are in your project hierarchy. If you want to switch to a plan in a different directory, first `cd` into that directory, then run `plandex cd` to select the plan.\n"
  },
  {
    "path": "docs/docs/core-concepts/prompts.md",
    "content": "---\nsidebar_position: 4\nsidebar_label: Prompts\n---\n\n# Prompts\n\n## Sending Prompts\n\n### In the REPL\n\nTo send a prompt in the [REPL](../repl.md), just type your prompt and press enter.\n\nYou can also use `\\multi` to enable multi-line mode. This will cause enter to produce line breaks. In multi-line mode, you can send your prompt with `\\send`.\n\nIf you want to pass in a file as a prompt in the REPL, you can use the `\\run` command with a relative file path:\n\n```\n\\run src/components/foobars-form.tsx\n```\n\n### With the CLI\n\nTo send a prompt with the CLI, use the `plandex tell` command for a task, or the `plandex chat` command to brainstorm or ask questions.\n\nYou can pass it in as a file with the `--file/-f` flag:\n\n```bash\nplandex tell -f prompt.txt\nplandex chat -f prompt.txt\n```\n\nWrite it in vim:\n\n```bash\nplandex tell # tell with no arguments opens vim so you can write your prompt there\nplandex chat # chat with no arguments does the same\n```\n\nPass it inline (use enter for line breaks):\n\n```bash\nplandex tell \"add a new line chart showing the number of foobars over time to components/charts.tsx\"\nplandex chat \"where's the database connection logic in this project?\"\n```\n\nYou can also pipe in the results of another command:\n\n```bash\ngit diff | plandex tell\ngit diff | plandex chat\n```\n\nWhen you pipe in results like this, you can also supply an inline string to give a label or additional context to the results:\n\n```bash\ngit diff | plandex tell \"'git diff' output\"\n```\n\n## Plan Stream TUI\n\nAfter you send a prompt with the REPL, `plandex tell`, or `plandex chat`, you'll see the plan stream TUI. The model's responses are streamed here. You'll see several hotkeys listed along the bottom row that allow you to stop the plan (s), send the plan to the background (b), scroll/page the streamed text, or jump to the beginning or end of the stream. If you're a vim user, you'll notice Plandex's scrolling hotkeys are the same as vim's.\n\nNote that scrolling the terminal window itself won't work while you're in the stream TUI. Use the scroll hotkeys instead.\n\n## Task Prompts\n\nWhen you give Plandex a task, it will first break down the task into steps, then it will proceed to implement each step in code. Plandex will automatically continue sending model requests until the task is determined to be complete.\n\n## Chat Prompts\n\nIf you want to ask Plandex questions or chat without generating files or making changes, use the `plandex chat` command instead of `plandex tell`.\n\n```bash\nplandex chat \"explain every function in lib/math.ts\"\n```\n\nPlandex will reply with just a single response, won't create or update any files, and won't automatically continue.\n\n`plandex chat` has the same options for passing in a prompt as `plandex tell`. You can pass a string inline, give it a file with `--file/-f`, type the prompt in vim by running `plandex chat` with no arguments, or pipe in the results of another command.\n\n### In the REPL\n\nIn the REPL, you can control whether prompts are sent to `plandex tell` or `plandex chat` under the hood by toggling `chat mode` with `\\chat (\\ch)` or `\\tell (\\t)`.\n\n## Stopping and Continuing\n\nWhen using `plandex tell`, you can prevent Plandex from automatically continuing for multiple responses by passing the `--stop/-s` flag:\n\n```bash\nplandex tell -s \"write tests for the charting helpers in lib/chart-helpers.ts\"\n```\n\nPlandex will then reply with just a single response. From there, you can continue if desired with the `continue` command. Like `tell`, `continue` can also accept a `--stop/-s` flag. Without the `--stop/-s` flag, `continue` will also cause Plandex to continue automatically until the task is done. If you pass the `--stop/-s` flag, it will continue for just one more response.\n\n```bash\nplandex continue -s\n```\n\nApart from `--stop/-s` Plandex's plan stream TUI also has an `s` hotkey that allows you to immediately stop a plan.\n\nYou can also stop a plan from automatically continuing by setting the `auto-continue` config option to `false` in a plan's [configuration](./configuration.md):\n\n```bash\nplandex set-config auto-continue false\nplandex set-config default auto-continue false # set the default config's auto-continue to false for all new plans\n```\n\nor by setting the `auto-mode` ([autonomy level](./autonomy.md)) to `none`:\n\n```bash\nplandex set-auto none\nplandex set-default-auto none # set the default auto-mode to none for all new plans\n```\n\n## Background Tasks\n\nBy default, `plandex tell` opens the plan stream TUI and streams Plandex's response(s) there, but you can also pass the `--bg` flag to run a task in the background instead.\n\nYou can learn more about using and interacting with background tasks [here](./background-tasks.md).\n\n## Keeping Context Updated\n\nWhen you send a prompt, whether through `plandex tell` or `plandex chat`, Plandex will check whether the content of any files, directory layouts, or URLs you've loaded into [context](./context-management.md) have changed. If so, you'll need to update the context before continuing.\n\nBy default, Plandex will update any outdated context automatically, but if you'd rather approve these updates, you can set the `auto-update-context` config option to `false`:\n\n```bash\nplandex set-config auto-update-context false\nplandex set-config default auto-update-context false # set the default config's auto-update-context to false for all new plans\n```\n\nor you can set the `auto-mode` to `basic` or `none`:\n\n```bash\nplandex set-auto basic\nplandex set-auto none\n```\n\n## Building Files\n\nAs Plandex implements your task, files it creates or updates will appear in the `Building Plan` section of the plan stream TUI. Plandex will **build** all changes proposed by the plan into a set of pending changesets for each affected file.\n\nBy default, these changes initially **will not** be directly applied to your project files. Instead, they will be **pending** in Plandex's version-controlled sandbox.\n\nThis allows you to review the proposed changes or continue iterating and accumulating more changes. You can view the pending changes with `plandex diff` (for git diff format in the terminal) or `plandex diff --ui` (to view them in a local browser UI). Once you're happy with the changes, you can apply them to your project files with `plandex apply`.\n\n- [Learn more about reviewing changes.](./reviewing-changes.md)\n- [Learn more about version control.](./version-control.md)\n\n### Full auto mode\n\nAn important caveat to the above: if you set the `auto-mode` to `full`, Plandex _will_ automatically apply the changes to your project files,\n\n### Skipping builds / `plandex build`\n\nYou can skip building files when you send a prompt by passing the `--no-build` flag to `plandex tell` or `plandex continue`. This can be useful if you want to ensure that a plan is on the right track before building files.\n\n```bash\nplandex tell \"implement sign up and sign in forms in src/components\" --no-build\n```\n\nYou can later build any changes that were implemented in the plan with the `plandex build` command:\n\n```bash\nplandex build\n```\n\nThis will show a smaller version of the plan stream TUI that only includes the `Building Plan` section.\n\nLike full plan streams, build streams can be stopped with the `s` hotkey or sent to the background with the `b` hotkey. They can also be run fully in the background with the `--bg` flag:\n\n```bash\nplandex build --bg\n```\n\nThere's one more thing to keep in mind about builds. If you send a prompt with the `--no-build` flag:\n\n```bash\nplandex tell \"implement a forgot password email in src/emails\" --no-build\n```\n\nThen you later send _another_ prompt with `plandex tell` or continue the plan with `plandex continue` and you _don't_ include the `--no-build` flag, any changes that were implemented previously but weren't built will immediately start building when the plan stream begins.\n\n```bash\nplandex tell \"now implement the UI portion of the forgot password flow\"\n# the above will start building the changes proposed in the earlier prompt that was passed --no-build\n```\n\n## Automatically Applying Changes\n\nIf you want Plandex to _automatically_ apply changes when a plan is complete, you can pass the `--apply/-a` flag to `plandex tell`, `plandex continue`, or `plandex build`:\n\n```bash\nplandex tell \"add a new route for updating notification settings to src/routes.ts\" --apply\n```\n\nThe `--apply/-a` flag will also automatically update context if needed, just as the `--yes/-y` flag does.\n\nWhen passing `--apply/-a`, you can also use the `--commit/-c` flag to commit the changes to git with an auto-generated commit message. This will only commit the specific changes that were made by the plan. Any other changes in your git repository, staged or unstaged, will remain as they are.\n\n```bash\nplandex tell \"add a new route for updating notification settings to src/routes.ts\" --apply --commit\n```\n\n## Iterating on a Plan\n\nIf you send a prompt:\n\n```bash\nplandex tell \"implement a fully working and production-ready tic tac toe game, including a computer-controlled AI, in html, css, and javascript\"\n```\n\nAnd then you want to iterate on it, whether that's to add more functionality or correct something that went off track, you have a couple options.\n\n### Continue the convo\n\nThe most straightforward way to continue iterating is to simply send another `plandex tell` command:\n\n```bash\nplandex tell \"I plan to seek VC funding for this game, so please implement a dark mode toggle and give all buttons subtle gradient fills\"\n```\n\nThis is generally a good approach when you're happy with the current plan and want to extend it to add more functionality.\n\nNote, you can view the full conversation history with the `plandex convo` command:\n\n```bash\nplandex convo\n```\n\n### Rewind and iterate\n\nAnother option is to use Plandex's [version control](./version-control.md) features to rewind to the point just before your prompt was sent and then update it before sending the prompt again.\n\nYou can use `plandex log` to see the plan's history and determine which step to rewind to, then `plandex rewind` with the appropriate hash to rewind to that step:\n\n```bash\nplandex log # see the history\nplandex rewind accfe9 # rewind to right before your prompt\n```\n\nThis approach works well in conjunction with **prompt files**. You write your prompts in files somewhere in your codebase, then pass those to `plandex tell` using the `--file/-f` flag:\n\n```bash\nplandex tell -f prompts/tic-tac-toe.txt\n```\n\nThis makes it easy to continuously iterate on your prompt using `plandex rewind` and `plandex tell` until you get a result that you're happy with.\n\n### Which is better?\n\nThere's not necessarily one right answer on whether to use an ongoing conversation or the `rewind` approach with prompt files for iteration. Here are a few things to consider when making the choice:\n\n- Bad results tend to beget more bad results. Rewinding and iterating on the prompt is often more effective for correcting a wayward task than continuing to send more `tell` commands. Even if you are specifically prompting the model to _correct_ a problem, having the wrong approach in its context will tend to bias it toward additional errors. Using `rewind` to the give the model a clean slate can work better in these scenarios.\n\n- Iterating on a prompt file with the `rewind` approach until you find your way to an effective prompt has another benefit: you can keep the final version of the prompt that produced a given set of changes right alongside the changes themselves in your codebase. This can be helpful for other developers (or your future self) if you want to revisit a task later.\n\n- A downside of the `rewind` approach is that it can involve re-running early steps of a plan over and over, which can be **a lot** more expensive than iterating with additional `tell` commands.\n"
  },
  {
    "path": "docs/docs/core-concepts/reviewing-changes.md",
    "content": "---\nsidebar_position: 5\nsidebar_label: Pending Changes\n---\n\n# Pending Changes\n\nWhen you give Plandex a task, by default the changes aren't applied directly to your project files. Instead, they are accumulated in Plandex's version-controlled **sandbox** so that you can review them first.\n\n## Review Menu\n\nOnce Plandex has finished with a task, you'll see a review menu with several hotkey options. These hotkeys act as shortcuts for the commands described below.\n\n## Viewing Changes\n\n### `plandex diff` / `plandex diff --ui`\n\nWhen Plandex has finished with your task, you can review the proposed changes with the `plandex diff` command, which shows them in `git diff` format:\n\n```bash\nplandex diff\n```\n\n`--plain/-p`: Outputs the diff in plain text with no ANSI codes.\n\nYou can also view the changes in a local browser UI with the `plandex diff --ui` command:\n\n```bash\nplandex diff --ui\n```\n\nThe UI view offers additional options:\n\n- `--side-by-side/-s`: Show diffs in side-by-side view\n- `--line-by-line/-l`: Show diffs in line-by-line view (default)\n\n## Rejecting Files\n\nIf the plan's changes were applied incorrectly to a file, or you don't want to apply them for another reason, you can either [apply the changes](#applying-changes) and then fix the problems manually, _or_ you can reject the updates to that file and then make the proposed changes yourself manually.\n\nTo reject changes to a file (or multiple files), you can run `plandex reject`. You'll be prompted to select which files to reject.\n\n```bash\nplandex reject # select files to reject\n```\n\nYou can reject _all_ currently pending files by passing the `--all` flag to the reject command, or you can pass a list of specific files to reject:\n\n```bash\nplandex reject --all\nplandex reject file1.ts file2.ts\n```\n\nIf you rejected a file due to the changes being applied incorrectly, but you still want to use the code, either scroll up and copy the changes from the plan's output or run `plandex convo` to output the full conversation and copy from there. Then apply the updates to that file yourself.\n\n## Applying Changes\n\nOnce you're happy with the plan's changes, you can apply them to your project files with `plandex apply`:\n\n```bash\nplandex apply\n```\n\n### Apply Flags & Config\n\nPlandex v2 introduced several [new config settings and flags](./configuration.md) for the `apply` command that give you control over what happens after changes are applied.\n\n### Command Execution & Debugging\n\nAfter applying changes, Plandex can automatically execute pending commands. This is useful for running tests, starting servers, or performing other actions that verify the changes work as expected.\n\nIf commands fail, the changes are rolled back. Depending on the autonomy level and config, Plandex will then either attempt to debug automatically or prompt you with debugging options.\n\n## Auto-Applying Changes\n\nWhen `auto-apply` is enabled, Plandex will automatically apply changes after a plan is complete without prompting or review. This is enabled at the `full` [autonomy level](./autonomy.md), and also during auto-debugging.\n\n## Apply + Full Auto\n\nYou can also apply changes and debug in full auto mode with the `--full` flag:\n\n```bash\nplandex apply --full\n```\n\n## Autonomy Matrix\n\n| Setting       | None | Basic | Plus | Semi | Full |\n| ------------- | ---- | ----- | ---- | ---- | ---- |\n| `auto-apply`  | ❌   | ❌    | ❌   | ❌   | ✅   |\n| `auto-exec`   | ❌   | ❌    | ❌   | ❌   | ✅   |\n| `auto-debug`  | ❌   | ❌    | ❌   | ❌   | ✅   |\n| `auto-commit` | ❌   | ❌    | ✅   | ✅   | ✅   |\n"
  },
  {
    "path": "docs/docs/core-concepts/version-control.md",
    "content": "---\nsidebar_position: 7\nsidebar_label: Version Control\n---\n\n# Version Control\n\nJust about every aspect of a Plandex plan is version-controlled, and anything that can happen during a plan creates a new version in the plan's history. This includes:\n\n- Adding, removing, or updating context (when you do it manually or when Plandex does it automatically).\n- When you send a prompt.\n- When Plandex responds.\n- When Plandex builds the plan's proposed updates to a file into a pending change.\n- When pending changes are rejected.\n- When pending changes are applied to your project.\n- When models or model settings are updated.\n\n## Viewing History\n\nTo see the history of your plan, use the `plandex log` command:\n\n```bash\nplandex log\n```\n\n## Rewinding\n\nTo rewind the plan to an earlier state, use the `plandex rewind` command:\n\n```bash\nplandex rewind # Select a previous state to rewind to\nplandex rewind 3  # Rewind 3 steps\nplandex rewind a7c8d66  # Rewind to a specific step\n```\n\n## Preventing History Loss With Branches\n\nNote that currently, there's no way to undo a `rewind` and recover any history that may have been cleared as a result. That said, you can use `rewind` without losing any history with [branches](./branches.md). Use `plandex checkout` to a create a new branch before executing `rewind`, and the original branch will still include the history from before the `rewind`.\n\n```bash\nplandex checkout undo-changes # create a new branch called 'undo-changes'\nplandex rewind ef883a # history is rewound in 'undo-changes' branch\nplandex checkout main # main branch still retains original history\n```\n\n## Viewing Conversation\n\nWhile the Plandex history includes an entry for each message in the conversation, message content isn't included. To see the full conversation history, use the `plandex convo` command:\n\n```bash\nplandex convo\n```\n\n## Rewinding After `plandex apply`\n\nLike any other action that modifies a plan, running `plandex apply` to apply pending changes to your project file creates a new version in the plan's history. The `plandex apply` action can also be undone with `plandex rewind`.\n\nWhile previous versions Plandex would not also revert the changes to your project files, this is now the default behavior as of v2.0.0. If there are potential conflicts (i.e. you've made changes on top since applying), Plandex will prompt you to decide how to handle the conflict.\n\nThis behavior can be disabled if desired by setting the `auto-revert-on-rewind` config setting to `false`:\n\n```bash\nplandex set-config auto-revert-on-rewind false\nplandex set-config default auto-revert-on-rewind false # set the default value for all new plans\n```\n"
  },
  {
    "path": "docs/docs/development.md",
    "content": "---\nsidebar_position: 10\nsidebar_label: Development\n---\n\n# Development\n\nTo set up a development environment, first install dependencies:\n\n- Go 1.23.3 - [install here](https://go.dev/doc/install)\n- [reflex](https://github.com/cespare/reflex) 0.3.1 - for watching files and rebuilding in development. Install with `go install github.com/cespare/reflex@v0.3.1`\n- PostgreSQL 14 - https://www.postgresql.org/download/\n- Python 3 - for LiteLLM passthrough proxy - [install here](https://www.python.org/downloads/)\n\nMake sure `$GOPATH` is in your $PATH\n\n```bash\n# print $GOPATH\necho $GOPATH\n\n# if it's empty\nexport GOPATH=<path-to-go-folder>\n```\n\nMake sure the PostgreSQL server is running and create a database called `plandex`.\n\nThen make sure the following environment variables are set:\n\n```bash\nexport DATABASE_URL=postgres://user:password@host:5432/plandex?sslmode=disable # replace with your own database URL\nexport GOENV=development\n```\n\nNow from the root directory, run the script in `app/scripts/dev.sh`.\n\nOn Linux, you'll want to run this as `sudo` for copying the CLI to `/usr/local/bin` after it builds:\n\n```bash\nsudo ./app/scripts/dev.sh\n```\n\nYou might also need sudo on MacOS if you don't have write permissions to `/usr/local/bin`, but this shouldn't be the case for most users. Assuming you have those write permissions, you can run the script without `sudo`:\n\n```bash\n./app/scripts/dev.sh\n```\n\nThis creates watchers with `reflex` to rebuild both the server and the CLI when relevant files change.\n\nThe server runs on port 8099 by default.\n\nAfter each build, the CLI is copied to `/usr/local/bin/plandex-dev`so you can use it with just `plandex-dev` in any directory. A `pdxd` alias is also created. Note the difference from the `plandex` binary and `pdx` aliases which are installed for production usage—aliases are used for development to avoid overwriting the production install.\n\nThe output directory can be changed with the `PLANDEX_DEV_CLI_OUT_DIR` environment variable. The binary name can be changed with `PLANDEX_DEV_CLI_NAME` and the alias can be changed with `PLANDEX_DEV_CLI_ALIAS`.\n\nWhen running the Plandex CLI, set `export PLANDEX_ENV=development` to run in development mode, which connects to the development server by default.\n"
  },
  {
    "path": "docs/docs/environment-variables.md",
    "content": "---\nsidebar_position: 11\nsidebar_label: Environment Variables\n---\n\n# Environment Variables\n\nThis is an overview of all the environment variables that can be used with Plandex.\n\n## CLI\n\n### General\n\n```bash\nPLANDEX_ENV=development # Set this to 'development' to default to the local development server instead of Plandex Cloud when working on Plandex itself.\nPLANDEX_API_HOST= # Defaults to 'http://localhost:8099' if PLANDEX_ENV is development, otherwise it's 'https://api.plandex.ai'—override this to use a different host.\n```\n\n### LLM Providers\n\n```bash\n# OpenRouter.ai\nOPENROUTER_API_KEY= # Your OpenRouter.ai API key \n\n# OpenAI\nOPENAI_API_KEY= # Your OpenAI key \nOPENAI_ORG_ID= # Your OpenAI organization ID. Defaults to empty.\n\n# Anthropic\nANTHROPIC_API_KEY= # Your Anthropic API key \n\n# Google AI Studio\nGEMINI_API_KEY= # Your Google AI Studio API key \n\n# Google Vertex AI\nGOOGLE_APPLICATION_CREDENTIALS= # Your Google Vertex AI credentials file path\nVERTEXAI_PROJECT= # Your Google Vertex AI project ID\nVERTEXAI_LOCATION= # Your Google Vertex AI location\n\n# Azure OpenAI\nAZURE_OPENAI_API_KEY= # Your Azure OpenAI API key\nAZURE_API_BASE= # Your Azure OpenAI API base URL\nAZURE_API_VERSION= # Your Azure OpenAI API version\nAZURE_DEPLOYMENTS_MAP= # Your Azure OpenAI deployments map—a JSON object mapping model names to deployment names (only needed if deployment names are different from model names)\n\n# DeepSeek\nDEEPSEEK_API_KEY= # Your DeepSeek API key\n\n# Perplexity\nPERPLEXITY_API_KEY= # Your Perplexity API key\n\n# Amazon Bedrock\nPLANDEX_AWS_PROFILE= # Name of AWS profile in ~/.aws/credentials to use for AWS Bedrock. If not set, the credentials file won't be used.\nAWS_ACCESS_KEY_ID= # Your AWS access key ID\nAWS_SECRET_ACCESS_KEY= # Your AWS secret access key\nAWS_REGION= # Your AWS region\nAWS_SESSION_TOKEN= # Your AWS session token\nAWS_INFERENCE_PROFILE_ARN= # Your AWS inference profile ARN\n```\n\n### Upgrades\n\n```bash\nPLANDEX_SKIP_UPGRADE= # Set this to '1' to skip the auto-upgrade check when running the CLI.\n```\n\n### Development\n\nCheck out the [Development Guide](./development.md) for more details.\n\n```bash\nPLANDEX_OUT_DIR=/usr/local/bin # Where the development binary should be output when using dev.sh\nPLANDEX_DEV_CLI_OUT_DIR=/usr/local/bin # Where the development binary should be output when using dev.sh\nPLANDEX_DEV_CLI_NAME=plandex-dev # The name of the development binary when using dev.sh\nPLANDEX_DEV_CLI_ALIAS=pdxd # The alias for the development binary when using dev.sh\nGOPATH= # This should be already set to your Go folder if you've installed Golang.\n```\n\n## Server\n\nCheck out the [Self-Hosting Guide](./hosting/self-hosting/local-mode-quickstart.md) for more details.\n\n### General\n\n```bash\nGOENV=development # Whether to run in development or production mode. Must be 'development' or 'production'\nPLANDEX_BASE_DIR= # The base directory to read and write files. Defaults to '$HOME/plandex-server' in development mode, '/plandex-server' in production.\nAPI_HOST= # The host the API server listens on. Defaults to 'http://localhost:$PORT'. In production mode, should be a host like 'https://api.your-domain.ai'.\nPORT=8099 # The port the server listens on. Defaults to 8099.\nDATABASE_URL= # The URL of the PostgreSQL database. Defaults to 'postgres://plandex:plandex@plandex-postgres:5432/plandex?sslmode=disable' in development mode\nLOCAL_MODE= # Whether to run in local mode\nOLLAMA_BASE_URL= # The base URL of the Ollama server—only need when the server is running in a Docker container and needs to access Ollama models running outside of the container\n```\n\n### docker-compose\n\nFor self-hosting with docker-compose, default values for all necessary environment variables are set in the `app/docker-compose.yml` file. This file is designed to be used with [local mode](./hosting/self-hosting/local-mode-quickstart.md), but you can adapt it to your needs.\n\n### Other methods\n\nIf you're *not* using docker-compose, you'll need a `DATABASE_URL` environment variable that points to a PostgreSQL database. For example, if you're running PostgreSQL locally, you might set it to something like this:\n\n```bash\nDATABASE_URL=postgres://plandex:<password>@<host>:<port>/plandex?sslmode=disable\n```\n\nIf you're running in production mode, you'll also need to set `API_HOST` to the host the API server is running on.\n\n```bash\nAPI_HOST= https://api.your-domain.ai # The host of the API server in production mode. Defaults to 'http://localhost:$PORT' in development mode.\n```\n\n\n### SMTP\n\nIf you're running in production mode (with `GOENV=production`, typically on a remote server), you'll need SMTP credentials:\n\n```bash\nSMTP_HOST= # Your SMTP host.\nSMTP_PORT= # Set this to 1025 e.g. if you are using mailhog.\nSMTP_USER= # SMTP username.\nSMTP_PASSWORD= # SMTP password.\n```\n"
  },
  {
    "path": "docs/docs/hosting/_category_.json",
    "content": "{\n  \"label\": \"Hosting\",\n  \"position\": 7,\n  \"collapsible\": true,\n  \"collapsed\": false\n}"
  },
  {
    "path": "docs/docs/hosting/cloud.md",
    "content": "---\nsidebar_position: 1\nsidebar_label: Cloud\n---\n\n# Plandex Cloud\n\n## Overview\n\n:::info\nPlandex Cloud is winding down as of 10/3/2025 and is no longer accepting new users. <a href=\"https://plandex.ai/blog/winding-down\">Learn more.</a>\n:::\n\nPlandex Cloud is the easiest and most reliable way to use Plandex. You'll be prompted to start a trial when you launch the [REPL](../repl.md) with `plandex` or create your first plan with `plandex new`.\n\n## Billing Modes\n\nPlandex Cloud has two billing modes:\n\n### Integrated Models\n\n- Use Plandex credits to pay for AI models.\n- No separate accounts or API keys are required.\n- Credits are deducted at the model's price from the provider plus a small markup to cover credit card processing costs.\n- Start with a $10 trial (includes $10 in credits).\n- After the trial, you can upgrade to a paid plan for $45 per month—includes $20 in credits every month that never expire.\n\n[Get started with Integrated Models Mode.](https://app.plandex.ai/start?modelsMode=integrated)\n\n### BYO API Key\n\n- Use your own model provider accounts and API keys.\n- Start with a free trial up to 10 plans and 20 model responses per plan.\n- After the trial, you can upgrade to a paid plan for $30 per month.\n\n[Get started with BYO API Key Mode.](https://app.plandex.ai/start?modelsMode=byo)\n\n## Billing Settings\n\nRun `plandex billing` in the terminal to bring up the billing settings page in your default browser, or go to [your Billing Settings page](https://app.plandex.ai/settings/billing) (sign in if necessary).\n\nHere you can switch billing modes, view your current plan, manage your billing details, pause or cancel your subscription and more.\n\n### Integrated Models Mode\n\nIf you're using **Integrated Models Mode**, you can use the billing settings page to view your credits balance, purchase credits, and configure auto-recharge settings to automatically add credits to your account when your balance gets too low. You can also set a monthly budget and an email notification threshold.\n\n### `usage` command\n\nYou can see your current balance and a report on recent usage with `plandex usage` (`\\usage` in the REPL):\n\n```bash\nplandex usage\n```\n\nYou can see a log of individual transactions that includes every model call with `plandex usage --log`:\n\n```bash\nplandex usage --log\n```\n\nIn the Plandex REPL, `usage` defaults to showing usage for the current REPL session. Otherwise, it defaults to showing usage for the day so far.\n\nYou can use the `--today` flag to show usage for the day so far. You can use the `--month` flag to show usage for the current billing month. You can use the `--plan` flag to show usage for the current plan.\n\n```bash\nplandex usage --today # show usage for the day so far\nplandex usage --month # show usage for the current billing month\nplandex usage --plan # show usage for the current plan\n```\n\nYou can use the `--debits` flag to show only debits in the log. You can use the `--purchases` flag to show only purchases in the log.\n\n```bash\nplandex usage --log --debits --month # show only debits for the current billing month\nplandex usage --log --purchases --today # show only purchases for the day so far\n```\n\n## Privacy / Data Retention\n\nData you send to Plandex Cloud is retained in order to debug and improve Plandex. In the future, this data may also be used to train and fine-tune models to improve performance and reduce costs.\n\nThat said, if you delete your Plandex Cloud account, all associated data will be removed within 14 days (this delay allows for debugging and backups).\n\nData sent to Plandex Cloud may be shared with the following third parties:\n\n- [OpenAI](https://openai.com) for OpenAI models when using Integrated Models Mode.\n- [OpenRouter.ai](https://openrouter.ai/) for Anthropic, Google, and other non-OpenAI models when using Integrated Models Mode.\n- [Google Vertex AI](https://cloud.google.com/vertex-ai) for Google and Anthropic models when using Integrated Models Mode.\n- [AWS](https://aws.amazon.com/) for hosting and database services. Data is encrypted in transit and at rest.\n- Your name and email is shared with [Loops](https://loops.so/), an email marketing service, in order to send you updates on Plandex. You can opt out of these emails at any time with one click.\n- Your name and email are shared with our payment processor [Stripe](https://stripe.com/) if you subscribe to a paid plan or purchase the $10 trial.\n- Basic usage data is sent to [Google Analytics](https://analytics.google.com/) to help track usage and make improvements.\n- [Relace](https://relace.ai/) for an instant apply AI model that speeds up and reduces the cost of file edits. Used as a fallback if Plandex is unable to apply edits deterministically. Inputs are the original file and the edit snippet from a Plandex response.\n- [Rollbar](https://rollbar.com/) for error tracking and alerting. It's unlikely that any user data would be shared with Rollbar. If it was, it would be minimal and incidental to error reporting.\n\nApart from the above list, no other data will be shared with any other third party. The list will be updated if any new third party services are introduced.\n\nData sent to a model provider like OpenAI or OpenRouter.ai is subject to the model provider's privacy and data retention policies.\n\nSee our full [Privacy Policy](https://plandex.ai/privacy) for more details.\n"
  },
  {
    "path": "docs/docs/hosting/self-hosting/_category_.json",
    "content": "{\n  \"label\": \"Self-Hosting\",\n  \"position\": 1,\n  \"collapsible\": true,\n  \"collapsed\": false\n}"
  },
  {
    "path": "docs/docs/hosting/self-hosting/advanced-self-hosting.md",
    "content": "---\nsidebar_position: 2\nsidebar_label: Advanced Self-Hosting\n---\n\n# Advanced Self-Hosting\n\nThe easiest way to self-host Plandex is to use the [Local Mode Quickstart](./local-mode-quickstart.md). But if you need to run Plandex on a remote server with multiple users or orgs, or you want to run it without docker/docker-compose, keep reading below.\n\n## Requirements\n\nThe Plandex server requires a PostgreSQL database (ideally v14), a persistent file system, and git.\n\nDue to a dependency on tree-sitter, gcc, g++, and make are also required to build the server.\n\n## Development vs. Production\n\nThe Plandex server can be run in development or production mode. The main differences are how authentication pins and emails are handled, and the default path for the persistent file system.\n\nDevelopment mode is designed for local usage with a single user. Email isn't enabled and verification pins are skipped. In development mode, the default base directory is `$HOME/plandex-server`.\n\nProduction mode is designed for multiple users or organizations. Email is enabled and SMTP environment variables are required. Authentication pins are sent via email. In production mode, the default base directory is `/plandex-server`.\n\nDevelopment or production mode is set with the `GOENV` environment variable. It should be set to either `development` or `production`.\n\nIn both development and production mode, the server runs on port 8099 by default. This can be changed with the `PORT` environment variable.\n\n## PostgreSQL Database\n\nIf you aren't using docker-compose to start the server and run the database, you'll need a PostgreSQL database. You can run the following SQL to create a user and database, replacing `user` and `password` with your own values:\n\n```sql\nCREATE USER 'user' WITH PASSWORD 'password';\nCREATE DATABASE 'plandex' OWNER 'user';\nGRANT ALL PRIVILEGES ON DATABASE 'plandex' TO 'user';\n```\n\n### Environment Variables\n\nSet `GOENV` to either `development` or `production` as described above in the [Development vs. Production](#development-vs-production) section:\n\n```bash\nexport GOENV=development\n```\n\nor\n  \n```bash\nexport GOENV=production\n```\n\nYou'll also need a `DATABASE_URL`:\n\n```bash\nexport DATABASE_URL=postgres://user:password@host:5432/plandex # replace with your own database URL\n```\n\nIn production mode, you'll also need to connect to SMTP to send emails. Set the following environment variables:\n\n```bash\nexport SMTP_HOST=smtp.example.com\nexport SMTP_PORT=587\nexport SMTP_USER=user\nexport SMTP_PASSWORD=password\nexport SMTP_FROM=user@example.com # optional, if not set then SMTP_USER is used\n```\n\nIn either development or production mode, the base directory for the persistent file system can optionally be overridden with the `PLANDEX_BASE_DIR` environment variable:\n\n```bash\nexport PLANDEX_BASE_DIR=~/some-dir/plandex-server\n```\n\nWhen running the Plandex CLI, to connect to a server running in production mode, set the API_HOST environment variable to the host the server is running on:\n\n```bash\nexport API_HOST=api.your-domain.ai\n```\n\n### Using Docker Build\n\nThe server can be run from a Dockerfile at `app/Dockerfile.server`:\n\n```bash\ngit clone https://github.com/plandex-ai/plandex.git\nVERSION=$(cat app/server/version.txt) # or use the version you want\ngit checkout server/v$VERSION\ncd plandex/app\nmkdir ~/plandex-server # or another directory where you want to store files\ndocker build -t plandex-server -f Dockerfile.server .\ndocker run -p 8099:8099 \\\n  -v ~/plandex-server:/plandex-server \\\n  -e DATABASE_URL \\\n  -e GOENV \\\n  -e API_HOST \\\n  -e SMTP_HOST \\ \n  -e SMTP_PORT \\\n  -e SMTP_USER \\\n  -e SMTP_PASSWORD \\\n  plandex-server\n```\n\nThe API_HOST and SMTP environment variables above are only required if you're running in [production mode](#development-vs-production).\n\n### DockerHub Server Images\n\nApart from building manually with the Dockerfile, server images are also built and pushed to [DockerHub](https://hub.docker.com/r/plandexai/plandex-server/tags) automatically when a new version of the server is released.\n\nYou can pull the latest server image with:\n\n```bash\ndocker pull plandexai/plandex-server:latest\n```\n\n### Run From Source\n\nYou can also run the server from source:\n\n```bash\ngit clone https://github.com/plandex-ai/plandex.git\ncd plandex/\nVERSION=$(cat app/server/version.txt) # or use the version you want\ngit checkout server/v$VERSION\ncd app/server\nexport GOENV=development # or production\nexport DATABASE_URL=postgres://user:password@host:5432/plandex # replace with your own database URL\nexport PLANDEX_BASE_DIR=~/plandex-server # or another directory where you want to store files\ngo run main.go\n```\n\n## Health Check\n\nYou can check if the server is running by sending a GET request to `/health`. If all is well, it will return a 200 status code.\n\n## Create a New Account\n\nOnce the server is running and you've [installed the Plandex CLI](../../install.md) on your local development machine, you can create a new account by running `plandex sign-in`: \n\n```bash\nplandex sign-in # follow the prompts to create a new account on your self-hosted server\n```\n\n## Note On Local CLI Files\n\nIf you use the Plandex CLI and then for some reason you reset the database or use a new one, you'll need to remove the local files that the CLI creates in directories where you used Plandex in order to start fresh. Otherwise, the CLI will attempt to authenticate with an account that doesn't exist in the new database and you'll get errors.\n\nTo resolve this, remove the following in any directory you used the CLI in:\n\n- `.plandex-dev` directory if you ran the CLI with `PLANDEX_ENV=development`\n- `.plandex` directory otherwise\n\nThen run `plandex sign-in` again to create a new account.\n\nIf you're still having trouble with accounts, you can also remove the following from your $HOME directory to fully reset them:\n\n- `.plandex-home-dev` directory if you ran the CLI with `PLANDEX_ENV=development`\n- `.plandex-home` directory otherwise"
  },
  {
    "path": "docs/docs/hosting/self-hosting/local-mode-quickstart.md",
    "content": "---\nsidebar_position: 1\nsidebar_label: Local Mode Quickstart\n---\n\n# Self-Hosting\n\nPlandex is open source and uses a client-server architecture. The server can be self-hosted. You can either run it locally or on a cloud server that you control. To run it on a cloud server, go to  [Advanced Self-Hosting](advanced-self-hosting.md) section. To run it locally, keep reading below.\n\n## Local Mode Quickstart\n\nThe quickstart requires git, docker, and docker-compose. It's designed for local use with a single user.\n\n1. Run the server in local mode: \n\n```bash\ngit clone https://github.com/plandex-ai/plandex.git\ncd plandex/app\n./start_local.sh\n```\n\n2. In a new terminal session, install the Plandex CLI if you haven't already:\n\n```bash\ncurl -sL https://plandex.ai/install.sh | bash\n```\n\n3. Run:\n\n```bash\nplandex sign-in\n```\n\n4. When prompted 'Use Plandex Cloud or another host?', select 'Local mode host'. Confirm the default host, which is `http://localhost:8099`.\n\n5. Decide on the model provider(s) you want to use. The quickest option is to use OpenRouter.ai, but you can also use [many other providers](https://docs.plandex.ai/models/model-providers).\n\nIf you're using OpenRouter.ai, first [sign up here.](https://openrouter.ai/signup) Then [generate an API key here.](https://openrouter.ai/keys) Set the `OPENROUTER_API_KEY` environment variable:\n\n```bash\nexport OPENROUTER_API_KEY=...\n```\n\n6. In a project directory, start the Plandex REPL:\n\n```bash\nplandex\n```\n\nYou're ready to start building!\n\n## Upgrade\n\nTo upgrade after a new release, just use `ctrl-c` to stop the server, then run the script again:\n\n```bash\n./start_local.sh\n```\n\nThe script will pull the latest image before the server starts.\n\n\n"
  },
  {
    "path": "docs/docs/install.md",
    "content": "---\nsidebar_position: 1\nsidebar_label: Install\n---\n\n# Install Plandex\n\n## Quick Install\n\n```bash\ncurl -sL https://plandex.ai/install.sh | bash\n```\n\n## Manual install\n\nGrab the appropriate binary for your platform from the latest [release](https://github.com/plandex-ai/plandex/releases) and put it somewhere in your `PATH`.\n\n## Build from source\n\n```bash\ngit clone https://github.com/plandex-ai/plandex.git\ncd plandex/app/cli\ngo build -ldflags \"-X plandex/version.Version=$(cat version.txt)\"\nmv plandex /usr/local/bin # adapt as needed for your system\n```\n\n## Windows\n\nWindows is supported via [WSL](https://learn.microsoft.com/en-us/windows/wsl/about).\n\nPlandex only works correctly in the WSL shell. It doesn't work in the Windows CMD prompt or PowerShell."
  },
  {
    "path": "docs/docs/models/_category_.json",
    "content": "{\n  \"label\": \"Models\",\n  \"position\": 6,\n  \"collapsible\": true,\n  \"collapsed\": false\n}"
  },
  {
    "path": "docs/docs/models/built-in/_category_.json",
    "content": "{\n  \"label\": \"Built-In\",\n  \"position\": 9,\n  \"collapsible\": true,\n  \"collapsed\": false\n}"
  },
  {
    "path": "docs/docs/models/built-in/built-in-models.md",
    "content": "---\nsidebar_position: 1\nsidebar_label: Models\n---\n\n# Built-In Models\n\nPlandex includes a curated selection of built-in models.\n\n## OpenAI\n\n### `openai/o3-high`\n\n- OpenAI o3 (high reasoning)\n- Max Tokens: 200k\n- Max Output: 100k\n- Reserved Output: 40k\n- Effective Input: 160k\n- Features: XML output, no system prompt, fixed parameters, reasoning effort\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/o3-medium`\n\n- OpenAI o3 (medium reasoning)\n- Max Tokens: 200k\n- Max Output: 100k\n- Reserved Output: 40k\n- Effective Input: 160k\n- Features: XML output, no system prompt, fixed parameters, reasoning effort\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/o3-low`\n\n- OpenAI o3 (low reasoning)\n- Max Tokens: 200k\n- Max Output: 100k\n- Reserved Output: 40k\n- Effective Input: 160k\n- Features: XML output, no system prompt, fixed parameters, reasoning effort\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/o4-mini-high`\n\n- OpenAI o4-mini (high reasoning)\n- Max Tokens: 200k\n- Max Output: 100k\n- Reserved Output: 40k\n- Effective Input: 160k\n- Features: JSON output, no system prompt, fixed parameters, reasoning effort\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/o4-mini-medium`\n\n- OpenAI o4-mini (medium reasoning)\n- Max Tokens: 200k\n- Max Output: 100k\n- Reserved Output: 30k\n- Effective Input: 170k\n- Features: JSON output, no system prompt, fixed parameters, reasoning effort\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/o4-mini-low`\n\n- OpenAI o4-mini (low reasoning)\n- Max Tokens: 200k\n- Max Output: 100k\n- Reserved Output: 20k\n- Effective Input: 180k\n- Features: JSON output, no system prompt, fixed parameters, reasoning effort\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/gpt-4.1`\n\n- OpenAI GPT-4.1\n- Max Tokens: 1,047,576\n- Max Output: 32,768\n- Reserved Output: 32,768\n- Effective Input: 1,014,808\n- Features: JSON output, full compatibility\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/gpt-4.1-mini`\n\n- OpenAI GPT-4.1 Mini\n- Max Tokens: 1,047,576\n- Max Output: 32,768\n- Reserved Output: 32,768\n- Effective Input: 1,014,808\n- Features: JSON output, full compatibility\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n### `openai/gpt-4.1-nano`\n\n- OpenAI GPT-4.1 Nano\n- Max Tokens: 1,047,576\n- Max Output: 32,768\n- Reserved Output: 32,768\n- Effective Input: 1,014,808\n- Features: JSON output, full compatibility\n- Providers: OpenAI, Azure OpenAI, OpenRouter\n\n## Anthropic\n\n### `anthropic/claude-opus-4`\n\n- Anthropic Claude Opus 4\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 20k\n- Effective Input: 180k\n- Features: XML output, cache control, single message mode\n- Providers: Anthropic, AWS Bedrock, Google Vertex, OpenRouter\n\n### `anthropic/claude-sonnet-4`\n\n- Anthropic Claude Sonnet 4\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 40k\n- Effective Input: 160k\n- Features: XML output, cache control, single message mode\n- Providers: Anthropic, AWS Bedrock, Google Vertex, OpenRouter\n\n### `anthropic/claude-sonnet-4-thinking`\n\n- Claude Sonnet 4 (visible reasoning)\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 40k\n- Effective Input: 160k\n- Features: XML output, cache control, single message mode, reasoning budget 32k\n- Providers: Anthropic, AWS Bedrock, Google Vertex, OpenRouter\n\n### `anthropic/claude-sonnet-4-thinking-hidden`\n\n- Claude Sonnet 4 (hidden reasoning)\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 40k\n- Effective Input: 160k\n- Features: XML output, cache control, single message mode, reasoning budget 32k\n- Providers: Anthropic, AWS Bedrock, Google Vertex, OpenRouter\n\n### `anthropic/claude-3.7-sonnet`\n\n- Anthropic Claude 3.7 Sonnet\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 20k\n- Effective Input: 180k\n- Features: XML output, cache control, single message mode\n- Providers: Anthropic, AWS Bedrock, Google Vertex, OpenRouter\n\n### `anthropic/claude-3.7-sonnet-thinking`\n\n- Claude 3.7 Sonnet (visible reasoning)\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 20k\n- Effective Input: 180k\n- Features: XML output, cache control, single message mode, reasoning budget 32k\n- Providers: Anthropic, AWS Bedrock, Google Vertex, OpenRouter\n\n### `anthropic/claude-3.7-sonnet-thinking-hidden`\n\n- Claude 3.7 Sonnet (hidden reasoning)\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 20k\n- Effective Input: 180k\n- Features: XML output, cache control, single message mode, reasoning budget 32k\n- Providers: Anthropic, AWS Bedrock, Google Vertex, OpenRouter\n\n### `anthropic/claude-3.5-sonnet`\n\n- Anthropic Claude 3.5 Sonnet\n- Max Tokens: 200k\n- Max Output: 128k\n- Reserved Output: 20k\n- Effective Input: 180k\n- Features: XML output, cache control, single message mode\n- Providers: Anthropic, Google Vertex, AWS Bedrock, OpenRouter\n\n### `anthropic/claude-3.5-haiku`\n\n- Anthropic Claude 3.5 Haiku\n- Max Tokens: 200k\n- Max Output: 8,192\n- Reserved Output: 8,192\n- Effective Input: 191,808\n- Features: XML output, cache control, single message mode\n- Providers: Anthropic, Google Vertex, AWS Bedrock, OpenRouter\n\n## Google\n\n### `google/gemini-2.5-pro`\n\n- Google Gemini 2.5 Pro\n- Max Tokens: 1,048,576\n- Max Output: 65,535\n- Reserved Output: 65,535\n- Effective Input: 983,041\n- Features: XML output\n- Providers: Google AI Studio, Google Vertex, OpenRouter\n\n### `google/gemini-2.5-flash`\n\n- Google Gemini 2.5 Flash\n- Max Tokens: 1,048,576\n- Max Output: 65,535\n- Reserved Output: 65,535\n- Effective Input: 983,041\n- Features: XML output\n- Providers: Google AI Studio, Google Vertex, OpenRouter\n\n### `google/gemini-2.5-flash-thinking`\n\n- Gemini 2.5 Flash (visible reasoning)\n- Max Tokens: 1,048,576\n- Max Output: 65,535\n- Reserved Output: 65,535\n- Effective Input: 983,041\n- Features: XML output, visible reasoning\n- Providers: Google AI Studio, Google Vertex, OpenRouter\n\n### `google/gemini-2.5-flash-thinking-hidden`\n\n- Gemini 2.5 Flash (hidden reasoning)\n- Max Tokens: 1,048,576\n- Max Output: 65,535\n- Reserved Output: 65,535\n- Effective Input: 983,041\n- Features: XML output, hidden reasoning\n- Providers: Google AI Studio, Google Vertex, OpenRouter\n\n### `google/gemini-pro-1.5`\n\n- Google Gemini 1.5 Pro\n- Max Tokens: 2M\n- Max Output: 8,192\n- Reserved Output: 8,192\n- Effective Input: 1,991,808\n- Features: XML output\n- Providers: Google AI Studio, Google Vertex, OpenRouter\n\n## DeepSeek\n\n### `deepseek/v3`\n\n- DeepSeek V3\n- Max Tokens: 64k\n- Max Output: 8,192\n- Reserved Output: 8,192\n- Effective Input: 55,808\n- Features: XML output\n- Providers: DeepSeek, OpenRouter\n\n### `deepseek/r1`\n\n- DeepSeek R1 (visible reasoning)\n- Max Tokens: 164k\n- Max Output: 33k\n- Reserved Output: 20k\n- Effective Input: 144k\n- Features: XML output, visible reasoning\n- Providers: DeepSeek, OpenRouter\n\n### `deepseek/r1-hidden`\n\n- DeepSeek R1 (hidden reasoning)\n- Max Tokens: 164k\n- Max Output: 33k\n- Reserved Output: 20k\n- Effective Input: 144k\n- Features: XML output, hidden reasoning\n- Providers: DeepSeek, OpenRouter\n\n### `deepseek/r1-70b`\n\n- DeepSeek R1 70B (Ollama only)\n- Max Tokens: 131,072\n- Max Output: 131,072\n- Reserved Output: 20k\n- Effective Input: 111,072\n- Features: XML output\n- Providers: Ollama\n\n### `deepseek/r1-32b`\n\n- DeepSeek R1 32B (Ollama only)\n- Max Tokens: 131,072\n- Max Output: 131,072\n- Reserved Output: 20k\n- Effective Input: 111,072\n- Features: XML output\n- Providers: Ollama\n\n### `deepseek/r1-14b`\n\n- DeepSeek R1 14B (Ollama only)\n- Max Tokens: 131,072\n- Max Output: 131,072\n- Reserved Output: 20k\n- Effective Input: 111,072\n- Features: XML output\n- Providers: Ollama\n\n### `deepseek/r1-8b`\n\n- DeepSeek R1 8B (Ollama only)\n- Max Tokens: 131,072\n- Max Output: 131,072\n- Reserved Output: 20k\n- Effective Input: 111,072\n- Features: XML output\n- Providers: Ollama\n\n## Perplexity\n\n### `perplexity/r1-1776`\n\n- Perplexity R1-1776 (visible reasoning)\n- Max Tokens: 128k\n- Max Output: 128k\n- Reserved Output: 30k\n- Effective Input: 98k\n- Features: XML output, visible reasoning\n- Providers: Perplexity, OpenRouter\n\n### `perplexity/r1-1776-hidden`\n\n- Perplexity R1-1776 (hidden reasoning)\n- Max Tokens: 128k\n- Max Output: 128k\n- Reserved Output: 30k\n- Effective Input: 98k\n- Features: XML output, hidden reasoning\n- Providers: Perplexity, OpenRouter\n\n### `perplexity/r1-1776-70b`\n\n- Perplexity R1-1776 70B (Ollama only)\n- Max Tokens: 131,072\n- Max Output: 131,072\n- Reserved Output: 20k\n- Effective Input: 111,072\n- Features: XML output\n- Providers: Ollama\n\n### `perplexity/sonar-reasoning`\n\n- Perplexity Sonar Reasoning (visible reasoning)\n- Max Tokens: 127k\n- Max Output: 127k\n- Reserved Output: 30k\n- Effective Input: 97k\n- Features: XML output, visible reasoning\n- Providers: Perplexity, OpenRouter\n\n### `perplexity/sonar-reasoning-hidden`\n\n- Perplexity Sonar Reasoning (hidden reasoning)\n- Max Tokens: 127k\n- Max Output: 127k\n- Reserved Output: 30k\n- Effective Input: 97k\n- Features: XML output, hidden reasoning\n- Providers: Perplexity, OpenRouter\n\n## Qwen\n\n### `qwen/qwen-2.5-coder-32b-instruct`\n\n- Qwen 2.5 Coder 32B\n- Max Tokens: 128k\n- Max Output: 8,192\n- Reserved Output: 8,192\n- Effective Input: 119,808\n- Features: XML output\n- Providers: OpenRouter\n\n### `qwen/qwen3-235b-local`\n\n- Qwen 3-235B (Ollama only)\n- Max Tokens: 40,960\n- Max Output: 40,960\n- Reserved Output: 8,192\n- Effective Input: 32,768\n- Features: XML output\n- Providers: Ollama\n\n### `qwen/qwen3-235b-a22b-cloud`\n\n- Qwen 3-235B A22B\n- Max Tokens: 40,960\n- Max Output: 40,960\n- Reserved Output: 8,192\n- Effective Input: 32,768\n- Features: XML output\n- Providers: OpenRouter\n\n### `qwen/qwen3-32b-local`\n\n- Qwen 3-32B (Ollama only)\n- Max Tokens: 40,960\n- Max Output: 40,960\n- Reserved Output: 8,192\n- Effective Input: 32,768\n- Features: XML output\n- Providers: Ollama\n\n### `qwen/qwen3-32b-cloud`\n\n- Qwen 3-32B\n- Max Tokens: 40,960\n- Max Output: 40,960\n- Reserved Output: 8,192\n- Effective Input: 32,768\n- Features: XML output\n- Providers: OpenRouter\n\n### `qwen/qwen3-14b-local`\n\n- Qwen 3-14B (Ollama only)\n- Max Tokens: 40,960\n- Max Output: 40,960\n- Reserved Output: 8,192\n- Effective Input: 32,768\n- Features: XML output\n- Providers: Ollama\n\n### `qwen/qwen3-14b-cloud`\n\n- Qwen 3-14B\n- Max Tokens: 40,960\n- Max Output: 40,960\n- Reserved Output: 8,192\n- Effective Input: 32,768\n- Features: XML output\n- Providers: OpenRouter\n\n### `qwen/qwen3-8b-local`\n\n- Qwen 3-8B (Ollama only)\n- Max Tokens: 32,768\n- Max Output: 32,768\n- Reserved Output: 8,192\n- Effective Input: 24,576\n- Features: XML output\n- Providers: Ollama\n\n### `qwen/qwen3-8b-cloud`\n\n- Qwen 3-8B\n- Max Tokens: 128k\n- Max Output: 20k\n- Reserved Output: 20k\n- Effective Input: 108k\n- Features: XML output\n- Providers: OpenRouter\n\n## Mistral\n\n### `mistral/devstral-small`\n\n- Mistral Devstral Small\n- Max Tokens: 128k\n- Max Output: 128k\n- Reserved Output: 16,384\n- Effective Input: 111,616\n- Features: XML output\n- Providers: Ollama, OpenRouter"
  },
  {
    "path": "docs/docs/models/built-in/built-in-packs.md",
    "content": "---\nsidebar_position: 2\nsidebar_label: Model Packs\n---\n\n# Built-In Model Packs\n\nPlandex includes a curated selection of built-in model packs that have been tested and optimized for different use cases.\n\n*A model pack is a mapping of [model roles](../roles.md) to [models](./built-in-models.md).*\n\n*They can also define fallback models for large context, large output, error handling, as well as a strong variant for the `builder` role.*\n\n## Core Packs\n\n### `daily-driver`\n*A mix of models from Anthropic, OpenAI, and Google that balances speed, quality, and cost. Supports up to 2M context.*\n\n- **planner** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `google/gemini-2.5-pro`\n    - largeContextFallback → `google/gemini-pro-1.5`\n- **architect** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `google/gemini-2.5-pro`\n    - largeContextFallback → `google/gemini-pro-1.5`\n- **coder** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `openai/gpt-4.1`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n  - strongModel → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-medium`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `reasoning`\n*Like the daily driver, but uses sonnet-4-thinking with reasoning enabled for planning and coding. Supports up to 160k input context.*\n\n- **planner** → `anthropic/claude-sonnet-4-thinking-hidden`\n- **architect** → Uses planner model\n- **coder** → `anthropic/claude-sonnet-4-thinking-hidden`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n  - strongModel → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-medium`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `strong`\n*For difficult tasks where slower responses and builds are ok. Uses o3-high for architecture and planning, claude-sonnet-4 thinking for implementation. Supports up to 160k input context.*\n\n- **planner** → `openai/o3-high`\n- **architect** → `openai/o3-high`\n- **coder** → `anthropic/claude-sonnet-4-thinking-hidden`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-high`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-medium`\n\n### `cheap`\n*Cost-effective models that can still get the job done for easier tasks. Supports up to 160k context. Uses OpenAI's o4-mini model for planning, GPT-4.1 for coding, and GPT-4.1 Mini for lighter tasks.*\n\n- **planner** → `openai/o4-mini-medium`\n- **architect** → Uses planner model\n- **coder** → `openai/gpt-4.1`\n- **summarizer** → `openai/gpt-4.1-mini`\n- **builder** → `openai/o4-mini-low`\n- **wholeFileBuilder** → `openai/o4-mini-low`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `oss`\n*An experimental mix of the best open source models for coding. Supports up to 144k context, 33k per file.*\n\n- **planner** → `deepseek/r1`\n- **architect** → Uses planner model\n- **coder** → `deepseek/v3`\n- **summarizer** → `deepseek/r1-hidden`\n- **builder** → `deepseek/r1-hidden`\n- **wholeFileBuilder** → `deepseek/r1-hidden`\n- **names** → `qwen/qwen3-8b-cloud`\n- **commitMessages** → `qwen/qwen3-8b-cloud`\n- **autoContinue** → `deepseek/r1-hidden`\n\n## Provider Packs\n\n\n### `openai`\n*OpenAI blend. Supports up to 1M context. Uses OpenAI's GPT-4.1 model for heavy lifting, GPT-4.1 Mini for lighter tasks.*\n\n- **planner** → `openai/gpt-4.1`\n- **architect** → Uses planner model\n- **coder** → Uses planner model\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n  - strongModel → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-medium`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `anthropic`\n*Anthropic blend. Supports up to 180k context. Uses Claude Sonnet 4 for heavy lifting, Claude 3 Haiku for lighter tasks.*\n\n- **planner** → `anthropic/claude-sonnet-4`\n- **architect** → Uses planner model\n- **coder** → `anthropic/claude-sonnet-4`\n- **summarizer** → `anthropic/claude-3.5-haiku`\n- **builder** → `anthropic/claude-sonnet-4`\n- **wholeFileBuilder** → `anthropic/claude-sonnet-4`\n- **names** → `anthropic/claude-3.5-haiku`\n- **commitMessages** → `anthropic/claude-3.5-haiku`\n- **autoContinue** → `anthropic/claude-sonnet-4`\n\n### `gemini-planner`\n*Uses Gemini 2.5 Pro for planning, default models for other roles. Supports up to 1M input context.*\n\n- **planner** → `google/gemini-2.5-pro`\n- **architect** → Uses planner model\n- **coder** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `openai/gpt-4.1`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n  - strongModel → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-medium`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `opus-planner`\n*Uses Claude Opus 4 for planning, default models for other roles. Supports up to 180k input context.*\n\n- **planner** → `anthropic/opus-4`\n- **architect** → Uses planner model\n- **coder** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `openai/gpt-4.1`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n  - strongModel → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-medium`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `o3-planner`\n*Uses OpenAI o3-medium for planning, default models for other roles. Supports up to 160k input context.*\n\n- **planner** → `openai/o3-medium`\n- **architect** → Uses planner model\n- **coder** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `openai/gpt-4.1`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n  - strongModel → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-medium`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `r1-planner`\n*Uses DeepSeek R1 for planning, Qwen for light tasks, and default models for implementation. Supports up to 56k input context.*\n\n- **planner** → `deepseek/r1`\n- **architect** → Uses planner model\n- **coder** → `anthropic/claude-sonnet-4`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n- **wholeFileBuilder** → `openai/o4-mini-low`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-medium`\n\n### `perplexity-planner`\n*Uses Perplexity Sonar for planning, Qwen for light tasks, and default models for implementation. Supports up to 97k input context.*\n\n- **planner** → `perplexity/sonar-reasoning`\n- **architect** → Uses planner model\n- **coder** → `anthropic/claude-sonnet-4`\n- **summarizer** → `openai/o4-mini-low`\n- **builder** → `openai/o4-mini-medium`\n- **wholeFileBuilder** → `openai/o4-mini-low`\n- **names** → `openai/gpt-4.1-mini`\n- **commitMessages** → `openai/gpt-4.1-mini`\n- **autoContinue** → `openai/o4-mini-medium`\n\n## Local Packs\n\n### `ollama`\n*Ollama experimental local blend. Supports up to 110k context. For now, more for experimentation and benchmarking than getting work done.*\n\n- **localProvider** → `ollama`\n- **planner** → `qwen/qwen3-32b-local`\n- **architect** → Uses planner model\n- **coder** → Uses planner model\n- **summarizer** → `mistral/devstral-small`\n- **builder** → `mistral/devstral-small`\n- **wholeFileBuilder** → `mistral/devstral-small`\n- **names** → `qwen/qwen3-8b-local`\n- **commitMessages** → `qwen/qwen3-8b-local`\n- **autoContinue** → `mistral/devstral-small`\n\n### `ollama-daily`\n*Ollama adaptive/daily-driver blend. Uses 'daily-driver' for heavy lifting, local models for lighter tasks.*\n\n- **localProvider** → `ollama`\n- **planner** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `google/gemini-2.5-pro`\n    - largeContextFallback → `google/gemini-pro-1.5`\n- **architect** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `google/gemini-2.5-pro`\n    - largeContextFallback → `google/gemini-pro-1.5`\n- **coder** → `anthropic/claude-sonnet-4`\n  - largeContextFallback → `openai/gpt-4.1`\n- **summarizer** → `mistral/devstral-small`\n- **builder** → `openai/o4-mini-medium`\n  - strongModel → `openai/o4-mini-high`\n- **wholeFileBuilder** → `openai/o4-mini-medium`\n- **names** → `qwen/qwen3-8b-local`\n- **commitMessages** → `qwen/qwen3-8b-local`\n- **autoContinue** → `openai/o4-mini-low`\n\n### `ollama-oss`\n*Ollama adaptive/oss blend. Uses local models for planning and context selection, open source cloud models for implementation and file edits. Supports up to 110k context.*\n\n- **localProvider** → `ollama`\n- **planner** → `deepseek/r1`\n- **architect** → Uses planner model\n- **coder** → `deepseek/v3`\n- **summarizer** → `mistral/devstral-small`\n- **builder** → `deepseek/r1-hidden`\n- **wholeFileBuilder** → `deepseek/r1-hidden`\n- **names** → `qwen/qwen3-8b-local`\n- **commitMessages** → `qwen/qwen3-8b-local`\n- **autoContinue** → `deepseek/r1-hidden`\n"
  },
  {
    "path": "docs/docs/models/claude-subscription.md",
    "content": "---\nsidebar_position: 3\nsidebar_label: Claude Pro/Max\n---\n\n# Claude Pro/Max Subscription\n\nIf you have a Claude Pro or Max subscription, Plandex can use it when calling Anthropic models. You can use it in either Integrated Models Mode on Plandex Cloud, or in BYO Key Mode (whether on Cloud or self-hosting).\n\n## Startup Prompt\n\nAssuming you're using Anthropic models (which the default model pack does), you'll be asked if you want to connect your Claude subscription the first time you run Plandex. Follow the instructions to connect.\n\n## CLI Commands\n\n### `connect-claude`\n\nYou can connect your subscription with the `connect-claude` command.\n\n```bash\nplandex connect-claude # CLI\n\\connect-claude  # REPL\n```\n\n### `disconnect-claude`\n\nYou can disconnect your subscription and clear credentials from your device with the `disconnect-claude` command.\n\n```bash\nplandex disconnect-claude # CLI\n\\disconnect-claude  # REPL\n```\n\n### `claude-status`\n\nYou can check whether a subscription is connected with `claude-status`\n\n```bash\nplandex claude-status # CLI\n\\claude-status  # REPL\n```\n\nThis command will also tell you if the subscription's quota has been exceeded and a backup provider is being used instead.\n\n## Quota Exhaustion\n\nIf you're using Plandex Cloud with Integrated Models Mode, Anthropic model calls will use your Claude subscription until it runs out of quota, then switch to using Plandex credits until the quota resets.\n\nIf you're self-hosting or using Plandex Cloud in BYO API Key Mode, Anthropic model calls will use your Claude subscription until it runs out of quota, then:\n\n- If you have an API key or credentials set for an [Anthropic provider](./model-providers.md) (like the Anthropic API, Google Vertex AI, AWS Bedrock, or OpenRouter), Plandex will switch to the backup provider until the quota resets.\n\n- If you have no API key or credentials set for an Anthropic provider, you'll get a rate limit error until your quota resets."
  },
  {
    "path": "docs/docs/models/custom-models.md",
    "content": "---\nsidebar_position: 5\nsidebar_label: Custom Models\n---\n\n# Custom Models, Providers, and Model Packs\n\nYou can extend Plandex with custom models, providers, and model packs using a JSON file.\n\n```bash\nplandex models custom # edit the custom models file\n\\models custom # REPL\n```\n\nThe first time you run this command, a template file with examples will be created and opened in your preferred editor.\n\nThe template file uses a JSON schema, allowing most editors to provide autocomplete, validation, and inline documentation.\n\n## Support Levels\n\nThe level of custom model support depends on how you use Plandex.\n\n**Self-hosted**: Everything - custom models, providers, and model packs are fully supported.\n\n**Cloud with BYO API Keys**: Custom models and model packs. Models can only use built-in providers.\n\n**Cloud with Integrated Models**: Custom model packs. Model packs can only use built-in models.\n\n## Basic Example\n\nHere's a minimal example that adds a model from Together.ai:\n\n```json\n{\n  \"$schema\": \"https://plandex.ai/schemas/models-input.schema.json\",\n  \"providers\": [\n    {\n      \"name\": \"togetherai\",\n      \"baseUrl\": \"https://api.together.xyz/v1\",\n      \"apiKeyEnvVar\": \"TOGETHER_API_KEY\"\n    }\n  ],\n  \"models\": [\n    {\n      \"modelId\": \"meta-llama/llama-4-maverick\",\n      \"publisher\": \"meta-llama\",\n      \"description\": \"Meta Llama 4 Maverick\",\n      \"defaultMaxConvoTokens\": 75000,\n      \"maxTokens\": 1048576,\n      \"maxOutputTokens\": 16000,\n      \"reservedOutputTokens\": 16000,\n      \"preferredOutputFormat\": \"xml\",\n      \"providers\": [\n        {\n          \"provider\": \"custom\",\n          \"customProvider\": \"togetherai\",\n          \"modelName\": \"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n## Custom Providers\n\nDefine providers that use OpenAI-compatible APIs:\n\n```json\n{\n  \"providers\": [\n    {\n      \"name\": \"local-llm\",\n      \"baseUrl\": \"http://localhost:8080/v1\",\n      \"skipAuth\": true\n    },\n    {\n      \"name\": \"my-provider\",\n      \"baseUrl\": \"https://api.myprovider.com/v1\",\n      \"apiKeyEnvVar\": \"MY_PROVIDER_API_KEY\"\n    }\n  ]\n}\n```\n\n### Provider Settings\n\n- `name` - Unique identifier for the provider\n- `baseUrl` - API endpoint (must be OpenAI-compatible)\n- `apiKeyEnvVar` - Environment variable containing the API key\n- `skipAuth` - Set to `true` for local models that don't need authentication\n- `extraAuthVars` - Additional authentication variables if needed\n\n## Custom Models\n\nDefine models with their capabilities and provider mappings:\n\n```json\n{\n  \"$schema\": \"https://plandex.ai/schemas/models-input.schema.json\",\n  \"models\": [\n    {\n      \"modelId\": \"my-model\",\n      \"publisher\": \"My Company\",\n      \"description\": \"My Custom Model\",\n      \"defaultMaxConvoTokens\": 50000,\n      \"maxTokens\": 128000,\n      \"maxOutputTokens\": 8192,\n      \"reservedOutputTokens\": 8192,\n      \"preferredOutputFormat\": \"xml\",\n      \"providers\": [\n        {\n          \"provider\": \"custom\",\n          \"customProvider\": \"my-provider\",\n          \"modelName\": \"exact-model-name-on-provider\"\n        },\n        {\n          \"provider\": \"openrouter\",\n          \"modelName\": \"my-company/my-model\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n### Model Settings\n\n- `modelId` - Unique identifier used in model packs\n- `maxTokens` - Total token limit (input + output)\n- `maxOutputTokens` - Maximum output tokens the model can generate\n- `reservedOutputTokens` - Tokens reserved for output (affects effective input limit)\n- `preferredOutputFormat` - Either `\"xml\"` or `\"tool-call-json\"`\n- `providers` - List of providers that can serve this model\n\n## Custom Model Packs\n\nCreate your own combinations of models for different roles:\n\n```json\n{\n  \"$schema\": \"https://plandex.ai/schemas/models-input.schema.json\",\n  \"modelPacks\": [\n    {\n      \"name\": \"custom-pack\",\n      \"description\": \"Custom model pack for my use case\",\n      \"planner\": \"custom-model-publisher/model-name\",\n      \"coder\": \"anthropic/claude-sonnet-4\",\n      \"architect\": \"custom-model-publisher/model-name\",\n      \"summarizer\": \"openai/gpt-4.1\",\n      \"builder\": {\n        \"modelId\": \"anthropic/claude-sonnet-4\",\n        \"strongModel\": \"openai/o3-medium\"\n      },\n      \"wholeFileBuilder\": {\n        \"modelId\": \"anthropic/claude-sonnet-4\",\n        \"largeContextFallback\": {\n          \"modelId\": \"google/gemini-2.5-pro\",\n          \"largeOutputFallback\": \"openai/o4-mini-low\"\n        },\n        \"errorFallback\": \"openai/gpt-4.1\"\n      },\n      \"names\": \"openai/gpt-4.1-mini\",\n      \"commitMessages\": \"openai/gpt-4.1-mini\",\n      \"autoContinue\": \"openai/o4-mini-medium\"\n    }\n  ]\n}\n```\n\n### Model Pack Settings\n\n- `name` - The name of the model pack\n- `description` - A description of the model pack\n- `localProvider` - The local provider to default to for the model pack. Currently only `ollama` is supported. This must be set for the model pack to use local models via ollama.\n\nCustom model packs can be configured with the same [roles](./roles.md) as built-in model packs:\n\n- `planner` (required)\n- `architect` (optional, defaults to `planner`)\n- `coder` (optional, defaults to `planner`)\n- `summarizer` (required)\n- `builder` (required)\n- `wholeFileBuilder` (optional, defaults to `builder`)\n- `names` (optional)\n- `commitMessages` (optional)\n- `autoContinue` (optional)\n\n### Role Config\n\nFor each role set in the model pack, you can either use a simple string (the model ID) or a config object with these settings:\n\n- `modelId` - The model to use (required when using object form)\n- `temperature` - Controls randomness (0-2, role-specific defaults)\n- `topP` - Alternative randomness control (0-1)\n- `largeContextFallback` - Model to use when context is large\n- `largeOutputFallback` - Model to use when output needs to be large\n- `errorFallback` - Model to use if the primary model fails\n- `strongModel` - Stronger model for complex tasks\n\nWhen using a config object, all settings except `modelId` are optional.\n\n## Complete Example\n\nHere's a full example combining `providers`, `models`, and `modelPacks`:\n\n```json\n{\n  \"$schema\": \"https://plandex.ai/schemas/models-input.schema.json\",\n  \"providers\": [\n    {\n      \"name\": \"replicate\",\n      \"baseUrl\": \"https://api.replicate.com/v1\",\n      \"apiKeyEnvVar\": \"REPLICATE_API_KEY\"\n    }\n  ],\n  \"models\": [\n    {\n      \"modelId\": \"llama-4-maverick-70b\",\n      \"publisher\": \"meta\",\n      \"description\": \"Llama 4 Maverick 70B\",\n      \"defaultMaxConvoTokens\": 50000,\n      \"maxTokens\": 128000,\n      \"maxOutputTokens\": 8192,\n      \"reservedOutputTokens\": 8192,\n      \"preferredOutputFormat\": \"xml\",\n      \"providers\": [\n        {\n          \"provider\": \"custom\",\n          \"customProvider\": \"replicate\",\n          \"modelName\": \"meta/llama-4-maverick-70b\"\n        }\n      ]\n    }\n  ],\n  \"modelPacks\": [\n    {\n      \"name\": \"hybrid-local-cloud\",\n      \"description\": \"Local models for simple tasks, cloud for complex\",\n      \"planner\": \"anthropic/claude-opus-4\",\n      \"coder\": \"anthropic/claude-sonnet-4\",\n      \"architect\": \"anthropic/claude-sonnet-4\",\n      \"summarizer\": \"llama-4-maverick-70b\",\n      \"builder\": \"anthropic/claude-sonnet-4\",\n      \"names\": \"llama-4-maverick-70b\",\n      \"commitMessages\": \"llama-4-maverick-70b\",\n      \"autoContinue\": \"llama-4-maverick-70b\"\n    }\n  ]\n}\n```\n\n## Notes\n\n- All custom providers must be OpenAI-compatible.\n- Model IDs must be unique across built-in and custom models.\n- Provider names must be unique across built-in and custom providers."
  },
  {
    "path": "docs/docs/models/model-providers.md",
    "content": "---\nsidebar_position: 2\nsidebar_label: Providers\n---\n\n# Model Providers\n\nIf you use [Plandex Cloud](../hosting/cloud.md) in **Integrated Models Mode**, you can use Plandex credits to pay for AI models. No separate accounts or API keys are required in this case.\n\nIf you instead use **BYO API Key Mode** with Plandex Cloud, or if you self-host Plandex, you'll need to set API keys (or other credentials) for the providers you want to use.\n\nTo see available providers, run:\n\n```bash\n\\providers # REPL\nplandex providers # CLI\n```\n\n## API Keys / Environment Variables\n\nAPI keys or credentials are set through **environment variables** when running the Plandex CLI. For example:\n\n```bash\nexport OPENROUTER_API_KEY=...\nexport OPENAI_API_KEY=...\nexport ANTHROPIC_API_KEY=...\n\nplandex # start the Plandex REPL\n```\n\n## Provider Selection\n\nMany models can be served by multiple different providers. For example, OpenAI models are available via OpenAI, Microsoft Azure, and OpenRouter.\n\nWhen multiple providers are available for a model, which provider is selected depends on the authentication environment variables that are set when running the CLI or REPL. If environment variables are set for multiple providers, the direct provider takes precedence. For example, if you set both `ANTHROPIC_API_KEY` (for the direct Anthropic API) and `OPENROUTER_API_KEY` (for OpenRouter), Plandex will use the direct Anthropic API for Anthropic models.\n\n## Claude Pro/Max Subscription\n\nIf you have a Claude Pro or Max subscription, Plandex can use it when calling Anthropic models. [Learn more.](./claude-subscription.md)\n\n## Built-In Providers\n\n### OpenRouter\n\nApart from Plandex Cloud's Integrated Models Mode, the quickest way to get started is to use [OpenRouter.ai](https://openrouter.ai/), which allows you to use a wide range of models—including all those Plandex uses by default—with a single account and API key.\n\nTo use OpenRouter, you'll need to create an account and generate an API key, then set the `OPENROUTER_API_KEY` environment variable.\n\n```bash\nexport OPENROUTER_API_KEY=...\n\nplandex # start the Plandex REPL\n```\n\nYou can also use OpenRouter alongside other providers. For example, if you set both `OPENROUTER_API_KEY` and `OPENAI_API_KEY`, Plandex will use the OpenAI API directly for OpenAI models and OpenRouter for other models. Using direct providers offers slightly lower latency and costs about 5% less than OpenRouter.\n\nIf you set a `OPENROUTER_API_KEY` and are also using other providers, Plandex will also **fail over** to OpenRouter if another provider has an error. This offers a strong layer of redundancy since OpenRouter itself routes model calls across a number of different upstream providers.\n\n### OpenAI\n\nYou can optionally set an `OPENAI_API_KEY` to use the OpenAI API directly with your own OpenAI account when calling OpenAI models.\n\n```bash\nexport OPENAI_API_KEY=... # set your OpenAI API key for OpenAI models\nexport OPENAI_ORG_ID=... # optionally set your OpenAI OrgID if you have multiple orgs\n```\n\n### Anthropic\n\nYou can optionally set an `ANTHROPIC_API_KEY` to use the Anthropic API directly with your own Anthropic account when calling Anthropic models.\n\n```bash\nexport ANTHROPIC_API_KEY=... # set your Anthropic API key for Anthropic models\n```\n\n### Google AI Studio\n\nYou can optionally set a `GEMINI_API_KEY` to use Google Gemini models with your own Google account via Google AI Studio.\n\n```bash\nexport GEMINI_API_KEY=... # set your Google AI Studio API key for Google Gemini models\n```\n\n### Google Vertex AI\n\nYou can optionally use Google Vertex AI, which offers models from Gemini, Anthropic, and more. Vertex authentication requires a few environment variables to be set.\n\n```bash\nexport GOOGLE_APPLICATION_CREDENTIALS=... # either a path to a JSON file, the JSON itself as a string, or the base64 encoded JSON\nexport VERTEXAI_PROJECT=... # your Vertex project ID\nexport VERTEXAI_LOCATION=... # your Vertex location (e.g. us-east5)\n```\n\n### Azure\n\nYou can optionally use Microsoft Azure for OpenAI models. Azure authentication requires both an API key and a base URL.\n\n```bash\nexport AZURE_OPENAI_API_KEY=... # set your Azure OpenAI API key\nexport AZURE_API_BASE=... # set your Azure API base URL (required)\nexport AZURE_API_VERSION=... # optionally set your Azure API version - defaults to 2025-04-01-preview\nexport AZURE_DEPLOYMENTS_MAP='{\"gpt-4.1\": \"gpt-4.1-deployment-name\"}' # optionally set a map of model names to deployment names with a JSON object (only needed if deployment names are different from model names)\n```\n\n### AWS Bedrock\n\nYou can optionally use AWS Bedrock for Anthropic models.\n\nIf you have an AWS credentials file at `~/.aws/credentials`, you can use that to authenticate by setting the `PLANDEX_AWS_PROFILE` environment variable:\n\n```bash\nexport PLANDEX_AWS_PROFILE=... # set the name of the profile in ~/.aws/credentials to use\n```\n\nNote that the credentials file will _only_ be read if `PLANDEX_AWS_PROFILE` is set.\n\nYou can also use environment variables for AWS authentication:\n\n```bash\nexport AWS_ACCESS_KEY_ID=... # set your AWS access key ID\nexport AWS_SECRET_ACCESS_KEY=... # set your AWS secret access key\nexport AWS_REGION=... # set your AWS region (e.g. us-east-1)\n\nexport AWS_SESSION_TOKEN=... # optionally set your AWS session token\nexport AWS_INFERENCE_PROFILE_ARN=... # optionally set your AWS inference profile ARN\n```\n\n### DeepSeek\n\nYou can optionally use DeepSeek models with your own DeepSeek account.\n\n```bash\nexport DEEPSEEK_API_KEY=... # set your DeepSeek API key for DeepSeek models\n```\n\n### Perplexity\n\nYou can optionally use Perplexity models with your own Perplexity account.\n\n```bash\nexport PERPLEXITY_API_KEY=... # set your Perplexity API key for Perplexity models\n```\n\n## Custom Providers\n\nIf you're self-hosting Plandex, you can also use with models from any provider that provides an OpenAI-compatible API.\n\nTo configure custom providers, you can use a dev-friendly JSON config file:\n\n```bash\n\\models custom # REPL\nplandex models custom # CLI\n```\n\n[More details on custom providers](./custom-models.md)\n\n## Local Models\n\nPlandex supports local models via [Ollama](https://ollama.com/). It doesn't require any authentication or API keys.\n \nFor more details, see the [Ollama Quickstart](./ollama.md)."
  },
  {
    "path": "docs/docs/models/model-settings.md",
    "content": "---\nsidebar_position: 4\nsidebar_label: Settings\n---\n\n# Model Settings\n\nPlandex gives you a number of ways to control the models used in your plans. Changes to models are [version controlled](../core-concepts/version-control.md) and can be [branched](../core-concepts/branches.md).\n\n## `models` and `set-model`\n\nYou can see the current plan's models with the `models` command and change them with the `set-model` command.\n\n```bash\nplandex models # show the current models\nplandex set-model # select a model pack or configure model settings in JSON\n```\n\n## Model Defaults \n\n`set-model` updates model settings for the current plan. If you want to change the default model settings for all new plans, use `set-model default`.\n\n```bash\nplandex models default # show the default models for new plans\nplandex set-model default # select a default model pack for new plans or configure default model settings in JSON\n```\n\n## Model Settings JSON\n\nIf you select the 'edit JSON' option in either the `set-model` or `set-model default` commands, or you use the `--json` flag, you can edit the model settings in a JSON file in your preferred editor.\n\nThe models file lets you configure which model to use for each role, along with settings like temperature/top-p and fallback models. It uses a JSON schema, allowing most editors to provide autocomplete, validation, and inline documentation.\n\nModel roles can either be a string (the model ID) or an object with model config.\n\n### Basic Example\n\n```json\n{\n  \"$schema\": \"https://plandex.ai/schemas/model-pack-inline.schema.json\",\n  \"planner\": \"anthropic/claude-opus-4\",\n  \"coder\": \"anthropic/claude-sonnet-4\",\n  \"architect\": \"anthropic/claude-sonnet-4\",\n  \"summarizer\": \"anthropic/claude-3.5-haiku\",\n  \"builder\": \"anthropic/claude-sonnet-4\",\n  \"names\": \"anthropic/claude-3.5-haiku\",\n  \"commitMessages\": \"anthropic/claude-3.5-haiku\",\n  \"autoContinue\": \"anthropic/claude-3.5-haiku\"\n}\n```\n\n### Advanced Example\n\nYou can also configure individual role settings and fallbacks with an object:\n\n```json\n{\n  \"$schema\": \"https://plandex.ai/schemas/model-pack-inline.schema.json\",\n  \"planner\": {\n    \"modelId\": \"anthropic/claude-opus-4\",\n    \"temperature\": 0.7,\n    \"topP\": 0.9,\n    \"largeContextFallback\": \"google/gemini-2.5-pro\"\n  },\n  \"coder\": \"anthropic/claude-sonnet-4\",\n  \"architect\": \"anthropic/claude-sonnet-4\",\n  \"summarizer\": \"anthropic/claude-3.5-haiku\",\n  \"builder\": {\n    \"modelId\": \"anthropic/claude-sonnet-4\",\n    \"errorFallback\": \"openai/gpt-4.1\"\n  },\n  \"wholeFileBuilder\": {\n    \"modelId\": \"anthropic/claude-sonnet-4\",\n    \"largeContextFallback\": {\n      \"modelId\": \"google/gemini-2.5-pro\",\n      \"largeOutputFallback\": \"openai/o4-mini-low\"\n    }\n  },\n  \"names\": \"anthropic/claude-3.5-haiku\",\n  \"commitMessages\": \"anthropic/claude-3.5-haiku\",\n  \"autoContinue\": \"anthropic/claude-3.5-haiku\"\n}\n```\n\n### Role Config\n\nFor each role, you can either use a simple string (the model ID) or a config object with these settings:\n\n- `modelId` - The model to use (required when using object form)\n- `temperature` - Controls randomness (0-2, role-specific defaults)\n- `topP` - Alternative randomness control (0-1)\n- `largeContextFallback` - Model to use when context is large\n- `largeOutputFallback` - Model to use when output needs to be large\n- `errorFallback` - Model to use if the primary model fails\n- `strongModel` - Stronger model for complex tasks\n\nWhen using a config object, all settings except `modelId` are optional.\n\n## Local Provider\n\nYou can set the top-level `localProvider` key to `ollama` to use local models via [Ollama](https://ollama.com/):\n\n```json\n{\n  \"$schema\": \"https://plandex.ai/schemas/model-pack-inline.schema.json\",\n  \"localProvider\": \"ollama\",\n  \"planner\": \"ollama/deepseek-r1:14b\",\n  ...\n}\n```\n"
  },
  {
    "path": "docs/docs/models/models-overview.md",
    "content": "---\nsidebar_position: 1\nsidebar_label: Overview\n---\n\n# Models Overview\n\nBy default, Plandex uses a mix of Anthropic, OpenAI, and Google models. While this is a good starting point, and is the recommended way to use Plandex for most users, full customization of models and providers is also supported.\n\n## Roles and Model Packs\n\nPlandex has multiple [roles](./roles.md) which are responsible for different aspects of planning, coding, and applying changes. Each role can be assigned a different model. A **model pack** is a mapping of roles to specific models.\n\n## Built-in Models and Model Packs\n\nPlandex provides a curated set of built-in models and model packs.\n\nYou can see the list of available model packs with:\n\n```bash\n\\model-packs # REPL\nplandex model-packs # CLI\n```\n\nYou can see the details of a specific model pack with:\n\n```bash\n\\model-packs show model-pack-name # REPL\nplandex model-packs show model-pack-name # CLI\n```\n\nYou can see the list of available models with:\n\n```bash\n\\models available # REPL\nplandex models available # CLI\n```\n\n## Model Settings\n\nYou can see the model settings for the current plan with:\n\n```bash\n\\models # REPL\nplandex models # CLI\n```\n\nAnd you can see the default model settings for new plans with:\n\n```bash\n\\models default # REPL\nplandex models default # CLI\n```\n\nYou can change the model settings for the current plan with:\n\n```bash\n\\set-model # REPL\nplandex set-model # CLI\n```\n\nAnd you can set the default model settings for new plans with:\n\n```bash\n\\set-model default # REPL\nplandex set-model default # CLI\n```\n\n[More details on model settings](./model-settings.md)\n\n## Providers\n\nPlandex offers flexibility on the providers you can use to serve models.\n\nIf you use [Plandex Cloud](../hosting/cloud.md) in **Integrated Models Mode**, you can use Plandex credits to pay for AI models. In that case, you won't need to worry about providers, provider accounts, or API keys.\n\nIf instead you use **BYO API Key Mode** with Plandex Cloud, or if you [self-host](../hosting/self-hosting/local-mode-quickstart.md) Plandex, you'll need to set API keys (or other credentials) for the providers you want to use. Multiple built-in providers are supported. \n\nIf you're self-hosting, you can also configure custom providers—they just need to be OpenAI-compatible.\n\n[More details on providers](./model-providers.md)\n\n## Custom Models, Providers, and Model Packs\n\nYou can configure custom models, providers, and model packs with a dev-friendly JSON config file:\n\n```bash\n\\models custom # REPL\nplandex models custom # CLI\n```\n\n[More details on custom models, providers, and model packs](./custom-models.md)\n\n## Model Performance\n\nWhile you can use Plandex with many different providers and models as described above, Plandex's prompts have mainly been written and tested against the built-in models and model packs, so you should expect them to give the best results.\n\n## Local Models\n\nPlandex supports local models via [Ollama](https://ollama.com/). For more details, see the [Ollama Quickstart](./ollama.md).\n\n\n"
  },
  {
    "path": "docs/docs/models/ollama.md",
    "content": "---\nsidebar_position: 8\nsidebar_label: Ollama Quickstart\n---\n\n# Ollama Quickstart\n\nPlandex works with [Ollama](https://ollama.com/) models. To use them, you need to [self-host Plandex.](../hosting/self-hosting/local-mode-quickstart.md)\n**Ollama isn't supported with Plandex Cloud.**\n\n## Disclaimer\n\nWhile local models are supported via Ollama, small models that can be run locally often aren't strong enough to produce usable results for the [heavy-lifting roles](./roles.md) like `planner`, `architect`, `coder`, and `builder`. The prompts for these roles require strong instruction following that can be hard to achieve with small models.\n\nThe strongest open source models _are_ capable enough for decent results, but these models are quite large for running locally without a very powerful system. This isn't meant to discourage experimentation with local models, but to set expectations for what is achievable.\n\nTo help bridge the gap as local models continue to improve their capabilities, two 'adaptive' model packs are available: `ollama-oss` and `ollama-daily`. These model packs use local Ollama models for less demanding roles, plus larger remote models for heavy-lifting (open source models for the `oss` variant, and the same models used in the default `daily-driver` model pack for the `daily` variant).\n\nOver time, as local models improve, these adaptive model packs will be updated to use local models for more roles.\n\nThere's also a built-in experimental `ollama` model pack that uses local models for all roles—this is recommended for testing and benchmarking, but not (yet) for getting real work done.\n\n## System requirements\n\nTo use Ollama models, you need enough system resources to run the models you want to use. To use the built-in experimental `ollama` model pack (with qwen3:32b as the largest model), at least 32GB of RAM is recommended as an absolute minimum—48GB or more is recommended for breathing room. For the `ollama-daily` and `ollama-oss` model packs (with devstral:24b as the largest model), at least 16GB of RAM is recommended as an absolute minimum—24GB or more is recommended for breathing room.\n\nIf you use [custom models and a custom model pack](./custom-models.md), you'll have full flexibility to choose the appropriate models for your system. Just remember that running Plandex prompts successfully is a challenge for even the largest local models.\n\n## Install and run Ollama\n\n[Download and install ollama](https://ollama.com/download) for your platform.\n\nThen make sure the Ollama server is running:\n\n```bash\nollama serve\n```\n\n## Pull Ollama models\n\nPull the models you want to use. \n\nFor the built-in `ollama` model pack, pull the following models:\n\n```bash\nollama pull qwen3:8b\nollama pull qwen3:14b\nollama pull qwen3:32b\nollama pull devstral:24b\n```\n\nFor the `ollama-daily` and `ollama-oss` model packs, pull the following models:\n\n```bash\nollama pull qwen3:8b\nollama pull devstral:24b\n```\n\n## Use Ollama in Plandex\n\n### Built-in model packs\n\nTo use one of the built-in Ollama model packs in Plandex, decide whether you want to use `ollama`, which uses local models for all roles, but may struggle in practice, `ollama-daily`, which uses local models for less demanding roles, plus the default Plandex models from the `daily-driver` model pack for heavy-lifting, or `ollama-oss`, which uses local models for less demanding roles, plus the open source Plandex models from the `oss` model pack for heavy-lifting.\n\n```bash\n\\set-model ollama # REPL\nplandex set-model ollama # CLI\n```\n\nOr:\n\n```bash\n\\set-model ollama-daily # REPL\n\\set-model ollama-oss\nplandex set-model ollama-daily # CLI\nplandex set-model ollama-oss\n```\n\n### Custom models and model packs\n\nYou can also setup [custom models and model packs](./custom-models.md) for use with Ollama.\n\nWhen configuring a custom model, be sure you add the `ollama` provider to the `providers` array with the `modelName` set to the name of the model you want to use, exactly as it appears in the [Ollama model list](https://ollama.com/models), prefixed with `ollama_chat/`. For example, to use the `qwen3:32b` model, you would add the following to the `providers` array:\n\n```json\n\"providers\": [\n  {\n    \"provider\": \"ollama\",\n    \"modelName\": \"ollama_chat/qwen3:32b\"\n  }\n]\n```\n\nWhen configuring a custom model pack to use Ollama, set the top-level `localProvider` key to `ollama`. For example:\n\n```json\n{\n  ...\n  \"modelPacks\": [\n    {\n      \"localProvider\": \"ollama\",\n      ...\n    }\n  ]\n}\n```\n\n## Contributors\n\nIf you experiment with Plandex and Ollama and you find model combinations that work better than the built-in model packs, please chime in on [Discord](https://discord.gg/plandex-ai) or [open a PR](https://github.com/plandex-ai/plandex/pulls). The world of local models moves fast, and we can't always keep up with the cutting edge ourselves, so it would be great to have community help on filling in the gaps. With your help, we'd love to get to a place where Plandex can work effectively with 100% local models."
  },
  {
    "path": "docs/docs/models/roles.md",
    "content": "---\nsidebar_position: 6\nsidebar_label: Roles\n---\n\n# Model Roles\n\nPlandex has multiple **roles** that are used for different aspects of its functionality.\n\n## Roles\n\n### `planner`\n\nThis is the 'main' role that replies to prompts and makes plans.\n\nCan optionally have a 'large context fallback' set, which is the model to use when the context input limit is exceeded.\n\n### `architect`\n\nWhen 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.\n\nThis role is optional. It falls back to the `planner` role if not set.\n\n### `coder`\n\nThis 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.\n\nThis role is optional. It falls back to the `planner` role if not set.\n\n### `summarizer`\n\nSummarizes conversations to stay under the limit set in `max-convo-tokens`.\n\n### `auto-continue`\n\nDetermines whether a plan is finished or should automatically continue based on the previous response.\n\n### `builder`\n\nBuilds the proposed changes described by the `planner` role into pending file updates.\n\n### `whole-file-builder`\n\nBuilds 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.\n\n### `names`\n\nGives automatically-generated names to plans and context.\n\n### `commit-messages`\n\nAutomatically generates commit messages for a set of pending updates."
  },
  {
    "path": "docs/docs/quick-start.md",
    "content": "---\nsidebar_position: 2\nsidebar_label: Quickstart\n---\n\n# Quickstart\n\n## Install Plandex\n\n```bash\ncurl -sL https://plandex.ai/install.sh | bash\n```\n\n[Click here for more installation options.](./install.md)\n\nNote that Windows is supported via [WSL](https://learn.microsoft.com/en-us/windows/wsl/about). Plandex only works correctly on Windows in the WSL shell. It doesn't work in the Windows CMD prompt or PowerShell.\n\n## Hosting Options\n\n| Option                     | Description                                                                                                                                                                                                                                                                                                           |\n| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Plandex Cloud**          | Winding down as of 10/3/2025 and no longer accepting new users. <a href=\"https://plandex.ai/blog/winding-down\">Learn more.</a>                                                                                                                                                                                        |\n| **Self-hosted/Local Mode** | • Run Plandex locally with Docker or host on your own server.<br/>• Use your own [OpenRouter.ai](https://openrouter.ai) key (or [other model provider](./models/model-providers.md) accounts and API keys).<br/>• Follow the [local-mode quickstart](./hosting/self-hosting/local-mode-quickstart.md) to get started. |\n\nIf you're going with a 'BYO API Key' option above (whether cloud or self-hosted), decide on the model provider(s) you want to use. The quickest option is to use OpenRouter.ai, but you can also use [many other providers](./models/model-providers.md).\n\nIf you're using OpenRouter.ai, first [sign up here.](https://openrouter.ai/signup) Then [generate an API key here.](https://openrouter.ai/keys) Set the `OPENROUTER_API_KEY` environment variable:\n\n```bash\nexport OPENROUTER_API_KEY=...\n```\n\n## Get Started\n\nIf you're starting on a new project, make a directory first:\n\n```bash\nmkdir your-project-dir\n```\n\nNow `cd` into your **project's directory.**\n\n```bash\ncd your-project-dir\n```\n\nThen just give a quick the REPL help text a quick read, and you're ready go. The REPL starts in _chat mode_ by default, which is good for fleshing out ideas before moving to implementation. Once the task is clear, Plandex will prompt you to switch to _tell mode_ to make a detailed plan and start writing code.\n\n```bash\nplandex\n```\n\nor for short:\n\n```bash\npdx\n```\n\nThen just give the REPL help text a quick read, and you're ready go.\n"
  },
  {
    "path": "docs/docs/repl.md",
    "content": "---\nsidebar_position: 4\nsidebar_label: REPL\n---\n\n# REPL\n\nThe Plandex REPL is a developer-friendly chat interface. It's the easiest way to use Plandex.\n\n## Start\n\nTo start the REPL, just run `plandex` or `pdx` in any project directory (or sub-directory).\n\nIf you don't have a current plan in the directory, the REPL will create a new one. Otherwise, it will use the current plan.\n\n## Commands\n\nAll Plandex CLI commands are available in the REPL. Just type a backslash (`\\`) followed by the command.\n\nThe REPL also has a few special commands of its own:\n\n- `\\quit` or `\\q` to quit the REPL\n- `\\help` or `\\h` for help\n- `@` plus a relative file path for loading files into context (note that if you're using auto-context mode, loading files yourself is optional)\n- `\\run` or `\\r` plus a relative file path for using a file as a prompt\n- `\\chat` or `\\ch` to switch to chat mode and have a conversation without making changes\n- `\\tell` or `\\t` to switch to tell mode and implement tasks\n- `\\multi` or `\\m` to switch to multi-line mode\n- `\\send` or `\\s` to send the current prompt to Plandex (for sending a prompt in multi-line mode, since enter gives you a newline)\n\n## REPL Flags\n\nThe REPL has a few convenient flags you can use to start it with different modes, autonomy settings, and model packs. You can pass any of these to `plandex` or `pdx` when starting the REPL.\n\n```\n  Mode\n    --chat, -c     Start in chat mode (for conversation without making changes)\n    --tell, -t     Start in tell mode (for implementation)\n\n  Autonomy\n    --no-auto      None → step-by-step, no automation\n    --basic        Basic → auto-continue plans, no other automation\n    --plus         Plus → auto-update context, smart context, auto-commit changes\n    --semi         Semi-Auto → auto-load context\n    --full         Full-Auto → auto-apply, auto-exec, auto-debug\n\n  Models\n    --daily        Daily driver pack (default models, balanced capability, cost, and speed)\n    --strong       Strong pack (more capable models, higher cost and slower)\n    --cheap        Cheap pack (less capable models, lower cost and faster)\n    --oss          Open source pack (open source models)\n\n    --gemini-planner       Gemini pack (Gemini 2.5 Pro for planning, default models for other roles)\n    --o3-planner           OpenAI o3-medium for planning, default models for other roles\n    --r1-planner           DeepSeek R1 for planning, default models for other roles\n    --perplexity-planner   Perplexity for planning, default models for other roles\n    --opus-planner         Anthropic Opus 4 for planning, default models for other roles\n\n```\n"
  },
  {
    "path": "docs/docs/security.md",
    "content": "---\nsidebar_position: 9\nsidebar_label: Security\n---\n\n# Security\n## Ignoring Sensitive Files\n\nPlandex respects `.gitignore` and won't load any files that you're ignoring unless you use the `--force/-f` flag with `plandex load`. You can also add a `.plandexignore` file with ignore patterns to any directory.\n\n## API Key Security\n\nWhen [self-hosting](./hosting/self-hosting/local-mode-quickstart.md) or using [Plandex Cloud](./hosting/cloud.md) in BYO API Key Mode, API keys are only stored ephemerally in RAM while they are in active use. They are never written to disk, logged, or stored in a database. As soon as a plan stream ends, the API key is removed from memory and no longer exists anywhere on the Plandex server.\n\nIt's also up to you to manage your API keys securely. Try to avoid storing them in multiple places, exposing them to third party services, or sending them around in plain text.\n\nYou may also want to consider using Plandex Cloud in [Integrated Models Mode](./hosting/cloud.md#integrated-models-mode), which lets you skip dealing with API keys at all."
  },
  {
    "path": "docs/docusaurus.config.ts",
    "content": "import {themes as prismThemes} from 'prism-react-renderer';\nimport type {Config} from '@docusaurus/types';\nimport type * as Preset from '@docusaurus/preset-classic';\n// import search from \"docusaurus-lunr-search\"\n// import redirect from \"@docusaurus/plugin-client-redirects\"\n\nconst config: Config = {\n  title: 'Plandex Docs',\n  tagline: 'An AI coding engine for large, real-world tasks',\n  favicon: 'img/favicon.ico',\n\n  // Set the production url of your site here\n  url: 'https://docs.plandex.ai',\n  // Set the /<baseUrl>/ pathname under which your site is served\n  // For GitHub pages deployment, it is often '/<projectName>/'\n  baseUrl: '/',\n\n  onBrokenLinks: 'throw',\n  onBrokenMarkdownLinks: 'warn',\n\n  // Even if you don't use internationalization, you can use this field to set\n  // useful metadata like html lang. For example, if your site is Chinese, you\n  // may want to replace \"en\" with \"zh-Hans\".\n  i18n: {\n    defaultLocale: 'en',\n    locales: ['en'],\n  },\n\n  presets: [\n    [\n      'classic',\n      {\n        docs: {\n          sidebarPath: './sidebars.ts',\n          routeBasePath: '/', // Serve the docs at the site's root\n          editUrl:\n            'https://github.com/plandex-ai/plandex/tree/main/docs/',\n        },\n        blog: false, // Disable the blog\n        theme: {\n          customCss: './src/css/custom.css',\n        },\n        \n      } satisfies Preset.Options,\n    ],\n  ],\n  themeConfig: {\n    // Replace with your project's social card\n    image: 'img/plandex-social-preview.png',\n    colorMode: {\n      defaultMode: \"dark\",\n    },  \n    navbar: {\n      title: '',\n      logo: {\n        alt: 'Plandex Logo',\n        src: 'img/plandex-logo-light.png',\n        srcDark: 'img/plandex-logo-dark.png',\n        href: \"https://plandex.ai\",\n        height: \"2.7rem\",\n      },\n      items: [\n        {\n          href: 'https://github.com/plandex-ai/plandex',\n          label: 'GitHub',\n          position: 'right',\n        },\n        {\n          label: 'Discord',\n          href: 'https://discord.gg/plandex-ai',\n          position: 'right',\n        },\n        {\n          label: 'X',\n          href: 'https://x.com/PlandexAI',\n          position: 'right',\n        },\n        {\n          label: 'YouTube',\n          href: 'https://www.youtube.com/@plandex-ny5ry',\n          position: 'right',\n        },\n      ],\n    },\n    footer: {\n      style: 'dark',      \n      copyright: `Copyright © ${new Date().getFullYear()} PlandexAI, Inc.`,\n    },\n    prism: {\n      theme: prismThemes.github,\n      darkTheme: prismThemes.dracula,\n    },\n\n    algolia: {\n      // The application ID provided by Algolia\n      appId: 'EG57NOYLYX',\n      // Public API key: it is safe to commit it\n      apiKey: 'a811f8bcdd87a8b3fe7f22a353b968ef',\n      indexName: 'plandex',\n    }\n  } satisfies Preset.ThemeConfig,\n\n  // plugins: [\n  //   search,\n  //   // [\n  //   //   '@docusaurus/plugin-client-redirects',\n  //   //   { redirects: [{ from: '/', to: '/intro'}] },\n  //   // ],\n  // ]\n};\n\nexport default config;\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\",\n    \"typecheck\": \"tsc\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"3.4.0\",\n    \"@docusaurus/plugin-client-redirects\": \"^3.4.0\",\n    \"@docusaurus/preset-classic\": \"3.4.0\",\n    \"@docusaurus/theme-search-algolia\": \"^3.4.0\",\n    \"@mdx-js/react\": \"^3.0.0\",\n    \"clsx\": \"^2.0.0\",\n    \"docusaurus-lunr-search\": \"^3.4.0\",\n    \"prism-react-renderer\": \"^2.3.0\",\n    \"react\": \"^18.0.0\",\n    \"react-dom\": \"^18.0.0\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/module-type-aliases\": \"3.4.0\",\n    \"@docusaurus/tsconfig\": \"3.4.0\",\n    \"@docusaurus/types\": \"3.4.0\",\n    \"typescript\": \"~5.2.2\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 3 chrome version\",\n      \"last 3 firefox version\",\n      \"last 5 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=18.0\"\n  }\n}\n"
  },
  {
    "path": "docs/sidebars.ts",
    "content": "import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';\n\n/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that group\n - provide next/previous navigation\n\n The sidebars can be generated from the filesystem, or explicitly defined here.\n\n Create as many sidebars as you want.\n */\nconst sidebars: SidebarsConfig = {\n  // By default, Docusaurus generates a sidebar from the docs folder structure\n  tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],\n\n  // But you can create a sidebar manually\n  /*\n  tutorialSidebar: [\n    'intro',\n    'hello',\n    {\n      type: 'category',\n      label: 'Tutorial',\n      items: ['tutorial-basics/create-a-document'],\n    },\n  ],\n   */\n};\n\nexport default sidebars;\n"
  },
  {
    "path": "docs/src/css/custom.css",
    "content": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framework designed to\n * work well for content-centric websites.\n */\n\n/* You can override the default Infima variables here. */\n:root {\n  --ifm-color-primary: #2e8555;\n  --ifm-color-primary-dark: #29784c;\n  --ifm-color-primary-darker: #277148;\n  --ifm-color-primary-darkest: #205d3b;\n  --ifm-color-primary-light: #33925d;\n  --ifm-color-primary-lighter: #359962;\n  --ifm-color-primary-lightest: #3cad6e;\n  --ifm-code-font-size: 95%;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);\n}\n\n/* For readability concerns, you should choose a lighter palette in dark mode. */\n[data-theme='dark'] {\n  --ifm-color-primary: #25c2a0;\n  --ifm-color-primary-dark: #21af90;\n  --ifm-color-primary-darker: #1fa588;\n  --ifm-color-primary-darkest: #1a8870;\n  --ifm-color-primary-light: #29d5b0;\n  --ifm-color-primary-lighter: #32d8b4;\n  --ifm-color-primary-lightest: #4fddbf;\n  --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);\n}\n\n.navbar__logo img {\n  height: 145%;\n  margin-top: -6px;\n}"
  },
  {
    "path": "docs/static/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/static/_redirects",
    "content": "/ /install 301!"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  // This file is not used in compilation. It is here just for a nice editor experience.\n  \"extends\": \"@docusaurus/tsconfig\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\"\n  }\n}\n"
  },
  {
    "path": "plans/invite-commands.txt",
    "content": "Let's add the following commands to the 'app/cli/cmd' directory.\n\nFor all commands, look at the cli/types/api.go and shared/req_res.go files for the API request and response types.\n\nCommands:\n\ninvite.go - invite a new user to the org. look at the 'checkout' command on accepting optional parameters or prompting if parameters aren't provided.\n\nusers.go - list all users in the org, as well as all pending invites in the org, then list them in a table like the one in the 'plans' command.\n\nrevoke.go - revoke an invite or remove a user from the org. optionally accept an email parameter. if email is supplied, revoke the user or invite with that email. if email is not supplied, prompt the user to select a user or invite to revoke, similar to how branches are selected in the 'checkout' command.\n\n\n\n"
  },
  {
    "path": "plans/model-sets-custom-models-crud.txt",
    "content": "I want to add functionality on both the CLI (cobra commands, api calls) and server (routes, handlers, db access functions) that allow a user to create, list, and delete custom models, and also create, list, or delete model sets for their org. I also want to add commands that list all the available models and model sets, including both built-in and user-created models and model sets.\n\nUse the existing functionality for models and model sets as a guide and follow the same architecture, coding style, and ux.\n\nHere are the commands/subcommands I want to add:\n\n`plandex models available` - list all available models, both built-in and custom, in a nicely formatted table (use tablewriter like the other commands)\n\n`plandex models create` - use terminal prompts in a similar way to the 'set-model' command to prompt the user for all necessary values to create a custom model, then call the api function to store it on the server\n\n`plandex models delete` - prompt the user to choose from a list of custom models to delete \n\n`plandex model-sets` - list all available model sets, both built-in and user-created\n\n`plandex model-sets create` - use terminal prompts in a similar way to the 'set-model' command to prompt the user for all necessary values to create a model set, then call the api function to store it on the server\n\nAdd all the required CLI and server code to make this work. Put the server-side handlers in 'app/server/handlers/models.go'. Create a new file in 'app/server/db/' for the db access functions.\n\nOn the client-side, update the Api interface and implementation to add the new api calls.\n\n\n"
  },
  {
    "path": "plans/pdx-file.md",
    "content": ""
  },
  {
    "path": "plans/race_cond_chatgpt.txt",
    "content": "Based on conversation below with ChatGPT, I want you to fix the race condition as described with a retry mechanism similar to deadlock handling. I want it to be absolutely fucking impossible to have a race condition no matter the fuck what.\n\nConvo with chatgpt:\n\nlocks.go:\n\n```\npackage db\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"github.com/lib/pq\"\n)\n\nconst lockHeartbeatInterval = 700 * time.Millisecond\nconst lockHeartbeatTimeout = 4 * time.Second\n\n// distributed locking to ensure only one user can write to a plan repo at a time\n// multiple readers are allowed, but read locks block writes\n// write lock is exclusive (blocks both reads and writes)\n\ntype LockRepoParams struct {\n\tOrgId       string\n\tUserId      string\n\tPlanId      string\n\tBranch      string\n\tScope       LockScope\n\tPlanBuildId string\n\tCtx         context.Context\n\tCancelFn    context.CancelFunc\n}\n\nfunc LockRepo(params LockRepoParams) (string, error) {\n\treturn lockRepo(params, 0)\n}\n\nfunc lockRepo(params LockRepoParams, numRetry int) (string, error) {\n\tlog.Println(\"locking repo\")\n\t// spew.Dump(params)\n\n\torgId := params.OrgId\n\tuserId := params.UserId\n\tplanId := params.PlanId\n\tbranch := params.Branch\n\tscope := params.Scope\n\tplanBuildId := params.PlanBuildId\n\tctx := params.Ctx\n\tcancelFn := params.CancelFn\n\n\ttx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error starting transaction: %v\", err)\n\t}\n\n\t// Ensure that rollback is attempted in case of failure\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tif rbErr := tx.Rollback(); rbErr != nil {\n\t\t\t\tlog.Printf(\"transaction rollback error: %v\\n\", rbErr)\n\t\t\t} else {\n\t\t\t\tlog.Println(\"transaction rolled back\")\n\t\t\t}\n\t\t}\n\t}()\n\n\tquery := \"SELECT id, org_id, user_id, plan_id, plan_build_id, scope, branch, created_at FROM repo_locks WHERE plan_id = $1 FOR UPDATE\"\n\tqueryArgs := []interface{}{planId}\n\n\tvar locks []*repoLock\n\n\tfn := func() error {\n\t\trows, err := tx.Query(query, queryArgs...)\n\t\tif err != nil {\n\t\t\tif pqErr, ok := err.(*pq.Error); ok && (pqErr.Code == \"40001\" || pqErr.Code == \"40P01\") {\n\t\t\t\t// return concurrency errors directly for retries\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"error getting repo locks: %v\", err)\n\t\t}\n\n\t\tdefer rows.Close()\n\n\t\tvar expiredLockIds []string\n\n\t\tnow := time.Now()\n\t\tfor rows.Next() {\n\t\t\tvar lock repoLock\n\t\t\tif err := rows.Scan(&lock.Id, &lock.OrgId, &lock.UserId, &lock.PlanId, &lock.PlanBuildId, &lock.Scope, &lock.Branch, &lock.CreatedAt); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error scanning repo lock: %v\", err)\n\t\t\t}\n\n\t\t\t// ensure heartbeat hasn't timed out\n\t\t\tif now.Sub(lock.LastHeartbeatAt) < lockHeartbeatTimeout {\n\t\t\t\tlocks = append(locks, &lock)\n\t\t\t} else {\n\t\t\t\texpiredLockIds = append(expiredLockIds, lock.Id)\n\t\t\t}\n\t\t}\n\n\t\tif len(expiredLockIds) > 0 {\n\t\t\tquery := \"DELETE FROM repo_locks WHERE id = ANY($1)\"\n\t\t\t_, err := tx.Exec(query, pq.Array(expiredLockIds))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error removing expired locks: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\terr = fn()\n\tif err != nil {\n\t\tif pqErr, ok := err.(*pq.Error); ok && (pqErr.Code == \"40001\" || pqErr.Code == \"40P01\") {\n\t\t\tif numRetry > 10 {\n\t\t\t\terr = fmt.Errorf(\"plan is currently being updated by another user\")\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\tlog.Printf(\"Serialization or deadlock error, retrying transaction: %v\\n\", err)\n\n\t\t\twait := time.Duration(numRetry) * 300 * time.Millisecond * time.Duration(rand.Intn(500)*int(time.Millisecond))\n\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn \"\", fmt.Errorf(\"context finished during retry transaction\")\n\t\t\tcase <-time.After(wait):\n\t\t\t\treturn lockRepo(params, numRetry+1)\n\t\t\t}\n\t\t}\n\n\t\treturn \"\", err\n\t}\n\n\tcanAcquire := true\n\tcanRetry := true\n\n\t// log.Println(\"locks:\")\n\t// spew.Dump(locks)\n\n\tfor _, lock := range locks {\n\t\tlockBranch := \"\"\n\t\tif lock.Branch != nil {\n\t\t\tlockBranch = *lock.Branch\n\t\t}\n\n\t\tif scope == LockScopeRead {\n\t\t\tcanAcquireThisLock := lock.Scope == LockScopeRead && lockBranch == branch\n\t\t\tif !canAcquireThisLock {\n\t\t\t\tcanAcquire = false\n\t\t\t}\n\t\t} else if scope == LockScopeWrite {\n\t\t\tcanAcquire = false\n\n\t\t\t// if lock is for the same plan plan and branch, allow parallel writes\n\t\t\tif planId == lock.PlanId && branch == lockBranch {\n\t\t\t\tcanAcquire = true\n\t\t\t}\n\n\t\t\tif lock.Scope == LockScopeWrite && lockBranch == branch {\n\t\t\t\tcanRetry = false\n\t\t\t}\n\t\t} else {\n\t\t\terr = fmt.Errorf(\"invalid lock scope: %v\", scope)\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif !canAcquire {\n\t\tlog.Println(\"can't acquire lock. canRetry:\", canRetry, \"numRetry:\", numRetry)\n\n\t\tif canRetry {\n\t\t\t// 10 second timeout\n\t\t\tif numRetry > 20 {\n\t\t\t\terr = fmt.Errorf(\"plan is currently being updated by another user\")\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\treturn lockRepo(params, numRetry+1)\n\t\t}\n\t\terr = fmt.Errorf(\"plan is currently being updated by another user\")\n\t\treturn \"\", err\n\t}\n\n\t// Insert the new lock\n\tvar lockPlanBuildId *string\n\tif planBuildId != \"\" {\n\t\tlockPlanBuildId = &planBuildId\n\t}\n\n\tvar lockBranch *string\n\tif branch != \"\" {\n\t\tlockBranch = &branch\n\t}\n\n\tnewLock := &repoLock{\n\t\tOrgId:       orgId,\n\t\tUserId:      userId,\n\t\tPlanId:      planId,\n\t\tPlanBuildId: lockPlanBuildId,\n\t\tScope:       scope,\n\t\tBranch:      lockBranch,\n\t}\n\t// log.Println(\"newLock:\")\n\t// spew.Dump(newLock)\n\n\tinsertQuery := \"INSERT INTO repo_locks (org_id, user_id, plan_id, plan_build_id, scope, branch) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id\"\n\terr = tx.QueryRow(\n\t\tinsertQuery,\n\t\tnewLock.OrgId,\n\t\tnewLock.UserId,\n\t\tnewLock.PlanId,\n\t\tnewLock.PlanBuildId,\n\t\tnewLock.Scope,\n\t\tnewLock.Branch,\n\t).Scan(&newLock.Id)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error inserting new lock: %v\", err)\n\t}\n\n\t// check if git lock file exists\n\t// remove it if so\n\terr = gitRemoveIndexLockFileIfExists(getPlanDir(orgId, planId))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error removing lock file: %v\", err)\n\t}\n\n\tbranches, err := GitListBranches(orgId, planId)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting branches: %v\", err)\n\t}\n\n\tlog.Println(\"branches:\", branches)\n\n\tif branch != \"\" {\n\t\t// checkout the branch\n\t\terr = gitCheckoutBranch(getPlanDir(orgId, planId), branch)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error checking out branch: %v\", err)\n\t\t}\n\t}\n\n\t// Commit the transaction\n\tif err = tx.Commit(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error committing transaction: %v\", err)\n\t}\n\n\t// Start a goroutine to keep the lock alive\n\tgo func() {\n\t\tnumErrors := 0\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\t// log.Printf(\"case <-stream.Ctx.Done(): %s\\n\", newLock.Id)\n\t\t\t\terr := UnlockRepo(newLock.Id)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error unlocking repo: %v\\n\", err)\n\t\t\t\t}\n\t\t\t\treturn\n\n\t\t\tdefault:\n\t\t\t\tres, err := Conn.Exec(\"UPDATE repo_locks SET last_heartbeat_at = NOW() WHERE id = $1\", newLock.Id)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error updating repo lock last heartbeat: %v\\n\", err)\n\t\t\t\t\tnumErrors++\n\n\t\t\t\t\tif numErrors > 5 {\n\t\t\t\t\t\tlog.Printf(\"Too many errors updating repo lock last heartbeat: %v\\n\", err)\n\t\t\t\t\t\tcancelFn()\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// check if 0 rows were updated\n\t\t\t\trowsAffected, err := res.RowsAffected()\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Error getting rows affected: %v\\n\", err)\n\t\t\t\t\tcancelFn()\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif rowsAffected == 0 {\n\t\t\t\t\tlog.Printf(\"Lock not found: %s | stopping heartbeat loop\\n\", newLock.Id)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(lockHeartbeatInterval)\n\t\t\t}\n\n\t\t}\n\t}()\n\n\tlog.Println(\"repo locked. id:\", newLock.Id)\n\n\treturn newLock.Id, nil\n}\n\nfunc UnlockRepo(id string) error {\n\tlog.Println(\"unlocking repo:\", id)\n\n\tquery := \"DELETE FROM repo_locks WHERE id = $1\"\n\t_, err := Conn.Exec(query, id)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error removing lock: %v\", err)\n\t}\n\n\tlog.Println(\"repo unlocked successfully:\", id)\n\n\treturn nil\n}\n```\n\ngit.go:\n\n\n```\npackage db\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n)\n\nfunc init() {\n\t// ensure git is available\n\tcmd := exec.Command(\"git\", \"--version\")\n\tif err := cmd.Run(); err != nil {\n\t\tpanic(fmt.Errorf(\"error running git --version: %v\", err))\n\t}\n}\n\nfunc InitGitRepo(orgId, planId string) error {\n\tdir := getPlanDir(orgId, planId)\n\treturn initGitRepo(dir)\n}\n\nfunc initGitRepo(dir string) error {\n\t// Set the default branch name to 'main' for the new repository\n\tres, err := exec.Command(\"git\", \"-C\", dir, \"init\", \"-b\", \"main\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error initializing git repository with 'main' as default branch for dir: %s, err: %v, output: %s\", dir, err, string(res))\n\t}\n\n\t// Configure user name and email for the repository\n\tif err := setGitConfig(dir, \"user.email\", \"server@plandex.ai\"); err != nil {\n\t\treturn err\n\t}\n\tif err := setGitConfig(dir, \"user.name\", \"Plandex\"); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc GitAddAndCommit(orgId, planId, branch, message string) error {\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitAdd(dir, \".\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\terr = gitCommit(dir, message)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn nil\n}\n\n// func GitAddAndAmendCommit(orgId, planId, branch, addMessage string) error {\n// \tdir := getPlanDir(orgId, planId)\n\n// \terr := gitAdd(dir, \".\")\n// \tif err != nil {\n// \t\treturn fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v\", dir, err)\n// \t}\n\n// \t// Get the latest commit message\n// \t_, latestCommitMsg, err := getLatestCommit(dir)\n// \tif err != nil {\n// \t\treturn fmt.Errorf(\"error getting latest commit message for dir: %s, err: %v\", dir, err)\n// \t}\n\n// \t// Amend the latest commit with the new message\n// \tmessage := latestCommitMsg + \"\\n\\n\" + addMessage\n// \tres, err := exec.Command(\"git\", \"-C\", dir, \"commit\", \"--amend\", \"-m\", message).CombinedOutput()\n\n// \tif err != nil {\n// \t\treturn fmt.Errorf(\"error amending commit for dir: %s, err: %v, output: %s\", dir, err, string(res))\n// \t}\n\n// \treturn nil\n// }\n\nfunc GitRewindToSha(orgId, planId, branch, sha string) error {\n\tdir := getPlanDir(orgId, planId)\n\n\terr := gitRewindToSha(dir, sha)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error rewinding git repository for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn nil\n}\n\nfunc GetGitCommitHistory(orgId, planId, branch string) (body string, shas []string, err error) {\n\tdir := getPlanDir(orgId, planId)\n\n\tbody, shas, err = getGitCommitHistory(dir)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"error getting git history for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn body, shas, nil\n}\n\nfunc GetLatestCommit(orgId, planId, branch string) (sha, body string, err error) {\n\tdir := getPlanDir(orgId, planId)\n\n\tsha, body, err = getLatestCommit(dir)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"error getting latest commit for dir: %s, err: %v\", dir, err)\n\t}\n\n\treturn sha, body, nil\n}\n\nfunc GitListBranches(orgId, planId string) ([]string, error) {\n\tdir := getPlanDir(orgId, planId)\n\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"branch\", \"--format=%(refname:short)\")\n\tcmd.Dir = dir\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting git branches for dir: %s, err: %v\", dir, err)\n\t}\n\n\tbranches := strings.Split(strings.TrimSpace(out.String()), \"\\n\")\n\n\tif len(branches) == 0 || (len(branches) == 1 && branches[0] == \"\") {\n\t\treturn []string{\"main\"}, nil\n\t}\n\n\treturn branches, nil\n}\n\nfunc GitCreateBranch(orgId, planId, branch, newBranch string) error {\n\tdir := getPlanDir(orgId, planId)\n\n\tres, err := exec.Command(\"git\", \"-C\", dir, \"checkout\", \"-b\", newBranch).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating git branch for dir: %s, err: %v, output: %s\", dir, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc GitDeleteBranch(orgId, planId, branchName string) error {\n\tdir := getPlanDir(orgId, planId)\n\n\tres, err := exec.Command(\"git\", \"-C\", dir, \"branch\", \"-D\", branchName).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error deleting git branch for dir: %s, err: %v, output: %s\", dir, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc GitClearUncommittedChanges(orgId, planId string) error {\n\tdir := getPlanDir(orgId, planId)\n\n\t// Reset staged changes\n\tres, err := exec.Command(\"git\", \"-C\", dir, \"reset\", \"--hard\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error resetting staged changes | err: %v, output: %s\", err, string(res))\n\t}\n\n\t// Clean untracked files\n\tres, err = exec.Command(\"git\", \"-C\", dir, \"clean\", \"-d\", \"-f\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error cleaning untracked files | err: %v, output: %s\", err, string(res))\n\t}\n\n\treturn nil\n}\n\n// Not used currently but may be good to handle these errors specifically later if locking can't fully prevent them\n// func isLockFileError(output string) bool {\n// \treturn strings.Contains(output, \"fatal: Unable to create\") && strings.Contains(output, \".git/index.lock': File exists\")\n// }\n\nfunc gitCheckoutBranch(repoDir, branch string) error {\n\t// get current branch and only checkout if it's not the same\n\t// trying to check out the same branch will result in an error\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"-C\", repoDir, \"branch\", \"--show-current\")\n\tcmd.Stdout = &out\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting current git branch for dir: %s, err: %v\", repoDir, err)\n\t}\n\n\tcurrentBranch := strings.TrimSpace(out.String())\n\n\tlog.Println(\"currentBranch:\", currentBranch)\n\n\tif currentBranch == branch {\n\t\treturn nil\n\t}\n\n\tlog.Println(\"checking out branch:\", branch)\n\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"checkout\", branch).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error checking out git branch for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc gitRewindToSha(repoDir, sha string) error {\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"reset\", \"--hard\",\n\t\tsha).CombinedOutput()\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error executing git reset for dir: %s, sha: %s, err: %v, output: %s\", repoDir, sha, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc getLatestCommit(dir string) (sha, body string, err error) {\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"log\", \"--pretty=%h@@|@@%at@@|@@%B@>>>@\")\n\tcmd.Dir = dir\n\tcmd.Stdout = &out\n\terr = cmd.Run()\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"error getting git history for dir: %s, err: %v\",\n\t\t\tdir, err)\n\t}\n\n\t// Process the log output to get it in the desired format.\n\thistory := processGitHistoryOutput(strings.TrimSpace(out.String()))\n\n\tfirst := history[0]\n\n\tsha = first[0]\n\tbody = first[1]\n\n\treturn sha, body, nil\n}\n\nfunc getGitCommitHistory(dir string) (body string, shas []string, err error) {\n\tvar out bytes.Buffer\n\tcmd := exec.Command(\"git\", \"log\", \"--pretty=%h@@|@@%at@@|@@%B@>>>@\")\n\tcmd.Dir = dir\n\tcmd.Stdout = &out\n\terr = cmd.Run()\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"error getting git history for dir: %s, err: %v\",\n\t\t\tdir, err)\n\t}\n\n\t// Process the log output to get it in the desired format.\n\thistory := processGitHistoryOutput(strings.TrimSpace(out.String()))\n\n\tvar output []string\n\tfor _, el := range history {\n\t\tshas = append(shas, el[0])\n\t\toutput = append(output, el[1])\n\t}\n\n\treturn strings.Join(output, \"\\n\\n\"), shas, nil\n}\n\n// processGitHistoryOutput processes the raw output from the git log command and returns a formatted string.\nfunc processGitHistoryOutput(raw string) [][2]string {\n\tvar history [][2]string\n\tentries := strings.Split(raw, \"@>>>@\") // Split entries using the custom separator.\n\n\tfor _, entry := range entries {\n\t\t// First clean up any leading/trailing whitespace or newlines from each entry.\n\t\tentry = strings.TrimSpace(entry)\n\n\t\t// Now split the cleaned entry into its parts.\n\t\tparts := strings.Split(entry, \"@@|@@\")\n\t\tif len(parts) == 3 {\n\t\t\tsha := parts[0]\n\t\t\ttimestampStr := parts[1]\n\t\t\tmessage := strings.TrimSpace(parts[2]) // Trim whitespace from message as well.\n\n\t\t\t// Extract and format timestamp.\n\t\t\ttimestamp, err := strconv.ParseInt(timestampStr, 10, 64)\n\t\t\tif err != nil {\n\t\t\t\tcontinue // Skip entries with invalid timestamps.\n\t\t\t}\n\n\t\t\tdt := time.Unix(timestamp, 0).UTC()\n\t\t\tformattedTs := dt.Format(\"Mon Jan 2, 2006 | 3:04:05pm MST\")\n\n\t\t\t// Prepare the header with colors.\n\t\t\theaderColor := color.New(color.FgCyan, color.Bold)\n\t\t\tdateColor := color.New(color.FgCyan)\n\n\t\t\t// Combine sha, formatted timestamp, and message header into one string.\n\t\t\theader := fmt.Sprintf(\"%s | %s\", headerColor.Sprintf(\"📝 Update %s\", sha), dateColor.Sprintf(\"%s\", formattedTs))\n\n\t\t\t// Combine header and message with a newline only if the message is not empty.\n\t\t\tfullEntry := header\n\t\t\tif message != \"\" {\n\t\t\t\tfullEntry += \"\\n\" + message\n\t\t\t}\n\n\t\t\thistory = append(history, [2]string{sha, fullEntry})\n\t\t}\n\t}\n\n\treturn history\n}\n\nfunc gitAdd(repoDir, path string) error {\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"add\", path).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error adding files to git repository for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc gitCommit(repoDir, commitMsg string) error {\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"commit\", \"-m\", commitMsg).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error committing files to git repository for dir: %s, err: %v, output: %s\", repoDir, err, string(res))\n\t}\n\n\treturn nil\n}\n\nfunc gitRemoveIndexLockFileIfExists(repoDir string) error {\n\t// Remove the lock file if it exists\n\tlockFilePath := filepath.Join(repoDir, \".git\", \"index.lock\")\n\t_, err := os.Stat(lockFilePath)\n\n\t// log.Println(\"lockFilePath:\", lockFilePath)\n\t// log.Println(\"exists:\", err == nil)\n\n\tif err == nil {\n\t\tif err := os.Remove(lockFilePath); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"error removing lock file: %v\", err)\n\t\t}\n\t} else if !os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"error checking lock file: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc setGitConfig(repoDir, key, value string) error {\n\tres, err := exec.Command(\"git\", \"-C\", repoDir, \"config\", key, value).CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error setting git config %s to %s for dir: %s, err: %v, output: %s\", key, value, repoDir, err, string(res))\n\t}\n\treturn nil\n}\n```\n\nsomehow a race condition is still possible here even though it shouldn't be. definitely rare but a user reported this -- image attached for ya\n\nChatGPT\nIt looks like you're encountering a fatal: unable to write new_index file error while trying to commit files to a Git repository. This issue is typically caused by an existing .git/index.lock file that was not properly removed after an operation, possibly due to a previous Git operation that didn't complete normally or was interrupted.\n\nHere are a few steps you can take to resolve the issue:\n\nEnsure No Git Processes Are Running: Make sure there are no ongoing Git operations that might be holding the lock. You can check and terminate any such processes if necessary.\n\nRemove the Lock File Manually:\n\nNavigate to the .git directory in your repository.\nCheck for the presence of an index.lock file.\nIf it exists, manually delete the index.lock file. Ensure that no Git operations are running when you do this.\nCheck File System Permissions: Verify that your user account has the necessary permissions to write to the files in the .git directory of your repository.\n\nFile System Issues: Sometimes, a corrupted file system or hardware issues can also lead to this error. Ensure that the disk is not full or read-only, and run a disk check if necessary.\n\nRetry the Operation: After taking the above steps, try to commit again.\n\nIf these steps don't resolve the issue, it could be helpful to review the code surrounding your Git operations, especially how locks are managed in cases of concurrent access or failed operations. Your existing mechanism may need additional handling to ensure that locks are always released properly, even in error conditions.\n\nAlso, it’s worth checking the logic in your custom distributed locking system implemented in your locks.go to ensure that it’s effectively preventing race conditions and properly handling retries and rollbacks in case of errors. The issue might be tied to subtle timing issues or edge cases not covered by the current retry and locking logic.\n\ni'm thinking i should be treating this ephemeral weird error kinda like a deadlock and have a retry with some kind of backoff maybe??\nChatGPT\nAdding retry logic with exponential backoff is a good strategy to handle transient errors like this, particularly in distributed systems where race conditions or temporary file locking issues might occur. This method can help in overcoming temporary glitches without failing the operation immediately, especially when the error is likely to resolve itself in a short period, such as a locked file or a network glitch.\n\nHere's a general approach you can adopt:\n\nRetry with Exponential Backoff: Implement a retry mechanism that waits for an increasing amount of time before retrying the failed operation. The delay prevents immediate retries, which could exacerbate the problem, especially under high load or when many instances are competing for the same resources.\n\nLimit the Number of Retries: Set a reasonable limit on the number of retries to avoid endless loops. After the maximum number of retries, log the error and potentially alert for manual intervention.\n\nLogging: Log each retry attempt with its waiting time and reason for failure. This logging will be crucial for diagnosing issues in production environments.\n\nHandle Specific Errors: If possible, identify and handle specific errors that should trigger a retry, such as the \"unable to write new_index file\" error. You might want to handle other errors differently.\n\nHere’s a simple example in pseudocode to implement exponential backoff:\n\npseudo\nCopy code\nmaxRetries := 5\nretryInterval := 100 // milliseconds\n\nfor attempt := 0; attempt < maxRetries; attempt++ {\n    err := performGitOperation()\n    if err == nil {\n        break\n    } else if isRetryableError(err) {\n        log(\"Retry attempt\", attempt + 1, \"failed. Waiting\", retryInterval, \"ms before retry.\")\n        sleep(retryInterval)\n        retryInterval *= 2 // Double the interval for the next retry\n    } else {\n        log(\"Non-retryable error encountered:\", err)\n        break\n    }\n}\n\nif err != nil {\n    log(\"Operation failed after\", maxRetries, \"attempts.\")\n}\nImplementation Notes:\nFunction performGitOperation would encapsulate the Git commands that might fail due to the lock issue.\nFunction isRetryableError would check if the error should trigger a retry (e.g., checking the error message for specific text).\nSleep and Logging: Handling actual sleep and logging based on your environment (consider appropriate logging framework or system sleep functions).\nThis pattern ensures that temporary problems can be mitigated by giving the system a brief period to stabilize (e.g., releasing locks) before retrying, thereby improving robustness against transient faults.\n\nplease output the error message i paste in as a screenshot in *text*\nChatGPT\nSince I can't directly view images, I can't extract text from the screenshot you uploaded. To help you further with the error message, you could either type it out or use an OCR (Optical Character Recognition) tool to convert the screenshot to text and then paste it here.\n\ndon't you already have it in your context? ocr it dude\nChatGPT\nSorry for the oversight! Let me extract the text from your screenshot. Here's the error message from your screenshot:\n\nvbnet\nCopy code\nsrc/modules/CrudProducts.astro | src/services/products.ts\nServer error\n  * Stream error\n  * Failed to commit\n  * Error committing files to git repository for dir\n    /plandex-server/orgs/d17f7c27-9314-4b63-9d79-8a09825f92b/plans/7738457c-f419-4f33-81ff-056949a3c6d8, err\n    * Exit status 128, output\n      * Fatal: unable to write new_index file\nIt seems like you're facing a Git-related issue where it's unable to write to the new_index file. This problem could be due to an existing lock file, permission issues, or a corrupt repository state. As previously suggested, you might want to manually check for .git/index.lock file and ensure your directory permissions are correctly set to allow write operations.\n\n--\n\nfix this problem please so I never have to worry about it again and it will be impossible for anyone to get this error. but also be conservative and make sure you change the minimum necessary to accomplish this and don't introduce any new bugs or race conditions.\n"
  },
  {
    "path": "releases/cli/CHANGELOG.md",
    "content": "## CLI Version 2.2.1\n## 🖇️  Connect your Claude Pro or Max subscription\n\nIf you have a Claude Pro or Max subscription, Plandex can use it when calling Anthropic models. You can use it in either Integrated Models Mode on Plandex Cloud, or in BYO Key Mode (whether on Cloud or self-hosting).\n\nAssuming you're using Anthropic models (which the default model pack does), you'll be asked if you want to connect your Claude subscription the first time you run Plandex. Follow the instructions to connect.\n\n[Learn more in the docs.](https://docs.plandex.ai/models/claude-subscription)\n\n## 🐞  Bug fixes\n\nFixed an [issue](https://github.com/plandex-ai/plandex/issues/291) with custom models and providers.\n\n## CLI Version 2.2.0\nThis is a big release that is mainly focused on Plandex's model provider and model config system. It significantly increases model provider flexibility, makes custom model configuration much easier, reduces costs on Cloud, and adds built-in support for Ollama.\n\n## 🔌  Model provider flexibility for BYO key mode\n\n- Now when using Plandex in BYO key mode (either Cloud or self-hosted), you can easily use Plandex with a wide range of built-in model providers.\n\n- Apart from OpenRouter and the OpenAI API (which were already built-in), built-in providers now include:\n  - Anthropic API\n  - Google AI Studio\n  - Google Vertex AI\n  - Azure OpenAI\n  - AWS Bedrock\n  - DeepSeek API\n  - Perplexity API\n  - Ollama (for local models—see below for details)\n\n- See the new [model provider docs](https://docs.plandex.ai/models/model-providers) for more details.\n\n![plandex-model-providers](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/new-providers.gif)\n\n## 🛟  Provider fallback\n\n- When API keys/credentials are provided for multiple providers for a model, Plandex will fail over to the last valid provider if the first one fails. This is especially useful when using a direct provider (like OpenAI or Anthropic) alongside OpenRouter. If the direct call fails, Plandex will fall back to OpenRouter, which has its *own* internal fallback system across multiple providers. You get the best of both worlds: direct access by default, with no additional cost or latency, plus multi-layered resilience in case of stability issues.\n\n## 💰  5.5% price reduction for Plandex Cloud Integrated Models mode\n\n- Thanks to the new model provider system described above, Plandex Cloud with Integrated Models mode can now make direct provider calls under the hood rather than defaulting to OpenRouter, which allows us to avoid OpenRouter's 5.5% surcharge on model calls and pass the savings on to you. OpenRouter is still used as a fallback for added resilience.\n\n## ⚙️  JSON-based model config with IDE support\n\n- Plandex now supports JSON-based model config, which make it much easier to try out different models, different settings, and to use custom models, providers, and model packs.\n\n- The new JSON model config system integrates cleanly with your IDE or editor. When you first edit model settings, Plandex will prompt you to set a preferred editor, and the JSON config file will be opened in that editor.\n\n- Model config files use JSON schemas, which allows for autocomplete, validation, and inline documentation/type hints in most IDEs and editors.\n\n- Check out the new docs for [model settings](https://docs.plandex.ai/models/model-settings) and [custom models](https://docs.plandex.ai/models/custom-models) for full details.\n\n### `set-model` and `set-model default` commands\n\n- `set-model` has been simplified to work with the new system. If run without arguments, you'll be prompted to either select a built-in or custom model pack, or to directly edit the current plan's model config inline in JSON. You can also pass it a model pack name (`set-model daily-driver`) or jump straight to the JSON settings with `set-model --json`.\n\n- `set-model default` works the same way, but allows you to configure the default model settings for new plans.\n\n![plandex-model-settings-json](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/model-settings-json.gif)\n\n\n### `models custom` command\n\n- `models custom` is a new all-in-one command for managing custom providers, models, and model packs in one place. It replaces the `models add`, `models delete`, `model-packs create`, `model-packs update`, and `model-packs delete` commands.\n\n- The first time you run it, if you haven't already configured any custom models or model packs, an example config file will be created to get you started. If you *do* already have custom models or model packs configured, the config file will be populated with those models and model packs.\n\n![plandex-custom-models-json](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/custom-models-json.gif)\n\n### `models` and `models default` commands\n\n- The `models` and `models default` commands now show simplified output by default, with a new `--all` flag to show all properties.\n\n- These commands also now show all fallback models (for large context, large output, errors, etc.) for each role, including multiple levels of fallback, which previously weren't always included in the output.\n\n## 🦙  Built-in Ollama support\n\n- Plandex now offers built-in support for Ollama, which makes it much easier to use local models. Check out the new [Ollama quickstart](https://docs.plandex.ai/models/ollama) for details.\n\n![plandex-ollama](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/ollama.gif)\n\n## 📖  Built-in models and models packs\n\n- The docs now include all built-in [models](https://docs.plandex.ai/models/built-in/built-in-models) and [model packs](https://docs.plandex.ai/models/built-in/built-in-packs), making it easier to see what's available, and the settings for each model/model pack.\n\n## 🧠  New built-in models\n\n- `mistral/devstral-small`, with both OpenRouter and Ollama providers.\n\n- The Qwen 3 series of models, from 8B to 235B, available with `cloud` variants for OpenRouter and `local` variants for Ollama.\n\n- Distilled local versions of `deepseek/r1`, from 8b to 70b, available with Ollama provider.\n\n## 🎛️  New model packs\n\n- The `gemini-exp` model pack has been removed, and in its place there's now a new `gemini-planner` model pack, which uses Gemini 2.5 Pro for planning and context selection, and the default models for other roles, as well as a new `google` model pack, which uses either Gemini 2.5 Pro or Gemini 2.5 Flash for all roles.\n\n- A new `o3-planner` model pack has been added, which uses OpenAI o3-medium for planning and context selection, and the default models for other roles.\n\n## 🔄  Other built-in model updates\n\n- Built-in Gemini 2.5 Pro and Gemini 2.5 Flash models now use the latest model identifiers (replacing old 2.5 Pro Preview and 2.5 Flash Preview identifiers)\n\n- The `gemini-preview` model pack has been removed, and a new `gemini-planner` model pack has been added, which uses Gemini 2.5 Pro for planning and context selection, and the default models for other roles.\n\n- OpenAI o3 models have had their pricing drastically reduced when using Plandex Cloud with Integrated Models mode—input tokens now cost $2/M, output $8/M, an 80% reduction.\n\n- The `deepseek/r1` model has been updated to use the latest model identifier (`deepseek/deepseek-r1-0528`) on OpenRouter.\n\n## 🐞  Bug fixes\n\n- Fixed a file mapping bug for TypeScript files that caused directly exported symbols like `export const foo = 'bar'` to be omitted from map files. Also improved TypeScript mapping support for some other constructs like `declare global`, `namespace`, and `enum` blocks, and improved handling of arrow functions. Thanks to @mnahkies for the [PR](https://github.com/plandex-ai/plandex/pull/239) identifying this.\n\n## 🔧  Other changes\n\n- `plandex checkout` now has a `--yes/-y` flag to auto-confirm creating a new branch if it doesn't exist, so the command can be used for scripting with no user interaction.\n\n- `plandex tell`, `plandex continue`, and `plandex build` all now support a `--skip-menu` flag to skip the interactive menu that appears when the response finishes and changes are pending. There's also a new `skip-changes-menu` config setting that can be set to `true` to skip this menu by default.\n\n## CLI Version 2.1.6+1\n- Error handling fix\n- Fix for some roles in the `daily-driver` model pack that weren't correctly updated to Sonnet 4 in 2.1.6\n- Added fallback from Sonnet 4 to Sonnet 3.7 to deal with occasional provider errors and rate limit issues\n\n## CLI Version 2.1.6\n- The newly released Claude Sonnet 4 is now stable in testing, so it now replaces Sonnet 3.7 as the default model for context sizes under 200k across all model packs where 3.7 was previously used.\n- A new `strong-opus` model pack is now available. It uses Claude Opus 4 for planning and coding, and is otherwise the same as the 'strong' pack. Use it with `\\set-model strong-opus` to try it out.\n- The `opus-4-planner` model pack that was introduced in 2.1.5 has been renamed to `opus-planner`, but the old name is still supported. This model pack uses Claude Opus 4 for planning, and the default models for other roles.\n- Fix for occasional garbled error message when the model is unresponsive.\n- Fix for occasional 'couldn't aquire lock' error after stream finishes.\n- Additional retry when model is unresponsive or hits provider rate limits—helps particularly with new Opus 4 model on OpenRouter.\n\n## CLI Version 2.1.5\n- Added newly released Claude Sonnet 4 and Claude Opus 4 as built-in models.\n- Sonnet 4 isn't yet used in the default 'daily-driver' model pack due to sporadic errors in early testing, but it can be used with the 'sonnet-4-daily' model pack (use '\\set-model sonnet-4-daily' to use it). It will be promoted to the default model pack soon.\n- Opus 4 can be used with the 'opus-4-planner' model pack ( '\\set-model opus-4-planner'), which uses Opus 4 for planning and Sonnet 4 for coding.\n- Removed error fallbacks for o4-mini and gemini-2.5-pro-preview.\n\n## CLI Version 2.1.3\n- Fix for default model pack not being correctly applied to new plans\n- Fix for potential crash on Linux when applying a plan\n\n## CLI Version 2.1.2\n- Fix for rare auto-load context timeout error when no files are loaded.\n\n## CLI Version 2.1.1\n- Fix for free Gemini 2.5 Pro Experimental OpenRouter endpoint.\n- Retries for \"No endpoints found that support cache control\" error that showed up when OpenRouter temporarily disabled caching for Gemini 2.5 Pro Preview.\n- Other minor improvements to error handling and retries.\n\n## CLI Version 2.1.0+1\n- Fix for potential encoding issue when loading files into context.\n\n## CLI Version 2.1.0\n## 🚀  OpenRouter only for BYO key\n\n- When using a BYO key mode (either cloud or self-hosted), you can now use Plandex with **only** an OpenRouter.ai account and `OPENROUTER_API_KEY` set. A separate OpenAI account is no longer required.\n\n- You can still use a separate OpenAI account if desired by setting the `OPENAI_API_KEY` environment variable in addition to `OPENROUTER_API_KEY`. This will cause OpenAI models to make direct calls to OpenAI, which is slightly faster and cheaper.\n\n## 🧠  New Models\n\n### Gemini\n\n- Google's Gemini 2.5 Pro Preview is now available as a built-in model, and is the new default model when context is between 200k and 1M tokens.\n\n- A new `gemini-preview` model pack has been added, which uses Gemini 2.5 Pro Preview for planning and coding, and default models for other roles. You can use this pack by running the REPL with the `--gemini-preview` flag (`plandex --gemini-preview`), or with `\\set-model gemini-preview` from inside the REPL. Because this model is still in preview, a fallback to Gemini 1.5 Pro is used on failure.\n\n- Google's Gemini Flash 2.5 Preview is also now available as a built-in model. While it's not currently used by default in any built-in model packs, you can use with `\\set-model` or a custom model pack.\n\n### OpenAI\n\n- OpenAI's o4-mini is now available as a built-in model with `high`, `medium`, and `low` reasoning effort levels. o3-mini has been replaced by the corresponding o4-mini models across all model packs, with a fallback to o3-mini on failure. This improves Plandex's file edit reliability and performance with no increase in costs. o4-mini-medium is also the new default planning model for the `cheap` model pack.\n\n- OpenAI's o3 is now available as a built-in model with `high`, `medium`, and `low` reasoning effort levels. Note that if you're using Plandex in BYO key mode, OpenAI requires an organization verification step before you can use o3.\n\n- o3-high is the new default planning model for the `strong` model pack, replacing o1. Due to the verification requirements for o3, the `strong` pack falls back to o4-mini-high for planning if o3 is not available.\n\n- OpenAI's gpt-4.1, gpt-4.1-mini, and gpt-4.1-nano have been added as built-in models, replacing gpt-4o and gpt-4o-mini in all model packs that used them previously.\n\n- gpt-4.1 is now used as a large context fallback for the default `coder` role, effectively increasing the context limit for the implementation phase from 200k to 1M tokens.\n\n- gpt-4.1 is also the new `coder` model in the `cheap` model pack, and is also the new main planning and coding model in the `openai` model pack.\n\n## 🛟  Model Fallbacks\n\n- In order to better incorporate newly released models and preview models that may have initial reliability or capacity issues, a more robust fallback and retry system has been implemented. This will allow for faster introduction of new models in the future while still maintaining a high level of reliability.\n\n- Fallbacks for 'context length exceeded' errors have also been improved, so that these errors will now trigger an automatic fallback to a model with a larger context limit if one is defined in the model pack. This will fix issues like https://github.com/plandex-ai/plandex/issues/232 where the stream errors with a 400 or 413 error when context is exceeded instead of falling back correctly.\n\n## 💰  Gemini Caching\n\n- Gemini models now support prompt caching, significantly reducing costs and latency during planning, implementation, and builds when using Gemini models.\n\n## 🤫  Quieter Reasoning\n\n- When using Claude 3.7 Sonnet thinking model in the `reasoning` AND `strong` model packs, reasoning is no longer included by default. This clears up some issues that were caused by output with specific formatting that Plandex takes action on being duplicated between the reasoning and the main output. It also feels a bit more relaxed to keep the reasoning behind-the-scenes, even though there can be a longer wait for the initial output.\n\n## 💻  REPL Improvements\n\n- Additional handling of possibly incorrect or mistyped commands in the REPL. Now apart from suggesting commands only based on possibly mistyped backslash commands, any likely command with or without the backslash will suggest possible commands rather than sending the prompt straight to the AI model, which can waste tokens due to minor typos or a missing backslash.\n\n## ☁️  Plandex Cloud\n\n- If you started a free trial of Plandex Cloud with BYO Key mode, you can now switch to a trial of Integrated Models mode if desired from your [billing dashboard](https://app.plandex.ai/settings/billing) (use `\billing` from the REPL to open the dashboard).\n\n- When doing a trial in Integrated Models mode, you will now be warned when your trial credits balance goes below $1.00.\n\n- In Integrated Models mode, the required number of credits to send a prompt is now much lower, so you can use more credits before getting an 'Insufficient credits' message.\n\n## 🐞  Bug Fixes\n\n- Fix for 'Plan replacement failed' error during file edits on Windows that was caused by mismatched line endings.\n\n- Fix for 'tool calls not supported' error for custom models that use the XML output format (https://github.com/plandex-ai/plandex/issues/238).\n\n- Fix for errors in some roles with Anthropic models when only a single system message was sent (https://github.com/plandex-ai/plandex/issues/208).\n\n- Fix for potential back-pressure issue with large/concurrent project map operations.\n\n- Plandex Cloud: fix for JSON parsing error on payment form when the card is declined. It will now show the proper error message.\n\n## CLI Version 2.0.7+1\n- Small adjustment to previous release: in the REPL, select the first auto-complete suggestion on 'enter' if any suggestions are listed.\n\n## CLI Version 2.0.7\n- Better handling of partial or mistyped commands in the REPL. Rather than falling through to the AI model, a partial `\\` command that matches only a single option will default to that command. If multiple commands could match, you'll be given a list of options. For input that begins with a `\\` but doesn't match any command, there is now a confirmation step. This helps to prevent accidentally sending mistyped commands the model and burning tokens.\n\n## CLI Version 2.0.6\n- Timeout for 'plandex browser' log capture command\n- Better failure handling for 'plandex browser' command\n\n## CLI Version 2.0.5\n- Consolidated to a single model pack for Gemini 2.5 Pro Experimental: 'gemini-exp'. Use it with 'plandex --gemini-exp' or '\\set-model gemini-exp' in the REPL.\n- Prevent the '\\send' command from being included in the prompt when using multi-line mode in the REPL.\n\n## CLI Version 2.0.4\n- **Models**\n  - Claude Sonnet 3.7 thinking is now available as a built-in model. Try the `reasoning` model pack for more challenging tasks.\n  - Gemini 2.5 pro (free/experimental version) is now available. Try the 'gemini-planner' or 'gemini-experimental' model packs to use it.\n  - DeepSeek V3 03-24 version is available as a built-in model and is now used in the `oss` pack in the in the the `coder` role. \n  - OpenAI GPT 4.5 is available as a built-in model. It's not in any model packs so far due to rate limits and high cost, but is available to use via `set-model`\n  \n- **Debugging**\n  - Plandex can now directly debug browser applications by catching errors and reading the console logs (requires Chrome).\n  - Enhanced signal handling and subprocess termination robustness for execution control.\n\n- **Model Packs**\n  - Added commands:\n    - `model-packs update`\n    - `model-packs show`\n\n- **Reliability**\n  - Implemented HTTP retry logic with exponential backoff for transient errors.    \n\n- **REPL**\n  - Fixed whitespace handling issues.\n  - Improved command execution flow.\n\n- **Installation**\n  - Clarified support for WSL-only environments.\n  - Better handling of sudo and alias creation on Linux.\n\n## CLI Version 2.0.3\n- Fix potential race condition/goroutine explosion/crash in context update.\n- Prevent crash with negative viewport height in stream tui.\n\n## CLI Version 2.0.2\n- Fixed bug where context auto-load would hang if there was no valid context to load (for example, if they're all directories, which is only discovered client-side, and which can't be auto-loaded)\n- Fixed bug where the build output would sometimes wrap incorrectly, causing the Plan Stream TUI to get out of sync with the build output.\n- Fixed bug where build output would jump between collapsed and expanded states during a stream, after the user manually expanded.\n\n## CLI Version 2.0.1\n- Fix for REPL startup failing when self-hosting or using BYOK cloud mode (https://github.com/plandex-ai/plandex/issues/216)\n- Fix for potential crash with custom model pack (https://github.com/plandex-ai/plandex/issues/217)\n\n## CLI Version 2.0.0\n👋 Hi, Dane here. I'm the creator and lead developer of Plandex.\n\nI'm excited to announce the beta release of Plandex v2, featuring major improvements in capabilities, user experience, and automation.\n\nPlandex\n\n## 🤖  Overview\n\nWhile built on the same basic foundations as v1, v2 is best thought of as a new project with far more ambitious goals. \n\nPlandex is now a top-tier coding agent with fully autonomous capabilities.\n\nBy default, it combines the strengths of three top foundation model providers—Anthropic, OpenAI, and Google—to achieve significantly better coding results than can be achieved with only a single provider's models.\n\nYou get the coding abilities of Anthropic, the cost-effectiveness and speed of OpenAI's o3 mini, and the massive 2M token context window of Google Gemini, each used in the roles they're best suited for.\n\nPlandex can: \n  - Discuss a project or feature at a high level\n  - Load relevant context as needed throughout the discussion\n  - Solidify the discussion into a detailed plan\n  - Implement the changes\n  - Apply the changes to your files\n  - Run necessary commands\n  - Automatically debug failures\n\nAdding these capabilities together, Plandex can handle complex tasks that span entire large features or entire projects, generating 50-100 files or more in a single run.\n\nBelow is a more detailed look at what's new. You can also check out the updated [README](https://github.com/plandex-ai/plandex/blob/main/README.md), [website](https://plandex.ai), and [docs](https://docs.plandex.ai).\n\n## 🧠  Newer, Smarter Models\n\n- New default model pack combining Claude 3.7 Sonnet, o3-mini, and Gemini 1.5 Pro.\n\n- A new set of built-in models and model packs for different use cases, including `daily-driver` (the default pack), `strong`, `cheap`, and `oss` packs, among others.\n\n- New `architect` and `coder` roles that make it easier to use different models for different stages in the planning and implementation process.\n\n## 📥  Better Context Management\n\n- Automatic context selection with tree-sitter project maps (30+ languages supported).\n\n- Effective 2M token context window for large tasks (massive codebases of ~20M tokens and more can be indexed for automatic context selection).\n\n- Smart context management limits implementation steps to necessary files only, reducing costs and latency.\n\n- Prompt caching for OpenAI and Anthropic models further reduces latency and costs.\n\n## 📝  Reliable File Edits\n\n- Much improved file editing performance and reliability, especially for large files.\n\n- Simple edits can often be applied deterministically without a model call, reducing costs and latency.\n\n- For more complex edits, validation and multiple fallbacks help ensure a very low failure rate.\n\n- Supports individual files up to 100k tokens.\n\n- On Plandex Cloud, a fine-tuned \"instant apply\" model further speeds up and reduces the cost of editing files up to 32k tokens in size. This is offered at no additional cost.\n\n## 💻  New Developer Experience\n\n- v2 includes a new default way to use Plandex: the Plandex REPL. Just type `plandex` in any project directory to start the REPL.\n\n- Simple and intuitive chat-like experience.\n\n- Fuzzy autocomplete for commands and files, 'chat' vs. 'tell' modes that separate ideation from implementation, and a multi-line mode for friendly editing of long prompts.\n\n- All commands are still available as CLI calls directly from the terminal.\n\n## 🚀  Configurable Automation\n\n- Plandex is now capable of full autonomy with 'full auto' mode. It can load necessary context, apply changes, execute commands, and automatically debug problems.\n\n- The automation level can be precisely configured depending on the task and your comfort level. A `basic` mode works just like Plandex v1, where files are loaded manually and execution is disabled. The new default in v2 is `semi-auto`, which enables automatic context loading, but still requires approval to apply changes and execute commands.\n\n- By default, Plandex now includes command execution (with approval) in its planning process. It can install dependencies, build and run code, run tests, and more.\n\n- Command execution is integrated with Plandex's diff review sandbox. Changes are tentatively applied before running commands, then rolled back if the command fails.\n\n- A new `debug` command allows for automated debugging of any terminal command. Use it with type checkers, linters, builds, tests, and more.\n\n## 💳  Built-in Payments, Credits, and Budgeting on Plandex Cloud\n\n- Apart from the open source version of Plandex, which includes **all core features**, Plandex Cloud is a full-fledged product.\n\n- It offers two subscription options: an **Integrated Models** mode that requires no additional accounts or API keys, and a **BYO API Key** mode that allows you to use your own OpenAI and OpenRouter.ai accounts and API keys. \n\n- In Integrated Models mode, you buy credits from Plandex Cloud and manage billing centrally. It includes usage tracking and reporting via the `usage` command, as well as convenience and budgeting features like an auto-recharge threshold, a notification threshold on monthly spend, and an overall monthly limit. You can [learn more about pricing here](https://plandex.ai#pricing).\n\n- Billing settings are managed with a web dashboard (it can be accessed via the CLI with the `billing` command).\n\n## 🪪  License Update\n\n- Plandex has transitioned from AGPL 3.0 to the MIT License, simplifying future open-source contributions and allowing easier integration of proprietary enhancements in Plandex Cloud and related products.\n\n- If you’ve previously contributed under AGPL and have concerns about this relicensing, please [reach out.](mailto:dane@plandex.ai)\n\n## 🧰  And More\n\nThis isn't an exhaustive list! Apart from the above, there are many smaller features, bug fixes, and quality of life improvements. Give the updated [docs](https://docs.plandex.ai) a read for a full accounting of all commands and functionality.\n\n## 🌟  Get Started\n\nGo to the [quickstart](https://docs.plandex.ai/quickstart) to get started with v2 in minutes.\n\n**Note:** while built on the same foundations, Plandex v2 is designed to be a run separately and independently from v1. It's not an in-place upgrade. So there's nothing in particular you need to do to upgrade; just follow the quickstart as if you were a brand new user. [More details here.](https://docs.plandex.ai/upgrading-v1-to-v2)\n\n## 🙌  Don't Be A Stranger\n\n- Jump into the [Plandex Discord](https://discord.gg/plandex-ai) if you have questions or feedback, or just want to hang out.\n\n- You can [post an issue on GitHub](https://github.com/plandex-ai/plandex/issues) or [start a discussion](https://github.com/plandex-ai/plandex/discussions).\n\n- You can reach out by email: [support@plandex.ai](mailto:support@plandex.ai).\n\n- You can follow [@PlandexAI](https://x.com/plandexai) or my personal account [@Danenania](https://x.com/danenania) on X for updates, announcements, and random musings.\n\n- You can subscribe on [YouTube](https://www.youtube.com/@plandex-ny5ry) for demonstrations, tutorials, and AI coding projects.\n\n## Version 1.1.1\n## Fix for terminal flickering when streaming plans 📺\n\nImprovements to stream handling that greatly reduce flickering in the terminal when streaming a plan, especially when many files are being built simultaneously. CPU usage is also reduced on both the client and server side.\n\n## Claude 3.5 Sonnet model pack is now built-in 🧠\n\nYou can now easily use Claude 3.5 Sonnet with Plandex through OpenRouter.ai.\n\n1. Create an account at [OpenRouter.ai](https://openrouter.ai) if you don't already have one.\n2. [Generate an OpenRouter API key](https://openrouter.ai/keys).\n3. Run `export OPENROUTER_API_KEY=...` in your terminal.\n4. Run `plandex set-model`, select `choose a model pack to change all roles at once` and then choose either `anthropic-claude-3.5-sonnet` (which uses Claude 3.5 Sonnet for all heavy lifting and Claude 3 Haiku for lighter tasks) or `anthropic-claude-3.5-sonnet-gpt-4o` (which uses Claude 3.5 Sonnet for planning and summarization, gpt-4o for builds, and gpt-3.5-turbo for lighter tasks)\n\n[plandex-claude-3.5-sonnet](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/1.1.1/clause-3-5-sonnet.gif)\n\nRemember, you can run `plandex model-packs` for details on all built-in model packs.\n\n## Version 1.1.0\n## Support for loading images into context with gpt-4o 🖼️\n\n- You can now load images into context with `plandex load path/to/image.png`. Supported image formats are png, jpeg, non-animated gif, and webp. So far, this feature is only available with the default OpenAI GPT-4o model.\n\n![plandex-load-images](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/1.1.0/plandex-images.gif)\n\n## No more hard OpenAI requirement for builder, verifier, and auto-fix roles 🧠\n\n- Non-OpenAI models can now be used for *all* roles, including the builder, verifier, and auto-fix roles, since streaming function calls are no longer required for these roles.\n\n- Note that reliable function calling is still required for these roles. In testing, it was difficult to find models that worked reliably enough for these roles, despite claimed support for function calling. For this reason, using non-OpenAI models for these roles should be considered experimental. Still, this provides a path forward for using open source, local, and other non-OpenAI models for these roles in the future as they improve.\n\n## Reject pending changes with `plandex reject` 🚫\n\n- You can now reject pending changes to one or more files with the `plandex reject` command. Running it with no arguments will reject all pending changes after confirmation. You can also reject changes to specific files by passing one or more file paths as arguments.\n\n![plandex-reject](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/1.1.0/plandex-reject.gif)\n\n## Summarization and auto-continue fixes 🛤 ️\n\n- Fixes for summarization and auto-continue issues that could Plandex to lose track of where it is in the plan and repeat tasks or do tasks out of order, especially when using `tell` and `continue` after the initial `tell`.\n\n## Verification and auto-fix improvements 🛠️\n\n- Improvements to the verification and auto-fix step. Plandex is now more likely to catch and fix placeholder references like \"// ... existing code ...\" as well as incorrect removal or overwriting of code.\n\n## Stale context fixes 🔄\n\n- After a context file is updated, Plandex is less likely to use an old version of the code from earlier in the conversation--it now uses the latest version much more reliably.\n\n## `plandex convo` command improvements 🗣️\n\n- Added a `--plain / -p` flag to `plandex convo` and `plandex summary` that outputs the conversation/summary in plain text with no ANSI codes.\n- `plandex convo` now accepts a message number or range of messages to display (e.g. `plandex convo 1`, `plandex convo 1-5`, `plandex convo 2-`). Use `plandex convo 1` to show the initial prompt.\n\n## Context management improvements 📄\n\n- Give notes added to context with `plandex load -n 'some note'` an auto-generated name in the `context ls` list.\n- `plandex rm` can now accept a range of indices to remove (e.g. `plandex rm 1-5`)\n- Better help text if `plandex load` is run with incorrect arguments\n- Fix for `plandex load` issue loading paths that begin with `./`\n\n## Better rate limit tolerance 🕰️\n\n- Increase wait times when receiving rate limit errors from OpenAI API (common with new OpenAI accounts that haven't spent $50)\n\n## Built-in model updates 🧠\n\n- Removed 'gpt-4-turbo-preview' from list of built-in models and model packs\n\n## Other fixes 🐞\n\n- Fixes for some occasional rendering issues when streaming plans and build counts\n- Fix for `plandex set-model` model selection showing built-in model options that aren't compatible with the selected role--now only compatible models are shown\n\n## Help updates 📚\n\n- `plandex help` now shows a brief overview on getting started with Plandex rather than the full command list\n- `plandex help --all` or `plandex help -a` shows the full command list\n\n## Version 1.0.0\n- CLI updates for the 1.0.0 release\n- See the [server/v1.0.0 release notes](https://github.com/plandex-ai/plandex/releases/tag/server%2Fv1.0.0) for full details\n\n## Version 0.9.1\n- Fix for occasional stream TUI panic during builds with long file paths (https://github.com/plandex-ai/plandex/issues/105)\n- If auto-upgrade fails due to a permissions issue, suggest re-running command with `sudo` (https://github.com/plandex-ai/plandex/issues/97 - thanks @kalil0321!)\n- Include 'openrouter' in list of model providers when adding a custom model (https://github.com/plandex-ai/plandex/issues/107)\n- Make terminal prompts that shouldn't be optional (like the Base URL for a custom model) required across the board (https://github.com/plandex-ai/plandex/issues/108)\n- Data that is piped into `plandex load` is now automatically given a name in `context ls` via a call to the `namer` role model (previously it had no name, making multiple pipes hard to disambiguate).\n- Still show the '(r)eject file' hotkey in the `plandex changes` TUI when the current file isn't scrollable. \n\n## Version 0.9.0\n## Major file update improvements 📄\n- Much better accuracy for updates to existing files.\n- Plandex is much less likely to screw up braces, parentheses, and other code structures.\n- Plandex is much less likely to mistakenly remove code that it shouldn't.\n\n## Major improvements to long plans with many steps 🛤️\n- Plandex's 'working memory' has been upgraded. It is now much better at working through very long plans without skipping tasks, repeating tasks it's already done, or otherwise losing track of what it's doing.\n\n## 'plandex diff' command ⚖️\n\n![plandex-diff](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-diff.gif)\n\n- New `plandex diff` command shows pending plan changes in `git diff` format.\n\n## Plans can be archived 🗄️\n\n![plandex-archive](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-archive.gif)\n\n- If you aren't using a plan anymore, but you don't want to delete it, you can now archive it.\n- Use `plandex archive` (or `plandex arc` for short) to archive a plan.\n- Use `plandex plans --archived` (or `plandex plans -a`) to see archived plans in the current directory.\n- Use `plandex unarchive` (or `plandex unarc`) to restore an archived plan.\n\n## Custom models!! 🧠\n### Use Plandex with models from OpenRouter, Together.ai, and more\n\n![plandex-models](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-models.gif)\n\n- Use `plandex models add` to add a custom model and use any provider that is compatible with OpenAI, including OpenRouter.ai, Together.ai, Ollama, Replicate, and more.\n- Anthropic Claude models are available via OpenRouter.ai. Google Gemini 1.5 preview is also available on OpenRouter.ai but was flakey in initial testing. Tons of open source models are available on both OpenRouter.ai and Together.ai, among other providers.\n- Some built-in models and model packs (see 'Model packs' below) have been included as a quick way to try out a few of the more powerful model options. Just use `plandex set-model` to try these.\n- You can use a custom model you've added with `plandex set-model`, or add it to a model pack (see 'Model packs' below) with `plandex model-packs create`. Delete custom models you've added with `plandex models delete`.\n- The roles a custom model can be used for depend on its OpenAI compatibility.\n- Each model provider has an `ApiKeyEnvVar` associated with it, like `OPENROUTER_API_KEY`, `TOGETHER_API_KEY`, etc. You will need to have the appropriate environment variables set with a valid api key for each provider that you're using.\n- Because all of Plandex's prompts have been tested against OpenAI models, support for new models should be considered **experimental**.\n- If you find prompting patterns that are effective for certain models, please share them on Discord (https://discord.gg/plandex-ai) or GitHub (https://github.com/plandex-ai/plandex/discussions) and they may be included in future releases.\n\n## Model packs 🎛️\n- Instead of changing models for each role one by one, a model packs let you switch out all roles at once.\n- Use `plandex model-packs create` qto create your own model packs. \n- Use `plandex model-packs` to list built-in and custom model packs. \n- Use `plandex set-model` to load a model pack.\n- Use `plandex model-packs delete` to remove a custom model pack.\n\n## Model defaults ⚙️\n- Instead of only changing models on a per-plan basis, you can set model defaults that will apply to all new plans you start.\n- Use `plandex models default` to see default model settings and `plandex set-model default` to update them. \n\n## More commands 💻\n- `plandex summary` to see the latest plan summary\n- `plandex rename` to rename the current plan\n\n## Quality of life improvements 🧘‍♀️\n- Descriptive top-line for `plandex apply` commit messages instead of just \"applied pending changes\".\n\n![plandex-commit](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-commit.png)\n\n- Better message in `plandex log` when a single piece of context is loaded or updated.\n- Abbreviate really long file paths in `plandex ls`.\n- Changed `OPENAI_ENDPOINT` env var to `OPENAI_API_BASE`, which is more standardized. OPENAI_ENDPOINT is still quietly supported.\n- guides/ENV_VARS.md now lists environment variables you can use with Plandex (and a few convenience varaiables have been addded) - thanks @knno! → https://github.com/plandex-ai/plandex/pull/94\n\n## Bug fixes 🐞\n- Fix for potential crash in `plandex changes` TUI.\n- Fixes for some rare potential deadlocks and conflicts when building a file or stopping a plan stream.\n\n## Version 0.8.3\n- Add support for new OpenAI models: `gpt-4-turbo` and `gpt-4-turbo-2024-04-09`\n- Make `gpt-4-turbo` model the new default model for the planner, builder, and auto-continue roles -- in testing it seems to be better at reasoning and significantly less lazy than the previous default for these roles, `gpt-4-turbo-preview` -- any plan that has not previously had its model settings modified will now use `gpt-4-turbo` by default (those that have been modified will need to be updated manually) -- remember that you can always use `plandex set-model` to change models for your plans\n- Fix for `set-model` command argument parsing (https://github.com/plandex-ai/plandex/issues/75)\n- Fix for panic during plan stream when a file name's length exceeds the terminal width (https://github.com/plandex-ai/plandex/issues/84)\n- Fix for handling files that are loaded into context and later deleted from the file system (https://github.com/plandex-ai/plandex/issues/47)\n- Fix to prevent loading of duplicate files, directory trees, or urls into context (https://github.com/plandex-ai/plandex/issues/57)\n\n## Version 0.8.2\n- Fix root level --help/-h to use custom help command rather than cobra's help message (re: https://github.com/plandex-ai/plandex/issues/25)\n- Include 'survey' fork (https://github.com/plandex-ai/survey) as a proper module instead of a local reference (https://github.com/plandex-ai/plandex/pull/37)\n- Add support for OPENAI_ENDPOINT environment variable for custom OpenAI endpoints (https://github.com/plandex-ai/plandex/pull/46)\n- Add support for OPENAI_ORG_ID environment variable for setting the OpenAI organization ID when using an API key with multiple OpenAI organizations.\n\n## Version 0.8.1\n- Fix for missing 'host' key when creating an account or signing in to a self-hosted server (https://github.com/plandex-ai/plandex/issues/11)\n- `add` alias for `load` command + `unload` alias for `rm` command (https://github.com/plandex-ai/plandex/issues/12)\n- Add `invite`, `revoke`, and `users` commands to `plandex help` output\n- A bit of cleanup of extraneous logging\n\n## Version 0.8.0\n- `plandex invite` command to invite users to an org\n- `plandex users` command to list users and pending invites for an org\n- `plandex revoke` command to revoke an invite or remove a user from an org\n- `plandex sign-in` fixes\n- Fix for context update of directory tree when some paths are ignored\n- Fix for `plandex branches` command showing no branches immediately after plan creation rather than showing the default 'main' branch\n\n## Version 0.7.3\n- Fixes for changes TUI replacement view\n- Fixes for changes TUI text encoding issue\n- Fixes context loading\n- `plandex rm` can now remove a directory from context\n- `plandex apply` fixes to avoid possible conflicts\n- `plandex apply` ask user whether to commit changes\n- Context update fixes\n- Command suggestions can be disabled with PLANDEX_DISABLE_SUGGESTIONS environment variable\n\n## Version 0.7.2\n- PLANDEX_SKIP_UPGRADE environment variable can be used to disable upgrades\n- Color fixes for light backgrounds\n\n## Version 0.7.1\n- Fix for re-running command after an upgrade\n- Fix for user input prompts\n"
  },
  {
    "path": "releases/cli/versions/0.7.1.md",
    "content": "- Fix for re-running command after an upgrade\n- Fix for user input prompts\n"
  },
  {
    "path": "releases/cli/versions/0.7.2.md",
    "content": "- PLANDEX_SKIP_UPGRADE environment variable can be used to disable upgrades\n- Color fixes for light backgrounds\n"
  },
  {
    "path": "releases/cli/versions/0.7.3.md",
    "content": "- Fixes for changes TUI replacement view\n- Fixes for changes TUI text encoding issue\n- Fixes context loading\n- `plandex rm` can now remove a directory from context\n- `plandex apply` fixes to avoid possible conflicts\n- `plandex apply` ask user whether to commit changes\n- Context update fixes\n- Command suggestions can be disabled with PLANDEX_DISABLE_SUGGESTIONS environment variable"
  },
  {
    "path": "releases/cli/versions/0.8.0.md",
    "content": "- `plandex invite` command to invite users to an org\n- `plandex users` command to list users and pending invites for an org\n- `plandex revoke` command to revoke an invite or remove a user from an org\n- `plandex sign-in` fixes\n- Fix for context update of directory tree when some paths are ignored\n- Fix for `plandex branches` command showing no branches immediately after plan creation rather than showing the default 'main' branch"
  },
  {
    "path": "releases/cli/versions/0.8.1.md",
    "content": "- Fix for missing 'host' key when creating an account or signing in to a self-hosted server (https://github.com/plandex-ai/plandex/issues/11)\n- `add` alias for `load` command + `unload` alias for `rm` command (https://github.com/plandex-ai/plandex/issues/12)\n- Add `invite`, `revoke`, and `users` commands to `plandex help` output\n- A bit of cleanup of extraneous logging\n\n"
  },
  {
    "path": "releases/cli/versions/0.8.2.md",
    "content": "- Fix root level --help/-h to use custom help command rather than cobra's help message (re: https://github.com/plandex-ai/plandex/issues/25)\n- Include 'survey' fork (https://github.com/plandex-ai/survey) as a proper module instead of a local reference (https://github.com/plandex-ai/plandex/pull/37)\n- Add support for OPENAI_ENDPOINT environment variable for custom OpenAI endpoints (https://github.com/plandex-ai/plandex/pull/46)\n- Add support for OPENAI_ORG_ID environment variable for setting the OpenAI organization ID when using an API key with multiple OpenAI organizations.\n"
  },
  {
    "path": "releases/cli/versions/0.8.3.md",
    "content": "- Add support for new OpenAI models: `gpt-4-turbo` and `gpt-4-turbo-2024-04-09`\n- Make `gpt-4-turbo` model the new default model for the planner, builder, and auto-continue roles -- in testing it seems to be better at reasoning and significantly less lazy than the previous default for these roles, `gpt-4-turbo-preview` -- any plan that has not previously had its model settings modified will now use `gpt-4-turbo` by default (those that have been modified will need to be updated manually) -- remember that you can always use `plandex set-model` to change models for your plans\n- Fix for `set-model` command argument parsing (https://github.com/plandex-ai/plandex/issues/75)\n- Fix for panic during plan stream when a file name's length exceeds the terminal width (https://github.com/plandex-ai/plandex/issues/84)\n- Fix for handling files that are loaded into context and later deleted from the file system (https://github.com/plandex-ai/plandex/issues/47)\n- Fix to prevent loading of duplicate files, directory trees, or urls into context (https://github.com/plandex-ai/plandex/issues/57)\n"
  },
  {
    "path": "releases/cli/versions/0.9.0.md",
    "content": "## Major file update improvements 📄\n- Much better accuracy for updates to existing files.\n- Plandex is much less likely to screw up braces, parentheses, and other code structures.\n- Plandex is much less likely to mistakenly remove code that it shouldn't.\n\n## Major improvements to long plans with many steps 🛤️\n- Plandex's 'working memory' has been upgraded. It is now much better at working through very long plans without skipping tasks, repeating tasks it's already done, or otherwise losing track of what it's doing.\n\n## 'plandex diff' command ⚖️\n\n![plandex-diff](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-diff.gif)\n\n- New `plandex diff` command shows pending plan changes in `git diff` format.\n\n## Plans can be archived 🗄️\n\n![plandex-archive](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-archive.gif)\n\n- If you aren't using a plan anymore, but you don't want to delete it, you can now archive it.\n- Use `plandex archive` (or `plandex arc` for short) to archive a plan.\n- Use `plandex plans --archived` (or `plandex plans -a`) to see archived plans in the current directory.\n- Use `plandex unarchive` (or `plandex unarc`) to restore an archived plan.\n\n## Custom models!! 🧠\n### Use Plandex with models from OpenRouter, Together.ai, and more\n\n![plandex-models](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-models.gif)\n\n- Use `plandex models add` to add a custom model and use any provider that is compatible with OpenAI, including OpenRouter.ai, Together.ai, Ollama, Replicate, and more.\n- Anthropic Claude models are available via OpenRouter.ai. Google Gemini 1.5 preview is also available on OpenRouter.ai but was flakey in initial testing. Tons of open source models are available on both OpenRouter.ai and Together.ai, among other providers.\n- Some built-in models and model packs (see 'Model packs' below) have been included as a quick way to try out a few of the more powerful model options. Just use `plandex set-model` to try these.\n- You can use a custom model you've added with `plandex set-model`, or add it to a model pack (see 'Model packs' below) with `plandex model-packs create`. Delete custom models you've added with `plandex models delete`.\n- The roles a custom model can be used for depend on its OpenAI compatibility.\n- Each model provider has an `ApiKeyEnvVar` associated with it, like `OPENROUTER_API_KEY`, `TOGETHER_API_KEY`, etc. You will need to have the appropriate environment variables set with a valid api key for each provider that you're using.\n- Because all of Plandex's prompts have been tested against OpenAI models, support for new models should be considered **experimental**.\n- If you find prompting patterns that are effective for certain models, please share them on Discord (https://discord.gg/plandex-ai) or GitHub (https://github.com/plandex-ai/plandex/discussions) and they may be included in future releases.\n\n## Model packs 🎛️\n- Instead of changing models for each role one by one, a model packs let you switch out all roles at once.\n- Use `plandex model-packs create` qto create your own model packs. \n- Use `plandex model-packs` to list built-in and custom model packs. \n- Use `plandex set-model` to load a model pack.\n- Use `plandex model-packs delete` to remove a custom model pack.\n\n## Model defaults ⚙️\n- Instead of only changing models on a per-plan basis, you can set model defaults that will apply to all new plans you start.\n- Use `plandex models default` to see default model settings and `plandex set-model default` to update them. \n\n## More commands 💻\n- `plandex summary` to see the latest plan summary\n- `plandex rename` to rename the current plan\n\n## Quality of life improvements 🧘‍♀️\n- Descriptive top-line for `plandex apply` commit messages instead of just \"applied pending changes\".\n\n![plandex-commit](https://github.com/plandex-ai/plandex/blob/03263a83d76785846fd472693aed03d36a68b86c/releases/images/cli/0.9.0/plandex-commit.png)\n\n- Better message in `plandex log` when a single piece of context is loaded or updated.\n- Abbreviate really long file paths in `plandex ls`.\n- Changed `OPENAI_ENDPOINT` env var to `OPENAI_API_BASE`, which is more standardized. OPENAI_ENDPOINT is still quietly supported.\n- guides/ENV_VARS.md now lists environment variables you can use with Plandex (and a few convenience varaiables have been addded) - thanks @knno! → https://github.com/plandex-ai/plandex/pull/94\n\n## Bug fixes 🐞\n- Fix for potential crash in `plandex changes` TUI.\n- Fixes for some rare potential deadlocks and conflicts when building a file or stopping a plan stream.\n"
  },
  {
    "path": "releases/cli/versions/0.9.1.md",
    "content": "- Fix for occasional stream TUI panic during builds with long file paths (https://github.com/plandex-ai/plandex/issues/105)\n- If auto-upgrade fails due to a permissions issue, suggest re-running command with `sudo` (https://github.com/plandex-ai/plandex/issues/97 - thanks @kalil0321!)\n- Include 'openrouter' in list of model providers when adding a custom model (https://github.com/plandex-ai/plandex/issues/107)\n- Make terminal prompts that shouldn't be optional (like the Base URL for a custom model) required across the board (https://github.com/plandex-ai/plandex/issues/108)\n- Data that is piped into `plandex load` is now automatically given a name in `plandex ls` via a call to the `namer` role model (previously it had no name, making multiple pipes hard to disambiguate).\n- Still show the '(r)eject file' hotkey in the `plandex changes` TUI when the current file isn't scrollable. "
  },
  {
    "path": "releases/cli/versions/1.0.0.md",
    "content": "- CLI updates for the 1.0.0 release\n- See the [server/v1.0.0 release notes](https://github.com/plandex-ai/plandex/releases/tag/server%2Fv1.0.0) for full details"
  },
  {
    "path": "releases/cli/versions/1.1.0.md",
    "content": "## Support for loading images into context with gpt-4o 🖼️\n\n- You can now load images into context with `plandex load path/to/image.png`. Supported image formats are png, jpeg, non-animated gif, and webp. So far, this feature is only available with the default OpenAI GPT-4o model.\n\n![plandex-load-images](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/1.1.0/plandex-images.gif)\n\n## No more hard OpenAI requirement for builder, verifier, and auto-fix roles 🧠\n\n- Non-OpenAI models can now be used for *all* roles, including the builder, verifier, and auto-fix roles, since streaming function calls are no longer required for these roles.\n\n- Note that reliable function calling is still required for these roles. In testing, it was difficult to find models that worked reliably enough for these roles, despite claimed support for function calling. For this reason, using non-OpenAI models for these roles should be considered experimental. Still, this provides a path forward for using open source, local, and other non-OpenAI models for these roles in the future as they improve.\n\n## Reject pending changes with `plandex reject` 🚫\n\n- You can now reject pending changes to one or more files with the `plandex reject` command. Running it with no arguments will reject all pending changes after confirmation. You can also reject changes to specific files by passing one or more file paths as arguments.\n\n![plandex-reject](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/1.1.0/plandex-reject.gif)\n\n## Summarization and auto-continue fixes 🛤 ️\n\n- Fixes for summarization and auto-continue issues that could Plandex to lose track of where it is in the plan and repeat tasks or do tasks out of order, especially when using `tell` and `continue` after the initial `tell`.\n\n## Verification and auto-fix improvements 🛠️\n\n- Improvements to the verification and auto-fix step. Plandex is now more likely to catch and fix placeholder references like \"// ... existing code ...\" as well as incorrect removal or overwriting of code.\n\n## Stale context fixes 🔄\n\n- After a context file is updated, Plandex is less likely to use an old version of the code from earlier in the conversation--it now uses the latest version much more reliably.\n\n## `plandex convo` command improvements 🗣️\n\n- Added a `--plain / -p` flag to `plandex convo` and `plandex summary` that outputs the conversation/summary in plain text with no ANSI codes.\n- `plandex convo` now accepts a message number or range of messages to display (e.g. `plandex convo 1`, `plandex convo 1-5`, `plandex convo 2-`). Use `plandex convo 1` to show the initial prompt.\n\n## Context management improvements 📄\n\n- Give notes added to context with `plandex load -n 'some note'` an auto-generated name in the `context ls` list.\n- `plandex rm` can now accept a range of indices to remove (e.g. `plandex rm 1-5`)\n- Better help text if `plandex load` is run with incorrect arguments\n- Fix for `plandex load` issue loading paths that begin with `./`\n\n## Better rate limit tolerance 🕰️\n\n- Increase wait times when receiving rate limit errors from OpenAI API (common with new OpenAI accounts that haven't spent $50)\n\n## Built-in model updates 🧠\n\n- Removed 'gpt-4-turbo-preview' from list of built-in models and model packs\n\n## Other fixes 🐞\n\n- Fixes for some occasional rendering issues when streaming plans and build counts\n- Fix for `plandex set-model` model selection showing built-in model options that aren't compatible with the selected role--now only compatible models are shown\n\n## Help updates 📚\n\n- `plandex help` now shows a brief overview on getting started with Plandex rather than the full command list\n- `plandex help --all` or `plandex help -a` shows the full command list"
  },
  {
    "path": "releases/cli/versions/1.1.1.md",
    "content": "## Fix for terminal flickering when streaming plans 📺\n\nImprovements to stream handling that greatly reduce flickering in the terminal when streaming a plan, especially when many files are being built simultaneously. CPU usage is also reduced on both the client and server side.\n\n## Claude 3.5 Sonnet model pack is now built-in 🧠\n\nYou can now easily use Claude 3.5 Sonnet with Plandex through OpenRouter.ai.\n\n1. Create an account at [OpenRouter.ai](https://openrouter.ai) if you don't already have one.\n2. [Generate an OpenRouter API key](https://openrouter.ai/keys).\n3. Run `export OPENROUTER_API_KEY=...` in your terminal.\n4. Run `plandex set-model`, select `choose a model pack to change all roles at once` and then choose either `anthropic-claude-3.5-sonnet` (which uses Claude 3.5 Sonnet for all heavy lifting and Claude 3 Haiku for lighter tasks) or `anthropic-claude-3.5-sonnet-gpt-4o` (which uses Claude 3.5 Sonnet for planning and summarization, gpt-4o for builds, and gpt-3.5-turbo for lighter tasks)\n\n![plandex-claude-3.5-sonnet](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/1.1.1/claude-3-5-sonnet.gif)\n\nRemember, you can run `plandex model-packs` for details on all built-in model packs.\n"
  },
  {
    "path": "releases/cli/versions/1.1.2.md",
    "content": "- Minor updates to command help and aliases.\n- Fix for help message shown on invalid command or flags.\n- Fix for `set-model` not updating settings for auto-fix and verifier roles (https://github.com/plandex-ai/plandex/issues/171)"
  },
  {
    "path": "releases/cli/versions/2.0.0.md",
    "content": "👋 Hi, Dane here. I'm the creator and lead developer of Plandex.\n\nI'm excited to announce the beta release of Plandex v2, featuring major improvements in capabilities, user experience, and automation.\n\nPlandex\n\n## 🤖  Overview\n\nWhile built on the same basic foundations as v1, v2 is best thought of as a new project with far more ambitious goals. \n\nPlandex is now a top-tier coding agent with fully autonomous capabilities.\n\nBy default, it combines the strengths of three top foundation model providers—Anthropic, OpenAI, and Google—to achieve significantly better coding results than can be achieved with only a single provider's models.\n\nYou get the coding abilities of Anthropic, the cost-effectiveness and speed of OpenAI's o3 mini, and the massive 2M token context window of Google Gemini, each used in the roles they're best suited for.\n\nPlandex can: \n  - Discuss a project or feature at a high level\n  - Load relevant context as needed throughout the discussion\n  - Solidify the discussion into a detailed plan\n  - Implement the changes\n  - Apply the changes to your files\n  - Run necessary commands\n  - Automatically debug failures\n\nAdding these capabilities together, Plandex can handle complex tasks that span entire large features or entire projects, generating 50-100 files or more in a single run.\n\nBelow is a more detailed look at what's new. You can also check out the updated [README](https://github.com/plandex-ai/plandex/blob/main/README.md), [website](https://plandex.ai), and [docs](https://docs.plandex.ai).\n\n## 🧠  Newer, Smarter Models\n\n- New default model pack combining Claude 3.7 Sonnet, o3-mini, and Gemini 1.5 Pro.\n\n- A new set of built-in models and model packs for different use cases, including `daily-driver` (the default pack), `strong`, `cheap`, and `oss` packs, among others.\n\n- New `architect` and `coder` roles that make it easier to use different models for different stages in the planning and implementation process.\n\n## 📥  Better Context Management\n\n- Automatic context selection with tree-sitter project maps (30+ languages supported).\n\n- Effective 2M token context window for large tasks (massive codebases of ~20M tokens and more can be indexed for automatic context selection).\n\n- Smart context management limits implementation steps to necessary files only, reducing costs and latency.\n\n- Prompt caching for OpenAI and Anthropic models further reduces latency and costs.\n\n## 📝  Reliable File Edits\n\n- Much improved file editing performance and reliability, especially for large files.\n\n- Simple edits can often be applied deterministically without a model call, reducing costs and latency.\n\n- For more complex edits, validation and multiple fallbacks help ensure a very low failure rate.\n\n- Supports individual files up to 100k tokens.\n\n- On Plandex Cloud, a fine-tuned \"instant apply\" model further speeds up and reduces the cost of editing files up to 32k tokens in size. This is offered at no additional cost.\n\n## 💻  New Developer Experience\n\n- v2 includes a new default way to use Plandex: the Plandex REPL. Just type `plandex` in any project directory to start the REPL.\n\n- Simple and intuitive chat-like experience.\n\n- Fuzzy autocomplete for commands and files, 'chat' vs. 'tell' modes that separate ideation from implementation, and a multi-line mode for friendly editing of long prompts.\n\n- All commands are still available as CLI calls directly from the terminal.\n\n## 🚀  Configurable Automation\n\n- Plandex is now capable of full autonomy with 'full auto' mode. It can load necessary context, apply changes, execute commands, and automatically debug problems.\n\n- The automation level can be precisely configured depending on the task and your comfort level. A `basic` mode works just like Plandex v1, where files are loaded manually and execution is disabled. The new default in v2 is `semi-auto`, which enables automatic context loading, but still requires approval to apply changes and execute commands.\n\n- By default, Plandex now includes command execution (with approval) in its planning process. It can install dependencies, build and run code, run tests, and more.\n\n- Command execution is integrated with Plandex's diff review sandbox. Changes are tentatively applied before running commands, then rolled back if the command fails.\n\n- A new `debug` command allows for automated debugging of any terminal command. Use it with type checkers, linters, builds, tests, and more.\n\n## 💳  Built-in Payments, Credits, and Budgeting on Plandex Cloud\n\n- Apart from the open source version of Plandex, which includes **all core features**, Plandex Cloud is a full-fledged product.\n\n- It offers two subscription options: an **Integrated Models** mode that requires no additional accounts or API keys, and a **BYO API Key** mode that allows you to use your own OpenAI and OpenRouter.ai accounts and API keys. \n\n- In Integrated Models mode, you buy credits from Plandex Cloud and manage billing centrally. It includes usage tracking and reporting via the `usage` command, as well as convenience and budgeting features like an auto-recharge threshold, a notification threshold on monthly spend, and an overall monthly limit. You can [learn more about pricing here](https://plandex.ai#pricing).\n\n- Billing settings are managed with a web dashboard (it can be accessed via the CLI with the `billing` command).\n\n## 🪪  License Update\n\n- Plandex has transitioned from AGPL 3.0 to the MIT License, simplifying future open-source contributions and allowing easier integration of proprietary enhancements in Plandex Cloud and related products.\n\n- If you’ve previously contributed under AGPL and have concerns about this relicensing, please [reach out.](mailto:dane@plandex.ai)\n\n## 🧰  And More\n\nThis isn't an exhaustive list! Apart from the above, there are many smaller features, bug fixes, and quality of life improvements. Give the updated [docs](https://docs.plandex.ai) a read for a full accounting of all commands and functionality.\n\n## 🌟  Get Started\n\nGo to the [quickstart](https://docs.plandex.ai/quickstart) to get started with v2 in minutes.\n\n**Note:** while built on the same foundations, Plandex v2 is designed to be a run separately and independently from v1. It's not an in-place upgrade. So there's nothing in particular you need to do to upgrade; just follow the quickstart as if you were a brand new user. [More details here.](https://docs.plandex.ai/upgrading-v1-to-v2)\n\n## 🙌  Don't Be A Stranger\n\n- Jump into the [Plandex Discord](https://discord.gg/plandex-ai) if you have questions or feedback, or just want to hang out.\n\n- You can [post an issue on GitHub](https://github.com/plandex-ai/plandex/issues) or [start a discussion](https://github.com/plandex-ai/plandex/discussions).\n\n- You can reach out by email: [support@plandex.ai](mailto:support@plandex.ai).\n\n- You can follow [@PlandexAI](https://x.com/plandexai) or my personal account [@Danenania](https://x.com/danenania) on X for updates, announcements, and random musings.\n\n- You can subscribe on [YouTube](https://www.youtube.com/@plandex-ny5ry) for demonstrations, tutorials, and AI coding projects.\n\n\n\n"
  },
  {
    "path": "releases/cli/versions/2.0.1.md",
    "content": "- Fix for REPL startup failing when self-hosting or using BYOK cloud mode (https://github.com/plandex-ai/plandex/issues/216)\n- Fix for potential crash with custom model pack (https://github.com/plandex-ai/plandex/issues/217)"
  },
  {
    "path": "releases/cli/versions/2.0.2.md",
    "content": "- Fixed bug where context auto-load would hang if there was no valid context to load (for example, if they're all directories, which is only discovered client-side, and which can't be auto-loaded)\n- Fixed bug where the build output would sometimes wrap incorrectly, causing the Plan Stream TUI to get out of sync with the build output.\n- Fixed bug where build output would jump between collapsed and expanded states during a stream, after the user manually expanded."
  },
  {
    "path": "releases/cli/versions/2.0.3.md",
    "content": "- Fix potential race condition/goroutine explosion/crash in context update.\n- Prevent crash with negative viewport height in stream tui.\n"
  },
  {
    "path": "releases/cli/versions/2.0.4.md",
    "content": "- **Models**\n  - Claude Sonnet 3.7 thinking is now available as a built-in model. Try the `reasoning` model pack for more challenging tasks.\n  - Gemini 2.5 pro (free/experimental version) is now available. Try the 'gemini-planner' or 'gemini-experimental' model packs to use it.\n  - DeepSeek V3 03-24 version is available as a built-in model and is now used in the `oss` pack in the in the the `coder` role. \n  - OpenAI GPT 4.5 is available as a built-in model. It's not in any model packs so far due to rate limits and high cost, but is available to use via `set-model`\n  \n- **Debugging**\n  - Plandex can now directly debug browser applications by catching errors and reading the console logs (requires Chrome).\n  - Enhanced signal handling and subprocess termination robustness for execution control.\n\n- **Model Packs**\n  - Added commands:\n    - `model-packs update`\n    - `model-packs show`\n\n- **Reliability**\n  - Implemented HTTP retry logic with exponential backoff for transient errors.    \n\n- **REPL**\n  - Fixed whitespace handling issues.\n  - Improved command execution flow.\n\n- **Installation**\n  - Clarified support for WSL-only environments.\n  - Better handling of sudo and alias creation on Linux."
  },
  {
    "path": "releases/cli/versions/2.0.5.md",
    "content": "- Consolidated to a single model pack for Gemini 2.5 Pro Experimental: 'gemini-exp'. Use it with 'plandex --gemini-exp' or '\\set-model gemini-exp' in the REPL.\n- Prevent the '\\send' command from being included in the prompt when using multi-line mode in the REPL.\n"
  },
  {
    "path": "releases/cli/versions/2.0.6.md",
    "content": "- Timeout for 'plandex browser' log capture command\n- Better failure handling for 'plandex browser' command"
  },
  {
    "path": "releases/cli/versions/2.0.7+1.md",
    "content": "- Small adjustment to previous release: in the REPL, select the first auto-complete suggestion on 'enter' if any suggestions are listed."
  },
  {
    "path": "releases/cli/versions/2.0.7.md",
    "content": "- Better handling of partial or mistyped commands in the REPL. Rather than falling through to the AI model, a partial `\\` command that matches only a single option will default to that command. If multiple commands could match, you'll be given a list of options. For input that begins with a `\\` but doesn't match any command, there is now a confirmation step. This helps to prevent accidentally sending mistyped commands the model and burning tokens."
  },
  {
    "path": "releases/cli/versions/2.0.8.md",
    "content": "- Additional handling of possibly incorrect or mistyped commands in the REPL. Now apart from suggesting commands only based on possibly mistyped backslash commands, any likely command with or without the backslash will will suggest possible commands rather than sending the prompt straight to the AI model, which can waste tokens due to minor typos or a missing backslash."
  },
  {
    "path": "releases/cli/versions/2.1.0+1.md",
    "content": "- Fix for potential encoding issue when loading files into context."
  },
  {
    "path": "releases/cli/versions/2.1.0.md",
    "content": "## 🚀  OpenRouter only for BYO key\n\n- When using a BYO key mode (either cloud or self-hosted), you can now use Plandex with **only** an OpenRouter.ai account and `OPENROUTER_API_KEY` set. A separate OpenAI account is no longer required.\n\n- You can still use a separate OpenAI account if desired by setting the `OPENAI_API_KEY` environment variable in addition to `OPENROUTER_API_KEY`. This will cause OpenAI models to make direct calls to OpenAI, which is slightly faster and cheaper.\n\n## 🧠  New Models\n\n### Gemini\n\n- Google's Gemini 2.5 Pro Preview is now available as a built-in model, and is the new default model when context is between 200k and 1M tokens.\n\n- A new `gemini-preview` model pack has been added, which uses Gemini 2.5 Pro Preview for planning and coding, and default models for other roles. You can use this pack by running the REPL with the `--gemini-preview` flag (`plandex --gemini-preview`), or with `\\set-model gemini-preview` from inside the REPL. Because this model is still in preview, a fallback to Gemini 1.5 Pro is used on failure.\n\n- Google's Gemini Flash 2.5 Preview is also now available as a built-in model. While it's not currently used by default in any built-in model packs, you can use with `\\set-model` or a custom model pack.\n\n### OpenAI\n\n- OpenAI's o4-mini is now available as a built-in model with `high`, `medium`, and `low` reasoning effort levels. o3-mini has been replaced by the corresponding o4-mini models across all model packs, with a fallback to o3-mini on failure. This improves Plandex's file edit reliability and performance with no increase in costs. o4-mini-medium is also the new default planning model for the `cheap` model pack.\n\n- OpenAI's o3 is now available as a built-in model with `high`, `medium`, and `low` reasoning effort levels. Note that if you're using Plandex in BYO key mode, OpenAI requires an organization verification step before you can use o3.\n\n- o3-high is the new default planning model for the `strong` model pack, replacing o1. Due to the verification requirements for o3, the `strong` pack falls back to o4-mini-high for planning if o3 is not available.\n\n- OpenAI's gpt-4.1, gpt-4.1-mini, and gpt-4.1-nano have been added as built-in models, replacing gpt-4o and gpt-4o-mini in all model packs that used them previously.\n\n- gpt-4.1 is now used as a large context fallback for the default `coder` role, effectively increasing the context limit for the implementation phase from 200k to 1M tokens.\n\n- gpt-4.1 is also the new `coder` model in the `cheap` model pack, and is also the new main planning and coding model in the `openai` model pack.\n\n## 🛟  Model Fallbacks\n\n- In order to better incorporate newly released models and preview models that may have initial reliability or capacity issues, a more robust fallback and retry system has been implemented. This will allow for faster introduction of new models in the future while still maintaining a high level of reliability.\n\n- Fallbacks for 'context length exceeded' errors have also been improved, so that these errors will now trigger an automatic fallback to a model with a larger context limit if one is defined in the model pack. This will fix issues like https://github.com/plandex-ai/plandex/issues/232 where the stream errors with a 400 or 413 error when context is exceeded instead of falling back correctly.\n\n## 💰  Gemini Caching\n\n- Gemini models now support prompt caching, significantly reducing costs and latency during planning, implementation, and builds when using Gemini models.\n\n## 🤫  Quieter Reasoning\n\n- When using Claude 3.7 Sonnet thinking model in the `reasoning` AND `strong` model packs, reasoning is no longer included by default. This clears up some issues that were caused by output with specific formatting that Plandex takes action on being duplicated between the reasoning and the main output. It also feels a bit more relaxed to keep the reasoning behind-the-scenes, even though there can be a longer wait for the initial output.\n\n## 💻  REPL Improvements\n\n- Additional handling of possibly incorrect or mistyped commands in the REPL. Now apart from suggesting commands only based on possibly mistyped backslash commands, any likely command with or without the backslash will suggest possible commands rather than sending the prompt straight to the AI model, which can waste tokens due to minor typos or a missing backslash.\n\n## ☁️  Plandex Cloud\n\n- If you started a free trial of Plandex Cloud with BYO Key mode, you can now switch to a trial of Integrated Models mode if desired from your [billing dashboard](https://app.plandex.ai/settings/billing) (use `\\billing` from the REPL to open the dashboard).\n\n- When doing a trial in Integrated Models mode, you will now be warned when your trial credits balance goes below $1.00.\n\n- In Integrated Models mode, the required number of credits to send a prompt is now much lower, so you can use more credits before getting an 'Insufficient credits' message.\n\n## 🐞  Bug Fixes\n\n- Fix for 'Plan replacement failed' error during file edits on Windows that was caused by mismatched line endings.\n\n- Fix for 'tool calls not supported' error for custom models that use the XML output format (https://github.com/plandex-ai/plandex/issues/238).\n\n- Fix for errors in some roles with Anthropic models when only a single system message was sent (https://github.com/plandex-ai/plandex/issues/208).\n\n- Fix for potential back-pressure issue with large/concurrent project map operations.\n\n- Plandex Cloud: fix for JSON parsing error on payment form when the card is declined. It will now show the proper error message."
  },
  {
    "path": "releases/cli/versions/2.1.1.md",
    "content": "- Fix for free Gemini 2.5 Pro Experimental OpenRouter endpoint.\n- Retries for \"No endpoints found that support cache control\" error that showed up when OpenRouter temporarily disabled caching for Gemini 2.5 Pro Preview.\n- Other minor improvements to error handling and retries.\n"
  },
  {
    "path": "releases/cli/versions/2.1.2.md",
    "content": "- Fix for rare auto-load context timeout error when no files are loaded."
  },
  {
    "path": "releases/cli/versions/2.1.3.md",
    "content": "- Fix for default model pack not being correctly applied to new plans\n- Fix for potential crash on Linux when applying a plan"
  },
  {
    "path": "releases/cli/versions/2.1.5.md",
    "content": "- Added newly released Claude Sonnet 4 and Claude Opus 4 as built-in models.\n- Sonnet 4 isn't yet used in the default 'daily-driver' model pack due to sporadic errors in early testing, but it can be used with the 'sonnet-4-daily' model pack (use '\\set-model sonnet-4-daily' to use it). It will be promoted to the default model pack soon.\n- Opus 4 can be used with the 'opus-4-planner' model pack ( '\\set-model opus-4-planner'), which uses Opus 4 for planning and Sonnet 4 for coding.\n- Removed error fallbacks for o4-mini and gemini-2.5-pro-preview.\n\n\n\n\n"
  },
  {
    "path": "releases/cli/versions/2.1.6+1.md",
    "content": "- Error handling fix\n- Fix for some roles in the `daily-driver` model pack that weren't correctly updated to Sonnet 4 in 2.1.6\n- Added fallback from Sonnet 4 to Sonnet 3.7 to deal with occasional provider errors and rate limit issues"
  },
  {
    "path": "releases/cli/versions/2.1.6.md",
    "content": "- The newly released Claude Sonnet 4 is now stable in testing, so it now replaces Sonnet 3.7 as the default model for context sizes under 200k across all model packs where 3.7 was previously used.\n- A new `strong-opus` model pack is now available. It uses Claude Opus 4 for planning and coding, and is otherwise the same as the 'strong' pack. Use it with `\\set-model strong-opus` to try it out.\n- The `opus-4-planner` model pack that was introduced in 2.1.5 has been renamed to `opus-planner`, but the old name is still supported. This model pack uses Claude Opus 4 for planning, and the default models for other roles.\n- Fix for occasional garbled error message when the model is unresponsive.\n- Fix for occasional 'couldn't aquire lock' error after stream finishes.\n- Additional retry when model is unresponsive or hits provider rate limits—helps particularly with new Opus 4 model on OpenRouter."
  },
  {
    "path": "releases/cli/versions/2.2.0.md",
    "content": "This is a big release that is mainly focused on Plandex's model provider and model config system. It significantly increases model provider flexibility, makes custom model configuration much easier, reduces costs on Cloud, and adds built-in support for Ollama.\n\n## 🔌  Model provider flexibility for BYO key mode\n\n- Now when using Plandex in BYO key mode (either Cloud or self-hosted), you can easily use Plandex with a wide range of built-in model providers.\n\n- Apart from OpenRouter and the OpenAI API (which were already built-in), built-in providers now include:\n  - Anthropic API\n  - Google AI Studio\n  - Google Vertex AI\n  - Azure OpenAI\n  - AWS Bedrock\n  - DeepSeek API\n  - Perplexity API\n  - Ollama (for local models—see below for details)\n\n- See the new [model provider docs](https://docs.plandex.ai/models/model-providers) for more details.\n\n![plandex-model-providers](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/new-providers.gif)\n\n## 🛟  Provider fallback\n\n- When API keys/credentials are provided for multiple providers for a model, Plandex will fail over to the last valid provider if the first one fails. This is especially useful when using a direct provider (like OpenAI or Anthropic) alongside OpenRouter. If the direct call fails, Plandex will fall back to OpenRouter, which has its *own* internal fallback system across multiple providers. You get the best of both worlds: direct access by default, with no additional cost or latency, plus multi-layered resilience in case of stability issues.\n\n## 💰  5.5% price reduction for Plandex Cloud Integrated Models mode\n\n- Thanks to the new model provider system described above, Plandex Cloud with Integrated Models mode can now make direct provider calls under the hood rather than defaulting to OpenRouter, which allows us to avoid OpenRouter's 5.5% surcharge on model calls and pass the savings on to you. OpenRouter is still used as a fallback for added resilience.\n\n## ⚙️  JSON-based model config with IDE support\n\n- Plandex now supports JSON-based model config, which make it much easier to try out different models, different settings, and to use custom models, providers, and model packs.\n\n- The new JSON model config system integrates cleanly with your IDE or editor. When you first edit model settings, Plandex will prompt you to set a preferred editor, and the JSON config file will be opened in that editor.\n\n- Model config files use JSON schemas, which allows for autocomplete, validation, and inline documentation/type hints in most IDEs and editors.\n\n- Check out the new docs for [model settings](https://docs.plandex.ai/models/model-settings) and [custom models](https://docs.plandex.ai/models/custom-models) for full details.\n\n### `set-model` and `set-model default` commands\n\n- `set-model` has been simplified to work with the new system. If run without arguments, you'll be prompted to either select a built-in or custom model pack, or to directly edit the current plan's model config inline in JSON. You can also pass it a model pack name (`set-model daily-driver`) or jump straight to the JSON settings with `set-model --json`.\n\n- `set-model default` works the same way, but allows you to configure the default model settings for new plans.\n\n![plandex-model-settings-json](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/model-settings-json.gif)\n\n\n### `models custom` command\n\n- `models custom` is a new all-in-one command for managing custom providers, models, and model packs in one place. It replaces the `models add`, `models delete`, `model-packs create`, `model-packs update`, and `model-packs delete` commands.\n\n- The first time you run it, if you haven't already configured any custom models or model packs, an example config file will be created to get you started. If you *do* already have custom models or model packs configured, the config file will be populated with those models and model packs.\n\n![plandex-custom-models-json](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/custom-models-json.gif)\n\n### `models` and `models default` commands\n\n- The `models` and `models default` commands now show simplified output by default, with a new `--all` flag to show all properties.\n\n- These commands also now show all fallback models (for large context, large output, errors, etc.) for each role, including multiple levels of fallback, which previously weren't always included in the output.\n\n## 🦙  Built-in Ollama support\n\n- Plandex now offers built-in support for Ollama, which makes it much easier to use local models. Check out the new [Ollama quickstart](https://docs.plandex.ai/models/ollama) for details.\n\n![plandex-ollama](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/2.2.0/ollama.gif)\n\n## 📖  Built-in models and models packs\n\n- The docs now include all built-in [models](https://docs.plandex.ai/models/built-in/built-in-models) and [model packs](https://docs.plandex.ai/models/built-in/built-in-packs), making it easier to see what's available, and the settings for each model/model pack.\n\n## 🧠  New built-in models\n\n- `mistral/devstral-small`, with both OpenRouter and Ollama providers.\n\n- The Qwen 3 series of models, from 8B to 235B, available with `cloud` variants for OpenRouter and `local` variants for Ollama.\n\n- Distilled local versions of `deepseek/r1`, from 8b to 70b, available with Ollama provider.\n\n## 🎛️  New model packs\n\n- The `gemini-exp` model pack has been removed, and in its place there's now a new `gemini-planner` model pack, which uses Gemini 2.5 Pro for planning and context selection, and the default models for other roles, as well as a new `google` model pack, which uses either Gemini 2.5 Pro or Gemini 2.5 Flash for all roles.\n\n- A new `o3-planner` model pack has been added, which uses OpenAI o3-medium for planning and context selection, and the default models for other roles.\n\n## 🔄  Other built-in model updates\n\n- Built-in Gemini 2.5 Pro and Gemini 2.5 Flash models now use the latest model identifiers (replacing old 2.5 Pro Preview and 2.5 Flash Preview identifiers)\n\n- The `gemini-preview` model pack has been removed, and a new `gemini-planner` model pack has been added, which uses Gemini 2.5 Pro for planning and context selection, and the default models for other roles.\n\n- OpenAI o3 models have had their pricing drastically reduced when using Plandex Cloud with Integrated Models mode—input tokens now cost $2/M, output $8/M, an 80% reduction.\n\n- The `deepseek/r1` model has been updated to use the latest model identifier (`deepseek/deepseek-r1-0528`) on OpenRouter.\n\n## 🐞  Bug fixes\n\n- Fixed a file mapping bug for TypeScript files that caused directly exported symbols like `export const foo = 'bar'` to be omitted from map files. Also improved TypeScript mapping support for some other constructs like `declare global`, `namespace`, and `enum` blocks, and improved handling of arrow functions. Thanks to @mnahkies for the [PR](https://github.com/plandex-ai/plandex/pull/239) identifying this.\n\n## 🔧  Other changes\n\n- `plandex checkout` now has a `--yes/-y` flag to auto-confirm creating a new branch if it doesn't exist, so the command can be used for scripting with no user interaction.\n\n- `plandex tell`, `plandex continue`, and `plandex build` all now support a `--skip-menu` flag to skip the interactive menu that appears when the response finishes and changes are pending. There's also a new `skip-changes-menu` config setting that can be set to `true` to skip this menu by default."
  },
  {
    "path": "releases/cli/versions/2.2.1.md",
    "content": "## 🖇️  Connect your Claude Pro or Max subscription\n\nIf you have a Claude Pro or Max subscription, Plandex can use it when calling Anthropic models. You can use it in either Integrated Models Mode on Plandex Cloud, or in BYO Key Mode (whether on Cloud or self-hosting).\n\nAssuming you're using Anthropic models (which the default model pack does), you'll be asked if you want to connect your Claude subscription the first time you run Plandex. Follow the instructions to connect.\n\n[Learn more in the docs.](https://docs.plandex.ai/models/claude-subscription)\n\n## 🐞  Bug fixes\n\nFixed an [issue](https://github.com/plandex-ai/plandex/issues/291) with custom models and providers."
  },
  {
    "path": "releases/server/CHANGELOG.md",
    "content": "## Server Version 2.2.1\nSee CLI 2.2.1 release notes.\n\n## Server Version 2.2.1\nSee CLI 2.2.1 release notes.\n\n## Server Version 2.2.0\nSee CLI 2.2.0 release notes.\n\n## Server Version 2.1.8\n- Fix for potential hang in file map queue\n\n## Server Version 2.1.7\n- Fix for \"conversation summary timestamp not found in conversation\" error (https://github.com/plandex-ai/plandex/issues/274)\n- Fix for potential panic/crash during plan stream (https://github.com/plandex-ai/plandex/issues/275)\n- Better protection against panics/crashes in server goroutines across the board\n\n## Server Version 2.1.6+1\nSee CLI 2.1.6+1 release notes.\n\n## Server Version 2.1.6\nSee CLI 2.1.6 release notes.\n\n## Server Version 2.1.5\nSee CLI 2.1.5 release notes.\n\n## Server Version 2.1.4\n- Fix to remove occasional extraneous blank lines from start/end of edited files.\n\n## Server Version 2.1.3\n- Fix for 'panic in execTellPlan' error when using a model pack that doesn't explicitly set the 'coder' or 'whole-file-builder' roles\n\n## Server Version 2.1.2\n- Fix for auto-load context error: 'Error decoding response → EOF'\n\n## Server Version 2.1.1+1\n- Improve error handling to catch yet another \"context length exceeded\" error message variation from Anthropic.\n\n## Server Version 2.1.1\nSee CLI 2.1.1 release notes.\n\n## Server Version 2.1.0+1\n- Fix for context length exceeded error that still wasn't being caught and retried by the fallback correctly.\n\n## Server Version 2.1.0\nSee CLI 2.1.0 release notes.\n\n## Server Version 2.0.6\n- Improvements to process management and cleanup for command execution\n- Remove extraneous model request logging\n\n## Server Version 2.0.5\n- Fix for a bug that was causing occasional model errors. Model calls should be much more reliable now.\n- Better error handling and error messages for model errors (rate limits or other errors).\n- No error retries for rate limit errors.\n- Fixed bug that caused retries to add the prompt to the conversation multiple times.\n- Error responses with no output no longer create a log entry.\n\n## Server Version 2.0.4\n- **Stability**\n  - Enhanced database locking mechanisms.\n  - Improved error notifications.\n\n- **API Enhancements**\n  - Added endpoints for managing custom models and updating model packs.\n\n- **Execution**\n  - Increased robustness in plan execution and subprocess lifecycle management.\n\n- **Observability**\n  - Real-time internal notifications for critical errors implemented.\n\n- **Consistency**\n  - Improved token management.\n  - Enhanced summarization accuracy.\n\n## Server Version 2.0.3\n- Fix for potential crash during chat/tell operation.\n- Panic handling to prevent crashes in general.\n- Fix for local queue handling bug during builds that could cause queue to get stuck and cause subsequent operations to hang.\n\n## Server Version 2.0.2\nServer-side fix for context auto-load hanging when there's no valid context to load (for example, if they're all directories, which is only discovered client-side, and which can't be auto-loaded)\n\n## Server Version 2.0.0+2\n- Version tag sanitation fix for GitHub Action to build and push server image to DockerHub\n\n## Server Version 2.0.0+1\n- Fix for custom model creation (https://github.com/plandex-ai/plandex/issues/214)\n- Fix for version check on self-hosted (https://github.com/plandex-ai/plandex/issues/213)\n- Fix for GitHub Action to build and push server image to DockerHub\n\n## Server Version 2.0.0\nSee CLI 2.0.0 release notes.\n\n## Version 1.1.1\n- Improvements to stream handling that greatly reduce flickering in the terminal when streaming a plan, especially when many files are being built simultaneously. CPU usage is also reduced on both the client and server side.\n- Claude 3.5 Sonnet model and model pack (via OpenRouter.ai) is now built-in.\n\n## Version 1.1.1\n- Improvements to stream handling that greatly reduce flickering in the terminal when streaming a plan, especially when many files are being built simultaneously. CPU usage is also reduced on both the client and server side.\n- Claude 3.5 Sonnet model and model pack (via OpenRouter.ai) is now built-in.\n\n## Version 1.1.1\n- Improvements to stream handling that greatly reduce flickering in the terminal when streaming a plan, especially when many files are being built simultaneously. CPU usage is also reduced on both the client and server side.\n- Claude 3.5 Sonnet model and model pack (via OpenRouter.ai) is now built-in.\n\n## Version 1.1.0\n- Give notes added to context with `plandex load -n 'some note'` automatically generated names in `context ls` list.\n- Fixes for summarization and auto-continue issues that could Plandex to lose track of where it is in the plan and repeat tasks or do tasks out of order, especially when using `tell` and `continue` after the initial `tell`.\n- Improvements to the verification and auto-fix step. Plandex is now more likely to catch and fix placeholder references like \"// ... existing code ...\" as well as incorrect removal or overwriting of code.\n- After a context file is updated, Plandex is less likely to use an old version of the code from earlier in the conversation--it now uses the latest version much more reliably.\n- Increase wait times when receiving rate limit errors from OpenAI API (common with new OpenAI accounts that haven't spent $50).\n\n## Version 1.0.1\n- Fix for occasional 'Error getting verify state for file' error\n- Fix for occasional 'Fatal: unable to write new_index file' error\n- Fix for occasional 'nothing to commit, working tree clean' error\n- When hitting OpenAI rate limits, Plandex will now parse error messages that include a recommended wait time and automatically wait that long before retrying, up to 30 seconds (https://github.com/plandex-ai/plandex/issues/123)\n- Some prompt updates to encourage creation of multiple smaller files rather than one mega-file when generating files for a new feature or project. Multiple smaller files are faster to generate, use less tokens, and have a lower error rate compared to a continually updated large file.\n\n## Version 1.0.0\n##   ☄️  🌅   gpt-4o is the real deal for coding\n\n- gpt-4o, OpenAI's latest model, is the new default model for Plandex. 4o is much better than gpt-4-turbo (the previous default model) in early testing for coding tasks and agent workflows.\n- If you have not used `plandex set-model` or `plandex set-model default` previously to set a custom model, you will now be use gpt-4o by default. If you *have* used one of those commands, use `plandex set-model` or `plandex set-model default` and select the new `gpt-4o-latest` model-pack to upgrade. \n \n##   🛰️  🏥   Reliability improvements: 90% reduction in syntax errors in early testing\n\n- Automatic syntax and logic validation with an auto-correction step for file updates.\n- Significantly improves reliability and reduces syntax errors, mistaken duplication or removal of code, placeholders that reference other code and other similar issues. \n- With a set of ~30 internal evals spanning 5 common languages, syntax errors were reduced by over 90% on average with gpt-4o. \n- Logical errors are also reduced (I'm still working on evals for those to get more precise numbers).\n- Plandex is now much better at handling large files and plans that make many updates to the same file. Both could be problematic in previous versions.\n- Plandex is much more resilient to incorrectly labelled file blocks when the model uses the file label format incorrectly to explain something rather than for a file. i.e. \"Run this script\" and then a bash script block. Previously Plandex would mistakenly create a file called \"Run this script\". It now ignores blocks like these.\n\n##   🧠  🚞   Improvements to core planning engine: better memory and less laziness allow you to accomplish larger and more complex tasks without errors or stopping early\n\n- Plandex is now much better at working through long plans without skipping tasks, repeating tasks it's already done, or otherwise losing track of what it's doing.\n- Plandex is much less likely to leave TODO placeholders in comments instead of fully completing a task, or to otherwise leave a task incomplete.\n- Plandex is much less likely to end a plan before all tasks are completed.\n\n##   🏎️  📈   Performance improvements: 2x faster planning and execution\n\n- gpt-4o is twice as fast as gpt-4-turbo for planning, summarization, builds, and more.\n- If you find it's streaming too fast and you aren't able to review the output, try using the `--stop / -s` flag with `plandex tell` or `plandex continue`. It will stop the plan after a single response so you can review it before proceeding. Use `plandex continue` to proceed with the plan once you're ready.\n- Speaking of which, if you're in exploratory mode and want to use less tokens, you can also use the `--no-build / -n` flag with `plandex tell` and `plandex continue`. This prevents Plandex from building files until you run `plandex build` manually.\n\n##   💰  🪙   2x cost reduction: gpt-4o is half the per-token price of gpt-4-turbo\n\n- For the same quantity of tokens, with improved quality and 2x speed, you'll pay half-price.\n\n##   👩‍💻  🎭   New `plandex-dev` and `pdxd` alias in development mode\n\n- In order to avoid conflicts/overwrites with the `plandex` CLI and `pdx` alias, a new `plandex-dev` command and `pdxd` alias have been added in development mode. \n\n##  🐛  🛠️   Bug fixes\n\n- Fix for a potential panic during account creation (https://github.com/plandex-ai/plandex/issues/76)\n- Fixes for some account creation flow issues (https://github.com/plandex-ai/plandex/issues/106)\n- Fix for occasional \"Stream buffer tokens too high\" error (https://github.com/plandex-ai/plandex/issues/34).\n- Fix for potential panic when updating model settings. Might possibly be the cause of or somehow related to https://github.com/plandex-ai/plandex/issues/121 but hard to be sure (maybe AWS was just being flakey).\n- Attempted fix for rare git repo race condition @jesseswell_1 caught that gives error ending with: \n```\nExit status 128, output\n      * Fatal: unable to write new_index file\n```\n\n##   📚  🤔   Readme updates\n\n- The [readme](https://github.com/plandex-ai/plandex) has been revamped to be more informative and easier to navigate.\n\n##  🏡  📦   Easy self-contained startup script for local mode and self-hosting\n\n```bash\ngit clone https://github.com/plandex-ai/plandex.git\ncd plandex/app\n./start_local.sh\n``` \n\n- Sincere thanks to @ZanzyTHEbar aka @daofficialwizard on Discord who wrote the script! 🙏🙏\n\n##   🚀  ☝️   Upgrading   \n\n- As always, cloud has already been updated with the latest version. To upgrade the CLI, run any `plandex` command (like `plandex version` or `plandex help` or whatever command you were about to run anyway 🙂)\n\n##   💬  📆   Join me for office hours every Friday 12:30-1:30pm PST in Discord, starting May 17th\n\n- I'll be available by voice and text chat to answer questions, talk about the new version, and hear about your use cases. Come on over and hang out! \n- Join the discord to get a reminder when office hours are starting: https://discord.gg/plandex-ai\n\n## Version 0.9.1\n- Improvements to auto-continue check. Plandex now does a better job determining whether a plan is finished or should automatically continue by incorporating the either the latest plan summary or the previous conversation message (if the summary isn't ready yet) into the auto-continue check. Previously the check was using only the latest conversation message.\n- Fix for 'exit status 128' errors in a couple of edge case scenarios.\n- Data that is piped into `plandex load` is now automatically given a name in `context ls` via a call to the `namer` role model (previously it had no name, making multiple pipes hard to disambiguate).\n\n## Version 0.9.0\n- Support for custom models, model packs, and default models (see CLI 0.9.0 release notes for details).\n- Better accuracy for updates to existing files.\n- Plandex is less likely to screw up braces, parentheses, and other code structures.\n- Plandex is less likely to mistakenly remove code that it shouldn't.\n- Plandex is now much better at working through very long plans without skipping tasks, repeating tasks it's already done, or otherwise losing track of what it's doing.\n- Server-side support for `plandex diff` command to show pending plan changes in `git diff` format.\n- Server-side support for archiving and unarchiving plans.\n- Server-side support for `plandex summary` command.\n- Server-side support for `plandex rename` command.\n- Descriptive top-line for `plandex apply` commit messages instead of just \"applied pending changes\".\n- Better message in `plandex log` when a single piece of context is loaded or updated.\n- Fixes for some rare potential deadlocks and conflicts when building a file or stopping astream.\n\n## Version 0.8.4\n- Add support for new OpenAI models: `gpt-4-turbo` and `gpt-4-turbo-2024-04-09`\n- Make `gpt-4-turbo` model the new default model for the planner, builder, and auto-continue roles -- in testing it seems to be better at reasoning and significantly less lazy than the previous default for these roles, `gpt-4-turbo-preview` -- any plan that has not previously had its model settings modified will now use `gpt-4-turbo` by default (those that have been modified will need to be updated manually) -- remember that you can always use `plandex set-model` to change models for your plans\n- Fix for handling files that are loaded into context and later deleted from the file system (https://github.com/plandex-ai/plandex/issues/47)\n- Handle file paths with ### prefixes (https://github.com/plandex-ai/plandex/issues/77)\n- Fix for occasional race condition during file builds that causes error \"Fatal: Unable to write new index file\"\n\n## Version 0.8.3\n- SMTP_FROM environment variable for setting from address when self-hosting and using SMTP (https://github.com/plandex-ai/plandex/pull/39)\n- Add support for OPENAI_ENDPOINT environment variable for custom OpenAI endpoints (https://github.com/plandex-ai/plandex/pull/46)\n- Add support for OPENAI_ORG_ID environment variable for setting the OpenAI organization ID when using an API key with multiple OpenAI organizations.\n- Fix for unhelpful \"Error getting plan, context, convo, or summaries\" error message when OpenAI returns an error for invalid API key or insufficient credits (https://github.com/plandex-ai/plandex/issues/32)\n\n## Version 0.8.2\n- Fix for creating an org that auto-adds users based on email domain (https://github.com/plandex-ai/plandex/issues/24)\n- Fix for possible crash after error in file build\n- Added crash prevention measures across the board\n- Fix for occasional \"replacements failed\" error\n- Reliability and improvements for file updates\n- Fix for role name of auto-continue model\n\n## Version 0.8.1\n- Fixes for two potential server crashes\n- Fix for server git repo remaining in locked state after a crash, which caused various issues\n- Fix for server git user and email not being set in some environments (https://github.com/plandex-ai/plandex/issues/8)\n- Fix for 'replacements failed' error that was popping up in some circumstances\n- Fix for build issue that could cause large updates to fail, take too long, or use too many tokens in some circumstances\n- Clean up extraneous logging\n- Prompt update to prevent ouputting files at absolute paths (like '/etc/config.txt')\n- Prompt update to prevent sometimes using file block format for explanations, causing explanations to be outputted as files\n- Prompt update to prevent stopping before the plan is really finished \n- Increase maximum number of auto-continuations to 50 (from 30)\n\n## Version 0.8.0\n- User management improvements and fixes\n- Backend support for `plandex invite`, `plandex users`, and `plandex revoke` commands\n- Improvements to copy for email verification emails\n- Fix for org creation when creating a new account\n- Send an email to invited user when they are invited to an org\n- Add timeout when forwarding requests from one instance to another within a cluster\n\n## Version 0.7.1\n- Fix for SMTP email issue\n- Add '/version' endpoint to server\n\n## Version 0.7.0\nInitial release\n"
  },
  {
    "path": "releases/server/versions/0.7.0.md",
    "content": "Initial release"
  },
  {
    "path": "releases/server/versions/0.7.1.md",
    "content": "- Fix for SMTP email issue\n- Add '/version' endpoint to server"
  },
  {
    "path": "releases/server/versions/0.8.0.md",
    "content": "- User management improvements and fixes\n- Backend support for `plandex invite`, `plandex users`, and `plandex revoke` commands\n- Improvements to copy for email verification emails\n- Fix for org creation when creating a new account\n- Send an email to invited user when they are invited to an org\n- Add timeout when forwarding requests from one instance to another within a cluster"
  },
  {
    "path": "releases/server/versions/0.8.1.md",
    "content": "- Fixes for two potential server crashes\n- Fix for server git repo remaining in locked state after a crash, which caused various issues\n- Fix for server git user and email not being set in some environments (https://github.com/plandex-ai/plandex/issues/8)\n- Fix for 'replacements failed' error that was popping up in some circumstances\n- Fix for build issue that could cause large updates to fail, take too long, or use too many tokens in some circumstances\n- Clean up extraneous logging\n- Prompt update to prevent ouputting files at absolute paths (like '/etc/config.txt')\n- Prompt update to prevent sometimes using file block format for explanations, causing explanations to be outputted as files\n- Prompt update to prevent stopping before the plan is really finished \n- Increase maximum number of auto-continuations to 50 (from 30)\n"
  },
  {
    "path": "releases/server/versions/0.8.2.md",
    "content": "- Fix for creating an org that auto-adds users based on email domain (https://github.com/plandex-ai/plandex/issues/24)\n- Fix for possible crash after error in file build\n- Added crash prevention measures across the board\n- Fix for occasional \"replacements failed\" error\n- Reliability and improvements for file updates\n- Fix for role name of auto-continue model"
  },
  {
    "path": "releases/server/versions/0.8.3.md",
    "content": "- SMTP_FROM environment variable for setting from address when self-hosting and using SMTP (https://github.com/plandex-ai/plandex/pull/39)\n- Add support for OPENAI_ENDPOINT environment variable for custom OpenAI endpoints (https://github.com/plandex-ai/plandex/pull/46)\n- Add support for OPENAI_ORG_ID environment variable for setting the OpenAI organization ID when using an API key with multiple OpenAI organizations.\n- Fix for unhelpful \"Error getting plan, context, convo, or summaries\" error message when OpenAI returns an error for invalid API key or insufficient credits (https://github.com/plandex-ai/plandex/issues/32)\n"
  },
  {
    "path": "releases/server/versions/0.8.4.md",
    "content": "- Add support for new OpenAI models: `gpt-4-turbo` and `gpt-4-turbo-2024-04-09`\n- Make `gpt-4-turbo` model the new default model for the planner, builder, and auto-continue roles -- in testing it seems to be better at reasoning and significantly less lazy than the previous default for these roles, `gpt-4-turbo-preview` -- any plan that has not previously had its model settings modified will now use `gpt-4-turbo` by default (those that have been modified will need to be updated manually) -- remember that you can always use `plandex set-model` to change models for your plans\n- Fix for handling files that are loaded into context and later deleted from the file system (https://github.com/plandex-ai/plandex/issues/47)\n- Handle file paths with ### prefixes (https://github.com/plandex-ai/plandex/issues/77)\n- Fix for occasional race condition during file builds that causes error \"Fatal: Unable to write new index file\""
  },
  {
    "path": "releases/server/versions/0.9.0.md",
    "content": "- Support for custom models, model packs, and default models (see CLI 0.9.0 release notes for details).\n- Better accuracy for updates to existing files.\n- Plandex is less likely to screw up braces, parentheses, and other code structures.\n- Plandex is less likely to mistakenly remove code that it shouldn't.\n- Plandex is now much better at working through very long plans without skipping tasks, repeating tasks it's already done, or otherwise losing track of what it's doing.\n- Server-side support for `plandex diff` command to show pending plan changes in `git diff` format.\n- Server-side support for archiving and unarchiving plans.\n- Server-side support for `plandex summary` command.\n- Server-side support for `plandex rename` command.\n- Descriptive top-line for `plandex apply` commit messages instead of just \"applied pending changes\".\n- Better message in `plandex log` when a single piece of context is loaded or updated.\n- Fixes for some rare potential deadlocks and conflicts when building a file or stopping astream."
  },
  {
    "path": "releases/server/versions/0.9.1.md",
    "content": "- Improvements to auto-continue check. Plandex now does a better job determining whether a plan is finished or should automatically continue by incorporating the either the latest plan summary or the previous conversation message (if the summary isn't ready yet) into the auto-continue check. Previously the check was using only the latest conversation message.\n- Fix for 'exit status 128' errors in a couple of edge case scenarios.\n- Data that is piped into `plandex load` is now automatically given a name in `context ls` via a call to the `namer` role model (previously it had no name, making multiple pipes hard to disambiguate)."
  },
  {
    "path": "releases/server/versions/1.0.0.md",
    "content": "##   ☄️  🌅   gpt-4o is the real deal for coding\n\n- gpt-4o, OpenAI's latest model, is the new default model for Plandex. 4o is much better than gpt-4-turbo (the previous default model) in early testing for coding tasks and agent workflows.\n- If you have not used `plandex set-model` or `plandex set-model default` previously to set a custom model, you will now be use gpt-4o by default. If you *have* used one of those commands, use `plandex set-model` or `plandex set-model default` and select the new `gpt-4o-latest` model-pack to upgrade. \n \n##   🛰️  🏥   Reliability improvements: 90% reduction in syntax errors in early testing\n\n- Automatic syntax and logic validation with an auto-correction step for file updates.\n- Significantly improves reliability and reduces syntax errors, mistaken duplication or removal of code, placeholders that reference other code and other similar issues. \n- With a set of ~30 internal evals spanning 5 common languages, syntax errors were reduced by over 90% on average with gpt-4o. \n- Logical errors are also reduced (I'm still working on evals for those to get more precise numbers).\n- Plandex is now much better at handling large files and plans that make many updates to the same file. Both could be problematic in previous versions.\n- Plandex is much more resilient to incorrectly labelled file blocks when the model uses the file label format incorrectly to explain something rather than for a file. i.e. \"Run this script\" and then a bash script block. Previously Plandex would mistakenly create a file called \"Run this script\". It now ignores blocks like these.\n\n##   🧠  🚞   Improvements to core planning engine: better memory and less laziness allow you to accomplish larger and more complex tasks without errors or stopping early\n\n- Plandex is now much better at working through long plans without skipping tasks, repeating tasks it's already done, or otherwise losing track of what it's doing.\n- Plandex is much less likely to leave TODO placeholders in comments instead of fully completing a task, or to otherwise leave a task incomplete.\n- Plandex is much less likely to end a plan before all tasks are completed.\n\n##   🏎️  📈   Performance improvements: 2x faster planning and execution\n\n- gpt-4o is twice as fast as gpt-4-turbo for planning, summarization, builds, and more.\n- If you find it's streaming too fast and you aren't able to review the output, try using the `--stop / -s` flag with `plandex tell` or `plandex continue`. It will stop the plan after a single response so you can review it before proceeding. Use `plandex continue` to proceed with the plan once you're ready.\n- Speaking of which, if you're in exploratory mode and want to use less tokens, you can also use the `--no-build / -n` flag with `plandex tell` and `plandex continue`. This prevents Plandex from building files until you run `plandex build` manually.\n\n##   💰  🪙   2x cost reduction: gpt-4o is half the per-token price of gpt-4-turbo\n\n- For the same quantity of tokens, with improved quality and 2x speed, you'll pay half-price.\n\n##   👩‍💻  🎭   New `plandex-dev` and `pdxd` alias in development mode\n\n- In order to avoid conflicts/overwrites with the `plandex` CLI and `pdx` alias, a new `plandex-dev` command and `pdxd` alias have been added in development mode. \n\n##  🐛  🛠️   Bug fixes\n\n- Fix for a potential panic during account creation (https://github.com/plandex-ai/plandex/issues/76)\n- Fixes for some account creation flow issues (https://github.com/plandex-ai/plandex/issues/106)\n- Fix for occasional \"Stream buffer tokens too high\" error (https://github.com/plandex-ai/plandex/issues/34).\n- Fix for potential panic when updating model settings. Might possibly be the cause of or somehow related to https://github.com/plandex-ai/plandex/issues/121 but hard to be sure (maybe AWS was just being flakey).\n- Attempted fix for rare git repo race condition @jesseswell_1 caught that gives error ending with: \n```\nExit status 128, output\n      * Fatal: unable to write new_index file\n```\n\n##   📚  🤔   Readme updates\n\n- The [readme](https://github.com/plandex-ai/plandex) has been revamped to be more informative and easier to navigate.\n\n##  🏡  📦   Easy self-contained startup script for local mode and self-hosting\n\n```bash\ngit clone https://github.com/plandex-ai/plandex.git\ncd plandex/app\n./start_local.sh\n``` \n\n- Sincere thanks to @ZanzyTHEbar aka @daofficialwizard on Discord who wrote the script! 🙏🙏\n\n##   🚀  ☝️   Upgrading   \n\n- As always, cloud has already been updated with the latest version. To upgrade the CLI, run any `plandex` command (like `plandex version` or `plandex help` or whatever command you were about to run anyway 🙂)\n\n##   💬  📆   Join me for office hours every Friday 12:30-1:30pm PST in Discord, starting May 17th\n\n- I'll be available by voice and text chat to answer questions, talk about the new version, and hear about your use cases. Come on over and hang out! \n- Join the discord to get a reminder when office hours are starting: https://discord.gg/plandex-ai"
  },
  {
    "path": "releases/server/versions/1.0.1.md",
    "content": "- Fix for occasional 'Error getting verify state for file' error\n- Fix for occasional 'Fatal: unable to write new_index file' error\n- Fix for occasional 'nothing to commit, working tree clean' error\n- When hitting OpenAI rate limits, Plandex will now parse error messages that include a recommended wait time and automatically wait that long before retrying, up to 30 seconds (https://github.com/plandex-ai/plandex/issues/123)\n- Some prompt updates to encourage creation of multiple smaller files rather than one mega-file when generating files for a new feature or project. Multiple smaller files are faster to generate, use less tokens, and have a lower error rate compared to a continually updated large file."
  },
  {
    "path": "releases/server/versions/1.1.0.md",
    "content": "- Give notes added to context with `plandex load -n 'some note'` automatically generated names in `context ls` list.\n- Fixes for summarization and auto-continue issues that could Plandex to lose track of where it is in the plan and repeat tasks or do tasks out of order, especially when using `tell` and `continue` after the initial `tell`.\n- Improvements to the verification and auto-fix step. Plandex is now more likely to catch and fix placeholder references like \"// ... existing code ...\" as well as incorrect removal or overwriting of code.\n- After a context file is updated, Plandex is less likely to use an old version of the code from earlier in the conversation--it now uses the latest version much more reliably.\n- Increase wait times when receiving rate limit errors from OpenAI API (common with new OpenAI accounts that haven't spent $50)."
  },
  {
    "path": "releases/server/versions/1.1.1.md",
    "content": "- Improvements to stream handling that greatly reduce flickering in the terminal when streaming a plan, especially when many files are being built simultaneously. CPU usage is also reduced on both the client and server side.\n- Claude 3.5 Sonnet model and model pack (via OpenRouter.ai) is now built-in."
  },
  {
    "path": "releases/server/versions/2.0.0+1.md",
    "content": "- Fix for custom model creation (https://github.com/plandex-ai/plandex/issues/214)\n- Fix for version check on self-hosted (https://github.com/plandex-ai/plandex/issues/213)\n- Fix for GitHub Action to build and push server image to DockerHub"
  },
  {
    "path": "releases/server/versions/2.0.0+2.md",
    "content": "- Version tag sanitation fix for GitHub Action to build and push server image to DockerHub\n"
  },
  {
    "path": "releases/server/versions/2.0.0.md",
    "content": "See CLI 2.0.0 release notes."
  },
  {
    "path": "releases/server/versions/2.0.2.md",
    "content": "Server-side fix for context auto-load hanging when there's no valid context to load (for example, if they're all directories, which is only discovered client-side, and which can't be auto-loaded)"
  },
  {
    "path": "releases/server/versions/2.0.3.md",
    "content": "- Fix for potential crash during chat/tell operation.\n- Panic handling to prevent crashes in general.\n- Fix for local queue handling bug during builds that could cause queue to get stuck and cause subsequent operations to hang."
  },
  {
    "path": "releases/server/versions/2.0.4.md",
    "content": "- **Stability**\n  - Enhanced database locking mechanisms.\n  - Improved error notifications.\n\n- **API Enhancements**\n  - Added endpoints for managing custom models and updating model packs.\n\n- **Execution**\n  - Increased robustness in plan execution and subprocess lifecycle management.\n\n- **Observability**\n  - Real-time internal notifications for critical errors implemented.\n\n- **Consistency**\n  - Improved token management.\n  - Enhanced summarization accuracy."
  },
  {
    "path": "releases/server/versions/2.0.5.md",
    "content": "- Fix for a bug that was causing occasional model errors. Model calls should be much more reliable now.\n- Better error handling and error messages for model errors (rate limits or other errors).\n- No error retries for rate limit errors.\n- Fixed bug that caused retries to add the prompt to the conversation multiple times.\n- Error responses with no output no longer create a log entry."
  },
  {
    "path": "releases/server/versions/2.0.6.md",
    "content": "- Improvements to process management and cleanup for command execution\n- Remove extraneous model request logging"
  },
  {
    "path": "releases/server/versions/2.1.0+1.md",
    "content": "- Fix for context length exceeded error that still wasn't being caught and retried by the fallback correctly."
  },
  {
    "path": "releases/server/versions/2.1.0.md",
    "content": "See CLI 2.1.0 release notes."
  },
  {
    "path": "releases/server/versions/2.1.1+1.md",
    "content": "- Improve error handling to catch yet another \"context length exceeded\" error message variation from Anthropic."
  },
  {
    "path": "releases/server/versions/2.1.1.md",
    "content": "See CLI 2.1.1 release notes."
  },
  {
    "path": "releases/server/versions/2.1.2.md",
    "content": "- Fix for auto-load context error: 'Error decoding response → EOF'"
  },
  {
    "path": "releases/server/versions/2.1.3.md",
    "content": "- Fix for 'panic in execTellPlan' error when using a model pack that doesn't explicitly set the 'coder' or 'whole-file-builder' roles"
  },
  {
    "path": "releases/server/versions/2.1.4.md",
    "content": "- Fix to remove occasional extraneous blank lines from start/end of edited files."
  },
  {
    "path": "releases/server/versions/2.1.5.md",
    "content": "See CLI 2.1.5 release notes."
  },
  {
    "path": "releases/server/versions/2.1.6+1.md",
    "content": "See CLI 2.1.6+1 release notes."
  },
  {
    "path": "releases/server/versions/2.1.6.md",
    "content": "See CLI 2.1.6 release notes."
  },
  {
    "path": "releases/server/versions/2.1.7.md",
    "content": "- Fix for \"conversation summary timestamp not found in conversation\" error (https://github.com/plandex-ai/plandex/issues/274)\n- Fix for potential panic/crash during plan stream (https://github.com/plandex-ai/plandex/issues/275)\n- Better protection against panics/crashes in server goroutines across the board"
  },
  {
    "path": "releases/server/versions/2.1.8.md",
    "content": "- Fix for potential hang in file map queue"
  },
  {
    "path": "releases/server/versions/2.2.0.md",
    "content": "See CLI 2.2.0 release notes.\n"
  },
  {
    "path": "releases/server/versions/2.2.1.md",
    "content": "See CLI 2.2.1 release notes."
  },
  {
    "path": "scripts/merge_from_reflog.sh",
    "content": "#!/bin/bash\n\n# Check if the correct number of arguments are provided\nif [ \"$#\" -ne 2 ]; then\n    echo \"Usage: $0 <commit-hash> <branch-name>\"\n    exit 1\nfi\n\n# Get the commit hash and branch name from the arguments\ncommit_hash=$1\nbranch_name=$2\n\n# Create and checkout a new branch\necho \"Creating and checking out new branch: $branch_name\"\ngit checkout -b \"$branch_name\"\n\n# Generate the patch from the specified commit to HEAD\necho \"Generating patch from commit $commit_hash to HEAD\"\ngit format-patch -1 \"$commit_hash\" --stdout > changes.patch\n\n# Apply the patch with the three-way merge option\necho \"Applying patch...\"\nif git am --3way < changes.patch; then\n    echo \"Patch applied successfully\"\nelse\n    echo \"Merge conflicts detected. Please resolve them manually.\"\n    git status\n    echo \"After resolving conflicts, run the following commands to continue:\"\n    echo \"  git add .\"\n    echo \"  git am --continue\"\nfi\n\n# Find all .rej files and process them\necho \"Processing rejected patches...\"\nfind . -name \"*.rej\" | while read -r rej_file; do\n    # Extract the original file path\n    original_file=\"${rej_file%.rej}\"\n\n    # Check if the original file exists\n    if [ ! -f \"$original_file\" ]; then\n        # If the original file does not exist, create it\n        echo \"Creating new file: $original_file\"\n        touch \"$original_file\"\n    fi\n\n    # Append the content of the .rej file to the original file\n    echo \"Applying rejected patch to $original_file\"\n    cat \"$rej_file\" >> \"$original_file\"\n\n    # Remove the .rej file after applying the patch\n    rm \"$rej_file\"\ndone\n\n# Stage all changes\necho \"Staging all changes...\"\ngit add .\n\n# Check for conflicts again, to ensure the manual changes are staged correctly\nconflicts=$(git ls-files -u | wc -l)\nif [ \"$conflicts\" -gt 0 ]; then\n    echo \"Merge conflicts detected. Please resolve them manually.\"\n    git status\n    echo \"After resolving conflicts, run the following commands to commit the changes:\"\n    echo \"  git add .\"\n    echo \"  git commit -m 'Resolved merge conflicts from reflog commit $commit_hash'\"\nelse\n    # Commit the changes\n    echo \"Committing changes...\"\n    git commit -m \"Merged changes from reflog commit $commit_hash, applied rejected patches and added missing files\"\nfi\n\n# Clean up\necho \"Cleaning up...\"\nrm changes.patch\n\necho \"Done!\"\n"
  },
  {
    "path": "test/_test_apply.sh",
    "content": "#! /bin/bash\n\nset -euo pipefail\n\necho \"Opening application in browser...\"\nplandex-dev browser \"http://localhost:8765/error-test.html\" || {\n  echo \"Browser command failed. Exiting.\"\n  exit 1\n}\n\n# Check if python3 is available\n# if command -v python3 &> /dev/null; then\n#     echo \"Starting HTTP server with Python 3...\"\n#     # Start a simple HTTP server on port 8765\n#     python3 -m http.server 8765 &\n#     SERVER_PID=$!\n    \n#     # Give the server a moment to start\n#     sleep 1\n    \n#     # Open the browser to the application\n#     echo \"Opening application in browser...\"\n#     plandex-dev browser \"http://localhost:8765/error-test.html\"\n#     echo \"plandex-dev browser exit code: $?\"\n    \n#     # If browser command fails, kill the server and exit\n#     if [ $? -ne 0 ]; then\n#         echo \"Browser command failed. Killing server...\"\n#         kill $SERVER_PID\n#         exit 1\n#     fi\n    \n#     # Wait for the server process\n#     wait $SERVER_PID\n    \n# # Check if python is available (for Python 2)\n# elif command -v python &> /dev/null; then\n#     echo \"Starting HTTP server with Python 2...\"\n#     # Start a simple HTTP server on port 8765\n#     python -m SimpleHTTPServer 8765 &\n#     SERVER_PID=$!\n    \n#     # Give the server a moment to start\n#     sleep 1\n    \n#     # Open the browser to the application\n#     echo \"Opening application in browser...\"\n#     plandex-dev browser \"http://localhost:8765\"\n#     echo \"plandex-dev browser exit code: $?\"\n    \n#     # If browser command fails, kill the server and exit\n#     if [ $? -ne 0 ]; then\n#         echo \"Browser command failed. Killing server...\"\n#         kill $SERVER_PID\n#         exit 1\n#     fi\n    \n#     # Wait for the server process\n#     wait $SERVER_PID\n    \n# else\n#     echo \"Error: Python is not installed. Please install Python to run the HTTP server.\"\n#     exit 1\n# fi"
  },
  {
    "path": "test/error-test.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <title>Test JS Error</title>\n</head>\n\n<body>\n  <h1>This page intentionally throws a JS error</h1>\n  <script>\n    // This will throw an unhandled error\n    nonExistentFunction();\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "test/evals/promptfoo-poc/README.md",
    "content": "# Test Driven Development of Prompts\n\nThis directory is dedicated to the systematic development of prompts for the plandex project. The prompts are developed in a test-driven manner, where the prompt is first written in a markdown file, and then the prompt is tested by running the prompt through the various evaluations. The output of the prompt is then graded and A/B tested for various metrics (see [metrics](#metrics)). The prompt is then iteratively improved until it meets the desired metrics.\n\nWe have decided to write the majority of the evals using the [promptfoo]() framework, as it is robust and contains customizations with a clear ease of setup.\n\n## Usage\n\nUsage will be broken down into [run](#run-evals) and [create](#create-evals) sections:\n\n### Setup\n\nTo run or create evals, you will need to have the following installed:\n\n- [Go](https://golang.org/)\n- [Promptfoo]()\n\n### Run Evals\n\nTo run the evaluations, you can cd into the relevant directory and use the following command:\n\n```bash\nmake eval <name_of_eval_dir>\n```\n\nOr, you can run all the evaluations by running the following command:\n\n```bash\nmake evals all\n```\n\n### Create Evals\n\nTo create the evaluations, you can use our `gen-*` commands. The `gen-*` commands are designed to setup the eval environment and will create the evaluations directory structure in the `evals/promptfoo` directory. You have access to the following commands:\n\n```bash\nmake gen-eval # Generates the evaluation directory structure\nmake gen-provider # Generates a provider file based on the directory config files\n```\n\n> ![IMPORTANT]\\\n> Make sure to run the `gen-eval` command before running the `gen-provider` command.\n> You need to have the config files filled out with your details before running the `gen-provider` command.\n> Depending on the provider you use, you will need to setup an environment variable with the provider's API key.\n\n## Metrics\n\nThe metrics we are currently tracking are:\n\n> ![NOTE]\\\n> COMING SOON\n"
  },
  {
    "path": "test/evals/promptfoo-poc/build/assets/build/changes.md",
    "content": "### Subtask 1: Parse the range of indices from the command-line arguments.\n\n```\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-shared\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n```\n\n### Subtask 2: Update the logic to handle the range of indices and mark the corresponding contexts for deletion.\n\n```\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-shared\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No current plan\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor _, arg := range args {\n\t\tindices, err := parseRange(arg)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error parsing range: %v\", err)\n\t\t}\n\n\t\tfor _, index := range indices {\n\t\t\tif index > 0 && index <= len(contexts) {\n\t\t\t\tcontext := contexts[index-1]\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i, context := range contexts {\n\t\tfor _, id := range args {\n\t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n```\n\n### Subtask 3: Update the `contextRm` function to include the new logic for handling ranges.\n\n\n```\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n```"
  },
  {
    "path": "test/evals/promptfoo-poc/build/assets/build/post_build.go",
    "content": "pdx-1: package cmd\npdx-2: \npdx-3: import (\npdx-4: \t\"fmt\"\npdx-5: \t\"path/filepath\"\npdx-6: \t\"plandex/api\"\npdx-7: \t\"plandex/auth\"\npdx-8: \t\"plandex/lib\"\npdx-9: \t\"plandex/term\"\npdx-10: \t\"strconv\"\npdx-11: \t\"strings\"\npdx-12: \npdx-13: \t\"plandex-shared\"\npdx-14: \t\"github.com/spf13/cobra\"\npdx-15: )\npdx-16: \npdx-17: func parseRange(arg string) ([]int, error) {\npdx-18: \tvar indices []int \npdx-19: \tparts := strings.Split(arg, \"-\")\npdx-20: \tif len(parts) == 2 {\npdx-21: \t\tstart, err := strconv.Atoi(parts[0])\npdx-22: \t\tif err != nil {\npdx-23: \t\t\treturn nil, err\npdx-24: \t\t}\npdx-25: \t\tend, err := strconv.Atoi(parts[1])\npdx-26: \t\tif err != nil {\npdx-27: \t\t\treturn nil, err\npdx-28: \t\t}\npdx-29: \t\tfor i := start; i <= end; i++ {\npdx-30: \t\t\tindices = append(indices, i)\npdx-31: \t\t}\npdx-32: \t} else {\npdx-33: \t\tindex, err := strconv.Atoi(arg)\npdx-34: \t\tif err != nil {\npdx-35: \t\t\treturn nil, err\npdx-36: \t\t}\npdx-37: \t\tindices = append(indices, index)\npdx-38: \t}\npdx-39: \treturn indices, nil\npdx-40: }\npdx-41: \npdx-42: func contextRm(cmd *cobra.Command, args []string) {\npdx-43: \tauth.MustResolveAuthWithOrg()\npdx-44: \tlib.MustResolveProject()\npdx-45: \npdx-46: \tif lib.CurrentPlanId == \"\" {\npdx-47: \t\tfmt.Println(\"🤷‍♂️ No current plan\")\npdx-48: \t\treturn\npdx-49: \t}\npdx-50: \npdx-51: \tterm.StartSpinner(\"\")\npdx-52: \tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\npdx-53: \npdx-54: \tif err != nil {\npdx-55: \t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\npdx-56: \t}\npdx-57: \npdx-58: \tdeleteIds := map[string]bool{}\npdx-59: \npdx-60: \tfor _, arg := range args {\npdx-61: \t\tindices, err := parseRange(arg)\npdx-62: \t\tif err != nil {\npdx-63: \t\t\tterm.OutputErrorAndExit(\"Error parsing range: %v\", err)\npdx-64: \t\t}\npdx-65: \npdx-66: \t\tfor _, index := range indices {\npdx-67: \t\t\tif index > 0 && index <= len(contexts) {\npdx-68: \t\t\t\tcontext := contexts[index-1]\npdx-69: \t\t\t\tdeleteIds[context.Id] = true\npdx-70: \t\t\t}\npdx-71: \t\t}\npdx-72: \t}\npdx-73: \npdx-74: \tfor i, context := range contexts {\npdx-75: \t\tfor _, id := range args {\npdx-76: \t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\npdx-77: \t\t\t\tdeleteIds[context.Id] = true\npdx-78: \t\t\t\tbreak\npdx-79: \t\t\t} else if context.FilePath != \"\" {\npdx-80: \t\t\t\t// Check if id is a glob pattern\npdx-81: \t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\npdx-82: \t\t\t\tif err != nil {\npdx-83: \t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\npdx-84: \t\t\t\t}\npdx-85: \t\t\t\tif matched {\npdx-86: \t\t\t\t\tdeleteIds[context.Id] = true\npdx-87: \t\t\t\t\tbreak\npdx-88: \t\t\t\t}\npdx-89: \npdx-90: \t\t\t\t// Check if id is a parent directory\npdx-91: \t\t\t\tparentDir := context.FilePath\npdx-92: \t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\npdx-93: \t\t\t\t\tif parentDir == id {\npdx-94: \t\t\t\t\t\tdeleteIds[context.Id] = true\npdx-95: \t\t\t\t\t\tbreak\npdx-96: \t\t\t\t\t}\npdx-97: \t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\npdx-98: \t\t\t\t}\npdx-99: \t\t\t}\npdx-100: \t\t}\npdx-101: \t}\npdx-102: \npdx-103: \tif len(deleteIds) > 0 {\npdx-104: \t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\npdx-105: \t\t\tIds: deleteIds,\npdx-106: \t\t})\npdx-107: \t\tterm.StopSpinner()\npdx-108: \npdx-109: \t\tif err != nil {\npdx-110: \t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\npdx-111: \t\t}\npdx-112: \npdx-113: \t\tfmt.Println(\"✅ \" + res.Msg)\npdx-114: \t} else {\npdx-115: \t\tterm.StopSpinner()\npdx-116: \t\tfmt.Println(\"🤷‍♂️ No context removed\")\npdx-117: \t}\npdx-118: }\npdx-119: \npdx-120: func init() {\npdx-121: \tRootCmd.AddCommand(contextRmCmd)\npdx-122: }\npdx-123: "
  },
  {
    "path": "test/evals/promptfoo-poc/build/assets/shared/pre_build.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\n\t\"plandex-shared\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar contextRmCmd = &cobra.Command{\n\tUse:     \"rm\",\n\tAliases: []string{\"remove\", \"unload\"},\n\tShort:   \"Remove context\",\n\tLong:    `Remove context by index, name, or glob.`,\n\tArgs:    cobra.MinimumNArgs(1),\n\tRun:     contextRm,\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No current plan\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor i, context := range contexts {\n\t\tfor _, id := range args {\n\t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/build/build.config.properties",
    "content": "provider_id=openai:gpt-4o\ntemperature=0.1\nmax_tokens=4096\ntop_p=0.1\nresponse_format=json_object\nfunction_name=listChangesWithLineNums\ntool_type=function\nfunction_param_type=object\ntool_choice_type=function\ntool_choice_function_name=listChangesWithLineNums\nnested_parameters_json=build.parameters.json\n"
  },
  {
    "path": "test/evals/promptfoo-poc/build/build.parameters.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"comments\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"txt\": {\n            \"type\": \"string\"\n          },\n          \"reference\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\"txt\", \"reference\"]\n      }\n    },\n    \"filePath\": {\n      \"type\": \"string\"\n    },\n    \"changes\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"summary\": {\n            \"type\": \"string\"\n          },\n          \"hasChange\": {\n            \"type\": \"boolean\"\n          },\n          \"old\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"entireFile\": {\n                \"type\": \"boolean\"\n              },\n              \"startLineString\": {\n                \"type\": \"string\"\n              },\n              \"endLineString\": {\n                \"type\": \"string\"\n              }\n            },\n            \"required\": [\"startLineString\", \"endLineString\"]\n          },\n          \"startLineIncludedReasoning\": {\n            \"type\": \"string\"\n          },\n          \"startLineIncluded\": {\n            \"type\": \"boolean\"\n          },\n          \"endLineIncludedReasoning\": {\n            \"type\": \"string\"\n          },\n          \"endLineIncluded\": {\n            \"type\": \"boolean\"\n          },\n          \"new\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"summary\",\n          \"hasChange\",\n          \"old\",\n          \"startLineIncludedReasoning\",\n          \"startLineIncluded\",\n          \"endLineIncludedReasoning\",\n          \"endLineIncluded\",\n          \"new\"\n        ]\n      }\n    }\n  },\n  \"required\": [\"comments\", \"filePath\", \"changes\"]\n}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/build/build.prompt.txt",
    "content": "You are an AI that analyzes a code file and an AI-generated plan to update the code file and produces a list of changes.\n\n[YOUR INSTRUCTIONS]\n\nCall the 'listChangesWithLineNums' function with a valid JSON object that includes the 'comments', 'problems', and 'changes' keys.\n\nYou ABSOLUTELY MUST NOT generate overlapping changes. Group smaller changes together into larger changes where necessary to avoid overlap. Only generate multiple changes when you are ABSOLUTELY CERTAIN that they do not overlap--otherwise group them together into a single change. If changes are close to each other (within several lines), group them together into a single change. You MUST group changes together and make fewer, larger changes rather than many small changes, unless the changes are completely independent of each other and not close to each other in the file. You MUST NEVER generate changes that are adjacent or close to adjacent. Adjacent or closely adjacent changes MUST ALWAYS be grouped into a single larger change.\n\nFurthermore, unless doing so would require a very large change because some changes are far apart in the file, it's ideal to call the 'listChangesWithLineNums' with just a SINGLE change.\n\nChanges must be ordered in the array according to the order they appear in the file. The 'startLineString' of each 'old' property must come after the 'endLineString' of the previous 'old' property. Changes MUST NOT overlap. If a change is dependent on another change or intersects with it, group those changes together into a single change.\n\nYou MUST NOT repeat changes to the same block of lines multiple teams. You MUST NOT duplicate changes. It is extremely important that a given change is only applied *once*.\n\nThe 'comments' key is an array of objects with two properties: 'txt' and 'reference'. 'txt' is the exact text of a code comment. 'reference' is a boolean that indicates whether the comment is a placeholder of or reference to the original code, like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function\" or \"// rest of your function...\" or \"// Existing methods...\" or \"// Remaining methods\" or \"// Existing code...\" or \"// ... existing setup code ...\" or \"// ... existing code ...\" or \"// ...\" or other comments which reference code from the original file. References DO NOT need to exactly match any of the previous examples. Use your judgement to determine whether each comment is a reference. If 'reference' is true, the comment is a placeholder or reference to the original code. If 'reference' is false, the comment is not a placeholder or reference to the original code.\n\nIn 'comments', you must list EVERY comment included in the proposed updates. Only list *code comments* that are valid comments for the programming language being used. Do not list logging statements or any other non-comment text that is not a valid code comment. If there are no code comments in the proposed updates, 'comments' must be an empty array.\n\nIf there are multiple identical comments in the proposed updates, you MUST list them *all* in the 'comments' array--list each identical comment as a separate object in the array.\n\nIn the 'problems' key, you MUST explain how you will strategically generate changes in order to avoid any problems in the updated file. You should explain which changes you will make and how you will *avoid* making any overlapping or invalid changes. Consider whether any changes are close together or whether any change is potentially contained by another. If so, group those changes together into a single change.\n\nYou must consider whether you will apply partial changes or replace the entire file. If the original file is long, you MUST NOT replace the entire file with a single change. Instead, you should apply changes to specific sections of the file. If the original file is short and the changes are complex, you may consider replacing the entire file with a single change.\n\nYou must consider how you will avoid *incorrectly removing or overwriting code* from the original file. Explain whether any code from the original file needs to be merged with the proposed updates in order to avoid removing or overwriting code that should not be removed. It is ABSOLUTELY CRITICAL that no pre-existing code or functionality is removed or overwritten unless the plan explicitly intends for it to be removed or overwritten. New code and functionality introduced in the proposed updates MUST be *merged* with existing code and functionality in the original file. Explain how you will achieve this. \n\nYou must consider how you will avoid including any references in the updated file if any are present in the proposed updates. \n\nYou must consider how you will *avoid incorrect duplication* in making your changes. For example if a 'main' function is present in the original file and the proposed updates include update code for the 'main' function, you must ensure the changes are applied within the existing 'main' function rather than incorrectly adding a duplicate 'main' function.\n\nIf the proposed updates include large sections that are identical to the original file, consider whether the changes can be made more minimal in order to only replace sections of code that are *changing*. If you are making the changes more minimal and specific, explain how you will do this without generating any overlapping changes or introducing any new problems.\n\n'changes': An array of NON-OVERLAPPING changes. Each change is an object with properties: 'summary', 'hasChange', 'old', 'startLineIncludedReasoning', 'startLineIncluded', 'endLineIncludedReasoning', 'endLineIncluded', and 'new'.\n\nNote: all line numbers that are used below are prefixed with 'pdx-', like this 'pdx-5: for i := 0; i < 10; i++ {'. This is to help you identify the line numbers in the file. You *must* include the 'pdx-' prefix in the line numbers in the 'old' property.\n\nThe 'summary' property is a brief summary of the change. At the end of the summary, consider if this change will overlap with any ensuing changes. If it will, include those changes in *this* change instead. Continue the summary and includes those ensuing changes that would otherwise overlap. Changes that remove code are especially likely to overlap with ensuing changes. \n\n'summary' examples: \n\t- 'Update loop that aggregates the results to iterate 10 times instead of 5 and log the value of someVar.'\n\t- 'Update the Org model to include StripeCustomerId and StripeSubscriptionId fields.'\n\t- 'Add function ExecQuery to execute a query.'\n\t\n'summary' that is larger to avoid overlap:\n\t- 'Insert function ExecQuery after GetResults function in loop body. Update loop that aggregates the results to iterate 10 times instead of 5 and log the value of someVar. Add function ExecQuery to execute a query.'\n\nThe 'hasChange' property is a boolean that indicates whether there is anything to change. If there is nothing to change, set 'hasChange' to false. If there is something to change, set 'hasChange' to true.\n\nThe 'old' property is an object with 3 properties: 'entireFile', 'startLineString' and 'endLineString'.\n\n\t'entireFile' is a boolean that indicates whether the **entire file** is being replaced. If 'entireFile' is true, 'startLineString' and 'endLineString' must be empty strings. If 'entireFile' is false, 'startLineString' and 'endLineString' must be valid strings that exactly match lines from the original file. If 'entireFile' is false, 'startLineString' and 'endLineString' MUST NEVER be empty strings.\n\n\t'startLineString' is the **entire, exact line** where the section to be replaced begins in the original file, including the line number. Unless it's the first change, 'startLineString' ABSOLUTELY MUST begin with a line number that is HIGHER than both the 'endLineString' of the previous change and the 'startLineString' of the previous change. **The line number and line MUST EXACTLY MATCH a line from the original file.**\n\t\n\tIf the previous change's 'endLineString' starts with 'pdx-75: ', then the current change's 'startLineString' MUST start with 'pdx-76: ' or higher. It MUST NOT be 'pdx-75: ' or lower. If the previous change's 'startLineString' starts with 'pdx-88: ' and the previous change's 'endLineString' is an empty string, then the current change's 'startLineString' MUST start with 'pdx-89: ' or higher. If the previous change's 'startLineString' starts with 'pdx-100: ' and the previous change's 'endLineString' starts with 'pdx-105: ', then the current change's 'startLineString' MUST start with 'pdx-106: ' or higher.\n\t\n\t'endLineString' is the **entire, exact line** where the section to be replaced ends in the original file. Pay careful attention to spaces and indentation. 'startLineString' and 'endLineString' must be *entire lines* and *not partial lines*. Even if a line is very long, you must include the entire line, including the line number and all text on the line. **The line number and line MUST EXACTLY MATCH a line from the original file.**\n\t\n\t**For a single line replacement, 'endLineString' MUST be an empty string.**\n\n\t'endLineString' MUST ALWAYS come *after* 'startLineString' in the original file. It must start with a line number that is HIGHER than the 'startLineString' line number. If 'startLineString' starts with 'pdx-22: ', then 'endLineString' MUST either be an empty string (for a single line replacement) or start with 'pdx-23: ' or higher (for a multi-line replacement).\t\n\n\tIf 'hasChange' is false, both 'startLineString' and 'endLineString' must be empty strings. If 'hasChange' is true, 'startLineString' and 'endLineString' must be valid strings that exactly match lines from the original file. If 'hasChange' is true, 'startLineString' and 'endLineString' MUST NEVER be empty strings.\n\n\tIf you are replacing the entire file, 'startLineString' MUST be the first line of the original file and 'endLineString' MUST be the last line of the original file.\n\nThe 'startLineIncludedReasoning' property is a string that very briefly explains whether 'startLineString' should be included in the 'new' property. For example, if the 'startLineString' is the closing bracket of a function and you are adding another function after it, you *MUST* include the 'startLineString' in the 'new' property, or the previous function will lose its closing bracket when the change is applied. Similarly, if the 'startLineString' is a function definition and you are updating the body of the function, you *MUST* also include 'startLineString' so that they function definition is not removed. The only time 'startLineString' should not be included in 'new' is if it is a line that should be removed or replaced. Generalize the above to all types of code blocks, changes, and syntax to ensure the 'new' property will not remove or overwrite code that should not be removed or overwritten. That also includes newlines, line breaks, and indentation.\n\n'startLineIncluded' is a boolean that indicates whether 'startLineString' should be included in the 'new' property. If 'startLineIncluded' is true, 'startLineString' MUST be included in the 'new' property. If 'startLineIncluded' is false, 'startLineString' MUST not be included in the 'new' property.\n\nThe 'endLineIncludedReasoning' property is a string that very briefly explains whether 'endLineString' should be included in the 'new' property. For example, if the 'endLineString' is the opening bracket of a function and you are adding another function before it, you *MUST* include the 'endLineString' in the 'new' property, or the subsequent function will lose its opening bracket when the change is applied. Similarly, if the 'endLineString' is the closing bracket of a function and you are updating the body of the function, you *MUST* also include 'endLineString' so that the closing bracket not removed. The only time 'endLineString' should not be included in 'new' is if it is a line that should be removed or replaced. Generalize the above to all types of code blocks, changes, and syntax to ensure the 'new' property will not remove or overwrite code that should not be removed or overwritten. That also includes newlines, line breaks, and indentation.\n\n'endLineIncluded' is a boolean that indicates whether 'endLineString' should be included in the 'new' property. If 'endLineIncluded' is true, 'endLineString' MUST be included in the 'new' property. If 'endLineIncluded' is false, 'endLineString' MUST not be included in the 'new' property.\n\nThe 'new' property is a string that represents the new code that will replace the old code. The new code must be valid and consistent with the intention of the plan. If the proposed update is to remove code, the 'new' property should be an empty string. Be precise about newlines, line breaks, and indentation. 'new' must include only full lines of code and *no partial lines*. Do NOT include line numbers in the 'new' property.\n\nIf the proposed update includes references to the original code in comments like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function...\" or \"// rest of your function...\" or **any other reference to the original code,** you *MUST* ensure that the comment making the reference is *NOT* included in the 'new' property. Instead, include the **exact code** from the original file that the comment is referencing. Do not be overly strict in identifying references. If there is a comment that seems like it could plausibly be a reference and there is code in the original file that could plausibly be the code being referenced, then treat that as a reference and handle it accordingly by including the code from the original file in the 'new' property instead of the comment. YOU MUST NOT MISS ANY REFERENCES.\n\nIf the 'startLineIncluded' property is true, the 'startLineString' MUST be the first line of 'new'. If the 'startLineIncluded' property is false, the 'startLineString' MUST NOT be included in 'new'. If the 'endLineIncluded' property is true, the 'endLineString' MUST be the last line of 'new'. If the 'endLineIncluded' property is false, the 'endLineString' MUST NOT be included in 'new'.\n\nIf the 'hasChange' property is false, the 'new' property must be an empty string. If the 'hasChange' property is true, the 'new' property must be a valid string.\n\nIf *any* change has the 'entireFile' key in the 'old' property set to true, the corresponding 'new' key MUST be the entire updated file, and there MUST only be a single change in the 'changes' array.\n\nExample change object:\n\n```json\n  {\n    summary: \"Fix syntax error in loop body.\",\n   \told: {\n      startLineString: \"pdx-5: for i := 0; i < 10; i++ { \",\n      endLineString: \"pdx-7: }\"\n    },\n    new: \"for i := 0; i < 10; i++ {\\n  execQuery()\\n  }\\n  }\\n}\"\n  }\n```\n\n\nApply 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.\n\nChanges MUST be ordered based on their position in the original file. ALWAYS go from top to bottom IN ORDER when generating replacements. DO NOT EVER GENERATE AN OVERLAPPING CHANGE. If a change would fall within OR overlap a prior change in the list, SKIP that change and move on to the next one.\n\nYou 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. \n\nPay *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.\n\nThe 'listChangesWithLineNums' function MUST be called *valid JSON*. Double quotes within json properties of the 'listChangesWithLineNums' function call parameters JSON object *must be properly escaped* with a backslash. Pay careful attention to newlines, tabs, and other special characters. The JSON object must be properly formatted and must include all required keys. **You generate perfect JSON -every- time**, no matter how many quotes or special characters are in the input. You must always call 'listChangesWithLineNums' with a valid JSON object. Don't call any other function.\n\n[END YOUR INSTRUCTIONS]\n\n\n**The current file is {{filePath}} Original state of the file:**\n\n```\n{{preBuildState}}\n```\n\nProposed updates:\n\n{{changes}}\n\n\nNow call the 'listChangesWithLineNums' function with a valid JSON array of changes according to your instructions. You must always call 'listChangesWithLineNums' with one or more valid changes. Don't call any other function."
  },
  {
    "path": "test/evals/promptfoo-poc/build/build.provider.yml",
    "content": "id: openai:gpt-4o\nconfig:\n  temperature: 0.1\n  max_tokens: 4096\n  response_format: { type: json_object }\n  top_p: 0.1\n  tools:\n    [\n      {\n        \"type\": \"function\",\n        \"function\":\n          {\n            \"name\": \"listChangesWithLineNums\",\n            \"parameters\":\n              {\n                \"properties\":\n                  {\n                    \"changes\":\n                      {\n                        \"items\":\n                          {\n                            \"properties\":\n                              {\n                                \"endLineIncluded\": { \"type\": \"boolean\" },\n                                \"endLineIncludedReasoning\":\n                                  { \"type\": \"string\" },\n                                \"hasChange\": { \"type\": \"boolean\" },\n                                \"new\": { \"type\": \"string\" },\n                                \"old\":\n                                  {\n                                    \"properties\":\n                                      {\n                                        \"endLineString\": { \"type\": \"string\" },\n                                        \"entireFile\": { \"type\": \"boolean\" },\n                                        \"startLineString\": { \"type\": \"string\" },\n                                      },\n                                    \"required\":\n                                      [\"startLineString\", \"endLineString\"],\n                                    \"type\": \"object\",\n                                  },\n                                \"startLineIncluded\": { \"type\": \"boolean\" },\n                                \"startLineIncludedReasoning\":\n                                  { \"type\": \"string\" },\n                                \"summary\": { \"type\": \"string\" },\n                              },\n                            \"required\":\n                              [\n                                \"summary\",\n                                \"hasChange\",\n                                \"old\",\n                                \"startLineIncludedReasoning\",\n                                \"startLineIncluded\",\n                                \"endLineIncludedReasoning\",\n                                \"endLineIncluded\",\n                                \"new\",\n                              ],\n                            \"type\": \"object\",\n                          },\n                        \"type\": \"array\",\n                      },\n                    \"comments\":\n                      {\n                        \"items\":\n                          {\n                            \"properties\":\n                              {\n                                \"reference\": { \"type\": \"boolean\" },\n                                \"txt\": { \"type\": \"string\" },\n                              },\n                            \"required\": [\"txt\", \"reference\"],\n                            \"type\": \"object\",\n                          },\n                        \"type\": \"array\",\n                      },\n                    \"filePath\": { \"type\": \"string\" },\n                  },\n                \"required\": [\"comments\", \"filePath\", \"changes\"],\n                \"type\": \"object\",\n              },\n          },\n      },\n    ]\n  tool_choice:\n    type: \"function\"\n    function:\n      name: \"listChangesWithLineNums\"\n"
  },
  {
    "path": "test/evals/promptfoo-poc/build/promptfooconfig.yaml",
    "content": "description: \"build\"\n\nprompts:\n  - file://build.prompt.txt\nproviders:\n  - file://build.provider.yml\ntests: tests/*.test.yml\n"
  },
  {
    "path": "test/evals/promptfoo-poc/build/tests/build.test.yml",
    "content": "- description: \"Check Build with Line numbers\"\n  vars:\n    preBuildState: file://assets/shared/pre_build.go\n    changes: file://assets/build/changes.md\n    filePath: parse.go\n    postBuildState: file://assets/build/post_build.go\n  assert:\n    - type: is-json\n    - type: is-valid-openai-tools-call\n    - type: javascript\n      value: |\n        var args = JSON.parse(output[0].function.arguments)\n        return ( \n          args.changes.length > 0 &&\n          args.changes.some(\n            change => change.hasChange && \n                      change.new.includes(\"var contextRmCmd = &cobra.Command{\")\n          )\n        )\n"
  },
  {
    "path": "test/evals/promptfoo-poc/evals.md",
    "content": "# Evals\n\nEvals for plandex.\n\n## Overview\n\n`Classification:`\n\n- MCC\n- Specificity\n- Sensitivity\n- Accuracy\n\n`Regression:`\n\n- RMSE\n- R2\n- MSE\n\n## Types of Evals\n\n---\n\n### Build Prompts Evaluations\n\n1. **Syntax Check**\n   - Ensure the prompt is syntactically correct and follows the required format.\n2. **Completeness Check**\n   - Verify that all necessary components (e.g., headers, body, footers) are included.\n3. **Clarity and Precision**\n   - Evaluate if the instructions are clear and unambiguous.\n4. **Context Appropriateness**\n   - Assess if the prompt is appropriate for the intended context and audience.\n5. **Error Handling**\n   - Check if there are adequate instructions for handling potential errors.\n6. **Dependency Evaluation**\n   - Ensure all dependencies (libraries, tools) are correctly specified.\n7. **Output Validation**\n   - Define criteria to validate the expected output.\n\n### Verify Prompts Evaluations\n\n1. **Accuracy Check**\n   - Ensure the prompt's instructions lead to the correct and intended results.\n2. **Validation Criteria**\n   - Define and check the validation criteria for the outputs.\n3. **Consistency Check**\n   - Verify if the prompt maintains consistency in terminology and steps.\n4. **Logic and Flow**\n   - Evaluate the logical flow of the instructions to ensure they are coherent.\n5. **Edge Cases Handling**\n   - Assess if the prompt considers and handles edge cases effectively.\n6. **User Feedback Integration**\n   - Ensure there are provisions for incorporating user feedback.\n7. **Performance Metrics**\n   - Define and evaluate the performance metrics for the verification process.\n\n### Fix Prompts Evaluations\n\n1. **Error Identification**\n   - Ensure the prompt accurately identifies the errors to be fixed.\n2. **Correctness of Fix**\n   - Verify that the proposed fix is correct and resolves the issue.\n3. **Impact Analysis**\n   - Assess the impact of the fix on the overall system or application.\n4. **Regression Testing**\n   - Ensure that the fix does not introduce new issues or regressions.\n5. **Documentation Update**\n   - Check if the documentation is updated to reflect the fix.\n6. **Code Quality**\n   - Evaluate the quality of the code after the fix (e.g., readability, maintainability).\n7. **Performance Impact**\n   - Assess if the fix affects the performance and ensure it remains optimal.\n\n### Function Call Schemas Evaluations\n\n1. **Schema Validity**\n   - Ensure the schema is valid and conforms to the defined standards.\n2. **Parameter Consistency**\n   - Verify that the parameters are consistently defined and used.\n3. **Return Type Verification**\n   - Ensure the return types are correctly specified and handled.\n4. **Error Handling Mechanism**\n   - Assess the error handling mechanisms in the schema.\n5. **Compatibility Check**\n   - Check if the schema is compatible with different environments or systems.\n6. **Documentation Completeness**\n   - Ensure the schema is well-documented with clear explanations of each parameter and return type.\n7. **Security Considerations**\n   - Evaluate the schema for potential security vulnerabilities or risks.\n"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/assets/removal/changes.md",
    "content": "### Subtask 1: Parse the range of indices from the command-line arguments.\n\n```\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-shared\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n```\n\n### Subtask 2: Update the logic to handle the range of indices and mark the corresponding contexts for deletion.\n\n```\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-shared\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No current plan\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor _, arg := range args {\n\t\tindices, err := parseRange(arg)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error parsing range: %v\", err)\n\t\t}\n\n\t\tfor _, index := range indices {\n\t\t\tif index > 0 && index <= len(contexts) {\n\t\t\t\tcontext := contexts[index-1]\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i, context := range contexts {\n\t\tfor _, id := range args {\n\t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n```\n\n### Subtask 3: Update the `contextRm` function to include the new logic for handling ranges.\n\n\n```\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n```"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/assets/removal/post_build.go",
    "content": "pdx-1: package cmd\npdx-2: \npdx-3: import (\npdx-4: \t\"fmt\"\npdx-5: \t\"path/filepath\"\npdx-6: \t\"plandex/api\"\npdx-7: \t\"plandex/auth\"\npdx-8: \t\"plandex/lib\"\npdx-9: \t\"plandex/term\"\npdx-10: \t\"strconv\"\npdx-11: \t\"strings\"\npdx-12: \npdx-13: \t\"plandex-shared\"\npdx-14: \t\"github.com/spf13/cobra\"\npdx-15: )\npdx-16: \npdx-17: func parseRange(arg string) ([]int, error) {\npdx-18: \tvar indices []int \npdx-19: \tparts := strings.Split(arg, \"-\")\npdx-20: \tif len(parts) == 2 {\npdx-21: \t\tstart, err := strconv.Atoi(parts[0])\npdx-22: \t\tif err != nil {\npdx-23: \t\t\treturn nil, err\npdx-24: \t\t}\npdx-25: \t\tend, err := strconv.Atoi(parts[1])\npdx-26: \t\tif err != nil {\npdx-27: \t\t\treturn nil, err\npdx-28: \t\t}\npdx-29: \t\tfor i := start; i <= end; i++ {\npdx-30: \t\t\tindices = append(indices, i)\npdx-31: \t\t}\npdx-32: \t} else {\npdx-33: \t\tindex, err := strconv.Atoi(arg)\npdx-34: \t\tif err != nil {\npdx-35: \t\t\treturn nil, err\npdx-36: \t\t}\npdx-37: \t\tindices = append(indices, index)\npdx-38: \t}\npdx-39: \treturn indices, nil\npdx-40: }\npdx-41: \npdx-42: func contextRm(cmd *cobra.Command, args []string) {\npdx-43: \tauth.MustResolveAuthWithOrg()\npdx-44: \tlib.MustResolveProject()\npdx-45: \npdx-46: \tif lib.CurrentPlanId == \"\" {\npdx-47: \t\tfmt.Println(\"🤷‍♂️ No current plan\")\npdx-48: \t\treturn\npdx-49: \t}\npdx-50: \npdx-51: \tterm.StartSpinner(\"\")\npdx-52: \tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\npdx-53: \npdx-54: \tif err != nil {\npdx-55: \t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\npdx-56: \t}\npdx-57: \npdx-58: \tdeleteIds := map[string]bool{}\npdx-59: \npdx-60: \tfor _, arg := range args {\npdx-61: \t\tindices, err := parseRange(arg)\npdx-62: \t\tif err != nil {\npdx-63: \t\t\tterm.OutputErrorAndExit(\"Error parsing range: %v\", err)\npdx-64: \t\t}\npdx-65: \npdx-66: \t\tfor _, index := range indices {\npdx-67: \t\t\tif index > 0 && index <= len(contexts) {\npdx-68: \t\t\t\tcontext := contexts[index-1]\npdx-69: \t\t\t\tdeleteIds[context.Id] = true\npdx-70: \t\t\t}\npdx-71: \t\t}\npdx-72: \t}\npdx-73: \npdx-74: \tfor i, context := range contexts {\npdx-75: \t\tfor _, id := range args {\npdx-76: \t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\npdx-77: \t\t\t\tdeleteIds[context.Id] = true\npdx-78: \t\t\t\tbreak\npdx-79: \t\t\t} else if context.FilePath != \"\" {\npdx-80: \t\t\t\t// Check if id is a glob pattern\npdx-81: \t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\npdx-82: \t\t\t\tif err != nil {\npdx-83: \t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\npdx-84: \t\t\t\t}\npdx-85: \t\t\t\tif matched {\npdx-86: \t\t\t\t\tdeleteIds[context.Id] = true\npdx-87: \t\t\t\t\tbreak\npdx-88: \t\t\t\t}\npdx-89: \npdx-90: \t\t\t\t// Check if id is a parent directory\npdx-91: \t\t\t\tparentDir := context.FilePath\npdx-92: \t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\npdx-93: \t\t\t\t\tif parentDir == id {\npdx-94: \t\t\t\t\t\tdeleteIds[context.Id] = true\npdx-95: \t\t\t\t\t\tbreak\npdx-96: \t\t\t\t\t}\npdx-97: \t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\npdx-98: \t\t\t\t}\npdx-99: \t\t\t}\npdx-100: \t\t}\npdx-101: \t}\npdx-102: \npdx-103: \tif len(deleteIds) > 0 {\npdx-104: \t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\npdx-105: \t\t\tIds: deleteIds,\npdx-106: \t\t})\npdx-107: \t\tterm.StopSpinner()\npdx-108: \npdx-109: \t\tif err != nil {\npdx-110: \t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\npdx-111: \t\t}\npdx-112: \npdx-113: \t\tfmt.Println(\"✅ \" + res.Msg)\npdx-114: \t} else {\npdx-115: \t\tterm.StopSpinner()\npdx-116: \t\tfmt.Println(\"🤷‍♂️ No context removed\")\npdx-117: \t}\npdx-118: }\npdx-119: \npdx-120: func init() {\npdx-121: \tRootCmd.AddCommand(contextRmCmd)\npdx-122: }\npdx-123: "
  },
  {
    "path": "test/evals/promptfoo-poc/fix/assets/removal/problems.txt",
    "content": "The command definition for 'contextRmCmd' was incorrectly removed. The proposed updates did not specify this removal, and it breaks the functionality of the command. To correct this, the command definition should be re-added to the updated file."
  },
  {
    "path": "test/evals/promptfoo-poc/fix/assets/shared/pre_build.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar contextRmCmd = &cobra.Command{\n\tUse:     \"rm\",\n\tAliases: []string{\"remove\", \"unload\"},\n\tShort:   \"Remove context\",\n\tLong:    `Remove context by index, name, or glob.`,\n\tArgs:    cobra.MinimumNArgs(1),\n\tRun:     contextRm,\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No current plan\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor i, context := range contexts {\n\t\tfor _, id := range args {\n\t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/fix.config.properties",
    "content": "provider_id=openai:gpt-4o\ntemperature=0.1\nmax_tokens=4096\ntop_p=0.1\nresponse_format=json_object\nfunction_name=listChangesWithLineNums\ntool_type=function\nfunction_param_type=object\ntool_choice_type=function\ntool_choice_function_name=listChangesWithLineNums\nnested_parameters_json=fix.parameters.json\n"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/fix.parameters.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"comments\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"txt\": {\n            \"type\": \"string\"\n          },\n          \"reference\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\"txt\", \"reference\"]\n      }\n    },\n    \"problems\": {\n      \"type\": \"string\"\n    },\n    \"changes\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"summary\": {\n            \"type\": \"string\"\n          },\n          \"hasChange\": {\n            \"type\": \"boolean\"\n          },\n          \"old\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"entireFile\": {\n                \"type\": \"boolean\"\n              },\n              \"startLineString\": {\n                \"type\": \"string\"\n              },\n              \"endLineString\": {\n                \"type\": \"string\"\n              }\n            },\n            \"required\": [\"startLineString\", \"endLineString\"]\n          },\n          \"startLineIncludedReasoning\": {\n            \"type\": \"string\"\n          },\n          \"startLineIncluded\": {\n            \"type\": \"boolean\"\n          },\n          \"endLineIncludedReasoning\": {\n            \"type\": \"string\"\n          },\n          \"endLineIncluded\": {\n            \"type\": \"boolean\"\n          },\n          \"new\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"summary\",\n          \"hasChange\",\n          \"old\",\n          \"startLineIncludedReasoning\",\n          \"startLineIncluded\",\n          \"endLineIncludedReasoning\",\n          \"endLineIncluded\",\n          \"new\"\n        ]\n      }\n    }\n  },\n  \"required\": [\"comments\", \"problems\", \"changes\"]\n}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/fix.prompt.txt",
    "content": "You are an AI that analyzes an original file (if present), an incorrectly updated file, the changes that were proposed, and a description of the problems with the file, and then produces a list of changes to apply to the *incorrectly updated file* that will fix *ALL* the problems.\n\nProblems you MUST fix include:\n- Syntax errors, including unbalanced brackets, parentheses, braces, quotes, indentation, and other code structure errors\n- Missing or incorrectly scoped declarations\n- Any other errors that make the code invalid and would prevent it from being run as-is for the programming language being used\n- Incorrectly applied changes\n- Incorrectly removed code\n- Incorrectly overwritten code\n- Incorrectly duplicated code\n- Incorrectly applied comments that reference the original code\n\nIf the updated file includes references to the original code in comments like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function...\" or \"// rest of your function...\" or \"// Existing methods...\" **any other reference to the original code, the file is incorrect. References like these must be handled by including the exact code from the original file that the comment is referencing.\n\n[YOUR INSTRUCTIONS]\nCall the 'listChangesWithLineNums' function with a valid JSON object that includes the 'comments','problems' and 'changes' keys.\n\nThe 'comments' key is an array of objects with two properties: 'txt' and 'reference'. 'txt' is the exact text of a code comment. 'reference' is a boolean that indicates whether the comment is a placeholder of or reference to the original code, like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function\" or \"// rest of your function...\" or \"// Existing methods...\" or \"// Remaining methods\" or \"// Existing code...\" or \"// ... existing setup code ...\"\" or other comments which reference code from the original file. References DO NOT need to exactly match any of the previous examples. Use your judgement to determine whether each comment is a reference. If 'reference' is true, the comment is a placeholder or reference to the original code. If 'reference' is false, the comment is not a placeholder or reference to the original code.\n\nIn 'comments', you must list EVERY comment included in the proposed updates. Only list *code comments* that are valid comments for the programming language being used. Do not list logging statements or any other non-comment text that is not a valid code comment. If there are no code comments in the proposed updates, 'comments' must be an empty array.\n\nIf there are multiple identical comments in the proposed updates, you MUST list them *all* in the 'comments' array--list each identical comment as a separate object in the array.\n\n'problems': A string that describes all problems present within the updated file. Explain the cause of each problem and how it should be fixed. Do not just restate that there is a syntax error on a specific line. Explain what the syntax error is and how to fix it. Be exhaustive and include *every* problem that is present in the file.\n\nSince you are fixing an incorrectly updated file, you *MUST* include the 'problems' key and you *MUST* describe *all* problems present in the file. If you cannot identify any problems immediately, output a few hypotheses about what might be wrong and then explain which of them are actually present in the file. The file definitely does have problems, so you *must* identify them.\n\n'changes': An array of NON-OVERLAPPING changes. Each change is an object with properties: 'summary', 'hasChange', 'old', 'startLineIncludedReasoning', 'startLineIncluded', 'endLineIncludedReasoning', 'endLineIncluded', and 'new'.\n\nNote: all line numbers that are used below are prefixed with 'pdx-', like this 'pdx-5: for i := 0; i < 10; i++ {'. This is to help you identify the line numbers in the file. You *must* include the 'pdx-' prefix in the line numbers in the 'old' property.\n\nThe 'summary' property is a brief summary of the change. At the end of the summary, consider if this change will overlap with any ensuing changes. If it will, include those changes in *this* change instead. Continue the summary and includes those ensuing changes that would otherwise overlap. Changes that remove code are especially likely to overlap with ensuing changes. \n\n'summary' examples: \n\t- 'Update loop that aggregates the results to iterate 10 times instead of 5 and log the value of someVar.'\n\t- 'Update the Org model to include StripeCustomerId and StripeSubscriptionId fields.'\n\t- 'Add function ExecQuery to execute a query.'\n\t\n'summary' that is larger to avoid overlap:\n\t- 'Insert function ExecQuery after GetResults function in loop body. Update loop that aggregates the results to iterate 10 times instead of 5 and log the value of someVar. Add function ExecQuery to execute a query.'\n\nThe 'hasChange' property is a boolean that indicates whether there is anything to change. If there is nothing to change, set 'hasChange' to false. If there is something to change, set 'hasChange' to true.\n\nThe 'old' property is an object with 3 properties: 'entireFile', 'startLineString' and 'endLineString'.\n\n\t'entireFile' is a boolean that indicates whether the **entire file** is being replaced. If 'entireFile' is true, 'startLineString' and 'endLineString' must be empty strings. If 'entireFile' is false, 'startLineString' and 'endLineString' must be valid strings that exactly match lines from the original file. If 'entireFile' is false, 'startLineString' and 'endLineString' MUST NEVER be empty strings.\n\n\t'startLineString' is the **entire, exact line** where the section to be replaced begins in the original file, including the line number. Unless it's the first change, 'startLineString' ABSOLUTELY MUST begin with a line number that is HIGHER than both the 'endLineString' of the previous change and the 'startLineString' of the previous change. **The line number and line MUST EXACTLY MATCH a line from the original file.**\n\t\n\tIf the previous change's 'endLineString' starts with 'pdx-75: ', then the current change's 'startLineString' MUST start with 'pdx-76: ' or higher. It MUST NOT be 'pdx-75: ' or lower. If the previous change's 'startLineString' starts with 'pdx-88: ' and the previous change's 'endLineString' is an empty string, then the current change's 'startLineString' MUST start with 'pdx-89: ' or higher. If the previous change's 'startLineString' starts with 'pdx-100: ' and the previous change's 'endLineString' starts with 'pdx-105: ', then the current change's 'startLineString' MUST start with 'pdx-106: ' or higher.\n\t\n\t'endLineString' is the **entire, exact line** where the section to be replaced ends in the original file. Pay careful attention to spaces and indentation. 'startLineString' and 'endLineString' must be *entire lines* and *not partial lines*. Even if a line is very long, you must include the entire line, including the line number and all text on the line. **The line number and line MUST EXACTLY MATCH a line from the original file.**\n\t\n\t**For a single line replacement, 'endLineString' MUST be an empty string.**\n\n\t'endLineString' MUST ALWAYS come *after* 'startLineString' in the original file. It must start with a line number that is HIGHER than the 'startLineString' line number. If 'startLineString' starts with 'pdx-22: ', then 'endLineString' MUST either be an empty string (for a single line replacement) or start with 'pdx-23: ' or higher (for a multi-line replacement).\t\n\n\tIf 'hasChange' is false, both 'startLineString' and 'endLineString' must be empty strings. If 'hasChange' is true, 'startLineString' and 'endLineString' must be valid strings that exactly match lines from the original file. If 'hasChange' is true, 'startLineString' and 'endLineString' MUST NEVER be empty strings.\n\n\tIf you are replacing the entire file, 'startLineString' MUST be the first line of the original file and 'endLineString' MUST be the last line of the original file.\n\nThe 'startLineIncludedReasoning' property is a string that very briefly explains whether 'startLineString' should be included in the 'new' property. For example, if the 'startLineString' is the closing bracket of a function and you are adding another function after it, you *MUST* include the 'startLineString' in the 'new' property, or the previous function will lose its closing bracket when the change is applied. Similarly, if the 'startLineString' is a function definition and you are updating the body of the function, you *MUST* also include 'startLineString' so that they function definition is not removed. The only time 'startLineString' should not be included in 'new' is if it is a line that should be removed or replaced. Generalize the above to all types of code blocks, changes, and syntax to ensure the 'new' property will not remove or overwrite code that should not be removed or overwritten. That also includes newlines, line breaks, and indentation.\n\n'startLineIncluded' is a boolean that indicates whether 'startLineString' should be included in the 'new' property. If 'startLineIncluded' is true, 'startLineString' MUST be included in the 'new' property. If 'startLineIncluded' is false, 'startLineString' MUST not be included in the 'new' property.\n\nThe 'endLineIncludedReasoning' property is a string that very briefly explains whether 'endLineString' should be included in the 'new' property. For example, if the 'endLineString' is the opening bracket of a function and you are adding another function before it, you *MUST* include the 'endLineString' in the 'new' property, or the subsequent function will lose its opening bracket when the change is applied. Similarly, if the 'endLineString' is the closing bracket of a function and you are updating the body of the function, you *MUST* also include 'endLineString' so that the closing bracket not removed. The only time 'endLineString' should not be included in 'new' is if it is a line that should be removed or replaced. Generalize the above to all types of code blocks, changes, and syntax to ensure the 'new' property will not remove or overwrite code that should not be removed or overwritten. That also includes newlines, line breaks, and indentation.\n\n'endLineIncluded' is a boolean that indicates whether 'endLineString' should be included in the 'new' property. If 'endLineIncluded' is true, 'endLineString' MUST be included in the 'new' property. If 'endLineIncluded' is false, 'endLineString' MUST not be included in the 'new' property.\n\nThe 'new' property is a string that represents the new code that will replace the old code. The new code must be valid and consistent with the intention of the plan. If the proposed update is to remove code, the 'new' property should be an empty string. Be precise about newlines, line breaks, and indentation. 'new' must include only full lines of code and *no partial lines*. Do NOT include line numbers in the 'new' property.\n\nIf the proposed update includes references to the original code in comments like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function...\" or \"// rest of your function...\" or **any other reference to the original code,** you *MUST* ensure that the comment making the reference is *NOT* included in the 'new' property. Instead, include the **exact code** from the original file that the comment is referencing. Do not be overly strict in identifying references. If there is a comment that seems like it could plausibly be a reference and there is code in the original file that could plausibly be the code being referenced, then treat that as a reference and handle it accordingly by including the code from the original file in the 'new' property instead of the comment. YOU MUST NOT MISS ANY REFERENCES.\n\nIf the 'startLineIncluded' property is true, the 'startLineString' MUST be the first line of 'new'. If the 'startLineIncluded' property is false, the 'startLineString' MUST NOT be included in 'new'. If the 'endLineIncluded' property is true, the 'endLineString' MUST be the last line of 'new'. If the 'endLineIncluded' property is false, the 'endLineString' MUST NOT be included in 'new'.\n\nIf the 'hasChange' property is false, the 'new' property must be an empty string. If the 'hasChange' property is true, the 'new' property must be a valid string.\n\nIf *any* change has the 'entireFile' key in the 'old' property set to true, the corresponding 'new' key MUST be the entire updated file, and there MUST only be a single change in the 'changes' array.\n\nYou MUST ensure the line numbers for the 'old' property correctly remove *ALL* code that has problems and that the 'new' property correctly fixes *ALL* the problems present in the updated file. You MUST NOT miss any problems, fail to fix any problems, or introduce any new problems.\n\n\nBecause you are implementing a fix, be more willing to make larger changes in order to fix all the problems. Smaller changes are more error-prone, and the fact that you are fixing a file means a line-number based change already failed. This likely means there was some tricky structural challenge in applying the changes with line numbers, so be prepared to make a larger change in order to avoid continuing to fail to fix the file.\n\nExample change object:\n  \n```json\n{\n  \"summary\": \"Fix syntax error in loop body.\",\n \t\"old\": {\n    \"startLineString\": \"pdx-5: for i := 0; i < 10; i++ { \",\n    \"endLineString\": \"pdx-7: }\"\n  },\n  \"new\": \"for i := 0; i < 10; i++ {\\n  execQuery()\\n  }\\n  }\\n}\"\n}\n```\n\n\nYou ABSOLUTELY MUST NOT generate overlapping changes. Group smaller changes together into larger changes where necessary to avoid overlap. Only generate multiple changes when you are ABSOLUTELY CERTAIN that they do not overlap--otherwise group them together into a single change. If changes are close to each other (within several lines), group them together into a single change. You MUST group changes together and make fewer, larger changes rather than many small changes, unless the changes are completely independent of each other and not close to each other in the file. You MUST NEVER generate changes that are adjacent or close to adjacent. Adjacent or closely adjacent changes MUST ALWAYS be grouped into a single larger change.\n\nFurthermore, unless doing so would require a very large change because some changes are far apart in the file, it's ideal to call the 'listChangesWithLineNums' with just a SINGLE change.\n\nChanges must be ordered in the array according to the order they appear in the file. The 'startLineString' of each 'old' property must come after the 'endLineString' of the previous 'old' property. Changes MUST NOT overlap. If a change is dependent on another change or intersects with it, group those changes together into a single change.\n\nYou MUST NOT repeat changes to the same block of lines multiple teams. You MUST NOT duplicate changes. It is extremely important that a given change is only applied *once*.\n\nApply 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.\n\nChanges MUST be ordered based on their position in the original file. ALWAYS go from top to bottom IN ORDER when generating replacements. DO NOT EVER GENERATE AN OVERLAPPING CHANGE. If a change would fall within OR overlap a prior change in the list, SKIP that change and move on to the next one.\n\nYou 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. \n\nPay *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.\n\nThe 'listChangesWithLineNums' function MUST be called *valid JSON*. Double quotes within json properties of the 'listChangesWithLineNums' function call parameters JSON object *must be properly escaped* with a backslash. Pay careful attention to newlines, tabs, and other special characters. The JSON object must be properly formatted and must include all required keys. **You generate perfect JSON -every- time**, no matter how many quotes or special characters are in the input. You must always call 'listChangesWithLineNums' with a valid JSON object. Don't call any other function.\n\n[END YOUR INSTRUCTIONS]\n\n**Original file:**\n\n```\n{{preBuildState}}\n```\n\n**Proposed updates:**\n\n{{changes}}\n\n--\n\n**The incorrectly updated file is:**\n\n```\n{{incorrectlyUpdatedFile}}\n```\n\n**The problems with the file are:**\n\n{{problems}}\n\n--\n\nNow call the 'listChangesWithLineNums' function with a valid JSON array of changes according to your instructions. You must always call 'listChangesWithLineNums' with one or more valid changes. Don't call any other function.\n"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/fix.provider.yml",
    "content": "id: openai:gpt-4o\nconfig:\n  temperature: 0.1\n  max_tokens: 4096\n  response_format: { type: json_object }\n  top_p: 0.1\n  tools:\n    [\n      {\n        \"type\": \"function\",\n        \"function\":\n          { \"name\": \"listChangesWithLineNums\", \"parameters\": {\"properties\":{\"changes\":{\"items\":{\"properties\":{\"endLineIncluded\":{\"type\":\"boolean\"},\"endLineIncludedReasoning\":{\"type\":\"string\"},\"hasChange\":{\"type\":\"boolean\"},\"new\":{\"type\":\"string\"},\"old\":{\"properties\":{\"endLineString\":{\"type\":\"string\"},\"entireFile\":{\"type\":\"boolean\"},\"startLineString\":{\"type\":\"string\"}},\"required\":[\"startLineString\",\"endLineString\"],\"type\":\"object\"},\"startLineIncluded\":{\"type\":\"boolean\"},\"startLineIncludedReasoning\":{\"type\":\"string\"},\"summary\":{\"type\":\"string\"}},\"required\":[\"summary\",\"hasChange\",\"old\",\"startLineIncludedReasoning\",\"startLineIncluded\",\"endLineIncludedReasoning\",\"endLineIncluded\",\"new\"],\"type\":\"object\"},\"type\":\"array\"},\"comments\":{\"items\":{\"properties\":{\"reference\":{\"type\":\"boolean\"},\"txt\":{\"type\":\"string\"}},\"required\":[\"txt\",\"reference\"],\"type\":\"object\"},\"type\":\"array\"},\"problems\":{\"type\":\"string\"}},\"required\":[\"comments\",\"problems\",\"changes\"],\"type\":\"object\"} },\n      },\n    ]\n  tool_choice:\n    type: \"function\"\n    function:\n      name: \"listChangesWithLineNums\"\n"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/promptfooconfig.yaml",
    "content": "# This configuration compares LLM output of 2 prompts x 2 GPT models across 3 test cases.\n# Learn more: https://promptfoo.dev/docs/configuration/guide\ndescription: \"fix\"\n\nprompts:\n  - file://fix.prompt.txt\n\nproviders:\n  - file://fix.provider.yml\n\ntests: tests/*.test.yml\n"
  },
  {
    "path": "test/evals/promptfoo-poc/fix/tests/fix.test.yml",
    "content": "- description: \"Check Fix with Line numbers\"\n  vars:\n    preBuildState: file://assets/shared/pre_build.go\n    changes: file://assets/removal/changes.md\n    problems: file://assets/removal/problems.txt\n    postBuildState: file://assets/removal/post_build.go\n  assert:\n    - type: is-json\n    - type: is-valid-openai-tools-call\n    - type: javascript\n      value: |\n        var args = JSON.parse(output[0].function.arguments)\n        return (\n          args.problems && \n          args.changes.length > 0 &&\n          args.changes.some(\n            change => change.hasChange && \n                      change.new.includes(\"var contextRmCmd = &cobra.Command{\")\n          )\n        )\n"
  },
  {
    "path": "test/evals/promptfoo-poc/templates/provider.template.yml",
    "content": "# TODO: Add support for more dynamic creation, support for multiple tools, different API providers parameters, etc.\n\nid: {{ .provider_id }}\nconfig:\n  temperature: {{ .temperature }}\n  max_tokens: {{ .max_tokens }}\n  response_format: { type: {{ .response_format }} }\n  top_p: {{ .top_p }}\n  tools:\n    [\n      {\n        \"type\": \"{{ .tool_type }}\",\n        \"function\":\n          { \"name\": \"{{ .function_name }}\", \"parameters\": {{ .parameters }} },\n      },\n    ]\n  tool_choice:\n    type: \"{{ .tool_choice_type }}\"\n    function:\n      name: \"{{ .tool_choice_function_name }}\"\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/assets/removal/changes.md",
    "content": "### Subtask 1: Parse the range of indices from the command-line arguments.\n\n```\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-shared\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n```\n\n### Subtask 2: Update the logic to handle the range of indices and mark the corresponding contexts for deletion.\n\n```\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"plandex-shared\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No current plan\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor _, arg := range args {\n\t\tindices, err := parseRange(arg)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error parsing range: %v\", err)\n\t\t}\n\n\t\tfor _, index := range indices {\n\t\t\tif index > 0 && index <= len(contexts) {\n\t\t\t\tcontext := contexts[index-1]\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i, context := range contexts {\n\t\tfor _, id := range args {\n\t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n```\n\n### Subtask 3: Update the `contextRm` function to include the new logic for handling ranges.\n\n\n```\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n```"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/assets/removal/diff.txt",
    "content": "diff --git a/pre_build.go b/tests/removal/post_build.go\nindex c90eeb7..e9da642 100644\n--- a/pre_build.go\n+++ b/tests/removal/post_build.go\n@@ -7,18 +7,36 @@ import (\n \t\"plandex/auth\"\n \t\"plandex/lib\"\n \t\"plandex/term\"\n+\t\"strconv\"\n+\t\"strings\"\n \n \t\"plandex-shared\"\n \t\"github.com/spf13/cobra\"\n )\n \n-var contextRmCmd = &cobra.Command{\n-\tUse:     \"rm\",\n-\tAliases: []string{\"remove\", \"unload\"},\n-\tShort:   \"Remove context\",\n-\tLong:    `Remove context by index, name, or glob.`,\n-\tArgs:    cobra.MinimumNArgs(1),\n-\tRun:     contextRm,\n+func parseRange(arg string) ([]int, error) {\n+\tvar indices []int\n+\tparts := strings.Split(arg, \"-\")\n+\tif len(parts) == 2 {\n+\t\tstart, err := strconv.Atoi(parts[0])\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tend, err := strconv.Atoi(parts[1])\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tfor i := start; i <= end; i++ {\n+\t\t\tindices = append(indices, i)\n+\t\t}\n+\t} else {\n+\t\tindex, err := strconv.Atoi(arg)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tindices = append(indices, index)\n+\t}\n+\treturn indices, nil\n }\n \n func contextRm(cmd *cobra.Command, args []string) {\n@@ -39,6 +57,20 @@ func contextRm(cmd *cobra.Command, args []string) {\n \n \tdeleteIds := map[string]bool{}\n \n+\tfor _, arg := range args {\n+\t\tindices, err := parseRange(arg)\n+\t\tif err != nil {\n+\t\t\tterm.OutputErrorAndExit(\"Error parsing range: %v\", err)\n+\t\t}\n+\n+\t\tfor _, index := range indices {\n+\t\t\tif index > 0 && index <= len(contexts) {\n+\t\t\t\tcontext := contexts[index-1]\n+\t\t\t\tdeleteIds[context.Id] = true\n+\t\t\t}\n+\t\t}\n+\t}\n+\n \tfor i, context := range contexts {\n \t\tfor _, id := range args {\n \t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n@@ -64,7 +96,6 @@ func contextRm(cmd *cobra.Command, args []string) {\n \t\t\t\t\t}\n \t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n \t\t\t\t}\n-\n \t\t\t}\n \t\t}\n \t}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/assets/removal/post_build.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No current plan\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor _, arg := range args {\n\t\tindices, err := parseRange(arg)\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error parsing range: %v\", err)\n\t\t}\n\n\t\tfor _, index := range indices {\n\t\t\tif index > 0 && index <= len(contexts) {\n\t\t\t\tcontext := contexts[index-1]\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i, context := range contexts {\n\t\tfor _, id := range args {\n\t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/assets/shared/pre_build.go",
    "content": "pdx-1: package cmd\npdx-2: \npdx-3: import (\npdx-4: \t\"fmt\"\npdx-5: \t\"path/filepath\"\npdx-6: \t\"plandex/api\"\npdx-7: \t\"plandex/auth\"\npdx-8: \t\"plandex/lib\"\npdx-9: \t\"plandex/term\"\npdx-10: \npdx-11: \t\"plandex-shared\"\npdx-12: \t\"github.com/spf13/cobra\"\npdx-13: )\npdx-14: \npdx-15: var contextRmCmd = &cobra.Command{\npdx-16: \tUse:     \"rm\",\npdx-17: \tAliases: []string{\"remove\", \"unload\"},\npdx-18: \tShort:   \"Remove context\",\npdx-19: \tLong:    `Remove context by index, name, or glob.`,\npdx-20: \tArgs:    cobra.MinimumNArgs(1),\npdx-21: \tRun:     contextRm,\npdx-22: }\npdx-23: \npdx-24: func contextRm(cmd *cobra.Command, args []string) {\npdx-25: \tauth.MustResolveAuthWithOrg()\npdx-26: \tlib.MustResolveProject()\npdx-27: \npdx-28: \tif lib.CurrentPlanId == \"\" {\npdx-29: \t\tfmt.Println(\"🤷‍♂️ No current plan\")\npdx-30: \t\treturn\npdx-31: \t}\npdx-32: \npdx-33: \tterm.StartSpinner(\"\")\npdx-34: \tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\npdx-35: \npdx-36: \tif err != nil {\npdx-37: \t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\npdx-38: \t}\npdx-39: \npdx-40: \tdeleteIds := map[string]bool{}\npdx-41: \npdx-42: \tfor i, context := range contexts {\npdx-43: \t\tfor _, id := range args {\npdx-44: \t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\npdx-45: \t\t\t\tdeleteIds[context.Id] = true\npdx-46: \t\t\t\tbreak\npdx-47: \t\t\t} else if context.FilePath != \"\" {\npdx-48: \t\t\t\t// Check if id is a glob pattern\npdx-49: \t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\npdx-50: \t\t\t\tif err != nil {\npdx-51: \t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\npdx-52: \t\t\t\t}\npdx-53: \t\t\t\tif matched {\npdx-54: \t\t\t\t\tdeleteIds[context.Id] = true\npdx-55: \t\t\t\t\tbreak\npdx-56: \t\t\t\t}\npdx-57: \npdx-58: \t\t\t\t// Check if id is a parent directory\npdx-59: \t\t\t\tparentDir := context.FilePath\npdx-60: \t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\npdx-61: \t\t\t\t\tif parentDir == id {\npdx-62: \t\t\t\t\t\tdeleteIds[context.Id] = true\npdx-63: \t\t\t\t\t\tbreak\npdx-64: \t\t\t\t\t}\npdx-65: \t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\npdx-66: \t\t\t\t}\npdx-67: \npdx-68: \t\t\t}\npdx-69: \t\t}\npdx-70: \t}\npdx-71: \npdx-72: \tif len(deleteIds) > 0 {\npdx-73: \t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\npdx-74: \t\t\tIds: deleteIds,\npdx-75: \t\t})\npdx-76: \t\tterm.StopSpinner()\npdx-77: \npdx-78: \t\tif err != nil {\npdx-79: \t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\npdx-80: \t\t}\npdx-81: \npdx-82: \t\tfmt.Println(\"✅ \" + res.Msg)\npdx-83: \t} else {\npdx-84: \t\tterm.StopSpinner()\npdx-85: \t\tfmt.Println(\"🤷‍♂️ No context removed\")\npdx-86: \t}\npdx-87: }\npdx-88: \npdx-89: func init() {\npdx-90: \tRootCmd.AddCommand(contextRmCmd)\npdx-91: }\npdx-92: "
  },
  {
    "path": "test/evals/promptfoo-poc/verify/assets/valid/changes.md",
    "content": " ### Subtask 1: Parse the range of indices from the command-line arguments.  \n                                                               \n                                                        ```                      \n    package cmd                                                               \n                                                                              \n    import (                                                                  \n    \t\"fmt\"                                                                     \n    \t\"path/filepath\"                                                           \n    \t\"plandex/api\"                                                             \n    \t\"plandex/auth\"                                                            \n    \t\"plandex/lib\"                                                             \n    \t\"plandex/term\"                                                            \n    \t\"strconv\"                                                                 \n    \t\"strings\"                                                                 \n                                                                              \n    \t\"plandex-shared\"                                       \n    \t\"github.com/spf13/cobra\"                                                  \n    )                                                                         \n                                                                              \n    func parseRange(arg string) ([]int, error) {                              \n    \tvar indices []int                                                         \n    \tparts := strings.Split(arg, \"-\")                                          \n    \tif len(parts) == 2 {                                                      \n    \t\tstart, err := strconv.Atoi(parts[0])                                      \n    \t\tif err != nil {                                                           \n    \t\t\treturn nil, err                                                           \n    \t\t}                                                                         \n    \t\tend, err := strconv.Atoi(parts[1])                                        \n    \t\tif err != nil {                                                           \n    \t\t\treturn nil, err                                                           \n    \t\t}                                                                         \n    \t\tfor i := start; i <= end; i++ {                                           \n    \t\t\tindices = append(indices, i)                                              \n    \t\t}                                                                         \n    \t} else {                                                                  \n    \t\tindex, err := strconv.Atoi(arg)                                           \n    \t\tif err != nil {                                                           \n    \t\t\treturn nil, err                                                           \n    \t\t}                                                                         \n    \t\tindices = append(indices, index)                                          \n    \t}                                                                         \n    \treturn indices, nil                                                       \n    }\n\t```"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/assets/valid/diff.txt",
    "content": "diff --git a/tests/go/shared/pre_build.go b/tests/go/valid/post_build.go\nindex c90eeb7..90405e7 100644\n--- a/tests/go/shared/pre_build.go\n+++ b/tests/go/valid/post_build.go\n@@ -7,11 +7,38 @@ import (\n \t\"plandex/auth\"\n \t\"plandex/lib\"\n \t\"plandex/term\"\n+\t\"strconv\"\n+\t\"strings\"\n \n \t\"plandex-shared\"\n \t\"github.com/spf13/cobra\"\n )\n \n+func parseRange(arg string) ([]int, error) {\n+\tvar indices []int\n+\tparts := strings.Split(arg, \"-\")\n+\tif len(parts) == 2 {\n+\t\tstart, err := strconv.Atoi(parts[0])\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tend, err := strconv.Atoi(parts[1])\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tfor i := start; i <= end; i++ {\n+\t\t\tindices = append(indices, i)\n+\t\t}\n+\t} else {\n+\t\tindex, err := strconv.Atoi(arg)\n+\t\tif err != nil {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tindices = append(indices, index)\n+\t}\n+\treturn indices, nil\n+}\n+\n var contextRmCmd = &cobra.Command{\n \tUse:     \"rm\",\n \tAliases: []string{\"remove\", \"unload\"},\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/assets/valid/post_build.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"plandex/api\"\n\t\"plandex/auth\"\n\t\"plandex/lib\"\n\t\"plandex/term\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc parseRange(arg string) ([]int, error) {\n\tvar indices []int\n\tparts := strings.Split(arg, \"-\")\n\tif len(parts) == 2 {\n\t\tstart, err := strconv.Atoi(parts[0])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tend, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor i := start; i <= end; i++ {\n\t\t\tindices = append(indices, i)\n\t\t}\n\t} else {\n\t\tindex, err := strconv.Atoi(arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tindices = append(indices, index)\n\t}\n\treturn indices, nil\n}\n\nvar contextRmCmd = &cobra.Command{\n\tUse:     \"rm\",\n\tAliases: []string{\"remove\", \"unload\"},\n\tShort:   \"Remove context\",\n\tLong:    `Remove context by index, name, or glob.`,\n\tArgs:    cobra.MinimumNArgs(1),\n\tRun:     contextRm,\n}\n\nfunc contextRm(cmd *cobra.Command, args []string) {\n\tauth.MustResolveAuthWithOrg()\n\tlib.MustResolveProject()\n\n\tif lib.CurrentPlanId == \"\" {\n\t\tfmt.Println(\"🤷‍♂️ No current plan\")\n\t\treturn\n\t}\n\n\tterm.StartSpinner(\"\")\n\tcontexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)\n\n\tif err != nil {\n\t\tterm.OutputErrorAndExit(\"Error retrieving context: %v\", err)\n\t}\n\n\tdeleteIds := map[string]bool{}\n\n\tfor i, context := range contexts {\n\t\tfor _, id := range args {\n\t\t\tif fmt.Sprintf(\"%d\", i+1) == id || context.Name == id || context.FilePath == id || context.Url == id {\n\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\tbreak\n\t\t\t} else if context.FilePath != \"\" {\n\t\t\t\t// Check if id is a glob pattern\n\t\t\t\tmatched, err := filepath.Match(id, context.FilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tterm.OutputErrorAndExit(\"Error matching glob pattern: %v\", err)\n\t\t\t\t}\n\t\t\t\tif matched {\n\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// Check if id is a parent directory\n\t\t\t\tparentDir := context.FilePath\n\t\t\t\tfor parentDir != \".\" && parentDir != \"/\" && parentDir != \"\" {\n\t\t\t\t\tif parentDir == id {\n\t\t\t\t\t\tdeleteIds[context.Id] = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tparentDir = filepath.Dir(parentDir) // Move up one directory\n\t\t\t\t}\n\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleteIds) > 0 {\n\t\tres, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{\n\t\t\tIds: deleteIds,\n\t\t})\n\t\tterm.StopSpinner()\n\n\t\tif err != nil {\n\t\t\tterm.OutputErrorAndExit(\"Error deleting context: %v\", err)\n\t\t}\n\n\t\tfmt.Println(\"✅ \" + res.Msg)\n\t} else {\n\t\tterm.StopSpinner()\n\t\tfmt.Println(\"🤷‍♂️ No context removed\")\n\t}\n}\n\nfunc init() {\n\tRootCmd.AddCommand(contextRmCmd)\n}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/promptfooconfig.yaml",
    "content": "# This configuration compares LLM output of 2 prompts x 2 GPT models across 3 test cases.\n# Learn more: https://promptfoo.dev/docs/configuration/guide\ndescription: \"verify\"\n\nprompts:\n  - file://verify.prompt.txt\nproviders:\n  - file://verify.provider.yml\ntests: tests/*.test.yml\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/tests/removal.test.yml",
    "content": "- description: \"Removal of code errors\"\n  vars:\n      preBuildState: file://assets/shared/pre_build.go\n      changes: file://assets/removal/changes.md\n      postBuildState: file://assets/removal/post_build.go\n      diffs: file://assets/removal/diff.txt\n  assert:\n    - type: is-json\n    - type: is-valid-openai-tools-call\n    - type: javascript\n      value: |\n        var args = JSON.parse(output[0].function.arguments)\n        return args.hasRemovedCodeErrors"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/tests/validate.test.yml",
    "content": "- description: \"Validation of the code changes\"\n  vars:\n    preBuildState: file://assets/shared/pre_build.go\n    changes: file://assets/valid/changes.md\n    postBuildState: file://assets/valid/post_build.go\n    diffs: file://assets/valid/diff.txt\n  assert:\n    - type: is-json\n    - type: is-valid-openai-tools-call\n    - type: javascript\n      value: |\n        var args = JSON.parse(output[0].function.arguments)\n        return !(\n          args.hasSyntaxErrors ||\n          args.hasRemovedCodeErrors ||\n          args.hasDuplicationErrors ||\n          args.hasReferenceErrors            \n        )\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/verify.config.properties",
    "content": "provider_id=openai:gpt-4o\ntemperature=0.1\nmax_tokens=4096\ntop_p=0.1\nresponse_format=json_object\nfunction_name=verifyOutput\ntool_type=function\nfunction_param_type=object\ntool_choice_type=function\ntool_choice_function_name=verifyOutput\nnested_parameters_json=verify.parameters.json\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/verify.parameters.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"syntaxErrorsReasoning\": {\n      \"type\": \"string\"\n    },\n    \"hasSyntaxErrors\": {\n      \"type\": \"boolean\"\n    },\n    \"removed\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\"\n          },\n          \"reasoning\": {\n            \"type\": \"string\"\n          },\n          \"correct\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\"code\", \"reasoning\", \"correct\"]\n      }\n    },\n    \"removedCodeErrorsReasoning\": {\n      \"type\": \"string\"\n    },\n    \"hasRemovedCodeErrors\": {\n      \"type\": \"boolean\"\n    },\n    \"duplicationErrorsReasoning\": {\n      \"type\": \"string\"\n    },\n    \"hasDuplicationErrors\": {\n      \"type\": \"boolean\"\n    },\n    \"comments\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"txt\": {\n            \"type\": \"string\"\n          },\n          \"reference\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\"txt\", \"reference\"]\n      }\n    },\n    \"referenceErrorsReasoning\": {\n      \"type\": \"string\"\n    },\n    \"hasReferenceErrors\": {\n      \"type\": \"boolean\"\n    }\n  },\n  \"required\": [\n    \"syntaxErrorsReasoning\",\n    \"hasSyntaxErrors\",\n    \"removed\",\n    \"removedCodeErrorsReasoning\",\n    \"hasRemovedCodeErrors\",\n    \"duplicationErrorsReasoning\",\n    \"hasDuplicationErrors\",\n    \"comments\",\n    \"referenceErrorsReasoning\",\n    \"hasReferenceErrors\"\n  ]\n}\n"
  },
  {
    "path": "test/evals/promptfoo-poc/verify/verify.prompt.txt",
    "content": "Based on an original file (if one exists), an AI-generated plan, an updated file and a set of diffs (if one exists), determine whether the updated file's syntax is correct and whether the proposed updates were applied correctly to the updated file.\n\nYou must consider whether any of the following problems are present in the updated file:\n- Syntax errors, including unbalanced brackets, parentheses, braces, quotes, indentation, and other code structure errors\n- Missing or incorrectly scoped declarations\n- Any other errors that make the code invalid and would prevent it from being run as-is for the programming language being used\n- Code from the original file was incorrectly removed or overwritten.\n- Code was incorrectly duplicated. For example, if a file should have a single main function, but instead of updating the existing main function, the updated file includes multiple main functions, then the file is incorrect. The same applies to any other functions or elements that should not be duplicated.\n- Incorrectly included comments that reference the original code.. If the updated file includes comments like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function...\" or \"// rest of your function...\" or \"// Existing methods...\", \"// Existing code...\" **any other reference to the original code**, the file is incorrect. References like these must be handled by including the exact code from the original file that the comment is referencing.\n\nIf there is no original file, it means that a new file was created from scratch based on the AI-generated plan. In this case, the syntax in the new file must be valid and consistent with the intention of the plan. You must ensure there are no syntax errors or other clear mistakes in the new file.\n\nCall the 'verifyOutput' function with a valid JSON object that include the following keys:\n\n'syntaxErrorsReasoning': A string that succinctly explains whether there are any syntax or scoping errors in the updated file. Explain all syntax errors, scoping errors, or other code structure errors that are present in the updated file. \n\n'hasSyntaxErrors': A boolean that indicates whether there are any syntax errors in the updated file, based on the reasoning provided in 'syntaxErrorsReasoning'.\n\n'removed': an array of objects with three properties: 'code', 'reasoning', and 'correct'. \n   - 'code' is a string. It shows the section of code that was removed or overwritten in the updated file. This can be abbreviated by detailing how the section starts and end and describing the purpose of the code. If the section is longer than a few lines, rather than reproducding a long section of code verbatim, provide a summary of the code that was removed or overwritten, as well as the exact code which starts and ends the section. The summary and start/end code should be details enough to disambiguate the section of code that was removed or overwritten from any other similar sections of code in the file.\n   - 'reasoning' is a string that explains whether, based on the proposed changes, this section was deliberately removed consistent with the intention of the plan, or whether the plan did NOT specify that this section should be removed, and the code was therefore removed incorrectly. Also consider whether this removal breaks the syntax or functionality of the code in the updated file based on the programming language being used--if it does, explain this and state that the removal was incorrect. If the removal either wasn't intended by the proposed updates *or* breaks the syntax or functionality of the code, the removal is incorrect. If the removal was consistent with the intention of the plan *and* did not break the syntax or functionality of the code, the removal is correct.\n   - 'correct' is a boolean that indicates whether the removal or overwriting was correct based on the 'reasoning' provided. If the removal was correct, set 'correct' to true. If the removal was incorrect, set 'correct' to false.\n\nBased on supplied diffs, you must list EVERY code section that was removed or overwritten in the updated file in 'removed'. If there are no code sections that were removed or overwritten, 'removed' must be an empty array. If multiple code sections were removed or overwritten, list each one as a separate object in the 'removed' array. If multiple identical code sections were removed or overwritten, you MUST list them *all* in the 'removed' array--list each identical code section as a separate object in the array. Do NOT include removals that only modify whitespace. Do NOT include sections that were moved or refactored, only sections that were fully removed or overwritten.\n\n'removedCodeErrorsReasoning': A string that succinctly explains whether any code was incorrectly removed or overwritten in the updated file based on the 'removed' array. If code was incorrectly removed or overwritten, succinctly explain why it was incorrect, and how the file can be corrected. If code was correctly removed or overwritten, consistent with the intention of the plan, state this.\n\n'hasRemovedCodeErrors': A boolean that indicates whether any code was *incorrectly* removed or overwritten in the updated file, based on the reasoning provided in 'removedCodeErrorsReasoning'.\n\n'duplicationErrorsReasoning': A string that succinctly explains whether any code was *incorrectly* duplicated in the updated file. First explain whether any code, functions, or other elements are duplicated in the updated file, then explain whether the duplication is deliberate and consistent with the plan, and whether the duplication is correct and valid in the programming language being used.\n\n'hasDuplicationErrors': A boolean that indicates whether any code was *incorrectly* duplicated in the updated file, based on the reasoning provided in 'duplicationErrorsReasoning'. If code was *incorrectly* duplicated, set 'hasDuplicationErrors' to true. If code was *correctly* duplicated, consistent with the intention of the plan, set 'hasDuplicationErrors' to false.\n\n'comments':  an array of objects with two properties: 'txt' and 'reference'. 'txt' is the exact text of a code comment. 'reference' is a boolean that indicates whether the comment is a placeholder of or reference to the original code, like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function\" or \"// rest of your function...\" or \"// Existing methods...\" or \"// Remaining methods\" or \"// Existing code...\" or \"// ... existing setup code ...\"\" or other comments which reference code from the original file. References DO NOT need to exactly match any of the previous examples. Use your judgement to determine whether each comment is a reference. If 'reference' is true, the comment is a placeholder or reference to the original code. If 'reference' is false, the comment is not a placeholder or reference to the original code.\n\nIn 'comments', you must list EVERY comment included in the *updated file*. Only list *code comments* that are valid comments for the programming language being used. Do not list logging statements or any other non-comment text that is not a valid code comment. If there are no code comments in the *updated file*, 'comments' must be an empty array.\n\nIf there are multiple identical comments in the *updated file*, you MUST list them *all* in the 'comments' array--list each identical comment as a separate object in the array.\n\n'referenceErrorsReasoning': A string that succinctly explains whether any comments in the updated file are placeholders/references that should have been replaced with code from the original file. These are comments like \"// rest of the function...\" or \"# existing init code...\", or \"// rest of the main function...\" or \"// rest of your function...\" or \"// Existing methods...\", \"// Existing code...\" or other  comments which reference code from the original file. Only include comments that *are not* present in the original file and *are* present in the proposed updates. If there are no such comments, explain that there are no reference errors.\n\n'hasReferenceErrors': A boolean that indicates whether any comments in the updated file are placeholders/references that should be replaced with code from the original file, based on the reasoning provided in 'referenceErrorsReasoning'.\n\nIn each of the reasoning keys above, be exhaustive and include *every* problem that is present in the file. But if there are no problems in a reasoning key, do NOT invent problems--explain according to your instructions for each key that there are no problems in that category.\n\n--\n\n## **Original file:**\n\n{{preBuildState}}\n\n--\n\n## **Proposed updates:**\n\n{{changes}}\n\n--\n\n## **Updated file:**\n\n{{postBuildState}}\n\n--\n\n## **Diffs:**\n\n{{diffs}}\n\n--\n\nNow call the 'verifyOutput' function with a valid JSON object. Don't call any other function.\n\nYou absolutely MUST generate PERFECTLY VALID JSON. Pay extremely close attention to the JSON syntax and structure. Double quotes within JSON properties *MUST* be properly escaped with a backslash."
  },
  {
    "path": "test/evals/promptfoo-poc/verify/verify.provider.yml",
    "content": "id: openai:gpt-4o\nconfig:\n  temperature: 0.1\n  max_tokens: 4096\n  response_format: { type: json_object }\n  top_p: 0.1\n  tools:\n    [\n      {\n        \"type\": \"function\",\n        \"function\":\n          { \"name\": \"verifyOutput\", \"parameters\": {\"properties\":{\"comments\":{\"items\":{\"properties\":{\"reference\":{\"type\":\"boolean\"},\"txt\":{\"type\":\"string\"}},\"required\":[\"txt\",\"reference\"],\"type\":\"object\"},\"type\":\"array\"},\"duplicationErrorsReasoning\":{\"type\":\"string\"},\"hasDuplicationErrors\":{\"type\":\"boolean\"},\"hasReferenceErrors\":{\"type\":\"boolean\"},\"hasRemovedCodeErrors\":{\"type\":\"boolean\"},\"hasSyntaxErrors\":{\"type\":\"boolean\"},\"referenceErrorsReasoning\":{\"type\":\"string\"},\"removed\":{\"items\":{\"properties\":{\"code\":{\"type\":\"string\"},\"correct\":{\"type\":\"boolean\"},\"reasoning\":{\"type\":\"string\"}},\"required\":[\"code\",\"reasoning\",\"correct\"],\"type\":\"object\"},\"type\":\"array\"},\"removedCodeErrorsReasoning\":{\"type\":\"string\"},\"syntaxErrorsReasoning\":{\"type\":\"string\"}},\"required\":[\"syntaxErrorsReasoning\",\"hasSyntaxErrors\",\"removed\",\"removedCodeErrorsReasoning\",\"hasRemovedCodeErrors\",\"duplicationErrorsReasoning\",\"hasDuplicationErrors\",\"comments\",\"referenceErrorsReasoning\",\"hasReferenceErrors\"],\"type\":\"object\"} },\n      },\n    ]\n  tool_choice:\n    type: \"function\"\n    function:\n      name: \"verifyOutput\"\n"
  },
  {
    "path": "test/plan_deletion_test.sh",
    "content": "# Plandex hasn't been able to get this working yet\n\n#!/bin/bash\nset -e\n\n# Enable debug output\nset -x\n\n# Helper function to check if a plan exists\ncheck_plan_exists() {\n    local plan_name=$1\n    pdxd plans | grep -q \"$plan_name\"\n}\n\n# Helper function to count total plans\ncount_plans() {\n    # Add sleep to ensure plan list is updated\n    sleep 1\n    # Strip ANSI codes, skip help text and header, then count only the test plans\n    pdxd plans | sed 's/\\x1B\\[[0-9;]*[mK]//g' | grep -A 1000 \"^+----+\" | tail -n +3 | head -n 5 | grep -E '\\| (plan-config-[123]|other-plan-[12])(\\.[0-9]+)? \\|' | wc -l\n}\n\n# Clean up any existing test plans before starting\necho \"Cleaning up any existing test plans...\"\necho \"y\" | pdxd dp \"plan-config-*\" || true\necho \"y\" | pdxd dp \"other-plan-*\" || true\nsleep 2\n\n# Create test plans with verification\necho \"Creating test plans...\"\nfor plan in \"plan-config-1\" \"plan-config-2\" \"plan-config-3\" \"other-plan-1\" \"other-plan-2\"; do\n    pdxd new -n \"$plan\"\n    if ! check_plan_exists \"$plan\"; then\n        echo \"❌ Failed to create plan '$plan'\"\n        exit 1\n    fi\n    # Add sleep to ensure plan creation is complete\n    sleep 1\ndone\n\n# Verify initial plans were created\necho \"Verifying plans were created...\"\ninitial_count=$(count_plans)\necho \"Found $initial_count plans\"\n\nif [ \"$initial_count\" -ne 5 ]; then\n    echo \"❌ Expected 5 plans, but found $initial_count\"\n    pdxd plans\n    exit 1\nfi\n\nfor plan in \"plan-config-1\" \"plan-config-2\" \"plan-config-3\" \"other-plan-1\" \"other-plan-2\"; do\n    if ! check_plan_exists \"$plan\"; then\n        echo \"❌ Plan '$plan' was not created successfully\"\n        exit 1\n    fi\ndone\n\n# Test wildcard deletion\necho \"Testing wildcard deletion...\"\necho \"y\" | pdxd dp \"plan-config-*\"\nsleep 1\n\n# Verify wildcard deletion worked\nremaining_count=$(count_plans)\necho \"Found $remaining_count plans after wildcard deletion\"\n\nif [ \"$remaining_count\" -ne 2 ]; then\n    echo \"❌ Expected 2 plans after wildcard deletion, but found $remaining_count\"\n    pdxd plans\n    exit 1\nfi\n\nfor plan in \"plan-config-1\" \"plan-config-2\" \"plan-config-3\"; do\n    if check_plan_exists \"$plan\"; then\n        echo \"❌ Plan '$plan' should have been deleted\"\n        exit 1\n    fi\ndone\n\n# Create more plans for range deletion test\necho \"Creating plans for range deletion test...\"\nfor plan in \"range-test-1\" \"range-test-2\" \"range-test-3\"; do\n    pdxd new -n \"$plan\"\n    if ! check_plan_exists \"$plan\"; then\n        echo \"❌ Failed to create plan '$plan'\"\n        exit 1\n    fi\n    sleep 1\ndone\n\n# Test range deletion (should delete first 3 plans)\necho \"Testing range deletion...\"\necho \"y\" | pdxd dp \"1-3\"\nsleep 1\n\n# Verify range deletion worked\nfinal_count=$(count_plans)\necho \"Found $final_count plans after range deletion\"\n\nif [ \"$final_count\" -ne 2 ]; then\n    echo \"❌ Expected 2 plans after range deletion, but found $final_count\"\n    pdxd plans\n    exit 1\nfi\n\n# Clean up any remaining test plans\necho \"Cleaning up remaining test plans...\"\necho \"y\" | pdxd dp \"plan-config-*\" || true\necho \"y\" | pdxd dp \"other-plan-*\" || true\necho \"y\" | pdxd dp \"range-test-*\" || true\nsleep 2\n\n# Verify all plans were cleaned up\ncleanup_count=$(count_plans)\nif [ \"$cleanup_count\" -ne 0 ]; then\n    echo \"❌ Expected 0 plans after cleanup, but found $cleanup_count\"\n    pdxd plans\n    exit 1\nfi\n\necho \"✅ All tests passed!\"\n"
  },
  {
    "path": "test/project/react-redux-foobar/action.ts",
    "content": "export const INCREMENT_COUNT = 'INCREMENT_COUNT';\n\ninterface IncrementCountAction {\n  type: typeof INCREMENT_COUNT;\n  payload: number; // Payload now specifically expects a number\n}\n\nexport const incrementCount = (amount: number): IncrementCountAction => ({\n  type: INCREMENT_COUNT,\n  payload: amount,\n});\n"
  },
  {
    "path": "test/project/react-redux-foobar/component.ts",
    "content": "import React, { useState } from 'react';\n\ninterface Props {\n  name: string;\n}\n\nconst MyComponent: React.FC<Props> = ({ name }) => {\n  const [count, setCount] = useState(0);\n\n  return (\n    <div>\n      <h1>Hello, {name}!</h1>\n      <p>You clicked {count} times</p>\n      <button onClick={() => setCount(count + 1)}>\n        Click me\n      </button>\n    </div>\n  );\n};\n\nexport default MyComponent;\n"
  },
  {
    "path": "test/project/react-redux-foobar/lib/constants.ts",
    "content": "export const SOME_CONSTANT = 'SOME_CONSTANT_VALUE';\n"
  },
  {
    "path": "test/project/react-redux-foobar/lib/utils.ts",
    "content": "export const formatDate = (date: Date): string => {\n  return date.toLocaleDateString('en-US', {\n    weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'\n  });\n}\n"
  },
  {
    "path": "test/project/react-redux-foobar/package.json",
    "content": "{\n  \"name\": \"react-redux-foobar\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A realistic React/Redux/TypeScript project\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\"\n  },\n  \"dependencies\": {\n    \"@reduxjs/toolkit\": \"^1.8.0\",\n    \"react\": \"^18.0.0\",\n    \"react-dom\": \"^18.0.0\",\n    \"react-redux\": \"^8.0.0\",\n    \"react-scripts\": \"5.0.0\",\n    \"typescript\": \"^4.5.5\"\n  },\n  \"devDependencies\": {\n    \"@testing-library/jest-dom\": \"^5.16.2\",\n    \"@testing-library/react\": \"^13.2.0\",\n    \"@testing-library/user-event\": \"^14.2.1\",\n    \"@types/jest\": \"^27.4.0\",\n    \"@types/node\": \"^17.0.21\",\n    \"@types/react\": \"^18.0.0\",\n    \"@types/react-dom\": \"^18.0.0\"\n  },\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\"\n    ]\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "test/project/react-redux-foobar/reducer.ts",
    "content": "import { createReducer } from '@reduxjs/toolkit';\nimport { INCREMENT_COUNT } from './action';\n\ninterface State {\n  count: number;\n}\n\nconst initialState: State = {\n  count: 0,\n};\n\nconst countReducer = createReducer(initialState, (builder) => {\n  builder.addCase(INCREMENT_COUNT, (state, action) => {\n    state.count += action.payload;\n  });\n});\n\nexport default countReducer;\n"
  },
  {
    "path": "test/project/react-redux-foobar/tests/action.test.ts",
    "content": "import { myAction, MY_ACTION_TYPE } from '../action';\n\ndescribe('myAction', () => {\n  it('creates an action with the correct type and payload', () => {\n    const payload = {}; // Define payload\n    const expectedAction = {\n      type: MY_ACTION_TYPE,\n      payload,\n    };\n    expect(myAction(payload)).toEqual(expectedAction);\n  });\n});\n"
  },
  {
    "path": "test/project/react-redux-foobar/tests/component.test.ts",
    "content": "import { render, fireEvent } from '@testing-library/react';  it('increments count on button click', () => {\n    const { getByText } = render(<MyComponent name=\"John Doe\" />);\n    fireEvent.click(getByText('Click me'));\n    expect(getByText('You clicked 1 times')).toBeInTheDocument();\n  });\n});\nimport MyComponent from '../component';\n\ndescribe('MyComponent', () => {\n  it('renders with the correct name', () => {\n    const { getByText } = render(<MyComponent name=\"John Doe\" />);\n    expect(getByText('Hello, John Doe!')).toBeInTheDocument();\n  });\n});\n\n"
  },
  {
    "path": "test/project/react-redux-foobar/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "test/smoke_test.sh",
    "content": "#!/bin/bash\n# Plandex Smoke Test Script\n# Tests core functionality in a linear flow mimicking real usage\n# Assumes: Already signed in to Plandex Cloud (dev or staging account)\n\nset -e  # Exit on error\n\n# Source common utilities\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"${SCRIPT_DIR}/test_utils.sh\"\n\n# Test-specific variables\nPROMPT_CREATE_FUNCTION=\"add a simple hello world function in main.go\"\nPROMPT_ADD_TEST=\"add a test for the hello function\"\nPROMPT_CHAT_QUESTION=\"what does the hello function do?\"\nPROMPT_ADD_FEATURE=\"add a goodbye function that returns: goodbye world\"\n\n# Setup for this test\nsetup() {\n    setup_test_dir \"smoke-test\"\n    \n    # Create a simple Go project structure\n    mkdir -p cmd\n    echo \"package main\" > main.go\n    echo \"func main() {}\" >> main.go\n    \n    # Create a test file to load as context\n    cat > README.md << EOF\n# Test Project\nThis is a test project for Plandex smoke testing.\nEOF\n}\n\n# Set trap for cleanup on exit\ntrap cleanup_test_dir EXIT\n\n# Main test flow\nmain() {\n    log \"=== Plandex Smoke Test Started at $(date) ===\"\n    \n    setup\n    \n    # 1. PLAN MANAGEMENT\n    log \"\\n=== Testing Plan Management ===\"\n    \n    # Create new plan with name\n    run_plandex_cmd \"new -n smoke-test-plan\" \"Create named plan\"\n    \n    # Check current plan\n    run_plandex_cmd \"current\"\n    \n    # List plans\n    run_plandex_cmd \"plans\"\n    \n    # 2. CONTEXT MANAGEMENT\n    log \"\\n=== Testing Context Management ===\"\n    \n    # Load single file\n    run_plandex_cmd \"load main.go\" \"Load single file\"\n    \n    # Load with note\n    run_plandex_cmd \"load -n 'keep code simple and well-commented'\" \"Load note\"\n    \n    # Load directory tree\n    run_plandex_cmd \"load . --tree\" \"Load directory tree\"\n    \n    # List context\n    run_plandex_cmd \"ls\"\n    \n    # Show specific context\n    run_plandex_cmd \"show main.go\"\n    \n    # 3. BASIC TASK EXECUTION\n    log \"\\n=== Testing Task Execution ===\"\n\n    # Skip changes menu so we don't have to interact with the menu\n    run_plandex_cmd \"set-config skip-changes-menu true\" \"Set skip-changes-menu to true\"\n    \n    # Tell command with simple task\n    run_plandex_cmd \"tell '$PROMPT_CREATE_FUNCTION'\" \"Execute tell command\"\n    \n    # Check diff\n    run_plandex_cmd \"diff --git\"\n    \n    # Apply changes\n    run_plandex_cmd \"apply --auto-exec --debug 2 --skip-commit\" \"Apply changes\"\n    \n    # Verify file was updated\n    check_file \"main.go\"\n    \n    # 4. CHAT FUNCTIONALITY\n    log \"\\n=== Testing Chat ===\"\n    \n    # Chat without making changes\n    run_plandex_cmd \"chat '$PROMPT_CHAT_QUESTION'\"\n    \n    # 5. CONTINUE AND BUILD\n    log \"\\n=== Testing Continue and Build ===\"\n    \n    # Tell another task\n    run_plandex_cmd \"tell '$PROMPT_ADD_TEST' --no-build\" \"Tell without building\"\n    \n    # Build pending changes\n    run_plandex_cmd \"build\" \"Build pending changes\"\n    \n    # Review and apply\n    run_plandex_cmd \"diff --git\"\n    run_plandex_cmd \"apply --auto-exec --debug 2 --skip-commit\" \"Apply test changes\"\n    \n    # 6. BRANCHES\n    log \"\\n=== Testing Branches ===\"\n    \n    # Create and switch to new branch\n    run_plandex_cmd \"checkout feature-branch -y\" \"Create new branch\"\n    \n    # Make changes on branch\n    run_plandex_cmd \"tell '$PROMPT_ADD_FEATURE'\" \"Add feature on branch\"\n    run_plandex_cmd \"apply --auto-exec --debug 2 --skip-commit\" \"Apply on branch\"\n    \n    # List branches\n    run_plandex_cmd \"branches\"\n    \n    # Switch back to main\n    run_plandex_cmd \"checkout main\" \"Switch to main branch\"\n    \n    # 7. VERSION CONTROL\n    log \"\\n=== Testing Version Control ===\"\n    \n    # View log\n    run_plandex_cmd \"log\"\n    \n    # View conversation\n    run_plandex_cmd \"convo\"\n    \n    # Get current state for rewind test\n    REWIND_STEPS=2\n    info \"Will rewind $REWIND_STEPS steps\"\n    \n    # Rewind\n    run_plandex_cmd \"rewind $REWIND_STEPS --revert\" \"Rewind $REWIND_STEPS steps\"\n    \n    # 8. CONFIGURATION\n    log \"\\n=== Testing Configuration ===\"\n    \n    # View current config\n    run_plandex_cmd \"config\"\n    \n    # Change a setting\n    run_plandex_cmd \"set-config auto-continue false\" \"Set auto-continue to false\"\n\n    # Change it back\n    run_plandex_cmd \"set-config auto-continue true\" \"Set auto-continue to true\"\n    \n    # View models\n    run_plandex_cmd \"models\"\n    \n    # List model packs\n    run_plandex_cmd \"model-packs\"\n    \n    # 9. CONTEXT UPDATES\n    log \"\\n=== Testing Context Updates ===\"\n    \n    # Modify a file outside of Plandex\n    echo \"// Modified outside Plandex\" >> main.go\n    \n    # Update context\n    run_plandex_cmd \"update\" \"Update outdated context\"\n    \n    # Remove context\n    run_plandex_cmd \"rm main.go\" \"Remove file from context\"\n    \n    # Clear all context\n    run_plandex_cmd \"clear\" \"Clear all context\"\n    \n    # 10. REJECT FUNCTIONALITY\n    log \"\\n=== Testing Reject ===\"\n    \n    # Load context again and make changes\n    run_plandex_cmd \"load . -r\" \"Reload context\"\n    run_plandex_cmd \"tell 'add a function that has an intentional syntax error'\" \"Create changes to reject\"\n    \n    # Reject all pending changes\n    run_plandex_cmd \"reject --all\" \"Reject all pending changes\"\n    \n    # 11. ARCHIVE FUNCTIONALITY\n    log \"\\n=== Testing Archive ===\"\n    \n    # Archive the plan\n    run_plandex_cmd \"archive smoke-test-plan\" \"Archive plan\"\n    \n    # List archived plans\n    run_plandex_cmd \"plans --archived\"\n    \n    # Unarchive\n    run_plandex_cmd \"unarchive smoke-test-plan\" \"Unarchive plan\"\n    \n    # 12. MULTIPLE PLANS\n    log \"\\n=== Testing Multiple Plans ===\"\n    \n    # Create another plan with model pack\n    run_plandex_cmd \"new -n second-plan --cheap\" \"Create plan with cheap model pack\"\n    \n    # Switch between plans\n    run_plandex_cmd \"cd smoke-test-plan\" \"Switch to first plan\"\n    run_plandex_cmd \"current\"\n    \n    log \"\\n=== Plandex Smoke Test Completed Successfully at $(date) ===\"\n}\n\n# Run the tests\nmain"
  },
  {
    "path": "test/test_custom_models.sh",
    "content": "#!/bin/bash\n# custom-models-test.sh - Plandex custom models functionality test\n\nset -e  # Exit on error\n\n# Source common utilities\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"${SCRIPT_DIR}/test_utils.sh\"\n\n# Setup for this test\nsetup() {\n    setup_test_dir \"custom-models-test\"\n    \n    # Create a simple test file\n    echo \"package main\" > main.go\n}\n\n# Set trap for cleanup on exit\ntrap cleanup_test_dir EXIT\n\n# Create custom models JSON matching the GitHub issue\ncreate_custom_models_json() {\n    cat > custom-models.json << 'EOF'\n{\n  \"$schema\": \"https://plandex.ai/schemas/models-input.schema.json\",\n  \"models\": [\n    {\n      \"modelId\": \"custom-claude-4\",\n      \"publisher\": \"test\",\n      \"description\": \"Claude 4 Sonnet test\",\n      \"defaultMaxConvoTokens\": 15000,\n      \"maxTokens\": 200000,\n      \"maxOutputTokens\": 64000,\n      \"reservedOutputTokens\": 16000,\n      \"preferredOutputFormat\": \"xml\",\n      \"hasImageSupport\": true,\n      \"providers\": [\n        {\n          \"provider\": \"openrouter\",\n          \"modelName\": \"anthropic/claude-sonnet-4\"\n        }\n      ]\n    }\n  ],\n  \"modelPacks\": [\n    {\n      \"name\": \"test-pack\",\n      \"description\": \"Test model pack\",\n      \"$schema\": \"https://plandex.ai/schemas/model-pack-inline.schema.json\",\n      \"planner\": {\n        \"modelId\": \"custom-claude-4\",\n        \"largeContextFallback\": \"custom-claude-4\"\n      },\n      \"architect\": \"custom-claude-4\",\n      \"coder\": \"custom-claude-4\",\n      \"summarizer\": \"custom-claude-4\",\n      \"builder\": \"custom-claude-4\",\n      \"wholeFileBuilder\": \"custom-claude-4\",\n      \"names\": \"custom-claude-4\",\n      \"commitMessages\": \"custom-claude-4\",\n      \"autoContinue\": \"custom-claude-4\"\n    }\n  ]\n}\nEOF\n}\n\nmain() {\n    log \"=== Plandex Custom Models Test Started at $(date) ===\"\n    \n    setup\n\n    echo \"OPENROUTER_API_KEY: $OPENROUTER_API_KEY\"\n\n    run_plandex_cmd \"new -n custom-model-test\" \"Create test plan\"\n    run_plandex_cmd \"models\" \"Show current models\"\n    \n    log \"\\n=== Testing Custom Models with Custom Provider ===\"\n    \n    create_custom_models_json\n    run_plandex_cmd \"models custom --file custom-models.json --save\" \"Import custom models\"\n    run_plandex_cmd \"models available --custom\" \"List custom models\"\n    run_plandex_cmd \"set-model test-pack\" \"Set custom model pack\"\n    \n    # test without required API key\n    PREV_KEY=$OPENROUTER_API_KEY\n    unset OPENROUTER_API_KEY\n    expect_plandex_failure \"tell 'write a hello world program in Go'\" \"Tell with custom models (should fail due to missing API key)\"\n    \n    # restore API key\n    export OPENROUTER_API_KEY=$PREV_KEY\n    run_plandex_cmd \"tell 'write a hello world program in Go'\" \"Tell with custom models\"\n\n    log \"\\n=== Custom Models Test Completed at $(date) ===\"\n}\n\n# Run the tests\nmain"
  },
  {
    "path": "test/test_prompts/aws-infra.txt",
    "content": "I want to deploy api infrastructure with the following components:          \n                                                                              \nThese should all be done with the AWS CDK and tied together in              \ninfra/lib/main.ts. The infra project will need simple, minimalistic package.json and tsconfig.json files in `infra`.                                                          \n                                                                            \nPlease pay close attention to detail and ensure every setting is correct,   \nsecure, and production ready. At the same time, do not include any          \nnonessential attributes or complexity. Keep it as simple as it can be while \nstill being correct.                                                        \n                                                                            \n• A basic VPC                                                               \n• Two public subnets that allow outgoing requests                           \n• Two private subnets for the database                                      \n• An RDS Aurora postgresql database with smallest possible size--just a single\ninstance with no replicas for now. it should run in the private subnets.    \n• An ECR repository where container images can be stored that are loaded by \nthe fargate cluster                                                         \n• A fargate ECS cluster that runs an image from the ECR repository. just one\nreplica in the cluster for now                                              \n• The fargate cluster should have access to the RDS database. the cluster   \nshould run in public subnets that can make outgoing requests to the         \ninternet.                                                                   \n• Replicas within the fargate cluster should be able to make requests to    \neach other internally- The fargate cluster and RDS should use a secrets     \nmanager secret for the database credentials.                                \n• I need an EFS shared file system that all the fargate cluster containers  \nare connected to                                                            \n• The fargate containers also need to send emails via SES... please include \nany necessary resources                                                     \n                                                                            \nplease use your best judgement and select reasonable values for all of      \nthese. the db password should be generated by secrets manager. use defaults \nand best practices.\n"
  },
  {
    "path": "test/test_prompts/pong.txt",
    "content": "Let's create a basic pong game using C and OpenGL. For now let's make it 1-player pong with the computer as the opponent. Create a basic AI for the computer that will move the paddle up and down based on the ball's position.\n\nCreate all the necessary functionality for the project so that it will be ready to compile and run.\n\nInclude a script that will install any necessary dependencies that are missing on Mac OSC. Prefer homebrew where possible for package management.\n\nAlso include a Makefile that will compile the project.\n\nInclude a README.md file that will describe the project and how to run it from the command line.\n"
  },
  {
    "path": "test/test_prompts/robust-logging.txt",
    "content": "insert simple log statements throughout the operations of the 'app/server/model/plan/tell.go' file so it's easy to track the flow of logic. add detailed and rigorous logging to all branches of control flow. log using log.Printf / log.Println                                                                 \n                                                                              \nonly log key data and don't log anything that will show too large of an     \noutput. the goal is to be able to follow the flow of the logic, not see *all\n* the data.\n\nespecially add comprehensive logging to anything related to the 'missingFileResponse' functionality. as well as everything within spitting distance of this code: \n                                                                              \n  active.Stream(shared.StreamMessage{ Type:                                   \n  shared.StreamMessagePromptMissingFile, MissingFilePath: currentFile, })     \n                                                                              \n                                // stop stream for now                                                    \n                                active.CancelModelStreamFn()                                              \n                                                                              \n                                // wait for user response to come in                                      \n                                userChoice := <-active.MissingFileResponseCh  "
  },
  {
    "path": "test/test_prompts/stripe-plan.txt",
    "content": "I want to integrate this app with Stripe on the backend. For now let’s just concern ourselves with the backend portion (no frontend or client-side logic yet).\n\nI want to start with a single Stripe plan. Assuming IS_CLOUD == “1”, each org should be on this plan after the user converts from a free trial. The price should be $15 per user per month.\n\nWe should add functionality to add and remove users.\n\nThere should be no proration and no immediate charge when a user is added (or removed). The org will simply be charged once per month based on how many users they have at that point.\n\nWe should also add webhook callbacks for charge successful, charge failed, and subscription canceled events. Implement these webhooks and the associated routes.\n\nThe Stripe secret key will be available in an environment variable.\n\nDon't add the stripe library dependency. I'll take care of that."
  },
  {
    "path": "test/test_prompts/tic-tac-toe.txt",
    "content": "Build a complete tic-tac-toe game in rust\n"
  },
  {
    "path": "test/test_utils.sh",
    "content": "#!/bin/bash\n# test-utils.sh - Common utilities for Plandex test scripts\n\nexport PLANDEX_ENV='development'\n\n# Colors for output\nexport RED='\\033[0;31m'\nexport GREEN='\\033[0;32m'\nexport YELLOW='\\033[1;33m'\nexport NC='\\033[0m' # No Color\n\n# Default command\nexport PLANDEX_CMD=\"${PLANDEX_CMD:-plandex-dev}\"\n\n# Logging functions\nlog() {\n    echo -e \"$1\"\n}\n\nsuccess() {\n    log \"${GREEN}✓ $1${NC}\"\n}\n\nerror() {\n    log \"${RED}✗ $1${NC}\"\n    exit 1\n}\n\ninfo() {\n    log \"${YELLOW}→ $1${NC}\"\n}\n\n# Run command and check for success\nrun_cmd() {\n    local cmd=\"$1\"\n    local description=\"$2\"\n    \n    info \"Running: $cmd\"\n    \n    # Run command and capture output and exit code properly\n    set +e  # Temporarily disable exit on error\n    output=$(eval \"$cmd\" 2>&1)\n    local exit_code=$?\n    set -e  # Re-enable exit on error\n    \n    # Log the output\n    echo \"$output\"\n    \n    if [ \"$exit_code\" -eq 0 ]; then\n        success \"$description\"\n    else\n        error \"$description failed (exit code: $exit_code)\"\n    fi\n}\n\n# Run plandex command\nrun_plandex_cmd() {\n    local cmd=\"$1\"\n    local description=\"$2\"\n    run_cmd \"$PLANDEX_CMD $cmd\" \"$description\"\n}\n\n# Run plandex command and check if output contains substring\ncheck_plandex_contains() {\n    local cmd=\"$1\"\n    local expected=\"$2\"\n    local description=\"$3\"\n    \n    info \"Running: $PLANDEX_CMD $cmd\"\n    \n    local output=$($PLANDEX_CMD $cmd 2>&1)\n    echo \"$output\"\n    \n    if echo \"$output\" | grep -q \"$expected\"; then\n        success \"$description\"\n    else\n        error \"$description - expected to find '$expected'\"\n    fi\n}\n\n# Check if command fails (expecting failure)\nexpect_failure() {\n    local cmd=\"$1\"\n    local description=\"$2\"\n    \n    info \"Running (expecting failure): $cmd\"\n    \n    # Run the command and capture both output and exit code\n    set +e  # Temporarily disable exit on error\n    output=$(eval \"$cmd\" 2>&1)\n    local exit_code=$?\n    set -e  # Re-enable exit on error\n    \n    echo \"$output\"\n    \n    if [ \"$exit_code\" -ne 0 ]; then\n        success \"$description (failed as expected with exit code $exit_code)\"\n    else\n        error \"$description should have failed but succeeded (exit code: $exit_code)\"\n    fi\n}\n\n# Expect plandex command to fail\nexpect_plandex_failure() {\n    local cmd=\"$1\"\n    local description=\"$2\"\n    expect_failure \"$PLANDEX_CMD $cmd\" \"$description\"\n}\n\n# Check if file exists\ncheck_file() {\n    if [ -f \"$1\" ]; then\n        success \"File exists: $1\"\n    else\n        error \"File missing: $1\"\n    fi\n}\n\n# Setup test environment\nsetup_test_dir() {\n    source ../.env.client-keys\n\n    local test_name=\"$1\"\n    TEST_DIR=\"/tmp/plandex-${test_name}-$$\"\n    TIMESTAMP=$(date +%Y%m%d_%H%M%S)\n    \n    info \"Setting up test environment in $TEST_DIR\"\n    mkdir -p \"$TEST_DIR\"\n    cd \"$TEST_DIR\"\n    \n    success \"Test environment created\"\n}\n\n# Cleanup function\ncleanup_test_dir() {\n    info \"Cleaning up test environment\"\n    cd /\n    rm -rf \"$TEST_DIR\"\n    success \"Cleanup complete\"\n}"
  }
]