@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/CHANGELOG.md +15 -0
- package/Makefile +6 -1
- package/README.md +8 -2
- package/agentic +728 -64
- package/docs/agentic-usage.md +19 -2
- package/docs/mcp/README.md +28 -0
- package/docs/opencode_setup.md +11 -1
- package/extensions/opencode/profiles/githubcopilot/opencode.json +88 -0
- package/extensions/opencode/profiles/openai/opencode.json +100 -0
- package/package.json +1 -1
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
|
-
|
|
1044
|
-
|
|
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
|
|
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
|
-
|
|
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=(
|
|
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
|
-
|
|
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
|
|
3936
|
-
|
|
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 < <(
|
|
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
|
|
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
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
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
|