@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/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 setup instructions for target project: $PROJECT_DIR"
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) Initialize project-local MemPalace cache:"
1827
+ out "3) Optionally initialize project-local MemPalace cache:"
1755
1828
  out " mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
1756
- out "4) Index existing project memory:"
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
- setup_mempalace_for_agentic_opencode() {
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
- log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR"
1792
- if command -v mempalace >/dev/null 2>&1; then
1793
- if mempalace init "$PROJECT_DIR" --yes --auto-mine >/dev/null 2>&1; then
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
- if selected_agent_os_contains "opencode"; then
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
- if command -v mempalace-mcp >/dev/null 2>&1; then
1842
- log "MemPalace MCP binary found: mempalace-mcp"
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
- warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
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
- if [[ -z "$CONTEXT7_API_KEY" ]]; then
1897
- read -r -p "Context7 API key (optional, empty = no key): " answer
1898
- CONTEXT7_API_KEY="$(trim "$answer")"
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, "botToken": "", "chatId": ""},
1948
- "modelChecker": {"enabled": False},
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" "llm-quota-checker")
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" telegram_token telegram_chat enable_model_checker="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
- llm-quota-checker) enable_model_checker="y" ;;
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
- read -r -p "Telegram bot token (empty disables plugin): " telegram_token
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" "$telegram_token" "$telegram_chat" "$enable_model_checker" <<'PY'
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
- token = sys.argv[2]
2016
- chat = sys.argv[3]
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": bool(token and chat),
2021
- "botToken": token,
2022
- "chatId": chat,
2230
+ "enabled": enable_telegram,
2023
2231
  },
2024
- "modelChecker": {
2025
- "enabled": enable_model,
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="$(doctor_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 --full-auto -C "$work_dir" "$prompt" >"$output_file" 2>&1
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 --command develop-feature "напиши hello world python" >"$output_file" 2>&1
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
- run_doctor_command "$agent_os" "$work_dir" "$output_file"
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: /develop-feature smoke failed (exit $status, log: $output_file)"
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: /develop-feature smoke reported integration errors (log: $output_file)"
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: /develop-feature smoke passed"
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() {