@jetrabbits/agentic 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +8 -0
  2. package/CHANGELOG.md +18 -0
  3. package/Makefile +26 -5
  4. package/README.md +25 -6
  5. package/agentic +801 -66
  6. package/areas/devops/ci-cd/workflows/onboard-repo.md +29 -0
  7. package/areas/devops/ci-cd/workflows/pipeline-debug.md +26 -0
  8. package/areas/devops/ci-cd/workflows/release-pipeline.md +53 -0
  9. package/areas/devops/database-ops/workflows/backup-verify.md +27 -0
  10. package/areas/devops/database-ops/workflows/db-incident.md +30 -0
  11. package/areas/devops/devsecops/workflows/policy-onboard.md +34 -0
  12. package/areas/devops/devsecops/workflows/security-scan-pipeline.md +33 -0
  13. package/areas/devops/infrastructure/workflows/destroy-environment.md +31 -0
  14. package/areas/devops/infrastructure/workflows/drift-remediation.md +29 -0
  15. package/areas/devops/infrastructure/workflows/module-development.md +32 -0
  16. package/areas/devops/infrastructure/workflows/provision-environment.md +29 -0
  17. package/areas/devops/kubernetes/workflows/cluster-bootstrap.md +36 -0
  18. package/areas/devops/kubernetes/workflows/debug-workload.md +29 -0
  19. package/areas/devops/kubernetes/workflows/onboard-service.md +35 -0
  20. package/areas/devops/kubernetes/workflows/upgrade-cluster.md +30 -0
  21. package/areas/devops/networking/workflows/onboard-ingress.md +27 -0
  22. package/areas/devops/networking/workflows/service-mesh-onboard.md +27 -0
  23. package/areas/devops/observability/workflows/alert-investigation.md +29 -0
  24. package/areas/devops/observability/workflows/observability-stack-setup.md +33 -0
  25. package/areas/devops/observability/workflows/onboard-service-monitoring.md +31 -0
  26. package/areas/devops/sre/workflows/incident-response.md +48 -0
  27. package/areas/devops/sre/workflows/postmortem.md +32 -0
  28. package/areas/devops/sre/workflows/slo-review.md +35 -1
  29. package/areas/software/backend/workflows/add-migration.md +33 -0
  30. package/areas/software/backend/workflows/create-endpoint.md +40 -0
  31. package/areas/software/backend/workflows/debug-issue.md +31 -0
  32. package/areas/software/backend/workflows/develop-epic.md +37 -0
  33. package/areas/software/backend/workflows/develop-feature.md +44 -0
  34. package/areas/software/backend/workflows/refactor-module.md +35 -0
  35. package/areas/software/backend/workflows/test-feature.md +30 -0
  36. package/areas/software/data-engineering/workflows/backfill-data.md +25 -0
  37. package/areas/software/data-engineering/workflows/data-quality-incident.md +31 -0
  38. package/areas/software/data-engineering/workflows/lineage-trace.md +25 -0
  39. package/areas/software/data-engineering/workflows/new-model.md +30 -0
  40. package/areas/software/data-engineering/workflows/schema-migration.md +29 -0
  41. package/areas/software/frontend/workflows/a11y-fix.md +30 -0
  42. package/areas/software/frontend/workflows/bundle-analyze.md +28 -0
  43. package/areas/software/frontend/workflows/release-prep.md +33 -0
  44. package/areas/software/frontend/workflows/scaffold-component.md +32 -0
  45. package/areas/software/frontend/workflows/visual-regression.md +32 -0
  46. package/areas/software/full-stack/workflows/backend-project-full-cycle.md +47 -2
  47. package/areas/software/full-stack/workflows/debug-issue.md +29 -0
  48. package/areas/software/full-stack/workflows/develop-feature.md +38 -0
  49. package/areas/software/full-stack/workflows/feature-implementation-flow.md +38 -0
  50. package/areas/software/full-stack/workflows/testing-ci-pipeline.md +30 -0
  51. package/areas/software/general/workflows/code-review-workflow.md +31 -0
  52. package/areas/software/general/workflows/development-cycle-workflow.md +38 -0
  53. package/areas/software/general/workflows/project-setup-workflow.md +38 -0
  54. package/areas/software/mlops/workflows/champion-challenger.md +29 -0
  55. package/areas/software/mlops/workflows/deploy-endpoint.md +30 -0
  56. package/areas/software/mlops/workflows/evaluate-model.md +28 -0
  57. package/areas/software/mlops/workflows/model-incident.md +29 -0
  58. package/areas/software/mlops/workflows/train-experiment.md +25 -0
  59. package/areas/software/mobile/workflows/crash-triage.md +28 -0
  60. package/areas/software/mobile/workflows/device-testing.md +27 -0
  61. package/areas/software/mobile/workflows/ota-update.md +25 -0
  62. package/areas/software/mobile/workflows/release-build.md +30 -0
  63. package/areas/software/mobile/workflows/store-submission.md +29 -0
  64. package/areas/software/platform/workflows/cost-audit.md +28 -0
  65. package/areas/software/platform/workflows/deploy-production.md +30 -0
  66. package/areas/software/platform/workflows/drift-check.md +29 -0
  67. package/areas/software/platform/workflows/incident-response.md +33 -0
  68. package/areas/software/platform/workflows/provision-env.md +36 -0
  69. package/areas/software/qa/workflows/flakiness-investigation.md +30 -0
  70. package/areas/software/qa/workflows/performance-audit.md +29 -0
  71. package/areas/software/qa/workflows/regression-suite.md +28 -0
  72. package/areas/software/qa/workflows/smoke-test.md +31 -0
  73. package/areas/software/qa/workflows/test-coverage-report.md +28 -0
  74. package/areas/software/security/workflows/compliance-report.md +27 -0
  75. package/areas/software/security/workflows/pen-test-sim.md +28 -0
  76. package/areas/software/security/workflows/secret-rotation.md +33 -2
  77. package/areas/software/security/workflows/security-scan.md +29 -0
  78. package/areas/software/security/workflows/threat-model-review.md +30 -0
  79. package/docs/agentic-usage.md +19 -2
  80. package/docs/catalog.schema.json +5 -1
  81. package/docs/mcp/README.md +28 -0
  82. package/docs/opencode_setup.md +21 -1
  83. package/docs/site/README.md +15 -1
  84. package/docs/site/app.js +68 -0
  85. package/docs/site/catalog.json +74 -1
  86. package/docs/site/index.html +5 -1
  87. package/docs/site/styles.css +52 -4
  88. package/extensions/opencode/opencode.json +0 -1
  89. package/extensions/opencode/profiles/githubcopilot/opencode.json +87 -0
  90. package/extensions/opencode/profiles/openai/opencode.json +100 -0
  91. package/package.json +1 -1
  92. package/scripts/build_docs_catalog.py +13 -1
  93. package/scripts/sync_workflow_diagrams.py +199 -0
  94. package/extensions/opencode/plugins/sound-notification.ts +0 -13
package/agentic CHANGED
@@ -3,6 +3,13 @@
3
3
  set -euo pipefail
4
4
  shopt -s inherit_errexit 2>/dev/null || true
5
5
 
6
+ # macOS ships bash 3.2, where empty arrays and set -u interact poorly.
7
+ # Keep the CLI quiet and compatible there while retaining nounset on newer bash.
8
+ AGENTIC_BASH_MAJOR="${AGENTIC_TEST_BASH_MAJOR:-${BASH_VERSINFO[0]}}"
9
+ if [[ "$AGENTIC_BASH_MAJOR" =~ ^[0-9]+$ ]] && (( AGENTIC_BASH_MAJOR < 4 )); then
10
+ set +u
11
+ fi
12
+
6
13
  SCRIPT_SOURCE="${BASH_SOURCE[0]}"
7
14
  SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_SOURCE")" && pwd -P)"
8
15
  SCRIPT_NAME="$(basename -- "$SCRIPT_SOURCE")"
@@ -24,6 +31,7 @@ XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
24
31
  APP_CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME"
25
32
  APP_CONFIG_FILE="$APP_CONFIG_DIR/config"
26
33
  OPENCODE_PLUGIN_CONFIG_FILE="$APP_CONFIG_DIR/opencode-plugins.json"
34
+ OPENCODE_USER_PROFILES_DIR="$HOME/.config/$APP_NAME/opencode/profiles"
27
35
  APP_DATA_DIR="$XDG_DATA_HOME/$APP_NAME"
28
36
  APP_REPO_DIR="$APP_DATA_DIR/repo"
29
37
 
@@ -42,6 +50,8 @@ ACTIVE_THEME="dark"
42
50
  SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
43
51
  SELECTED_AREAS=()
44
52
  SELECTED_SPECS=()
53
+ SELECTED_MCPS=()
54
+ SELECTED_OPENCODE_PROFILE=""
45
55
  INSTALL_SETTINGS_REPLAY=false
46
56
 
47
57
  SELF_INSTALL_FORCE=false
@@ -749,6 +759,245 @@ ensure_agentic_runtime_requirements() {
749
759
  ensure_hash_available
750
760
  }
751
761
 
762
+
763
+ MCP_REGISTRY_IDS=(opencode-docs playwright kubernetes youtube-transcript docker-mcp context7 mempalace anydb)
764
+ MCP_NONE_OPTION="None / skip"
765
+ OPENCODE_PROFILE_IDS=(openai githubcopilot)
766
+ OPENCODE_PROFILE_NONE_OPTION="No OpenCode model profile"
767
+
768
+ mcp_registry_contains() {
769
+ local expected="$1"
770
+ local mcp_id
771
+ for mcp_id in "${MCP_REGISTRY_IDS[@]}"; do
772
+ [[ "$mcp_id" == "$expected" ]] && return 0
773
+ done
774
+ return 1
775
+ }
776
+
777
+ mcp_title() {
778
+ case "$1" in
779
+ opencode-docs) echo "OpenCode Docs" ;;
780
+ playwright) echo "Playwright" ;;
781
+ kubernetes) echo "Kubernetes" ;;
782
+ youtube-transcript) echo "YouTube Transcript" ;;
783
+ docker-mcp) echo "Docker MCP" ;;
784
+ context7) echo "Context7" ;;
785
+ mempalace) echo "MemPalace" ;;
786
+ anydb) echo "AnyDB" ;;
787
+ *) echo "$1" ;;
788
+ esac
789
+ }
790
+
791
+ mcp_description() {
792
+ case "$1" in
793
+ opencode-docs) echo "OpenCode docs MCP" ;;
794
+ playwright) echo "Browser automation via Playwright MCP" ;;
795
+ kubernetes) echo "Kubernetes pods/logs/exec management" ;;
796
+ youtube-transcript) echo "YouTube transcript extraction" ;;
797
+ docker-mcp) echo "Docker MCP Gateway" ;;
798
+ context7) echo "Fresh library documentation" ;;
799
+ mempalace) echo "Persistent project memory" ;;
800
+ anydb) echo "Database access MCP" ;;
801
+ *) echo "MCP server" ;;
802
+ esac
803
+ }
804
+
805
+ mcp_security() {
806
+ case "$1" in
807
+ kubernetes|docker-mcp|anydb) echo "dangerous" ;;
808
+ playwright|mempalace) echo "sensitive" ;;
809
+ *) echo "safe" ;;
810
+ esac
811
+ }
812
+
813
+ mcp_verified() {
814
+ case "$1" in
815
+ opencode-docs|playwright|kubernetes|youtube-transcript|docker-mcp|context7|mempalace|anydb) return 0 ;;
816
+ *) return 1 ;;
817
+ esac
818
+ }
819
+
820
+ mcp_display_row() {
821
+ local mcp_id="$1"
822
+ local checked="${2:-false}"
823
+ local mark="[ ]"
824
+ [[ "$checked" == true ]] && mark="[x]"
825
+ printf '%s %-20s %s\n' "$mark" "$mcp_id" "$(mcp_description "$mcp_id")"
826
+ }
827
+
828
+ mcp_id_from_display_row() {
829
+ local row="$1"
830
+ row="${row#\[ \] }"
831
+ row="${row#\[x\] }"
832
+ row="${row#\[X\] }"
833
+ row="${row%%[[:space:]]*}"
834
+ printf '%s\n' "$row"
835
+ }
836
+
837
+ selected_mcp_contains() {
838
+ local expected="$1"
839
+ local mcp_id
840
+ for mcp_id in "${SELECTED_MCPS[@]}"; do
841
+ [[ "$mcp_id" == "$expected" ]] && return 0
842
+ done
843
+ return 1
844
+ }
845
+
846
+ add_selected_mcp() {
847
+ local mcp_id="$1"
848
+ mcp_registry_contains "$mcp_id" || {
849
+ warn "Ignoring unknown MCP integration '$mcp_id'"
850
+ return 0
851
+ }
852
+ mcp_verified "$mcp_id" || {
853
+ warn "Skipping MCP integration '$mcp_id' because its package/config is not verified"
854
+ return 0
855
+ }
856
+ unique_append "$mcp_id" SELECTED_MCPS
857
+ }
858
+
859
+ sync_selected_mcps_from_env() {
860
+ local raw="${AGENTIC_ENABLE_MCPS:-}"
861
+ local item
862
+ if [[ -n "$raw" ]]; then
863
+ local old_ifs="$IFS"
864
+ IFS=,
865
+ for item in $raw; do
866
+ item="$(trim "$item")"
867
+ [[ -z "$item" ]] && continue
868
+ add_selected_mcp "$item"
869
+ done
870
+ IFS="$old_ifs"
871
+ fi
872
+ if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
873
+ add_selected_mcp "context7"
874
+ fi
875
+ if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
876
+ add_selected_mcp "mempalace"
877
+ fi
878
+ }
879
+
880
+ sync_legacy_mcp_env_from_selected() {
881
+ if selected_mcp_contains "context7"; then
882
+ AGENTIC_ENABLE_CONTEXT7="y"
883
+ elif [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
884
+ AGENTIC_ENABLE_CONTEXT7="n"
885
+ fi
886
+ if selected_mcp_contains "mempalace"; then
887
+ AGENTIC_ENABLE_MEMPALACE="y"
888
+ elif [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
889
+ AGENTIC_ENABLE_MEMPALACE="n"
890
+ fi
891
+ }
892
+
893
+ selected_mcps_csv() {
894
+ local old_ifs="$IFS"
895
+ IFS=,
896
+ printf '%s\n' "${SELECTED_MCPS[*]:-}"
897
+ IFS="$old_ifs"
898
+ }
899
+
900
+ mcp_registry_json() {
901
+ cat <<'JSON'
902
+ {
903
+ "opencode-docs": {"security":"safe", "default_enabled":false, "target":"mcpServers", "server":"opencode", "command":"npx", "args":["-y", "opencode-docs-mcp"]},
904
+ "playwright": {"security":"sensitive", "default_enabled":false, "target":"mcpServers", "server":"playwright", "command":"npx", "args":["@playwright/mcp@latest"]},
905
+ "kubernetes": {"security":"dangerous", "default_enabled":false, "target":"mcpServers", "server":"kubernetes", "command":"npx", "args":["-y", "kubernetes-mcp-server"]},
906
+ "youtube-transcript": {"security":"safe", "default_enabled":false, "target":"mcpServers", "server":"youtube-transcript", "command":"npx", "args":["-y", "@kimtaeyoon83/mcp-server-youtube-transcript"]},
907
+ "docker-mcp": {"security":"dangerous", "default_enabled":false, "target":"mcp", "server":"docker", "opencode":{"type":"local", "command":["docker", "mcp", "gateway", "run"], "enabled":true}, "generic":{"command":"docker", "args":["mcp", "gateway", "run"]}},
908
+ "context7": {"security":"safe", "default_enabled":false, "target":"mcpServers", "server":"context7", "command":"npx", "args":["-y", "@upstash/context7-mcp"], "remote":"https://mcp.context7.com/mcp"},
909
+ "mempalace": {"security":"sensitive", "default_enabled":false, "target":"mcpServers", "server":"mempalace", "command":"mempalace-mcp", "args":[]},
910
+ "anydb": {"security":"dangerous", "default_enabled":false, "target":"mcpServers", "server":"anydb", "command":"npx", "args":["-y", "anydb-mcp"]}
911
+ }
912
+ JSON
913
+ }
914
+
915
+ opencode_builtin_profile_contains() {
916
+ local expected="$1"
917
+ local profile_id
918
+ for profile_id in "${OPENCODE_PROFILE_IDS[@]}"; do
919
+ [[ "$profile_id" == "$expected" ]] && return 0
920
+ done
921
+ return 1
922
+ }
923
+
924
+ opencode_profile_contains() {
925
+ local expected="$1"
926
+ opencode_builtin_profile_contains "$expected" && return 0
927
+ [[ -f "$OPENCODE_USER_PROFILES_DIR/$expected/opencode.json" ]] && return 0
928
+ return 1
929
+ }
930
+
931
+ opencode_user_profile_ids() {
932
+ [[ -d "$OPENCODE_USER_PROFILES_DIR" ]] || return 0
933
+ find "$OPENCODE_USER_PROFILES_DIR" -mindepth 2 -maxdepth 2 -type f -name opencode.json -print 2>/dev/null | \
934
+ while IFS= read -r profile_config; do
935
+ basename -- "$(dirname -- "$profile_config")"
936
+ done | sort
937
+ }
938
+
939
+ opencode_profile_source_path() {
940
+ local profile_id="$1"
941
+ local bundled_src="$EXTENSIONS_ROOT/opencode/profiles/$profile_id/opencode.json"
942
+ local user_src="$OPENCODE_USER_PROFILES_DIR/$profile_id/opencode.json"
943
+ if [[ -f "$bundled_src" ]]; then
944
+ printf '%s\n' "$bundled_src"
945
+ return
946
+ fi
947
+ printf '%s\n' "$user_src"
948
+ }
949
+
950
+ opencode_profile_label() {
951
+ case "$1" in
952
+ openai) echo "OpenAI Model Profile" ;;
953
+ githubcopilot) echo "GitHub Copilot Model Profile" ;;
954
+ *) echo "$1 profile" ;;
955
+ esac
956
+ }
957
+
958
+ opencode_profile_id_from_label() {
959
+ local label="$1"
960
+ local profile_id
961
+ for profile_id in "${OPENCODE_PROFILE_IDS[@]}"; do
962
+ if [[ "$label" == "$(opencode_profile_label "$profile_id")" ]]; then
963
+ printf '%s\n' "$profile_id"
964
+ return
965
+ fi
966
+ done
967
+ case "$label" in
968
+ *" profile")
969
+ printf '%s\n' "${label%" profile"}"
970
+ return
971
+ ;;
972
+ esac
973
+ printf '%s\n' "$label"
974
+ }
975
+
976
+ opencode_plugin_label() {
977
+ case "$1" in
978
+ telegram-notification) echo "Telegram Notifications" ;;
979
+ agent-model-mapper) echo "Agent Model Mapping" ;;
980
+ *) echo "$1" ;;
981
+ esac
982
+ }
983
+
984
+ opencode_plugin_id_from_label() {
985
+ case "$1" in
986
+ "Telegram Notifications") echo "telegram-notification" ;;
987
+ "Agent Model Mapping") echo "agent-model-mapper" ;;
988
+ *) echo "$1" ;;
989
+ esac
990
+ }
991
+
992
+ opencode_profile_is_none() {
993
+ local profile_id
994
+ profile_id="$(trim "${1:-}")"
995
+ case "$profile_id" in
996
+ ""|none|None|skip|Skip|no|No) return 0 ;;
997
+ *) return 1 ;;
998
+ esac
999
+ }
1000
+
752
1001
  selected_agent_os_contains() {
753
1002
  local expected="$1"
754
1003
  local agent
@@ -764,6 +1013,34 @@ project_manifest_path() {
764
1013
  printf '%s\n' "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
765
1014
  }
766
1015
 
1016
+ normalize_project_dir_path() {
1017
+ local raw="$1"
1018
+ local parent base parent_real
1019
+
1020
+ if [[ -z "$raw" ]]; then
1021
+ printf '%s\n' "$raw"
1022
+ return
1023
+ fi
1024
+
1025
+ if [[ -d "$raw" ]]; then
1026
+ (cd -- "$raw" && pwd -P)
1027
+ return
1028
+ fi
1029
+
1030
+ parent="$(dirname -- "$raw")"
1031
+ base="$(basename -- "$raw")"
1032
+ if [[ -d "$parent" ]]; then
1033
+ parent_real="$(cd -- "$parent" && pwd -P)" || {
1034
+ printf '%s\n' "$raw"
1035
+ return
1036
+ }
1037
+ printf '%s/%s\n' "$parent_real" "$base"
1038
+ return
1039
+ fi
1040
+
1041
+ printf '%s\n' "$raw"
1042
+ }
1043
+
767
1044
  project_rel_path() {
768
1045
  local path="$1"
769
1046
  local rel="${path#"$PROJECT_DIR"/}"
@@ -1040,21 +1317,11 @@ write_agentic_manifest() {
1040
1317
  specs_csv="${SELECTED_SPECS[*]}"
1041
1318
  IFS="$old_ifs"
1042
1319
 
1043
- # Build mcp_integrations list from current env selections
1044
- local mcp_integrations=()
1045
- if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
1046
- mcp_integrations+=("context7")
1047
- fi
1048
- if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
1049
- mcp_integrations+=("mempalace")
1050
- fi
1051
- old_ifs="$IFS"
1052
- IFS=,
1053
- mcp_integrations_csv="${mcp_integrations[*]:-}"
1054
- IFS="$old_ifs"
1320
+ sync_legacy_mcp_env_from_selected
1321
+ mcp_integrations_csv="$(selected_mcps_csv)"
1055
1322
 
1056
1323
  local manifest_status
1057
- manifest_status="$(python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" "$(app_version_label)" "$mcp_integrations_csv" "$OPENCODE_TELEGRAM_ENABLED" "$OPENCODE_TELEGRAM_BOT_TOKEN" "$OPENCODE_TELEGRAM_CHAT_ID" "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" <<'PY'
1324
+ manifest_status="$(python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" "$(app_version_label)" "$mcp_integrations_csv" "$OPENCODE_TELEGRAM_ENABLED" "$OPENCODE_TELEGRAM_BOT_TOKEN" "$OPENCODE_TELEGRAM_CHAT_ID" "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" "$SELECTED_OPENCODE_PROFILE" <<'PY'
1058
1325
  import json
1059
1326
  import sys
1060
1327
  from datetime import datetime, timezone
@@ -1074,6 +1341,7 @@ telegram_enabled = sys.argv[11].lower() == "true" if len(sys.argv) > 11 and sys.
1074
1341
  telegram_bot_token = sys.argv[12] if len(sys.argv) > 12 else ""
1075
1342
  telegram_chat_id = sys.argv[13] if len(sys.argv) > 13 else ""
1076
1343
  mapper_enabled = sys.argv[14].lower() == "true" if len(sys.argv) > 14 and sys.argv[14] else None
1344
+ opencode_profile = sys.argv[15] if len(sys.argv) > 15 else ""
1077
1345
  now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
1078
1346
 
1079
1347
  existing = {}
@@ -1150,6 +1418,7 @@ data = {
1150
1418
  "areas": areas,
1151
1419
  "specializations": specs,
1152
1420
  "mcp_integrations": mcp_integrations,
1421
+ "opencode_profile": opencode_profile,
1153
1422
  "opencode_plugins": opencode_plugins,
1154
1423
  "source_repo": repo_link,
1155
1424
  "source_checkout": repo_root,
@@ -1198,6 +1467,9 @@ for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
1198
1467
  print("::" + key)
1199
1468
  for value in settings.get(key, []):
1200
1469
  print(value)
1470
+ print("::opencode_profile")
1471
+ if settings.get("opencode_profile"):
1472
+ print(settings.get("opencode_profile"))
1201
1473
  plugins = settings.get("opencode_plugins", {})
1202
1474
  if isinstance(plugins, dict):
1203
1475
  telegram = plugins.get("telegram", {})
@@ -1228,6 +1500,7 @@ PY
1228
1500
  local loaded_telegram_bot_token=""
1229
1501
  local loaded_telegram_chat_id=""
1230
1502
  local loaded_mapper_enabled=""
1503
+ local loaded_opencode_profile=""
1231
1504
  local value
1232
1505
  for value in "${values[@]}"; do
1233
1506
  case "$value" in
@@ -1235,12 +1508,18 @@ PY
1235
1508
  "::areas") section="areas" ;;
1236
1509
  "::specializations") section="specializations" ;;
1237
1510
  "::mcp_integrations") section="mcp_integrations" ;;
1511
+ "::opencode_profile") section="opencode_profile" ;;
1512
+ "::opencode_telegram_enabled") section="opencode_telegram_enabled" ;;
1513
+ "::opencode_telegram_bot_token") section="opencode_telegram_bot_token" ;;
1514
+ "::opencode_telegram_chat_id") section="opencode_telegram_chat_id" ;;
1515
+ "::opencode_agent_model_mapper_enabled") section="opencode_agent_model_mapper_enabled" ;;
1238
1516
  *)
1239
1517
  case "$section" in
1240
1518
  agent_os) loaded_agent_os+=("$value") ;;
1241
1519
  areas) loaded_areas+=("$value") ;;
1242
1520
  specializations) loaded_specs+=("$value") ;;
1243
1521
  mcp_integrations) loaded_mcp_integrations+=("$value") ;;
1522
+ opencode_profile) loaded_opencode_profile="$value" ;;
1244
1523
  opencode_telegram_enabled) loaded_telegram_enabled="$value" ;;
1245
1524
  opencode_telegram_bot_token) loaded_telegram_bot_token="$value" ;;
1246
1525
  opencode_telegram_chat_id) loaded_telegram_chat_id="$value" ;;
@@ -1260,23 +1539,13 @@ PY
1260
1539
  SELECTED_SPECS=("${loaded_specs[@]}")
1261
1540
  fi
1262
1541
 
1263
- # Restore MCP integration selections so configure_*_if_needed skip interactive prompts
1542
+ # Restore MCP integration selections so MCP config replay skips interactive prompts.
1264
1543
  local mcp_item
1265
1544
  if [[ "${#loaded_mcp_integrations[@]}" -gt 0 ]]; then
1266
1545
  for mcp_item in "${loaded_mcp_integrations[@]}"; do
1267
- case "$mcp_item" in
1268
- context7)
1269
- if [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
1270
- AGENTIC_ENABLE_CONTEXT7="y"
1271
- fi
1272
- ;;
1273
- mempalace)
1274
- if [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
1275
- AGENTIC_ENABLE_MEMPALACE="y"
1276
- fi
1277
- ;;
1278
- esac
1546
+ add_selected_mcp "$mcp_item"
1279
1547
  done
1548
+ sync_legacy_mcp_env_from_selected
1280
1549
  fi
1281
1550
 
1282
1551
  if [[ -n "$loaded_telegram_enabled" ]]; then
@@ -1289,6 +1558,9 @@ PY
1289
1558
  OPENCODE_AGENT_MODEL_MAPPER_ENABLED="$loaded_mapper_enabled"
1290
1559
  OPENCODE_PLUGINS_CONFIGURED=true
1291
1560
  fi
1561
+ if [[ -n "$loaded_opencode_profile" ]]; then
1562
+ SELECTED_OPENCODE_PROFILE="$loaded_opencode_profile"
1563
+ fi
1292
1564
  }
1293
1565
 
1294
1566
  path_ref_for_shell_export() {
@@ -1367,6 +1639,7 @@ ensure_bin_dir_in_shell_path() {
1367
1639
  copy_dir_contents() {
1368
1640
  local src="$1"
1369
1641
  local dest="$2"
1642
+ local skip_opencode_base_config="${3:-false}"
1370
1643
  ensure_dir "$dest"
1371
1644
  if [[ "$DRY_RUN" == true ]]; then
1372
1645
  log "DRY-RUN copy managed contents $src -> $dest"
@@ -1376,7 +1649,7 @@ copy_dir_contents() {
1376
1649
 
1377
1650
  local event kind value events_file
1378
1651
  events_file="$(mktemp "${TMPDIR:-/tmp}/agentic-copy-events.XXXXXX")"
1379
- python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" "$(app_version_label)" > "$events_file" <<'PY'
1652
+ python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" "$(app_version_label)" "$skip_opencode_base_config" > "$events_file" <<'PY'
1380
1653
  import hashlib
1381
1654
  import json
1382
1655
  import re
@@ -1390,6 +1663,7 @@ project_dir = Path(sys.argv[4])
1390
1663
  manifest = Path(sys.argv[5])
1391
1664
  repo = sys.argv[6]
1392
1665
  version = sys.argv[7]
1666
+ skip_opencode_base_config = sys.argv[8].lower() == "true"
1393
1667
 
1394
1668
 
1395
1669
  def emit(kind: str, value: str) -> None:
@@ -1495,6 +1769,12 @@ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
1495
1769
  emit("DIR", str(dest_root))
1496
1770
  for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
1497
1771
  rel = file_path.relative_to(src)
1772
+ if str(src).endswith("/extensions/opencode") and rel.parts and rel.parts[0] == "profiles":
1773
+ continue
1774
+ if str(src).endswith("/extensions/opencode") and skip_opencode_base_config and rel == Path("opencode.json"):
1775
+ continue
1776
+ if str(src).endswith("/extensions/opencode") and skip_opencode_base_config and rel == Path("plugins/telegram-notification.ts"):
1777
+ continue
1498
1778
  target = dest_root / rel
1499
1779
  project_rel = rel_to_project(target)
1500
1780
  source_ref = rel_to_repo(file_path)
@@ -1727,11 +2007,192 @@ PY
1727
2007
  fi
1728
2008
  }
1729
2009
 
2010
+
2011
+ detect_configured_mcps() {
2012
+ local project_dir="$1"
2013
+ [[ -n "$project_dir" ]] || return 0
2014
+ python3 - "$project_dir" "$HOME" <<'PY'
2015
+ import json
2016
+ import re
2017
+ import sys
2018
+ from pathlib import Path
2019
+ project = Path(sys.argv[1]); home = Path(sys.argv[2]); found = set()
2020
+ server_to_id = {"opencode":"opencode-docs","playwright":"playwright","kubernetes":"kubernetes","youtube-transcript":"youtube-transcript","docker":"docker-mcp","MCP_DOCKER":"docker-mcp","context7":"context7","mempalace":"mempalace","anydb":"anydb"}
2021
+ json_paths = [project/"opencode.json", project/".opencode"/"opencode.json", project/".mcp.json", project/".cursor"/"mcp.json", project/".gemini"/"settings.json", project/".kilocode"/"mcp.json", home/".gemini"/"antigravity"/"mcp_config.json"]
2022
+ for path in json_paths:
2023
+ if not path.exists(): continue
2024
+ try: data = json.loads(path.read_text(encoding="utf-8"))
2025
+ except Exception: continue
2026
+ if not isinstance(data, dict): continue
2027
+ for section in ("mcpServers", "mcp"):
2028
+ value = data.get(section, {})
2029
+ if isinstance(value, dict):
2030
+ for server in value:
2031
+ if server in server_to_id: found.add(server_to_id[server])
2032
+ config = project/".codex"/"config.toml"
2033
+ if config.exists():
2034
+ try: text = config.read_text(encoding="utf-8")
2035
+ except Exception: text = ""
2036
+ for server, mcp_id in server_to_id.items():
2037
+ if re.search(rf"(?m)^\[mcp_servers\.{re.escape(server)}\]", text): found.add(mcp_id)
2038
+ for mcp_id in sorted(found): print(mcp_id)
2039
+ PY
2040
+ }
2041
+
2042
+ write_selected_mcp_json_config() {
2043
+ local dest="$1"
2044
+ local source_ref="$2"
2045
+ local platform="$3"
2046
+ shift 3
2047
+ local selected_ids=("$@")
2048
+ [[ "${#selected_ids[@]}" -gt 0 ]] || return 0
2049
+ local selected_csv old_ifs registry_json
2050
+ old_ifs="$IFS"; IFS=,; selected_csv="${selected_ids[*]}"; IFS="$old_ifs"
2051
+ registry_json="$(mcp_registry_json)"
2052
+ local body
2053
+ body="$(python3 - "$platform" "$selected_csv" "$registry_json" <<'PY'
2054
+ import json, sys
2055
+ platform = sys.argv[1]
2056
+ selected = [x for x in sys.argv[2].split(",") if x]
2057
+ registry = json.loads(sys.argv[3])
2058
+ print('registry = ' + repr(registry))
2059
+ print('selected = ' + repr(selected))
2060
+ print('platform = ' + repr(platform))
2061
+ print(r'''
2062
+ def opencode_local(command, args=None):
2063
+ values = [command] + list(args or [])
2064
+ return {"type": "local", "command": values, "enabled": True}
2065
+
2066
+ def opencode_remote(url, headers=None):
2067
+ cfg = {"type": "remote", "url": url, "enabled": True}
2068
+ if headers:
2069
+ cfg["headers"] = headers
2070
+ return cfg
2071
+
2072
+ if platform == "opencode":
2073
+ data.setdefault("$schema", "https://opencode.ai/config.json")
2074
+ legacy_servers = data.pop("mcpServers", {})
2075
+ mcp = data.setdefault("mcp", {})
2076
+ if isinstance(legacy_servers, dict):
2077
+ for legacy_server, legacy_cfg in legacy_servers.items():
2078
+ if legacy_server in mcp:
2079
+ continue
2080
+ if not isinstance(legacy_cfg, dict):
2081
+ continue
2082
+ command = legacy_cfg.get("command")
2083
+ args = legacy_cfg.get("args", [])
2084
+ if isinstance(command, str):
2085
+ mcp[legacy_server] = opencode_local(command, args if isinstance(args, list) else [])
2086
+ for mcp_id in selected:
2087
+ entry = registry.get(mcp_id)
2088
+ if not entry:
2089
+ continue
2090
+ server = entry["server"]
2091
+ if platform == "opencode":
2092
+ if mcp_id == "docker-mcp":
2093
+ data.setdefault("mcp", {})[server] = dict(entry.get("opencode", {}))
2094
+ elif mcp_id == "context7":
2095
+ headers = {"CONTEXT7_API_KEY": context7_api_key} if context7_api_key else None
2096
+ data.setdefault("mcp", {})[server] = opencode_remote(entry.get("remote", ""), headers)
2097
+ else:
2098
+ data.setdefault("mcp", {})[server] = opencode_local(entry["command"], entry.get("args", []))
2099
+ continue
2100
+ if mcp_id == "docker-mcp":
2101
+ cfg = dict(entry.get("generic", {}))
2102
+ else:
2103
+ cfg = {"command": entry["command"]}
2104
+ args = list(entry.get("args", []))
2105
+ if args:
2106
+ cfg["args"] = args
2107
+ if mcp_id == "context7" and context7_api_key:
2108
+ cfg["env"] = {"CONTEXT7_API_KEY": context7_api_key}
2109
+ data.setdefault("mcpServers", {})[server] = cfg
2110
+ ''')
2111
+ PY
2112
+ )"
2113
+ write_json_config_file "$dest" "$source_ref" "$body"
2114
+ }
2115
+
2116
+ write_selected_mcp_codex_config() {
2117
+ local selected_ids=("$@")
2118
+ [[ "${#selected_ids[@]}" -gt 0 ]] || return 0
2119
+ local dest="$PROJECT_DIR/.codex/config.toml"
2120
+ local selected_csv old_ifs registry_json
2121
+ old_ifs="$IFS"; IFS=,; selected_csv="${selected_ids[*]}"; IFS="$old_ifs"
2122
+ registry_json="$(mcp_registry_json)"
2123
+ local body
2124
+ body="$(python3 - "$dest" "$selected_csv" "$registry_json" "$CONTEXT7_API_KEY" <<'PY'
2125
+ import json, re, sys
2126
+ from pathlib import Path
2127
+ path = Path(sys.argv[1]); selected = [x for x in sys.argv[2].split(",") if x]; registry = json.loads(sys.argv[3]); context7_api_key = sys.argv[4]
2128
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
2129
+ for mcp_id in selected:
2130
+ entry = registry.get(mcp_id)
2131
+ if not entry: continue
2132
+ server = entry["server"]
2133
+ text = re.sub(rf"(?ms)^\[mcp_servers\.{re.escape(server)}\]\n.*?(?=^\[|\Z)", "", text).strip()
2134
+ if mcp_id == "docker-mcp": command, args = "docker", ["mcp", "gateway", "run"]
2135
+ else: command, args = entry["command"], entry.get("args", [])
2136
+ block = f"[mcp_servers.{server}]\ncommand = {json.dumps(command)}\n"
2137
+ if args: block += "args = [" + ", ".join(json.dumps(a) for a in args) + "]\n"
2138
+ if mcp_id == "context7" and context7_api_key: block += 'env = { "CONTEXT7_API_KEY" = ' + json.dumps(context7_api_key) + ' }\n'
2139
+ text = (text.rstrip() + "\n\n" + block).strip() if text else block.strip()
2140
+ print(text.rstrip() + "\n", end="")
2141
+ PY
2142
+ )"
2143
+ write_text_config_file "$dest" "generated:mcp-codex-config" "$body"
2144
+ }
2145
+
2146
+ configure_selected_mcps_if_needed() {
2147
+ sync_selected_mcps_from_env
2148
+ sync_legacy_mcp_env_from_selected
2149
+ [[ "${#SELECTED_MCPS[@]}" -gt 0 ]] || return 0
2150
+ local generic_mcps=()
2151
+ local mcp_id
2152
+ for mcp_id in "${SELECTED_MCPS[@]}"; do
2153
+ case "$mcp_id" in context7|mempalace) ;; *) generic_mcps+=("$mcp_id") ;; esac
2154
+ done
2155
+ [[ "${#generic_mcps[@]}" -gt 0 ]] || return 0
2156
+ if selected_agent_os_contains "opencode"; then
2157
+ write_selected_mcp_json_config "$PROJECT_DIR/opencode.json" "generated:mcp-opencode-config" "opencode" "${generic_mcps[@]}"
2158
+ write_selected_mcp_json_config "$PROJECT_DIR/.opencode/opencode.json" "generated:mcp-opencode-legacy-config" "opencode" "${generic_mcps[@]}"
2159
+ fi
2160
+ if selected_agent_os_contains "codex"; then write_selected_mcp_codex_config "${generic_mcps[@]}"; fi
2161
+ if selected_agent_os_contains "claude"; then write_selected_mcp_json_config "$PROJECT_DIR/.mcp.json" "generated:mcp-claude-config" "generic" "${generic_mcps[@]}"; fi
2162
+ if selected_agent_os_contains "cursor"; then write_selected_mcp_json_config "$PROJECT_DIR/.cursor/mcp.json" "generated:mcp-cursor-config" "generic" "${generic_mcps[@]}"; fi
2163
+ if selected_agent_os_contains "gemini"; then write_selected_mcp_json_config "$PROJECT_DIR/.gemini/settings.json" "generated:mcp-gemini-config" "gemini" "${generic_mcps[@]}"; fi
2164
+ if selected_agent_os_contains "kilocode"; then write_selected_mcp_json_config "$PROJECT_DIR/.kilocode/mcp.json" "generated:mcp-kilocode-config" "generic" "${generic_mcps[@]}"; fi
2165
+ if selected_agent_os_contains "antigravity"; then write_selected_mcp_json_config "$HOME/.gemini/antigravity/mcp_config.json" "generated:mcp-antigravity-config" "generic" "${generic_mcps[@]}"; fi
2166
+ }
2167
+
2168
+ check_selected_mcp_runtime_prerequisites() {
2169
+ if selected_mcp_contains "kubernetes"; then
2170
+ if ! kubectl version >/dev/null 2>&1; then
2171
+ warn "Kubernetes MCP selected, but 'kubectl version' did not complete successfully. Install or configure kubectl: https://kubernetes.io/docs/tasks/tools/"
2172
+ fi
2173
+ fi
2174
+
2175
+ if selected_mcp_contains "docker-mcp"; then
2176
+ if ! docker mcp --version >/dev/null 2>&1; then
2177
+ warn "Docker MCP selected, but 'docker mcp --version' did not complete successfully. Install Docker and Docker MCP support: https://docs.docker.com/get-started/get-docker/ and https://docs.docker.com/ai/mcp-catalog-and-toolkit/"
2178
+ fi
2179
+ fi
2180
+ }
2181
+
1730
2182
  write_context7_opencode_config() {
1731
2183
  local dest="$PROJECT_DIR/opencode.json"
1732
2184
  local body
1733
2185
  body='
2186
+ legacy_servers = data.pop("mcpServers", {})
1734
2187
  mcp = data.setdefault("mcp", {})
2188
+ if isinstance(legacy_servers, dict):
2189
+ for legacy_server, legacy_cfg in legacy_servers.items():
2190
+ if legacy_server in mcp or not isinstance(legacy_cfg, dict):
2191
+ continue
2192
+ command = legacy_cfg.get("command")
2193
+ args = legacy_cfg.get("args", [])
2194
+ if isinstance(command, str):
2195
+ mcp[legacy_server] = {"type": "local", "command": [command] + (args if isinstance(args, list) else []), "enabled": True}
1735
2196
  context7 = {
1736
2197
  "type": "remote",
1737
2198
  "url": "https://mcp.context7.com/mcp",
@@ -1748,7 +2209,16 @@ write_context7_opencode_legacy_config() {
1748
2209
  local dest="$PROJECT_DIR/.opencode/opencode.json"
1749
2210
  local body
1750
2211
  body='
2212
+ legacy_servers = data.pop("mcpServers", {})
1751
2213
  mcp = data.setdefault("mcp", {})
2214
+ if isinstance(legacy_servers, dict):
2215
+ for legacy_server, legacy_cfg in legacy_servers.items():
2216
+ if legacy_server in mcp or not isinstance(legacy_cfg, dict):
2217
+ continue
2218
+ command = legacy_cfg.get("command")
2219
+ args = legacy_cfg.get("args", [])
2220
+ if isinstance(command, str):
2221
+ mcp[legacy_server] = {"type": "local", "command": [command] + (args if isinstance(args, list) else []), "enabled": True}
1752
2222
  context7 = {
1753
2223
  "type": "remote",
1754
2224
  "url": "https://mcp.context7.com/mcp",
@@ -1761,6 +2231,51 @@ mcp["context7"] = context7
1761
2231
  write_json_config_file "$dest" "generated:context7-opencode-legacy-config" "$body"
1762
2232
  }
1763
2233
 
2234
+ write_codex_features_config() {
2235
+ local dest="$PROJECT_DIR/.codex/config.toml"
2236
+ local body
2237
+ body="$(python3 - "$dest" <<'PY'
2238
+ import re
2239
+ import sys
2240
+ from pathlib import Path
2241
+
2242
+ path = Path(sys.argv[1])
2243
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
2244
+ features_re = re.compile(r"(?ms)^(\[features\]\n)(.*?)(?=^\[|\Z)")
2245
+
2246
+
2247
+ def enable_memories(match):
2248
+ header = match.group(1)
2249
+ body = match.group(2)
2250
+ lines = body.splitlines(keepends=True)
2251
+ output = []
2252
+ found = False
2253
+ for line in lines:
2254
+ if re.match(r"\s*memories\s*=", line):
2255
+ output.append("memories = true" + ("\n" if line.endswith("\n") else ""))
2256
+ found = True
2257
+ else:
2258
+ output.append(line)
2259
+ if not found:
2260
+ trailing = re.search(r"\n*\Z", body).group(0)
2261
+ main = body[: len(body) - len(trailing)] if trailing else body
2262
+ if main and not main.endswith("\n"):
2263
+ main += "\n"
2264
+ return header + main + "memories = true\n" + trailing
2265
+ return header + "".join(output)
2266
+
2267
+
2268
+ if features_re.search(text):
2269
+ print(features_re.sub(enable_memories, text, count=1), end="")
2270
+ elif text.strip():
2271
+ print(text.rstrip() + "\n\n[features]\nmemories = true\n", end="")
2272
+ else:
2273
+ print("[features]\nmemories = true\n", end="")
2274
+ PY
2275
+ )"
2276
+ write_text_config_file "$dest" "generated:codex-features-config" "$body"
2277
+ }
2278
+
1764
2279
  write_context7_codex_config() {
1765
2280
  local dest="$PROJECT_DIR/.codex/config.toml"
1766
2281
  local body
@@ -1900,12 +2415,98 @@ write_mempalace_opencode_config() {
1900
2415
  local dest="$1"
1901
2416
  local body
1902
2417
  body='
2418
+ legacy_servers = data.pop("mcpServers", {})
1903
2419
  mcp = data.setdefault("mcp", {})
2420
+ if isinstance(legacy_servers, dict):
2421
+ for legacy_server, legacy_cfg in legacy_servers.items():
2422
+ if legacy_server in mcp or not isinstance(legacy_cfg, dict):
2423
+ continue
2424
+ command = legacy_cfg.get("command")
2425
+ args = legacy_cfg.get("args", [])
2426
+ if isinstance(command, str):
2427
+ mcp[legacy_server] = {"type": "local", "command": [command] + (args if isinstance(args, list) else []), "enabled": True}
1904
2428
  mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"]}
1905
2429
  '
1906
2430
  write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
1907
2431
  }
1908
2432
 
2433
+ configure_opencode_profile_if_needed() {
2434
+ selected_agent_os_contains "opencode" || return 0
2435
+
2436
+ local profile_id="${AGENTIC_OPENCODE_PROFILE:-$SELECTED_OPENCODE_PROFILE}"
2437
+ profile_id="$(trim "$profile_id")"
2438
+ case "$profile_id" in
2439
+ none|None|skip|Skip|no|No) return 0 ;;
2440
+ esac
2441
+ [[ -n "$profile_id" ]] || return 0
2442
+ if ! opencode_profile_contains "$profile_id"; then
2443
+ warn "Ignoring unknown OpenCode profile '$profile_id'"
2444
+ return 0
2445
+ fi
2446
+ SELECTED_OPENCODE_PROFILE="$profile_id"
2447
+
2448
+ local src
2449
+ src="$(opencode_profile_source_path "$profile_id")"
2450
+ local dest="$PROJECT_DIR/.opencode/opencode.json"
2451
+ if [[ ! -f "$src" ]]; then
2452
+ warn "OpenCode profile not found: $src"
2453
+ return 0
2454
+ fi
2455
+ if [[ "$DRY_RUN" == true ]]; then
2456
+ log "DRY-RUN apply OpenCode profile $(opencode_profile_label "$profile_id") to $dest"
2457
+ unique_append "$dest" COPIED_PATHS
2458
+ return 0
2459
+ fi
2460
+ can_write_managed_file "$dest" || return 0
2461
+ ensure_dir "$(dirname -- "$dest")"
2462
+
2463
+ local write_status
2464
+ write_status="$(python3 - "$src" "$dest" <<'PY'
2465
+ import json
2466
+ import sys
2467
+ from pathlib import Path
2468
+
2469
+ src = Path(sys.argv[1])
2470
+ dest = Path(sys.argv[2])
2471
+ profile = json.loads(src.read_text(encoding="utf-8"))
2472
+ if not isinstance(profile, dict):
2473
+ raise SystemExit("OpenCode profile must be a JSON object")
2474
+ try:
2475
+ data = json.loads(dest.read_text(encoding="utf-8")) if dest.exists() else {}
2476
+ except Exception:
2477
+ data = {}
2478
+ if not isinstance(data, dict):
2479
+ data = {}
2480
+
2481
+ def merge(base, incoming):
2482
+ for key, value in incoming.items():
2483
+ if isinstance(value, dict) and isinstance(base.get(key), dict):
2484
+ merge(base[key], value)
2485
+ else:
2486
+ base[key] = value
2487
+ return base
2488
+
2489
+ merge(data, profile)
2490
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
2491
+ if dest.exists():
2492
+ try:
2493
+ if dest.read_text(encoding="utf-8") == output:
2494
+ print("unchanged")
2495
+ raise SystemExit(0)
2496
+ except UnicodeDecodeError:
2497
+ pass
2498
+ dest.write_text(output, encoding="utf-8")
2499
+ print("written")
2500
+ PY
2501
+ )"
2502
+ if [[ "$write_status" == "unchanged" ]]; then
2503
+ register_managed_file "$dest" "generated:opencode-profile-$profile_id" "config" false
2504
+ else
2505
+ register_managed_file "$dest" "generated:opencode-profile-$profile_id" "config"
2506
+ fi
2507
+ log "Applied OpenCode profile: $(opencode_profile_label "$profile_id")"
2508
+ }
2509
+
1909
2510
  write_mempalace_codex_config() {
1910
2511
  local dest="$PROJECT_DIR/.codex/config.toml"
1911
2512
  local body
@@ -2526,7 +3127,19 @@ configure_opencode_plugins_if_needed() {
2526
3127
  return
2527
3128
  fi
2528
3129
 
2529
- local plugin_options=("telegram-notification" "agent-model-mapper")
3130
+ local plugin_options=(
3131
+ "$(opencode_plugin_label "telegram-notification")"
3132
+ "$(opencode_plugin_label "agent-model-mapper")"
3133
+ "$(opencode_profile_label "openai")"
3134
+ "$(opencode_profile_label "githubcopilot")"
3135
+ )
3136
+ local user_profile_id
3137
+ while IFS= read -r user_profile_id; do
3138
+ [[ -z "$user_profile_id" ]] && continue
3139
+ opencode_builtin_profile_contains "$user_profile_id" && continue
3140
+ opencode_profile_contains "$user_profile_id" || continue
3141
+ plugin_options+=("$(opencode_profile_label "$user_profile_id")")
3142
+ done < <(opencode_user_profile_ids)
2530
3143
  local selected_plugins=()
2531
3144
  local use_fzf_plugins=false
2532
3145
  if fzf_available; then
@@ -2545,12 +3158,20 @@ configure_opencode_plugins_if_needed() {
2545
3158
 
2546
3159
  local enable_telegram="n" enable_agent_model_mapper="n"
2547
3160
  local selected_plugin
2548
- for selected_plugin in "${selected_plugins[@]}"; do
3161
+ for selected_plugin in ${selected_plugins[@]+"${selected_plugins[@]}"}; do
2549
3162
  selected_plugin="$(trim "$selected_plugin")"
2550
3163
  [[ -z "$selected_plugin" ]] && continue
3164
+ selected_plugin="$(opencode_plugin_id_from_label "$selected_plugin")"
2551
3165
  case "$selected_plugin" in
2552
3166
  telegram-notification|telegram-opencode-notifier) enable_telegram="y" ;;
2553
3167
  agent-model-mapper) enable_agent_model_mapper="y" ;;
3168
+ *)
3169
+ local selected_profile_id
3170
+ selected_profile_id="$(opencode_profile_id_from_label "$selected_plugin")"
3171
+ if opencode_profile_contains "$selected_profile_id"; then
3172
+ SELECTED_OPENCODE_PROFILE="$selected_profile_id"
3173
+ fi
3174
+ ;;
2554
3175
  esac
2555
3176
  done
2556
3177
 
@@ -3045,7 +3666,17 @@ copy_extension_for_agent() {
3045
3666
  return
3046
3667
  fi
3047
3668
 
3048
- copy_dir_contents "$src" "$dest"
3669
+ local skip_opencode_base_config=false
3670
+ if [[ "$agent_os" == "opencode" ]]; then
3671
+ local profile_id="${AGENTIC_OPENCODE_PROFILE:-$SELECTED_OPENCODE_PROFILE}"
3672
+ if opencode_profile_is_none "$profile_id" \
3673
+ && [[ "$OPENCODE_TELEGRAM_ENABLED" != "true" ]] \
3674
+ && [[ "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" != "true" ]]; then
3675
+ skip_opencode_base_config=true
3676
+ fi
3677
+ fi
3678
+
3679
+ copy_dir_contents "$src" "$dest" "$skip_opencode_base_config"
3049
3680
  }
3050
3681
 
3051
3682
  copy_extensions() {
@@ -3165,23 +3796,12 @@ append_root_agents_template() {
3165
3796
  generate_agents_md() {
3166
3797
  local project_dir="$1"
3167
3798
  local outputs=()
3168
- local needs_root=false
3169
- local agent_os
3170
3799
 
3171
3800
  if selected_agent_os_contains "opencode"; then
3172
3801
  unique_append "$project_dir/.opencode/AGENTS.md" outputs
3173
3802
  fi
3174
3803
 
3175
- for agent_os in "${SELECTED_AGENT_OS[@]}"; do
3176
- if [[ "$agent_os" != "opencode" ]]; then
3177
- needs_root=true
3178
- break
3179
- fi
3180
- done
3181
-
3182
- if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
3183
- unique_append "$project_dir/AGENTS.md" outputs
3184
- fi
3804
+ unique_append "$project_dir/AGENTS.md" outputs
3185
3805
 
3186
3806
  if [[ "$DRY_RUN" == true ]]; then
3187
3807
  local dry_run_out
@@ -3651,15 +4271,24 @@ run_install() {
3651
4271
  normalize_selected_agent_os
3652
4272
  validate_inputs
3653
4273
 
4274
+ PROJECT_DIR="$(normalize_project_dir_path "$PROJECT_DIR")"
3654
4275
  ensure_dir "$PROJECT_DIR"
3655
4276
  configure_opencode_plugins_if_needed
3656
4277
  copy_extensions "$PROJECT_DIR"
4278
+ configure_opencode_profile_if_needed
3657
4279
  configure_opencode_agent_model_mapper_if_needed
3658
4280
  copy_specialization_assets "$PROJECT_DIR"
3659
4281
  generate_agents_md "$PROJECT_DIR"
3660
4282
  copy_memory_md "$PROJECT_DIR"
4283
+ if selected_agent_os_contains "codex"; then
4284
+ write_codex_features_config
4285
+ fi
4286
+ sync_selected_mcps_from_env
4287
+ sync_legacy_mcp_env_from_selected
3661
4288
  configure_context7_if_needed
3662
4289
  configure_mempalace_if_needed
4290
+ configure_selected_mcps_if_needed
4291
+ check_selected_mcp_runtime_prerequisites
3663
4292
  write_agentic_manifest "$PROJECT_DIR"
3664
4293
  print_report
3665
4294
  print_missing_agent_binary_guides
@@ -3865,10 +4494,12 @@ choose_multi_by_index() {
3865
4494
  unique_append "${options[$((idx - 1))]}" out
3866
4495
  done
3867
4496
 
3868
- printf '%s\n' "${out[@]}"
4497
+ printf '%s\n' ${out[@]+"${out[@]}"}
3869
4498
  }
3870
4499
 
3871
4500
  fzf_available() {
4501
+ [[ "${AGENTIC_DISABLE_FZF:-}" != "1" ]] || return 1
4502
+ [[ "${AGENTIC_AGENT_MODEL_MAPPER_NO_FZF:-}" != "1" ]] || return 1
3872
4503
  command -v fzf >/dev/null 2>&1
3873
4504
  }
3874
4505
 
@@ -3931,11 +4562,88 @@ auto_install_fzf_windows() {
3931
4562
  return 1
3932
4563
  }
3933
4564
 
4565
+ add_fzf_to_path_if_installed() {
4566
+ local candidate candidate_dir
4567
+ for candidate in "/opt/homebrew/bin/fzf" "/usr/local/bin/fzf"; do
4568
+ if [[ -x "$candidate" ]]; then
4569
+ candidate_dir="$(dirname -- "$candidate")"
4570
+ case ":$PATH:" in
4571
+ *:"$candidate_dir":*) ;;
4572
+ *) PATH="$candidate_dir:$PATH" ;;
4573
+ esac
4574
+ return 0
4575
+ fi
4576
+ done
4577
+ return 1
4578
+ }
4579
+
4580
+ fzf_install_hint() {
4581
+ local platform
4582
+ platform="$(detect_platform)"
4583
+ case "$platform" in
4584
+ macos)
4585
+ cat <<'HINT'
4586
+ Install fzf on macOS with one of:
4587
+ brew install fzf
4588
+ /opt/homebrew/opt/fzf/install
4589
+ If Homebrew is not installed, install it from https://brew.sh/ first.
4590
+ HINT
4591
+ ;;
4592
+ linux)
4593
+ cat <<'HINT'
4594
+ Install fzf with your package manager, for example:
4595
+ sudo apt-get install -y fzf
4596
+ sudo dnf install -y fzf
4597
+ HINT
4598
+ ;;
4599
+ windows)
4600
+ cat <<'HINT'
4601
+ Install fzf with one of:
4602
+ winget install --id junegunn.fzf -e
4603
+ choco install fzf -y
4604
+ scoop install fzf
4605
+ HINT
4606
+ ;;
4607
+ *)
4608
+ echo "Install fzf from https://github.com/junegunn/fzf and re-run agentic tui."
4609
+ ;;
4610
+ esac
4611
+ }
4612
+
4613
+ print_fzf_install_hint() {
4614
+ while IFS= read -r line || [[ -n "$line" ]]; do
4615
+ [[ -n "$line" ]] && warn "$line"
4616
+ done < <(fzf_install_hint)
4617
+ }
4618
+
3934
4619
  auto_install_fzf_macos() {
3935
- if command -v brew >/dev/null 2>&1; then
3936
- brew install fzf
4620
+ if add_fzf_to_path_if_installed && fzf_available; then
4621
+ return 0
4622
+ fi
4623
+
4624
+ if ! command -v brew >/dev/null 2>&1; then
4625
+ warn "Homebrew was not found; cannot auto-install fzf on macOS."
4626
+ print_fzf_install_hint
4627
+ return 1
4628
+ fi
4629
+
4630
+ if ! brew install fzf; then
4631
+ warn "Homebrew failed to install fzf."
4632
+ print_fzf_install_hint
4633
+ return 1
4634
+ fi
4635
+
4636
+ if fzf_available; then
4637
+ return 0
4638
+ fi
4639
+
4640
+ if add_fzf_to_path_if_installed && fzf_available; then
4641
+ log "Added installed fzf to PATH for this session"
3937
4642
  return 0
3938
4643
  fi
4644
+
4645
+ warn "fzf was installed but is not available in PATH for this shell."
4646
+ print_fzf_install_hint
3939
4647
  return 1
3940
4648
  }
3941
4649
 
@@ -4052,7 +4760,7 @@ choose_multi_fzf_strict() {
4052
4760
  readlines picked < <(choose_multi_fzf "$prompt" "$sentinel" "${options[@]}")
4053
4761
 
4054
4762
  local item
4055
- for item in "${picked[@]}"; do
4763
+ for item in ${picked[@]+"${picked[@]}"}; do
4056
4764
  item="$(trim "$item")"
4057
4765
  [[ -z "$item" || "$item" == "$sentinel" ]] && continue
4058
4766
  printf '%s\n' "$item"
@@ -4144,54 +4852,81 @@ run_tui() {
4144
4852
 
4145
4853
  local picked_agent_os=()
4146
4854
  if [[ "$use_fzf" == true ]]; then
4147
- readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" "${agentos_choices[@]}")
4855
+ readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" ${agentos_choices[@]+"${agentos_choices[@]}"})
4148
4856
  else
4149
4857
  local picked_agent_os_output
4150
- picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")"
4858
+ picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" ${agentos_choices[@]+"${agentos_choices[@]}"})"
4151
4859
  readlines picked_agent_os <<< "$picked_agent_os_output"
4152
4860
  fi
4153
4861
  if [[ "${#picked_agent_os[@]}" -eq 0 ]]; then
4154
4862
  SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
4155
4863
  else
4156
- SELECTED_AGENT_OS=("${picked_agent_os[@]}")
4157
- fi
4864
+ SELECTED_AGENT_OS=(${picked_agent_os[@]+"${picked_agent_os[@]}"})
4865
+ fi
4866
+
4867
+ local detected_mcps=()
4868
+ readlines detected_mcps < <(detect_configured_mcps "$PROJECT_DIR" || true)
4869
+ local mcp_options=("$MCP_NONE_OPTION")
4870
+ local registry_mcp
4871
+ for registry_mcp in "${MCP_REGISTRY_IDS[@]}"; do
4872
+ local checked=false
4873
+ local detected_mcp
4874
+ for detected_mcp in ${detected_mcps[@]+"${detected_mcps[@]}"}; do
4875
+ if [[ "$detected_mcp" == "$registry_mcp" ]]; then
4876
+ checked=true
4877
+ break
4878
+ fi
4879
+ done
4880
+ mcp_options+=("$(mcp_display_row "$registry_mcp" "$checked")")
4881
+ done
4158
4882
 
4159
- local mcp_options=("context7" "mempalace")
4160
4883
  local picked_mcps=()
4161
4884
  if [[ "$use_fzf" == true ]]; then
4162
- readlines picked_mcps < <(choose_multi_fzf_strict "Select optional MCP integration(s):" "${mcp_options[@]}")
4885
+ readlines picked_mcps < <(choose_multi_fzf "Select MCP servers to enable:" ${mcp_options[@]+"${mcp_options[@]}"})
4163
4886
  else
4164
4887
  local picked_mcps_output
4165
- picked_mcps_output="$(choose_multi_by_index "Select optional MCP integration(s):" "<none>" "${mcp_options[@]}")"
4888
+ picked_mcps_output="$(choose_multi_by_index "Select MCP servers to enable:" ${mcp_options[@]+"${mcp_options[@]}"})"
4166
4889
  readlines picked_mcps <<< "$picked_mcps_output"
4167
4890
  fi
4168
4891
 
4892
+ SELECTED_MCPS=()
4169
4893
  AGENTIC_ENABLE_CONTEXT7="n"
4170
4894
  AGENTIC_ENABLE_MEMPALACE="n"
4171
- local picked_mcp
4172
- for picked_mcp in "${picked_mcps[@]}"; do
4173
- case "$picked_mcp" in
4174
- context7) AGENTIC_ENABLE_CONTEXT7="y" ;;
4175
- mempalace) AGENTIC_ENABLE_MEMPALACE="y" ;;
4176
- esac
4177
- done
4895
+ local picked_mcp picked_mcp_id
4896
+ if [[ "${#picked_mcps[@]}" -eq 0 && "${#detected_mcps[@]}" -gt 0 ]]; then
4897
+ for picked_mcp_id in ${detected_mcps[@]+"${detected_mcps[@]}"}; do
4898
+ add_selected_mcp "$picked_mcp_id"
4899
+ done
4900
+ else
4901
+ for picked_mcp in ${picked_mcps[@]+"${picked_mcps[@]}"}; do
4902
+ picked_mcp="$(trim "$picked_mcp")"
4903
+ [[ -z "$picked_mcp" ]] && continue
4904
+ if [[ "$picked_mcp" == "$MCP_NONE_OPTION" ]]; then
4905
+ SELECTED_MCPS=()
4906
+ break
4907
+ fi
4908
+ picked_mcp_id="$(mcp_id_from_display_row "$picked_mcp")"
4909
+ add_selected_mcp "$picked_mcp_id"
4910
+ done
4911
+ fi
4912
+ sync_legacy_mcp_env_from_selected
4178
4913
 
4179
4914
  local areas=()
4180
4915
  readlines areas < <(list_areas)
4181
4916
 
4182
4917
  local picked_areas=()
4183
4918
  if [[ "$use_fzf" == true ]]; then
4184
- readlines picked_areas < <(choose_multi_fzf "Select area(s):" "${areas[@]}")
4919
+ readlines picked_areas < <(choose_multi_fzf "Select area(s):" ${areas[@]+"${areas[@]}"})
4185
4920
  else
4186
4921
  local picked_areas_output
4187
- picked_areas_output="$(choose_multi_by_index "Select area(s):" "${areas[@]}")"
4922
+ picked_areas_output="$(choose_multi_by_index "Select area(s):" ${areas[@]+"${areas[@]}"})"
4188
4923
  readlines picked_areas <<< "$picked_areas_output"
4189
4924
  fi
4190
4925
 
4191
4926
  if [[ "${#picked_areas[@]}" -eq 0 ]]; then
4192
4927
  SELECTED_AREAS=(software)
4193
4928
  else
4194
- SELECTED_AREAS=("${picked_areas[@]}")
4929
+ SELECTED_AREAS=(${picked_areas[@]+"${picked_areas[@]}"})
4195
4930
  fi
4196
4931
 
4197
4932
  SELECTED_SPECS=()
@@ -4202,10 +4937,10 @@ run_tui() {
4202
4937
 
4203
4938
  local chosen_specs=()
4204
4939
  if [[ "$use_fzf" == true ]]; then
4205
- readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" "${specs[@]}")
4940
+ readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" ${specs[@]+"${specs[@]}"})
4206
4941
  else
4207
4942
  local chosen_specs_output
4208
- chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")"
4943
+ chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" ${specs[@]+"${specs[@]}"})"
4209
4944
  readlines chosen_specs <<< "$chosen_specs_output"
4210
4945
  fi
4211
4946
 
@@ -4215,7 +4950,7 @@ run_tui() {
4215
4950
  fi
4216
4951
 
4217
4952
  local spec
4218
- for spec in "${chosen_specs[@]}"; do
4953
+ for spec in ${chosen_specs[@]+"${chosen_specs[@]}"}; do
4219
4954
  SELECTED_SPECS+=("$area.$spec")
4220
4955
  done
4221
4956
  done