├── .dockerignore
├── .gitattributes
├── .github
└── workflows
│ └── docker-publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .dockerignore
├── .gitignore
├── clear_local.sh
├── cli
│ ├── api
│ │ ├── clients.go
│ │ ├── errors.go
│ │ ├── methods.go
│ │ └── stream.go
│ ├── auth
│ │ ├── account.go
│ │ ├── api.go
│ │ ├── auth.go
│ │ ├── org.go
│ │ ├── state.go
│ │ └── trial.go
│ ├── cmd
│ │ ├── apply.go
│ │ ├── archive.go
│ │ ├── billing.go
│ │ ├── branches.go
│ │ ├── browser.go
│ │ ├── build.go
│ │ ├── cd.go
│ │ ├── chat.go
│ │ ├── checkout.go
│ │ ├── clear.go
│ │ ├── config.go
│ │ ├── connect.go
│ │ ├── context_show.go
│ │ ├── continue.go
│ │ ├── convo.go
│ │ ├── current.go
│ │ ├── debug.go
│ │ ├── delete_branch.go
│ │ ├── delete_plan.go
│ │ ├── diffs.go
│ │ ├── invite.go
│ │ ├── load.go
│ │ ├── log.go
│ │ ├── ls.go
│ │ ├── model_packs.go
│ │ ├── models.go
│ │ ├── new.go
│ │ ├── plan_exec_helpers.go
│ │ ├── plan_start_helpers.go
│ │ ├── plans.go
│ │ ├── ps.go
│ │ ├── reject.go
│ │ ├── rename.go
│ │ ├── repl.go
│ │ ├── revoke.go
│ │ ├── rewind.go
│ │ ├── rm.go
│ │ ├── root.go
│ │ ├── set_config.go
│ │ ├── set_model.go
│ │ ├── sign_in.go
│ │ ├── stop.go
│ │ ├── summary.go
│ │ ├── tell.go
│ │ ├── unarchive.go
│ │ ├── update.go
│ │ ├── usage.go
│ │ ├── users.go
│ │ └── version.go
│ ├── dev.sh
│ ├── format
│ │ ├── file.go
│ │ └── time.go
│ ├── fs
│ │ ├── fs.go
│ │ ├── paths.go
│ │ └── projects.go
│ ├── go.mod
│ ├── go.sum
│ ├── install.sh
│ ├── lib
│ │ ├── active_stream.go
│ │ ├── apply.go
│ │ ├── apply_cgroup_linux.go
│ │ ├── apply_cgroup_other.go
│ │ ├── apply_proc.go
│ │ ├── build.go
│ │ ├── context_auto_load.go
│ │ ├── context_conflict.go
│ │ ├── context_display.go
│ │ ├── context_load.go
│ │ ├── context_paths.go
│ │ ├── context_shared.go
│ │ ├── context_update.go
│ │ ├── current.go
│ │ ├── git.go
│ │ ├── legacy_files.go
│ │ ├── log_format.go
│ │ ├── model_settings.go
│ │ ├── plan_config.go
│ │ ├── plans.go
│ │ ├── repl.go
│ │ └── rewind.go
│ ├── main.go
│ ├── nodemon.json
│ ├── plan.json
│ ├── plan_exec
│ │ ├── action_menu.go
│ │ ├── apply_exec.go
│ │ ├── build.go
│ │ ├── params.go
│ │ └── tell.go
│ ├── stream
│ │ └── stream.go
│ ├── stream_tui
│ │ ├── debouncer.go
│ │ ├── model.go
│ │ ├── run.go
│ │ ├── update.go
│ │ └── view.go
│ ├── term
│ │ ├── color.go
│ │ ├── errors.go
│ │ ├── format.go
│ │ ├── help.go
│ │ ├── os.go
│ │ ├── prompt.go
│ │ ├── repl.go
│ │ ├── select.go
│ │ ├── spinner.go
│ │ └── utils.go
│ ├── types
│ │ ├── api.go
│ │ ├── apply.go
│ │ ├── exec.go
│ │ ├── fs.go
│ │ └── types.go
│ ├── ui
│ │ └── ui.go
│ ├── upgrade.go
│ ├── url
│ │ └── url.go
│ ├── utils
│ │ └── utils.go
│ ├── version.txt
│ └── version
│ │ └── version.go
├── docker-compose.yml
├── plans
│ ├── credits-cmd.txt
│ ├── credits-log-cmd.txt
│ ├── json-prompts-to-xml.md
│ └── plan-config.md
├── reset_local.sh
├── scripts
│ ├── cmd
│ │ ├── gen
│ │ │ └── gen.go
│ │ └── provider
│ │ │ └── gen_provider.go
│ ├── dev.sh
│ └── wait-for-it.sh
├── server
│ ├── .gitignore
│ ├── Dockerfile
│ ├── db
│ │ ├── account_helpers.go
│ │ ├── ai_model_helpers.go
│ │ ├── auth_helpers.go
│ │ ├── branch_helpers.go
│ │ ├── build_helpers.go
│ │ ├── context_helpers_conflicts.go
│ │ ├── context_helpers_get.go
│ │ ├── context_helpers_load.go
│ │ ├── context_helpers_map.go
│ │ ├── context_helpers_remove.go
│ │ ├── context_helpers_store.go
│ │ ├── context_helpers_update.go
│ │ ├── convo_helpers.go
│ │ ├── data_models.go
│ │ ├── db.go
│ │ ├── diff_helpers.go
│ │ ├── fs.go
│ │ ├── git.go
│ │ ├── invite_helpers.go
│ │ ├── locks.go
│ │ ├── models.go
│ │ ├── org_helpers.go
│ │ ├── plan_config_helpers.go
│ │ ├── plan_helpers.go
│ │ ├── project_helpers.go
│ │ ├── queue.go
│ │ ├── rbac_helpers.go
│ │ ├── result_helpers.go
│ │ ├── settings_helpers.go
│ │ ├── stream_helpers.go
│ │ ├── subtask_helpers.go
│ │ ├── summary_helpers.go
│ │ ├── transactions.go
│ │ ├── user_helpers.go
│ │ └── utils.go
│ ├── diff
│ │ └── diff.go
│ ├── email
│ │ ├── email.go
│ │ ├── invite.go
│ │ └── verification.go
│ ├── go.mod
│ ├── go.sum
│ ├── handlers
│ │ ├── accounts.go
│ │ ├── auth_helpers.go
│ │ ├── branches.go
│ │ ├── client_helper.go
│ │ ├── context_helper.go
│ │ ├── err_helper.go
│ │ ├── file_maps.go
│ │ ├── file_maps_queue.go
│ │ ├── invites.go
│ │ ├── models.go
│ │ ├── org_helpers.go
│ │ ├── orgs.go
│ │ ├── plan_config.go
│ │ ├── plans_changes.go
│ │ ├── plans_context.go
│ │ ├── plans_convo.go
│ │ ├── plans_crud.go
│ │ ├── plans_exec.go
│ │ ├── plans_versions.go
│ │ ├── projects.go
│ │ ├── proxy_helper.go
│ │ ├── sessions.go
│ │ ├── settings.go
│ │ ├── stream_helper.go
│ │ └── users.go
│ ├── hooks
│ │ └── hooks.go
│ ├── host
│ │ └── ip.go
│ ├── main.go
│ ├── migrations
│ │ ├── 2023120500_init.down.sql
│ │ ├── 2023120500_init.up.sql
│ │ ├── 2024011700_rbac.down.sql
│ │ ├── 2024011700_rbac.up.sql
│ │ ├── 2024012400_streams.down.sql
│ │ ├── 2024012400_streams.up.sql
│ │ ├── 2024012500_locks.down.sql
│ │ ├── 2024012500_locks.up.sql
│ │ ├── 2024013000_plan_build_convo_ids.down.sql
│ │ ├── 2024013000_plan_build_convo_ids.up.sql
│ │ ├── 2024020800_heartbeats.down.sql
│ │ ├── 2024020800_heartbeats.up.sql
│ │ ├── 2024022000_revert_plan_build_convo_ids.down.sql
│ │ ├── 2024022000_revert_plan_build_convo_ids.up.sql
│ │ ├── 2024032700_remove_billing_admin.down.sql
│ │ ├── 2024032700_remove_billing_admin.up.sql
│ │ ├── 2024032701_drop_users_projects.down.sql
│ │ ├── 2024032701_drop_users_projects.up.sql
│ │ ├── 2024040400_add_orgs_users_unique.down.sql
│ │ ├── 2024040400_add_orgs_users_unique.up.sql
│ │ ├── 2024041500_model_sets_models.down.sql
│ │ ├── 2024041500_model_sets_models.up.sql
│ │ ├── 2024042600_default_plan_settings.down.sql
│ │ ├── 2024042600_default_plan_settings.up.sql
│ │ ├── 2024091800_sign_in_codes.down.sql
│ │ ├── 2024091800_sign_in_codes.up.sql
│ │ ├── 2024092100_remove_trial_fields.down.sql
│ │ ├── 2024092100_remove_trial_fields.up.sql
│ │ ├── 2024100900_update_locks.down.sql
│ │ ├── 2024100900_update_locks.up.sql
│ │ ├── 2024121400_plan_config.down.sql
│ │ ├── 2024121400_plan_config.up.sql
│ │ ├── 2025012600_update_custom_models.down.sql
│ │ ├── 2025012600_update_custom_models.up.sql
│ │ ├── 2025021101_locks_unique.down.sql
│ │ ├── 2025021101_locks_unique.up.sql
│ │ ├── 2025022700_remove_models_col.down.sql
│ │ ├── 2025022700_remove_models_col.up.sql
│ │ ├── 2025031300_add_model_roles.down.sql
│ │ ├── 2025031300_add_model_roles.up.sql
│ │ ├── 2025031900_add_custom_model_cols.down.sql
│ │ ├── 2025031900_add_custom_model_cols.up.sql
│ │ ├── 2025032400_sign_in_codes_on_delete.down.sql
│ │ └── 2025032400_sign_in_codes_on_delete.up.sql
│ ├── model
│ │ ├── client.go
│ │ ├── client_stream.go
│ │ ├── model_error.go
│ │ ├── model_request.go
│ │ ├── name.go
│ │ ├── parse
│ │ │ ├── subtasks.go
│ │ │ └── subtasks_test.go
│ │ ├── plan
│ │ │ ├── activate.go
│ │ │ ├── build_exec.go
│ │ │ ├── build_finish.go
│ │ │ ├── build_load.go
│ │ │ ├── build_race.go
│ │ │ ├── build_state.go
│ │ │ ├── build_structured_edits.go
│ │ │ ├── build_validate_and_fix.go
│ │ │ ├── build_whole_file.go
│ │ │ ├── commit_msg.go
│ │ │ ├── exec_status.go
│ │ │ ├── shutdown.go
│ │ │ ├── state.go
│ │ │ ├── stop.go
│ │ │ ├── tell_build_pending.go
│ │ │ ├── tell_context.go
│ │ │ ├── tell_exec.go
│ │ │ ├── tell_load.go
│ │ │ ├── tell_missing_file.go
│ │ │ ├── tell_prompt_message.go
│ │ │ ├── tell_stage.go
│ │ │ ├── tell_state.go
│ │ │ ├── tell_stream_error.go
│ │ │ ├── tell_stream_finish.go
│ │ │ ├── tell_stream_main.go
│ │ │ ├── tell_stream_processor.go
│ │ │ ├── tell_stream_processor_test.go
│ │ │ ├── tell_stream_status.go
│ │ │ ├── tell_stream_store.go
│ │ │ ├── tell_stream_usage.go
│ │ │ ├── tell_subtasks.go
│ │ │ ├── tell_summary.go
│ │ │ ├── tell_sys_prompt.go
│ │ │ └── utils.go
│ │ ├── prompts
│ │ │ ├── apply_exec.go
│ │ │ ├── architect_context.go
│ │ │ ├── build_helpers.go
│ │ │ ├── build_validation_replacements.go
│ │ │ ├── build_whole_file.go
│ │ │ ├── chat.go
│ │ │ ├── code_block_langs.go
│ │ │ ├── describe.go
│ │ │ ├── exec_status.go
│ │ │ ├── explanation_format.go
│ │ │ ├── file_ops.go
│ │ │ ├── implement.go
│ │ │ ├── missing_file.go
│ │ │ ├── name.go
│ │ │ ├── planning.go
│ │ │ ├── shared.go
│ │ │ ├── summary.go
│ │ │ ├── update_format.go
│ │ │ └── user_prompt.go
│ │ ├── summarize.go
│ │ └── tokens.go
│ ├── notify
│ │ └── errors.go
│ ├── routes
│ │ └── routes.go
│ ├── setup
│ │ └── setup.go
│ ├── shutdown
│ │ └── shutdown.go
│ ├── syntax
│ │ ├── comments.go
│ │ ├── file_map
│ │ │ ├── cli
│ │ │ │ ├── .gitignore
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ └── main.go
│ │ │ ├── examples
│ │ │ │ ├── bash_example.sh
│ │ │ │ ├── c_example.c
│ │ │ │ ├── cpp_example.cpp
│ │ │ │ ├── csharp_example.cs
│ │ │ │ ├── css_example.css
│ │ │ │ ├── cue_example.cue
│ │ │ │ ├── dockerfile_example
│ │ │ │ ├── elixir_example.ex
│ │ │ │ ├── elm_example.elm
│ │ │ │ ├── go_example.go
│ │ │ │ ├── groovy_example.groovy
│ │ │ │ ├── hcl_example.hcl
│ │ │ │ ├── html_example.html
│ │ │ │ ├── java_example.java
│ │ │ │ ├── javascript_example.js
│ │ │ │ ├── kotlin_example.kt
│ │ │ │ ├── lua_example.lua
│ │ │ │ ├── markdown_example.md
│ │ │ │ ├── ocaml_example.ml
│ │ │ │ ├── php_example.php
│ │ │ │ ├── protobuf_example.proto
│ │ │ │ ├── python_example.py
│ │ │ │ ├── ruby_example.rb
│ │ │ │ ├── rust_example.rs
│ │ │ │ ├── scala_example.scala
│ │ │ │ ├── svelte_example.svelte
│ │ │ │ ├── swift_example.swift
│ │ │ │ ├── toml_example.toml
│ │ │ │ ├── tsx_example.tsx
│ │ │ │ ├── typescript_example.ts
│ │ │ │ └── yaml_example.yaml
│ │ │ ├── map.go
│ │ │ ├── markup.go
│ │ │ ├── multi.go
│ │ │ ├── nodes_config.go
│ │ │ ├── nodes_find.go
│ │ │ └── svelte.go
│ │ ├── map.txt
│ │ ├── parsers.go
│ │ ├── structured_edits_apply.go
│ │ ├── structured_edits_generic.go
│ │ ├── structured_edits_sections.go
│ │ ├── structured_edits_test.go
│ │ ├── structured_edits_tree_sitter.go
│ │ ├── unique_replacement.go
│ │ ├── unique_replacement_test.go
│ │ └── validate.go
│ ├── types
│ │ ├── active_plan.go
│ │ ├── active_plan_pending_builds.go
│ │ ├── auth.go
│ │ ├── convo_message_desc.go
│ │ ├── exec_status.go
│ │ ├── message.go
│ │ ├── model.go
│ │ ├── reply.go
│ │ ├── reply_test.go
│ │ ├── reply_test_examples
│ │ │ ├── 1.md
│ │ │ ├── 10.md
│ │ │ ├── 2.md
│ │ │ ├── 3.md
│ │ │ ├── 4.md
│ │ │ ├── 5.md
│ │ │ ├── 6.md
│ │ │ ├── 7.md
│ │ │ ├── 8.md
│ │ │ └── 9.md
│ │ ├── safe_map.go
│ │ └── trial.go
│ ├── utils
│ │ ├── whitespace.go
│ │ ├── whitespace_test.go
│ │ └── xml.go
│ └── version.txt
├── shared
│ ├── ai_models_api_keys.go
│ ├── ai_models_available.go
│ ├── ai_models_compatibility.go
│ ├── ai_models_config.go
│ ├── ai_models_data_models.go
│ ├── ai_models_errors.go
│ ├── ai_models_large_context.go
│ ├── ai_models_openrouter.go
│ ├── ai_models_packs.go
│ ├── ai_models_providers.go
│ ├── ai_models_roles.go
│ ├── auth.go
│ ├── context.go
│ ├── convo_message.go
│ ├── data_models.go
│ ├── email.go
│ ├── file_maps.go
│ ├── go.mod
│ ├── go.sum
│ ├── images.go
│ ├── plan_config.go
│ ├── plan_model_settings.go
│ ├── plan_result.go
│ ├── plan_result_exec_history.go
│ ├── plan_result_pending_summary.go
│ ├── plan_result_replacements.go
│ ├── plan_status.go
│ ├── rbac.go
│ ├── req_res.go
│ ├── stream.go
│ ├── streamed_change.go
│ ├── syntax.go
│ ├── tokens.go
│ ├── tygo.yaml
│ └── utils.go
└── start_local.sh
├── docs
├── .gitignore
├── README.md
├── babel.config.js
├── blog
│ ├── 2019-05-28-first-blog-post.md
│ ├── 2019-05-29-long-blog-post.md
│ ├── 2021-08-01-mdx-blog-post.mdx
│ ├── 2021-08-26-welcome
│ │ ├── docusaurus-plushie-banner.jpeg
│ │ └── index.md
│ ├── authors.yml
│ └── tags.yml
├── docs
│ ├── cli-reference.md
│ ├── core-concepts
│ │ ├── _category_.json
│ │ ├── autonomy.md
│ │ ├── background-tasks.md
│ │ ├── branches.md
│ │ ├── configuration.md
│ │ ├── context-management.md
│ │ ├── conversations.md
│ │ ├── execution-and-debugging.md
│ │ ├── orgs.md
│ │ ├── plans.md
│ │ ├── prompts.md
│ │ ├── reviewing-changes.md
│ │ └── version-control.md
│ ├── development.md
│ ├── environment-variables.md
│ ├── hosting
│ │ ├── _category_.json
│ │ ├── cloud.md
│ │ └── self-hosting
│ │ │ ├── _category_.json
│ │ │ ├── advanced-self-hosting.md
│ │ │ └── local-mode-quickstart.md
│ ├── install.md
│ ├── models
│ │ ├── _category_.json
│ │ ├── model-providers.md
│ │ ├── model-settings.md
│ │ └── roles.md
│ ├── quick-start.md
│ ├── repl.md
│ ├── security.md
│ └── upgrading-v1-to-v2.md
├── docusaurus.config.ts
├── package-lock.json
├── package.json
├── sidebars.ts
├── src
│ └── css
│ │ └── custom.css
├── static
│ ├── .nojekyll
│ ├── _redirects
│ └── img
│ │ ├── favicon.ico
│ │ ├── plandex-logo-dark.png
│ │ ├── plandex-logo-light.png
│ │ ├── plandex-logo-thumb.png
│ │ └── plandex-social-preview.png
└── tsconfig.json
├── images
├── plandex-browser-debug-yt.png
├── plandex-intro-vimeo.png
├── plandex-logo-dark-bg.png
├── plandex-logo-dark-v2.png
├── plandex-logo-dark.png
├── plandex-logo-light-v2.png
├── plandex-logo-light.png
├── plandex-logo-thumb.png
├── plandex-v2-yt.png
└── plandex-workflow.png
├── plans
├── invite-commands.txt
├── model-sets-custom-models-crud.txt
├── pdx-file.md
└── race_cond_chatgpt.txt
├── releases
├── cli
│ ├── CHANGELOG.md
│ └── versions
│ │ ├── 0.7.1.md
│ │ ├── 0.7.2.md
│ │ ├── 0.7.3.md
│ │ ├── 0.8.0.md
│ │ ├── 0.8.1.md
│ │ ├── 0.8.2.md
│ │ ├── 0.8.3.md
│ │ ├── 0.9.0.md
│ │ ├── 0.9.1.md
│ │ ├── 1.0.0.md
│ │ ├── 1.1.0.md
│ │ ├── 1.1.1.md
│ │ ├── 1.1.2.md
│ │ ├── 2.0.0.md
│ │ ├── 2.0.1.md
│ │ ├── 2.0.2.md
│ │ ├── 2.0.3.md
│ │ ├── 2.0.4.md
│ │ ├── 2.0.5.md
│ │ ├── 2.0.6.md
│ │ ├── 2.0.7+1.md
│ │ ├── 2.0.7.md
│ │ ├── 2.1.0+1.md
│ │ ├── 2.1.0.md
│ │ ├── 2.1.1.md
│ │ ├── 2.1.2.md
│ │ ├── 2.1.3.md
│ │ ├── 2.1.5.md
│ │ ├── 2.1.6+1.md
│ │ └── 2.1.6.md
├── images
│ └── cli
│ │ ├── 0.9.0
│ │ ├── plandex-archive.gif
│ │ ├── plandex-commit.png
│ │ ├── plandex-diff.gif
│ │ └── plandex-models.gif
│ │ ├── 1.1.0
│ │ ├── plandex-images.gif
│ │ └── plandex-reject.gif
│ │ └── 1.1.1
│ │ └── claude-3-5-sonnet.gif
└── server
│ ├── CHANGELOG.md
│ └── versions
│ ├── 0.7.0.md
│ ├── 0.7.1.md
│ ├── 0.8.0.md
│ ├── 0.8.1.md
│ ├── 0.8.2.md
│ ├── 0.8.3.md
│ ├── 0.8.4.md
│ ├── 0.9.0.md
│ ├── 0.9.1.md
│ ├── 1.0.0.md
│ ├── 1.0.1.md
│ ├── 1.1.0.md
│ ├── 1.1.1.md
│ ├── 2.0.0+1.md
│ ├── 2.0.0+2.md
│ ├── 2.0.0.md
│ ├── 2.0.2.md
│ ├── 2.0.3.md
│ ├── 2.0.4.md
│ ├── 2.0.5.md
│ ├── 2.0.6.md
│ ├── 2.1.0+1.md
│ ├── 2.1.0.md
│ ├── 2.1.1+1.md
│ ├── 2.1.1.md
│ ├── 2.1.2.md
│ ├── 2.1.3.md
│ ├── 2.1.4.md
│ ├── 2.1.5.md
│ ├── 2.1.6+1.md
│ └── 2.1.6.md
├── scripts
└── merge_from_reflog.sh
└── test
├── _test_apply.sh
├── error-test.html
├── evals
└── promptfoo-poc
│ ├── README.md
│ ├── build
│ ├── assets
│ │ ├── build
│ │ │ ├── changes.md
│ │ │ └── post_build.go
│ │ └── shared
│ │ │ └── pre_build.go
│ ├── build.config.properties
│ ├── build.parameters.json
│ ├── build.prompt.txt
│ ├── build.provider.yml
│ ├── promptfooconfig.yaml
│ └── tests
│ │ └── build.test.yml
│ ├── evals.md
│ ├── fix
│ ├── assets
│ │ ├── removal
│ │ │ ├── changes.md
│ │ │ ├── post_build.go
│ │ │ └── problems.txt
│ │ └── shared
│ │ │ └── pre_build.go
│ ├── fix.config.properties
│ ├── fix.parameters.json
│ ├── fix.prompt.txt
│ ├── fix.provider.yml
│ ├── promptfooconfig.yaml
│ └── tests
│ │ └── fix.test.yml
│ ├── templates
│ └── provider.template.yml
│ └── verify
│ ├── assets
│ ├── removal
│ │ ├── changes.md
│ │ ├── diff.txt
│ │ └── post_build.go
│ ├── shared
│ │ └── pre_build.go
│ └── valid
│ │ ├── changes.md
│ │ ├── diff.txt
│ │ └── post_build.go
│ ├── promptfooconfig.yaml
│ ├── tests
│ ├── removal.test.yml
│ └── validate.test.yml
│ ├── verify.config.properties
│ ├── verify.parameters.json
│ ├── verify.prompt.txt
│ └── verify.provider.yml
├── plan_deletion_test.sh
├── pong
├── .gitignore
├── Makefile
├── README.md
├── ball.c
├── ball.h
├── install_dependencies.sh
├── main.c
├── paddle.c
├── paddle.h
├── prompt.txt
├── render.c
└── render.h
├── project
└── react-redux-foobar
│ ├── action.ts
│ ├── component.ts
│ ├── lib
│ ├── constants.ts
│ └── utils.ts
│ ├── package.json
│ ├── reducer.ts
│ ├── tests
│ ├── action.test.ts
│ └── component.test.ts
│ └── tsconfig.json
└── test_prompts
├── aws-infra.txt
├── pong.txt
├── robust-logging.txt
├── stripe-plan.txt
└── tic-tac-toe.txt
/.dockerignore:
--------------------------------------------------------------------------------
1 | dist-server/
2 | cli/
3 | node_modules/
4 | plandex-server
5 | plandex-cloud
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.sh text eol=lf
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .plandex/
2 | .plandex-dev/
3 | .plandex-v2/
4 | .plandex-dev-v2/
5 | .envkey
6 | .env
7 | .env.*
8 | plandex
9 | plandex-dev
10 | plandex-server
11 | *.exe
12 | node_modules/
13 | /tools/
14 | /static/
15 | /infra/
16 | /payments-dashboard/
17 | .DS_Store
18 | .goreleaser.yml
19 | dist/
20 |
21 | .aider.*
22 | *.code-workspace
23 |
24 | __pycache__/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 PlandexAI Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/.dockerignore:
--------------------------------------------------------------------------------
1 | cli/
2 | plandex-server
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 |
--------------------------------------------------------------------------------
/app/clear_local.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Get the absolute path to the script's directory, regardless of where it's run from
4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
5 |
6 | # Change to the app directory if we're not already there
7 | cd "$SCRIPT_DIR"
8 |
9 | echo "WARNING: This will delete all Plandex server data and reset the database."
10 | echo "This action cannot be undone."
11 | read -p "Are you sure you want to continue? (y/N) " -n 1 -r
12 | echo
13 | if [[ ! $REPLY =~ ^[Yy]$ ]]
14 | then
15 | echo "Reset cancelled."
16 | exit 1
17 | fi
18 |
19 | echo "Resetting local mode..."
20 | echo "Stopping containers and removing volumes..."
21 |
22 | # Stop containers and remove volumes
23 | docker compose down -v
24 |
25 | echo "Database and data directories cleared. Server stopped."
--------------------------------------------------------------------------------
/app/cli/api/errors.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 | "plandex-cli/auth"
8 | "plandex-cli/term"
9 | "strings"
10 |
11 | shared "plandex-shared"
12 | )
13 |
14 | func HandleApiError(r *http.Response, errBody []byte) *shared.ApiError {
15 | // Check if the response is JSON
16 | if r.Header.Get("Content-Type") != "application/json" {
17 | return &shared.ApiError{
18 | Type: shared.ApiErrorTypeOther,
19 | Status: r.StatusCode,
20 | Msg: strings.TrimSpace(string(errBody)),
21 | }
22 | }
23 |
24 | var apiError shared.ApiError
25 | if err := json.Unmarshal(errBody, &apiError); err != nil {
26 | log.Printf("Error unmarshalling JSON: %v\n", err)
27 | return &shared.ApiError{
28 | Type: shared.ApiErrorTypeOther,
29 | Status: r.StatusCode,
30 | Msg: strings.TrimSpace(string(errBody)),
31 | }
32 | }
33 |
34 | // return error if token/auth refresh is needed
35 | if apiError.Type == shared.ApiErrorTypeInvalidToken || apiError.Type == shared.ApiErrorTypeAuthOutdated {
36 | return &apiError
37 | }
38 |
39 | term.HandleApiError(&apiError)
40 |
41 | return &apiError
42 | }
43 |
44 | func refreshAuthIfNeeded(apiErr *shared.ApiError) (bool, *shared.ApiError) {
45 | if apiErr.Type == shared.ApiErrorTypeInvalidToken {
46 | err := auth.RefreshInvalidToken()
47 | if err != nil {
48 | return false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: "error refreshing invalid token"}
49 | }
50 | return true, nil
51 | } else if apiErr.Type == shared.ApiErrorTypeAuthOutdated {
52 | err := auth.RefreshAuth()
53 | if err != nil {
54 | return false, &shared.ApiError{Type: shared.ApiErrorTypeOther, Msg: "error refreshing auth"}
55 | }
56 |
57 | return true, nil
58 | }
59 |
60 | return false, apiErr
61 | }
62 |
--------------------------------------------------------------------------------
/app/cli/auth/api.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "plandex-cli/types"
9 |
10 | shared "plandex-shared"
11 | )
12 |
13 | var apiClient types.ApiClient
14 |
15 | func SetApiClient(client types.ApiClient) {
16 | apiClient = client
17 | }
18 |
19 | func SetAuthHeader(req *http.Request) error {
20 | if Current == nil {
21 | return fmt.Errorf("error setting auth header: auth not loaded")
22 | }
23 | hash := Current.ToHash()
24 |
25 | authHeader := shared.AuthHeader{
26 | Token: Current.Token,
27 | OrgId: Current.OrgId,
28 | Hash: hash,
29 | }
30 |
31 | bytes, err := json.Marshal(authHeader)
32 |
33 | if err != nil {
34 | return fmt.Errorf("error marshalling auth header: %v", err)
35 | }
36 |
37 | // base64 encode
38 | token := base64.URLEncoding.EncodeToString(bytes)
39 |
40 | req.Header.Set("Authorization", "Bearer "+token)
41 |
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/app/cli/cmd/billing.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "plandex-cli/auth"
5 | "plandex-cli/term"
6 | "plandex-cli/ui"
7 |
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | var billingCmd = &cobra.Command{
12 | Use: "billing",
13 | Short: "Open the billing page in the browser",
14 | Run: billing,
15 | }
16 |
17 | func init() {
18 | RootCmd.AddCommand(billingCmd)
19 | }
20 |
21 | func billing(cmd *cobra.Command, args []string) {
22 | auth.MustResolveAuthWithOrg()
23 |
24 | if !auth.Current.IsCloud {
25 | term.OutputErrorAndExit("This command is only available for Plandex Cloud accounts.")
26 | }
27 |
28 | ui.OpenAuthenticatedURL("Opening billing page in your default browser...", "/settings/billing")
29 | }
30 |
--------------------------------------------------------------------------------
/app/cli/cmd/chat.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/auth"
6 | "plandex-cli/lib"
7 | "plandex-cli/plan_exec"
8 | "plandex-cli/types"
9 |
10 | shared "plandex-shared"
11 |
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | var chatCmd = &cobra.Command{
16 | Use: "chat [prompt]",
17 | Aliases: []string{"c"},
18 | Short: "Chat without making changes",
19 | // Long: ``,
20 | Args: cobra.RangeArgs(0, 1),
21 | Run: doChat,
22 | }
23 |
24 | func init() {
25 | RootCmd.AddCommand(chatCmd)
26 |
27 | initExecFlags(chatCmd, initExecFlagsParams{
28 | omitNoBuild: true,
29 | omitStop: true,
30 | omitBg: true,
31 | omitApply: true,
32 | omitExec: true,
33 | omitSmartContext: true,
34 | })
35 |
36 | }
37 |
38 | func doChat(cmd *cobra.Command, args []string) {
39 | auth.MustResolveAuthWithOrg()
40 | lib.MustResolveProject()
41 | mustSetPlanExecFlags(cmd)
42 |
43 | var apiKeys map[string]string
44 | if !auth.Current.IntegratedModelsMode {
45 | apiKeys = lib.MustVerifyApiKeys()
46 | }
47 |
48 | prompt := getTellPrompt(args)
49 |
50 | if prompt == "" {
51 | fmt.Println("🤷♂️ No prompt to send")
52 | return
53 | }
54 |
55 | plan_exec.TellPlan(plan_exec.ExecParams{
56 | CurrentPlanId: lib.CurrentPlanId,
57 | CurrentBranch: lib.CurrentBranch,
58 | ApiKeys: apiKeys,
59 | CheckOutdatedContext: func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error) {
60 | auto := autoConfirm || tellAutoApply || tellAutoContext
61 | return lib.CheckOutdatedContextWithOutput(auto, auto, maybeContexts, projectPaths)
62 | },
63 | }, prompt, types.TellFlags{
64 | IsChatOnly: true,
65 | AutoContext: tellAutoContext,
66 | })
67 | }
68 |
--------------------------------------------------------------------------------
/app/cli/cmd/clear.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/api"
6 | "plandex-cli/auth"
7 | "plandex-cli/lib"
8 | "plandex-cli/term"
9 |
10 | shared "plandex-shared"
11 |
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | var clearCmd = &cobra.Command{
16 | Use: "clear",
17 | Short: "Clear all context",
18 | Long: `Clear all context.`,
19 | Run: clearAllContext,
20 | }
21 |
22 | func clearAllContext(cmd *cobra.Command, args []string) {
23 | auth.MustResolveAuthWithOrg()
24 | lib.MustResolveProject()
25 |
26 | if lib.CurrentPlanId == "" {
27 | term.OutputNoCurrentPlanErrorAndExit()
28 | }
29 |
30 | term.StartSpinner("")
31 | contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
32 | term.StopSpinner()
33 |
34 | if err != nil {
35 | term.OutputErrorAndExit("Error retrieving context: %v", err)
36 | }
37 |
38 | deleteIds := map[string]bool{}
39 |
40 | for _, context := range contexts {
41 | deleteIds[context.Id] = true
42 | }
43 |
44 | if len(deleteIds) > 0 {
45 | res, err := api.Client.DeleteContext(lib.CurrentPlanId, lib.CurrentBranch, shared.DeleteContextRequest{
46 | Ids: deleteIds,
47 | })
48 |
49 | if err != nil {
50 | term.OutputErrorAndExit("Error deleting context: %v", err)
51 | }
52 |
53 | fmt.Println("✅ " + res.Msg)
54 | } else {
55 | fmt.Println("🤷♂️ No context removed")
56 | }
57 |
58 | }
59 |
60 | func init() {
61 | RootCmd.AddCommand(clearCmd)
62 | }
63 |
--------------------------------------------------------------------------------
/app/cli/cmd/config.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/api"
6 | "plandex-cli/auth"
7 | "plandex-cli/lib"
8 | "plandex-cli/term"
9 |
10 | "github.com/fatih/color"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | func init() {
15 | RootCmd.AddCommand(configCmd)
16 | configCmd.AddCommand(defaultConfigCmd)
17 | }
18 |
19 | var configCmd = &cobra.Command{
20 | Use: "config",
21 | Short: "Show plan config",
22 | Run: config,
23 | }
24 |
25 | var defaultConfigCmd = &cobra.Command{
26 | Use: "default",
27 | Short: "Show default config for new plans",
28 | Run: defaultConfig,
29 | }
30 |
31 | func config(cmd *cobra.Command, args []string) {
32 | auth.MustResolveAuthWithOrg()
33 | lib.MustResolveProject()
34 |
35 | if lib.CurrentPlanId == "" {
36 | term.OutputNoCurrentPlanErrorAndExit()
37 | }
38 |
39 | term.StartSpinner("")
40 |
41 | config, apiErr := api.Client.GetPlanConfig(lib.CurrentPlanId)
42 | if apiErr != nil {
43 | term.StopSpinner()
44 | term.OutputErrorAndExit("Error getting config: %v", apiErr.Msg)
45 | return
46 | }
47 |
48 | term.StopSpinner()
49 |
50 | color.New(color.Bold, term.ColorHiCyan).Println("⚙️ Plan Config")
51 | lib.ShowPlanConfig(config, "")
52 | fmt.Println()
53 |
54 | term.PrintCmds("", "set-config", "config default", "set-config default")
55 | }
56 |
57 | func defaultConfig(cmd *cobra.Command, args []string) {
58 | auth.MustResolveAuth(false)
59 |
60 | term.StartSpinner("")
61 | config, err := api.Client.GetDefaultPlanConfig()
62 | term.StopSpinner()
63 |
64 | if err != nil {
65 | term.OutputErrorAndExit("Error getting default config: %v", err)
66 | return
67 | }
68 |
69 | color.New(color.Bold, term.ColorHiCyan).Println("⚙️ Default Config")
70 | lib.ShowPlanConfig(config, "")
71 | fmt.Println()
72 |
73 | term.PrintCmds("", "set-config default", "config", "set-config")
74 | }
75 |
--------------------------------------------------------------------------------
/app/cli/cmd/connect.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "plandex-cli/api"
7 | "plandex-cli/auth"
8 | "plandex-cli/lib"
9 | "plandex-cli/stream"
10 | streamtui "plandex-cli/stream_tui"
11 | "plandex-cli/term"
12 |
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | var connectCmd = &cobra.Command{
17 | Use: "connect [stream-id-or-plan] [branch]",
18 | Aliases: []string{"conn"},
19 | Short: "Connect to an active stream",
20 | // Long: ``,
21 | Args: cobra.MaximumNArgs(2),
22 | Run: connect,
23 | }
24 |
25 | func init() {
26 | RootCmd.AddCommand(connectCmd)
27 |
28 | }
29 |
30 | func connect(cmd *cobra.Command, args []string) {
31 | auth.MustResolveAuthWithOrg()
32 | lib.MustResolveProject()
33 |
34 | if lib.CurrentPlanId == "" {
35 | term.OutputNoCurrentPlanErrorAndExit()
36 | }
37 |
38 | planId, branch, shouldContinue := lib.SelectActiveStream(args)
39 |
40 | if !shouldContinue {
41 | return
42 | }
43 |
44 | term.StartSpinner("")
45 | apiErr := api.Client.ConnectPlan(planId, branch, stream.OnStreamPlan)
46 | term.StopSpinner()
47 |
48 | if apiErr != nil {
49 | term.OutputErrorAndExit("Error connecting to stream: %v", apiErr)
50 | }
51 |
52 | go func() {
53 | err := streamtui.StartStreamUI("", false, true)
54 |
55 | if err != nil {
56 | term.OutputErrorAndExit("Error starting stream UI", err)
57 | }
58 |
59 | fmt.Println()
60 | term.PrintCmds("", "diff", "diff --ui", "apply", "reject", "log")
61 |
62 | os.Exit(0)
63 | }()
64 |
65 | // Wait for the stream to finish
66 | select {}
67 | }
68 |
--------------------------------------------------------------------------------
/app/cli/cmd/context_show.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "plandex-cli/api"
7 | "plandex-cli/auth"
8 | "plandex-cli/lib"
9 | "strconv"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | func init() {
15 | RootCmd.AddCommand(contextShowCmd)
16 | }
17 |
18 | var contextShowCmd = &cobra.Command{
19 | Use: "show [name-or-index]",
20 | Short: "Show the body of a context by name or list index",
21 | Args: cobra.ExactArgs(1),
22 | RunE: func(cmd *cobra.Command, args []string) error {
23 | auth.MustResolveAuthWithOrg()
24 | lib.MustResolveProject()
25 |
26 | nameOrIndex := args[0]
27 |
28 | // Get list of contexts first
29 | contexts, err := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
30 | if err != nil {
31 | log.Printf("Error listing contexts: %v\n", err)
32 | return fmt.Errorf("error listing contexts: %v", err)
33 | }
34 |
35 | var contextId string
36 |
37 | // Try parsing as index first
38 | if idx, err := strconv.Atoi(nameOrIndex); err == nil {
39 | // Convert to 0-based index
40 | idx--
41 | if idx < 0 || idx >= len(contexts) {
42 | return fmt.Errorf("invalid context index: %s", nameOrIndex)
43 | }
44 | contextId = contexts[idx].Id
45 | } else {
46 | // Try finding by name
47 | found := false
48 | for _, ctx := range contexts {
49 | if ctx.Name == nameOrIndex || ctx.FilePath == nameOrIndex {
50 | contextId = ctx.Id
51 | found = true
52 | break
53 | }
54 | }
55 | if !found {
56 | return fmt.Errorf("no context found with name: %s", nameOrIndex)
57 | }
58 | }
59 |
60 | res, apiErr := api.Client.GetContextBody(lib.CurrentPlanId, lib.CurrentBranch, contextId)
61 | if apiErr != nil {
62 | log.Printf("Error getting context body: %v\n", apiErr)
63 | return fmt.Errorf("error getting context body: %v", apiErr)
64 | }
65 |
66 | fmt.Println(res.Body)
67 | return nil
68 | },
69 | }
70 |
--------------------------------------------------------------------------------
/app/cli/cmd/current.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/api"
6 | "plandex-cli/auth"
7 | "plandex-cli/lib"
8 | "plandex-cli/term"
9 |
10 | shared "plandex-shared"
11 |
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | var currentCmd = &cobra.Command{
16 | Use: "current",
17 | Aliases: []string{"cu"},
18 | Short: "Get the current plan",
19 | Run: current,
20 | }
21 |
22 | func init() {
23 | RootCmd.AddCommand(currentCmd)
24 | }
25 |
26 | func current(cmd *cobra.Command, args []string) {
27 | auth.MustResolveAuthWithOrg()
28 | lib.MaybeResolveProject()
29 |
30 | if lib.CurrentPlanId == "" {
31 | term.OutputNoCurrentPlanErrorAndExit()
32 | }
33 |
34 | term.StartSpinner("")
35 | plan, err := api.Client.GetPlan(lib.CurrentPlanId)
36 | term.StopSpinner()
37 |
38 | if err != nil {
39 | term.OutputErrorAndExit("Error getting plan: %v", err)
40 | return
41 | }
42 |
43 | currentBranchesByPlanId, err := api.Client.GetCurrentBranchByPlanId(lib.CurrentProjectId, shared.GetCurrentBranchByPlanIdRequest{
44 | CurrentBranchByPlanId: map[string]string{
45 | lib.CurrentPlanId: lib.CurrentBranch,
46 | },
47 | })
48 |
49 | if err != nil {
50 | term.OutputErrorAndExit("Error getting current branches: %v", err)
51 | }
52 |
53 | table := lib.GetCurrentPlanTable(plan, currentBranchesByPlanId, nil)
54 | fmt.Println(table)
55 |
56 | term.PrintCmds("", "tell", "ls", "plans")
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/app/cli/cmd/rename.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/api"
6 | "plandex-cli/auth"
7 | "plandex-cli/lib"
8 | "plandex-cli/term"
9 |
10 | "github.com/fatih/color"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var renameCmd = &cobra.Command{
15 | Use: "rename [new-name]",
16 | Short: "Rename the current plan",
17 | Args: cobra.MaximumNArgs(1),
18 | Run: rename,
19 | }
20 |
21 | func init() {
22 | RootCmd.AddCommand(renameCmd)
23 | }
24 |
25 | func rename(cmd *cobra.Command, args []string) {
26 | auth.MustResolveAuthWithOrg()
27 | lib.MustResolveProject()
28 |
29 | if lib.CurrentPlanId == "" {
30 | term.OutputNoCurrentPlanErrorAndExit()
31 | }
32 |
33 | var newName string
34 | if len(args) > 0 {
35 | newName = args[0]
36 | } else {
37 | var err error
38 | newName, err = term.GetRequiredUserStringInput("New name:")
39 | if err != nil {
40 | term.OutputErrorAndExit("Error reading new name: %v", err)
41 | }
42 | }
43 |
44 | if newName == "" {
45 | fmt.Println("🤷♂️ No new name provided")
46 | return
47 | }
48 |
49 | term.StartSpinner("")
50 | err := api.Client.RenamePlan(lib.CurrentPlanId, newName)
51 | term.StopSpinner()
52 |
53 | if err != nil {
54 | term.OutputErrorAndExit("Error renaming plan: %v", err)
55 | }
56 |
57 | fmt.Printf("✅ Plan renamed to %s\n", color.New(color.Bold, term.ColorHiGreen).Sprint(newName))
58 | }
59 |
--------------------------------------------------------------------------------
/app/cli/cmd/sign_in.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "plandex-cli/auth"
5 | "plandex-cli/term"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var pin string
11 |
12 | var signInCmd = &cobra.Command{
13 | Use: "sign-in",
14 | Short: "Sign in to a Plandex account",
15 | Args: cobra.NoArgs,
16 | Run: signIn,
17 | }
18 |
19 | func init() {
20 | RootCmd.AddCommand(signInCmd)
21 |
22 | signInCmd.Flags().StringVar(&pin, "pin", "", "Sign in with a pin from the Plandex Cloud web UI")
23 | }
24 |
25 | func signIn(cmd *cobra.Command, args []string) {
26 | if pin != "" {
27 | err := auth.SignInWithCode(pin, "")
28 |
29 | if err != nil {
30 | term.OutputErrorAndExit("Error signing in: %v", err)
31 | }
32 |
33 | return
34 | }
35 |
36 | err := auth.SelectOrSignInOrCreate()
37 |
38 | if err != nil {
39 | term.OutputErrorAndExit("Error signing in: %v", err)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/cli/cmd/stop.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "plandex-cli/api"
7 | "plandex-cli/auth"
8 | "plandex-cli/lib"
9 | "plandex-cli/term"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var stopCmd = &cobra.Command{
15 | Use: "stop [stream-id-or-plan] [branch]",
16 | Short: "Connect to an active stream",
17 | // Long: ``,
18 | Args: cobra.MaximumNArgs(2),
19 | Run: stop,
20 | }
21 |
22 | func init() {
23 | RootCmd.AddCommand(stopCmd)
24 | }
25 |
26 | func stop(cmd *cobra.Command, args []string) {
27 | auth.MustResolveAuthWithOrg()
28 | lib.MustResolveProject()
29 |
30 | if lib.CurrentPlanId == "" {
31 | term.OutputNoCurrentPlanErrorAndExit()
32 | }
33 |
34 | planId, branch, shouldContinue := lib.SelectActiveStream(args)
35 |
36 | if !shouldContinue {
37 | return
38 | }
39 |
40 | term.StartSpinner("")
41 | apiErr := api.Client.StopPlan(context.Background(), planId, branch)
42 | term.StopSpinner()
43 |
44 | if apiErr != nil {
45 | term.OutputErrorAndExit("Error stopping stream: %v", apiErr.Msg)
46 | }
47 |
48 | fmt.Println("✅ Plan stream stopped")
49 |
50 | fmt.Println()
51 | term.PrintCmds("", "convo", "log")
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/app/cli/cmd/summary.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/api"
6 | "plandex-cli/auth"
7 | "plandex-cli/lib"
8 | "plandex-cli/term"
9 |
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var summaryPlain bool
14 |
15 | var statusCmd = &cobra.Command{
16 | Use: "summary",
17 | Short: "Show the latest summary of the current plan",
18 | Run: status,
19 | }
20 |
21 | func init() {
22 | RootCmd.AddCommand(statusCmd)
23 |
24 | statusCmd.Flags().BoolVarP(&summaryPlain, "plain", "p", false, "Output summary in plain text with no ANSI codes")
25 | }
26 |
27 | func status(cmd *cobra.Command, args []string) {
28 | auth.MustResolveAuthWithOrg()
29 | lib.MustResolveProject()
30 |
31 | term.StartSpinner("")
32 | status, apiErr := api.Client.GetPlanStatus(lib.CurrentPlanId, lib.CurrentBranch)
33 | term.StopSpinner()
34 |
35 | if apiErr != nil {
36 | term.OutputErrorAndExit("Error loading conversation: %v", apiErr.Msg)
37 | }
38 |
39 | if status == "" {
40 | fmt.Println("🤷♂️ No summary available")
41 | }
42 |
43 | if summaryPlain {
44 | fmt.Println(status)
45 | return
46 | }
47 |
48 | md, err := term.GetMarkdown(status)
49 |
50 | if err != nil {
51 | term.OutputErrorAndExit("Error formatting markdown: %v", err)
52 | }
53 |
54 | fmt.Println(md)
55 | }
56 |
--------------------------------------------------------------------------------
/app/cli/cmd/update.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/api"
6 | "plandex-cli/auth"
7 | "plandex-cli/fs"
8 | "plandex-cli/lib"
9 | "plandex-cli/term"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var updateCmd = &cobra.Command{
15 | Use: "update ",
16 | Aliases: []string{"u"},
17 | Short: "Update outdated context",
18 | Args: cobra.MaximumNArgs(1),
19 | Run: update,
20 | }
21 |
22 | func init() {
23 | RootCmd.AddCommand(updateCmd)
24 |
25 | }
26 |
27 | func update(cmd *cobra.Command, args []string) {
28 | auth.MustResolveAuthWithOrg()
29 | lib.MustResolveProject()
30 |
31 | term.StartSpinner("")
32 |
33 | contexts, apiErr := api.Client.ListContext(lib.CurrentPlanId, lib.CurrentBranch)
34 |
35 | if apiErr != nil {
36 | term.StopSpinner()
37 | term.OutputErrorAndExit("failed to list context: %s", apiErr)
38 | }
39 |
40 | paths, err := fs.GetProjectPaths(fs.ProjectRoot)
41 |
42 | if err != nil {
43 | term.OutputErrorAndExit("error getting project paths: %v", err)
44 | }
45 |
46 | outdated, err := lib.CheckOutdatedContext(contexts, paths)
47 |
48 | if err != nil {
49 | term.StopSpinner()
50 | term.OutputErrorAndExit("failed to check outdated context: %s", err)
51 | }
52 |
53 | if len(outdated.UpdatedContexts) == 0 {
54 | term.StopSpinner()
55 | fmt.Println("✅ Context is up to date")
56 | return
57 | }
58 |
59 | lib.UpdateContextWithOutput(lib.UpdateContextParams{
60 | Contexts: contexts,
61 | OutdatedRes: *outdated,
62 | ReqFn: outdated.ReqFn,
63 | })
64 | }
65 |
--------------------------------------------------------------------------------
/app/cli/cmd/version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "plandex-cli/version"
7 |
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | var versionCmd = &cobra.Command{
12 | Use: "version",
13 | Short: "Print the version number of Plandex",
14 | Long: `All software has versions. This is Plandex's`,
15 | Run: func(cmd *cobra.Command, args []string) {
16 | fmt.Println(version.Version)
17 | },
18 | }
19 |
20 | func init() {
21 | RootCmd.AddCommand(versionCmd)
22 | }
23 |
--------------------------------------------------------------------------------
/app/cli/dev.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | OUT="${PLANDEX_DEV_CLI_OUT_DIR:-/usr/local/bin}"
4 | NAME="${PLANDEX_DEV_CLI_NAME:-plandex-dev}"
5 | ALIAS="${PLANDEX_DEV_CLI_ALIAS:-pdxd}"
6 |
7 | # Double quote to prevent globbing and word splitting.
8 | go build -o "$NAME" &&
9 | rm -f "$OUT"/"$NAME" &&
10 | cp "$NAME" "$OUT"/"$NAME" &&
11 | ln -sf "$OUT"/"$NAME" "$OUT"/"$ALIAS" &&
12 | echo built "$NAME" cli and added "$ALIAS" alias to "$OUT"
13 |
--------------------------------------------------------------------------------
/app/cli/format/file.go:
--------------------------------------------------------------------------------
1 | package format
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 | )
7 |
8 | func GetFileNameWithoutExt(path string) string {
9 | name := path[:len(path)-len(filepath.Ext(path))]
10 |
11 | name = strings.ToLower(name)
12 | name = strings.ReplaceAll(name, "_", "-")
13 | name = strings.ReplaceAll(name, " ", "-")
14 | name = strings.ReplaceAll(name, ".", "-")
15 | name = strings.ReplaceAll(name, "/", "-")
16 | name = strings.ReplaceAll(name, "\\", "-")
17 | name = strings.ReplaceAll(name, "'", "")
18 | name = strings.ReplaceAll(name, "`", "")
19 | name = strings.ReplaceAll(name, "\"", "")
20 |
21 | return name
22 | }
23 |
--------------------------------------------------------------------------------
/app/cli/lib/apply_cgroup_other.go:
--------------------------------------------------------------------------------
1 | //go:build !linux
2 | // +build !linux
3 |
4 | package lib
5 |
6 | import "os/exec"
7 |
8 | func MaybeIsolateCgroup(cmd *exec.Cmd) (deleteFn func()) {
9 | return func() {}
10 | }
11 |
--------------------------------------------------------------------------------
/app/cli/lib/apply_proc.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "os/exec"
5 | "syscall"
6 | )
7 |
8 | func SetPlatformSpecificAttrs(cmd *exec.Cmd) {
9 | cmd.SysProcAttr = &syscall.SysProcAttr{
10 | Setpgid: true,
11 | }
12 | }
13 |
14 | func KillProcessGroup(cmd *exec.Cmd, signal syscall.Signal) error {
15 | return syscall.Kill(-cmd.Process.Pid, signal)
16 | }
17 |
--------------------------------------------------------------------------------
/app/cli/lib/build.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import shared "plandex-shared"
4 |
5 | var buildPlanInlineFn func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error)
6 |
7 | func SetBuildPlanInlineFn(fn func(autoConfirm bool, maybeContexts []*shared.Context) (bool, error)) {
8 | buildPlanInlineFn = fn
9 | }
10 |
--------------------------------------------------------------------------------
/app/cli/lib/context_conflict.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "plandex-cli/api"
7 | "plandex-cli/term"
8 |
9 | "github.com/fatih/color"
10 | )
11 |
12 | func checkContextConflicts(filesByPath map[string]string) (bool, error) {
13 | // log.Println("Checking for context conflicts.")
14 | // log.Println(spew.Sdump(filesByPath))
15 |
16 | currentPlan, err := api.Client.GetCurrentPlanState(CurrentPlanId, CurrentBranch)
17 |
18 | if err != nil {
19 | return false, fmt.Errorf("error getting current plan state: %v", err)
20 | }
21 |
22 | conflictedPaths := currentPlan.PlanResult.FileResultsByPath.ConflictedPaths(filesByPath)
23 |
24 | // log.Println("Conflicted paths:", conflictedPaths)
25 |
26 | if len(conflictedPaths) > 0 {
27 | term.StopSpinner()
28 | color.New(color.Bold, term.ColorHiYellow).Println("⚠️ Some updates conflict with pending changes:")
29 | for path := range conflictedPaths {
30 | fmt.Println("📄 " + path)
31 | }
32 |
33 | fmt.Println()
34 |
35 | res, err := term.ConfirmYesNo("Update context and rebuild changes?")
36 |
37 | if err != nil {
38 | return false, fmt.Errorf("error confirming update and rebuild: %v", err)
39 | }
40 |
41 | if !res {
42 | fmt.Println("Context update canceled")
43 | os.Exit(0)
44 | }
45 | }
46 |
47 | return len(conflictedPaths) > 0, nil
48 | }
49 |
--------------------------------------------------------------------------------
/app/cli/lib/context_display.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import shared "plandex-shared"
4 |
5 | func GetContextLabelAndIcon(contextType shared.ContextType) (string, string) {
6 | var icon string
7 | var lbl string
8 | switch contextType {
9 | case shared.ContextFileType:
10 | icon = "📄"
11 | lbl = "file"
12 | case shared.ContextURLType:
13 | icon = "🌎"
14 | lbl = "url"
15 | case shared.ContextDirectoryTreeType:
16 | icon = "🗂 "
17 | lbl = "tree"
18 | case shared.ContextNoteType:
19 | icon = "✏️ "
20 | lbl = "note"
21 | case shared.ContextPipedDataType:
22 | icon = "↔️ "
23 | lbl = "piped"
24 | case shared.ContextImageType:
25 | icon = "🖼️ "
26 | lbl = "image"
27 | case shared.ContextMapType:
28 | icon = "🗺️ "
29 | lbl = "map"
30 | }
31 |
32 | return lbl, icon
33 | }
34 |
35 | func FindContextByIndex(contexts []*shared.Context, index int) *shared.Context {
36 | // Convert to 0-based index
37 | index--
38 | if index < 0 || index >= len(contexts) {
39 | return nil
40 | }
41 | return contexts[index]
42 | }
43 |
44 | func FindContextByName(contexts []*shared.Context, name string) *shared.Context {
45 | for _, ctx := range contexts {
46 | if ctx.Name == name || ctx.FilePath == name {
47 | return ctx
48 | }
49 | }
50 | return nil
51 | }
52 |
--------------------------------------------------------------------------------
/app/cli/lib/context_paths.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "fmt"
5 | "plandex-cli/fs"
6 | "plandex-cli/types"
7 | )
8 |
9 | type ParseInputPathsParams struct {
10 | FileOrDirPaths []string
11 | BaseDir string
12 | ProjectPaths *types.ProjectPaths
13 | LoadParams *types.LoadContextParams
14 | }
15 |
16 | func ParseInputPaths(params ParseInputPathsParams) ([]string, error) {
17 | fileOrDirPaths := params.FileOrDirPaths
18 | baseDir := params.BaseDir
19 | projectPaths := params.ProjectPaths
20 | loadParams := params.LoadParams
21 |
22 | resPaths := []string{}
23 |
24 | for path := range projectPaths.AllPaths {
25 | // see if it's a child of any of the fileOrDirPaths
26 | found := false
27 | for _, p := range fileOrDirPaths {
28 | var err error
29 | found, err = fs.IsSubpathOf(p, path, baseDir)
30 | if err != nil {
31 | return nil, fmt.Errorf("error checking if %s is a subpath of %s: %s", path, p, err)
32 | }
33 | if found {
34 | break
35 | }
36 | }
37 |
38 | if !found {
39 | continue
40 | }
41 |
42 | if projectPaths.AllDirs[path] {
43 | if !(loadParams.Recursive || loadParams.NamesOnly || loadParams.DefsOnly) {
44 | // log.Println("path", path, "info.Name()", info.Name())
45 | return nil, fmt.Errorf("cannot process directory %s: requires --recursive/-r, --tree, or --map flag", path)
46 | }
47 |
48 | // calculate directory depth from base
49 | // depth := strings.Count(path[len(p):], string(filepath.Separator))
50 | // if params.MaxDepth != -1 && depth > params.MaxDepth {
51 | // return filepath.SkipDir
52 | // }
53 |
54 | if loadParams.NamesOnly {
55 | // add directory name to results
56 | resPaths = append(resPaths, path)
57 | }
58 | } else {
59 | // add file path to results
60 | resPaths = append(resPaths, path)
61 | }
62 | }
63 |
64 | return resPaths, nil
65 | }
66 |
--------------------------------------------------------------------------------
/app/cli/lib/plan_config.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "os"
5 | "sort"
6 |
7 | shared "plandex-shared"
8 |
9 | "github.com/olekukonko/tablewriter"
10 | )
11 |
12 | func ShowPlanConfig(config *shared.PlanConfig, key string) {
13 | table := tablewriter.NewWriter(os.Stdout)
14 | table.SetAutoWrapText(true)
15 | table.SetHeader([]string{"Name", "Value", "Description"})
16 |
17 | numVisibleSettings := 0
18 | for k, setting := range shared.ConfigSettingsByKey {
19 | if key != "" && k != key {
20 | continue
21 | }
22 |
23 | if setting.Visible == nil || setting.Visible(config) {
24 | numVisibleSettings++
25 | }
26 | }
27 | numOutput := 0
28 |
29 | sortedSettings := make([][]string, 0, len(shared.ConfigSettingsByKey))
30 |
31 | for k, setting := range shared.ConfigSettingsByKey {
32 | if key != "" && k != key {
33 | continue
34 | }
35 |
36 | if setting.Visible == nil || setting.Visible(config) {
37 | var sortKey string
38 | if setting.SortKey != "" {
39 | sortKey = setting.SortKey
40 | } else {
41 | sortKey = k
42 | }
43 | sortedSettings = append(sortedSettings, []string{sortKey, setting.Name, setting.Getter(config), setting.Desc})
44 | }
45 | }
46 |
47 | sort.Slice(sortedSettings, func(i, j int) bool {
48 | return sortedSettings[i][0] < sortedSettings[j][0]
49 | })
50 |
51 | for _, row := range sortedSettings {
52 | table.Append(row[1:])
53 | numOutput++
54 | if numOutput < numVisibleSettings {
55 | table.Append([]string{"", "", ""})
56 | }
57 | }
58 |
59 | table.Render()
60 | }
61 |
--------------------------------------------------------------------------------
/app/cli/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [
3 | ".",
4 | "../shared"
5 | ],
6 | "ext": "go,mod,sum",
7 | "exec": "./dev.sh"
8 | }
--------------------------------------------------------------------------------
/app/cli/plan.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "draft",
3 | "proposalId": "",
4 | "rootId": "",
5 | "createdAt": "2023-11-04T09:44:29.147Z",
6 | "updatedAt": "2023-11-04T09:44:29.147Z",
7 | "description": null,
8 | "contextTokens": 0,
9 | "convoTokens": 0,
10 | "convoSummarizedTokens": 0
11 | }
--------------------------------------------------------------------------------
/app/cli/plan_exec/params.go:
--------------------------------------------------------------------------------
1 | package plan_exec
2 |
3 | import (
4 | "plandex-cli/types"
5 | shared "plandex-shared"
6 | )
7 |
8 | type ExecParams struct {
9 | CurrentPlanId string
10 | CurrentBranch string
11 | ApiKeys map[string]string
12 | CheckOutdatedContext func(maybeContexts []*shared.Context, projectPaths *types.ProjectPaths) (bool, bool, error)
13 | }
14 |
--------------------------------------------------------------------------------
/app/cli/stream/stream.go:
--------------------------------------------------------------------------------
1 | package stream
2 |
3 | import (
4 | "log"
5 | "plandex-cli/api"
6 | "plandex-cli/lib"
7 | streamtui "plandex-cli/stream_tui"
8 | "plandex-cli/term"
9 | "plandex-cli/types"
10 | "strings"
11 |
12 | shared "plandex-shared"
13 | )
14 |
15 | var OnStreamPlan types.OnStreamPlan
16 |
17 | func init() {
18 | OnStreamPlan = func(params types.OnStreamPlanParams) {
19 | if params.Err != nil {
20 | if strings.Contains(params.Err.Error(), "missing heartbeats") || strings.Contains(strings.ToLower(params.Err.Error()), "eof") {
21 | log.Println("Error in stream:", params.Err)
22 | streamtui.Send(shared.StreamMessage{
23 | Type: shared.StreamMessageError,
24 | Error: &shared.ApiError{
25 | Msg: "Stream error: " + params.Err.Error(),
26 | },
27 | })
28 |
29 | // try to reconnect
30 | term.StartSpinner("Reconnecting...")
31 | apiErr := api.Client.ConnectPlan(lib.CurrentPlanId, lib.CurrentBranch, OnStreamPlan)
32 | term.StopSpinner()
33 |
34 | if apiErr != nil {
35 | log.Println("Error reconnecting to stream:", apiErr)
36 | }
37 | }
38 |
39 | return
40 | }
41 |
42 | if params.Msg.Type == shared.StreamMessageStart {
43 | log.Println("Stream started")
44 | return
45 | }
46 |
47 | // log.Println("Stream message:")
48 | // log.Println(spew.Sdump(*params.Msg))
49 |
50 | streamtui.Send(*params.Msg)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/cli/stream_tui/debouncer.go:
--------------------------------------------------------------------------------
1 | package streamtui
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | // UpdateDebouncer helps prevent visual glitches from rapid updates
9 | type UpdateDebouncer struct {
10 | mu sync.Mutex
11 | lastUpdate time.Time
12 | minInterval time.Duration
13 | pending bool
14 | }
15 |
16 | func NewUpdateDebouncer(minInterval time.Duration) *UpdateDebouncer {
17 | return &UpdateDebouncer{
18 | minInterval: minInterval,
19 | }
20 | }
21 |
22 | // ShouldUpdate returns true if enough time has passed since the last update
23 | func (d *UpdateDebouncer) ShouldUpdate() bool {
24 | d.mu.Lock()
25 | defer d.mu.Unlock()
26 |
27 | now := time.Now()
28 | if now.Sub(d.lastUpdate) < d.minInterval {
29 | d.pending = true
30 | return false
31 | }
32 |
33 | d.lastUpdate = now
34 | d.pending = false
35 | return true
36 | }
37 |
--------------------------------------------------------------------------------
/app/cli/term/color.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import (
4 | "github.com/fatih/color"
5 | "github.com/muesli/termenv"
6 | )
7 |
8 | var IsDarkBg = termenv.HasDarkBackground()
9 |
10 | var ColorHiGreen color.Attribute
11 | var ColorHiMagenta color.Attribute
12 | var ColorHiRed color.Attribute
13 | var ColorHiYellow color.Attribute
14 | var ColorHiCyan color.Attribute
15 | var ColorHiBlue color.Attribute
16 |
17 | func init() {
18 |
19 | if IsDarkBg {
20 | ColorHiGreen = color.FgHiGreen
21 | ColorHiMagenta = color.FgHiMagenta
22 | ColorHiRed = color.FgHiRed
23 | ColorHiYellow = color.FgHiYellow
24 | ColorHiCyan = color.FgHiCyan
25 | ColorHiBlue = color.FgHiBlue
26 | } else {
27 | ColorHiGreen = color.FgGreen
28 | ColorHiMagenta = color.FgMagenta
29 | ColorHiRed = color.FgRed
30 | ColorHiYellow = color.FgYellow
31 | ColorHiCyan = color.FgCyan
32 | ColorHiBlue = color.FgBlue
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/cli/term/os.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | )
8 |
9 | func GetOsDetails() string {
10 | return fmt.Sprintf(
11 | "OS: %s\nArchitecture: %s\nCPUs: %d\nShell: %s",
12 | runtime.GOOS,
13 | runtime.GOARCH,
14 | runtime.NumCPU(),
15 | os.Getenv("SHELL"),
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/cli/term/repl.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import "os"
4 |
5 | var IsRepl = os.Getenv("PLANDEX_REPL") != ""
6 |
7 | func SetIsRepl(value bool) {
8 | IsRepl = value
9 | }
10 |
--------------------------------------------------------------------------------
/app/cli/term/select.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/fatih/color"
8 | "github.com/plandex-ai/survey/v2"
9 | )
10 |
11 | func SelectFromList(msg string, options []string) (string, error) {
12 | var selected string
13 | prompt := &survey.Select{
14 | Message: color.New(ColorHiMagenta, color.Bold).Sprint(msg),
15 | Options: convertToStringSlice(options),
16 | FilterMessage: "",
17 | }
18 | err := survey.AskOne(prompt, &selected)
19 | if err != nil {
20 | if err.Error() == "interrupt" {
21 | os.Exit(0)
22 | }
23 |
24 | return "", err
25 | }
26 |
27 | return selected, nil
28 | }
29 |
30 | func convertToStringSlice[T any](input []T) []string {
31 | var result []string
32 | for _, v := range input {
33 | result = append(result, fmt.Sprint(v))
34 | }
35 | return result
36 | }
37 |
--------------------------------------------------------------------------------
/app/cli/term/spinner.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import (
4 | "sync/atomic"
5 | "time"
6 |
7 | "github.com/briandowns/spinner"
8 | )
9 |
10 | const withMessageMinDuration = 700 * time.Millisecond
11 | const withoutMessageMinDuration = 350 * time.Millisecond
12 |
13 | var s = spinner.New(spinner.CharSets[33], 100*time.Millisecond)
14 | var startedAt time.Time
15 |
16 | var lastMessage string
17 | var active bool
18 | var currentWarningLoop int32
19 |
20 | func StartSpinner(msg string) {
21 | if active {
22 | if msg == lastMessage {
23 | return
24 | }
25 |
26 | s.Stop()
27 | }
28 |
29 | startedAt = time.Now()
30 | s.Prefix = msg + " "
31 | lastMessage = msg
32 | s.Start()
33 | active = true
34 | }
35 |
36 | func StopSpinner() {
37 | elapsed := time.Since(startedAt)
38 |
39 | if lastMessage != "" && elapsed < withMessageMinDuration {
40 | time.Sleep(withMessageMinDuration - elapsed)
41 | } else if elapsed < withoutMessageMinDuration {
42 | time.Sleep(withoutMessageMinDuration - elapsed)
43 | }
44 |
45 | s.Stop()
46 | ClearCurrentLine()
47 |
48 | active = false
49 | }
50 |
51 | func ResumeSpinner() {
52 | if !active {
53 | StartSpinner(lastMessage)
54 | }
55 | }
56 |
57 | func LongSpinnerWithWarning(msg, warning string) {
58 | atomic.AddInt32(¤tWarningLoop, 1)
59 | currentLoop := currentWarningLoop
60 |
61 | StartSpinner(msg)
62 |
63 | var flashWarning func()
64 | flashWarning = func() {
65 | go func() {
66 | time.Sleep(3 * time.Second)
67 | if !active || atomic.LoadInt32(¤tWarningLoop) != currentLoop {
68 | return
69 | }
70 | StartSpinner(warning)
71 |
72 | time.Sleep(2 * time.Second)
73 | if !active || atomic.LoadInt32(¤tWarningLoop) != currentLoop {
74 | return
75 | }
76 | StartSpinner(msg)
77 | flashWarning()
78 | }()
79 | }
80 | flashWarning()
81 | }
82 |
--------------------------------------------------------------------------------
/app/cli/types/apply.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | type ApplyFlags struct {
8 | AutoConfirm bool
9 | AutoCommit bool
10 | NoCommit bool
11 | AutoExec bool
12 | NoExec bool
13 | AutoDebug int
14 | }
15 |
16 | type ApplyRollbackOption string
17 |
18 | const (
19 | ApplyRollbackOptionKeep ApplyRollbackOption = "Apply file changes"
20 | ApplyRollbackOptionRollback ApplyRollbackOption = "Roll back file changes"
21 | )
22 |
23 | type OnApplyExecFailFn func(status int, output string, attempt int, toRollback *ApplyRollbackPlan, onErr OnErrFn, onSuccess func())
24 |
25 | type ApplyReversion struct {
26 | Content string
27 | Mode os.FileMode
28 | }
29 |
30 | type ApplyRollbackPlan struct {
31 | ToRevert map[string]ApplyReversion
32 | ToRemove []string
33 | PreviousProjectPaths *ProjectPaths
34 | }
35 |
36 | func (r *ApplyRollbackPlan) HasChanges() bool {
37 | return len(r.ToRevert) > 0 || len(r.ToRemove) > 0
38 | }
39 |
--------------------------------------------------------------------------------
/app/cli/types/exec.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type TellFlags struct {
4 | TellBg bool
5 | TellStop bool
6 | TellNoBuild bool
7 | IsUserContinue bool
8 | IsUserDebug bool
9 | IsApplyDebug bool
10 | IsChatOnly bool
11 | AutoContext bool
12 | SmartContext bool
13 | ContinuedAfterAction bool
14 | ExecEnabled bool
15 | AutoApply bool
16 | IsImplementationOfChat bool
17 | }
18 | type BuildFlags struct {
19 | BuildBg bool
20 | AutoApply bool
21 | }
22 |
--------------------------------------------------------------------------------
/app/cli/types/fs.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import ignore "github.com/sabhiram/go-gitignore"
4 |
5 | type ProjectPaths struct {
6 | ActivePaths map[string]bool
7 | AllPaths map[string]bool
8 | ActiveDirs map[string]bool
9 | AllDirs map[string]bool
10 | PlandexIgnored *ignore.GitIgnore
11 | IgnoredPaths map[string]string
12 | GitIgnoredDirs map[string]bool
13 | }
14 |
--------------------------------------------------------------------------------
/app/cli/ui/ui.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "plandex-cli/api"
9 | "plandex-cli/term"
10 | "strings"
11 |
12 | "github.com/fatih/color"
13 | "github.com/pkg/browser"
14 |
15 | shared "plandex-shared"
16 | )
17 |
18 | func OpenAuthenticatedURL(msg, path string) {
19 | signInCode, apiErr := api.Client.CreateSignInCode()
20 | if apiErr != nil {
21 | log.Fatalf("Error creating sign in code: %v", apiErr)
22 | }
23 |
24 | apiHost := api.GetApiHost()
25 | appHost := strings.Replace(apiHost, "api-v2.", "app.", 1)
26 |
27 | token := shared.UiSignInToken{
28 | Pin: signInCode,
29 | RedirectTo: path,
30 | }
31 |
32 | jsonToken, err := json.Marshal(token)
33 | if err != nil {
34 | log.Fatalf("Error marshalling token: %v", err)
35 | }
36 |
37 | encodedToken := base64.URLEncoding.EncodeToString(jsonToken)
38 |
39 | url := fmt.Sprintf("%s/auth/%s", appHost, encodedToken)
40 |
41 | OpenURL(msg, url)
42 | }
43 |
44 | func OpenUnauthenticatedCloudURL(msg, path string) {
45 | apiHost := api.GetApiHost()
46 | appHost := strings.Replace(apiHost, "api-v2.", "app.", 1)
47 |
48 | url := fmt.Sprintf("%s%s", appHost, path)
49 |
50 | OpenURL(msg, url)
51 | }
52 |
53 | func OpenURL(msg, url string) {
54 |
55 | fmt.Printf(
56 | "%s\n\nIf it doesn't open automatically, use this URL:\n%s\n",
57 | color.New(term.ColorHiGreen).Sprintf(msg),
58 | url,
59 | )
60 |
61 | err := browser.OpenURL(url)
62 | if err != nil {
63 | fmt.Printf("Failed to open URL automatically: %v\n", err)
64 | fmt.Println("Please open the URL manually in your browser.")
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/cli/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | func EnsureMinDuration(start time.Time, minDuration time.Duration) {
8 | elapsed := time.Since(start)
9 | if elapsed < minDuration {
10 | time.Sleep(minDuration - elapsed)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/cli/version.txt:
--------------------------------------------------------------------------------
1 | 2.1.6+1
2 |
--------------------------------------------------------------------------------
/app/cli/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | // Version will be set at build time using -ldflags
4 | var Version = "development"
5 |
--------------------------------------------------------------------------------
/app/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | plandex-postgres:
3 | image: postgres:latest
4 | restart: always
5 | environment:
6 | POSTGRES_PASSWORD: plandex
7 | POSTGRES_USER: plandex
8 | POSTGRES_DB: plandex
9 | ports:
10 | - "5432:5432"
11 | volumes:
12 | - plandex-db:/var/lib/postgresql/data
13 | networks:
14 | - plandex-network
15 | plandex-server:
16 | image: plandexai/plandex-server:latest
17 | volumes:
18 | - plandex-files:/plandex-server
19 | ports:
20 | - "8099:8099"
21 | environment:
22 | DATABASE_URL: "postgres://plandex:plandex@plandex-postgres:5432/plandex?sslmode=disable"
23 | GOENV: development
24 | LOCAL_MODE: 1
25 | PLANDEX_BASE_DIR: /plandex-server
26 | networks:
27 | - plandex-network
28 | depends_on:
29 | - plandex-postgres
30 | command: [ "/bin/sh", "-c", "/scripts/wait-for-it.sh plandex-postgres:5432 -- ./plandex-server" ]
31 |
32 | networks:
33 | plandex-network:
34 | driver: bridge
35 |
36 | volumes:
37 | plandex-db:
38 | plandex-files:
--------------------------------------------------------------------------------
/app/plans/credits-cmd.txt:
--------------------------------------------------------------------------------
1 |
2 | Add a cmd/credits.go file with a command that calls GetOrgSession to get the current org and then if IntegratedModelsMode is true, displays the current credit balance formatted in dollars, to 4 decimal places.
3 |
4 | If IntegratedModelsMode isn't true, output that the org isn't using integrated models mode and nothing else.
5 |
6 | Display the current balance nicely in a table.
7 |
--------------------------------------------------------------------------------
/app/plans/credits-log-cmd.txt:
--------------------------------------------------------------------------------
1 | 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.
2 |
3 | You will need to add an API handler and route for this endpoint in server/cloud/ and also do the CLI side of the endpoint in cli/types/api.go and cli/api/methods.go.
4 |
5 | We will also need a client-side version of the CreditsTransaction type in shared/data_models.go that includes appropriate attributes, as well as a ToApi method for the server-side CreditsTransaction type in server/cloud/types/credits.go
6 |
7 | Think through anything else we will need to make this work. Also think about the best way to format and display this data for maximum developer-friendliness.
8 |
--------------------------------------------------------------------------------
/app/reset_local.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Get the absolute path to the script's directory, regardless of where it's run from
4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
5 |
6 | # Change to the app directory if we're not already there
7 | cd "$SCRIPT_DIR"
8 |
9 | echo "Clearing local mode..."
10 |
11 | ./clear_local.sh
12 |
13 | echo "Starting local mode..."
14 |
15 | ./start_local.sh
--------------------------------------------------------------------------------
/app/scripts/dev.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Detect zsh and trigger it if its the shell
4 | if [ -n "$ZSH_VERSION" ]; then
5 | # shell is zsh
6 | echo "Detected zsh"
7 | zsh -c "source ~/.zshrc && $*"
8 | fi
9 |
10 | # Get the directory of the script
11 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12 |
13 | # Change to the script directory
14 | cd "$SCRIPT_DIR" || exit 1
15 |
16 | # Detect if reflex is installed and install it if not
17 | if ! [ -x "$(command -v reflex)" ]; then
18 |
19 | # Check if the $GOPATH is empty
20 | if [ -z "$GOPATH" ]; then
21 | echo "Error: GOPATH is not set. Please set it to continue..." >&2
22 | exit 1
23 | fi
24 |
25 | echo 'Error: reflex is not installed. Installing it now...' >&2
26 | go install github.com/cespare/reflex@latest
27 | fi
28 |
29 | terminate() {
30 | pkill -f 'plandex-server' # Assuming plandex-server is the name of your process
31 | kill -TERM "$pid1" 2>/dev/null
32 | kill -TERM "$pid2" 2>/dev/null
33 | }
34 |
35 | trap terminate SIGTERM SIGINT
36 |
37 | (cd .. && cd cli && ./dev.sh)
38 |
39 | cd ../
40 |
41 | reflex -r '^(cli|shared)/.*\.(go|mod|sum)$' -- sh -c 'cd cli && ./dev.sh' &
42 | pid1=$!
43 |
44 | reflex -r '^(server|shared)/.*\.(go|mod|sum)$' -s -- sh -c 'cd server && go build && ./plandex-server' &
45 | pid2=$!
46 |
47 | wait $pid1
48 | wait $pid2
49 |
--------------------------------------------------------------------------------
/app/server/.gitignore:
--------------------------------------------------------------------------------
1 | cloud/
--------------------------------------------------------------------------------
/app/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.23.3
2 |
3 | # Update and install necessary packages including build tools for Tree-sitter
4 | RUN apt-get update && \
5 | apt-get install -y git gcc g++ make
6 |
7 | WORKDIR /app
8 |
9 | # Copy go.mod and go.sum for shared and server, and install dependencies
10 | COPY ./shared/go.mod ./shared/go.sum ./shared/
11 | RUN cd shared && go mod download
12 |
13 | COPY ./server/go.mod ./server/go.sum ./server/
14 | RUN cd server && go mod download
15 |
16 | # Copy the actual source code
17 | COPY ./server ./server
18 | COPY ./shared ./shared
19 | COPY ./scripts /scripts
20 |
21 | # Set working directory to server
22 | WORKDIR /app/server
23 |
24 | # Build the application
25 | RUN rm -f plandex-server && go build -o plandex-server .
26 |
27 | # Set the port and expose it
28 | ENV PORT=8099
29 | EXPOSE 8099
30 |
31 | # Command to run the executable
32 | CMD ["./plandex-server"]
33 |
--------------------------------------------------------------------------------
/app/server/db/account_helpers.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/jmoiron/sqlx"
8 | )
9 |
10 | type CreateAccountResult struct {
11 | User *User
12 | OrgId string
13 | Token string
14 | }
15 |
16 | func CreateAccount(name, email, emailVerificationId string, tx *sqlx.Tx) (*CreateAccountResult, error) {
17 | isLocalMode := (os.Getenv("GOENV") == "development" && os.Getenv("LOCAL_MODE") == "1")
18 | // create user
19 | user, err := CreateUser(name, email, tx)
20 |
21 | if err != nil {
22 | return nil, fmt.Errorf("error creating user: %v", err)
23 | }
24 |
25 | userId := user.Id
26 | domain := user.Domain
27 |
28 | // create auth token
29 | token, authTokenId, err := CreateAuthToken(userId, tx)
30 |
31 | if err != nil {
32 | return nil, fmt.Errorf("error creating auth token: %v", err)
33 | }
34 |
35 | // skipping email verification in local mode
36 | if !isLocalMode {
37 | // update email verification with user and auth token ids
38 | _, err = tx.Exec("UPDATE email_verifications SET user_id = $1, auth_token_id = $2 WHERE id = $3", userId, authTokenId, emailVerificationId)
39 |
40 | if err != nil {
41 | return nil, fmt.Errorf("error updating email verification: %v", err)
42 | }
43 | }
44 |
45 | // add to org matching domain if one exists and auto add domain users is true for that org
46 | orgId, err := AddToOrgForDomain(domain, userId, tx)
47 |
48 | if err != nil {
49 | return nil, fmt.Errorf("error adding user to org for domain: %v", err)
50 | }
51 |
52 | return &CreateAccountResult{
53 | User: user,
54 | OrgId: orgId,
55 | Token: token,
56 | }, nil
57 | }
58 |
--------------------------------------------------------------------------------
/app/server/db/ai_model_helpers.go:
--------------------------------------------------------------------------------
1 | package db
2 |
--------------------------------------------------------------------------------
/app/server/db/build_helpers.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func StorePlanBuild(build *PlanBuild) error {
9 |
10 | query := `INSERT INTO plan_builds (org_id, plan_id, convo_message_id, file_path) VALUES (:org_id, :plan_id, :convo_message_id, :file_path) RETURNING id, created_at, updated_at`
11 |
12 | args := map[string]interface{}{
13 | "org_id": build.OrgId,
14 | "plan_id": build.PlanId,
15 | "convo_message_id": build.ConvoMessageId,
16 | "file_path": build.FilePath,
17 | }
18 |
19 | row, err := Conn.NamedQuery(query, args)
20 | if err != nil {
21 | return fmt.Errorf("error storing plan build: %v", err)
22 | }
23 | defer row.Close()
24 |
25 | if row.Next() {
26 | var createdAt, updatedAt time.Time
27 | var id string
28 | if err := row.Scan(&id, &createdAt, &updatedAt); err != nil {
29 | return fmt.Errorf("error storing plan build: %v", err)
30 | }
31 |
32 | build.Id = id
33 | build.CreatedAt = createdAt
34 | build.UpdatedAt = updatedAt
35 | }
36 |
37 | return nil
38 | }
39 |
40 | func SetBuildError(build *PlanBuild) error {
41 | _, err := Conn.Exec("UPDATE plan_builds SET error = $1 WHERE id = $2", build.Error, build.Id)
42 |
43 | if err != nil {
44 | return fmt.Errorf("error setting build error: %v", err)
45 | }
46 |
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/app/server/db/context_helpers_map.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "encoding/json"
7 | "fmt"
8 | "log"
9 | "os"
10 | "path/filepath"
11 | shared "plandex-shared"
12 | )
13 |
14 | func GetCachedMap(orgId, projectId, filePath string) (*Context, error) {
15 | mapCacheDir := getProjectMapCacheDir(orgId, projectId)
16 |
17 | filePathHash := md5.Sum([]byte(filePath))
18 | filePathHashStr := hex.EncodeToString(filePathHash[:])
19 |
20 | mapCachePath := filepath.Join(mapCacheDir, filePathHashStr+".json")
21 |
22 | log.Println("GetCachedMap - mapCachePath", mapCachePath)
23 |
24 | mapCacheBytes, err := os.ReadFile(mapCachePath)
25 | if err != nil {
26 | if os.IsNotExist(err) {
27 | return nil, nil
28 | }
29 |
30 | return nil, fmt.Errorf("error reading cached map: %v", err)
31 | }
32 |
33 | var context Context
34 | err = json.Unmarshal(mapCacheBytes, &context)
35 | if err != nil {
36 | return nil, fmt.Errorf("error unmarshalling cached map: %v", err)
37 | }
38 |
39 | return &context, nil
40 | }
41 |
42 | type CachedMap struct {
43 | MapParts shared.FileMapBodies
44 | MapShas map[string]string
45 | MapTokens map[string]int
46 | MapSizes map[string]int64
47 | }
48 |
--------------------------------------------------------------------------------
/app/server/db/plan_config_helpers.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 |
6 | shared "plandex-shared"
7 |
8 | "github.com/jmoiron/sqlx"
9 | )
10 |
11 | func GetPlanConfig(planId string) (*shared.PlanConfig, error) {
12 | query := "SELECT plan_config FROM plans WHERE id = $1"
13 |
14 | var config shared.PlanConfig
15 | err := Conn.Get(&config, query, planId)
16 |
17 | if err != nil {
18 | return nil, fmt.Errorf("error getting plan config: %v", err)
19 | }
20 |
21 | return &config, nil
22 | }
23 |
24 | func StorePlanConfig(planId string, config *shared.PlanConfig) error {
25 | query := `
26 | UPDATE plans
27 | SET plan_config = $1
28 | WHERE id = $2
29 | `
30 |
31 | _, err := Conn.Exec(query, config, planId)
32 |
33 | if err != nil {
34 | return fmt.Errorf("error storing plan config: %v", err)
35 | }
36 |
37 | return nil
38 | }
39 |
40 | func GetDefaultPlanConfig(userId string) (*shared.PlanConfig, error) {
41 | query := "SELECT default_plan_config FROM users WHERE id = $1"
42 |
43 | var config shared.PlanConfig
44 | err := Conn.Get(&config, query, userId)
45 |
46 | if err != nil {
47 | return nil, fmt.Errorf("error getting default plan config: %v", err)
48 | }
49 |
50 | return &config, nil
51 | }
52 |
53 | func StoreDefaultPlanConfig(userId string, config *shared.PlanConfig, tx *sqlx.Tx) error {
54 | query := `
55 | UPDATE users SET default_plan_config = $1 WHERE id = $2
56 | `
57 |
58 | _, err := tx.Exec(query, config, userId)
59 |
60 | if err != nil {
61 | return fmt.Errorf("error storing default plan config: %v", err)
62 | }
63 |
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/app/server/db/project_helpers.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/jmoiron/sqlx"
7 | )
8 |
9 | func ProjectExists(orgId, projectId string) (bool, error) {
10 | var count int
11 | err := Conn.QueryRow("SELECT COUNT(*) FROM projects WHERE org_id = $1 AND id = $2", orgId, projectId).Scan(&count)
12 |
13 | if err != nil {
14 | return false, fmt.Errorf("error checking if project exists: %v", err)
15 | }
16 |
17 | return count > 0, nil
18 | }
19 |
20 | func CreateProject(orgId, name string, tx *sqlx.Tx) (string, error) {
21 | var projectId string
22 | err := tx.QueryRow("INSERT INTO projects (org_id, name) VALUES ($1, $2) RETURNING id", orgId, name).Scan(&projectId)
23 |
24 | if err != nil {
25 | return "", fmt.Errorf("error creating project: %v", err)
26 | }
27 |
28 | return projectId, nil
29 | }
30 |
--------------------------------------------------------------------------------
/app/server/db/subtask_helpers.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | )
9 |
10 | func GetPlanSubtasks(orgId, planId string) ([]*Subtask, error) {
11 | planDir := getPlanDir(orgId, planId)
12 | subtasksPath := filepath.Join(planDir, "subtasks.json")
13 |
14 | bytes, err := os.ReadFile(subtasksPath)
15 |
16 | if err != nil {
17 | if os.IsNotExist(err) {
18 | return nil, nil
19 | }
20 |
21 | return nil, fmt.Errorf("error reading subtasks: %v", err)
22 | }
23 |
24 | var subtasks []*Subtask
25 | err = json.Unmarshal(bytes, &subtasks)
26 |
27 | if err != nil {
28 | return nil, fmt.Errorf("error unmarshalling subtasks: %v", err)
29 | }
30 |
31 | return subtasks, nil
32 | }
33 |
34 | func StorePlanSubtasks(orgId, planId string, subtasks []*Subtask) error {
35 | planDir := getPlanDir(orgId, planId)
36 |
37 | bytes, err := json.Marshal(subtasks)
38 |
39 | if err != nil {
40 | return fmt.Errorf("error marshalling subtasks: %v", err)
41 | }
42 |
43 | err = os.WriteFile(filepath.Join(planDir, "subtasks.json"), bytes, os.ModePerm)
44 |
45 | if err != nil {
46 | return fmt.Errorf("error writing subtasks: %v", err)
47 | }
48 |
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/app/server/db/summary_helpers.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/lib/pq"
8 | )
9 |
10 | func GetPlanSummaries(planId string, convoMessageIds []string) ([]*ConvoSummary, error) {
11 | var summaries []*ConvoSummary
12 |
13 | err := Conn.Select(&summaries, "SELECT * FROM convo_summaries WHERE plan_id = $1 AND latest_convo_message_id = ANY($2) ORDER BY created_at", planId, pq.Array(convoMessageIds))
14 |
15 | if err != nil {
16 | return nil, fmt.Errorf("error getting plan summaries: %v", err)
17 | }
18 | return summaries, nil
19 | }
20 |
21 | func StoreSummary(summary *ConvoSummary) error {
22 | query := "INSERT INTO convo_summaries (org_id, plan_id, latest_convo_message_id, latest_convo_message_created_at, summary, tokens, num_messages) VALUES (:org_id, :plan_id, :latest_convo_message_id, :latest_convo_message_created_at, :summary, :tokens, :num_messages) RETURNING id, created_at"
23 |
24 | row, err := Conn.NamedQuery(query, summary)
25 |
26 | if err != nil {
27 | return fmt.Errorf("error storing summary: %v", err)
28 | }
29 |
30 | defer row.Close()
31 |
32 | if row.Next() {
33 | var createdAt time.Time
34 | var id string
35 | if err := row.Scan(&id, &createdAt); err != nil {
36 | return fmt.Errorf("error storing summary: %v", err)
37 | }
38 |
39 | summary.Id = id
40 | summary.CreatedAt = createdAt
41 | }
42 |
43 | return nil
44 | }
45 |
--------------------------------------------------------------------------------
/app/server/db/utils.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/lib/pq"
5 | )
6 |
7 | func IsNonUniqueErr(err error) bool {
8 | if err, ok := err.(*pq.Error); ok {
9 | if err.Code == "23505" {
10 | return true
11 | }
12 | }
13 | return false
14 | }
15 |
--------------------------------------------------------------------------------
/app/server/email/verification.go:
--------------------------------------------------------------------------------
1 | package email
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | "github.com/atotto/clipboard"
9 | "github.com/gen2brain/beeep"
10 | )
11 |
12 | func SendVerificationEmail(email string, pin string) error {
13 | // Check if the environment is production
14 | if os.Getenv("GOENV") == "production" {
15 | // Production environment - send email using AWS SES
16 | subject := "Your Plandex Pin"
17 | htmlBody := fmt.Sprintf(`
Hi there,
18 | Welcome to Plandex! Your pin is:
19 | %s
20 | It will be valid for the next 5 minutes.
21 | If you didn't request this, you can safely ignore the email.
`, pin)
22 | textBody := fmt.Sprintf("Hi there,\n\nWelcome to Plandex! Your pin is:\n\n%s\n\nIt will be valid for the next 5 minutes.\n\nIf you didn't request this, you can safely ignore the email.", pin)
23 |
24 | if os.Getenv("IS_CLOUD") == "" {
25 | return sendEmailViaSMTP(email, subject, htmlBody, textBody)
26 | } else {
27 | return SendEmailViaSES(email, subject, htmlBody, textBody)
28 | }
29 | }
30 |
31 | if os.Getenv("GOENV") == "development" {
32 | // Development environment
33 | log.Printf("Development mode: Verification pin is %s for email %s", pin, email)
34 |
35 | // Copy pin to clipboard
36 | clipboard.WriteAll(pin) // ignore error
37 |
38 | // Send notification
39 | beeep.Notify("Verification Pin", fmt.Sprintf("Verification pin %s copied to clipboard %s", pin, email), "") // ignore error
40 | }
41 |
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/app/server/handlers/err_helper.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 |
8 | shared "plandex-shared"
9 | )
10 |
11 | func writeApiError(w http.ResponseWriter, apiErr shared.ApiError) {
12 | bytes, err := json.Marshal(apiErr)
13 | if err != nil {
14 | log.Printf("Error marshalling response: %v\n", err)
15 | // If marshalling fails, fall back to a simpler error message
16 | http.Error(w, "Error marshalling response", http.StatusInternalServerError)
17 | return
18 | }
19 |
20 | log.Printf("API Error: %v\n", apiErr.Msg)
21 |
22 | w.Header().Set("Content-Type", "application/json")
23 | w.WriteHeader(apiErr.Status)
24 |
25 | _, writeErr := w.Write(bytes)
26 | if writeErr != nil {
27 | log.Printf("Error writing response: %v\n", writeErr)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/server/handlers/org_helpers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "log"
5 | "plandex-server/db"
6 | "plandex-server/hooks"
7 |
8 | shared "plandex-shared"
9 | )
10 |
11 | func toApiOrgs(orgs []*db.Org) ([]*shared.Org, *shared.ApiError) {
12 | var orgIds []string
13 | for _, org := range orgs {
14 | orgIds = append(orgIds, org.Id)
15 | }
16 |
17 | hookRes, apiErr := hooks.ExecHook(hooks.GetApiOrgs, hooks.HookParams{
18 | GetApiOrgIds: orgIds,
19 | })
20 |
21 | if apiErr != nil {
22 | log.Printf("Error getting integrated models mode by org id: %v\n", apiErr)
23 | return nil, apiErr
24 | }
25 |
26 | var apiOrgs []*shared.Org
27 | for _, org := range orgs {
28 | if hookRes.ApiOrgsById != nil {
29 | hookApiOrg := hookRes.ApiOrgsById[org.Id]
30 | apiOrgs = append(apiOrgs, hookApiOrg)
31 | } else {
32 | apiOrgs = append(apiOrgs, org.ToApi())
33 | }
34 | }
35 |
36 | return apiOrgs, nil
37 | }
38 |
39 | func getApiOrg(orgId string) (*shared.Org, *shared.ApiError) {
40 | org, err := db.GetOrg(orgId)
41 | if err != nil {
42 | log.Printf("Error getting org: %v\n", err)
43 | return nil, &shared.ApiError{
44 | Type: shared.ApiErrorTypeOther,
45 | Msg: "Error getting org",
46 | }
47 | }
48 |
49 | hookRes, apiErr := hooks.ExecHook(hooks.GetApiOrgs, hooks.HookParams{
50 | GetApiOrgIds: []string{org.Id},
51 | })
52 |
53 | if apiErr != nil {
54 | log.Printf("Error getting integrated models mode by org id: %v\n", apiErr)
55 | return nil, apiErr
56 | }
57 |
58 | if hookRes.ApiOrgsById != nil {
59 | return hookRes.ApiOrgsById[org.Id], nil
60 | }
61 |
62 | return org.ToApi(), nil
63 | }
64 |
--------------------------------------------------------------------------------
/app/server/host/ip.go:
--------------------------------------------------------------------------------
1 | package host
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/http"
10 | "os"
11 | )
12 |
13 | var Ip string
14 |
15 | func LoadIp() error {
16 | if os.Getenv("GOENV") == "development" {
17 | Ip = "localhost"
18 | return nil
19 | }
20 |
21 | if os.Getenv("IS_CLOUD") != "" {
22 | var err error
23 | Ip, err = getAwsIp()
24 |
25 | if err != nil {
26 | return fmt.Errorf("error getting AWS ECS IP: %v", err)
27 | }
28 |
29 | log.Println("Got AWS ECS IP: ", Ip)
30 |
31 | } else if os.Getenv("IP") != "" {
32 | Ip = os.Getenv("IP")
33 | return nil
34 | }
35 |
36 | return nil
37 | }
38 |
39 | type ecsMetadata struct {
40 | Networks []struct {
41 | IPv4Addresses []string `json:"IPv4Addresses"`
42 | } `json:"Networks"`
43 | }
44 |
45 | var awsIp string
46 |
47 | func getAwsIp() (string, error) {
48 | ecsMetadataURL := os.Getenv("ECS_CONTAINER_METADATA_URI")
49 |
50 | log.Printf("Getting ECS metadata from %s\n", ecsMetadataURL)
51 |
52 | resp, err := http.Get(ecsMetadataURL)
53 | if err != nil {
54 | return "", err
55 | }
56 | defer resp.Body.Close()
57 |
58 | body, err := io.ReadAll(resp.Body)
59 | if err != nil {
60 | return "", err
61 | }
62 |
63 | var metadata ecsMetadata
64 | err = json.Unmarshal(body, &metadata)
65 | if err != nil {
66 | return "", err
67 | }
68 |
69 | if len(metadata.Networks) == 0 || len(metadata.Networks[0].IPv4Addresses) == 0 {
70 | return "", errors.New("no IP address found in ECS metadata")
71 | }
72 |
73 | awsIp = metadata.Networks[0].IPv4Addresses[0]
74 |
75 | return awsIp, nil
76 | }
77 |
--------------------------------------------------------------------------------
/app/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "os"
6 | "plandex-server/routes"
7 | "plandex-server/setup"
8 |
9 | "github.com/gorilla/mux"
10 | )
11 |
12 | func main() {
13 | // Configure the default logger to include milliseconds in timestamps
14 | log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
15 |
16 | routes.RegisterHandlePlandex(func(router *mux.Router, path string, isStreaming bool, handler routes.PlandexHandler) *mux.Route {
17 | return router.HandleFunc(path, handler)
18 | })
19 |
20 | r := mux.NewRouter()
21 | routes.AddHealthRoutes(r)
22 | routes.AddApiRoutes(r)
23 | routes.AddProxyableApiRoutes(r)
24 | setup.MustLoadIp()
25 | setup.MustInitDb()
26 | setup.StartServer(r, nil, nil)
27 | os.Exit(0)
28 | }
29 |
--------------------------------------------------------------------------------
/app/server/migrations/2023120500_init.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS branches;
2 |
3 | DROP TABLE IF EXISTS convo_summaries;
4 | DROP TABLE IF EXISTS plan_builds;
5 |
6 | DROP TABLE IF EXISTS users_projects;
7 | DROP TABLE IF EXISTS plans;
8 | DROP TABLE IF EXISTS projects;
9 | DROP TABLE IF EXISTS orgs_users;
10 |
11 | DROP TABLE IF EXISTS email_verifications;
12 | DROP TABLE IF EXISTS auth_tokens;
13 | DROP TABLE IF EXISTS invites;
14 |
15 | DROP TABLE IF EXISTS orgs;
16 | DROP TABLE IF EXISTS users;
17 |
--------------------------------------------------------------------------------
/app/server/migrations/2024011700_rbac.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE orgs_users DROP COLUMN org_role_id;
2 | ALTER TABLE invites DROP COLUMN org_role_id;
3 |
4 | DROP TABLE IF EXISTS org_roles_permissions;
5 | DROP TABLE IF EXISTS permissions;
6 | DROP TABLE IF EXISTS org_roles;
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/server/migrations/2024012400_streams.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS model_streams;
2 | -- DROP TABLE IF EXISTS model_stream_subscriptions;
--------------------------------------------------------------------------------
/app/server/migrations/2024012400_streams.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS model_streams (
2 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
3 | org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
4 | plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
5 | branch VARCHAR(255) NOT NULL,
6 | internal_ip VARCHAR(45) NOT NULL,
7 | created_at TIMESTAMP NOT NULL DEFAULT NOW(),
8 | finished_at TIMESTAMP
9 | );
10 |
11 | CREATE UNIQUE INDEX model_streams_plan_idx ON model_streams(plan_id, branch, finished_at);
12 |
13 | -- CREATE TABLE IF NOT EXISTS model_stream_subscriptions (
14 | -- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
15 | -- org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
16 | -- plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
17 | -- model_stream_id UUID NOT NULL REFERENCES model_streams(id) ON DELETE CASCADE,
18 | -- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
19 | -- user_ip VARCHAR(45) NOT NULL,
20 | -- created_at TIMESTAMP NOT NULL DEFAULT NOW()
21 | -- finished_at TIMESTAMP
22 | -- );
23 |
24 |
--------------------------------------------------------------------------------
/app/server/migrations/2024012500_locks.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS repo_locks;
--------------------------------------------------------------------------------
/app/server/migrations/2024012500_locks.up.sql:
--------------------------------------------------------------------------------
1 | CREATE UNLOGGED TABLE IF NOT EXISTS repo_locks (
2 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
3 | org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
4 | user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5 | plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
6 | plan_build_id UUID REFERENCES plan_builds(id) ON DELETE CASCADE,
7 | scope VARCHAR(1) NOT NULL,
8 | branch VARCHAR(255),
9 | created_at TIMESTAMP NOT NULL DEFAULT NOW()
10 | );
11 |
12 | CREATE INDEX repo_locks_plan_idx ON repo_locks(plan_id);
13 |
--------------------------------------------------------------------------------
/app/server/migrations/2024013000_plan_build_convo_ids.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE plan_builds
2 | RENAME COLUMN convo_message_ids TO convo_message_id;
3 |
4 | ALTER TABLE plan_builds
5 | ALTER COLUMN convo_message_id TYPE UUID USING (convo_message_id[1]);
--------------------------------------------------------------------------------
/app/server/migrations/2024013000_plan_build_convo_ids.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE plan_builds
2 | RENAME COLUMN convo_message_id TO convo_message_ids;
3 |
4 | ALTER TABLE plan_builds
5 | ALTER COLUMN convo_message_ids TYPE UUID[] USING ARRAY[convo_message_ids];
6 |
--------------------------------------------------------------------------------
/app/server/migrations/2024020800_heartbeats.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE repo_locks DROP COLUMN last_heartbeat_at;
2 | ALTER TABLE model_streams DROP COLUMN last_heartbeat_at;
--------------------------------------------------------------------------------
/app/server/migrations/2024020800_heartbeats.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE repo_locks ADD COLUMN last_heartbeat_at TIMESTAMP NOT NULL DEFAULT NOW();
2 | ALTER TABLE model_streams ADD COLUMN last_heartbeat_at TIMESTAMP NOT NULL DEFAULT NOW();
--------------------------------------------------------------------------------
/app/server/migrations/2024022000_revert_plan_build_convo_ids.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE plan_builds
2 | RENAME COLUMN convo_message_id TO convo_message_ids;
3 |
4 | ALTER TABLE plan_builds
5 | ALTER COLUMN convo_message_ids TYPE UUID[] USING ARRAY[convo_message_ids];
6 |
--------------------------------------------------------------------------------
/app/server/migrations/2024022000_revert_plan_build_convo_ids.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE plan_builds
2 | RENAME COLUMN convo_message_ids TO convo_message_id;
3 |
4 | ALTER TABLE plan_builds
5 | ALTER COLUMN convo_message_id TYPE UUID USING (convo_message_id[1]);
--------------------------------------------------------------------------------
/app/server/migrations/2024032700_remove_billing_admin.down.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/app/server/migrations/2024032700_remove_billing_admin.down.sql
--------------------------------------------------------------------------------
/app/server/migrations/2024032700_remove_billing_admin.up.sql:
--------------------------------------------------------------------------------
1 | DELETE FROM org_roles WHERE name = 'billing_admin';
--------------------------------------------------------------------------------
/app/server/migrations/2024032701_drop_users_projects.down.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS users_projects (
2 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
3 | org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
4 | user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5 | project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
6 | last_active_plan_id UUID REFERENCES plans(id),
7 | created_at TIMESTAMP NOT NULL DEFAULT NOW(),
8 | updated_at TIMESTAMP NOT NULL DEFAULT NOW()
9 | );
10 | CREATE TRIGGER update_users_projects_modtime BEFORE UPDATE ON users_projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
11 |
12 | CREATE INDEX users_projects_idx ON users_projects(user_id, org_id, project_id);
--------------------------------------------------------------------------------
/app/server/migrations/2024032701_drop_users_projects.up.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS users_projects;
--------------------------------------------------------------------------------
/app/server/migrations/2024040400_add_orgs_users_unique.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE orgs_users DROP CONSTRAINT org_user_unique;
--------------------------------------------------------------------------------
/app/server/migrations/2024040400_add_orgs_users_unique.up.sql:
--------------------------------------------------------------------------------
1 | -- clean up any duplicates added mistakenly earlier
2 | WITH ranked_duplicates AS (
3 | SELECT id,
4 | ROW_NUMBER() OVER (PARTITION BY org_id, user_id ORDER BY created_at) AS rn
5 | FROM orgs_users
6 | )
7 | DELETE FROM orgs_users
8 | WHERE id IN (
9 | SELECT id FROM ranked_duplicates WHERE rn > 1
10 | );
11 |
12 | ALTER TABLE orgs_users ADD CONSTRAINT org_user_unique UNIQUE (org_id, user_id);
13 |
--------------------------------------------------------------------------------
/app/server/migrations/2024041500_model_sets_models.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS model_sets;
2 | DROP TABLE IF EXISTS custom_models;
--------------------------------------------------------------------------------
/app/server/migrations/2024041500_model_sets_models.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS model_sets (
2 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
3 | org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
4 |
5 | name VARCHAR(255) NOT NULL,
6 | description TEXT,
7 |
8 | planner JSON,
9 | plan_summary JSON,
10 | builder JSON,
11 | namer JSON,
12 | commit_msg JSON,
13 | exec_status JSON,
14 |
15 | created_at TIMESTAMP NOT NULL DEFAULT NOW()
16 | );
17 |
18 | CREATE INDEX model_sets_org_idx ON model_sets(org_id);
19 |
20 | CREATE TABLE IF NOT EXISTS custom_models (
21 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
22 | org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
23 |
24 | provider VARCHAR(255) NOT NULL,
25 | custom_provider VARCHAR(255),
26 | base_url VARCHAR(255) NOT NULL,
27 | model_name VARCHAR(255) NOT NULL,
28 | description TEXT,
29 | max_tokens INTEGER NOT NULL,
30 | api_key_env_var VARCHAR(255),
31 |
32 | is_openai_compatible BOOLEAN NOT NULL,
33 | has_json_mode BOOLEAN NOT NULL,
34 | has_streaming BOOLEAN NOT NULL,
35 | has_function_calling BOOLEAN NOT NULL,
36 | has_streaming_function_calls BOOLEAN NOT NULL,
37 |
38 | default_max_convo_tokens INTEGER NOT NULL,
39 | default_reserved_output_tokens INTEGER NOT NULL,
40 |
41 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
42 | created_at TIMESTAMP NOT NULL DEFAULT NOW()
43 | );
44 |
45 | CREATE TRIGGER update_custom_models_modtime BEFORE UPDATE ON custom_models FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
46 |
47 | CREATE INDEX custom_models_org_idx ON custom_models(org_id);
48 |
--------------------------------------------------------------------------------
/app/server/migrations/2024042600_default_plan_settings.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS default_plan_settings;
--------------------------------------------------------------------------------
/app/server/migrations/2024042600_default_plan_settings.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS default_plan_settings (
2 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
3 | org_id UUID NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
4 |
5 | plan_settings JSON,
6 |
7 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
8 | created_at TIMESTAMP NOT NULL DEFAULT NOW()
9 | );
10 | CREATE TRIGGER update_default_plan_settings_modtime BEFORE UPDATE ON default_plan_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
11 |
12 | CREATE UNIQUE INDEX default_plan_settings_org_idx ON default_plan_settings(org_id);
13 |
--------------------------------------------------------------------------------
/app/server/migrations/2024091800_sign_in_codes.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS sign_in_codes;
--------------------------------------------------------------------------------
/app/server/migrations/2024091800_sign_in_codes.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS sign_in_codes (
2 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
3 | pin_hash VARCHAR(64) NOT NULL,
4 | user_id UUID REFERENCES users(id),
5 | org_id UUID REFERENCES orgs(id),
6 | auth_token_id UUID REFERENCES auth_tokens(id),
7 | created_at TIMESTAMP NOT NULL DEFAULT NOW(),
8 | updated_at TIMESTAMP NOT NULL DEFAULT NOW()
9 | );
10 | CREATE TRIGGER update_sign_in_codes_modtime BEFORE UPDATE ON sign_in_codes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
11 |
12 | CREATE UNIQUE INDEX sign_in_codes_idx ON sign_in_codes(pin_hash, created_at DESC);
--------------------------------------------------------------------------------
/app/server/migrations/2024092100_remove_trial_fields.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE auth_tokens ADD COLUMN is_trial BOOLEAN NOT NULL DEFAULT FALSE;
2 | ALTER TABLE users ADD COLUMN is_trial BOOLEAN NOT NULL DEFAULT FALSE;
--------------------------------------------------------------------------------
/app/server/migrations/2024092100_remove_trial_fields.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE auth_tokens DROP COLUMN is_trial;
2 | ALTER TABLE users DROP COLUMN is_trial;
--------------------------------------------------------------------------------
/app/server/migrations/2024100900_update_locks.down.sql:
--------------------------------------------------------------------------------
1 | -- Revert user_id to NOT NULL if no NULL values exist
2 | DO $$
3 | BEGIN
4 | -- Check for NULL values in user_id
5 | IF EXISTS (SELECT 1 FROM repo_locks WHERE user_id IS NULL) THEN
6 | RAISE EXCEPTION 'Cannot revert to NOT NULL, as there are rows with NULL values in user_id.';
7 | ELSE
8 | -- Proceed with setting the columns to NOT NULL
9 | ALTER TABLE repo_locks
10 | ALTER COLUMN user_id SET NOT NULL;
11 | END IF;
12 | END $$;
13 |
--------------------------------------------------------------------------------
/app/server/migrations/2024100900_update_locks.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE repo_locks
2 | ALTER COLUMN user_id DROP NOT NULL;
3 |
--------------------------------------------------------------------------------
/app/server/migrations/2024121400_plan_config.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE plans DROP COLUMN IF EXISTS plan_config;
2 |
3 | ALTER TABLE users DROP COLUMN IF EXISTS default_plan_config;
--------------------------------------------------------------------------------
/app/server/migrations/2024121400_plan_config.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE plans ADD COLUMN IF NOT EXISTS plan_config JSON;
2 |
3 | ALTER TABLE users ADD COLUMN IF NOT EXISTS default_plan_config JSON;
4 |
--------------------------------------------------------------------------------
/app/server/migrations/2025012600_update_custom_models.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE custom_models DROP COLUMN preferred_output_format;
2 |
3 | ALTER TABLE custom_models ADD COLUMN has_streaming BOOLEAN NOT NULL DEFAULT FALSE;
4 | ALTER TABLE custom_models ADD COLUMN has_function_calling BOOLEAN NOT NULL DEFAULT FALSE;
5 | ALTER TABLE custom_models ADD COLUMN has_streaming_function_calls BOOLEAN NOT NULL DEFAULT FALSE;
6 | ALTER TABLE custom_models ADD COLUMN has_json_mode BOOLEAN NOT NULL DEFAULT FALSE;
7 | ALTER TABLE custom_models ADD COLUMN is_openai_compatible BOOLEAN NOT NULL DEFAULT FALSE;
8 |
9 | ALTER TABLE custom_models DROP COLUMN has_image_support;
10 |
--------------------------------------------------------------------------------
/app/server/migrations/2025012600_update_custom_models.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE custom_models ADD COLUMN preferred_output_format VARCHAR(32) NOT NULL DEFAULT 'xml';
2 |
3 | ALTER TABLE custom_models DROP COLUMN has_streaming_function_calls;
4 | ALTER TABLE custom_models DROP COLUMN has_json_mode;
5 | ALTER TABLE custom_models DROP COLUMN has_streaming;
6 | ALTER TABLE custom_models DROP COLUMN has_function_calling;
7 | ALTER TABLE custom_models DROP COLUMN is_openai_compatible;
8 |
9 | ALTER TABLE custom_models ADD COLUMN has_image_support BOOLEAN NOT NULL DEFAULT FALSE;
--------------------------------------------------------------------------------
/app/server/migrations/2025021101_locks_unique.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS lockable_plan_ids;
2 | DROP INDEX IF EXISTS repo_locks_single_write_lock;
--------------------------------------------------------------------------------
/app/server/migrations/2025021101_locks_unique.up.sql:
--------------------------------------------------------------------------------
1 |
2 | CREATE UNIQUE INDEX repo_locks_single_write_lock
3 | ON repo_locks(plan_id)
4 | WHERE (scope = 'w');
5 |
6 | CREATE TABLE IF NOT EXISTS lockable_plan_ids (
7 | plan_id UUID NOT NULL PRIMARY KEY REFERENCES plans(id) ON DELETE CASCADE
8 | );
--------------------------------------------------------------------------------
/app/server/migrations/2025022700_remove_models_col.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE custom_models ADD COLUMN default_reserved_output_tokens INTEGER NOT NULL;
--------------------------------------------------------------------------------
/app/server/migrations/2025022700_remove_models_col.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE custom_models DROP COLUMN default_reserved_output_tokens;
--------------------------------------------------------------------------------
/app/server/migrations/2025031300_add_model_roles.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE model_sets DROP COLUMN context_loader;
2 | ALTER TABLE model_sets DROP COLUMN whole_file_builder;
3 | ALTER TABLE model_sets DROP COLUMN coder;
--------------------------------------------------------------------------------
/app/server/migrations/2025031300_add_model_roles.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE model_sets ADD COLUMN context_loader JSON;
2 | ALTER TABLE model_sets ADD COLUMN whole_file_builder JSON;
3 | ALTER TABLE model_sets ADD COLUMN coder JSON;
--------------------------------------------------------------------------------
/app/server/migrations/2025031900_add_custom_model_cols.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE custom_models DROP COLUMN max_output_tokens;
2 | ALTER TABLE custom_models DROP COLUMN reserved_output_tokens;
3 | ALTER TABLE custom_models DROP COLUMN model_id;
4 |
--------------------------------------------------------------------------------
/app/server/migrations/2025031900_add_custom_model_cols.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE custom_models
2 | ADD COLUMN max_output_tokens INTEGER NOT NULL,
3 | ADD COLUMN reserved_output_tokens INTEGER NOT NULL,
4 | ADD COLUMN model_id VARCHAR(255) NOT NULL;
--------------------------------------------------------------------------------
/app/server/migrations/2025032400_sign_in_codes_on_delete.down.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE sign_in_codes
2 | DROP CONSTRAINT sign_in_codes_org_id_fkey,
3 | ADD CONSTRAINT sign_in_codes_org_id_fkey
4 | FOREIGN KEY (org_id)
5 | REFERENCES orgs(id);
6 |
7 |
--------------------------------------------------------------------------------
/app/server/migrations/2025032400_sign_in_codes_on_delete.up.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE sign_in_codes
2 | DROP CONSTRAINT sign_in_codes_org_id_fkey,
3 | ADD CONSTRAINT sign_in_codes_org_id_fkey
4 | FOREIGN KEY (org_id)
5 | REFERENCES orgs(id)
6 | ON DELETE SET NULL;
7 |
8 |
--------------------------------------------------------------------------------
/app/server/model/plan/build_state.go:
--------------------------------------------------------------------------------
1 | package plan
2 |
3 | import (
4 | "plandex-server/db"
5 | "plandex-server/hooks"
6 | "plandex-server/model"
7 | "plandex-server/types"
8 |
9 | shared "plandex-shared"
10 |
11 | sitter "github.com/smacker/go-tree-sitter"
12 | )
13 |
14 | const MaxBuildErrorRetries = 3 // uses semi-exponential backoff so be careful with this
15 |
16 | type activeBuildStreamState struct {
17 | modelStreamId string
18 | clients map[string]model.ClientInfo
19 | auth *types.ServerAuth
20 | currentOrgId string
21 | currentUserId string
22 | plan *db.Plan
23 | branch string
24 | settings *shared.PlanSettings
25 | modelContext []*db.Context
26 | convo []*db.ConvoMessage
27 | }
28 |
29 | type activeBuildStreamFileState struct {
30 | *activeBuildStreamState
31 | filePath string
32 | convoMessageId string
33 | build *db.PlanBuild
34 | currentPlanState *shared.CurrentPlanState
35 | activeBuild *types.ActiveBuild
36 | preBuildState string
37 | parser *sitter.Parser
38 | language shared.Language
39 | syntaxCheckTimedOut bool
40 | preBuildStateSyntaxInvalid bool
41 | validationNumRetry int
42 | wholeFileNumRetry int
43 | isNewFile bool
44 | contextPart *db.Context
45 |
46 | builderRun hooks.DidFinishBuilderRunParams
47 | }
48 |
--------------------------------------------------------------------------------
/app/server/model/plan/shutdown.go:
--------------------------------------------------------------------------------
1 | package plan
2 |
--------------------------------------------------------------------------------
/app/server/model/plan/stop.go:
--------------------------------------------------------------------------------
1 | package plan
2 |
3 | import (
4 | "fmt"
5 | "plandex-server/db"
6 |
7 | "github.com/sashabaranov/go-openai"
8 | )
9 |
10 | func Stop(planId, branch, currentUserId, currentOrgId string) error {
11 | active := GetActivePlan(planId, branch)
12 |
13 | if active == nil {
14 | return fmt.Errorf("no active plan with id %s", planId)
15 | }
16 |
17 | active.SummaryCancelFn()
18 | active.CancelFn()
19 |
20 | return nil
21 | }
22 |
23 | func StorePartialReply(repo *db.GitRepo, planId, branch, currentUserId, currentOrgId string) error {
24 | active := GetActivePlan(planId, branch)
25 |
26 | if active == nil {
27 | return fmt.Errorf("no active plan with id %s", planId)
28 | }
29 |
30 | if !active.BuildOnly && !active.RepliesFinished {
31 | num := active.MessageNum + 1
32 |
33 | userMsg := db.ConvoMessage{
34 | OrgId: currentOrgId,
35 | PlanId: planId,
36 | UserId: currentUserId,
37 | Role: openai.ChatMessageRoleAssistant,
38 | Tokens: active.NumTokens,
39 | Num: num,
40 | Stopped: true,
41 | Message: active.CurrentReplyContent,
42 | }
43 |
44 | _, err := db.StoreConvoMessage(repo, &userMsg, currentUserId, branch, true)
45 |
46 | if err != nil {
47 | return fmt.Errorf("error storing convo message: %v", err)
48 | }
49 | }
50 |
51 | return nil
52 | }
53 |
--------------------------------------------------------------------------------
/app/server/model/plan/tell_build_pending.go:
--------------------------------------------------------------------------------
1 | package plan
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "plandex-server/notify"
8 |
9 | shared "plandex-shared"
10 | )
11 |
12 | func (state *activeTellStreamState) queuePendingBuilds() {
13 | plan := state.plan
14 | planId := plan.Id
15 | branch := state.branch
16 | auth := state.auth
17 | clients := state.clients
18 | currentOrgId := state.currentOrgId
19 | currentUserId := state.currentUserId
20 | active := GetActivePlan(planId, branch)
21 |
22 | if active == nil {
23 | log.Printf("execTellPlan: Active plan not found for plan ID %s on branch %s\n", planId, branch)
24 | return
25 | }
26 |
27 | pendingBuildsByPath, err := active.PendingBuildsByPath(auth.OrgId, auth.User.Id, state.convo)
28 |
29 | if err != nil {
30 | log.Printf("Error getting pending builds by path: %v\n", err)
31 | go notify.NotifyErr(notify.SeverityError, fmt.Errorf("error getting pending builds by path: %v", err))
32 |
33 | active.StreamDoneCh <- &shared.ApiError{
34 | Type: shared.ApiErrorTypeOther,
35 | Status: http.StatusInternalServerError,
36 | Msg: "Error getting pending builds by path",
37 | }
38 | return
39 | }
40 |
41 | if len(pendingBuildsByPath) == 0 {
42 | log.Println("Tell plan: no pending builds")
43 | return
44 | }
45 |
46 | log.Printf("Tell plan: found %d pending builds\n", len(pendingBuildsByPath))
47 | // spew.Dump(pendingBuildsByPath)
48 |
49 | buildState := &activeBuildStreamState{
50 | modelStreamId: active.ModelStreamId,
51 | clients: clients,
52 | auth: auth,
53 | currentOrgId: currentOrgId,
54 | currentUserId: currentUserId,
55 | plan: plan,
56 | branch: branch,
57 | settings: state.settings,
58 | modelContext: state.modelContext,
59 | }
60 |
61 | for _, pendingBuilds := range pendingBuildsByPath {
62 | buildState.queueBuilds(pendingBuilds)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/server/model/plan/utils.go:
--------------------------------------------------------------------------------
1 | package plan
2 |
3 | import (
4 | "plandex-server/types"
5 | "strings"
6 | )
7 |
8 | func StripBackticksWrapper(s string) string {
9 | check := strings.TrimSpace(s)
10 | split := strings.Split(check, "\n")
11 |
12 | if len(split) > 2 {
13 | firstLine := strings.TrimSpace(split[0])
14 | secondLine := strings.TrimSpace(split[1])
15 | lastLine := strings.TrimSpace(split[len(split)-1])
16 | if types.LineMaybeHasFilePath(firstLine) && strings.HasPrefix(secondLine, "```") {
17 | if lastLine == "```" {
18 | return strings.Join(split[1:len(split)-1], "\n")
19 | }
20 | } else if strings.HasPrefix(firstLine, "```") && lastLine == "```" {
21 | return strings.Join(split[1:len(split)-1], "\n")
22 | }
23 | }
24 |
25 | return s
26 | }
27 |
--------------------------------------------------------------------------------
/app/server/model/prompts/describe.go:
--------------------------------------------------------------------------------
1 |
2 | package prompts
3 |
4 | import (
5 | "github.com/sashabaranov/go-openai"
6 | "github.com/sashabaranov/go-openai/jsonschema"
7 | )
8 |
9 | const SysDescribeXml = `You are an AI parser. You turn an AI's plan for a programming task into a structured description. You MUST output a valid XML response that includes a tag. The tag should contain a good, succinct commit message for the changes proposed. Do not use XML attributes - put all data as tag content.
10 |
11 | Example response:
12 | Add user authentication system with JWT support `
13 |
14 | const SysDescribe = "You are an AI parser. You turn an AI's plan for a programming task into a structured description. You MUST call the 'describePlan' function with a valid JSON object that includes the 'commitMsg' key. 'commitMsg' should be a good, succinct commit message for the changes proposed. You must ALWAYS call the 'describePlan' function. Never call any other function."
15 |
16 | var DescribePlanFn = openai.FunctionDefinition{
17 | Name: "describePlan",
18 | Parameters: &jsonschema.Definition{
19 | Type: jsonschema.Object,
20 | Properties: map[string]jsonschema.Definition{
21 | "commitMsg": {
22 | Type: jsonschema.String,
23 | },
24 | },
25 | Required: []string{"commitMsg"},
26 | },
27 | }
28 |
29 | const SysPendingResults = "You are an AI commit message summarizer. You take a list of descriptions of pending changes and turn them into a succinct one-line summary of all the pending changes that makes for a good commit message title. Output ONLY this one-line title and nothing else."
30 |
31 |
--------------------------------------------------------------------------------
/app/server/model/prompts/missing_file.go:
--------------------------------------------------------------------------------
1 | package prompts
2 |
3 | import "fmt"
4 |
5 | func GetSkipMissingFilePrompt(path string) string {
6 | return fmt.Sprintf(`You *must not* generate content for the file %s. Skip this file and continue with the plan according to the 'Your instructions' section if there are any remaining tasks or subtasks. Don't repeat any part of the previous message. If there are no remaining tasks or subtasks, stop there.`, path)
7 | }
8 |
9 | func GetMissingFileContinueGeneratingPrompt(path string) string {
10 | return fmt.Sprintf("Continue generating the file '%s'. Continue EXACTLY where you left off in the previous message. Don't produce any other output before continuing or repeat any part of the previous message. Do *not* duplicate the last line of the previous response before continuing. Do *not* include an opening tag at the start of the response, since this has already been included in the previous message. Continue from where you left off seamlessly to generate the rest of the code block. You must include a closing tag at the end of the code block. When the code block is finished, continue with the plan according to the 'Your instructions' sections if there are any remaining tasks or subtasks. If there are no remaining tasks or subtasks, stop there. DO NOT UNDER ANY CIRCUMSTANCES INCLUDE THE FILE PATH OR THE OPENING TAG IN THE RESPONSE. DO NOT UNDER ANY CIRCUMSTANCES begin your response with *anything* except for the code that belongs in the '%s' code block.", path, path)
11 | }
12 |
--------------------------------------------------------------------------------
/app/server/model/prompts/shared.go:
--------------------------------------------------------------------------------
1 | package prompts
2 |
3 | const Identity = "You are Plandex, an AI programming and system administration assistant. You and the programmer collaborate to create a 'plan' for the task at hand."
4 |
--------------------------------------------------------------------------------
/app/server/model/tokens.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "plandex-server/types"
5 | shared "plandex-shared"
6 |
7 | "github.com/sashabaranov/go-openai"
8 | )
9 |
10 | const (
11 | // Per OpenAI's documentation:
12 | // Every message follows this format: {"role": "role_name", "content": "content"}
13 | // which has a 4-token overhead per message
14 | TokensPerMessage = 4
15 |
16 | // System, user, or assistant - each role name costs 1 token
17 | TokensPerName = 1
18 |
19 | // Tokens per request
20 | TokensPerRequest = 3
21 |
22 | TokensPerExtendedPart = 6
23 | )
24 |
25 | func GetMessagesTokenEstimate(messages ...types.ExtendedChatMessage) int {
26 | tokens := 0
27 |
28 | for _, msg := range messages {
29 | tokens += TokensPerMessage // Base message overhead
30 | tokens += TokensPerName // Role name
31 |
32 | if len(msg.Content) > 0 {
33 | // For each extended part, we need to account for the JSON structure
34 | // Each part follows format: {"type": "type_value", "text": "content"}
35 | // or {"type": "type_value", "image_url": {"url": "url_value"}}
36 | for _, part := range msg.Content {
37 | if part.Type == openai.ChatMessagePartTypeText {
38 | tokens += TokensPerExtendedPart // Overhead for the part object structure
39 | tokens += shared.GetNumTokensEstimate(part.Text)
40 | }
41 |
42 | // images are handled separately
43 |
44 | }
45 | }
46 |
47 | }
48 |
49 | return tokens
50 | }
51 |
--------------------------------------------------------------------------------
/app/server/notify/errors.go:
--------------------------------------------------------------------------------
1 | package notify
2 |
3 | // this allows Plandex Cloud to inject error monitoring
4 | // all non-streaming handlers are already wrapped with different logic, so this is only needed for errors in streaming handlers
5 |
6 | type Severity int
7 |
8 | const (
9 | SeverityInfo Severity = iota
10 | SeverityError
11 | )
12 |
13 | var NotifyErrFn func(severity Severity, data ...interface{})
14 |
15 | func RegisterNotifyErrFn(fn func(severity Severity, data ...interface{})) {
16 | NotifyErrFn = fn
17 | }
18 |
19 | func NotifyErr(severity Severity, data ...interface{}) {
20 | if NotifyErrFn != nil {
21 | NotifyErrFn(severity, data...)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/server/shutdown/shutdown.go:
--------------------------------------------------------------------------------
1 | package shutdown
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | var ShutdownCtx context.Context
8 | var ShutdownCancel context.CancelFunc
9 |
--------------------------------------------------------------------------------
/app/server/syntax/file_map/cli/.gitignore:
--------------------------------------------------------------------------------
1 | mapper
--------------------------------------------------------------------------------
/app/server/syntax/file_map/cli/go.mod:
--------------------------------------------------------------------------------
1 | module mapper
2 |
3 | go 1.23.3
4 |
5 | replace plandex-server => ../../../
6 |
7 | replace plandex-shared => ../../../../shared
8 |
9 | replace plandex => ../../../../cli
10 |
11 | require (
12 | plandex v0.0.0-00010101000000-000000000000
13 | plandex-server v0.0.0-00010101000000-000000000000
14 | plandex-shared v0.0.0-00010101000000-000000000000
15 | )
16 |
17 | require (
18 | github.com/davecgh/go-spew v1.1.1 // indirect
19 | github.com/dlclark/regexp2 v1.11.4 // indirect
20 | github.com/fatih/color v1.18.0 // indirect
21 | github.com/google/uuid v1.6.0 // indirect
22 | github.com/mattn/go-colorable v0.1.14 // indirect
23 | github.com/mattn/go-isatty v0.0.20 // indirect
24 | github.com/mattn/go-runewidth v0.0.16 // indirect
25 | github.com/olekukonko/tablewriter v0.0.5 // indirect
26 | github.com/pkoukk/tiktoken-go v0.1.7 // indirect
27 | github.com/rivo/uniseg v0.4.7 // indirect
28 | github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
29 | github.com/sashabaranov/go-openai v1.36.1 // indirect
30 | github.com/shopspring/decimal v1.4.0 // indirect
31 | github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 // indirect
32 | golang.org/x/image v0.23.0 // indirect
33 | golang.org/x/net v0.34.0 // indirect
34 | golang.org/x/sys v0.30.0 // indirect
35 | )
36 |
--------------------------------------------------------------------------------
/app/server/syntax/file_map/examples/bash_example.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Global variables
4 | GLOBAL_VAR="Hello World"
5 | readonly CONSTANT_VAR="This is constant"
6 |
7 | # Function definition
8 | function print_message() {
9 | local message="$1"
10 | echo "$message"
11 | }
12 |
13 | # Function with return value
14 | get_date() {
15 | echo $(date +%Y-%m-%d)
16 | }
17 |
18 | # Array declaration
19 | declare -a fruits=("apple" "banana" "orange")
20 |
21 | # Associative array
22 | declare -A user_info=(
23 | ["name"]="John"
24 | ["age"]="30"
25 | ["city"]="New York"
26 | )
27 |
28 | # Main script execution
29 | main() {
30 | print_message "$GLOBAL_VAR"
31 | current_date=$(get_date)
32 | echo "Today is: $current_date"
33 |
34 | # Loop through array
35 | for fruit in "${fruits[@]}"; do
36 | echo "Fruit: $fruit"
37 | done
38 |
39 | # Access associative array
40 | echo "User ${user_info[name]} is ${user_info[age]} years old"
41 | }
42 |
43 | # Call main function
44 | main
45 |
--------------------------------------------------------------------------------
/app/server/syntax/file_map/examples/dockerfile_example:
--------------------------------------------------------------------------------
1 | # Multi-stage build example
2 | FROM golang:1.21-alpine AS builder
3 |
4 | # Build arguments
5 | ARG VERSION=1.0.0
6 | ARG BUILD_DATE
7 |
8 | # Set working directory
9 | WORKDIR /app
10 |
11 | # Copy only necessary files for dependency resolution
12 | COPY go.mod go.sum ./
13 |
14 | # Download dependencies
15 | RUN go mod download
16 |
17 | # Copy source code
18 | COPY . .
19 |
20 | # Build the application
21 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-X main.Version=${VERSION} -X main.BuildDate=${BUILD_DATE}" -o /app/server
22 |
23 | # Create final lightweight image
24 | FROM alpine:latest
25 |
26 | # Labels for metadata
27 | LABEL maintainer="example@example.com" \
28 | version="${VERSION}" \
29 | description="Example Dockerfile with various syntax elements"
30 |
31 | # Environment variables
32 | ENV APP_ENV=production \
33 | PORT=8080
34 |
35 | # Create non-root user
36 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup
37 |
38 | # Install runtime dependencies
39 | RUN apk add --no-cache \
40 | ca-certificates \
41 | tzdata
42 |
43 | # Set working directory
44 | WORKDIR /app
45 |
46 | # Copy binary from builder stage
47 | COPY --from=builder /app/server .
48 |
49 | # Copy configuration files
50 | COPY config/production.yaml /etc/app/config.yaml
51 |
52 | # Create volume mount points
53 | VOLUME ["/data", "/logs"]
54 |
55 | # Expose ports
56 | EXPOSE 8080 8443
57 |
58 | # Switch to non-root user
59 | USER appuser
60 |
61 | # Health check
62 | HEALTHCHECK --interval=30s --timeout=3s \
63 | CMD wget --quiet --tries=1 --spider http://localhost:${PORT}/health || exit 1
64 |
65 | # Set entry point and default command
66 | ENTRYPOINT ["/app/server"]
67 | CMD ["--config", "/etc/app/config.yaml"]
68 |
--------------------------------------------------------------------------------
/app/server/types/auth.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "plandex-server/db"
5 |
6 | shared "plandex-shared"
7 | )
8 |
9 | type ServerAuth struct {
10 | AuthToken *db.AuthToken
11 | User *db.User
12 | OrgId string
13 | Permissions shared.Permissions
14 | }
15 |
16 | func (a *ServerAuth) HasPermission(permission shared.Permission) bool {
17 | return a.Permissions.HasPermission(permission)
18 | }
19 |
20 | func (a *ServerAuth) HasPermissionForResource(permission shared.Permission, resourceId string) bool {
21 | return a.Permissions.HasPermissionForResource(permission, resourceId)
22 | }
23 |
--------------------------------------------------------------------------------
/app/server/types/convo_message_desc.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "plandex-server/db"
5 |
6 | shared "plandex-shared"
7 | )
8 |
9 | func HasPendingBuilds(planDescs []*db.ConvoMessageDescription) bool {
10 | apiDescs := make([]*shared.ConvoMessageDescription, len(planDescs))
11 | for i, desc := range planDescs {
12 | apiDescs[i] = desc.ToApi()
13 | }
14 |
15 | return shared.HasPendingBuilds(apiDescs)
16 | }
17 |
--------------------------------------------------------------------------------
/app/server/types/exec_status.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | type ExecStatusResponse struct {
4 | Reasoning string `json:"reasoning"`
5 | SubtaskFinished bool `json:"subtaskFinished"`
6 | }
7 |
--------------------------------------------------------------------------------
/app/server/types/model.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | shared "plandex-shared"
5 | )
6 |
7 | type ChangesWithLineNums struct {
8 | Comments []struct {
9 | Txt string `json:"txt"`
10 | Reference bool `json:"reference"`
11 | }
12 | Changes []*shared.StreamedChangeWithLineNums `json:"changes"`
13 | }
14 |
--------------------------------------------------------------------------------
/app/server/types/reply_test_examples/4.md:
--------------------------------------------------------------------------------
1 | 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).
2 |
3 | Here is the structure we can follow:
4 |
5 | 1. 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.
6 | 2. A `SectionizeResponse` struct which contains a slice of `Section`.
7 |
8 | In `server/types/` directory, create a new `section.go` file.
9 |
10 | - server/types/section.go
11 |
12 |
13 | package types
14 |
15 | type Section struct {
16 | Name string
17 | Content string
18 | Subsection []Section
19 | }
20 |
21 | type SectionizeResponse struct {
22 | Sections []Section
23 | }
24 |
25 |
26 | Once you have checked and confirmed this task, I will proceed to the next task which is creating a sectionize function in the server model.
27 |
--------------------------------------------------------------------------------
/app/server/types/reply_test_examples/8.md:
--------------------------------------------------------------------------------
1 | **Creating the project structure and Makefile**
2 |
3 | First, I'll create a basic project structure with a Makefile that will compile our game.
4 |
5 | - Makefile:
6 |
7 | CC = gcc
8 | CFLAGS = -Wall -Wextra -I./include
9 | LDFLAGS = -framework OpenGL -framework GLUT
10 | SRC_DIR = src
11 | OBJ_DIR = obj
12 | BIN_DIR = bin
13 |
14 | SRCS = $(wildcard $(SRC_DIR)/*.c)
15 | OBJS = $(SRCS:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
16 | EXECUTABLE = $(BIN_DIR)/pong
17 |
18 | .PHONY: all clean directories
19 |
20 | all: directories $(EXECUTABLE)
21 |
22 | directories:
23 | @mkdir -p $(OBJ_DIR) $(BIN_DIR)
24 |
25 | $(EXECUTABLE): $(OBJS)
26 | $(CC) $(OBJS) -o $@ $(LDFLAGS)
27 |
28 | $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
29 | $(CC) $(CFLAGS) -c $< -o $@
30 |
31 | clean:
32 | rm -rf $(OBJ_DIR) $(BIN_DIR)
33 |
34 |
35 | - _apply.sh:
36 |
37 | # Create necessary directories if they don't exist
38 | mkdir -p src include obj bin
39 |
40 | # Check if Homebrew is installed
41 | if ! command -v brew &> /dev/null; then
42 | echo "Homebrew is not installed. Please install Homebrew first."
43 | exit 1
44 | fi
45 |
46 | # Install GLUT if not already installed
47 | if ! brew list | grep -q freeglut; then
48 | echo "Installing GLUT..."
49 | brew install freeglut
50 | fi
51 |
52 | # Compile the project
53 | make clean && make
54 |
55 | # Run the game
56 | ./bin/pong
57 |
58 |
59 | **Creating the project structure and Makefile** has been completed.
60 |
61 | Next, I'll implement the header files for game structures and constants.
62 |
--------------------------------------------------------------------------------
/app/server/types/reply_test_examples/9.md:
--------------------------------------------------------------------------------
1 | I'll move the game-related files to the game directory.
2 |
3 | ### Move Files
4 |
5 | - src/game.c → src/game/game.c
6 | - src/game.h → src/game/game.h
7 |
8 |
9 |
10 | Now I'll remove the README.md file as requested.
11 |
12 | ### Remove Files
13 |
14 | - src/README.md
15 |
16 |
17 |
18 | Then I'll reset the changes to the Makefile.
19 |
20 | ### Reset Changes
21 |
22 | - Makefile
23 |
24 |
25 |
26 | Now I'll write a new Makefile.
27 |
28 | - Makefile:
29 |
30 |
31 | # Create necessary directories if they don't exist
32 | mkdir -p src include obj bin
33 |
34 | # Check if Homebrew is installed
35 | if ! command -v brew &> /dev/null; then
36 | echo "Homebrew is not installed. Please install Homebrew first."
37 | exit 1
38 | fi
39 |
--------------------------------------------------------------------------------
/app/server/types/safe_map.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "sync"
4 |
5 | type SafeMap[V any] struct {
6 | items map[string]V
7 | mu sync.Mutex
8 | }
9 |
10 | func NewSafeMap[V any]() *SafeMap[V] {
11 | return &SafeMap[V]{items: make(map[string]V)}
12 | }
13 |
14 | func (sm *SafeMap[V]) Get(key string) V {
15 | sm.mu.Lock()
16 | defer sm.mu.Unlock()
17 | return sm.items[key]
18 | }
19 |
20 | func (sm *SafeMap[V]) Set(key string, value V) {
21 | sm.mu.Lock()
22 | defer sm.mu.Unlock()
23 | sm.items[key] = value
24 | }
25 |
26 | func (sm *SafeMap[V]) Delete(key string) {
27 | sm.mu.Lock()
28 | defer sm.mu.Unlock()
29 | delete(sm.items, key)
30 | }
31 |
32 | func (sm *SafeMap[V]) Update(key string, fn func(V)) {
33 | sm.mu.Lock()
34 | defer sm.mu.Unlock()
35 | if item, ok := sm.items[key]; ok {
36 | fn(item)
37 | sm.items[key] = item
38 | }
39 | }
40 |
41 | func (sm *SafeMap[V]) Items() map[string]V {
42 | sm.mu.Lock()
43 | defer sm.mu.Unlock()
44 | return sm.items
45 | }
46 |
47 | func (sm *SafeMap[V]) Keys() []string {
48 | sm.mu.Lock()
49 | defer sm.mu.Unlock()
50 | keys := make([]string, len(sm.items))
51 | i := 0
52 | for k := range sm.items {
53 | keys[i] = k
54 | i++
55 | }
56 | return keys
57 | }
58 |
59 | func (sm *SafeMap[V]) Len() int {
60 | sm.mu.Lock()
61 | defer sm.mu.Unlock()
62 | return len(sm.items)
63 | }
64 |
--------------------------------------------------------------------------------
/app/server/types/trial.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | const TrialMaxReplies = 20
4 | const TrialMaxPlans = 10
5 |
--------------------------------------------------------------------------------
/app/server/utils/whitespace.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "strings"
4 |
5 | func StripAddedBlankLines(orig, upd string) string {
6 | origLines := strings.Split(orig, "\n")
7 | updLines := strings.Split(upd, "\n")
8 |
9 | leadingOrig := 0
10 | for leadingOrig < len(origLines) && strings.TrimSpace(origLines[leadingOrig]) == "" {
11 | leadingOrig++
12 | }
13 |
14 | leadingUpd := 0
15 | for leadingUpd < len(updLines) && strings.TrimSpace(updLines[leadingUpd]) == "" {
16 | leadingUpd++
17 | }
18 |
19 | if leadingUpd > leadingOrig {
20 | updLines = updLines[leadingUpd-leadingOrig:] // trim surplus
21 | }
22 |
23 | trailingOrig := 0
24 | for trailingOrig < len(origLines) && strings.TrimSpace(origLines[len(origLines)-1-trailingOrig]) == "" {
25 | trailingOrig++
26 | }
27 |
28 | trailingUpd := 0
29 | for trailingUpd < len(updLines) && strings.TrimSpace(updLines[len(updLines)-1-trailingUpd]) == "" {
30 | trailingUpd++
31 | }
32 |
33 | if trailingUpd > trailingOrig {
34 | updLines = updLines[:len(updLines)-(trailingUpd-trailingOrig)]
35 | }
36 |
37 | return strings.Join(updLines, "\n")
38 | }
39 |
--------------------------------------------------------------------------------
/app/server/utils/whitespace_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "testing"
4 |
5 | func TestStripAddedBlankLines(t *testing.T) {
6 | tcs := []struct {
7 | name string
8 | orig string
9 | upd string
10 | want string
11 | }{
12 | {
13 | name: "no change",
14 | orig: "a\nb\nc\n",
15 | upd: "a\nb\nc\n",
16 | want: "a\nb\nc\n",
17 | },
18 | {
19 | name: "leading newline added",
20 | orig: "a\nb\n",
21 | upd: "\n\na\nb\n",
22 | want: "a\nb\n",
23 | },
24 | {
25 | name: "trailing newline added",
26 | orig: "a\nb\n",
27 | upd: "a\nb\n\n",
28 | want: "a\nb\n",
29 | },
30 | {
31 | name: "both ends, keep original padding",
32 | orig: "\nfoo\nbar\n\n",
33 | upd: "\n\nfoo\nbar\n\n\n",
34 | want: "\nfoo\nbar\n\n",
35 | },
36 | }
37 |
38 | for _, tc := range tcs {
39 | t.Run(tc.name, func(t *testing.T) {
40 | got := StripAddedBlankLines(tc.orig, tc.upd)
41 | if got != tc.want {
42 | t.Fatalf("\norig:\n%q\nupd:\n%q\nwant:\n%q\ngot:\n%q",
43 | tc.orig, tc.upd, tc.want, got)
44 | }
45 | })
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/server/version.txt:
--------------------------------------------------------------------------------
1 | 2.1.6+1
2 |
--------------------------------------------------------------------------------
/app/shared/ai_models_compatibility.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | var fullCompatibility = ModelCompatibility{
4 | HasImageSupport: true,
5 | }
6 |
7 | var RequiredCompatibilityByRole = map[ModelRole]ModelCompatibility{
8 | ModelRolePlanner: {},
9 | ModelRolePlanSummary: {},
10 | ModelRoleBuilder: {},
11 | ModelRoleName: {},
12 | ModelRoleCommitMsg: {},
13 | ModelRoleExecStatus: {},
14 | ModelRoleArchitect: {},
15 | ModelRoleCoder: {},
16 | ModelRoleWholeFileBuilder: {},
17 | }
18 |
19 | func FilterCompatibleModels(models []*AvailableModel, role ModelRole) []*AvailableModel {
20 | // required := RequiredCompatibilityByRole[role]
21 | var compatibleModels []*AvailableModel
22 |
23 | for _, model := range models {
24 | // no compatibility checks are needed in v2, but keeping this here in case compatibility checks are needed in the future
25 |
26 | compatibleModels = append(compatibleModels, model)
27 | }
28 |
29 | return compatibleModels
30 | }
31 |
--------------------------------------------------------------------------------
/app/shared/ai_models_config.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | func getPlannerModelConfig(provider ModelProvider, modelId ModelId) PlannerModelConfig {
4 | return PlannerModelConfig{
5 | MaxConvoTokens: GetAvailableModel(provider, modelId).DefaultMaxConvoTokens,
6 | }
7 | }
8 |
9 | var DefaultConfigByRole = map[ModelRole]ModelRoleConfig{
10 | ModelRolePlanner: {
11 | Temperature: 0.3,
12 | TopP: 0.3,
13 | },
14 | ModelRoleCoder: {
15 | Temperature: 0.3,
16 | TopP: 0.3,
17 | },
18 | ModelRoleArchitect: {
19 | Temperature: 0.3,
20 | TopP: 0.3,
21 | },
22 | ModelRolePlanSummary: {
23 | Temperature: 0.2,
24 | TopP: 0.2,
25 | },
26 | ModelRoleBuilder: {
27 | Temperature: 0.1,
28 | TopP: 0.1,
29 | },
30 | ModelRoleWholeFileBuilder: {
31 | Temperature: 0.1,
32 | TopP: 0.1,
33 | },
34 | ModelRoleName: {
35 | Temperature: 0.8,
36 | TopP: 0.5,
37 | },
38 | ModelRoleCommitMsg: {
39 | Temperature: 0.8,
40 | TopP: 0.5,
41 | },
42 | ModelRoleExecStatus: {
43 | Temperature: 0.1,
44 | TopP: 0.1,
45 | },
46 | }
47 |
--------------------------------------------------------------------------------
/app/shared/ai_models_errors.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | type ModelErrKind string
4 |
5 | const (
6 | ErrOverloaded ModelErrKind = "ErrOverloaded"
7 | ErrContextTooLong ModelErrKind = "ErrContextTooLong"
8 | ErrRateLimited ModelErrKind = "ErrRateLimited"
9 | ErrOther ModelErrKind = "ErrOther"
10 | ErrCacheSupport ModelErrKind = "ErrCacheSupport"
11 | )
12 |
13 | type ModelError struct {
14 | Kind ModelErrKind
15 | Retriable bool
16 | RetryAfterSeconds int
17 | }
18 |
19 | // if fallback is defined, retry with main model, then remaining tries use error fallback
20 | type FallbackType string
21 |
22 | const (
23 | FallbackTypeError FallbackType = "error"
24 | FallbackTypeContext FallbackType = "context"
25 | )
26 |
27 | type FallbackResult struct {
28 | ModelRoleConfig *ModelRoleConfig
29 | HasErrorFallback bool
30 | IsFallback bool
31 | FallbackType FallbackType
32 | }
33 |
34 | const MAX_RETRIES_BEFORE_FALLBACK = 1
35 |
36 | func (m *ModelRoleConfig) GetFallbackForModelError(numTotalRetry int, modelErr *ModelError) FallbackResult {
37 | if m == nil || modelErr == nil {
38 | return FallbackResult{
39 | ModelRoleConfig: m,
40 | IsFallback: false,
41 | }
42 | }
43 | if modelErr.Kind == ErrContextTooLong {
44 | if m.LargeContextFallback != nil {
45 | return FallbackResult{
46 | ModelRoleConfig: m.LargeContextFallback,
47 | FallbackType: FallbackTypeContext,
48 | IsFallback: true,
49 | }
50 | }
51 | } else if !modelErr.Retriable || numTotalRetry > MAX_RETRIES_BEFORE_FALLBACK {
52 | if m.ErrorFallback != nil {
53 | return FallbackResult{
54 | ModelRoleConfig: m.ErrorFallback,
55 | FallbackType: FallbackTypeError,
56 | IsFallback: true,
57 | }
58 | }
59 | }
60 |
61 | return FallbackResult{
62 | ModelRoleConfig: m,
63 | IsFallback: false,
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/shared/ai_models_openrouter.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | type OpenRouterFamily string
4 |
5 | const (
6 | OpenRouterFamilyAnthropic OpenRouterFamily = "anthropic"
7 | OpenRouterFamilyGoogle OpenRouterFamily = "google"
8 | OpenRouterFamilyOpenAI OpenRouterFamily = "openai"
9 | OpenRouterFamilyQwen OpenRouterFamily = "qwen"
10 | OpenRouterFamilyDeepSeek OpenRouterFamily = "deepseek"
11 | )
12 |
13 | // type OpenRouterProvider string
14 |
15 | // const (
16 | // OpenRouterProviderAnthropic OpenRouterProvider = "Anthropic"
17 | // OpenRouterProviderGoogle OpenRouterProvider = "Google Vertex"
18 | // OpenRouterProviderOpenAI OpenRouterProvider = "OpenAI"
19 | // OpenRouterProviderDeepSeek OpenRouterProvider = "DeepSeek"
20 | // OpenRouterProviderQwen OpenRouterProvider = "Hyperbolic"
21 | // OpenRouterProviderDeepInfra OpenRouterProvider = "DeepInfra"
22 | // OpenRouterProviderFireworks OpenRouterProvider = "Fireworks"
23 | // )
24 |
25 | // var DefaultOpenRouterProvidersByFamily = map[OpenRouterFamily][]OpenRouterProvider{
26 | // OpenRouterFamilyAnthropic: {OpenRouterProviderAnthropic},
27 | // OpenRouterFamilyGoogle: {OpenRouterProviderGoogle},
28 | // OpenRouterFamilyOpenAI: {OpenRouterProviderOpenAI},
29 | // OpenRouterFamilyQwen: {OpenRouterProviderQwen},
30 | // OpenRouterFamilyDeepSeek: {OpenRouterProviderDeepSeek},
31 | // }
32 |
33 | // // open source models don't have fallbacks enabled for now because pricing and context limits aren't predictable across providers
34 | // var DefaultOpenRouterAllowFallbacksByFamily = map[OpenRouterFamily]bool{
35 | // OpenRouterFamilyAnthropic: true,
36 | // OpenRouterFamilyGoogle: true,
37 | // OpenRouterFamilyOpenAI: false,
38 | // }
39 |
--------------------------------------------------------------------------------
/app/shared/ai_models_providers.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | const OpenAIEnvVar = "OPENAI_API_KEY"
4 | const OpenAIV1BaseUrl = "https://api.openai.com/v1"
5 | const OpenRouterApiKeyEnvVar = "OPENROUTER_API_KEY"
6 | const OpenRouterBaseUrl = "https://openrouter.ai/api/v1"
7 |
8 | type ModelProvider string
9 |
10 | const (
11 | ModelProviderOpenRouter ModelProvider = "openrouter"
12 | ModelProviderOpenAI ModelProvider = "openai"
13 | ModelProviderCustom ModelProvider = "custom"
14 | )
15 |
16 | var AllModelProviders = []string{
17 | string(ModelProviderOpenAI),
18 | string(ModelProviderOpenRouter),
19 | // string(ModelProviderTogether),
20 | string(ModelProviderCustom),
21 | }
22 |
23 | var BaseUrlByProvider = map[ModelProvider]string{
24 | ModelProviderOpenAI: OpenAIV1BaseUrl,
25 | ModelProviderOpenRouter: OpenRouterBaseUrl,
26 | }
27 |
28 | var ApiKeyByProvider = map[ModelProvider]string{
29 | ModelProviderOpenAI: OpenAIEnvVar,
30 | ModelProviderOpenRouter: OpenRouterApiKeyEnvVar,
31 | }
32 |
--------------------------------------------------------------------------------
/app/shared/ai_models_roles.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | type ModelRole string
4 |
5 | const (
6 | ModelRolePlanner ModelRole = "planner"
7 | ModelRoleCoder ModelRole = "coder"
8 | ModelRoleArchitect ModelRole = "architect"
9 | ModelRolePlanSummary ModelRole = "summarizer"
10 | ModelRoleBuilder ModelRole = "builder"
11 | ModelRoleWholeFileBuilder ModelRole = "whole-file-builder"
12 | ModelRoleName ModelRole = "names"
13 | ModelRoleCommitMsg ModelRole = "commit-messages"
14 | ModelRoleExecStatus ModelRole = "auto-continue"
15 | )
16 |
17 | var AllModelRoles = []ModelRole{ModelRolePlanner, ModelRoleCoder, ModelRoleArchitect, ModelRolePlanSummary, ModelRoleBuilder, ModelRoleWholeFileBuilder, ModelRoleName, ModelRoleCommitMsg, ModelRoleExecStatus}
18 |
19 | var ModelRoleDescriptions = map[ModelRole]string{
20 | ModelRolePlanner: "replies to prompts and makes plans",
21 | ModelRoleCoder: "writes code to implement a plan",
22 | ModelRolePlanSummary: "summarizes conversations exceeding max-convo-tokens",
23 | ModelRoleBuilder: "builds a plan into file diffs",
24 | ModelRoleWholeFileBuilder: "builds a plan into file diffs by writing the entire file",
25 | ModelRoleName: "names plans",
26 | ModelRoleCommitMsg: "writes commit messages",
27 | ModelRoleExecStatus: "determines whether to auto-continue",
28 | ModelRoleArchitect: "makes high level plan and decides what context to load using codebase map",
29 | }
30 |
--------------------------------------------------------------------------------
/app/shared/convo_message.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | func (f *ConvoMessageFlags) GetReplyTags() []string {
4 | var replyTags []string
5 | if f.DidLoadContext {
6 | replyTags = append(replyTags, "📥 Loaded Context")
7 | }
8 | if f.DidMakePlan {
9 | if f.DidMakeDebuggingPlan {
10 | replyTags = append(replyTags, "🐞 Made Debug Plan")
11 | } else if f.DidRemoveTasks {
12 | replyTags = append(replyTags, "🔄 Revised Plan")
13 | } else {
14 | replyTags = append(replyTags, "📋 Made Plan")
15 | }
16 | }
17 | if f.DidWriteCode {
18 | replyTags = append(replyTags, "👨💻 Wrote Code")
19 | }
20 | // if f.DidCompleteTask {
21 | // replyTags = append(replyTags, "✅")
22 | // }
23 | if f.DidCompletePlan {
24 | replyTags = append(replyTags, "🏁")
25 | }
26 |
27 | if f.HasError {
28 | replyTags = append(replyTags, "🚨 Error")
29 | }
30 |
31 | return replyTags
32 | }
33 |
--------------------------------------------------------------------------------
/app/shared/file_maps.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 | )
8 |
9 | func (m FileMapBodies) CombinedMap(tokensByPath map[string]int) string {
10 | var combinedMap strings.Builder
11 | paths := make([]string, 0, len(m))
12 | for path := range m {
13 | paths = append(paths, path)
14 | }
15 | sort.Strings(paths)
16 | for _, path := range paths {
17 | body := m[path]
18 | body = strings.TrimSpace(body)
19 | fileHeading := MapFileHeading(path, tokensByPath[path])
20 | combinedMap.WriteString(fileHeading)
21 | if body == "" {
22 | combinedMap.WriteString("[NO MAP]")
23 | } else {
24 | combinedMap.WriteString(body)
25 | }
26 | combinedMap.WriteString("\n")
27 | }
28 | return combinedMap.String()
29 | }
30 |
31 | func MapFileHeading(path string, tokens int) string {
32 | return fmt.Sprintf("\n### %s (%d 🪙)\n\n", path, tokens)
33 | }
34 |
--------------------------------------------------------------------------------
/app/shared/go.mod:
--------------------------------------------------------------------------------
1 | module plandex-shared
2 |
3 | go 1.23.3
4 |
5 | require (
6 | github.com/davecgh/go-spew v1.1.1
7 | github.com/pkoukk/tiktoken-go v0.1.7
8 | )
9 |
10 | require (
11 | github.com/mattn/go-runewidth v0.0.16 // indirect
12 | github.com/rivo/uniseg v0.4.7 // indirect
13 | )
14 |
15 | require (
16 | github.com/dlclark/regexp2 v1.11.5 // indirect
17 | github.com/google/uuid v1.6.0 // indirect
18 | github.com/olekukonko/tablewriter v0.0.5
19 | github.com/sashabaranov/go-openai v1.38.1
20 | github.com/shopspring/decimal v1.4.0
21 | golang.org/x/image v0.25.0
22 | )
23 |
--------------------------------------------------------------------------------
/app/shared/plan_result_exec_history.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | func (state *CurrentPlanState) ExecHistory() string {
4 | execHistory := ""
5 |
6 | if state.PlanResult == nil {
7 | return execHistory
8 | }
9 |
10 | for _, result := range state.PlanResult.Results {
11 | if result.Path == "_apply.sh" && result.AppliedAt != nil {
12 | execHistory += "Previously executed _apply.sh:\n\n```\n" + result.Content + "\n```\n\n"
13 | }
14 | }
15 |
16 | return execHistory
17 | }
18 |
--------------------------------------------------------------------------------
/app/shared/plan_status.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | type PlanStatus string
4 |
5 | const (
6 | PlanStatusDraft PlanStatus = "draft"
7 | PlanStatusReplying PlanStatus = "replying"
8 | PlanStatusDescribing PlanStatus = "describing"
9 | PlanStatusBuilding PlanStatus = "building"
10 | PlanStatusMissingFile PlanStatus = "missingFile"
11 | PlanStatusFinished PlanStatus = "finished"
12 | PlanStatusStopped PlanStatus = "stopped"
13 | PlanStatusError PlanStatus = "error"
14 | )
15 |
--------------------------------------------------------------------------------
/app/shared/rbac.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | type Permission string
8 |
9 | const (
10 | PermissionDeleteOrg Permission = "delete_org"
11 | PermissionManageEmailDomainAuth Permission = "manage_email_domain_auth"
12 | PermissionManageBilling Permission = "manage_billing"
13 | PermissionInviteUser Permission = "invite_user"
14 | PermissionRemoveUser Permission = "remove_user"
15 | PermissionSetUserRole Permission = "set_user_role"
16 | PermissionListOrgRoles Permission = "list_org_roles"
17 | PermissionCreateProject Permission = "create_project"
18 | PermissionRenameAnyProject Permission = "rename_any_project"
19 | PermissionDeleteAnyProject Permission = "delete_any_project"
20 | PermissionCreatePlan Permission = "create_plan"
21 | PermissionManageAnyPlanShares Permission = "manage_any_plan_shares"
22 | PermissionRenameAnyPlan Permission = "rename_any_plan"
23 | PermissionDeleteAnyPlan Permission = "delete_any_plan"
24 | PermissionUpdateAnyPlan Permission = "update_any_plan"
25 | PermissionArchiveAnyPlan Permission = "archive_any_plan"
26 | )
27 |
28 | type Permissions map[string]bool
29 |
30 | func (perms Permissions) HasPermission(permission Permission) bool {
31 | for p := range perms {
32 | split := strings.Split(p, "|")
33 | perm := Permission(split[0])
34 |
35 | if perm == permission {
36 | return true
37 | }
38 | }
39 | return false
40 | }
41 |
42 | func (perms Permissions) HasPermissionForResource(permission Permission, resourceId string) bool {
43 | for p := range perms {
44 | split := strings.Split(p, "|")
45 | perm := Permission(split[0])
46 | resId := split[1]
47 |
48 | if perm == permission && resId == resourceId {
49 | return true
50 | }
51 | }
52 | return false
53 | }
54 |
--------------------------------------------------------------------------------
/app/shared/tokens.go:
--------------------------------------------------------------------------------
1 | package shared
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/pkoukk/tiktoken-go"
7 | )
8 |
9 | var tkm *tiktoken.Tiktoken
10 |
11 | const EstimatedBytesPerToken = 4
12 |
13 | func init() {
14 | var err error
15 | tkm, err = tiktoken.EncodingForModel("gpt-4o")
16 | if err != nil {
17 | panic(fmt.Sprintf("error getting encoding for model: %v", err))
18 | }
19 | }
20 |
21 | func GetNumTokensEstimate(text string) int {
22 | return len(tkm.Encode(text, nil, nil))
23 | }
24 |
25 | func GetFastNumTokensEstimate(text string) int {
26 | return GetBytesToTokensEstimate(int64(len(text)))
27 | }
28 |
29 | func GetBytesToTokensEstimate(bytes int64) int {
30 | return int(bytes / EstimatedBytesPerToken)
31 | }
32 |
--------------------------------------------------------------------------------
/app/shared/tygo.yaml:
--------------------------------------------------------------------------------
1 | # You can specify more than one package
2 | packages:
3 | # The package path just like you would import it in Go
4 | - path: "shared"
5 |
6 | # Where this output should be written to.
7 | # 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.
8 | output_path: "../server/cloud/ui/node/src/types/generated.ts"
9 | include_files:
10 | - "data_models.go"
11 | - "plan_settings.go"
12 | - "streamed_change.go"
13 | - "plan_status.go"
14 |
15 | type_mappings:
16 | time.Time: "string /* RFC3339 */"
17 | decimal.Decimal: "string /* decimal.Decimal */"
18 | openai.ImageURLDetail: '"high" | "low" | "auto"'
19 |
--------------------------------------------------------------------------------
/app/start_local.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Get the absolute path to the script's directory, regardless of where it's run from
4 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
5 |
6 | # Change to the app directory if we're not already there
7 | cd "$SCRIPT_DIR"
8 |
9 | echo "Checking dependencies..."
10 |
11 | if ! [ -x "$(command -v git)" ]; then
12 | echo 'Error: git is not installed.' >&2
13 | echo 'Please install git before running this setup script.' >&2
14 | exit 1
15 | fi
16 |
17 | if ! [ -x "$(command -v docker)" ]; then
18 | echo 'Error: docker is not installed.' >&2
19 | echo 'Please install docker before running this setup script.' >&2
20 | exit 1
21 | fi
22 |
23 | if ! [ -x "$(command -v docker-compose)" ]; then
24 | docker compose 2>&1 > /dev/null
25 | if [[ $? -ne 0 ]]; then
26 | echo 'Error: docker-compose is not installed.' >&2
27 | echo 'Please install docker-compose before running this setup script.' >&2
28 | exit 1
29 | fi
30 | fi
31 |
32 | echo "Starting the local Plandex server and database..."
33 |
34 | docker compose pull plandex-server
35 | docker compose up
36 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/docs/blog/2019-05-28-first-blog-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: first-blog-post
3 | title: First Blog Post
4 | authors:
5 | name: Gao Wei
6 | title: Docusaurus Core Team
7 | url: https://github.com/wgao19
8 | image_url: https://github.com/wgao19.png
9 | tags: [hola, docusaurus]
10 | ---
11 |
12 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
13 |
--------------------------------------------------------------------------------
/docs/blog/2021-08-01-mdx-blog-post.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: mdx-blog-post
3 | title: MDX Blog Post
4 | authors: [slorber]
5 | tags: [docusaurus]
6 | ---
7 |
8 | Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
9 |
10 | :::tip
11 |
12 | Use the power of React to create interactive blog posts.
13 |
14 | ```js
15 | alert('button clicked!')}>Click me!
16 | ```
17 |
18 | alert('button clicked!')}>Click me!
19 |
20 | :::
21 |
--------------------------------------------------------------------------------
/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg
--------------------------------------------------------------------------------
/docs/blog/2021-08-26-welcome/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: welcome
3 | title: Welcome
4 | authors: [slorber, yangshun]
5 | tags: [facebook, hello, docusaurus]
6 | ---
7 |
8 | [Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
9 |
10 | Simply add Markdown files (or folders) to the `blog` directory.
11 |
12 | Regular blog authors can be added to `authors.yml`.
13 |
14 | The blog post date can be extracted from filenames, such as:
15 |
16 | - `2019-05-30-welcome.md`
17 | - `2019-05-30-welcome/index.md`
18 |
19 | A blog post folder can be convenient to co-locate blog post images:
20 |
21 | 
22 |
23 | The blog supports tags as well!
24 |
25 | **And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
26 |
--------------------------------------------------------------------------------
/docs/blog/authors.yml:
--------------------------------------------------------------------------------
1 | endi:
2 | name: Endilie Yacop Sucipto
3 | title: Maintainer of Docusaurus
4 | url: https://github.com/endiliey
5 | image_url: https://github.com/endiliey.png
6 |
7 | yangshun:
8 | name: Yangshun Tay
9 | title: Front End Engineer @ Facebook
10 | url: https://github.com/yangshun
11 | image_url: https://github.com/yangshun.png
12 |
13 | slorber:
14 | name: Sébastien Lorber
15 | title: Docusaurus maintainer
16 | url: https://sebastienlorber.com
17 | image_url: https://github.com/slorber.png
18 |
--------------------------------------------------------------------------------
/docs/blog/tags.yml:
--------------------------------------------------------------------------------
1 | facebook:
2 | label: Facebook
3 | permalink: /facebook
4 | description: Facebook tag description
5 | hello:
6 | label: Hello
7 | permalink: /hello
8 | description: Hello tag description
9 | docusaurus:
10 | label: Docusaurus
11 | permalink: /docusaurus
12 | description: Docusaurus tag description
13 | hola:
14 | label: Hola
15 | permalink: /hola
16 | description: Hola tag description
17 |
--------------------------------------------------------------------------------
/docs/docs/core-concepts/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Core Concepts",
3 | "position": 5,
4 | "collapsible": true,
5 | "collapsed": false
6 | }
--------------------------------------------------------------------------------
/docs/docs/core-concepts/background-tasks.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 12
3 | sidebar_label: Background Tasks
4 | ---
5 |
6 | # Background Tasks
7 |
8 | Plandex allows you to run tasks in the background, helping you work on multiple tasks in parallel.
9 |
10 | **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.
11 |
12 | ## Running a Task in the Background
13 |
14 | To run a task in the background, use the `--bg` flag with `plandex tell` or `plandex continue`.
15 |
16 | ```bash
17 | plandex tell --bg "Add an update credit card form to 'src/components/billing'"
18 | plandex continue --bg
19 | ```
20 |
21 | The plandex stream TUI also has a `b` hotkey that allows you to send a streaming plan to the background.
22 |
23 | ## Listing Background Tasks
24 |
25 | To list active and recently finished background tasks, use the `plandex ps` command:
26 |
27 | ```bash
28 | plandex ps
29 | ```
30 |
31 | ## Connecting to a Background Task
32 |
33 | To connect to a running background task and view its stream in the plan stream TUI, use the `plandex connect` command:
34 |
35 | ```bash
36 | plandex connect
37 | ```
38 |
39 | ## Stopping a Background Task
40 |
41 | To stop a running background task, use the `plandex stop` command:
42 |
43 | ```bash
44 | plandex stop
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/docs/core-concepts/branches.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 8
3 | sidebar_label: Branches
4 | ---
5 |
6 | # Branches
7 |
8 | Branches 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:
9 |
10 | - Comparing different prompting strategies.
11 | - Comparing results with different files in context.
12 | - Comparing results with different models or model-settings.
13 | - Using `plandex rewind` without losing history (first check out a new branch, then rewind).
14 |
15 | ## Creating a Branch
16 |
17 | To create a new branch, use the `plandex checkout` command:
18 |
19 | ```bash
20 | plandex checkout new-branch
21 | pdxd new-branch # alias
22 | ```
23 |
24 | ## Switching Branches
25 |
26 | To switch to a different branch, also use the `plandex checkout` command:
27 |
28 | ```bash
29 | plandex checkout existing-branch
30 | ```
31 |
32 | ## Listing Branches
33 |
34 | To list all branches, use the `plandex branches` command:
35 |
36 | ```bash
37 | plandex branches
38 | ```
39 |
40 | ## Deleting a Branch
41 |
42 | To delete a branch, use the `plandex delete-branch` command:
43 |
44 | ```bash
45 | plandex delete-branch branch-name
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/docs/core-concepts/conversations.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 9
3 | sidebar_label: Conversations
4 | ---
5 |
6 | # Conversations
7 |
8 | Each 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).
9 |
10 | ## Conversation History
11 |
12 | You can see the full conversation history with the `convo` command.
13 |
14 | ```bash
15 | plandex convo # show the full conversation history
16 | ```
17 |
18 | You can output the conversation in plain text with no ANSI codes with the `--plain` or `-p` flag.
19 |
20 | ```bash
21 | plandex convo --plain
22 | ```
23 |
24 | You can also show a specific message number or range of messages.
25 |
26 | ```bash
27 | plandex convo 1 # show the initial prompt
28 | plandex convo 1-5 # show messages 1 through 5
29 | plandex convo 2- # show messages 2 through the end of the conversation
30 | ```
31 |
32 | ## Conversation Summaries
33 |
34 | Every 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.
35 |
36 | Summaries 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.
37 |
38 | You can see the latest summary with the `summary` command.
39 |
40 | ```bash
41 | plandex summary # show the latest conversation summary
42 | ```
43 |
44 | As with the `convo` command, you can output the summary in plain text with no ANSI codes with the `--plain` or `-p` flag.
45 |
46 | ```bash
47 | plandex summary --plain
48 | ```
49 |
--------------------------------------------------------------------------------
/docs/docs/core-concepts/orgs.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 13
3 | sidebar_label: Collaboration / Orgs
4 | ---
5 |
6 | # Collaboration and Orgs
7 |
8 | While 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.
9 |
10 | ## Multiple Users
11 |
12 | Orgs 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.
13 |
14 | ## Domain Access
15 |
16 | When 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.
17 |
18 | ## Invitations
19 |
20 | If 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`:
21 |
22 | ```bash
23 | plandex invite
24 | ```
25 |
26 | ## Joining an Org
27 |
28 | To join an org you've been invited to, use `plandex sign-in`:
29 |
30 | ```bash
31 | plandex sign-in
32 | ```
33 |
34 | ## Listing Users and Invites
35 |
36 | To list users and pending invites, use `plandex users`:
37 |
38 | ```bash
39 | plandex users
40 | ```
41 |
42 | ## Revoking Users and Invites
43 |
44 | To revoke an invite or remove a user, use `plandex revoke`:
45 |
46 | ```bash
47 | plandex revoke
48 | ```
49 |
--------------------------------------------------------------------------------
/docs/docs/hosting/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Hosting",
3 | "position": 7,
4 | "collapsible": true,
5 | "collapsed": false
6 | }
--------------------------------------------------------------------------------
/docs/docs/hosting/self-hosting/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Self-Hosting",
3 | "position": 1,
4 | "collapsible": true,
5 | "collapsed": false
6 | }
--------------------------------------------------------------------------------
/docs/docs/install.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | sidebar_label: Install
4 | ---
5 |
6 | # Install Plandex
7 |
8 | ## Quick Install
9 |
10 | ```bash
11 | curl -sL https://plandex.ai/install.sh | bash
12 | ```
13 |
14 | ## Manual install
15 |
16 | Grab the appropriate binary for your platform from the latest [release](https://github.com/plandex-ai/plandex/releases) and put it somewhere in your `PATH`.
17 |
18 | ## Build from source
19 |
20 | ```bash
21 | git clone https://github.com/plandex-ai/plandex.git
22 | cd plandex/app/cli
23 | go build -ldflags "-X plandex/version.Version=$(cat version.txt)"
24 | mv plandex /usr/local/bin # adapt as needed for your system
25 | ```
26 |
27 | ## Windows
28 |
29 | Windows is supported via [WSL](https://learn.microsoft.com/en-us/windows/wsl/about).
30 |
31 | Plandex only works correctly in the WSL shell. It doesn't work in the Windows CMD prompt or PowerShell.
32 |
33 | ## Upgrading from v1 to v2
34 |
35 | When you install the Plandex v2 CLI with the quick install script, it will rename your existing `plandex` command to `plandex1` (and the `pdx` alias to `pdx1`). Plandex v2 is designed to run *separately* from v1 rather than upgrading in place. [More details here.](./upgrading-v1-to-v2.md)
--------------------------------------------------------------------------------
/docs/docs/models/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Models",
3 | "position": 6,
4 | "collapsible": true,
5 | "collapsed": false
6 | }
--------------------------------------------------------------------------------
/docs/docs/security.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 9
3 | sidebar_label: Security
4 | ---
5 |
6 | # Security
7 | ## Ignoring Sensitive Files
8 |
9 | Plandex 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.
10 |
11 | ## API Key Security
12 |
13 | When [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.
14 |
15 | It'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.
16 |
17 | You 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.
--------------------------------------------------------------------------------
/docs/docs/upgrading-v1-to-v2.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | sidebar_label: From v1 to v2
4 | ---
5 |
6 | # Moving from v1 to v2
7 |
8 | While built on the same foundations, Plandex v2 is designed to be 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 [install](./install.md) v2 as if you were a brand new user.
9 |
10 | ## Future Versions
11 |
12 | While it would have been nice to maintain compatibility, the large scope of changes and experimental nature of v1 made it more trouble than it was worth.
13 |
14 | That said, going forward, the intention is to release future versions as in-place upgrades so that all your projects and data will remain in one place.
15 |
16 | ## Feedback
17 |
18 | If you're a v1 user and you really want to bring your existing plans into v2, please [speak up on Discord](https://discord.gg/plandex-ai) or [on GitHub](https://github.com/plandex-ai/plandex/discussions). This is doable, but I want to be sure there's demand for it before putting in the work.
19 |
20 | ## What's New?
21 |
22 | Check out the [2.0.0 release notes](https://github.com/plandex-ai/plandex/releases/tag/v2.0.0) for an overview of what's new in v2. There's a lot!
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids",
15 | "typecheck": "tsc"
16 | },
17 | "dependencies": {
18 | "@docusaurus/core": "3.4.0",
19 | "@docusaurus/plugin-client-redirects": "^3.4.0",
20 | "@docusaurus/preset-classic": "3.4.0",
21 | "@docusaurus/theme-search-algolia": "^3.4.0",
22 | "@mdx-js/react": "^3.0.0",
23 | "clsx": "^2.0.0",
24 | "docusaurus-lunr-search": "^3.4.0",
25 | "prism-react-renderer": "^2.3.0",
26 | "react": "^18.0.0",
27 | "react-dom": "^18.0.0"
28 | },
29 | "devDependencies": {
30 | "@docusaurus/module-type-aliases": "3.4.0",
31 | "@docusaurus/tsconfig": "3.4.0",
32 | "@docusaurus/types": "3.4.0",
33 | "typescript": "~5.2.2"
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.5%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 3 chrome version",
43 | "last 3 firefox version",
44 | "last 5 safari version"
45 | ]
46 | },
47 | "engines": {
48 | "node": ">=18.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/docs/sidebars.ts:
--------------------------------------------------------------------------------
1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
2 |
3 | /**
4 | * Creating a sidebar enables you to:
5 | - create an ordered group of docs
6 | - render a sidebar for each doc of that group
7 | - provide next/previous navigation
8 |
9 | The sidebars can be generated from the filesystem, or explicitly defined here.
10 |
11 | Create as many sidebars as you want.
12 | */
13 | const sidebars: SidebarsConfig = {
14 | // By default, Docusaurus generates a sidebar from the docs folder structure
15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
16 |
17 | // But you can create a sidebar manually
18 | /*
19 | tutorialSidebar: [
20 | 'intro',
21 | 'hello',
22 | {
23 | type: 'category',
24 | label: 'Tutorial',
25 | items: ['tutorial-basics/create-a-document'],
26 | },
27 | ],
28 | */
29 | };
30 |
31 | export default sidebars;
32 |
--------------------------------------------------------------------------------
/docs/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #2e8555;
10 | --ifm-color-primary-dark: #29784c;
11 | --ifm-color-primary-darker: #277148;
12 | --ifm-color-primary-darkest: #205d3b;
13 | --ifm-color-primary-light: #33925d;
14 | --ifm-color-primary-lighter: #359962;
15 | --ifm-color-primary-lightest: #3cad6e;
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 | }
19 |
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme='dark'] {
22 | --ifm-color-primary: #25c2a0;
23 | --ifm-color-primary-dark: #21af90;
24 | --ifm-color-primary-darker: #1fa588;
25 | --ifm-color-primary-darkest: #1a8870;
26 | --ifm-color-primary-light: #29d5b0;
27 | --ifm-color-primary-lighter: #32d8b4;
28 | --ifm-color-primary-lightest: #4fddbf;
29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
30 | }
31 |
32 | .navbar__logo img {
33 | height: 145%;
34 | margin-top: -6px;
35 | }
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/docs/static/.nojekyll
--------------------------------------------------------------------------------
/docs/static/_redirects:
--------------------------------------------------------------------------------
1 | / /install 301!
--------------------------------------------------------------------------------
/docs/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/docs/static/img/favicon.ico
--------------------------------------------------------------------------------
/docs/static/img/plandex-logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/docs/static/img/plandex-logo-dark.png
--------------------------------------------------------------------------------
/docs/static/img/plandex-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/docs/static/img/plandex-logo-light.png
--------------------------------------------------------------------------------
/docs/static/img/plandex-logo-thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/docs/static/img/plandex-logo-thumb.png
--------------------------------------------------------------------------------
/docs/static/img/plandex-social-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/docs/static/img/plandex-social-preview.png
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@docusaurus/tsconfig",
4 | "compilerOptions": {
5 | "baseUrl": "."
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/images/plandex-browser-debug-yt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-browser-debug-yt.png
--------------------------------------------------------------------------------
/images/plandex-intro-vimeo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-intro-vimeo.png
--------------------------------------------------------------------------------
/images/plandex-logo-dark-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-logo-dark-bg.png
--------------------------------------------------------------------------------
/images/plandex-logo-dark-v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-logo-dark-v2.png
--------------------------------------------------------------------------------
/images/plandex-logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-logo-dark.png
--------------------------------------------------------------------------------
/images/plandex-logo-light-v2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-logo-light-v2.png
--------------------------------------------------------------------------------
/images/plandex-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-logo-light.png
--------------------------------------------------------------------------------
/images/plandex-logo-thumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-logo-thumb.png
--------------------------------------------------------------------------------
/images/plandex-v2-yt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-v2-yt.png
--------------------------------------------------------------------------------
/images/plandex-workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/images/plandex-workflow.png
--------------------------------------------------------------------------------
/plans/invite-commands.txt:
--------------------------------------------------------------------------------
1 | Let's add the following commands to the 'app/cli/cmd' directory.
2 |
3 | For all commands, look at the cli/types/api.go and shared/req_res.go files for the API request and response types.
4 |
5 | Commands:
6 |
7 | invite.go - invite a new user to the org. look at the 'checkout' command on accepting optional parameters or prompting if parameters aren't provided.
8 |
9 | users.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.
10 |
11 | revoke.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.
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/plans/model-sets-custom-models-crud.txt:
--------------------------------------------------------------------------------
1 | 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.
2 |
3 | Use the existing functionality for models and model sets as a guide and follow the same architecture, coding style, and ux.
4 |
5 | Here are the commands/subcommands I want to add:
6 |
7 | `plandex models available` - list all available models, both built-in and custom, in a nicely formatted table (use tablewriter like the other commands)
8 |
9 | `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
10 |
11 | `plandex models delete` - prompt the user to choose from a list of custom models to delete
12 |
13 | `plandex model-sets` - list all available model sets, both built-in and user-created
14 |
15 | `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
16 |
17 | Add 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.
18 |
19 | On the client-side, update the Api interface and implementation to add the new api calls.
20 |
21 |
22 |
--------------------------------------------------------------------------------
/plans/pdx-file.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/plans/pdx-file.md
--------------------------------------------------------------------------------
/releases/cli/versions/0.7.1.md:
--------------------------------------------------------------------------------
1 | - Fix for re-running command after an upgrade
2 | - Fix for user input prompts
3 |
--------------------------------------------------------------------------------
/releases/cli/versions/0.7.2.md:
--------------------------------------------------------------------------------
1 | - PLANDEX_SKIP_UPGRADE environment variable can be used to disable upgrades
2 | - Color fixes for light backgrounds
3 |
--------------------------------------------------------------------------------
/releases/cli/versions/0.7.3.md:
--------------------------------------------------------------------------------
1 | - Fixes for changes TUI replacement view
2 | - Fixes for changes TUI text encoding issue
3 | - Fixes context loading
4 | - `plandex rm` can now remove a directory from context
5 | - `plandex apply` fixes to avoid possible conflicts
6 | - `plandex apply` ask user whether to commit changes
7 | - Context update fixes
8 | - Command suggestions can be disabled with PLANDEX_DISABLE_SUGGESTIONS environment variable
--------------------------------------------------------------------------------
/releases/cli/versions/0.8.0.md:
--------------------------------------------------------------------------------
1 | - `plandex invite` command to invite users to an org
2 | - `plandex users` command to list users and pending invites for an org
3 | - `plandex revoke` command to revoke an invite or remove a user from an org
4 | - `plandex sign-in` fixes
5 | - Fix for context update of directory tree when some paths are ignored
6 | - Fix for `plandex branches` command showing no branches immediately after plan creation rather than showing the default 'main' branch
--------------------------------------------------------------------------------
/releases/cli/versions/0.8.1.md:
--------------------------------------------------------------------------------
1 | - 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)
2 | - `add` alias for `load` command + `unload` alias for `rm` command (https://github.com/plandex-ai/plandex/issues/12)
3 | - Add `invite`, `revoke`, and `users` commands to `plandex help` output
4 | - A bit of cleanup of extraneous logging
5 |
6 |
--------------------------------------------------------------------------------
/releases/cli/versions/0.8.2.md:
--------------------------------------------------------------------------------
1 | - 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)
2 | - 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)
3 | - Add support for OPENAI_ENDPOINT environment variable for custom OpenAI endpoints (https://github.com/plandex-ai/plandex/pull/46)
4 | - Add support for OPENAI_ORG_ID environment variable for setting the OpenAI organization ID when using an API key with multiple OpenAI organizations.
5 |
--------------------------------------------------------------------------------
/releases/cli/versions/0.8.3.md:
--------------------------------------------------------------------------------
1 | - Add support for new OpenAI models: `gpt-4-turbo` and `gpt-4-turbo-2024-04-09`
2 | - 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
3 | - Fix for `set-model` command argument parsing (https://github.com/plandex-ai/plandex/issues/75)
4 | - Fix for panic during plan stream when a file name's length exceeds the terminal width (https://github.com/plandex-ai/plandex/issues/84)
5 | - Fix for handling files that are loaded into context and later deleted from the file system (https://github.com/plandex-ai/plandex/issues/47)
6 | - Fix to prevent loading of duplicate files, directory trees, or urls into context (https://github.com/plandex-ai/plandex/issues/57)
7 |
--------------------------------------------------------------------------------
/releases/cli/versions/0.9.1.md:
--------------------------------------------------------------------------------
1 | - Fix for occasional stream TUI panic during builds with long file paths (https://github.com/plandex-ai/plandex/issues/105)
2 | - 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!)
3 | - Include 'openrouter' in list of model providers when adding a custom model (https://github.com/plandex-ai/plandex/issues/107)
4 | - 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)
5 | - 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).
6 | - Still show the '(r)eject file' hotkey in the `plandex changes` TUI when the current file isn't scrollable.
--------------------------------------------------------------------------------
/releases/cli/versions/1.0.0.md:
--------------------------------------------------------------------------------
1 | - CLI updates for the 1.0.0 release
2 | - See the [server/v1.0.0 release notes](https://github.com/plandex-ai/plandex/releases/tag/server%2Fv1.0.0) for full details
--------------------------------------------------------------------------------
/releases/cli/versions/1.1.1.md:
--------------------------------------------------------------------------------
1 | ## Fix for terminal flickering when streaming plans 📺
2 |
3 | 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.
4 |
5 | ## Claude 3.5 Sonnet model pack is now built-in 🧠
6 |
7 | You can now easily use Claude 3.5 Sonnet with Plandex through OpenRouter.ai.
8 |
9 | 1. Create an account at [OpenRouter.ai](https://openrouter.ai) if you don't already have one.
10 | 2. [Generate an OpenRouter API key](https://openrouter.ai/keys).
11 | 3. Run `export OPENROUTER_API_KEY=...` in your terminal.
12 | 4. 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)
13 |
14 | 
15 |
16 | Remember, you can run `plandex model-packs` for details on all built-in model packs.
17 |
--------------------------------------------------------------------------------
/releases/cli/versions/1.1.2.md:
--------------------------------------------------------------------------------
1 | - Minor updates to command help and aliases.
2 | - Fix for help message shown on invalid command or flags.
3 | - Fix for `set-model` not updating settings for auto-fix and verifier roles (https://github.com/plandex-ai/plandex/issues/171)
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.1.md:
--------------------------------------------------------------------------------
1 | - Fix for REPL startup failing when self-hosting or using BYOK cloud mode (https://github.com/plandex-ai/plandex/issues/216)
2 | - Fix for potential crash with custom model pack (https://github.com/plandex-ai/plandex/issues/217)
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.2.md:
--------------------------------------------------------------------------------
1 | - 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)
2 | - Fixed bug where the build output would sometimes wrap incorrectly, causing the Plan Stream TUI to get out of sync with the build output.
3 | - Fixed bug where build output would jump between collapsed and expanded states during a stream, after the user manually expanded.
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.3.md:
--------------------------------------------------------------------------------
1 | - Fix potential race condition/goroutine explosion/crash in context update.
2 | - Prevent crash with negative viewport height in stream tui.
3 |
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.4.md:
--------------------------------------------------------------------------------
1 | - **Models**
2 | - Claude Sonnet 3.7 thinking is now available as a built-in model. Try the `reasoning` model pack for more challenging tasks.
3 | - Gemini 2.5 pro (free/experimental version) is now available. Try the 'gemini-planner' or 'gemini-experimental' model packs to use it.
4 | - 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.
5 | - 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`
6 |
7 | - **Debugging**
8 | - Plandex can now directly debug browser applications by catching errors and reading the console logs (requires Chrome).
9 | - Enhanced signal handling and subprocess termination robustness for execution control.
10 |
11 | - **Model Packs**
12 | - Added commands:
13 | - `model-packs update`
14 | - `model-packs show`
15 |
16 | - **Reliability**
17 | - Implemented HTTP retry logic with exponential backoff for transient errors.
18 |
19 | - **REPL**
20 | - Fixed whitespace handling issues.
21 | - Improved command execution flow.
22 |
23 | - **Installation**
24 | - Clarified support for WSL-only environments.
25 | - Better handling of sudo and alias creation on Linux.
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.5.md:
--------------------------------------------------------------------------------
1 | - 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.
2 | - Prevent the '\send' command from being included in the prompt when using multi-line mode in the REPL.
3 |
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.6.md:
--------------------------------------------------------------------------------
1 | - Timeout for 'plandex browser' log capture command
2 | - Better failure handling for 'plandex browser' command
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.7+1.md:
--------------------------------------------------------------------------------
1 | - Small adjustment to previous release: in the REPL, select the first auto-complete suggestion on 'enter' if any suggestions are listed.
--------------------------------------------------------------------------------
/releases/cli/versions/2.0.7.md:
--------------------------------------------------------------------------------
1 | - 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.
--------------------------------------------------------------------------------
/releases/cli/versions/2.1.0+1.md:
--------------------------------------------------------------------------------
1 | - Fix for potential encoding issue when loading files into context.
--------------------------------------------------------------------------------
/releases/cli/versions/2.1.1.md:
--------------------------------------------------------------------------------
1 | - Fix for free Gemini 2.5 Pro Experimental OpenRouter endpoint.
2 | - Retries for "No endpoints found that support cache control" error that showed up when OpenRouter temporarily disabled caching for Gemini 2.5 Pro Preview.
3 | - Other minor improvements to error handling and retries.
4 |
--------------------------------------------------------------------------------
/releases/cli/versions/2.1.2.md:
--------------------------------------------------------------------------------
1 | - Fix for rare auto-load context timeout error when no files are loaded.
--------------------------------------------------------------------------------
/releases/cli/versions/2.1.3.md:
--------------------------------------------------------------------------------
1 | - Fix for default model pack not being correctly applied to new plans
2 | - Fix for potential crash on Linux when applying a plan
--------------------------------------------------------------------------------
/releases/cli/versions/2.1.5.md:
--------------------------------------------------------------------------------
1 | - Added newly released Claude Sonnet 4 and Claude Opus 4 as built-in models.
2 | - 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.
3 | - 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.
4 | - Removed error fallbacks for o4-mini and gemini-2.5-pro-preview.
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/releases/cli/versions/2.1.6+1.md:
--------------------------------------------------------------------------------
1 | - Error handling fix
2 | - Fix for some roles in the `daily-driver` model pack that weren't correctly updated to Sonnet 4 in 2.1.6
3 | - Added fallback from Sonnet 4 to Sonnet 3.7 to deal with occasional provider errors and rate limit issues
--------------------------------------------------------------------------------
/releases/cli/versions/2.1.6.md:
--------------------------------------------------------------------------------
1 | - 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.
2 | - 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.
3 | - 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.
4 | - Fix for occasional garbled error message when the model is unresponsive.
5 | - Fix for occasional 'couldn't aquire lock' error after stream finishes.
6 | - Additional retry when model is unresponsive or hits provider rate limits—helps particularly with new Opus 4 model on OpenRouter.
--------------------------------------------------------------------------------
/releases/images/cli/0.9.0/plandex-archive.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/releases/images/cli/0.9.0/plandex-archive.gif
--------------------------------------------------------------------------------
/releases/images/cli/0.9.0/plandex-commit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/releases/images/cli/0.9.0/plandex-commit.png
--------------------------------------------------------------------------------
/releases/images/cli/0.9.0/plandex-diff.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/releases/images/cli/0.9.0/plandex-diff.gif
--------------------------------------------------------------------------------
/releases/images/cli/0.9.0/plandex-models.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/releases/images/cli/0.9.0/plandex-models.gif
--------------------------------------------------------------------------------
/releases/images/cli/1.1.0/plandex-images.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/releases/images/cli/1.1.0/plandex-images.gif
--------------------------------------------------------------------------------
/releases/images/cli/1.1.0/plandex-reject.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/releases/images/cli/1.1.0/plandex-reject.gif
--------------------------------------------------------------------------------
/releases/images/cli/1.1.1/claude-3-5-sonnet.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plandex-ai/plandex/f3431a2ccb4a13384e5ac9a21620f1a6eeda2e5d/releases/images/cli/1.1.1/claude-3-5-sonnet.gif
--------------------------------------------------------------------------------
/releases/server/versions/0.7.0.md:
--------------------------------------------------------------------------------
1 | Initial release
--------------------------------------------------------------------------------
/releases/server/versions/0.7.1.md:
--------------------------------------------------------------------------------
1 | - Fix for SMTP email issue
2 | - Add '/version' endpoint to server
--------------------------------------------------------------------------------
/releases/server/versions/0.8.0.md:
--------------------------------------------------------------------------------
1 | - User management improvements and fixes
2 | - Backend support for `plandex invite`, `plandex users`, and `plandex revoke` commands
3 | - Improvements to copy for email verification emails
4 | - Fix for org creation when creating a new account
5 | - Send an email to invited user when they are invited to an org
6 | - Add timeout when forwarding requests from one instance to another within a cluster
--------------------------------------------------------------------------------
/releases/server/versions/0.8.1.md:
--------------------------------------------------------------------------------
1 | - Fixes for two potential server crashes
2 | - Fix for server git repo remaining in locked state after a crash, which caused various issues
3 | - Fix for server git user and email not being set in some environments (https://github.com/plandex-ai/plandex/issues/8)
4 | - Fix for 'replacements failed' error that was popping up in some circumstances
5 | - Fix for build issue that could cause large updates to fail, take too long, or use too many tokens in some circumstances
6 | - Clean up extraneous logging
7 | - Prompt update to prevent ouputting files at absolute paths (like '/etc/config.txt')
8 | - Prompt update to prevent sometimes using file block format for explanations, causing explanations to be outputted as files
9 | - Prompt update to prevent stopping before the plan is really finished
10 | - Increase maximum number of auto-continuations to 50 (from 30)
11 |
--------------------------------------------------------------------------------
/releases/server/versions/0.8.2.md:
--------------------------------------------------------------------------------
1 | - Fix for creating an org that auto-adds users based on email domain (https://github.com/plandex-ai/plandex/issues/24)
2 | - Fix for possible crash after error in file build
3 | - Added crash prevention measures across the board
4 | - Fix for occasional "replacements failed" error
5 | - Reliability and improvements for file updates
6 | - Fix for role name of auto-continue model
--------------------------------------------------------------------------------
/releases/server/versions/0.8.3.md:
--------------------------------------------------------------------------------
1 | - SMTP_FROM environment variable for setting from address when self-hosting and using SMTP (https://github.com/plandex-ai/plandex/pull/39)
2 | - Add support for OPENAI_ENDPOINT environment variable for custom OpenAI endpoints (https://github.com/plandex-ai/plandex/pull/46)
3 | - Add support for OPENAI_ORG_ID environment variable for setting the OpenAI organization ID when using an API key with multiple OpenAI organizations.
4 | - 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)
5 |
--------------------------------------------------------------------------------
/releases/server/versions/0.8.4.md:
--------------------------------------------------------------------------------
1 | - Add support for new OpenAI models: `gpt-4-turbo` and `gpt-4-turbo-2024-04-09`
2 | - 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
3 | - Fix for handling files that are loaded into context and later deleted from the file system (https://github.com/plandex-ai/plandex/issues/47)
4 | - Handle file paths with ### prefixes (https://github.com/plandex-ai/plandex/issues/77)
5 | - Fix for occasional race condition during file builds that causes error "Fatal: Unable to write new index file"
--------------------------------------------------------------------------------
/releases/server/versions/0.9.0.md:
--------------------------------------------------------------------------------
1 | - Support for custom models, model packs, and default models (see CLI 0.9.0 release notes for details).
2 | - Better accuracy for updates to existing files.
3 | - Plandex is less likely to screw up braces, parentheses, and other code structures.
4 | - Plandex is less likely to mistakenly remove code that it shouldn't.
5 | - 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.
6 | - Server-side support for `plandex diff` command to show pending plan changes in `git diff` format.
7 | - Server-side support for archiving and unarchiving plans.
8 | - Server-side support for `plandex summary` command.
9 | - Server-side support for `plandex rename` command.
10 | - Descriptive top-line for `plandex apply` commit messages instead of just "applied pending changes".
11 | - Better message in `plandex log` when a single piece of context is loaded or updated.
12 | - Fixes for some rare potential deadlocks and conflicts when building a file or stopping astream.
--------------------------------------------------------------------------------
/releases/server/versions/0.9.1.md:
--------------------------------------------------------------------------------
1 | - 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.
2 | - Fix for 'exit status 128' errors in a couple of edge case scenarios.
3 | - 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).
--------------------------------------------------------------------------------
/releases/server/versions/1.0.1.md:
--------------------------------------------------------------------------------
1 | - Fix for occasional 'Error getting verify state for file' error
2 | - Fix for occasional 'Fatal: unable to write new_index file' error
3 | - Fix for occasional 'nothing to commit, working tree clean' error
4 | - 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)
5 | - 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.
--------------------------------------------------------------------------------
/releases/server/versions/1.1.0.md:
--------------------------------------------------------------------------------
1 | - Give notes added to context with `plandex load -n 'some note'` automatically generated names in `context ls` list.
2 | - 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`.
3 | - 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.
4 | - 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.
5 | - Increase wait times when receiving rate limit errors from OpenAI API (common with new OpenAI accounts that haven't spent $50).
--------------------------------------------------------------------------------
/releases/server/versions/1.1.1.md:
--------------------------------------------------------------------------------
1 | - 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.
2 | - Claude 3.5 Sonnet model and model pack (via OpenRouter.ai) is now built-in.
--------------------------------------------------------------------------------
/releases/server/versions/2.0.0+1.md:
--------------------------------------------------------------------------------
1 | - Fix for custom model creation (https://github.com/plandex-ai/plandex/issues/214)
2 | - Fix for version check on self-hosted (https://github.com/plandex-ai/plandex/issues/213)
3 | - Fix for GitHub Action to build and push server image to DockerHub
--------------------------------------------------------------------------------
/releases/server/versions/2.0.0+2.md:
--------------------------------------------------------------------------------
1 | - Version tag sanitation fix for GitHub Action to build and push server image to DockerHub
2 |
--------------------------------------------------------------------------------
/releases/server/versions/2.0.0.md:
--------------------------------------------------------------------------------
1 | See CLI 2.0.0 release notes.
--------------------------------------------------------------------------------
/releases/server/versions/2.0.2.md:
--------------------------------------------------------------------------------
1 | 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)
--------------------------------------------------------------------------------
/releases/server/versions/2.0.3.md:
--------------------------------------------------------------------------------
1 | - Fix for potential crash during chat/tell operation.
2 | - Panic handling to prevent crashes in general.
3 | - Fix for local queue handling bug during builds that could cause queue to get stuck and cause subsequent operations to hang.
--------------------------------------------------------------------------------
/releases/server/versions/2.0.4.md:
--------------------------------------------------------------------------------
1 | - **Stability**
2 | - Enhanced database locking mechanisms.
3 | - Improved error notifications.
4 |
5 | - **API Enhancements**
6 | - Added endpoints for managing custom models and updating model packs.
7 |
8 | - **Execution**
9 | - Increased robustness in plan execution and subprocess lifecycle management.
10 |
11 | - **Observability**
12 | - Real-time internal notifications for critical errors implemented.
13 |
14 | - **Consistency**
15 | - Improved token management.
16 | - Enhanced summarization accuracy.
--------------------------------------------------------------------------------
/releases/server/versions/2.0.5.md:
--------------------------------------------------------------------------------
1 | - Fix for a bug that was causing occasional model errors. Model calls should be much more reliable now.
2 | - Better error handling and error messages for model errors (rate limits or other errors).
3 | - No error retries for rate limit errors.
4 | - Fixed bug that caused retries to add the prompt to the conversation multiple times.
5 | - Error responses with no output no longer create a log entry.
--------------------------------------------------------------------------------
/releases/server/versions/2.0.6.md:
--------------------------------------------------------------------------------
1 | - Improvements to process management and cleanup for command execution
2 | - Remove extraneous model request logging
--------------------------------------------------------------------------------
/releases/server/versions/2.1.0+1.md:
--------------------------------------------------------------------------------
1 | - Fix for context length exceeded error that still wasn't being caught and retried by the fallback correctly.
--------------------------------------------------------------------------------
/releases/server/versions/2.1.0.md:
--------------------------------------------------------------------------------
1 | See CLI 2.1.0 release notes.
--------------------------------------------------------------------------------
/releases/server/versions/2.1.1+1.md:
--------------------------------------------------------------------------------
1 | - Improve error handling to catch yet another "context length exceeded" error message variation from Anthropic.
--------------------------------------------------------------------------------
/releases/server/versions/2.1.1.md:
--------------------------------------------------------------------------------
1 | See CLI 2.1.1 release notes.
--------------------------------------------------------------------------------
/releases/server/versions/2.1.2.md:
--------------------------------------------------------------------------------
1 | - Fix for auto-load context error: 'Error decoding response → EOF'
--------------------------------------------------------------------------------
/releases/server/versions/2.1.3.md:
--------------------------------------------------------------------------------
1 | - Fix for 'panic in execTellPlan' error when using a model pack that doesn't explicitly set the 'coder' or 'whole-file-builder' roles
--------------------------------------------------------------------------------
/releases/server/versions/2.1.4.md:
--------------------------------------------------------------------------------
1 | - Fix to remove occasional extraneous blank lines from start/end of edited files.
--------------------------------------------------------------------------------
/releases/server/versions/2.1.5.md:
--------------------------------------------------------------------------------
1 | See CLI 2.1.5 release notes.
--------------------------------------------------------------------------------
/releases/server/versions/2.1.6+1.md:
--------------------------------------------------------------------------------
1 | See CLI 2.1.6+1 release notes.
--------------------------------------------------------------------------------
/releases/server/versions/2.1.6.md:
--------------------------------------------------------------------------------
1 | See CLI 2.1.6 release notes.
--------------------------------------------------------------------------------
/test/error-test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test JS Error
6 |
7 |
8 |
9 | This page intentionally throws a JS error
10 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/build/build.config.properties:
--------------------------------------------------------------------------------
1 | provider_id=openai:gpt-4o
2 | temperature=0.1
3 | max_tokens=4096
4 | top_p=0.1
5 | response_format=json_object
6 | function_name=listChangesWithLineNums
7 | tool_type=function
8 | function_param_type=object
9 | tool_choice_type=function
10 | tool_choice_function_name=listChangesWithLineNums
11 | nested_parameters_json=build.parameters.json
12 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/build/promptfooconfig.yaml:
--------------------------------------------------------------------------------
1 | description: "build"
2 |
3 | prompts:
4 | - file://build.prompt.txt
5 | providers:
6 | - file://build.provider.yml
7 | tests: tests/*.test.yml
8 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/build/tests/build.test.yml:
--------------------------------------------------------------------------------
1 | - description: "Check Build with Line numbers"
2 | vars:
3 | preBuildState: file://assets/shared/pre_build.go
4 | changes: file://assets/build/changes.md
5 | filePath: parse.go
6 | postBuildState: file://assets/build/post_build.go
7 | assert:
8 | - type: is-json
9 | - type: is-valid-openai-tools-call
10 | - type: javascript
11 | value: |
12 | var args = JSON.parse(output[0].function.arguments)
13 | return (
14 | args.changes.length > 0 &&
15 | args.changes.some(
16 | change => change.hasChange &&
17 | change.new.includes("var contextRmCmd = &cobra.Command{")
18 | )
19 | )
20 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/fix/assets/removal/problems.txt:
--------------------------------------------------------------------------------
1 | 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.
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/fix/fix.config.properties:
--------------------------------------------------------------------------------
1 | provider_id=openai:gpt-4o
2 | temperature=0.1
3 | max_tokens=4096
4 | top_p=0.1
5 | response_format=json_object
6 | function_name=listChangesWithLineNums
7 | tool_type=function
8 | function_param_type=object
9 | tool_choice_type=function
10 | tool_choice_function_name=listChangesWithLineNums
11 | nested_parameters_json=fix.parameters.json
12 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/fix/fix.provider.yml:
--------------------------------------------------------------------------------
1 | id: openai:gpt-4o
2 | config:
3 | temperature: 0.1
4 | max_tokens: 4096
5 | response_format: { type: json_object }
6 | top_p: 0.1
7 | tools:
8 | [
9 | {
10 | "type": "function",
11 | "function":
12 | { "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"} },
13 | },
14 | ]
15 | tool_choice:
16 | type: "function"
17 | function:
18 | name: "listChangesWithLineNums"
19 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/fix/promptfooconfig.yaml:
--------------------------------------------------------------------------------
1 | # This configuration compares LLM output of 2 prompts x 2 GPT models across 3 test cases.
2 | # Learn more: https://promptfoo.dev/docs/configuration/guide
3 | description: "fix"
4 |
5 | prompts:
6 | - file://fix.prompt.txt
7 |
8 | providers:
9 | - file://fix.provider.yml
10 |
11 | tests: tests/*.test.yml
12 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/fix/tests/fix.test.yml:
--------------------------------------------------------------------------------
1 | - description: "Check Fix with Line numbers"
2 | vars:
3 | preBuildState: file://assets/shared/pre_build.go
4 | changes: file://assets/removal/changes.md
5 | problems: file://assets/removal/problems.txt
6 | postBuildState: file://assets/removal/post_build.go
7 | assert:
8 | - type: is-json
9 | - type: is-valid-openai-tools-call
10 | - type: javascript
11 | value: |
12 | var args = JSON.parse(output[0].function.arguments)
13 | return (
14 | args.problems &&
15 | args.changes.length > 0 &&
16 | args.changes.some(
17 | change => change.hasChange &&
18 | change.new.includes("var contextRmCmd = &cobra.Command{")
19 | )
20 | )
21 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/templates/provider.template.yml:
--------------------------------------------------------------------------------
1 | # TODO: Add support for more dynamic creation, support for multiple tools, different API providers parameters, etc.
2 |
3 | id: {{ .provider_id }}
4 | config:
5 | temperature: {{ .temperature }}
6 | max_tokens: {{ .max_tokens }}
7 | response_format: { type: {{ .response_format }} }
8 | top_p: {{ .top_p }}
9 | tools:
10 | [
11 | {
12 | "type": "{{ .tool_type }}",
13 | "function":
14 | { "name": "{{ .function_name }}", "parameters": {{ .parameters }} },
15 | },
16 | ]
17 | tool_choice:
18 | type: "{{ .tool_choice_type }}"
19 | function:
20 | name: "{{ .tool_choice_function_name }}"
21 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/verify/assets/valid/diff.txt:
--------------------------------------------------------------------------------
1 | diff --git a/tests/go/shared/pre_build.go b/tests/go/valid/post_build.go
2 | index c90eeb7..90405e7 100644
3 | --- a/tests/go/shared/pre_build.go
4 | +++ b/tests/go/valid/post_build.go
5 | @@ -7,11 +7,38 @@ import (
6 | "plandex/auth"
7 | "plandex/lib"
8 | "plandex/term"
9 | + "strconv"
10 | + "strings"
11 |
12 | "plandex-shared"
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | +func parseRange(arg string) ([]int, error) {
17 | + var indices []int
18 | + parts := strings.Split(arg, "-")
19 | + if len(parts) == 2 {
20 | + start, err := strconv.Atoi(parts[0])
21 | + if err != nil {
22 | + return nil, err
23 | + }
24 | + end, err := strconv.Atoi(parts[1])
25 | + if err != nil {
26 | + return nil, err
27 | + }
28 | + for i := start; i <= end; i++ {
29 | + indices = append(indices, i)
30 | + }
31 | + } else {
32 | + index, err := strconv.Atoi(arg)
33 | + if err != nil {
34 | + return nil, err
35 | + }
36 | + indices = append(indices, index)
37 | + }
38 | + return indices, nil
39 | +}
40 | +
41 | var contextRmCmd = &cobra.Command{
42 | Use: "rm",
43 | Aliases: []string{"remove", "unload"},
44 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/verify/promptfooconfig.yaml:
--------------------------------------------------------------------------------
1 | # This configuration compares LLM output of 2 prompts x 2 GPT models across 3 test cases.
2 | # Learn more: https://promptfoo.dev/docs/configuration/guide
3 | description: "verify"
4 |
5 | prompts:
6 | - file://verify.prompt.txt
7 | providers:
8 | - file://verify.provider.yml
9 | tests: tests/*.test.yml
10 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/verify/tests/removal.test.yml:
--------------------------------------------------------------------------------
1 | - description: "Removal of code errors"
2 | vars:
3 | preBuildState: file://assets/shared/pre_build.go
4 | changes: file://assets/removal/changes.md
5 | postBuildState: file://assets/removal/post_build.go
6 | diffs: file://assets/removal/diff.txt
7 | assert:
8 | - type: is-json
9 | - type: is-valid-openai-tools-call
10 | - type: javascript
11 | value: |
12 | var args = JSON.parse(output[0].function.arguments)
13 | return args.hasRemovedCodeErrors
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/verify/tests/validate.test.yml:
--------------------------------------------------------------------------------
1 | - description: "Validation of the code changes"
2 | vars:
3 | preBuildState: file://assets/shared/pre_build.go
4 | changes: file://assets/valid/changes.md
5 | postBuildState: file://assets/valid/post_build.go
6 | diffs: file://assets/valid/diff.txt
7 | assert:
8 | - type: is-json
9 | - type: is-valid-openai-tools-call
10 | - type: javascript
11 | value: |
12 | var args = JSON.parse(output[0].function.arguments)
13 | return !(
14 | args.hasSyntaxErrors ||
15 | args.hasRemovedCodeErrors ||
16 | args.hasDuplicationErrors ||
17 | args.hasReferenceErrors
18 | )
19 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/verify/verify.config.properties:
--------------------------------------------------------------------------------
1 | provider_id=openai:gpt-4o
2 | temperature=0.1
3 | max_tokens=4096
4 | top_p=0.1
5 | response_format=json_object
6 | function_name=verifyOutput
7 | tool_type=function
8 | function_param_type=object
9 | tool_choice_type=function
10 | tool_choice_function_name=verifyOutput
11 | nested_parameters_json=verify.parameters.json
12 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/verify/verify.parameters.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "properties": {
4 | "syntaxErrorsReasoning": {
5 | "type": "string"
6 | },
7 | "hasSyntaxErrors": {
8 | "type": "boolean"
9 | },
10 | "removed": {
11 | "type": "array",
12 | "items": {
13 | "type": "object",
14 | "properties": {
15 | "code": {
16 | "type": "string"
17 | },
18 | "reasoning": {
19 | "type": "string"
20 | },
21 | "correct": {
22 | "type": "boolean"
23 | }
24 | },
25 | "required": ["code", "reasoning", "correct"]
26 | }
27 | },
28 | "removedCodeErrorsReasoning": {
29 | "type": "string"
30 | },
31 | "hasRemovedCodeErrors": {
32 | "type": "boolean"
33 | },
34 | "duplicationErrorsReasoning": {
35 | "type": "string"
36 | },
37 | "hasDuplicationErrors": {
38 | "type": "boolean"
39 | },
40 | "comments": {
41 | "type": "array",
42 | "items": {
43 | "type": "object",
44 | "properties": {
45 | "txt": {
46 | "type": "string"
47 | },
48 | "reference": {
49 | "type": "boolean"
50 | }
51 | },
52 | "required": ["txt", "reference"]
53 | }
54 | },
55 | "referenceErrorsReasoning": {
56 | "type": "string"
57 | },
58 | "hasReferenceErrors": {
59 | "type": "boolean"
60 | }
61 | },
62 | "required": [
63 | "syntaxErrorsReasoning",
64 | "hasSyntaxErrors",
65 | "removed",
66 | "removedCodeErrorsReasoning",
67 | "hasRemovedCodeErrors",
68 | "duplicationErrorsReasoning",
69 | "hasDuplicationErrors",
70 | "comments",
71 | "referenceErrorsReasoning",
72 | "hasReferenceErrors"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/test/evals/promptfoo-poc/verify/verify.provider.yml:
--------------------------------------------------------------------------------
1 | id: openai:gpt-4o
2 | config:
3 | temperature: 0.1
4 | max_tokens: 4096
5 | response_format: { type: json_object }
6 | top_p: 0.1
7 | tools:
8 | [
9 | {
10 | "type": "function",
11 | "function":
12 | { "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"} },
13 | },
14 | ]
15 | tool_choice:
16 | type: "function"
17 | function:
18 | name: "verifyOutput"
19 |
--------------------------------------------------------------------------------
/test/pong/.gitignore:
--------------------------------------------------------------------------------
1 | *.o
2 | pong
--------------------------------------------------------------------------------
/test/pong/Makefile:
--------------------------------------------------------------------------------
1 | CC = gcc
2 | CFLAGS = -Wall -g
3 | LDFLAGS = -framework OpenGL -framework GLUT
4 |
5 | SRCS = main.c paddle.c ball.c render.c
6 | OBJS = $(SRCS:.c=.o)
7 |
8 | all: pong
9 |
10 | pong: $(OBJS)
11 | $(CC) $(CFLAGS) -o pong $(OBJS) $(LDFLAGS)
12 |
13 | clean:
14 | rm -f pong $(OBJS)
15 |
--------------------------------------------------------------------------------
/test/pong/README.md:
--------------------------------------------------------------------------------
1 | # Building Pong in C/OpenGL with Plandex
2 |
3 | This directory is a result of running `plandex tell` with the `prompt.txt` file in the root of the repository, then using Plandex to work through a number of compiler errors.
4 |
5 | [Here's a video of the whole process.](https://www.youtube.com/watch?v=0ULjQx25S_Y)
6 |
7 | ## Building and running
8 |
9 | The project was designed to be run on a mac.
10 |
11 | First install dependencies with:
12 |
13 | ```bash
14 | ./install_dependencies.sh
15 | ```
16 |
17 | Then build the project with:
18 |
19 | ```bash
20 | make
21 | ```
22 |
23 | And run it with:
24 |
25 | ```bash
26 | ./pong
27 | ```
--------------------------------------------------------------------------------
/test/pong/ball.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "ball.h"
4 | #include "paddle.h"
5 |
6 | extern float playerPaddleY; // Declare the external playerPaddleY variable
7 | extern float computerPaddleY; // Declare the external computerPaddleY variable
8 |
9 | float ballX = 400.0f;
10 | float ballY = 300.0f;
11 | float ballSpeedX = 4.0f;
12 | float ballSpeedY = 4.0f;
13 |
14 | void initBall() {
15 | // Initialize ball position and speed
16 | ballX = 400.0f;
17 | ballY = 300.0f;
18 | ballSpeedX = 4.0f;
19 | ballSpeedY = 4.0f;
20 | }
21 |
22 | void updateBall() {
23 | // Update ball position
24 | ballX += ballSpeedX;
25 | ballY += ballSpeedY;
26 |
27 | // Check for collision with top and bottom walls
28 | if (ballY + 10.0f > 600.0f || ballY - 10.0f < 0.0f) {
29 | ballSpeedY = -ballSpeedY;
30 | }
31 |
32 | // Check for collision with player paddle
33 | if (ballX - 10.0f < 70.0f && ballY > playerPaddleY - 50.0f && ballY < playerPaddleY + 50.0f) {
34 | ballSpeedX = -ballSpeedX;
35 | }
36 |
37 | // Check for collision with computer paddle
38 | if (ballX + 10.0f > 730.0f && ballY > computerPaddleY - 50.0f && ballY < computerPaddleY + 50.0f) {
39 | ballSpeedX = -ballSpeedX;
40 | }
41 |
42 | // Check for scoring (ball goes out of bounds)
43 | if (ballX + 10.0f > 800.0f || ballX - 10.0f < 0.0f) {
44 | initBall(); // Reset ball position and speed
45 | }
46 | }
47 |
48 |
49 |
50 | float getBallY() {
51 | return ballY;
52 | }
53 |
54 |
55 |
--------------------------------------------------------------------------------
/test/pong/ball.h:
--------------------------------------------------------------------------------
1 | #ifndef BALL_H
2 | #define BALL_H
3 |
4 | void initBall();
5 | void updateBall();
6 | void renderBall();
7 | float getBallY();
8 |
9 | #endif
10 |
--------------------------------------------------------------------------------
/test/pong/install_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Check for Homebrew and install if not found
4 | if ! command -v brew &> /dev/null
5 | then
6 | echo "Homebrew not found. Installing Homebrew..."
7 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
8 | fi
9 |
10 | # Install necessary dependencies
11 | echo "Installing necessary dependencies..."
12 | brew install freeglut
13 |
14 | echo "Dependencies installed successfully."
15 |
--------------------------------------------------------------------------------
/test/pong/paddle.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "paddle.h"
5 | #include "ball.h"
6 |
7 | extern bool keyStates[256]; // Declare the external keyStates array
8 | extern bool specialKeyStates[256]; // Declare the external specialKeyStates array
9 |
10 | float playerPaddleY = 300.0f;
11 | float computerPaddleY = 300.0f;
12 | float playerPaddleSpeed = 5.0f;
13 | float computerPaddleSpeed = 3.0f;
14 |
15 | void initPaddle() {
16 | // Initialize paddle positions
17 | playerPaddleY = 300.0f;
18 | computerPaddleY = 300.0f;
19 | }
20 |
21 | void updatePaddle() {
22 | // Update player paddle position based on keyboard input
23 | if (specialKeyStates[GLUT_KEY_UP]) {
24 | playerPaddleY += playerPaddleSpeed;
25 | }
26 | if (specialKeyStates[GLUT_KEY_DOWN]) {
27 | playerPaddleY -= playerPaddleSpeed;
28 | }
29 |
30 | // Ensure the paddle stays within the window bounds
31 | if (playerPaddleY + 50.0f > 600.0f) {
32 | playerPaddleY = 550.0f;
33 | }
34 | if (playerPaddleY - 50.0f < 0.0f) {
35 | playerPaddleY = 50.0f;
36 | }
37 |
38 | // Update computer paddle position based on ball position
39 | if (getBallY() > computerPaddleY) {
40 | computerPaddleY += computerPaddleSpeed;
41 | } else if (getBallY() < computerPaddleY) {
42 | computerPaddleY -= computerPaddleSpeed;
43 | }
44 |
45 | // Ensure the paddle stays within the window bounds
46 | if (computerPaddleY + 50.0f > 600.0f) {
47 | computerPaddleY = 550.0f;
48 | }
49 | if (computerPaddleY - 50.0f < 0.0f) {
50 | computerPaddleY = 50.0f;
51 | }
52 | }
--------------------------------------------------------------------------------
/test/pong/paddle.h:
--------------------------------------------------------------------------------
1 | #ifndef PADDLE_H
2 | #define PADDLE_H
3 |
4 | void initPaddle();
5 | void updatePaddle();
6 | void renderPaddle();
7 |
8 | #endif
9 |
--------------------------------------------------------------------------------
/test/pong/prompt.txt:
--------------------------------------------------------------------------------
1 | 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.
2 |
3 | Create all the necessary functionality for the project so that it will be ready to compile and run.
4 |
5 | Include a script that will install any necessary dependencies that are missing on Mac OSC. Prefer homebrew where possible for package management.
6 |
7 | Also include a Makefile that will compile the project.
8 |
--------------------------------------------------------------------------------
/test/pong/render.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "paddle.h"
4 | #include "ball.h"
5 |
6 | extern float playerPaddleY; // Declare the external playerPaddleY variable
7 | extern float computerPaddleY; // Declare the external computerPaddleY variable
8 | extern float ballX; // Declare the external ballX variable
9 | extern float ballY; // Declare the external ballY variable
10 |
11 | void renderPaddle() {
12 | // Render player paddle
13 | glRectf(50.0f, playerPaddleY - 50.0f, 70.0f, playerPaddleY + 50.0f);
14 | // Render computer paddle
15 | glRectf(730.0f, computerPaddleY - 50.0f, 750.0f, computerPaddleY + 50.0f);
16 | }
17 |
18 | void renderBall() {
19 | // Render the ball
20 | glRectf(ballX - 10.0f, ballY - 10.0f, ballX + 10.0f, ballY + 10.0f);
21 | }
--------------------------------------------------------------------------------
/test/pong/render.h:
--------------------------------------------------------------------------------
1 | #ifndef RENDER_H
2 | #define RENDER_H
3 |
4 | void renderPaddle();
5 | void renderBall();
6 |
7 | #endif
8 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/action.ts:
--------------------------------------------------------------------------------
1 | export const INCREMENT_COUNT = 'INCREMENT_COUNT';
2 |
3 | interface IncrementCountAction {
4 | type: typeof INCREMENT_COUNT;
5 | payload: number; // Payload now specifically expects a number
6 | }
7 |
8 | export const incrementCount = (amount: number): IncrementCountAction => ({
9 | type: INCREMENT_COUNT,
10 | payload: amount,
11 | });
12 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/component.ts:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | interface Props {
4 | name: string;
5 | }
6 |
7 | const MyComponent: React.FC = ({ name }) => {
8 | const [count, setCount] = useState(0);
9 |
10 | return (
11 |
12 |
Hello, {name}!
13 |
You clicked {count} times
14 |
setCount(count + 1)}>
15 | Click me
16 |
17 |
18 | );
19 | };
20 |
21 | export default MyComponent;
22 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const SOME_CONSTANT = 'SOME_CONSTANT_VALUE';
2 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/lib/utils.ts:
--------------------------------------------------------------------------------
1 | export const formatDate = (date: Date): string => {
2 | return date.toLocaleDateString('en-US', {
3 | weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-foobar",
3 | "version": "1.0.0",
4 | "description": "A realistic React/Redux/TypeScript project",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "react-scripts start",
8 | "build": "react-scripts build",
9 | "test": "react-scripts test",
10 | "eject": "react-scripts eject"
11 | },
12 | "dependencies": {
13 | "@reduxjs/toolkit": "^1.8.0",
14 | "react": "^18.0.0",
15 | "react-dom": "^18.0.0",
16 | "react-redux": "^8.0.0",
17 | "react-scripts": "5.0.0",
18 | "typescript": "^4.5.5"
19 | },
20 | "devDependencies": {
21 | "@testing-library/jest-dom": "^5.16.2",
22 | "@testing-library/react": "^13.2.0",
23 | "@testing-library/user-event": "^14.2.1",
24 | "@types/jest": "^27.4.0",
25 | "@types/node": "^17.0.21",
26 | "@types/react": "^18.0.0",
27 | "@types/react-dom": "^18.0.0"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/reducer.ts:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit';
2 | import { INCREMENT_COUNT } from './action';
3 |
4 | interface State {
5 | count: number;
6 | }
7 |
8 | const initialState: State = {
9 | count: 0,
10 | };
11 |
12 | const countReducer = createReducer(initialState, (builder) => {
13 | builder.addCase(INCREMENT_COUNT, (state, action) => {
14 | state.count += action.payload;
15 | });
16 | });
17 |
18 | export default countReducer;
19 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/tests/action.test.ts:
--------------------------------------------------------------------------------
1 | import { myAction, MY_ACTION_TYPE } from '../action';
2 |
3 | describe('myAction', () => {
4 | it('creates an action with the correct type and payload', () => {
5 | const payload = {}; // Define payload
6 | const expectedAction = {
7 | type: MY_ACTION_TYPE,
8 | payload,
9 | };
10 | expect(myAction(payload)).toEqual(expectedAction);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/tests/component.test.ts:
--------------------------------------------------------------------------------
1 | import { render, fireEvent } from '@testing-library/react'; it('increments count on button click', () => {
2 | const { getByText } = render( );
3 | fireEvent.click(getByText('Click me'));
4 | expect(getByText('You clicked 1 times')).toBeInTheDocument();
5 | });
6 | });
7 | import MyComponent from '../component';
8 |
9 | describe('MyComponent', () => {
10 | it('renders with the correct name', () => {
11 | const { getByText } = render( );
12 | expect(getByText('Hello, John Doe!')).toBeInTheDocument();
13 | });
14 | });
15 |
16 |
--------------------------------------------------------------------------------
/test/project/react-redux-foobar/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx"
17 | },
18 | "include": ["src"]
19 | }
20 |
--------------------------------------------------------------------------------
/test/test_prompts/pong.txt:
--------------------------------------------------------------------------------
1 | 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.
2 |
3 | Create all the necessary functionality for the project so that it will be ready to compile and run.
4 |
5 | Include a script that will install any necessary dependencies that are missing on Mac OSC. Prefer homebrew where possible for package management.
6 |
7 | Also include a Makefile that will compile the project.
8 |
9 | Include a README.md file that will describe the project and how to run it from the command line.
10 |
--------------------------------------------------------------------------------
/test/test_prompts/robust-logging.txt:
--------------------------------------------------------------------------------
1 | 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
2 |
3 | only log key data and don't log anything that will show too large of an
4 | output. the goal is to be able to follow the flow of the logic, not see *all
5 | * the data.
6 |
7 | especially add comprehensive logging to anything related to the 'missingFileResponse' functionality. as well as everything within spitting distance of this code:
8 |
9 | active.Stream(shared.StreamMessage{ Type:
10 | shared.StreamMessagePromptMissingFile, MissingFilePath: currentFile, })
11 |
12 | // stop stream for now
13 | active.CancelModelStreamFn()
14 |
15 | // wait for user response to come in
16 | userChoice := <-active.MissingFileResponseCh
--------------------------------------------------------------------------------
/test/test_prompts/stripe-plan.txt:
--------------------------------------------------------------------------------
1 | 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).
2 |
3 | I 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.
4 |
5 | We should add functionality to add and remove users.
6 |
7 | There 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.
8 |
9 | We should also add webhook callbacks for charge successful, charge failed, and subscription canceled events. Implement these webhooks and the associated routes.
10 |
11 | The Stripe secret key will be available in an environment variable.
12 |
13 | Don't add the stripe library dependency. I'll take care of that.
--------------------------------------------------------------------------------
/test/test_prompts/tic-tac-toe.txt:
--------------------------------------------------------------------------------
1 | Build a complete tic-tac-toe game in rust
2 |
--------------------------------------------------------------------------------