@jetrabbits/agentic 0.3.3 → 0.4.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.
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")"
@@ -42,6 +49,8 @@ ACTIVE_THEME="dark"
42
49
  SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
43
50
  SELECTED_AREAS=()
44
51
  SELECTED_SPECS=()
52
+ SELECTED_MCPS=()
53
+ SELECTED_OPENCODE_PROFILE=""
45
54
  INSTALL_SETTINGS_REPLAY=false
46
55
 
47
56
  SELF_INSTALL_FORCE=false
@@ -749,6 +758,204 @@ ensure_agentic_runtime_requirements() {
749
758
  ensure_hash_available
750
759
  }
751
760
 
761
+
762
+ MCP_REGISTRY_IDS=(opencode-docs playwright kubernetes youtube-transcript docker-mcp context7 mempalace anydb)
763
+ MCP_NONE_OPTION="None / skip"
764
+ OPENCODE_PROFILE_IDS=(openai githubcopilot)
765
+ OPENCODE_PROFILE_NONE_OPTION="No OpenCode model profile"
766
+
767
+ mcp_registry_contains() {
768
+ local expected="$1"
769
+ local mcp_id
770
+ for mcp_id in "${MCP_REGISTRY_IDS[@]}"; do
771
+ [[ "$mcp_id" == "$expected" ]] && return 0
772
+ done
773
+ return 1
774
+ }
775
+
776
+ mcp_title() {
777
+ case "$1" in
778
+ opencode-docs) echo "OpenCode Docs" ;;
779
+ playwright) echo "Playwright" ;;
780
+ kubernetes) echo "Kubernetes" ;;
781
+ youtube-transcript) echo "YouTube Transcript" ;;
782
+ docker-mcp) echo "Docker MCP" ;;
783
+ context7) echo "Context7" ;;
784
+ mempalace) echo "MemPalace" ;;
785
+ anydb) echo "AnyDB" ;;
786
+ *) echo "$1" ;;
787
+ esac
788
+ }
789
+
790
+ mcp_description() {
791
+ case "$1" in
792
+ opencode-docs) echo "OpenCode docs MCP" ;;
793
+ playwright) echo "Browser automation via Playwright MCP" ;;
794
+ kubernetes) echo "Kubernetes pods/logs/exec management" ;;
795
+ youtube-transcript) echo "YouTube transcript extraction" ;;
796
+ docker-mcp) echo "Docker MCP Gateway" ;;
797
+ context7) echo "Fresh library documentation" ;;
798
+ mempalace) echo "Persistent project memory" ;;
799
+ anydb) echo "Database access MCP" ;;
800
+ *) echo "MCP server" ;;
801
+ esac
802
+ }
803
+
804
+ mcp_security() {
805
+ case "$1" in
806
+ kubernetes|docker-mcp|anydb) echo "dangerous" ;;
807
+ playwright|mempalace) echo "sensitive" ;;
808
+ *) echo "safe" ;;
809
+ esac
810
+ }
811
+
812
+ mcp_verified() {
813
+ case "$1" in
814
+ opencode-docs|playwright|kubernetes|youtube-transcript|docker-mcp|context7|mempalace|anydb) return 0 ;;
815
+ *) return 1 ;;
816
+ esac
817
+ }
818
+
819
+ mcp_display_row() {
820
+ local mcp_id="$1"
821
+ local checked="${2:-false}"
822
+ local mark="[ ]"
823
+ [[ "$checked" == true ]] && mark="[x]"
824
+ printf '%s %-20s %s\n' "$mark" "$mcp_id" "$(mcp_description "$mcp_id")"
825
+ }
826
+
827
+ mcp_id_from_display_row() {
828
+ local row="$1"
829
+ row="${row#\[ \] }"
830
+ row="${row#\[x\] }"
831
+ row="${row#\[X\] }"
832
+ row="${row%%[[:space:]]*}"
833
+ printf '%s\n' "$row"
834
+ }
835
+
836
+ selected_mcp_contains() {
837
+ local expected="$1"
838
+ local mcp_id
839
+ for mcp_id in "${SELECTED_MCPS[@]}"; do
840
+ [[ "$mcp_id" == "$expected" ]] && return 0
841
+ done
842
+ return 1
843
+ }
844
+
845
+ add_selected_mcp() {
846
+ local mcp_id="$1"
847
+ mcp_registry_contains "$mcp_id" || {
848
+ warn "Ignoring unknown MCP integration '$mcp_id'"
849
+ return 0
850
+ }
851
+ mcp_verified "$mcp_id" || {
852
+ warn "Skipping MCP integration '$mcp_id' because its package/config is not verified"
853
+ return 0
854
+ }
855
+ unique_append "$mcp_id" SELECTED_MCPS
856
+ }
857
+
858
+ sync_selected_mcps_from_env() {
859
+ local raw="${AGENTIC_ENABLE_MCPS:-}"
860
+ local item
861
+ if [[ -n "$raw" ]]; then
862
+ local old_ifs="$IFS"
863
+ IFS=,
864
+ for item in $raw; do
865
+ item="$(trim "$item")"
866
+ [[ -z "$item" ]] && continue
867
+ add_selected_mcp "$item"
868
+ done
869
+ IFS="$old_ifs"
870
+ fi
871
+ if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
872
+ add_selected_mcp "context7"
873
+ fi
874
+ if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
875
+ add_selected_mcp "mempalace"
876
+ fi
877
+ }
878
+
879
+ sync_legacy_mcp_env_from_selected() {
880
+ if selected_mcp_contains "context7"; then
881
+ AGENTIC_ENABLE_CONTEXT7="y"
882
+ elif [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
883
+ AGENTIC_ENABLE_CONTEXT7="n"
884
+ fi
885
+ if selected_mcp_contains "mempalace"; then
886
+ AGENTIC_ENABLE_MEMPALACE="y"
887
+ elif [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
888
+ AGENTIC_ENABLE_MEMPALACE="n"
889
+ fi
890
+ }
891
+
892
+ selected_mcps_csv() {
893
+ local old_ifs="$IFS"
894
+ IFS=,
895
+ printf '%s\n' "${SELECTED_MCPS[*]:-}"
896
+ IFS="$old_ifs"
897
+ }
898
+
899
+ mcp_registry_json() {
900
+ cat <<'JSON'
901
+ {
902
+ "opencode-docs": {"security":"safe", "default_enabled":false, "target":"mcpServers", "server":"opencode", "command":"npx", "args":["-y", "opencode-docs-mcp"]},
903
+ "playwright": {"security":"sensitive", "default_enabled":false, "target":"mcpServers", "server":"playwright", "command":"npx", "args":["@playwright/mcp@latest"]},
904
+ "kubernetes": {"security":"dangerous", "default_enabled":false, "target":"mcpServers", "server":"kubernetes", "command":"npx", "args":["-y", "kubernetes-mcp-server"]},
905
+ "youtube-transcript": {"security":"safe", "default_enabled":false, "target":"mcpServers", "server":"youtube-transcript", "command":"npx", "args":["-y", "@kimtaeyoon83/mcp-server-youtube-transcript"]},
906
+ "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"]}},
907
+ "context7": {"security":"safe", "default_enabled":false, "target":"mcpServers", "server":"context7", "command":"npx", "args":["-y", "@upstash/context7-mcp"], "remote":"https://mcp.context7.com/mcp"},
908
+ "mempalace": {"security":"sensitive", "default_enabled":false, "target":"mcpServers", "server":"mempalace", "command":"mempalace-mcp", "args":[]},
909
+ "anydb": {"security":"dangerous", "default_enabled":false, "target":"mcpServers", "server":"anydb", "command":"npx", "args":["-y", "anydb-mcp"]}
910
+ }
911
+ JSON
912
+ }
913
+
914
+ opencode_profile_contains() {
915
+ local expected="$1"
916
+ local profile_id
917
+ for profile_id in "${OPENCODE_PROFILE_IDS[@]}"; do
918
+ [[ "$profile_id" == "$expected" ]] && return 0
919
+ done
920
+ return 1
921
+ }
922
+
923
+ opencode_profile_label() {
924
+ case "$1" in
925
+ openai) echo "OpenAI Model Profile" ;;
926
+ githubcopilot) echo "GitHub Copilot Model Profile" ;;
927
+ *) echo "$1" ;;
928
+ esac
929
+ }
930
+
931
+ opencode_profile_id_from_label() {
932
+ local label="$1"
933
+ local profile_id
934
+ for profile_id in "${OPENCODE_PROFILE_IDS[@]}"; do
935
+ if [[ "$label" == "$(opencode_profile_label "$profile_id")" ]]; then
936
+ printf '%s\n' "$profile_id"
937
+ return
938
+ fi
939
+ done
940
+ printf '%s\n' "$label"
941
+ }
942
+
943
+ opencode_plugin_label() {
944
+ case "$1" in
945
+ telegram-notification) echo "Telegram Notifications" ;;
946
+ agent-model-mapper) echo "Agent Model Mapping" ;;
947
+ *) echo "$1" ;;
948
+ esac
949
+ }
950
+
951
+ opencode_plugin_id_from_label() {
952
+ case "$1" in
953
+ "Telegram Notifications") echo "telegram-notification" ;;
954
+ "Agent Model Mapping") echo "agent-model-mapper" ;;
955
+ *) echo "$1" ;;
956
+ esac
957
+ }
958
+
752
959
  selected_agent_os_contains() {
753
960
  local expected="$1"
754
961
  local agent
@@ -764,6 +971,34 @@ project_manifest_path() {
764
971
  printf '%s\n' "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
765
972
  }
766
973
 
974
+ normalize_project_dir_path() {
975
+ local raw="$1"
976
+ local parent base parent_real
977
+
978
+ if [[ -z "$raw" ]]; then
979
+ printf '%s\n' "$raw"
980
+ return
981
+ fi
982
+
983
+ if [[ -d "$raw" ]]; then
984
+ (cd -- "$raw" && pwd -P)
985
+ return
986
+ fi
987
+
988
+ parent="$(dirname -- "$raw")"
989
+ base="$(basename -- "$raw")"
990
+ if [[ -d "$parent" ]]; then
991
+ parent_real="$(cd -- "$parent" && pwd -P)" || {
992
+ printf '%s\n' "$raw"
993
+ return
994
+ }
995
+ printf '%s/%s\n' "$parent_real" "$base"
996
+ return
997
+ fi
998
+
999
+ printf '%s\n' "$raw"
1000
+ }
1001
+
767
1002
  project_rel_path() {
768
1003
  local path="$1"
769
1004
  local rel="${path#"$PROJECT_DIR"/}"
@@ -1040,21 +1275,11 @@ write_agentic_manifest() {
1040
1275
  specs_csv="${SELECTED_SPECS[*]}"
1041
1276
  IFS="$old_ifs"
1042
1277
 
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"
1278
+ sync_legacy_mcp_env_from_selected
1279
+ mcp_integrations_csv="$(selected_mcps_csv)"
1055
1280
 
1056
1281
  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'
1282
+ 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
1283
  import json
1059
1284
  import sys
1060
1285
  from datetime import datetime, timezone
@@ -1074,6 +1299,7 @@ telegram_enabled = sys.argv[11].lower() == "true" if len(sys.argv) > 11 and sys.
1074
1299
  telegram_bot_token = sys.argv[12] if len(sys.argv) > 12 else ""
1075
1300
  telegram_chat_id = sys.argv[13] if len(sys.argv) > 13 else ""
1076
1301
  mapper_enabled = sys.argv[14].lower() == "true" if len(sys.argv) > 14 and sys.argv[14] else None
1302
+ opencode_profile = sys.argv[15] if len(sys.argv) > 15 else ""
1077
1303
  now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
1078
1304
 
1079
1305
  existing = {}
@@ -1150,6 +1376,7 @@ data = {
1150
1376
  "areas": areas,
1151
1377
  "specializations": specs,
1152
1378
  "mcp_integrations": mcp_integrations,
1379
+ "opencode_profile": opencode_profile,
1153
1380
  "opencode_plugins": opencode_plugins,
1154
1381
  "source_repo": repo_link,
1155
1382
  "source_checkout": repo_root,
@@ -1198,6 +1425,9 @@ for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
1198
1425
  print("::" + key)
1199
1426
  for value in settings.get(key, []):
1200
1427
  print(value)
1428
+ print("::opencode_profile")
1429
+ if settings.get("opencode_profile"):
1430
+ print(settings.get("opencode_profile"))
1201
1431
  plugins = settings.get("opencode_plugins", {})
1202
1432
  if isinstance(plugins, dict):
1203
1433
  telegram = plugins.get("telegram", {})
@@ -1228,6 +1458,7 @@ PY
1228
1458
  local loaded_telegram_bot_token=""
1229
1459
  local loaded_telegram_chat_id=""
1230
1460
  local loaded_mapper_enabled=""
1461
+ local loaded_opencode_profile=""
1231
1462
  local value
1232
1463
  for value in "${values[@]}"; do
1233
1464
  case "$value" in
@@ -1235,12 +1466,18 @@ PY
1235
1466
  "::areas") section="areas" ;;
1236
1467
  "::specializations") section="specializations" ;;
1237
1468
  "::mcp_integrations") section="mcp_integrations" ;;
1469
+ "::opencode_profile") section="opencode_profile" ;;
1470
+ "::opencode_telegram_enabled") section="opencode_telegram_enabled" ;;
1471
+ "::opencode_telegram_bot_token") section="opencode_telegram_bot_token" ;;
1472
+ "::opencode_telegram_chat_id") section="opencode_telegram_chat_id" ;;
1473
+ "::opencode_agent_model_mapper_enabled") section="opencode_agent_model_mapper_enabled" ;;
1238
1474
  *)
1239
1475
  case "$section" in
1240
1476
  agent_os) loaded_agent_os+=("$value") ;;
1241
1477
  areas) loaded_areas+=("$value") ;;
1242
1478
  specializations) loaded_specs+=("$value") ;;
1243
1479
  mcp_integrations) loaded_mcp_integrations+=("$value") ;;
1480
+ opencode_profile) loaded_opencode_profile="$value" ;;
1244
1481
  opencode_telegram_enabled) loaded_telegram_enabled="$value" ;;
1245
1482
  opencode_telegram_bot_token) loaded_telegram_bot_token="$value" ;;
1246
1483
  opencode_telegram_chat_id) loaded_telegram_chat_id="$value" ;;
@@ -1260,23 +1497,13 @@ PY
1260
1497
  SELECTED_SPECS=("${loaded_specs[@]}")
1261
1498
  fi
1262
1499
 
1263
- # Restore MCP integration selections so configure_*_if_needed skip interactive prompts
1500
+ # Restore MCP integration selections so MCP config replay skips interactive prompts.
1264
1501
  local mcp_item
1265
1502
  if [[ "${#loaded_mcp_integrations[@]}" -gt 0 ]]; then
1266
1503
  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
1504
+ add_selected_mcp "$mcp_item"
1279
1505
  done
1506
+ sync_legacy_mcp_env_from_selected
1280
1507
  fi
1281
1508
 
1282
1509
  if [[ -n "$loaded_telegram_enabled" ]]; then
@@ -1289,6 +1516,9 @@ PY
1289
1516
  OPENCODE_AGENT_MODEL_MAPPER_ENABLED="$loaded_mapper_enabled"
1290
1517
  OPENCODE_PLUGINS_CONFIGURED=true
1291
1518
  fi
1519
+ if [[ -n "$loaded_opencode_profile" ]]; then
1520
+ SELECTED_OPENCODE_PROFILE="$loaded_opencode_profile"
1521
+ fi
1292
1522
  }
1293
1523
 
1294
1524
  path_ref_for_shell_export() {
@@ -1495,6 +1725,8 @@ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
1495
1725
  emit("DIR", str(dest_root))
1496
1726
  for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
1497
1727
  rel = file_path.relative_to(src)
1728
+ if str(src).endswith("/extensions/opencode") and rel.parts and rel.parts[0] == "profiles":
1729
+ continue
1498
1730
  target = dest_root / rel
1499
1731
  project_rel = rel_to_project(target)
1500
1732
  source_ref = rel_to_repo(file_path)
@@ -1727,11 +1959,192 @@ PY
1727
1959
  fi
1728
1960
  }
1729
1961
 
1962
+
1963
+ detect_configured_mcps() {
1964
+ local project_dir="$1"
1965
+ [[ -n "$project_dir" ]] || return 0
1966
+ python3 - "$project_dir" "$HOME" <<'PY'
1967
+ import json
1968
+ import re
1969
+ import sys
1970
+ from pathlib import Path
1971
+ project = Path(sys.argv[1]); home = Path(sys.argv[2]); found = set()
1972
+ 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"}
1973
+ 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"]
1974
+ for path in json_paths:
1975
+ if not path.exists(): continue
1976
+ try: data = json.loads(path.read_text(encoding="utf-8"))
1977
+ except Exception: continue
1978
+ if not isinstance(data, dict): continue
1979
+ for section in ("mcpServers", "mcp"):
1980
+ value = data.get(section, {})
1981
+ if isinstance(value, dict):
1982
+ for server in value:
1983
+ if server in server_to_id: found.add(server_to_id[server])
1984
+ config = project/".codex"/"config.toml"
1985
+ if config.exists():
1986
+ try: text = config.read_text(encoding="utf-8")
1987
+ except Exception: text = ""
1988
+ for server, mcp_id in server_to_id.items():
1989
+ if re.search(rf"(?m)^\[mcp_servers\.{re.escape(server)}\]", text): found.add(mcp_id)
1990
+ for mcp_id in sorted(found): print(mcp_id)
1991
+ PY
1992
+ }
1993
+
1994
+ write_selected_mcp_json_config() {
1995
+ local dest="$1"
1996
+ local source_ref="$2"
1997
+ local platform="$3"
1998
+ shift 3
1999
+ local selected_ids=("$@")
2000
+ [[ "${#selected_ids[@]}" -gt 0 ]] || return 0
2001
+ local selected_csv old_ifs registry_json
2002
+ old_ifs="$IFS"; IFS=,; selected_csv="${selected_ids[*]}"; IFS="$old_ifs"
2003
+ registry_json="$(mcp_registry_json)"
2004
+ local body
2005
+ body="$(python3 - "$platform" "$selected_csv" "$registry_json" <<'PY'
2006
+ import json, sys
2007
+ platform = sys.argv[1]
2008
+ selected = [x for x in sys.argv[2].split(",") if x]
2009
+ registry = json.loads(sys.argv[3])
2010
+ print('registry = ' + repr(registry))
2011
+ print('selected = ' + repr(selected))
2012
+ print('platform = ' + repr(platform))
2013
+ print(r'''
2014
+ def opencode_local(command, args=None):
2015
+ values = [command] + list(args or [])
2016
+ return {"type": "local", "command": values, "enabled": True}
2017
+
2018
+ def opencode_remote(url, headers=None):
2019
+ cfg = {"type": "remote", "url": url, "enabled": True}
2020
+ if headers:
2021
+ cfg["headers"] = headers
2022
+ return cfg
2023
+
2024
+ if platform == "opencode":
2025
+ data.setdefault("$schema", "https://opencode.ai/config.json")
2026
+ legacy_servers = data.pop("mcpServers", {})
2027
+ mcp = data.setdefault("mcp", {})
2028
+ if isinstance(legacy_servers, dict):
2029
+ for legacy_server, legacy_cfg in legacy_servers.items():
2030
+ if legacy_server in mcp:
2031
+ continue
2032
+ if not isinstance(legacy_cfg, dict):
2033
+ continue
2034
+ command = legacy_cfg.get("command")
2035
+ args = legacy_cfg.get("args", [])
2036
+ if isinstance(command, str):
2037
+ mcp[legacy_server] = opencode_local(command, args if isinstance(args, list) else [])
2038
+ for mcp_id in selected:
2039
+ entry = registry.get(mcp_id)
2040
+ if not entry:
2041
+ continue
2042
+ server = entry["server"]
2043
+ if platform == "opencode":
2044
+ if mcp_id == "docker-mcp":
2045
+ data.setdefault("mcp", {})[server] = dict(entry.get("opencode", {}))
2046
+ elif mcp_id == "context7":
2047
+ headers = {"CONTEXT7_API_KEY": context7_api_key} if context7_api_key else None
2048
+ data.setdefault("mcp", {})[server] = opencode_remote(entry.get("remote", ""), headers)
2049
+ else:
2050
+ data.setdefault("mcp", {})[server] = opencode_local(entry["command"], entry.get("args", []))
2051
+ continue
2052
+ if mcp_id == "docker-mcp":
2053
+ cfg = dict(entry.get("generic", {}))
2054
+ else:
2055
+ cfg = {"command": entry["command"]}
2056
+ args = list(entry.get("args", []))
2057
+ if args:
2058
+ cfg["args"] = args
2059
+ if mcp_id == "context7" and context7_api_key:
2060
+ cfg["env"] = {"CONTEXT7_API_KEY": context7_api_key}
2061
+ data.setdefault("mcpServers", {})[server] = cfg
2062
+ ''')
2063
+ PY
2064
+ )"
2065
+ write_json_config_file "$dest" "$source_ref" "$body"
2066
+ }
2067
+
2068
+ write_selected_mcp_codex_config() {
2069
+ local selected_ids=("$@")
2070
+ [[ "${#selected_ids[@]}" -gt 0 ]] || return 0
2071
+ local dest="$PROJECT_DIR/.codex/config.toml"
2072
+ local selected_csv old_ifs registry_json
2073
+ old_ifs="$IFS"; IFS=,; selected_csv="${selected_ids[*]}"; IFS="$old_ifs"
2074
+ registry_json="$(mcp_registry_json)"
2075
+ local body
2076
+ body="$(python3 - "$dest" "$selected_csv" "$registry_json" "$CONTEXT7_API_KEY" <<'PY'
2077
+ import json, re, sys
2078
+ from pathlib import Path
2079
+ 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]
2080
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
2081
+ for mcp_id in selected:
2082
+ entry = registry.get(mcp_id)
2083
+ if not entry: continue
2084
+ server = entry["server"]
2085
+ text = re.sub(rf"(?ms)^\[mcp_servers\.{re.escape(server)}\]\n.*?(?=^\[|\Z)", "", text).strip()
2086
+ if mcp_id == "docker-mcp": command, args = "docker", ["mcp", "gateway", "run"]
2087
+ else: command, args = entry["command"], entry.get("args", [])
2088
+ block = f"[mcp_servers.{server}]\ncommand = {json.dumps(command)}\n"
2089
+ if args: block += "args = [" + ", ".join(json.dumps(a) for a in args) + "]\n"
2090
+ if mcp_id == "context7" and context7_api_key: block += 'env = { "CONTEXT7_API_KEY" = ' + json.dumps(context7_api_key) + ' }\n'
2091
+ text = (text.rstrip() + "\n\n" + block).strip() if text else block.strip()
2092
+ print(text.rstrip() + "\n", end="")
2093
+ PY
2094
+ )"
2095
+ write_text_config_file "$dest" "generated:mcp-codex-config" "$body"
2096
+ }
2097
+
2098
+ configure_selected_mcps_if_needed() {
2099
+ sync_selected_mcps_from_env
2100
+ sync_legacy_mcp_env_from_selected
2101
+ [[ "${#SELECTED_MCPS[@]}" -gt 0 ]] || return 0
2102
+ local generic_mcps=()
2103
+ local mcp_id
2104
+ for mcp_id in "${SELECTED_MCPS[@]}"; do
2105
+ case "$mcp_id" in context7|mempalace) ;; *) generic_mcps+=("$mcp_id") ;; esac
2106
+ done
2107
+ [[ "${#generic_mcps[@]}" -gt 0 ]] || return 0
2108
+ if selected_agent_os_contains "opencode"; then
2109
+ write_selected_mcp_json_config "$PROJECT_DIR/opencode.json" "generated:mcp-opencode-config" "opencode" "${generic_mcps[@]}"
2110
+ write_selected_mcp_json_config "$PROJECT_DIR/.opencode/opencode.json" "generated:mcp-opencode-legacy-config" "opencode" "${generic_mcps[@]}"
2111
+ fi
2112
+ if selected_agent_os_contains "codex"; then write_selected_mcp_codex_config "${generic_mcps[@]}"; fi
2113
+ if selected_agent_os_contains "claude"; then write_selected_mcp_json_config "$PROJECT_DIR/.mcp.json" "generated:mcp-claude-config" "generic" "${generic_mcps[@]}"; fi
2114
+ 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
2115
+ 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
2116
+ 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
2117
+ 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
2118
+ }
2119
+
2120
+ check_selected_mcp_runtime_prerequisites() {
2121
+ if selected_mcp_contains "kubernetes"; then
2122
+ if ! kubectl version >/dev/null 2>&1; then
2123
+ warn "Kubernetes MCP selected, but 'kubectl version' did not complete successfully. Install or configure kubectl: https://kubernetes.io/docs/tasks/tools/"
2124
+ fi
2125
+ fi
2126
+
2127
+ if selected_mcp_contains "docker-mcp"; then
2128
+ if ! docker mcp --version >/dev/null 2>&1; then
2129
+ 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/"
2130
+ fi
2131
+ fi
2132
+ }
2133
+
1730
2134
  write_context7_opencode_config() {
1731
2135
  local dest="$PROJECT_DIR/opencode.json"
1732
2136
  local body
1733
2137
  body='
2138
+ legacy_servers = data.pop("mcpServers", {})
1734
2139
  mcp = data.setdefault("mcp", {})
2140
+ if isinstance(legacy_servers, dict):
2141
+ for legacy_server, legacy_cfg in legacy_servers.items():
2142
+ if legacy_server in mcp or not isinstance(legacy_cfg, dict):
2143
+ continue
2144
+ command = legacy_cfg.get("command")
2145
+ args = legacy_cfg.get("args", [])
2146
+ if isinstance(command, str):
2147
+ mcp[legacy_server] = {"type": "local", "command": [command] + (args if isinstance(args, list) else []), "enabled": True}
1735
2148
  context7 = {
1736
2149
  "type": "remote",
1737
2150
  "url": "https://mcp.context7.com/mcp",
@@ -1748,7 +2161,16 @@ write_context7_opencode_legacy_config() {
1748
2161
  local dest="$PROJECT_DIR/.opencode/opencode.json"
1749
2162
  local body
1750
2163
  body='
2164
+ legacy_servers = data.pop("mcpServers", {})
1751
2165
  mcp = data.setdefault("mcp", {})
2166
+ if isinstance(legacy_servers, dict):
2167
+ for legacy_server, legacy_cfg in legacy_servers.items():
2168
+ if legacy_server in mcp or not isinstance(legacy_cfg, dict):
2169
+ continue
2170
+ command = legacy_cfg.get("command")
2171
+ args = legacy_cfg.get("args", [])
2172
+ if isinstance(command, str):
2173
+ mcp[legacy_server] = {"type": "local", "command": [command] + (args if isinstance(args, list) else []), "enabled": True}
1752
2174
  context7 = {
1753
2175
  "type": "remote",
1754
2176
  "url": "https://mcp.context7.com/mcp",
@@ -1761,6 +2183,51 @@ mcp["context7"] = context7
1761
2183
  write_json_config_file "$dest" "generated:context7-opencode-legacy-config" "$body"
1762
2184
  }
1763
2185
 
2186
+ write_codex_features_config() {
2187
+ local dest="$PROJECT_DIR/.codex/config.toml"
2188
+ local body
2189
+ body="$(python3 - "$dest" <<'PY'
2190
+ import re
2191
+ import sys
2192
+ from pathlib import Path
2193
+
2194
+ path = Path(sys.argv[1])
2195
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
2196
+ features_re = re.compile(r"(?ms)^(\[features\]\n)(.*?)(?=^\[|\Z)")
2197
+
2198
+
2199
+ def enable_memories(match):
2200
+ header = match.group(1)
2201
+ body = match.group(2)
2202
+ lines = body.splitlines(keepends=True)
2203
+ output = []
2204
+ found = False
2205
+ for line in lines:
2206
+ if re.match(r"\s*memories\s*=", line):
2207
+ output.append("memories = true" + ("\n" if line.endswith("\n") else ""))
2208
+ found = True
2209
+ else:
2210
+ output.append(line)
2211
+ if not found:
2212
+ trailing = re.search(r"\n*\Z", body).group(0)
2213
+ main = body[: len(body) - len(trailing)] if trailing else body
2214
+ if main and not main.endswith("\n"):
2215
+ main += "\n"
2216
+ return header + main + "memories = true\n" + trailing
2217
+ return header + "".join(output)
2218
+
2219
+
2220
+ if features_re.search(text):
2221
+ print(features_re.sub(enable_memories, text, count=1), end="")
2222
+ elif text.strip():
2223
+ print(text.rstrip() + "\n\n[features]\nmemories = true\n", end="")
2224
+ else:
2225
+ print("[features]\nmemories = true\n", end="")
2226
+ PY
2227
+ )"
2228
+ write_text_config_file "$dest" "generated:codex-features-config" "$body"
2229
+ }
2230
+
1764
2231
  write_context7_codex_config() {
1765
2232
  local dest="$PROJECT_DIR/.codex/config.toml"
1766
2233
  local body
@@ -1900,12 +2367,97 @@ write_mempalace_opencode_config() {
1900
2367
  local dest="$1"
1901
2368
  local body
1902
2369
  body='
2370
+ legacy_servers = data.pop("mcpServers", {})
1903
2371
  mcp = data.setdefault("mcp", {})
2372
+ if isinstance(legacy_servers, dict):
2373
+ for legacy_server, legacy_cfg in legacy_servers.items():
2374
+ if legacy_server in mcp or not isinstance(legacy_cfg, dict):
2375
+ continue
2376
+ command = legacy_cfg.get("command")
2377
+ args = legacy_cfg.get("args", [])
2378
+ if isinstance(command, str):
2379
+ mcp[legacy_server] = {"type": "local", "command": [command] + (args if isinstance(args, list) else []), "enabled": True}
1904
2380
  mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"]}
1905
2381
  '
1906
2382
  write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
1907
2383
  }
1908
2384
 
2385
+ configure_opencode_profile_if_needed() {
2386
+ selected_agent_os_contains "opencode" || return 0
2387
+
2388
+ local profile_id="${AGENTIC_OPENCODE_PROFILE:-$SELECTED_OPENCODE_PROFILE}"
2389
+ profile_id="$(trim "$profile_id")"
2390
+ case "$profile_id" in
2391
+ none|None|skip|Skip|no|No) return 0 ;;
2392
+ esac
2393
+ [[ -n "$profile_id" ]] || return 0
2394
+ if ! opencode_profile_contains "$profile_id"; then
2395
+ warn "Ignoring unknown OpenCode profile '$profile_id'"
2396
+ return 0
2397
+ fi
2398
+ SELECTED_OPENCODE_PROFILE="$profile_id"
2399
+
2400
+ local src="$EXTENSIONS_ROOT/opencode/profiles/$profile_id/opencode.json"
2401
+ local dest="$PROJECT_DIR/.opencode/opencode.json"
2402
+ if [[ ! -f "$src" ]]; then
2403
+ warn "OpenCode profile not found: $src"
2404
+ return 0
2405
+ fi
2406
+ if [[ "$DRY_RUN" == true ]]; then
2407
+ log "DRY-RUN apply OpenCode profile $(opencode_profile_label "$profile_id") to $dest"
2408
+ unique_append "$dest" COPIED_PATHS
2409
+ return 0
2410
+ fi
2411
+ can_write_managed_file "$dest" || return 0
2412
+ ensure_dir "$(dirname -- "$dest")"
2413
+
2414
+ local write_status
2415
+ write_status="$(python3 - "$src" "$dest" <<'PY'
2416
+ import json
2417
+ import sys
2418
+ from pathlib import Path
2419
+
2420
+ src = Path(sys.argv[1])
2421
+ dest = Path(sys.argv[2])
2422
+ profile = json.loads(src.read_text(encoding="utf-8"))
2423
+ if not isinstance(profile, dict):
2424
+ raise SystemExit("OpenCode profile must be a JSON object")
2425
+ try:
2426
+ data = json.loads(dest.read_text(encoding="utf-8")) if dest.exists() else {}
2427
+ except Exception:
2428
+ data = {}
2429
+ if not isinstance(data, dict):
2430
+ data = {}
2431
+
2432
+ def merge(base, incoming):
2433
+ for key, value in incoming.items():
2434
+ if isinstance(value, dict) and isinstance(base.get(key), dict):
2435
+ merge(base[key], value)
2436
+ else:
2437
+ base[key] = value
2438
+ return base
2439
+
2440
+ merge(data, profile)
2441
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
2442
+ if dest.exists():
2443
+ try:
2444
+ if dest.read_text(encoding="utf-8") == output:
2445
+ print("unchanged")
2446
+ raise SystemExit(0)
2447
+ except UnicodeDecodeError:
2448
+ pass
2449
+ dest.write_text(output, encoding="utf-8")
2450
+ print("written")
2451
+ PY
2452
+ )"
2453
+ if [[ "$write_status" == "unchanged" ]]; then
2454
+ register_managed_file "$dest" "generated:opencode-profile-$profile_id" "config" false
2455
+ else
2456
+ register_managed_file "$dest" "generated:opencode-profile-$profile_id" "config"
2457
+ fi
2458
+ log "Applied OpenCode profile: $(opencode_profile_label "$profile_id")"
2459
+ }
2460
+
1909
2461
  write_mempalace_codex_config() {
1910
2462
  local dest="$PROJECT_DIR/.codex/config.toml"
1911
2463
  local body
@@ -2526,7 +3078,12 @@ configure_opencode_plugins_if_needed() {
2526
3078
  return
2527
3079
  fi
2528
3080
 
2529
- local plugin_options=("telegram-notification" "agent-model-mapper")
3081
+ local plugin_options=(
3082
+ "$(opencode_plugin_label "telegram-notification")"
3083
+ "$(opencode_plugin_label "agent-model-mapper")"
3084
+ "$(opencode_profile_label "openai")"
3085
+ "$(opencode_profile_label "githubcopilot")"
3086
+ )
2530
3087
  local selected_plugins=()
2531
3088
  local use_fzf_plugins=false
2532
3089
  if fzf_available; then
@@ -2545,12 +3102,15 @@ configure_opencode_plugins_if_needed() {
2545
3102
 
2546
3103
  local enable_telegram="n" enable_agent_model_mapper="n"
2547
3104
  local selected_plugin
2548
- for selected_plugin in "${selected_plugins[@]}"; do
3105
+ for selected_plugin in ${selected_plugins[@]+"${selected_plugins[@]}"}; do
2549
3106
  selected_plugin="$(trim "$selected_plugin")"
2550
3107
  [[ -z "$selected_plugin" ]] && continue
3108
+ selected_plugin="$(opencode_plugin_id_from_label "$selected_plugin")"
2551
3109
  case "$selected_plugin" in
2552
3110
  telegram-notification|telegram-opencode-notifier) enable_telegram="y" ;;
2553
3111
  agent-model-mapper) enable_agent_model_mapper="y" ;;
3112
+ "OpenAI Model Profile") SELECTED_OPENCODE_PROFILE="openai" ;;
3113
+ "GitHub Copilot Model Profile") SELECTED_OPENCODE_PROFILE="githubcopilot" ;;
2554
3114
  esac
2555
3115
  done
2556
3116
 
@@ -3165,23 +3725,12 @@ append_root_agents_template() {
3165
3725
  generate_agents_md() {
3166
3726
  local project_dir="$1"
3167
3727
  local outputs=()
3168
- local needs_root=false
3169
- local agent_os
3170
3728
 
3171
3729
  if selected_agent_os_contains "opencode"; then
3172
3730
  unique_append "$project_dir/.opencode/AGENTS.md" outputs
3173
3731
  fi
3174
3732
 
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
3733
+ unique_append "$project_dir/AGENTS.md" outputs
3185
3734
 
3186
3735
  if [[ "$DRY_RUN" == true ]]; then
3187
3736
  local dry_run_out
@@ -3651,15 +4200,24 @@ run_install() {
3651
4200
  normalize_selected_agent_os
3652
4201
  validate_inputs
3653
4202
 
4203
+ PROJECT_DIR="$(normalize_project_dir_path "$PROJECT_DIR")"
3654
4204
  ensure_dir "$PROJECT_DIR"
3655
4205
  configure_opencode_plugins_if_needed
3656
4206
  copy_extensions "$PROJECT_DIR"
4207
+ configure_opencode_profile_if_needed
3657
4208
  configure_opencode_agent_model_mapper_if_needed
3658
4209
  copy_specialization_assets "$PROJECT_DIR"
3659
4210
  generate_agents_md "$PROJECT_DIR"
3660
4211
  copy_memory_md "$PROJECT_DIR"
4212
+ if selected_agent_os_contains "codex"; then
4213
+ write_codex_features_config
4214
+ fi
4215
+ sync_selected_mcps_from_env
4216
+ sync_legacy_mcp_env_from_selected
3661
4217
  configure_context7_if_needed
3662
4218
  configure_mempalace_if_needed
4219
+ configure_selected_mcps_if_needed
4220
+ check_selected_mcp_runtime_prerequisites
3663
4221
  write_agentic_manifest "$PROJECT_DIR"
3664
4222
  print_report
3665
4223
  print_missing_agent_binary_guides
@@ -3865,10 +4423,12 @@ choose_multi_by_index() {
3865
4423
  unique_append "${options[$((idx - 1))]}" out
3866
4424
  done
3867
4425
 
3868
- printf '%s\n' "${out[@]}"
4426
+ printf '%s\n' ${out[@]+"${out[@]}"}
3869
4427
  }
3870
4428
 
3871
4429
  fzf_available() {
4430
+ [[ "${AGENTIC_DISABLE_FZF:-}" != "1" ]] || return 1
4431
+ [[ "${AGENTIC_AGENT_MODEL_MAPPER_NO_FZF:-}" != "1" ]] || return 1
3872
4432
  command -v fzf >/dev/null 2>&1
3873
4433
  }
3874
4434
 
@@ -3931,11 +4491,88 @@ auto_install_fzf_windows() {
3931
4491
  return 1
3932
4492
  }
3933
4493
 
4494
+ add_fzf_to_path_if_installed() {
4495
+ local candidate candidate_dir
4496
+ for candidate in "/opt/homebrew/bin/fzf" "/usr/local/bin/fzf"; do
4497
+ if [[ -x "$candidate" ]]; then
4498
+ candidate_dir="$(dirname -- "$candidate")"
4499
+ case ":$PATH:" in
4500
+ *:"$candidate_dir":*) ;;
4501
+ *) PATH="$candidate_dir:$PATH" ;;
4502
+ esac
4503
+ return 0
4504
+ fi
4505
+ done
4506
+ return 1
4507
+ }
4508
+
4509
+ fzf_install_hint() {
4510
+ local platform
4511
+ platform="$(detect_platform)"
4512
+ case "$platform" in
4513
+ macos)
4514
+ cat <<'HINT'
4515
+ Install fzf on macOS with one of:
4516
+ brew install fzf
4517
+ /opt/homebrew/opt/fzf/install
4518
+ If Homebrew is not installed, install it from https://brew.sh/ first.
4519
+ HINT
4520
+ ;;
4521
+ linux)
4522
+ cat <<'HINT'
4523
+ Install fzf with your package manager, for example:
4524
+ sudo apt-get install -y fzf
4525
+ sudo dnf install -y fzf
4526
+ HINT
4527
+ ;;
4528
+ windows)
4529
+ cat <<'HINT'
4530
+ Install fzf with one of:
4531
+ winget install --id junegunn.fzf -e
4532
+ choco install fzf -y
4533
+ scoop install fzf
4534
+ HINT
4535
+ ;;
4536
+ *)
4537
+ echo "Install fzf from https://github.com/junegunn/fzf and re-run agentic tui."
4538
+ ;;
4539
+ esac
4540
+ }
4541
+
4542
+ print_fzf_install_hint() {
4543
+ while IFS= read -r line || [[ -n "$line" ]]; do
4544
+ [[ -n "$line" ]] && warn "$line"
4545
+ done < <(fzf_install_hint)
4546
+ }
4547
+
3934
4548
  auto_install_fzf_macos() {
3935
- if command -v brew >/dev/null 2>&1; then
3936
- brew install fzf
4549
+ if add_fzf_to_path_if_installed && fzf_available; then
4550
+ return 0
4551
+ fi
4552
+
4553
+ if ! command -v brew >/dev/null 2>&1; then
4554
+ warn "Homebrew was not found; cannot auto-install fzf on macOS."
4555
+ print_fzf_install_hint
4556
+ return 1
4557
+ fi
4558
+
4559
+ if ! brew install fzf; then
4560
+ warn "Homebrew failed to install fzf."
4561
+ print_fzf_install_hint
4562
+ return 1
4563
+ fi
4564
+
4565
+ if fzf_available; then
3937
4566
  return 0
3938
4567
  fi
4568
+
4569
+ if add_fzf_to_path_if_installed && fzf_available; then
4570
+ log "Added installed fzf to PATH for this session"
4571
+ return 0
4572
+ fi
4573
+
4574
+ warn "fzf was installed but is not available in PATH for this shell."
4575
+ print_fzf_install_hint
3939
4576
  return 1
3940
4577
  }
3941
4578
 
@@ -4052,7 +4689,7 @@ choose_multi_fzf_strict() {
4052
4689
  readlines picked < <(choose_multi_fzf "$prompt" "$sentinel" "${options[@]}")
4053
4690
 
4054
4691
  local item
4055
- for item in "${picked[@]}"; do
4692
+ for item in ${picked[@]+"${picked[@]}"}; do
4056
4693
  item="$(trim "$item")"
4057
4694
  [[ -z "$item" || "$item" == "$sentinel" ]] && continue
4058
4695
  printf '%s\n' "$item"
@@ -4144,54 +4781,81 @@ run_tui() {
4144
4781
 
4145
4782
  local picked_agent_os=()
4146
4783
  if [[ "$use_fzf" == true ]]; then
4147
- readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" "${agentos_choices[@]}")
4784
+ readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" ${agentos_choices[@]+"${agentos_choices[@]}"})
4148
4785
  else
4149
4786
  local picked_agent_os_output
4150
- picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")"
4787
+ picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" ${agentos_choices[@]+"${agentos_choices[@]}"})"
4151
4788
  readlines picked_agent_os <<< "$picked_agent_os_output"
4152
4789
  fi
4153
4790
  if [[ "${#picked_agent_os[@]}" -eq 0 ]]; then
4154
4791
  SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
4155
4792
  else
4156
- SELECTED_AGENT_OS=("${picked_agent_os[@]}")
4157
- fi
4793
+ SELECTED_AGENT_OS=(${picked_agent_os[@]+"${picked_agent_os[@]}"})
4794
+ fi
4795
+
4796
+ local detected_mcps=()
4797
+ readlines detected_mcps < <(detect_configured_mcps "$PROJECT_DIR" || true)
4798
+ local mcp_options=("$MCP_NONE_OPTION")
4799
+ local registry_mcp
4800
+ for registry_mcp in "${MCP_REGISTRY_IDS[@]}"; do
4801
+ local checked=false
4802
+ local detected_mcp
4803
+ for detected_mcp in ${detected_mcps[@]+"${detected_mcps[@]}"}; do
4804
+ if [[ "$detected_mcp" == "$registry_mcp" ]]; then
4805
+ checked=true
4806
+ break
4807
+ fi
4808
+ done
4809
+ mcp_options+=("$(mcp_display_row "$registry_mcp" "$checked")")
4810
+ done
4158
4811
 
4159
- local mcp_options=("context7" "mempalace")
4160
4812
  local picked_mcps=()
4161
4813
  if [[ "$use_fzf" == true ]]; then
4162
- readlines picked_mcps < <(choose_multi_fzf_strict "Select optional MCP integration(s):" "${mcp_options[@]}")
4814
+ readlines picked_mcps < <(choose_multi_fzf "Select MCP servers to enable:" ${mcp_options[@]+"${mcp_options[@]}"})
4163
4815
  else
4164
4816
  local picked_mcps_output
4165
- picked_mcps_output="$(choose_multi_by_index "Select optional MCP integration(s):" "<none>" "${mcp_options[@]}")"
4817
+ picked_mcps_output="$(choose_multi_by_index "Select MCP servers to enable:" ${mcp_options[@]+"${mcp_options[@]}"})"
4166
4818
  readlines picked_mcps <<< "$picked_mcps_output"
4167
4819
  fi
4168
4820
 
4821
+ SELECTED_MCPS=()
4169
4822
  AGENTIC_ENABLE_CONTEXT7="n"
4170
4823
  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
4824
+ local picked_mcp picked_mcp_id
4825
+ if [[ "${#picked_mcps[@]}" -eq 0 && "${#detected_mcps[@]}" -gt 0 ]]; then
4826
+ for picked_mcp_id in ${detected_mcps[@]+"${detected_mcps[@]}"}; do
4827
+ add_selected_mcp "$picked_mcp_id"
4828
+ done
4829
+ else
4830
+ for picked_mcp in ${picked_mcps[@]+"${picked_mcps[@]}"}; do
4831
+ picked_mcp="$(trim "$picked_mcp")"
4832
+ [[ -z "$picked_mcp" ]] && continue
4833
+ if [[ "$picked_mcp" == "$MCP_NONE_OPTION" ]]; then
4834
+ SELECTED_MCPS=()
4835
+ break
4836
+ fi
4837
+ picked_mcp_id="$(mcp_id_from_display_row "$picked_mcp")"
4838
+ add_selected_mcp "$picked_mcp_id"
4839
+ done
4840
+ fi
4841
+ sync_legacy_mcp_env_from_selected
4178
4842
 
4179
4843
  local areas=()
4180
4844
  readlines areas < <(list_areas)
4181
4845
 
4182
4846
  local picked_areas=()
4183
4847
  if [[ "$use_fzf" == true ]]; then
4184
- readlines picked_areas < <(choose_multi_fzf "Select area(s):" "${areas[@]}")
4848
+ readlines picked_areas < <(choose_multi_fzf "Select area(s):" ${areas[@]+"${areas[@]}"})
4185
4849
  else
4186
4850
  local picked_areas_output
4187
- picked_areas_output="$(choose_multi_by_index "Select area(s):" "${areas[@]}")"
4851
+ picked_areas_output="$(choose_multi_by_index "Select area(s):" ${areas[@]+"${areas[@]}"})"
4188
4852
  readlines picked_areas <<< "$picked_areas_output"
4189
4853
  fi
4190
4854
 
4191
4855
  if [[ "${#picked_areas[@]}" -eq 0 ]]; then
4192
4856
  SELECTED_AREAS=(software)
4193
4857
  else
4194
- SELECTED_AREAS=("${picked_areas[@]}")
4858
+ SELECTED_AREAS=(${picked_areas[@]+"${picked_areas[@]}"})
4195
4859
  fi
4196
4860
 
4197
4861
  SELECTED_SPECS=()
@@ -4202,10 +4866,10 @@ run_tui() {
4202
4866
 
4203
4867
  local chosen_specs=()
4204
4868
  if [[ "$use_fzf" == true ]]; then
4205
- readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" "${specs[@]}")
4869
+ readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" ${specs[@]+"${specs[@]}"})
4206
4870
  else
4207
4871
  local chosen_specs_output
4208
- chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")"
4872
+ chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" ${specs[@]+"${specs[@]}"})"
4209
4873
  readlines chosen_specs <<< "$chosen_specs_output"
4210
4874
  fi
4211
4875
 
@@ -4215,7 +4879,7 @@ run_tui() {
4215
4879
  fi
4216
4880
 
4217
4881
  local spec
4218
- for spec in "${chosen_specs[@]}"; do
4882
+ for spec in ${chosen_specs[@]+"${chosen_specs[@]}"}; do
4219
4883
  SELECTED_SPECS+=("$area.$spec")
4220
4884
  done
4221
4885
  done