├── .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 | 16 | ``` 17 | 18 | 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 | ![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) 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 | ![plandex-claude-3.5-sonnet](https://github.com/plandex-ai/plandex/blob/main/releases/images/cli/1.1.1/claude-3-5-sonnet.gif) 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 | 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 | --------------------------------------------------------------------------------