@jetrabbits/agentic 0.2.0 → 0.3.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/AGENTS.md +2 -11
- package/CHANGELOG.md +11 -0
- package/MEMORY.md +113 -0
- package/Makefile +26 -2
- package/README.md +2 -2
- package/agentic +724 -64
- package/docs/agentic-lifecycle.md +3 -3
- package/docs/agentic-stabilization/README.md +33 -0
- package/docs/agentic-token-minimization/README.md +1 -1
- package/docs/agentic-usage.md +10 -14
- package/docs/opencode_setup.md +4 -2
- package/extensions/opencode/opencode.json +1 -1
- package/extensions/opencode/plugins/agent-model-mapper.ts +106 -0
- package/extensions/opencode/plugins/telegram-notification.ts +21 -11
- package/package.json +2 -1
- package/extensions/opencode/plugins/model-checker.json +0 -13
- package/extensions/opencode/plugins/model-checker.ts +0 -302
package/agentic
CHANGED
|
@@ -58,6 +58,7 @@ CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
|
|
|
58
58
|
AGENTIC_ENABLE_CONTEXT7="${AGENTIC_ENABLE_CONTEXT7:-}"
|
|
59
59
|
AGENTIC_DOCTOR="${AGENTIC_DOCTOR:-1}"
|
|
60
60
|
AGENTIC_DOCTOR_KEEP_TMP="${AGENTIC_DOCTOR_KEEP_TMP:-0}"
|
|
61
|
+
AGENTIC_DOCTOR_TIMEOUT_SECONDS="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
|
|
61
62
|
|
|
62
63
|
RUN_LOG_ACTIVE=false
|
|
63
64
|
RUN_LOG_FILE=""
|
|
@@ -972,7 +973,7 @@ write_agentic_manifest() {
|
|
|
972
973
|
: > "$skipped_file"
|
|
973
974
|
fi
|
|
974
975
|
|
|
975
|
-
local agent_os_csv areas_csv specs_csv
|
|
976
|
+
local agent_os_csv areas_csv specs_csv mcp_integrations_csv
|
|
976
977
|
local old_ifs="$IFS"
|
|
977
978
|
IFS=,
|
|
978
979
|
agent_os_csv="${SELECTED_AGENT_OS[*]}"
|
|
@@ -980,8 +981,21 @@ write_agentic_manifest() {
|
|
|
980
981
|
specs_csv="${SELECTED_SPECS[*]}"
|
|
981
982
|
IFS="$old_ifs"
|
|
982
983
|
|
|
984
|
+
# Build mcp_integrations list from current env selections
|
|
985
|
+
local mcp_integrations=()
|
|
986
|
+
if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
|
|
987
|
+
mcp_integrations+=("context7")
|
|
988
|
+
fi
|
|
989
|
+
if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
|
|
990
|
+
mcp_integrations+=("mempalace")
|
|
991
|
+
fi
|
|
992
|
+
old_ifs="$IFS"
|
|
993
|
+
IFS=,
|
|
994
|
+
mcp_integrations_csv="${mcp_integrations[*]:-}"
|
|
995
|
+
IFS="$old_ifs"
|
|
996
|
+
|
|
983
997
|
local manifest_status
|
|
984
|
-
manifest_status="$(python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" "$(app_version_label)" <<'PY'
|
|
998
|
+
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" <<'PY'
|
|
985
999
|
import json
|
|
986
1000
|
import sys
|
|
987
1001
|
from datetime import datetime, timezone
|
|
@@ -996,6 +1010,7 @@ agent_os = [x for x in sys.argv[6].split(",") if x]
|
|
|
996
1010
|
areas = [x for x in sys.argv[7].split(",") if x]
|
|
997
1011
|
specs = [x for x in sys.argv[8].split(",") if x]
|
|
998
1012
|
app_version = sys.argv[9]
|
|
1013
|
+
mcp_integrations = [x for x in sys.argv[10].split(",") if x] if len(sys.argv) > 10 else []
|
|
999
1014
|
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
1000
1015
|
|
|
1001
1016
|
existing = {}
|
|
@@ -1052,6 +1067,7 @@ data = {
|
|
|
1052
1067
|
"agent_os": agent_os,
|
|
1053
1068
|
"areas": areas,
|
|
1054
1069
|
"specializations": specs,
|
|
1070
|
+
"mcp_integrations": mcp_integrations,
|
|
1055
1071
|
"source_repo": repo_link,
|
|
1056
1072
|
"source_checkout": repo_root,
|
|
1057
1073
|
},
|
|
@@ -1094,7 +1110,7 @@ from pathlib import Path
|
|
|
1094
1110
|
|
|
1095
1111
|
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
1096
1112
|
settings = data.get("settings", {})
|
|
1097
|
-
for key in ("agent_os", "areas", "specializations"):
|
|
1113
|
+
for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
|
|
1098
1114
|
print("::" + key)
|
|
1099
1115
|
for value in settings.get(key, []):
|
|
1100
1116
|
print(value)
|
|
@@ -1105,17 +1121,20 @@ PY
|
|
|
1105
1121
|
local loaded_agent_os=()
|
|
1106
1122
|
local loaded_areas=()
|
|
1107
1123
|
local loaded_specs=()
|
|
1124
|
+
local loaded_mcp_integrations=()
|
|
1108
1125
|
local value
|
|
1109
1126
|
for value in "${values[@]}"; do
|
|
1110
1127
|
case "$value" in
|
|
1111
1128
|
"::agent_os") section="agent_os" ;;
|
|
1112
1129
|
"::areas") section="areas" ;;
|
|
1113
1130
|
"::specializations") section="specializations" ;;
|
|
1131
|
+
"::mcp_integrations") section="mcp_integrations" ;;
|
|
1114
1132
|
*)
|
|
1115
1133
|
case "$section" in
|
|
1116
1134
|
agent_os) loaded_agent_os+=("$value") ;;
|
|
1117
1135
|
areas) loaded_areas+=("$value") ;;
|
|
1118
1136
|
specializations) loaded_specs+=("$value") ;;
|
|
1137
|
+
mcp_integrations) loaded_mcp_integrations+=("$value") ;;
|
|
1119
1138
|
esac
|
|
1120
1139
|
;;
|
|
1121
1140
|
esac
|
|
@@ -1130,6 +1149,23 @@ PY
|
|
|
1130
1149
|
if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
|
|
1131
1150
|
SELECTED_SPECS=("${loaded_specs[@]}")
|
|
1132
1151
|
fi
|
|
1152
|
+
|
|
1153
|
+
# Restore MCP integration selections so configure_*_if_needed skip interactive prompts
|
|
1154
|
+
local mcp_item
|
|
1155
|
+
for mcp_item in "${loaded_mcp_integrations[@]}"; do
|
|
1156
|
+
case "$mcp_item" in
|
|
1157
|
+
context7)
|
|
1158
|
+
if [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
|
|
1159
|
+
AGENTIC_ENABLE_CONTEXT7="y"
|
|
1160
|
+
fi
|
|
1161
|
+
;;
|
|
1162
|
+
mempalace)
|
|
1163
|
+
if [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
|
|
1164
|
+
AGENTIC_ENABLE_MEMPALACE="y"
|
|
1165
|
+
fi
|
|
1166
|
+
;;
|
|
1167
|
+
esac
|
|
1168
|
+
done
|
|
1133
1169
|
}
|
|
1134
1170
|
|
|
1135
1171
|
path_ref_for_shell_export() {
|
|
@@ -1704,6 +1740,43 @@ mcp_servers["context7"] = context7
|
|
|
1704
1740
|
write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
|
|
1705
1741
|
}
|
|
1706
1742
|
|
|
1743
|
+
print_context7_key_recommendation() {
|
|
1744
|
+
[[ -z "$CONTEXT7_API_KEY" ]] || return 0
|
|
1745
|
+
|
|
1746
|
+
out "Context7 MCP configured without an API key."
|
|
1747
|
+
out "To add a Context7 API key later, set CONTEXT7_API_KEY before rerunning agentic or edit the generated config:"
|
|
1748
|
+
|
|
1749
|
+
if selected_agent_os_contains "opencode"; then
|
|
1750
|
+
out " - $PROJECT_DIR/opencode.json"
|
|
1751
|
+
out " - $PROJECT_DIR/.opencode/opencode.json"
|
|
1752
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1753
|
+
fi
|
|
1754
|
+
if selected_agent_os_contains "codex"; then
|
|
1755
|
+
out " - $PROJECT_DIR/.codex/config.toml"
|
|
1756
|
+
out ' Example: http_headers = { "CONTEXT7_API_KEY" = "ctx7_your_api_key_here" }'
|
|
1757
|
+
fi
|
|
1758
|
+
if selected_agent_os_contains "claude"; then
|
|
1759
|
+
out " - $PROJECT_DIR/.mcp.json"
|
|
1760
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1761
|
+
fi
|
|
1762
|
+
if selected_agent_os_contains "cursor"; then
|
|
1763
|
+
out " - $PROJECT_DIR/.cursor/mcp.json"
|
|
1764
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1765
|
+
fi
|
|
1766
|
+
if selected_agent_os_contains "gemini"; then
|
|
1767
|
+
out " - $PROJECT_DIR/.gemini/settings.json"
|
|
1768
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1769
|
+
fi
|
|
1770
|
+
if selected_agent_os_contains "kilocode"; then
|
|
1771
|
+
out " - $PROJECT_DIR/.kilocode/mcp.json"
|
|
1772
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1773
|
+
fi
|
|
1774
|
+
if selected_agent_os_contains "antigravity"; then
|
|
1775
|
+
out " - $HOME/.gemini/antigravity/mcp_config.json"
|
|
1776
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1777
|
+
fi
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1707
1780
|
write_mempalace_opencode_config() {
|
|
1708
1781
|
local dest="$1"
|
|
1709
1782
|
local body
|
|
@@ -1747,34 +1820,179 @@ servers["mempalace"] = {"command": "mempalace-mcp"}
|
|
|
1747
1820
|
}
|
|
1748
1821
|
|
|
1749
1822
|
print_mempalace_project_setup_instructions() {
|
|
1750
|
-
log "MemPalace
|
|
1823
|
+
log "Optional MemPalace project indexing instructions for target project: $PROJECT_DIR"
|
|
1751
1824
|
out "1) Ensure Python is installed and available in PATH."
|
|
1752
1825
|
out "2) Install MemPalace:"
|
|
1753
1826
|
out " pip install mempalace"
|
|
1754
|
-
out "3)
|
|
1827
|
+
out "3) Optionally initialize project-local MemPalace cache:"
|
|
1755
1828
|
out " mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
|
|
1756
|
-
out "4)
|
|
1829
|
+
out "4) Optionally index existing project memory:"
|
|
1757
1830
|
out " # optional if --auto-mine was skipped"
|
|
1758
1831
|
out " mempalace mine \"$PROJECT_DIR\""
|
|
1759
1832
|
out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
|
|
1760
1833
|
out "Note: Ollama at localhost:11434 is optional; MemPalace can run heuristics-only without it."
|
|
1761
1834
|
}
|
|
1762
1835
|
|
|
1763
|
-
|
|
1836
|
+
write_mempalace_ignore_file() {
|
|
1837
|
+
local dest="$PROJECT_DIR/.mempalaceignore"
|
|
1838
|
+
local content
|
|
1839
|
+
content='node_modules/
|
|
1840
|
+
.venv/
|
|
1841
|
+
venv/
|
|
1842
|
+
dist/
|
|
1843
|
+
build/
|
|
1844
|
+
target/
|
|
1845
|
+
coverage/
|
|
1846
|
+
.git/
|
|
1847
|
+
|
|
1848
|
+
*.csv
|
|
1849
|
+
*.parquet
|
|
1850
|
+
*.log
|
|
1851
|
+
*.jsonl
|
|
1852
|
+
|
|
1853
|
+
data/
|
|
1854
|
+
tmp/
|
|
1855
|
+
'
|
|
1856
|
+
|
|
1857
|
+
if [[ -e "$dest" ]]; then
|
|
1858
|
+
log "MemPalace ignore file already exists: $dest"
|
|
1859
|
+
return 0
|
|
1860
|
+
fi
|
|
1861
|
+
|
|
1862
|
+
write_text_config_file "$dest" "generated:mempalace-ignore" "$content"
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
warn_mempalace_failure_reason() {
|
|
1866
|
+
local output_file="$1"
|
|
1867
|
+
[[ -f "$output_file" ]] || return 0
|
|
1868
|
+
|
|
1869
|
+
if grep -Fq "incompatible architecture" "$output_file" && grep -Fq "numpy" "$output_file"; then
|
|
1870
|
+
warn "MemPalace failed because Python/NumPy architecture is inconsistent. Reinstall MemPalace dependencies with the same architecture as the Python running 'mempalace'."
|
|
1871
|
+
warn "Typical fix: reinstall numpy/chromadb/mempalace in the active Python environment, or use a matching arm64/x86_64 Python. See the MemPalace log above for the exact Python path."
|
|
1872
|
+
return 0
|
|
1873
|
+
fi
|
|
1874
|
+
|
|
1875
|
+
if grep -Fq "No LLM provider reachable" "$output_file"; then
|
|
1876
|
+
warn "MemPalace could not reach an LLM provider and continued heuristics-only; this is non-fatal unless a later dependency error appears."
|
|
1877
|
+
fi
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
run_mempalace_command() {
|
|
1881
|
+
local label="$1"
|
|
1882
|
+
shift
|
|
1883
|
+
local output_file
|
|
1884
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace.XXXXXX")"
|
|
1885
|
+
if "$@" >"$output_file" 2>&1; then
|
|
1886
|
+
log "$label completed"
|
|
1887
|
+
log_file_block "$label" "$output_file"
|
|
1888
|
+
rm -f "$output_file"
|
|
1889
|
+
return 0
|
|
1890
|
+
fi
|
|
1891
|
+
|
|
1892
|
+
warn "Failed: $* (log: $output_file)"
|
|
1893
|
+
log_file_block "$label" "$output_file"
|
|
1894
|
+
warn_mempalace_failure_reason "$output_file"
|
|
1895
|
+
return 1
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
mempalace_venv_dir() {
|
|
1899
|
+
printf '%s\n' "${AGENTIC_MEMPALACE_VENV:-$HOME/.venvs/mempalace}"
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
mempalace_bin_dir() {
|
|
1903
|
+
printf '%s\n' "${AGENTIC_MEMPALACE_BIN_DIR:-$HOME/.local/bin}"
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
python3_command() {
|
|
1907
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
1908
|
+
printf '%s\n' "python3"
|
|
1909
|
+
return 0
|
|
1910
|
+
fi
|
|
1911
|
+
if command -v python >/dev/null 2>&1; then
|
|
1912
|
+
printf '%s\n' "python"
|
|
1913
|
+
return 0
|
|
1914
|
+
fi
|
|
1915
|
+
return 1
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
install_mempalace_managed() {
|
|
1919
|
+
local py_bin venv_dir bin_dir venv_python venv_mempalace
|
|
1920
|
+
|
|
1921
|
+
py_bin="$(python3_command)" || return 1
|
|
1922
|
+
venv_dir="$(mempalace_venv_dir)"
|
|
1923
|
+
bin_dir="$(mempalace_bin_dir)"
|
|
1924
|
+
|
|
1925
|
+
mkdir -p "$(dirname "$venv_dir")" "$bin_dir"
|
|
1926
|
+
|
|
1927
|
+
if [[ ! -x "$venv_dir/bin/python" ]]; then
|
|
1928
|
+
"$py_bin" -m venv "$venv_dir" || return 1
|
|
1929
|
+
fi
|
|
1930
|
+
|
|
1931
|
+
venv_python="$venv_dir/bin/python"
|
|
1932
|
+
venv_mempalace="$venv_dir/bin/mempalace"
|
|
1933
|
+
|
|
1934
|
+
"$venv_python" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || return 1
|
|
1935
|
+
"$venv_python" -m pip install --upgrade --no-cache-dir mempalace >/dev/null 2>&1 || return 1
|
|
1936
|
+
|
|
1937
|
+
[[ -x "$venv_mempalace" ]] || return 1
|
|
1938
|
+
|
|
1939
|
+
ln -sf "$venv_mempalace" "$bin_dir/mempalace"
|
|
1940
|
+
|
|
1941
|
+
if [[ -x "$venv_dir/bin/mempalace-mcp" ]]; then
|
|
1942
|
+
ln -sf "$venv_dir/bin/mempalace-mcp" "$bin_dir/mempalace-mcp"
|
|
1943
|
+
fi
|
|
1944
|
+
|
|
1945
|
+
export PATH="$bin_dir:$PATH"
|
|
1946
|
+
|
|
1947
|
+
command -v mempalace >/dev/null 2>&1
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
initialize_mempalace_project() {
|
|
1951
|
+
local step_prefix="$1"
|
|
1952
|
+
log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR"
|
|
1953
|
+
if ! command -v mempalace >/dev/null 2>&1; then
|
|
1954
|
+
warn "mempalace command is unavailable after install; please run setup manually"
|
|
1955
|
+
print_mempalace_project_setup_instructions
|
|
1956
|
+
return 1
|
|
1957
|
+
fi
|
|
1958
|
+
|
|
1959
|
+
if ! run_mempalace_command "MemPalace init" mempalace init "$PROJECT_DIR" --yes --auto-mine; then
|
|
1960
|
+
print_mempalace_project_setup_instructions
|
|
1961
|
+
return 1
|
|
1962
|
+
fi
|
|
1963
|
+
log "$step_prefix [4/4] Initialization step finished"
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
setup_mempalace_for_agentic() {
|
|
1967
|
+
local initialize_project="${1:-false}"
|
|
1764
1968
|
local step_prefix="MemPalace setup"
|
|
1765
1969
|
|
|
1766
1970
|
log "$step_prefix [1/4] Checking Python availability"
|
|
1767
1971
|
if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
|
|
1768
1972
|
warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
|
|
1769
1973
|
warn "Install help: https://www.python.org/downloads/"
|
|
1974
|
+
print_mempalace_project_setup_instructions
|
|
1770
1975
|
return 1
|
|
1771
1976
|
fi
|
|
1772
1977
|
log "$step_prefix [1/4] Python check passed"
|
|
1773
1978
|
|
|
1979
|
+
if [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]] && command -v mempalace-mcp >/dev/null 2>&1; then
|
|
1980
|
+
if [[ "$initialize_project" != "true" ]] || command -v mempalace >/dev/null 2>&1; then
|
|
1981
|
+
log "$step_prefix [2/4] MemPalace binaries already available; skipping pip install"
|
|
1982
|
+
if [[ "$initialize_project" != "true" ]]; then
|
|
1983
|
+
log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
|
|
1984
|
+
return 0
|
|
1985
|
+
fi
|
|
1986
|
+
initialize_mempalace_project "$step_prefix"
|
|
1987
|
+
return $?
|
|
1988
|
+
fi
|
|
1989
|
+
fi
|
|
1990
|
+
|
|
1774
1991
|
log "$step_prefix [2/4] Checking pip availability"
|
|
1775
1992
|
local pip_bin
|
|
1776
1993
|
if ! pip_bin="$(pip_command)"; then
|
|
1777
1994
|
warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
|
|
1995
|
+
print_mempalace_project_setup_instructions
|
|
1778
1996
|
return 1
|
|
1779
1997
|
fi
|
|
1780
1998
|
log "$step_prefix [2/4] pip check passed"
|
|
@@ -1788,24 +2006,12 @@ setup_mempalace_for_agentic_opencode() {
|
|
|
1788
2006
|
return 1
|
|
1789
2007
|
fi
|
|
1790
2008
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
log "MemPalace init completed"
|
|
1795
|
-
else
|
|
1796
|
-
warn "Failed: mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
|
|
1797
|
-
fi
|
|
1798
|
-
if mempalace mine "$PROJECT_DIR" >/dev/null 2>&1; then
|
|
1799
|
-
log "MemPalace mine completed"
|
|
1800
|
-
else
|
|
1801
|
-
warn "Failed: mempalace mine \"$PROJECT_DIR\""
|
|
1802
|
-
fi
|
|
1803
|
-
log "$step_prefix [4/4] Initialization step finished"
|
|
1804
|
-
else
|
|
1805
|
-
warn "mempalace command is unavailable after install; please run setup manually"
|
|
1806
|
-
print_mempalace_project_setup_instructions
|
|
1807
|
-
return 1
|
|
2009
|
+
if [[ "$initialize_project" != "true" ]]; then
|
|
2010
|
+
log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
|
|
2011
|
+
return 0
|
|
1808
2012
|
fi
|
|
2013
|
+
|
|
2014
|
+
initialize_mempalace_project "$step_prefix"
|
|
1809
2015
|
}
|
|
1810
2016
|
|
|
1811
2017
|
configure_mempalace_if_needed() {
|
|
@@ -1832,16 +2038,18 @@ configure_mempalace_if_needed() {
|
|
|
1832
2038
|
return
|
|
1833
2039
|
fi
|
|
1834
2040
|
|
|
1835
|
-
|
|
1836
|
-
setup_mempalace_for_agentic_opencode || true
|
|
1837
|
-
else
|
|
1838
|
-
print_mempalace_project_setup_instructions
|
|
1839
|
-
fi
|
|
2041
|
+
write_mempalace_ignore_file
|
|
1840
2042
|
|
|
1841
|
-
|
|
1842
|
-
|
|
2043
|
+
local initialize_mempalace_project="true"
|
|
2044
|
+
local mempalace_setup_ok="true"
|
|
2045
|
+
setup_mempalace_for_agentic "$initialize_mempalace_project" || mempalace_setup_ok="false"
|
|
2046
|
+
|
|
2047
|
+
if [[ "$mempalace_setup_ok" != "true" ]]; then
|
|
2048
|
+
if ! command -v mempalace-mcp >/dev/null 2>&1; then
|
|
2049
|
+
warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
|
|
2050
|
+
fi
|
|
1843
2051
|
else
|
|
1844
|
-
|
|
2052
|
+
log "MemPalace MCP binary found: mempalace-mcp"
|
|
1845
2053
|
fi
|
|
1846
2054
|
|
|
1847
2055
|
if selected_agent_os_contains "opencode"; then
|
|
@@ -1883,7 +2091,6 @@ configure_context7_if_needed() {
|
|
|
1883
2091
|
fi
|
|
1884
2092
|
|
|
1885
2093
|
if is_interactive_terminal; then
|
|
1886
|
-
local answer
|
|
1887
2094
|
if [[ -z "$enable_context7" ]]; then
|
|
1888
2095
|
read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
|
|
1889
2096
|
enable_context7="$(trim "$enable_context7")"
|
|
@@ -1893,9 +2100,10 @@ configure_context7_if_needed() {
|
|
|
1893
2100
|
return
|
|
1894
2101
|
fi
|
|
1895
2102
|
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2103
|
+
elif [[ -n "$enable_context7" ]]; then
|
|
2104
|
+
if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
|
|
2105
|
+
log "Context7 MCP configuration disabled"
|
|
2106
|
+
return
|
|
1899
2107
|
fi
|
|
1900
2108
|
elif [[ -z "$CONTEXT7_API_KEY" ]]; then
|
|
1901
2109
|
log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
|
|
@@ -1930,6 +2138,8 @@ configure_context7_if_needed() {
|
|
|
1930
2138
|
if selected_agent_os_contains "antigravity"; then
|
|
1931
2139
|
write_context7_antigravity_config
|
|
1932
2140
|
fi
|
|
2141
|
+
|
|
2142
|
+
print_context7_key_recommendation
|
|
1933
2143
|
}
|
|
1934
2144
|
|
|
1935
2145
|
write_default_opencode_plugin_config() {
|
|
@@ -1944,8 +2154,8 @@ from pathlib import Path
|
|
|
1944
2154
|
|
|
1945
2155
|
path = Path(sys.argv[1])
|
|
1946
2156
|
path.write_text(json.dumps({
|
|
1947
|
-
"telegram": {"enabled": False
|
|
1948
|
-
"
|
|
2157
|
+
"telegram": {"enabled": False},
|
|
2158
|
+
"agentModelMapper": {"enabled": False},
|
|
1949
2159
|
}, indent=2) + "\n", encoding="utf-8")
|
|
1950
2160
|
PY
|
|
1951
2161
|
fi
|
|
@@ -1962,6 +2172,12 @@ configure_opencode_plugins_if_needed() {
|
|
|
1962
2172
|
ensure_python_available
|
|
1963
2173
|
ensure_dir "$APP_CONFIG_DIR"
|
|
1964
2174
|
|
|
2175
|
+
# During upgrade/re-install with existing plugin config, keep current settings
|
|
2176
|
+
if [[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" && ( -n "${AGENTIC_ENABLE_MEMPALACE:-}" || -n "${AGENTIC_ENABLE_CONTEXT7:-}" ) ]]; then
|
|
2177
|
+
log "OpenCode plugin config already exists; keeping current settings"
|
|
2178
|
+
return
|
|
2179
|
+
fi
|
|
2180
|
+
|
|
1965
2181
|
if ! is_interactive_terminal; then
|
|
1966
2182
|
if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
|
|
1967
2183
|
write_default_opencode_plugin_config
|
|
@@ -1969,7 +2185,7 @@ configure_opencode_plugins_if_needed() {
|
|
|
1969
2185
|
return
|
|
1970
2186
|
fi
|
|
1971
2187
|
|
|
1972
|
-
local plugin_options=("telegram-opencode-notifier" "
|
|
2188
|
+
local plugin_options=("telegram-opencode-notifier" "agent-model-mapper")
|
|
1973
2189
|
local selected_plugins=()
|
|
1974
2190
|
local use_fzf_plugins=false
|
|
1975
2191
|
if fzf_available; then
|
|
@@ -1986,47 +2202,361 @@ configure_opencode_plugins_if_needed() {
|
|
|
1986
2202
|
readlines selected_plugins <<< "$selected_plugins_output"
|
|
1987
2203
|
fi
|
|
1988
2204
|
|
|
1989
|
-
local enable_telegram="n"
|
|
2205
|
+
local enable_telegram="n" enable_agent_model_mapper="n"
|
|
1990
2206
|
local selected_plugin
|
|
1991
2207
|
for selected_plugin in "${selected_plugins[@]}"; do
|
|
1992
2208
|
selected_plugin="$(trim "$selected_plugin")"
|
|
1993
2209
|
[[ -z "$selected_plugin" ]] && continue
|
|
1994
2210
|
case "$selected_plugin" in
|
|
1995
2211
|
telegram-opencode-notifier) enable_telegram="y" ;;
|
|
1996
|
-
|
|
2212
|
+
agent-model-mapper) enable_agent_model_mapper="y" ;;
|
|
1997
2213
|
esac
|
|
1998
2214
|
done
|
|
1999
2215
|
|
|
2000
|
-
telegram_token=""
|
|
2001
|
-
telegram_chat=""
|
|
2002
2216
|
if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
|
|
2003
|
-
|
|
2004
|
-
read -r -p "Telegram chat id (empty disables plugin): " telegram_chat
|
|
2005
|
-
telegram_token="$(trim "$telegram_token")"
|
|
2006
|
-
telegram_chat="$(trim "$telegram_chat")"
|
|
2217
|
+
log "Telegram plugin enabled; credentials are read only from OPENCODE_TELEGRAM_BOT_TOKEN and OPENCODE_TELEGRAM_CHAT_ID"
|
|
2007
2218
|
fi
|
|
2008
2219
|
|
|
2009
|
-
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$
|
|
2220
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$enable_telegram" "$enable_agent_model_mapper" <<'PY'
|
|
2010
2221
|
import json
|
|
2011
2222
|
import sys
|
|
2012
2223
|
from pathlib import Path
|
|
2013
2224
|
|
|
2014
2225
|
path = Path(sys.argv[1])
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
enable_model = sys.argv[4].lower() == "y"
|
|
2226
|
+
enable_telegram = sys.argv[2].lower() == "y"
|
|
2227
|
+
enable_mapper = sys.argv[3].lower() == "y"
|
|
2018
2228
|
data = {
|
|
2019
2229
|
"telegram": {
|
|
2020
|
-
"enabled":
|
|
2021
|
-
"botToken": token,
|
|
2022
|
-
"chatId": chat,
|
|
2230
|
+
"enabled": enable_telegram,
|
|
2023
2231
|
},
|
|
2024
|
-
"
|
|
2025
|
-
"enabled":
|
|
2232
|
+
"agentModelMapper": {
|
|
2233
|
+
"enabled": enable_mapper,
|
|
2026
2234
|
},
|
|
2027
2235
|
}
|
|
2028
2236
|
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
2029
2237
|
PY
|
|
2238
|
+
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
opencode_agent_model_mapper_config_enabled() {
|
|
2242
|
+
[[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
|
|
2243
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
|
|
2244
|
+
import json
|
|
2245
|
+
import sys
|
|
2246
|
+
from pathlib import Path
|
|
2247
|
+
|
|
2248
|
+
try:
|
|
2249
|
+
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
2250
|
+
except Exception:
|
|
2251
|
+
raise SystemExit(1)
|
|
2252
|
+
raise SystemExit(0 if data.get("agentModelMapper", {}).get("enabled") is True else 1)
|
|
2253
|
+
PY
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
opencode_mapper_read_roles() {
|
|
2257
|
+
local agents_dir="$PROJECT_DIR/.opencode/agents"
|
|
2258
|
+
[[ -d "$agents_dir" ]] || return 0
|
|
2259
|
+
python3 - "$agents_dir" <<'PY'
|
|
2260
|
+
import sys
|
|
2261
|
+
from pathlib import Path
|
|
2262
|
+
|
|
2263
|
+
agents_dir = Path(sys.argv[1])
|
|
2264
|
+
|
|
2265
|
+
def parse_frontmatter(text):
|
|
2266
|
+
if not text.startswith("---\n"):
|
|
2267
|
+
return {}
|
|
2268
|
+
end = text.find("\n---", 4)
|
|
2269
|
+
if end == -1:
|
|
2270
|
+
return {}
|
|
2271
|
+
result = {}
|
|
2272
|
+
for line in text[4:end].splitlines():
|
|
2273
|
+
if ":" not in line:
|
|
2274
|
+
continue
|
|
2275
|
+
key, value = line.split(":", 1)
|
|
2276
|
+
result[key.strip()] = value.strip().strip("'\"")
|
|
2277
|
+
return result
|
|
2278
|
+
|
|
2279
|
+
for path in sorted(agents_dir.glob("*.md")):
|
|
2280
|
+
frontmatter = parse_frontmatter(path.read_text(encoding="utf-8"))
|
|
2281
|
+
name = path.stem.replace("\t", " ")
|
|
2282
|
+
mode = (frontmatter.get("mode") or "subagent").replace("\t", " ")
|
|
2283
|
+
description = (frontmatter.get("description") or "OpenCode agent").replace("\t", " ")
|
|
2284
|
+
print(f"{name}\t{mode}\t{description}")
|
|
2285
|
+
PY
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
opencode_mapper_discover_models() {
|
|
2289
|
+
local config_path="$HOME/.config/opencode/opencode.json"
|
|
2290
|
+
python3 - "$config_path" <<'PY'
|
|
2291
|
+
import json
|
|
2292
|
+
import sys
|
|
2293
|
+
from pathlib import Path
|
|
2294
|
+
|
|
2295
|
+
fallback = ["opencode/minimax-m2.5-free"]
|
|
2296
|
+
path = Path(sys.argv[1])
|
|
2297
|
+
models = []
|
|
2298
|
+
|
|
2299
|
+
def collect_provider_models(data):
|
|
2300
|
+
"""Extract models from provider.<name>.models dict keys."""
|
|
2301
|
+
providers = data.get("provider")
|
|
2302
|
+
if not isinstance(providers, dict):
|
|
2303
|
+
return
|
|
2304
|
+
for provider_name, provider_data in providers.items():
|
|
2305
|
+
if not isinstance(provider_data, dict):
|
|
2306
|
+
continue
|
|
2307
|
+
provider_models = provider_data.get("models")
|
|
2308
|
+
if not isinstance(provider_models, dict):
|
|
2309
|
+
continue
|
|
2310
|
+
for model_name in provider_models:
|
|
2311
|
+
if isinstance(model_name, str) and model_name.strip():
|
|
2312
|
+
models.append(f"{provider_name}/{model_name}")
|
|
2313
|
+
|
|
2314
|
+
def collect(value):
|
|
2315
|
+
if isinstance(value, list):
|
|
2316
|
+
for item in value:
|
|
2317
|
+
collect(item)
|
|
2318
|
+
return
|
|
2319
|
+
if not isinstance(value, dict):
|
|
2320
|
+
return
|
|
2321
|
+
for key, item in value.items():
|
|
2322
|
+
if key in {"model", "id"} and isinstance(item, str) and "/" in item:
|
|
2323
|
+
models.append(item)
|
|
2324
|
+
if key == "fallback" and isinstance(item, list):
|
|
2325
|
+
models.extend(model for model in item if isinstance(model, str))
|
|
2326
|
+
collect(item)
|
|
2327
|
+
|
|
2328
|
+
try:
|
|
2329
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
2330
|
+
collect_provider_models(data)
|
|
2331
|
+
collect(data)
|
|
2332
|
+
except Exception:
|
|
2333
|
+
pass
|
|
2334
|
+
|
|
2335
|
+
seen = set()
|
|
2336
|
+
for model in models or fallback:
|
|
2337
|
+
model = model.strip()
|
|
2338
|
+
if model and model not in seen:
|
|
2339
|
+
seen.add(model)
|
|
2340
|
+
print(model)
|
|
2341
|
+
PY
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
opencode_mapper_has_complete_mapping() {
|
|
2345
|
+
local roles_file="$1"
|
|
2346
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2347
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2348
|
+
python3 - "$roles_file" "$config_path" "$state_path" <<'PY'
|
|
2349
|
+
import json
|
|
2350
|
+
import sys
|
|
2351
|
+
from pathlib import Path
|
|
2352
|
+
|
|
2353
|
+
roles_file, config_path, state_path = map(Path, sys.argv[1:])
|
|
2354
|
+
try:
|
|
2355
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
2356
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
2357
|
+
except Exception:
|
|
2358
|
+
raise SystemExit(1)
|
|
2359
|
+
if not state.get("configured"):
|
|
2360
|
+
raise SystemExit(1)
|
|
2361
|
+
agents = config.get("agent")
|
|
2362
|
+
if not isinstance(agents, dict):
|
|
2363
|
+
raise SystemExit(1)
|
|
2364
|
+
roles = [line.split("\t", 1)[0] for line in roles_file.read_text(encoding="utf-8").splitlines() if line]
|
|
2365
|
+
for role in roles:
|
|
2366
|
+
agent = agents.get(role)
|
|
2367
|
+
if not isinstance(agent, dict) or not str(agent.get("model", "")).strip():
|
|
2368
|
+
raise SystemExit(1)
|
|
2369
|
+
raise SystemExit(0)
|
|
2370
|
+
PY
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
choose_opencode_mapper_model() {
|
|
2374
|
+
local role_name="$1"
|
|
2375
|
+
local role_mode="$2"
|
|
2376
|
+
local role_description="$3"
|
|
2377
|
+
local kind="$4"
|
|
2378
|
+
shift 4
|
|
2379
|
+
local models=("$@")
|
|
2380
|
+
|
|
2381
|
+
if [[ "${AGENTIC_AGENT_MODEL_MAPPER_NO_FZF:-}" != "1" ]] && fzf_available; then
|
|
2382
|
+
local selected selected_model fzf_status
|
|
2383
|
+
set +e
|
|
2384
|
+
selected="$(for i in "${!models[@]}"; do printf '%s\t%s\n' "$((i + 1))" "${models[$i]}"; done | fzf \
|
|
2385
|
+
--ansi \
|
|
2386
|
+
--border \
|
|
2387
|
+
--height=70% \
|
|
2388
|
+
--layout=reverse \
|
|
2389
|
+
--no-sort \
|
|
2390
|
+
--prompt "$role_name $kind> " \
|
|
2391
|
+
--header "Select $kind model for $role_name" \
|
|
2392
|
+
--with-nth=2..)"
|
|
2393
|
+
fzf_status=$?
|
|
2394
|
+
set -e
|
|
2395
|
+
if [[ "$fzf_status" -eq 0 && -n "$(trim "$selected")" ]]; then
|
|
2396
|
+
selected_model="${selected#* }"
|
|
2397
|
+
local model
|
|
2398
|
+
for model in "${models[@]}"; do
|
|
2399
|
+
if [[ "$model" == "$selected_model" ]]; then
|
|
2400
|
+
printf '%s\n' "$selected_model"
|
|
2401
|
+
return 0
|
|
2402
|
+
fi
|
|
2403
|
+
done
|
|
2404
|
+
fi
|
|
2405
|
+
fi
|
|
2406
|
+
|
|
2407
|
+
echo >&2
|
|
2408
|
+
echo "$role_name ($role_mode) - $role_description" >&2
|
|
2409
|
+
local i
|
|
2410
|
+
for i in "${!models[@]}"; do
|
|
2411
|
+
echo " $((i + 1))) ${models[$i]}" >&2
|
|
2412
|
+
done
|
|
2413
|
+
local answer
|
|
2414
|
+
read -r -p "Select $kind model for $role_name [1]: " answer
|
|
2415
|
+
answer="$(trim "$answer")"
|
|
2416
|
+
if [[ -z "$answer" ]]; then
|
|
2417
|
+
printf '%s\n' "${models[0]}"
|
|
2418
|
+
return 0
|
|
2419
|
+
fi
|
|
2420
|
+
if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#models[@]} )); then
|
|
2421
|
+
printf '%s\n' "${models[$((answer - 1))]}"
|
|
2422
|
+
return 0
|
|
2423
|
+
fi
|
|
2424
|
+
local model
|
|
2425
|
+
for model in "${models[@]}"; do
|
|
2426
|
+
if [[ "$model" == "$answer" ]]; then
|
|
2427
|
+
printf '%s\n' "$answer"
|
|
2428
|
+
return 0
|
|
2429
|
+
fi
|
|
2430
|
+
done
|
|
2431
|
+
warn "Unknown model '$answer', using ${models[0]}"
|
|
2432
|
+
printf '%s\n' "${models[0]}"
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
write_opencode_agent_model_mapping() {
|
|
2436
|
+
local roles_file="$1"
|
|
2437
|
+
local mapping_file="$2"
|
|
2438
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2439
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2440
|
+
|
|
2441
|
+
python3 - "$roles_file" "$mapping_file" "$config_path" "$state_path" <<'PY'
|
|
2442
|
+
import json
|
|
2443
|
+
import sys
|
|
2444
|
+
from pathlib import Path
|
|
2445
|
+
|
|
2446
|
+
roles_file, mapping_file, config_path, state_path = map(Path, sys.argv[1:])
|
|
2447
|
+
roles = []
|
|
2448
|
+
for line in roles_file.read_text(encoding="utf-8").splitlines():
|
|
2449
|
+
if not line:
|
|
2450
|
+
continue
|
|
2451
|
+
name, mode, description = (line.split("\t") + ["", "", ""])[:3]
|
|
2452
|
+
roles.append({"name": name, "mode": mode, "description": description})
|
|
2453
|
+
|
|
2454
|
+
mapping = {}
|
|
2455
|
+
for line in mapping_file.read_text(encoding="utf-8").splitlines():
|
|
2456
|
+
if not line:
|
|
2457
|
+
continue
|
|
2458
|
+
name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
|
|
2459
|
+
mapping[name] = {"model": model, "fallback": [fallback] if fallback and fallback != model else []}
|
|
2460
|
+
|
|
2461
|
+
try:
|
|
2462
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
2463
|
+
except Exception:
|
|
2464
|
+
data = {}
|
|
2465
|
+
if not isinstance(data, dict):
|
|
2466
|
+
data = {}
|
|
2467
|
+
agents = data.setdefault("agent", {})
|
|
2468
|
+
for role in roles:
|
|
2469
|
+
selected = mapping.get(role["name"])
|
|
2470
|
+
if not selected:
|
|
2471
|
+
continue
|
|
2472
|
+
current = agents.get(role["name"])
|
|
2473
|
+
if not isinstance(current, dict):
|
|
2474
|
+
current = {}
|
|
2475
|
+
current.update({
|
|
2476
|
+
"mode": current.get("mode") or role["mode"],
|
|
2477
|
+
"description": current.get("description") or role["description"],
|
|
2478
|
+
"model": selected["model"],
|
|
2479
|
+
"fallback": selected["fallback"],
|
|
2480
|
+
})
|
|
2481
|
+
agents[role["name"]] = current
|
|
2482
|
+
|
|
2483
|
+
config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2484
|
+
state_path.write_text(json.dumps({
|
|
2485
|
+
"configured": True,
|
|
2486
|
+
"roles": [role["name"] for role in roles],
|
|
2487
|
+
}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2488
|
+
PY
|
|
2489
|
+
|
|
2490
|
+
register_managed_file "$config_path" "generated:opencode-agent-model-mapper-config" "config"
|
|
2491
|
+
register_managed_file "$state_path" "generated:opencode-agent-model-mapper-state" "config"
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
configure_opencode_agent_model_mapper_if_needed() {
|
|
2495
|
+
selected_agent_os_contains "opencode" || return 0
|
|
2496
|
+
opencode_agent_model_mapper_config_enabled || return 0
|
|
2497
|
+
|
|
2498
|
+
if ! is_interactive_terminal; then
|
|
2499
|
+
log "agent-model-mapper install-time setup skipped because no interactive terminal is available"
|
|
2500
|
+
return 0
|
|
2501
|
+
fi
|
|
2502
|
+
|
|
2503
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2504
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2505
|
+
can_write_managed_file "$config_path" || return 0
|
|
2506
|
+
if [[ -e "$state_path" ]]; then
|
|
2507
|
+
can_write_managed_file "$state_path" || return 0
|
|
2508
|
+
fi
|
|
2509
|
+
|
|
2510
|
+
local roles_file models_file mapping_file
|
|
2511
|
+
roles_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-roles.XXXXXX")"
|
|
2512
|
+
models_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-models.XXXXXX")"
|
|
2513
|
+
mapping_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-mapping.XXXXXX")"
|
|
2514
|
+
opencode_mapper_read_roles > "$roles_file"
|
|
2515
|
+
|
|
2516
|
+
if [[ ! -s "$roles_file" ]]; then
|
|
2517
|
+
log "agent-model-mapper: skipped because .opencode/agents/*.md was not found"
|
|
2518
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2519
|
+
return 0
|
|
2520
|
+
fi
|
|
2521
|
+
|
|
2522
|
+
if opencode_mapper_has_complete_mapping "$roles_file"; then
|
|
2523
|
+
log "agent-model-mapper: skipped because all Agentic roles already have model mappings"
|
|
2524
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2525
|
+
return 0
|
|
2526
|
+
fi
|
|
2527
|
+
|
|
2528
|
+
opencode_mapper_discover_models > "$models_file"
|
|
2529
|
+
local models=()
|
|
2530
|
+
readlines models < "$models_file"
|
|
2531
|
+
if [[ "${#models[@]}" -eq 0 ]]; then
|
|
2532
|
+
models=("opencode/minimax-m2.5-free")
|
|
2533
|
+
fi
|
|
2534
|
+
|
|
2535
|
+
out "agent-model-mapper: choose OpenCode models for Agentic roles"
|
|
2536
|
+
local role_name role_mode role_description model fallback
|
|
2537
|
+
exec 3<&0
|
|
2538
|
+
while IFS=$'\t' read -r role_name role_mode role_description || [[ -n "${role_name:-}" ]]; do
|
|
2539
|
+
[[ -n "$role_name" ]] || continue
|
|
2540
|
+
model="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "main" "${models[@]}" <&3)"
|
|
2541
|
+
fallback="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "fallback" "${models[@]}" <&3)"
|
|
2542
|
+
printf '%s\t%s\t%s\n' "$role_name" "$model" "$fallback" >> "$mapping_file"
|
|
2543
|
+
done < "$roles_file"
|
|
2544
|
+
|
|
2545
|
+
local confirm
|
|
2546
|
+
if ! read -r -p "Write .opencode/opencode.json agent model mapping? [y/N]: " confirm <&3; then
|
|
2547
|
+
confirm=""
|
|
2548
|
+
fi
|
|
2549
|
+
exec 3<&-
|
|
2550
|
+
confirm="$(trim "$confirm")"
|
|
2551
|
+
if [[ ! "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]]; then
|
|
2552
|
+
log "agent-model-mapper: skipped by user; no files changed"
|
|
2553
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2554
|
+
return 0
|
|
2555
|
+
fi
|
|
2556
|
+
|
|
2557
|
+
write_opencode_agent_model_mapping "$roles_file" "$mapping_file"
|
|
2558
|
+
log "agent-model-mapper: updated .opencode/opencode.json"
|
|
2559
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2030
2560
|
}
|
|
2031
2561
|
|
|
2032
2562
|
normalize_selected_agent_os() {
|
|
@@ -2232,6 +2762,40 @@ generate_agents_md() {
|
|
|
2232
2762
|
rm -f "$tmp"
|
|
2233
2763
|
}
|
|
2234
2764
|
|
|
2765
|
+
copy_memory_md() {
|
|
2766
|
+
local project_dir="$1"
|
|
2767
|
+
local src="$REPO_ROOT/MEMORY.md"
|
|
2768
|
+
|
|
2769
|
+
if [[ ! -f "$src" ]]; then
|
|
2770
|
+
warn "MEMORY.md not found in knowledge base at $src; skipping"
|
|
2771
|
+
return
|
|
2772
|
+
fi
|
|
2773
|
+
|
|
2774
|
+
local outputs=()
|
|
2775
|
+
|
|
2776
|
+
if selected_agent_os_contains "opencode"; then
|
|
2777
|
+
unique_append "$project_dir/.opencode/MEMORY.md" outputs
|
|
2778
|
+
fi
|
|
2779
|
+
|
|
2780
|
+
local needs_root=false
|
|
2781
|
+
local agent_os
|
|
2782
|
+
for agent_os in "${SELECTED_AGENT_OS[@]}"; do
|
|
2783
|
+
if [[ "$agent_os" != "opencode" ]]; then
|
|
2784
|
+
needs_root=true
|
|
2785
|
+
break
|
|
2786
|
+
fi
|
|
2787
|
+
done
|
|
2788
|
+
|
|
2789
|
+
if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
|
|
2790
|
+
unique_append "$project_dir/MEMORY.md" outputs
|
|
2791
|
+
fi
|
|
2792
|
+
|
|
2793
|
+
local out
|
|
2794
|
+
for out in "${outputs[@]}"; do
|
|
2795
|
+
write_file_with_agentic_marker "$src" "$out" "generated:MEMORY.md"
|
|
2796
|
+
done
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2235
2799
|
validate_inputs() {
|
|
2236
2800
|
local available_areas
|
|
2237
2801
|
available_areas="$(list_areas || true)"
|
|
@@ -2472,11 +3036,69 @@ doctor_prompt() {
|
|
|
2472
3036
|
printf '%s\n' "/develop-feature напиши hello world python"
|
|
2473
3037
|
}
|
|
2474
3038
|
|
|
3039
|
+
doctor_prompt_for_agent() {
|
|
3040
|
+
local agent_os="$1"
|
|
3041
|
+
case "$agent_os" in
|
|
3042
|
+
opencode)
|
|
3043
|
+
printf '%s\n' "Reply with exactly: AGENTIC_DOCTOR_OK"
|
|
3044
|
+
;;
|
|
3045
|
+
*)
|
|
3046
|
+
doctor_prompt
|
|
3047
|
+
;;
|
|
3048
|
+
esac
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
doctor_smoke_label() {
|
|
3052
|
+
local agent_os="$1"
|
|
3053
|
+
case "$agent_os" in
|
|
3054
|
+
opencode)
|
|
3055
|
+
printf '%s\n' "lightweight smoke"
|
|
3056
|
+
;;
|
|
3057
|
+
*)
|
|
3058
|
+
printf '%s\n' "/develop-feature smoke"
|
|
3059
|
+
;;
|
|
3060
|
+
esac
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2475
3063
|
doctor_output_has_fatal_patterns() {
|
|
2476
3064
|
local output_file="$1"
|
|
2477
3065
|
grep -Eiq 'MCP.*(error|failed|failure|connection|connect|startup)|plugin.*(error|failed|failure)|auth.*(required|failed)|login required|permission.*(denied|required)|SyntaxError|Traceback|Invalid regular expression flags|An unexpected critical error occurred|FatalError|RuntimeError|EPERM|EACCES|panic:' "$output_file"
|
|
2478
3066
|
}
|
|
2479
3067
|
|
|
3068
|
+
doctor_timeout_seconds() {
|
|
3069
|
+
local value="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
|
|
3070
|
+
if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then
|
|
3071
|
+
value=10
|
|
3072
|
+
fi
|
|
3073
|
+
printf '%s\n' "$value"
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
run_with_doctor_timeout() {
|
|
3077
|
+
local timeout_seconds="$1"
|
|
3078
|
+
shift
|
|
3079
|
+
|
|
3080
|
+
"$@" &
|
|
3081
|
+
local child_pid=$!
|
|
3082
|
+
local elapsed=0
|
|
3083
|
+
local status=0
|
|
3084
|
+
while kill -0 "$child_pid" 2>/dev/null; do
|
|
3085
|
+
if (( elapsed >= timeout_seconds )); then
|
|
3086
|
+
pkill -TERM -P "$child_pid" 2>/dev/null || true
|
|
3087
|
+
kill "$child_pid" 2>/dev/null || true
|
|
3088
|
+
sleep 1
|
|
3089
|
+
pkill -KILL -P "$child_pid" 2>/dev/null || true
|
|
3090
|
+
kill -9 "$child_pid" 2>/dev/null || true
|
|
3091
|
+
wait "$child_pid" 2>/dev/null || true
|
|
3092
|
+
return 124
|
|
3093
|
+
fi
|
|
3094
|
+
sleep 1
|
|
3095
|
+
elapsed=$((elapsed + 1))
|
|
3096
|
+
done
|
|
3097
|
+
wait "$child_pid"
|
|
3098
|
+
status=$?
|
|
3099
|
+
return "$status"
|
|
3100
|
+
}
|
|
3101
|
+
|
|
2480
3102
|
doctor_copy_project() {
|
|
2481
3103
|
local dest="$1"
|
|
2482
3104
|
mkdir -p "$dest"
|
|
@@ -2490,14 +3112,14 @@ run_doctor_command() {
|
|
|
2490
3112
|
local work_dir="$2"
|
|
2491
3113
|
local output_file="$3"
|
|
2492
3114
|
local prompt
|
|
2493
|
-
prompt="$(
|
|
3115
|
+
prompt="$(doctor_prompt_for_agent "$agent_os")"
|
|
2494
3116
|
|
|
2495
3117
|
case "$agent_os" in
|
|
2496
3118
|
codex)
|
|
2497
|
-
codex exec --skip-git-repo-check --
|
|
3119
|
+
codex exec --skip-git-repo-check --ephemeral --sandbox workspace-write -C "$work_dir" "$prompt" </dev/null >"$output_file" 2>&1
|
|
2498
3120
|
;;
|
|
2499
3121
|
opencode)
|
|
2500
|
-
opencode run --dir "$work_dir" --dangerously-skip-permissions --format json --
|
|
3122
|
+
OPENCODE_DISABLE_AUTOUPDATE=1 opencode run --pure --dir "$work_dir" --dangerously-skip-permissions --format json --log-level ERROR "$prompt" >"$output_file" 2>&1
|
|
2501
3123
|
;;
|
|
2502
3124
|
claude)
|
|
2503
3125
|
(cd "$work_dir" && claude -p --permission-mode bypassPermissions --output-format stream-json "$prompt") >"$output_file" 2>&1
|
|
@@ -2522,29 +3144,40 @@ run_doctor_for_agent() {
|
|
|
2522
3144
|
return 1
|
|
2523
3145
|
fi
|
|
2524
3146
|
|
|
2525
|
-
local work_dir output_file status
|
|
3147
|
+
local work_dir output_file status timeout_seconds started_at elapsed smoke_label
|
|
2526
3148
|
work_dir="$doctor_root/$agent_os"
|
|
2527
3149
|
output_file="$doctor_root/$agent_os.log"
|
|
3150
|
+
timeout_seconds="$(doctor_timeout_seconds)"
|
|
3151
|
+
smoke_label="$(doctor_smoke_label "$agent_os")"
|
|
2528
3152
|
doctor_copy_project "$work_dir"
|
|
2529
3153
|
|
|
2530
3154
|
set +e
|
|
2531
|
-
|
|
3155
|
+
started_at="$(date +%s)"
|
|
3156
|
+
run_with_doctor_timeout "$timeout_seconds" run_doctor_command "$agent_os" "$work_dir" "$output_file"
|
|
2532
3157
|
status=$?
|
|
3158
|
+
elapsed=$(( $(date +%s) - started_at ))
|
|
2533
3159
|
set -e
|
|
2534
3160
|
|
|
3161
|
+
log "$agent_os doctor finished: timeout=${timeout_seconds}s exit=$status elapsed=${elapsed}s"
|
|
3162
|
+
|
|
2535
3163
|
log_file_block "doctor $agent_os" "$output_file"
|
|
2536
3164
|
|
|
3165
|
+
if [[ "$status" -eq 124 || "$status" -eq 137 ]]; then
|
|
3166
|
+
out "❌ $agent_os: $smoke_label timed out after ${timeout_seconds}s (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
3167
|
+
return 1
|
|
3168
|
+
fi
|
|
3169
|
+
|
|
2537
3170
|
if [[ "$status" -ne 0 ]]; then
|
|
2538
|
-
out "❌ $agent_os:
|
|
3171
|
+
out "❌ $agent_os: $smoke_label failed (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
2539
3172
|
return 1
|
|
2540
3173
|
fi
|
|
2541
3174
|
|
|
2542
3175
|
if doctor_output_has_fatal_patterns "$output_file"; then
|
|
2543
|
-
out "❌ $agent_os:
|
|
3176
|
+
out "❌ $agent_os: $smoke_label reported integration errors (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
2544
3177
|
return 1
|
|
2545
3178
|
fi
|
|
2546
3179
|
|
|
2547
|
-
out "✅ $agent_os:
|
|
3180
|
+
out "✅ $agent_os: $smoke_label passed (exit $status, elapsed ${elapsed}s)"
|
|
2548
3181
|
return 0
|
|
2549
3182
|
}
|
|
2550
3183
|
|
|
@@ -2572,6 +3205,7 @@ run_agentic_doctor() {
|
|
|
2572
3205
|
out
|
|
2573
3206
|
out "=== Agentic doctor ===" "$COLOR_HEADER"
|
|
2574
3207
|
out "Doctor temp root: $doctor_root"
|
|
3208
|
+
out "Doctor timeout: $(doctor_timeout_seconds)s per agent"
|
|
2575
3209
|
|
|
2576
3210
|
local failures=0
|
|
2577
3211
|
for agent_os in "${selected_doctor_agents[@]}"; do
|
|
@@ -2603,8 +3237,10 @@ run_install() {
|
|
|
2603
3237
|
ensure_dir "$PROJECT_DIR"
|
|
2604
3238
|
configure_opencode_plugins_if_needed
|
|
2605
3239
|
copy_extensions "$PROJECT_DIR"
|
|
3240
|
+
configure_opencode_agent_model_mapper_if_needed
|
|
2606
3241
|
copy_specialization_assets "$PROJECT_DIR"
|
|
2607
3242
|
generate_agents_md "$PROJECT_DIR"
|
|
3243
|
+
copy_memory_md "$PROJECT_DIR"
|
|
2608
3244
|
configure_context7_if_needed
|
|
2609
3245
|
configure_mempalace_if_needed
|
|
2610
3246
|
write_agentic_manifest "$PROJECT_DIR"
|
|
@@ -3237,6 +3873,30 @@ sync_current_project_after_upgrade() {
|
|
|
3237
3873
|
load_install_settings_from_manifest "$manifest"
|
|
3238
3874
|
ensure_repo_layout
|
|
3239
3875
|
run_install
|
|
3876
|
+
upgrade_mempalace_graph
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
upgrade_mempalace_graph() {
|
|
3880
|
+
# Only run if mempalace was enabled for this project
|
|
3881
|
+
if [[ ! "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy] ]]; then
|
|
3882
|
+
return
|
|
3883
|
+
fi
|
|
3884
|
+
|
|
3885
|
+
if ! command -v mempalace >/dev/null 2>&1; then
|
|
3886
|
+
return
|
|
3887
|
+
fi
|
|
3888
|
+
|
|
3889
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
3890
|
+
log "DRY-RUN mempalace mine \"$PROJECT_DIR\""
|
|
3891
|
+
return
|
|
3892
|
+
fi
|
|
3893
|
+
|
|
3894
|
+
log "Refreshing MemPalace knowledge graph for $PROJECT_DIR"
|
|
3895
|
+
if mempalace mine "$PROJECT_DIR" >/dev/null 2>&1; then
|
|
3896
|
+
log "MemPalace graph updated"
|
|
3897
|
+
else
|
|
3898
|
+
warn "mempalace mine failed; graph may be stale — run manually: mempalace mine \"$PROJECT_DIR\""
|
|
3899
|
+
fi
|
|
3240
3900
|
}
|
|
3241
3901
|
|
|
3242
3902
|
parse_theme_option() {
|