@jetrabbits/agentic 0.2.0 → 0.3.1
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 +13 -15
- package/CHANGELOG.md +24 -0
- package/MEMORY.md +67 -0
- package/Makefile +96 -14
- package/README.md +2 -2
- package/agentic +1269 -99
- package/areas/devops/ci-cd/AGENTS.md +1 -15
- package/areas/devops/database-ops/AGENTS.md +1 -15
- package/areas/devops/devsecops/AGENTS.md +1 -15
- package/areas/devops/infrastructure/AGENTS.md +1 -15
- package/areas/devops/kubernetes/AGENTS.md +1 -15
- package/areas/devops/networking/AGENTS.md +1 -15
- package/areas/devops/observability/AGENTS.md +1 -15
- package/areas/devops/sre/AGENTS.md +1 -15
- package/areas/software/backend/AGENTS.md +1 -16
- package/areas/software/data-engineering/AGENTS.md +1 -16
- package/areas/software/frontend/AGENTS.md +1 -16
- package/areas/software/full-stack/AGENTS.md +1 -16
- package/areas/software/general/AGENTS.md +1 -7
- package/areas/software/mlops/AGENTS.md +1 -16
- package/areas/software/mobile/AGENTS.md +1 -16
- package/areas/software/platform/AGENTS.md +1 -16
- package/areas/software/qa/AGENTS.md +1 -16
- package/areas/software/security/AGENTS.md +1 -16
- package/areas/template/AGENTS.tmpl.md +1 -17
- package/docs/agentic-lifecycle.md +8 -4
- package/docs/agentic-stabilization/README.md +37 -0
- package/docs/agentic-token-minimization/README.md +7 -5
- package/docs/agentic-usage.md +17 -14
- package/docs/opencode_setup.md +8 -4
- package/extensions/opencode/opencode.json +1 -1
- package/extensions/opencode/plugins/agent-model-mapper.ts +117 -0
- package/extensions/opencode/plugins/telegram-notification.ts +30 -20
- 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
|
@@ -42,6 +42,7 @@ ACTIVE_THEME="dark"
|
|
|
42
42
|
SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
|
|
43
43
|
SELECTED_AREAS=()
|
|
44
44
|
SELECTED_SPECS=()
|
|
45
|
+
INSTALL_SETTINGS_REPLAY=false
|
|
45
46
|
|
|
46
47
|
SELF_INSTALL_FORCE=false
|
|
47
48
|
SELF_INSTALL_BIN_DIR="${HOME}/.local/bin"
|
|
@@ -58,9 +59,18 @@ CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
|
|
|
58
59
|
AGENTIC_ENABLE_CONTEXT7="${AGENTIC_ENABLE_CONTEXT7:-}"
|
|
59
60
|
AGENTIC_DOCTOR="${AGENTIC_DOCTOR:-1}"
|
|
60
61
|
AGENTIC_DOCTOR_KEEP_TMP="${AGENTIC_DOCTOR_KEEP_TMP:-0}"
|
|
62
|
+
AGENTIC_DOCTOR_TIMEOUT_SECONDS="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
|
|
63
|
+
AGENTIC_MEMPALACE_TIMEOUT_SECONDS="${AGENTIC_MEMPALACE_TIMEOUT_SECONDS:-60}"
|
|
64
|
+
|
|
65
|
+
OPENCODE_TELEGRAM_ENABLED=""
|
|
66
|
+
OPENCODE_TELEGRAM_BOT_TOKEN=""
|
|
67
|
+
OPENCODE_TELEGRAM_CHAT_ID=""
|
|
68
|
+
OPENCODE_AGENT_MODEL_MAPPER_ENABLED=""
|
|
69
|
+
OPENCODE_PLUGINS_CONFIGURED=false
|
|
61
70
|
|
|
62
71
|
RUN_LOG_ACTIVE=false
|
|
63
72
|
RUN_LOG_FILE=""
|
|
73
|
+
CHANGED_PATHS_REPORT_FILE=""
|
|
64
74
|
|
|
65
75
|
COLOR_RESET=""
|
|
66
76
|
COLOR_HEADER=""
|
|
@@ -276,6 +286,7 @@ init_run_logging() {
|
|
|
276
286
|
local stamp
|
|
277
287
|
stamp="$(date '+%Y%m%d-%H%M%S')"
|
|
278
288
|
RUN_LOG_FILE="$(mktemp "$base_dir/agentic-$stamp.XXXXXX")"
|
|
289
|
+
CHANGED_PATHS_REPORT_FILE="$RUN_LOG_FILE.changes"
|
|
279
290
|
RUN_LOG_ACTIVE=true
|
|
280
291
|
log "Run log initialized: $RUN_LOG_FILE"
|
|
281
292
|
}
|
|
@@ -363,6 +374,55 @@ log_file_block() {
|
|
|
363
374
|
write_run_log_line "$(timestamp_now) --- $label output end ---"
|
|
364
375
|
}
|
|
365
376
|
|
|
377
|
+
write_changed_paths_report() {
|
|
378
|
+
if [[ -z "$CHANGED_PATHS_REPORT_FILE" ]]; then
|
|
379
|
+
local base_dir="${TMPDIR:-/tmp}"
|
|
380
|
+
local stamp
|
|
381
|
+
stamp="$(date '+%Y%m%d-%H%M%S')"
|
|
382
|
+
CHANGED_PATHS_REPORT_FILE="$(mktemp "$base_dir/agentic-changed-paths-$stamp.XXXXXX")"
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
{
|
|
386
|
+
printf 'Agentic changed paths report\n'
|
|
387
|
+
printf 'Generated at: %s\n' "$(timestamp_now)"
|
|
388
|
+
printf 'Project dir: %s\n' "$PROJECT_DIR"
|
|
389
|
+
printf 'Knowledge base repo: %s\n' "$REPO_ROOT"
|
|
390
|
+
printf '\n'
|
|
391
|
+
|
|
392
|
+
printf 'Created directories (%s)\n' "${#CREATED_PATHS[@]}"
|
|
393
|
+
if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
|
|
394
|
+
printf -- '- (none)\n'
|
|
395
|
+
else
|
|
396
|
+
local created_path
|
|
397
|
+
for created_path in "${CREATED_PATHS[@]}"; do
|
|
398
|
+
printf -- '- %s\n' "$created_path"
|
|
399
|
+
done
|
|
400
|
+
fi
|
|
401
|
+
printf '\n'
|
|
402
|
+
|
|
403
|
+
printf 'Copied/generated paths (%s)\n' "${#COPIED_PATHS[@]}"
|
|
404
|
+
if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
|
|
405
|
+
printf -- '- (none)\n'
|
|
406
|
+
else
|
|
407
|
+
local copied_path
|
|
408
|
+
for copied_path in "${COPIED_PATHS[@]}"; do
|
|
409
|
+
printf -- '- %s\n' "$copied_path"
|
|
410
|
+
done
|
|
411
|
+
fi
|
|
412
|
+
printf '\n'
|
|
413
|
+
|
|
414
|
+
printf 'Warnings (%s)\n' "${#WARNINGS[@]}"
|
|
415
|
+
if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
|
|
416
|
+
printf -- '- (none)\n'
|
|
417
|
+
else
|
|
418
|
+
local warning
|
|
419
|
+
for warning in "${WARNINGS[@]}"; do
|
|
420
|
+
printf -- '- %s\n' "$warning"
|
|
421
|
+
done
|
|
422
|
+
fi
|
|
423
|
+
} > "$CHANGED_PATHS_REPORT_FILE"
|
|
424
|
+
}
|
|
425
|
+
|
|
366
426
|
unique_append() {
|
|
367
427
|
local value="$1"
|
|
368
428
|
local arr_name="$2"
|
|
@@ -972,7 +1032,7 @@ write_agentic_manifest() {
|
|
|
972
1032
|
: > "$skipped_file"
|
|
973
1033
|
fi
|
|
974
1034
|
|
|
975
|
-
local agent_os_csv areas_csv specs_csv
|
|
1035
|
+
local agent_os_csv areas_csv specs_csv mcp_integrations_csv
|
|
976
1036
|
local old_ifs="$IFS"
|
|
977
1037
|
IFS=,
|
|
978
1038
|
agent_os_csv="${SELECTED_AGENT_OS[*]}"
|
|
@@ -980,8 +1040,21 @@ write_agentic_manifest() {
|
|
|
980
1040
|
specs_csv="${SELECTED_SPECS[*]}"
|
|
981
1041
|
IFS="$old_ifs"
|
|
982
1042
|
|
|
1043
|
+
# Build mcp_integrations list from current env selections
|
|
1044
|
+
local mcp_integrations=()
|
|
1045
|
+
if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
|
|
1046
|
+
mcp_integrations+=("context7")
|
|
1047
|
+
fi
|
|
1048
|
+
if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
|
|
1049
|
+
mcp_integrations+=("mempalace")
|
|
1050
|
+
fi
|
|
1051
|
+
old_ifs="$IFS"
|
|
1052
|
+
IFS=,
|
|
1053
|
+
mcp_integrations_csv="${mcp_integrations[*]:-}"
|
|
1054
|
+
IFS="$old_ifs"
|
|
1055
|
+
|
|
983
1056
|
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'
|
|
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'
|
|
985
1058
|
import json
|
|
986
1059
|
import sys
|
|
987
1060
|
from datetime import datetime, timezone
|
|
@@ -996,6 +1069,11 @@ agent_os = [x for x in sys.argv[6].split(",") if x]
|
|
|
996
1069
|
areas = [x for x in sys.argv[7].split(",") if x]
|
|
997
1070
|
specs = [x for x in sys.argv[8].split(",") if x]
|
|
998
1071
|
app_version = sys.argv[9]
|
|
1072
|
+
mcp_integrations = [x for x in sys.argv[10].split(",") if x] if len(sys.argv) > 10 else []
|
|
1073
|
+
telegram_enabled = sys.argv[11].lower() == "true" if len(sys.argv) > 11 and sys.argv[11] else None
|
|
1074
|
+
telegram_bot_token = sys.argv[12] if len(sys.argv) > 12 else ""
|
|
1075
|
+
telegram_chat_id = sys.argv[13] if len(sys.argv) > 13 else ""
|
|
1076
|
+
mapper_enabled = sys.argv[14].lower() == "true" if len(sys.argv) > 14 and sys.argv[14] else None
|
|
999
1077
|
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
1000
1078
|
|
|
1001
1079
|
existing = {}
|
|
@@ -1037,7 +1115,26 @@ for line in records_file.read_text(encoding="utf-8").splitlines():
|
|
|
1037
1115
|
|
|
1038
1116
|
skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
|
|
1039
1117
|
old_agentic = old_data.get("_agentic", {}) if isinstance(old_data, dict) else {}
|
|
1118
|
+
old_settings = old_data.get("settings", {}) if isinstance(old_data, dict) else {}
|
|
1040
1119
|
created_by = old_agentic.get("created_by", app_version)
|
|
1120
|
+
opencode_plugins = old_settings.get("opencode_plugins", {}) if isinstance(old_settings, dict) else {}
|
|
1121
|
+
if not isinstance(opencode_plugins, dict):
|
|
1122
|
+
opencode_plugins = {}
|
|
1123
|
+
telegram = opencode_plugins.get("telegram", {}) if isinstance(opencode_plugins.get("telegram"), dict) else {}
|
|
1124
|
+
agent_model_mapper = opencode_plugins.get("agentModelMapper", {}) if isinstance(opencode_plugins.get("agentModelMapper"), dict) else {}
|
|
1125
|
+
if telegram_enabled is not None:
|
|
1126
|
+
telegram = {
|
|
1127
|
+
"enabled": telegram_enabled,
|
|
1128
|
+
"botToken": telegram_bot_token,
|
|
1129
|
+
"chatId": telegram_chat_id,
|
|
1130
|
+
}
|
|
1131
|
+
if mapper_enabled is not None:
|
|
1132
|
+
agent_model_mapper = {"enabled": mapper_enabled}
|
|
1133
|
+
if telegram or agent_model_mapper:
|
|
1134
|
+
opencode_plugins = {
|
|
1135
|
+
"telegram": telegram or {"enabled": False},
|
|
1136
|
+
"agentModelMapper": agent_model_mapper or {"enabled": False},
|
|
1137
|
+
}
|
|
1041
1138
|
data = {
|
|
1042
1139
|
"_agentic": {
|
|
1043
1140
|
"generated_by": "agentic",
|
|
@@ -1052,6 +1149,8 @@ data = {
|
|
|
1052
1149
|
"agent_os": agent_os,
|
|
1053
1150
|
"areas": areas,
|
|
1054
1151
|
"specializations": specs,
|
|
1152
|
+
"mcp_integrations": mcp_integrations,
|
|
1153
|
+
"opencode_plugins": opencode_plugins,
|
|
1055
1154
|
"source_repo": repo_link,
|
|
1056
1155
|
"source_checkout": repo_root,
|
|
1057
1156
|
},
|
|
@@ -1085,6 +1184,7 @@ load_install_settings_from_manifest() {
|
|
|
1085
1184
|
local manifest="$1"
|
|
1086
1185
|
[[ -f "$manifest" ]] || return 1
|
|
1087
1186
|
ensure_python_available
|
|
1187
|
+
INSTALL_SETTINGS_REPLAY=true
|
|
1088
1188
|
|
|
1089
1189
|
local values=()
|
|
1090
1190
|
readlines values < <(python3 - "$manifest" <<'PY'
|
|
@@ -1094,10 +1194,28 @@ from pathlib import Path
|
|
|
1094
1194
|
|
|
1095
1195
|
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
1096
1196
|
settings = data.get("settings", {})
|
|
1097
|
-
for key in ("agent_os", "areas", "specializations"):
|
|
1197
|
+
for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
|
|
1098
1198
|
print("::" + key)
|
|
1099
1199
|
for value in settings.get(key, []):
|
|
1100
1200
|
print(value)
|
|
1201
|
+
plugins = settings.get("opencode_plugins", {})
|
|
1202
|
+
if isinstance(plugins, dict):
|
|
1203
|
+
telegram = plugins.get("telegram", {})
|
|
1204
|
+
if isinstance(telegram, dict):
|
|
1205
|
+
print("::opencode_telegram_enabled")
|
|
1206
|
+
if telegram.get("enabled") is not None:
|
|
1207
|
+
print("true" if telegram.get("enabled") is True else "false")
|
|
1208
|
+
print("::opencode_telegram_bot_token")
|
|
1209
|
+
if telegram.get("botToken"):
|
|
1210
|
+
print(telegram.get("botToken"))
|
|
1211
|
+
print("::opencode_telegram_chat_id")
|
|
1212
|
+
if telegram.get("chatId"):
|
|
1213
|
+
print(telegram.get("chatId"))
|
|
1214
|
+
mapper = plugins.get("agentModelMapper", {})
|
|
1215
|
+
if isinstance(mapper, dict):
|
|
1216
|
+
print("::opencode_agent_model_mapper_enabled")
|
|
1217
|
+
if mapper.get("enabled") is not None:
|
|
1218
|
+
print("true" if mapper.get("enabled") is True else "false")
|
|
1101
1219
|
PY
|
|
1102
1220
|
)
|
|
1103
1221
|
|
|
@@ -1105,17 +1223,28 @@ PY
|
|
|
1105
1223
|
local loaded_agent_os=()
|
|
1106
1224
|
local loaded_areas=()
|
|
1107
1225
|
local loaded_specs=()
|
|
1226
|
+
local loaded_mcp_integrations=()
|
|
1227
|
+
local loaded_telegram_enabled=""
|
|
1228
|
+
local loaded_telegram_bot_token=""
|
|
1229
|
+
local loaded_telegram_chat_id=""
|
|
1230
|
+
local loaded_mapper_enabled=""
|
|
1108
1231
|
local value
|
|
1109
1232
|
for value in "${values[@]}"; do
|
|
1110
1233
|
case "$value" in
|
|
1111
1234
|
"::agent_os") section="agent_os" ;;
|
|
1112
1235
|
"::areas") section="areas" ;;
|
|
1113
1236
|
"::specializations") section="specializations" ;;
|
|
1237
|
+
"::mcp_integrations") section="mcp_integrations" ;;
|
|
1114
1238
|
*)
|
|
1115
1239
|
case "$section" in
|
|
1116
1240
|
agent_os) loaded_agent_os+=("$value") ;;
|
|
1117
1241
|
areas) loaded_areas+=("$value") ;;
|
|
1118
1242
|
specializations) loaded_specs+=("$value") ;;
|
|
1243
|
+
mcp_integrations) loaded_mcp_integrations+=("$value") ;;
|
|
1244
|
+
opencode_telegram_enabled) loaded_telegram_enabled="$value" ;;
|
|
1245
|
+
opencode_telegram_bot_token) loaded_telegram_bot_token="$value" ;;
|
|
1246
|
+
opencode_telegram_chat_id) loaded_telegram_chat_id="$value" ;;
|
|
1247
|
+
opencode_agent_model_mapper_enabled) loaded_mapper_enabled="$value" ;;
|
|
1119
1248
|
esac
|
|
1120
1249
|
;;
|
|
1121
1250
|
esac
|
|
@@ -1130,6 +1259,36 @@ PY
|
|
|
1130
1259
|
if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
|
|
1131
1260
|
SELECTED_SPECS=("${loaded_specs[@]}")
|
|
1132
1261
|
fi
|
|
1262
|
+
|
|
1263
|
+
# Restore MCP integration selections so configure_*_if_needed skip interactive prompts
|
|
1264
|
+
local mcp_item
|
|
1265
|
+
if [[ "${#loaded_mcp_integrations[@]}" -gt 0 ]]; then
|
|
1266
|
+
for mcp_item in "${loaded_mcp_integrations[@]}"; do
|
|
1267
|
+
case "$mcp_item" in
|
|
1268
|
+
context7)
|
|
1269
|
+
if [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
|
|
1270
|
+
AGENTIC_ENABLE_CONTEXT7="y"
|
|
1271
|
+
fi
|
|
1272
|
+
;;
|
|
1273
|
+
mempalace)
|
|
1274
|
+
if [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
|
|
1275
|
+
AGENTIC_ENABLE_MEMPALACE="y"
|
|
1276
|
+
fi
|
|
1277
|
+
;;
|
|
1278
|
+
esac
|
|
1279
|
+
done
|
|
1280
|
+
fi
|
|
1281
|
+
|
|
1282
|
+
if [[ -n "$loaded_telegram_enabled" ]]; then
|
|
1283
|
+
OPENCODE_TELEGRAM_ENABLED="$loaded_telegram_enabled"
|
|
1284
|
+
OPENCODE_TELEGRAM_BOT_TOKEN="$loaded_telegram_bot_token"
|
|
1285
|
+
OPENCODE_TELEGRAM_CHAT_ID="$loaded_telegram_chat_id"
|
|
1286
|
+
OPENCODE_PLUGINS_CONFIGURED=true
|
|
1287
|
+
fi
|
|
1288
|
+
if [[ -n "$loaded_mapper_enabled" ]]; then
|
|
1289
|
+
OPENCODE_AGENT_MODEL_MAPPER_ENABLED="$loaded_mapper_enabled"
|
|
1290
|
+
OPENCODE_PLUGINS_CONFIGURED=true
|
|
1291
|
+
fi
|
|
1133
1292
|
}
|
|
1134
1293
|
|
|
1135
1294
|
path_ref_for_shell_export() {
|
|
@@ -1704,6 +1863,39 @@ mcp_servers["context7"] = context7
|
|
|
1704
1863
|
write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
|
|
1705
1864
|
}
|
|
1706
1865
|
|
|
1866
|
+
print_context7_key_recommendation() {
|
|
1867
|
+
[[ -z "$CONTEXT7_API_KEY" ]] || return 0
|
|
1868
|
+
|
|
1869
|
+
out "Context7 MCP configured without an API key."
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
configure_context7_key_interactive() {
|
|
1873
|
+
is_interactive_terminal || return 0
|
|
1874
|
+
|
|
1875
|
+
local choice
|
|
1876
|
+
if fzf_available; then
|
|
1877
|
+
choice="$(choose_single_fzf "Context7 API key mode:" "Use without API key" "Enter CONTEXT7_API_KEY" || true)"
|
|
1878
|
+
else
|
|
1879
|
+
echo "Context7 API key mode:" >&2
|
|
1880
|
+
echo " 1) Use without API key" >&2
|
|
1881
|
+
echo " 2) Enter CONTEXT7_API_KEY" >&2
|
|
1882
|
+
local answer
|
|
1883
|
+
read -r -p "Select one (empty=1): " answer
|
|
1884
|
+
answer="$(trim "$answer")"
|
|
1885
|
+
case "$answer" in
|
|
1886
|
+
""|1) choice="Use without API key" ;;
|
|
1887
|
+
2) choice="Enter CONTEXT7_API_KEY" ;;
|
|
1888
|
+
*) error "Invalid choice"; exit 1 ;;
|
|
1889
|
+
esac
|
|
1890
|
+
fi
|
|
1891
|
+
|
|
1892
|
+
if [[ "$choice" == "Enter CONTEXT7_API_KEY" ]]; then
|
|
1893
|
+
CONTEXT7_API_KEY="$(prompt_text_interactive "CONTEXT7_API_KEY" "$CONTEXT7_API_KEY")"
|
|
1894
|
+
else
|
|
1895
|
+
CONTEXT7_API_KEY=""
|
|
1896
|
+
fi
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1707
1899
|
write_mempalace_opencode_config() {
|
|
1708
1900
|
local dest="$1"
|
|
1709
1901
|
local body
|
|
@@ -1747,65 +1939,380 @@ servers["mempalace"] = {"command": "mempalace-mcp"}
|
|
|
1747
1939
|
}
|
|
1748
1940
|
|
|
1749
1941
|
print_mempalace_project_setup_instructions() {
|
|
1750
|
-
|
|
1942
|
+
local project_wing
|
|
1943
|
+
project_wing="$(mempalace_project_wing)"
|
|
1944
|
+
log "Optional MemPalace project indexing instructions for target project: $PROJECT_DIR"
|
|
1751
1945
|
out "1) Ensure Python is installed and available in PATH."
|
|
1752
1946
|
out "2) Install MemPalace:"
|
|
1753
1947
|
out " pip install mempalace"
|
|
1754
|
-
out "3) Initialize project
|
|
1755
|
-
out " mempalace init \"$PROJECT_DIR\" --yes --
|
|
1756
|
-
out "4)
|
|
1757
|
-
out "
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1948
|
+
out "3) Initialize the project memory taxonomy without LLM calls:"
|
|
1949
|
+
out " mempalace init \"$PROJECT_DIR\" --yes --no-llm"
|
|
1950
|
+
out "4) Mine project knowledge into its isolated wing:"
|
|
1951
|
+
out " mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
|
|
1952
|
+
if [[ -d "$PROJECT_DIR/docs" ]]; then
|
|
1953
|
+
out "5) Mine shared project docs into the cross-project docs wing:"
|
|
1954
|
+
out " mempalace mine \"$PROJECT_DIR/docs\" --wing shared_docs"
|
|
1955
|
+
out "6) Verify in your IDE/agent that MemPalace MCP tools are connected."
|
|
1956
|
+
else
|
|
1957
|
+
out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
|
|
1958
|
+
fi
|
|
1959
|
+
out "Note: agentic uses --no-llm by default to keep MemPalace setup low-cost."
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
mempalace_sanitize_wing_name() {
|
|
1963
|
+
local raw="$1"
|
|
1964
|
+
local sanitized
|
|
1965
|
+
sanitized="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/_/g; s/^_+//; s/_+$//; s/_+/_/g')"
|
|
1966
|
+
if [[ -z "$sanitized" ]]; then
|
|
1967
|
+
sanitized="project"
|
|
1968
|
+
fi
|
|
1969
|
+
printf '%s\n' "$sanitized"
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
mempalace_project_wing() {
|
|
1973
|
+
mempalace_sanitize_wing_name "$(basename "$PROJECT_DIR")"
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
mempalace_shared_docs_wing() {
|
|
1977
|
+
printf '%s\n' "shared_docs"
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
write_mempalace_ignore_file() {
|
|
1981
|
+
local dest="$PROJECT_DIR/.mempalaceignore"
|
|
1982
|
+
local content
|
|
1983
|
+
content='node_modules/
|
|
1984
|
+
.venv/
|
|
1985
|
+
venv/
|
|
1986
|
+
dist/
|
|
1987
|
+
logs/
|
|
1988
|
+
build/
|
|
1989
|
+
target/
|
|
1990
|
+
coverage/
|
|
1991
|
+
.ai/
|
|
1992
|
+
.git/
|
|
1993
|
+
.github/
|
|
1994
|
+
.cursor/
|
|
1995
|
+
.agent/
|
|
1996
|
+
.opencode/
|
|
1997
|
+
.claude/
|
|
1998
|
+
.gemini/
|
|
1999
|
+
.codex/
|
|
2000
|
+
.idea/
|
|
2001
|
+
*.csv
|
|
2002
|
+
*.parquet
|
|
2003
|
+
*.log
|
|
2004
|
+
*.jsonl
|
|
2005
|
+
|
|
2006
|
+
data/
|
|
2007
|
+
dumps/
|
|
2008
|
+
tmp/
|
|
2009
|
+
'
|
|
2010
|
+
|
|
2011
|
+
if [[ -e "$dest" ]]; then
|
|
2012
|
+
log "MemPalace ignore file already exists: $dest"
|
|
2013
|
+
return 0
|
|
2014
|
+
fi
|
|
2015
|
+
|
|
2016
|
+
write_text_config_file "$dest" "generated:mempalace-ignore" "$content"
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
warn_mempalace_failure_reason() {
|
|
2020
|
+
local output_file="$1"
|
|
2021
|
+
[[ -f "$output_file" ]] || return 0
|
|
2022
|
+
|
|
2023
|
+
if grep -Fq "incompatible architecture" "$output_file" && grep -Fq "numpy" "$output_file"; then
|
|
2024
|
+
warn "MemPalace failed because Python/NumPy architecture is inconsistent. Reinstall MemPalace dependencies with the same architecture as the Python running 'mempalace'."
|
|
2025
|
+
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."
|
|
2026
|
+
return 0
|
|
2027
|
+
fi
|
|
2028
|
+
|
|
2029
|
+
if grep -Fq "No LLM provider reachable" "$output_file"; then
|
|
2030
|
+
warn "MemPalace could not reach an LLM provider and continued heuristics-only; this is non-fatal unless a later dependency error appears."
|
|
2031
|
+
fi
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
warn_mempalace_pip_failure_reason() {
|
|
2035
|
+
local output_file="$1"
|
|
2036
|
+
[[ -f "$output_file" ]] || return 0
|
|
2037
|
+
|
|
2038
|
+
local reason
|
|
2039
|
+
reason="$(sed -n '/[^[:space:]]/{p;q;}' "$output_file" 2>/dev/null || true)"
|
|
2040
|
+
if [[ -n "$reason" ]]; then
|
|
2041
|
+
warn "pip failure reason: $reason"
|
|
2042
|
+
else
|
|
2043
|
+
warn "pip failure output was empty; inspect the MemPalace pip install log for details."
|
|
2044
|
+
fi
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
mempalace_timeout_seconds() {
|
|
2048
|
+
local value="${AGENTIC_MEMPALACE_TIMEOUT_SECONDS:-60}"
|
|
2049
|
+
if [[ "$value" =~ ^[0-9]+$ ]] && (( value > 0 )); then
|
|
2050
|
+
printf '%s\n' "$value"
|
|
2051
|
+
return
|
|
2052
|
+
fi
|
|
2053
|
+
printf '%s\n' "60"
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
run_mempalace_command() {
|
|
2057
|
+
local label="$1"
|
|
2058
|
+
shift
|
|
2059
|
+
local output_file timeout_seconds child_pid elapsed status
|
|
2060
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace.XXXXXX")"
|
|
2061
|
+
timeout_seconds="$(mempalace_timeout_seconds)"
|
|
2062
|
+
|
|
2063
|
+
"$@" >"$output_file" 2>&1 &
|
|
2064
|
+
child_pid=$!
|
|
2065
|
+
elapsed=0
|
|
2066
|
+
status=0
|
|
2067
|
+
while kill -0 "$child_pid" 2>/dev/null; do
|
|
2068
|
+
if (( elapsed >= timeout_seconds )); then
|
|
2069
|
+
pkill -TERM -P "$child_pid" 2>/dev/null || true
|
|
2070
|
+
kill "$child_pid" 2>/dev/null || true
|
|
2071
|
+
sleep 1
|
|
2072
|
+
pkill -KILL -P "$child_pid" 2>/dev/null || true
|
|
2073
|
+
kill -9 "$child_pid" 2>/dev/null || true
|
|
2074
|
+
wait "$child_pid" 2>/dev/null || true
|
|
2075
|
+
warn "Timed out after ${timeout_seconds}s: $* (log: $output_file)"
|
|
2076
|
+
log_file_block "$label" "$output_file"
|
|
2077
|
+
return 1
|
|
2078
|
+
fi
|
|
2079
|
+
sleep 1
|
|
2080
|
+
elapsed=$((elapsed + 1))
|
|
2081
|
+
done
|
|
2082
|
+
|
|
2083
|
+
if wait "$child_pid"; then
|
|
2084
|
+
log "$label completed"
|
|
2085
|
+
log_file_block "$label" "$output_file"
|
|
2086
|
+
rm -f "$output_file"
|
|
2087
|
+
return 0
|
|
2088
|
+
fi
|
|
2089
|
+
status=$?
|
|
2090
|
+
|
|
2091
|
+
warn "Failed: $* (exit $status, log: $output_file)"
|
|
2092
|
+
log_file_block "$label" "$output_file"
|
|
2093
|
+
warn_mempalace_failure_reason "$output_file"
|
|
2094
|
+
return 1
|
|
1761
2095
|
}
|
|
1762
2096
|
|
|
1763
|
-
|
|
2097
|
+
install_mempalace_with_pip() {
|
|
2098
|
+
local pip_bin="$1"
|
|
2099
|
+
local output_file status
|
|
2100
|
+
local py_bin venv_dir venv_python
|
|
2101
|
+
|
|
2102
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace-pip.XXXXXX")"
|
|
2103
|
+
|
|
2104
|
+
if $pip_bin install mempalace >"$output_file" 2>&1; then
|
|
2105
|
+
log "MemPalace package installed via '$pip_bin install mempalace'"
|
|
2106
|
+
log_file_block "MemPalace pip install" "$output_file"
|
|
2107
|
+
rm -f "$output_file"
|
|
2108
|
+
return 0
|
|
2109
|
+
else
|
|
2110
|
+
status=$?
|
|
2111
|
+
fi
|
|
2112
|
+
|
|
2113
|
+
if grep -qi "externally-managed-environment" "$output_file"; then
|
|
2114
|
+
log "Detected PEP 668 externally-managed Python environment; retrying inside isolated venv"
|
|
2115
|
+
|
|
2116
|
+
py_bin="$(command -v python3 || command -v python)"
|
|
2117
|
+
if [[ -z "$py_bin" ]]; then
|
|
2118
|
+
warn "python3/python executable not found"
|
|
2119
|
+
return 1
|
|
2120
|
+
fi
|
|
2121
|
+
|
|
2122
|
+
venv_dir="${HOME}/.agentic/mempalace-venv"
|
|
2123
|
+
|
|
2124
|
+
if [[ ! -d "$venv_dir" ]]; then
|
|
2125
|
+
mkdir -p "$(dirname "$venv_dir")"
|
|
2126
|
+
|
|
2127
|
+
if ! "$py_bin" -m venv "$venv_dir" >>"$output_file" 2>&1; then
|
|
2128
|
+
warn "Unable to create virtual environment at $venv_dir"
|
|
2129
|
+
log_file_block "MemPalace pip install" "$output_file"
|
|
2130
|
+
return 1
|
|
2131
|
+
fi
|
|
2132
|
+
fi
|
|
2133
|
+
|
|
2134
|
+
venv_python="$venv_dir/bin/python"
|
|
2135
|
+
|
|
2136
|
+
if ! "$venv_python" -m pip install --upgrade pip setuptools wheel >>"$output_file" 2>&1; then
|
|
2137
|
+
warn "Unable to upgrade pip inside virtual environment"
|
|
2138
|
+
log_file_block "MemPalace pip install" "$output_file"
|
|
2139
|
+
return 1
|
|
2140
|
+
fi
|
|
2141
|
+
|
|
2142
|
+
if ! "$venv_python" -m pip install --no-cache-dir --upgrade mempalace >>"$output_file" 2>&1; then
|
|
2143
|
+
warn "Unable to install mempalace inside virtual environment"
|
|
2144
|
+
log_file_block "MemPalace pip install" "$output_file"
|
|
2145
|
+
return 1
|
|
2146
|
+
fi
|
|
2147
|
+
|
|
2148
|
+
local_bin_dir="$HOME/.local/bin"
|
|
2149
|
+
|
|
2150
|
+
mkdir -p "$local_bin_dir"
|
|
2151
|
+
|
|
2152
|
+
ln -sf "$venv_dir/bin/mempalace" "$local_bin_dir/mempalace"
|
|
2153
|
+
|
|
2154
|
+
export PATH="$local_bin_dir:$venv_dir/bin:$PATH"
|
|
2155
|
+
|
|
2156
|
+
shell_name="$(basename "${SHELL:-}")"
|
|
2157
|
+
|
|
2158
|
+
if [[ "$shell_name" == "zsh" ]]; then
|
|
2159
|
+
grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.zshrc" 2>/dev/null || \
|
|
2160
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.zshrc"
|
|
2161
|
+
else
|
|
2162
|
+
grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" 2>/dev/null || \
|
|
2163
|
+
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
|
|
2164
|
+
fi
|
|
2165
|
+
|
|
2166
|
+
log "MemPalace installed successfully inside virtual environment: $venv_dir"
|
|
2167
|
+
log "MemPalace binary linked to: $local_bin_dir/mempalace"
|
|
2168
|
+
log_file_block "MemPalace pip install" "$output_file"
|
|
2169
|
+
|
|
2170
|
+
rm -f "$output_file"
|
|
2171
|
+
return 0
|
|
2172
|
+
fi
|
|
2173
|
+
|
|
2174
|
+
warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions (exit $status, log: $output_file)"
|
|
2175
|
+
log_file_block "MemPalace pip install" "$output_file"
|
|
2176
|
+
warn_mempalace_pip_failure_reason "$output_file"
|
|
2177
|
+
|
|
2178
|
+
return 1
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
mempalace_venv_dir() {
|
|
2182
|
+
printf '%s\n' "${AGENTIC_MEMPALACE_VENV:-$HOME/.venvs/mempalace}"
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
mempalace_bin_dir() {
|
|
2186
|
+
printf '%s\n' "${AGENTIC_MEMPALACE_BIN_DIR:-$HOME/.local/bin}"
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
python3_command() {
|
|
2190
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
2191
|
+
printf '%s\n' "python3"
|
|
2192
|
+
return 0
|
|
2193
|
+
fi
|
|
2194
|
+
if command -v python >/dev/null 2>&1; then
|
|
2195
|
+
printf '%s\n' "python"
|
|
2196
|
+
return 0
|
|
2197
|
+
fi
|
|
2198
|
+
return 1
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
install_mempalace_managed() {
|
|
2202
|
+
local py_bin venv_dir bin_dir venv_python venv_mempalace
|
|
2203
|
+
|
|
2204
|
+
py_bin="$(python3_command)" || return 1
|
|
2205
|
+
venv_dir="$(mempalace_venv_dir)"
|
|
2206
|
+
bin_dir="$(mempalace_bin_dir)"
|
|
2207
|
+
|
|
2208
|
+
mkdir -p "$(dirname "$venv_dir")" "$bin_dir"
|
|
2209
|
+
|
|
2210
|
+
if [[ ! -x "$venv_dir/bin/python" ]]; then
|
|
2211
|
+
"$py_bin" -m venv "$venv_dir" || return 1
|
|
2212
|
+
fi
|
|
2213
|
+
|
|
2214
|
+
venv_python="$venv_dir/bin/python"
|
|
2215
|
+
venv_mempalace="$venv_dir/bin/mempalace"
|
|
2216
|
+
|
|
2217
|
+
"$venv_python" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || return 1
|
|
2218
|
+
"$venv_python" -m pip install --upgrade --no-cache-dir mempalace >/dev/null 2>&1 || return 1
|
|
2219
|
+
|
|
2220
|
+
[[ -x "$venv_mempalace" ]] || return 1
|
|
2221
|
+
|
|
2222
|
+
ln -sf "$venv_mempalace" "$bin_dir/mempalace"
|
|
2223
|
+
|
|
2224
|
+
if [[ -x "$venv_dir/bin/mempalace-mcp" ]]; then
|
|
2225
|
+
ln -sf "$venv_dir/bin/mempalace-mcp" "$bin_dir/mempalace-mcp"
|
|
2226
|
+
fi
|
|
2227
|
+
|
|
2228
|
+
export PATH="$bin_dir:$PATH"
|
|
2229
|
+
|
|
2230
|
+
command -v mempalace >/dev/null 2>&1
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
initialize_mempalace_project() {
|
|
2234
|
+
local step_prefix="$1"
|
|
2235
|
+
local project_wing shared_docs_wing
|
|
2236
|
+
project_wing="$(mempalace_project_wing)"
|
|
2237
|
+
shared_docs_wing="$(mempalace_shared_docs_wing)"
|
|
2238
|
+
log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR (wing: $project_wing)"
|
|
2239
|
+
if ! command -v mempalace >/dev/null 2>&1; then
|
|
2240
|
+
warn "mempalace command is unavailable after install; please run setup manually"
|
|
2241
|
+
print_mempalace_project_setup_instructions
|
|
2242
|
+
return 1
|
|
2243
|
+
fi
|
|
2244
|
+
|
|
2245
|
+
if ! run_mempalace_command "MemPalace init" mempalace init "$PROJECT_DIR" --yes --no-llm; then
|
|
2246
|
+
print_mempalace_project_setup_instructions
|
|
2247
|
+
return 1
|
|
2248
|
+
fi
|
|
2249
|
+
if ! run_mempalace_command "MemPalace mine project wing" mempalace mine "$PROJECT_DIR" --wing "$project_wing"; then
|
|
2250
|
+
print_mempalace_project_setup_instructions
|
|
2251
|
+
return 1
|
|
2252
|
+
fi
|
|
2253
|
+
if [[ -d "$PROJECT_DIR/docs" ]]; then
|
|
2254
|
+
if ! run_mempalace_command "MemPalace mine shared docs wing" mempalace mine "$PROJECT_DIR/docs" --wing "$shared_docs_wing"; then
|
|
2255
|
+
print_mempalace_project_setup_instructions
|
|
2256
|
+
return 1
|
|
2257
|
+
fi
|
|
2258
|
+
fi
|
|
2259
|
+
log "$step_prefix [4/4] Initialization step finished"
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
setup_mempalace_for_agentic() {
|
|
2263
|
+
local initialize_project="${1:-false}"
|
|
1764
2264
|
local step_prefix="MemPalace setup"
|
|
1765
2265
|
|
|
2266
|
+
if [[ "${AGENTIC_MEMPALACE_SETUP:-}" == "skip" ]]; then
|
|
2267
|
+
log "$step_prefix skipped by AGENTIC_MEMPALACE_SETUP=skip"
|
|
2268
|
+
if command -v mempalace-mcp >/dev/null 2>&1; then
|
|
2269
|
+
return 0
|
|
2270
|
+
fi
|
|
2271
|
+
return 1
|
|
2272
|
+
fi
|
|
2273
|
+
|
|
1766
2274
|
log "$step_prefix [1/4] Checking Python availability"
|
|
1767
2275
|
if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
|
|
1768
2276
|
warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
|
|
1769
2277
|
warn "Install help: https://www.python.org/downloads/"
|
|
2278
|
+
print_mempalace_project_setup_instructions
|
|
1770
2279
|
return 1
|
|
1771
2280
|
fi
|
|
1772
2281
|
log "$step_prefix [1/4] Python check passed"
|
|
1773
2282
|
|
|
2283
|
+
if [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]] && command -v mempalace-mcp >/dev/null 2>&1; then
|
|
2284
|
+
if [[ "$initialize_project" != "true" ]] || command -v mempalace >/dev/null 2>&1; then
|
|
2285
|
+
log "$step_prefix [2/4] MemPalace binaries already available; skipping pip install"
|
|
2286
|
+
if [[ "$initialize_project" != "true" ]]; then
|
|
2287
|
+
log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
|
|
2288
|
+
return 0
|
|
2289
|
+
fi
|
|
2290
|
+
initialize_mempalace_project "$step_prefix"
|
|
2291
|
+
return $?
|
|
2292
|
+
fi
|
|
2293
|
+
fi
|
|
2294
|
+
|
|
1774
2295
|
log "$step_prefix [2/4] Checking pip availability"
|
|
1775
2296
|
local pip_bin
|
|
1776
2297
|
if ! pip_bin="$(pip_command)"; then
|
|
1777
2298
|
warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
|
|
2299
|
+
print_mempalace_project_setup_instructions
|
|
1778
2300
|
return 1
|
|
1779
2301
|
fi
|
|
1780
2302
|
log "$step_prefix [2/4] pip check passed"
|
|
1781
2303
|
|
|
1782
2304
|
log "$step_prefix [3/4] Installing mempalace package"
|
|
1783
|
-
if $pip_bin
|
|
1784
|
-
log "MemPalace package installed via '$pip_bin install mempalace'"
|
|
1785
|
-
else
|
|
1786
|
-
warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions"
|
|
2305
|
+
if ! install_mempalace_with_pip "$pip_bin"; then
|
|
1787
2306
|
print_mempalace_project_setup_instructions
|
|
1788
2307
|
return 1
|
|
1789
2308
|
fi
|
|
1790
2309
|
|
|
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
|
|
2310
|
+
if [[ "$initialize_project" != "true" ]]; then
|
|
2311
|
+
log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
|
|
2312
|
+
return 0
|
|
1808
2313
|
fi
|
|
2314
|
+
|
|
2315
|
+
initialize_mempalace_project "$step_prefix"
|
|
1809
2316
|
}
|
|
1810
2317
|
|
|
1811
2318
|
configure_mempalace_if_needed() {
|
|
@@ -1832,16 +2339,18 @@ configure_mempalace_if_needed() {
|
|
|
1832
2339
|
return
|
|
1833
2340
|
fi
|
|
1834
2341
|
|
|
1835
|
-
|
|
1836
|
-
setup_mempalace_for_agentic_opencode || true
|
|
1837
|
-
else
|
|
1838
|
-
print_mempalace_project_setup_instructions
|
|
1839
|
-
fi
|
|
2342
|
+
write_mempalace_ignore_file
|
|
1840
2343
|
|
|
1841
|
-
|
|
1842
|
-
|
|
2344
|
+
local initialize_mempalace_project="true"
|
|
2345
|
+
local mempalace_setup_ok="true"
|
|
2346
|
+
setup_mempalace_for_agentic "$initialize_mempalace_project" || mempalace_setup_ok="false"
|
|
2347
|
+
|
|
2348
|
+
if [[ "$mempalace_setup_ok" != "true" ]]; then
|
|
2349
|
+
if ! command -v mempalace-mcp >/dev/null 2>&1; then
|
|
2350
|
+
warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
|
|
2351
|
+
fi
|
|
1843
2352
|
else
|
|
1844
|
-
|
|
2353
|
+
log "MemPalace MCP binary found: mempalace-mcp"
|
|
1845
2354
|
fi
|
|
1846
2355
|
|
|
1847
2356
|
if selected_agent_os_contains "opencode"; then
|
|
@@ -1883,7 +2392,6 @@ configure_context7_if_needed() {
|
|
|
1883
2392
|
fi
|
|
1884
2393
|
|
|
1885
2394
|
if is_interactive_terminal; then
|
|
1886
|
-
local answer
|
|
1887
2395
|
if [[ -z "$enable_context7" ]]; then
|
|
1888
2396
|
read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
|
|
1889
2397
|
enable_context7="$(trim "$enable_context7")"
|
|
@@ -1892,10 +2400,12 @@ configure_context7_if_needed() {
|
|
|
1892
2400
|
log "Context7 MCP configuration disabled"
|
|
1893
2401
|
return
|
|
1894
2402
|
fi
|
|
2403
|
+
configure_context7_key_interactive
|
|
1895
2404
|
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2405
|
+
elif [[ -n "$enable_context7" ]]; then
|
|
2406
|
+
if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
|
|
2407
|
+
log "Context7 MCP configuration disabled"
|
|
2408
|
+
return
|
|
1899
2409
|
fi
|
|
1900
2410
|
elif [[ -z "$CONTEXT7_API_KEY" ]]; then
|
|
1901
2411
|
log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
|
|
@@ -1930,10 +2440,17 @@ configure_context7_if_needed() {
|
|
|
1930
2440
|
if selected_agent_os_contains "antigravity"; then
|
|
1931
2441
|
write_context7_antigravity_config
|
|
1932
2442
|
fi
|
|
2443
|
+
|
|
2444
|
+
print_context7_key_recommendation
|
|
1933
2445
|
}
|
|
1934
2446
|
|
|
1935
2447
|
write_default_opencode_plugin_config() {
|
|
1936
2448
|
ensure_dir "$APP_CONFIG_DIR"
|
|
2449
|
+
OPENCODE_TELEGRAM_ENABLED="false"
|
|
2450
|
+
OPENCODE_TELEGRAM_BOT_TOKEN=""
|
|
2451
|
+
OPENCODE_TELEGRAM_CHAT_ID=""
|
|
2452
|
+
OPENCODE_AGENT_MODEL_MAPPER_ENABLED="false"
|
|
2453
|
+
OPENCODE_PLUGINS_CONFIGURED=true
|
|
1937
2454
|
if [[ "$DRY_RUN" == true ]]; then
|
|
1938
2455
|
log "DRY-RUN write disabled opencode plugin config to $OPENCODE_PLUGIN_CONFIG_FILE"
|
|
1939
2456
|
else
|
|
@@ -1944,13 +2461,40 @@ from pathlib import Path
|
|
|
1944
2461
|
|
|
1945
2462
|
path = Path(sys.argv[1])
|
|
1946
2463
|
path.write_text(json.dumps({
|
|
1947
|
-
"telegram": {"enabled": False
|
|
1948
|
-
"
|
|
2464
|
+
"telegram": {"enabled": False},
|
|
2465
|
+
"agentModelMapper": {"enabled": False},
|
|
1949
2466
|
}, indent=2) + "\n", encoding="utf-8")
|
|
1950
2467
|
PY
|
|
1951
2468
|
fi
|
|
1952
2469
|
}
|
|
1953
2470
|
|
|
2471
|
+
load_opencode_plugin_config_globals() {
|
|
2472
|
+
[[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
|
|
2473
|
+
local values=()
|
|
2474
|
+
readlines values < <(python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
|
|
2475
|
+
import json
|
|
2476
|
+
import sys
|
|
2477
|
+
from pathlib import Path
|
|
2478
|
+
|
|
2479
|
+
try:
|
|
2480
|
+
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
2481
|
+
except Exception:
|
|
2482
|
+
data = {}
|
|
2483
|
+
telegram = data.get("telegram", {}) if isinstance(data, dict) else {}
|
|
2484
|
+
mapper = data.get("agentModelMapper", {}) if isinstance(data, dict) else {}
|
|
2485
|
+
print("true" if telegram.get("enabled") is True else "false")
|
|
2486
|
+
print(telegram.get("botToken") or "")
|
|
2487
|
+
print(telegram.get("chatId") or "")
|
|
2488
|
+
print("true" if mapper.get("enabled") is True else "false")
|
|
2489
|
+
PY
|
|
2490
|
+
)
|
|
2491
|
+
OPENCODE_TELEGRAM_ENABLED="${values[0]:-false}"
|
|
2492
|
+
OPENCODE_TELEGRAM_BOT_TOKEN="${values[1]:-}"
|
|
2493
|
+
OPENCODE_TELEGRAM_CHAT_ID="${values[2]:-}"
|
|
2494
|
+
OPENCODE_AGENT_MODEL_MAPPER_ENABLED="${values[3]:-false}"
|
|
2495
|
+
OPENCODE_PLUGINS_CONFIGURED=true
|
|
2496
|
+
}
|
|
2497
|
+
|
|
1954
2498
|
configure_opencode_plugins_if_needed() {
|
|
1955
2499
|
selected_agent_os_contains "opencode" || return 0
|
|
1956
2500
|
|
|
@@ -1962,14 +2506,27 @@ configure_opencode_plugins_if_needed() {
|
|
|
1962
2506
|
ensure_python_available
|
|
1963
2507
|
ensure_dir "$APP_CONFIG_DIR"
|
|
1964
2508
|
|
|
2509
|
+
# During manifest replay/re-install, keep current global plugin settings and avoid prompts.
|
|
2510
|
+
if [[ "$INSTALL_SETTINGS_REPLAY" == true && "$OPENCODE_PLUGINS_CONFIGURED" == true ]]; then
|
|
2511
|
+
log "OpenCode plugin settings loaded from .agentic.json"
|
|
2512
|
+
return
|
|
2513
|
+
fi
|
|
2514
|
+
if [[ "$INSTALL_SETTINGS_REPLAY" == true && -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
|
|
2515
|
+
load_opencode_plugin_config_globals || true
|
|
2516
|
+
log "OpenCode plugin config already exists; keeping current settings"
|
|
2517
|
+
return
|
|
2518
|
+
fi
|
|
2519
|
+
|
|
1965
2520
|
if ! is_interactive_terminal; then
|
|
1966
2521
|
if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
|
|
1967
2522
|
write_default_opencode_plugin_config
|
|
2523
|
+
else
|
|
2524
|
+
load_opencode_plugin_config_globals || true
|
|
1968
2525
|
fi
|
|
1969
2526
|
return
|
|
1970
2527
|
fi
|
|
1971
2528
|
|
|
1972
|
-
local plugin_options=("telegram-
|
|
2529
|
+
local plugin_options=("telegram-notification" "agent-model-mapper")
|
|
1973
2530
|
local selected_plugins=()
|
|
1974
2531
|
local use_fzf_plugins=false
|
|
1975
2532
|
if fzf_available; then
|
|
@@ -1986,47 +2543,472 @@ configure_opencode_plugins_if_needed() {
|
|
|
1986
2543
|
readlines selected_plugins <<< "$selected_plugins_output"
|
|
1987
2544
|
fi
|
|
1988
2545
|
|
|
1989
|
-
local enable_telegram="n"
|
|
2546
|
+
local enable_telegram="n" enable_agent_model_mapper="n"
|
|
1990
2547
|
local selected_plugin
|
|
1991
2548
|
for selected_plugin in "${selected_plugins[@]}"; do
|
|
1992
2549
|
selected_plugin="$(trim "$selected_plugin")"
|
|
1993
2550
|
[[ -z "$selected_plugin" ]] && continue
|
|
1994
2551
|
case "$selected_plugin" in
|
|
1995
|
-
telegram-opencode-notifier) enable_telegram="y" ;;
|
|
1996
|
-
|
|
2552
|
+
telegram-notification|telegram-opencode-notifier) enable_telegram="y" ;;
|
|
2553
|
+
agent-model-mapper) enable_agent_model_mapper="y" ;;
|
|
1997
2554
|
esac
|
|
1998
2555
|
done
|
|
1999
2556
|
|
|
2000
|
-
telegram_token=""
|
|
2001
|
-
telegram_chat=""
|
|
2002
2557
|
if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2558
|
+
OPENCODE_TELEGRAM_BOT_TOKEN="$(prompt_text_interactive "Telegram botToken" "$OPENCODE_TELEGRAM_BOT_TOKEN")"
|
|
2559
|
+
OPENCODE_TELEGRAM_CHAT_ID="$(prompt_text_interactive "Telegram chatId" "$OPENCODE_TELEGRAM_CHAT_ID")"
|
|
2560
|
+
if [[ -z "$OPENCODE_TELEGRAM_BOT_TOKEN" || -z "$OPENCODE_TELEGRAM_CHAT_ID" ]]; then
|
|
2561
|
+
warn "Telegram plugin credentials are incomplete; disabling telegram-notification"
|
|
2562
|
+
enable_telegram="n"
|
|
2563
|
+
OPENCODE_TELEGRAM_BOT_TOKEN=""
|
|
2564
|
+
OPENCODE_TELEGRAM_CHAT_ID=""
|
|
2565
|
+
else
|
|
2566
|
+
log "Telegram plugin enabled; credentials will be stored in project .agentic.json"
|
|
2567
|
+
fi
|
|
2007
2568
|
fi
|
|
2569
|
+
OPENCODE_TELEGRAM_ENABLED=$([[ "$enable_telegram" =~ ^[Yy]$ ]] && echo "true" || echo "false")
|
|
2570
|
+
OPENCODE_AGENT_MODEL_MAPPER_ENABLED=$([[ "$enable_agent_model_mapper" =~ ^[Yy]$ ]] && echo "true" || echo "false")
|
|
2571
|
+
OPENCODE_PLUGINS_CONFIGURED=true
|
|
2008
2572
|
|
|
2009
|
-
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$
|
|
2573
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$enable_telegram" "$enable_agent_model_mapper" <<'PY'
|
|
2010
2574
|
import json
|
|
2011
2575
|
import sys
|
|
2012
2576
|
from pathlib import Path
|
|
2013
2577
|
|
|
2014
2578
|
path = Path(sys.argv[1])
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
enable_model = sys.argv[4].lower() == "y"
|
|
2579
|
+
enable_telegram = sys.argv[2].lower() == "y"
|
|
2580
|
+
enable_mapper = sys.argv[3].lower() == "y"
|
|
2018
2581
|
data = {
|
|
2019
2582
|
"telegram": {
|
|
2020
|
-
"enabled":
|
|
2021
|
-
"botToken": token,
|
|
2022
|
-
"chatId": chat,
|
|
2583
|
+
"enabled": enable_telegram,
|
|
2023
2584
|
},
|
|
2024
|
-
"
|
|
2025
|
-
"enabled":
|
|
2585
|
+
"agentModelMapper": {
|
|
2586
|
+
"enabled": enable_mapper,
|
|
2026
2587
|
},
|
|
2027
2588
|
}
|
|
2028
2589
|
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
2029
2590
|
PY
|
|
2591
|
+
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
opencode_agent_model_mapper_config_enabled() {
|
|
2595
|
+
if [[ "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" == "true" ]]; then
|
|
2596
|
+
return 0
|
|
2597
|
+
fi
|
|
2598
|
+
if [[ "$OPENCODE_AGENT_MODEL_MAPPER_ENABLED" == "false" ]]; then
|
|
2599
|
+
return 1
|
|
2600
|
+
fi
|
|
2601
|
+
[[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
|
|
2602
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
|
|
2603
|
+
import json
|
|
2604
|
+
import sys
|
|
2605
|
+
from pathlib import Path
|
|
2606
|
+
|
|
2607
|
+
try:
|
|
2608
|
+
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
2609
|
+
except Exception:
|
|
2610
|
+
raise SystemExit(1)
|
|
2611
|
+
raise SystemExit(0 if data.get("agentModelMapper", {}).get("enabled") is True else 1)
|
|
2612
|
+
PY
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
opencode_mapper_read_roles() {
|
|
2616
|
+
local agents_dir="$PROJECT_DIR/.opencode/agents"
|
|
2617
|
+
[[ -d "$agents_dir" ]] || return 0
|
|
2618
|
+
python3 - "$agents_dir" <<'PY'
|
|
2619
|
+
import sys
|
|
2620
|
+
from pathlib import Path
|
|
2621
|
+
|
|
2622
|
+
agents_dir = Path(sys.argv[1])
|
|
2623
|
+
|
|
2624
|
+
def parse_frontmatter(text):
|
|
2625
|
+
if not text.startswith("---\n"):
|
|
2626
|
+
return {}
|
|
2627
|
+
end = text.find("\n---", 4)
|
|
2628
|
+
if end == -1:
|
|
2629
|
+
return {}
|
|
2630
|
+
result = {}
|
|
2631
|
+
for line in text[4:end].splitlines():
|
|
2632
|
+
if ":" not in line:
|
|
2633
|
+
continue
|
|
2634
|
+
key, value = line.split(":", 1)
|
|
2635
|
+
result[key.strip()] = value.strip().strip("'\"")
|
|
2636
|
+
return result
|
|
2637
|
+
|
|
2638
|
+
for path in sorted(agents_dir.glob("*.md")):
|
|
2639
|
+
frontmatter = parse_frontmatter(path.read_text(encoding="utf-8"))
|
|
2640
|
+
name = path.stem.replace("\t", " ")
|
|
2641
|
+
mode = (frontmatter.get("mode") or "subagent").replace("\t", " ")
|
|
2642
|
+
description = (frontmatter.get("description") or "OpenCode agent").replace("\t", " ")
|
|
2643
|
+
print(f"{name}\t{mode}\t{description}")
|
|
2644
|
+
PY
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
opencode_mapper_discover_models() {
|
|
2648
|
+
local config_path="$HOME/.config/opencode/opencode.json"
|
|
2649
|
+
local auth_path="$HOME/.local/share/opencode/auth.json"
|
|
2650
|
+
local models_cache_path="$HOME/.cache/opencode/models.json"
|
|
2651
|
+
python3 - "$config_path" "$auth_path" "$models_cache_path" <<'PY'
|
|
2652
|
+
import json
|
|
2653
|
+
import sys
|
|
2654
|
+
from pathlib import Path
|
|
2655
|
+
|
|
2656
|
+
fallback = ["opencode/minimax-m2.5-free"]
|
|
2657
|
+
config_path = Path(sys.argv[1])
|
|
2658
|
+
auth_path = Path(sys.argv[2])
|
|
2659
|
+
models_cache_path = Path(sys.argv[3])
|
|
2660
|
+
models = []
|
|
2661
|
+
|
|
2662
|
+
def add_model(model):
|
|
2663
|
+
if isinstance(model, str) and model.strip() and "/" in model:
|
|
2664
|
+
models.append(model.strip())
|
|
2665
|
+
|
|
2666
|
+
def collect_provider_models(data):
|
|
2667
|
+
"""Extract models from provider.<name>.models dict keys."""
|
|
2668
|
+
providers = data.get("provider")
|
|
2669
|
+
if not isinstance(providers, dict):
|
|
2670
|
+
return
|
|
2671
|
+
for provider_name, provider_data in providers.items():
|
|
2672
|
+
if not isinstance(provider_data, dict):
|
|
2673
|
+
continue
|
|
2674
|
+
provider_models = provider_data.get("models")
|
|
2675
|
+
if not isinstance(provider_models, dict):
|
|
2676
|
+
continue
|
|
2677
|
+
for model_name in provider_models:
|
|
2678
|
+
if isinstance(model_name, str) and model_name.strip():
|
|
2679
|
+
add_model(f"{provider_name}/{model_name}")
|
|
2680
|
+
|
|
2681
|
+
def collect(value):
|
|
2682
|
+
if isinstance(value, list):
|
|
2683
|
+
for item in value:
|
|
2684
|
+
collect(item)
|
|
2685
|
+
return
|
|
2686
|
+
if not isinstance(value, dict):
|
|
2687
|
+
return
|
|
2688
|
+
for key, item in value.items():
|
|
2689
|
+
if key in {"model", "id"} and isinstance(item, str) and "/" in item:
|
|
2690
|
+
add_model(item)
|
|
2691
|
+
if key == "fallback" and isinstance(item, list):
|
|
2692
|
+
for model in item:
|
|
2693
|
+
add_model(model)
|
|
2694
|
+
collect(item)
|
|
2695
|
+
|
|
2696
|
+
def read_json(path):
|
|
2697
|
+
try:
|
|
2698
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
2699
|
+
except Exception:
|
|
2700
|
+
return None
|
|
2701
|
+
|
|
2702
|
+
def is_deprecated(model_data):
|
|
2703
|
+
if not isinstance(model_data, dict):
|
|
2704
|
+
return False
|
|
2705
|
+
status = str(model_data.get("status", "")).lower()
|
|
2706
|
+
lifecycle = str(model_data.get("lifecycle", "")).lower()
|
|
2707
|
+
return (
|
|
2708
|
+
model_data.get("deprecated") is True
|
|
2709
|
+
or status in {"deprecated", "retired", "removed"}
|
|
2710
|
+
or lifecycle in {"deprecated", "retired", "removed"}
|
|
2711
|
+
)
|
|
2712
|
+
|
|
2713
|
+
def collect_authenticated_provider_models(auth_data, cache_data):
|
|
2714
|
+
if not isinstance(auth_data, dict) or not isinstance(cache_data, dict):
|
|
2715
|
+
return
|
|
2716
|
+
for provider_name, auth in auth_data.items():
|
|
2717
|
+
if not isinstance(provider_name, str) or not provider_name.strip():
|
|
2718
|
+
continue
|
|
2719
|
+
if auth in (None, False):
|
|
2720
|
+
continue
|
|
2721
|
+
provider_data = cache_data.get(provider_name)
|
|
2722
|
+
if not isinstance(provider_data, dict):
|
|
2723
|
+
continue
|
|
2724
|
+
provider_models = provider_data.get("models")
|
|
2725
|
+
if isinstance(provider_models, dict):
|
|
2726
|
+
for model_name, model_data in provider_models.items():
|
|
2727
|
+
if isinstance(model_name, str) and model_name.strip() and not is_deprecated(model_data):
|
|
2728
|
+
add_model(f"{provider_name}/{model_name}")
|
|
2729
|
+
elif isinstance(provider_models, list):
|
|
2730
|
+
for item in provider_models:
|
|
2731
|
+
if isinstance(item, str):
|
|
2732
|
+
add_model(f"{provider_name}/{item}")
|
|
2733
|
+
elif isinstance(item, dict) and not is_deprecated(item):
|
|
2734
|
+
model_name = item.get("id") or item.get("name")
|
|
2735
|
+
if isinstance(model_name, str) and model_name.strip():
|
|
2736
|
+
add_model(f"{provider_name}/{model_name}")
|
|
2737
|
+
|
|
2738
|
+
try:
|
|
2739
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
2740
|
+
collect_provider_models(data)
|
|
2741
|
+
collect(data)
|
|
2742
|
+
except Exception:
|
|
2743
|
+
pass
|
|
2744
|
+
|
|
2745
|
+
collect_authenticated_provider_models(read_json(auth_path), read_json(models_cache_path))
|
|
2746
|
+
|
|
2747
|
+
seen = set()
|
|
2748
|
+
for model in models or fallback:
|
|
2749
|
+
model = model.strip()
|
|
2750
|
+
if model and model not in seen:
|
|
2751
|
+
seen.add(model)
|
|
2752
|
+
print(model)
|
|
2753
|
+
PY
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
opencode_mapper_has_complete_mapping() {
|
|
2757
|
+
local roles_file="$1"
|
|
2758
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2759
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2760
|
+
python3 - "$roles_file" "$config_path" "$state_path" <<'PY'
|
|
2761
|
+
import json
|
|
2762
|
+
import sys
|
|
2763
|
+
from pathlib import Path
|
|
2764
|
+
|
|
2765
|
+
roles_file, config_path, state_path = map(Path, sys.argv[1:])
|
|
2766
|
+
try:
|
|
2767
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
2768
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
2769
|
+
except Exception:
|
|
2770
|
+
raise SystemExit(1)
|
|
2771
|
+
if not state.get("configured"):
|
|
2772
|
+
raise SystemExit(1)
|
|
2773
|
+
agents = config.get("agent")
|
|
2774
|
+
if not isinstance(agents, dict):
|
|
2775
|
+
raise SystemExit(1)
|
|
2776
|
+
roles = [line.split("\t", 1)[0] for line in roles_file.read_text(encoding="utf-8").splitlines() if line]
|
|
2777
|
+
for role in roles:
|
|
2778
|
+
agent = agents.get(role)
|
|
2779
|
+
if not isinstance(agent, dict) or not str(agent.get("model", "")).strip():
|
|
2780
|
+
raise SystemExit(1)
|
|
2781
|
+
raise SystemExit(0)
|
|
2782
|
+
PY
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
choose_opencode_mapper_model() {
|
|
2786
|
+
local role_name="$1"
|
|
2787
|
+
local role_mode="$2"
|
|
2788
|
+
local role_description="$3"
|
|
2789
|
+
local kind="$4"
|
|
2790
|
+
shift 4
|
|
2791
|
+
local models=("$@")
|
|
2792
|
+
|
|
2793
|
+
if [[ "${AGENTIC_AGENT_MODEL_MAPPER_NO_FZF:-}" != "1" ]] && fzf_available; then
|
|
2794
|
+
local selected selected_model fzf_status
|
|
2795
|
+
set +e
|
|
2796
|
+
selected="$(for i in "${!models[@]}"; do printf '%s\t%s\n' "$((i + 1))" "${models[$i]}"; done | fzf \
|
|
2797
|
+
--ansi \
|
|
2798
|
+
--border \
|
|
2799
|
+
--height=70% \
|
|
2800
|
+
--layout=reverse \
|
|
2801
|
+
--no-sort \
|
|
2802
|
+
--prompt "$role_name $kind> " \
|
|
2803
|
+
--header "Select $kind model for $role_name" \
|
|
2804
|
+
--with-nth=2..)"
|
|
2805
|
+
fzf_status=$?
|
|
2806
|
+
set -e
|
|
2807
|
+
if [[ "$fzf_status" -eq 0 && -n "$(trim "$selected")" ]]; then
|
|
2808
|
+
selected_model="${selected#* }"
|
|
2809
|
+
local model
|
|
2810
|
+
for model in "${models[@]}"; do
|
|
2811
|
+
if [[ "$model" == "$selected_model" ]]; then
|
|
2812
|
+
printf '%s\n' "$selected_model"
|
|
2813
|
+
return 0
|
|
2814
|
+
fi
|
|
2815
|
+
done
|
|
2816
|
+
fi
|
|
2817
|
+
fi
|
|
2818
|
+
|
|
2819
|
+
echo >&2
|
|
2820
|
+
echo "$role_name ($role_mode) - $role_description" >&2
|
|
2821
|
+
local i
|
|
2822
|
+
for i in "${!models[@]}"; do
|
|
2823
|
+
echo " $((i + 1))) ${models[$i]}" >&2
|
|
2824
|
+
done
|
|
2825
|
+
local answer
|
|
2826
|
+
read -r -p "Select $kind model for $role_name [1]: " answer
|
|
2827
|
+
answer="$(trim "$answer")"
|
|
2828
|
+
if [[ -z "$answer" ]]; then
|
|
2829
|
+
printf '%s\n' "${models[0]}"
|
|
2830
|
+
return 0
|
|
2831
|
+
fi
|
|
2832
|
+
if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#models[@]} )); then
|
|
2833
|
+
printf '%s\n' "${models[$((answer - 1))]}"
|
|
2834
|
+
return 0
|
|
2835
|
+
fi
|
|
2836
|
+
local model
|
|
2837
|
+
for model in "${models[@]}"; do
|
|
2838
|
+
if [[ "$model" == "$answer" ]]; then
|
|
2839
|
+
printf '%s\n' "$answer"
|
|
2840
|
+
return 0
|
|
2841
|
+
fi
|
|
2842
|
+
done
|
|
2843
|
+
warn "Unknown model '$answer', using ${models[0]}"
|
|
2844
|
+
printf '%s\n' "${models[0]}"
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
write_opencode_agent_model_mapping() {
|
|
2848
|
+
local roles_file="$1"
|
|
2849
|
+
local mapping_file="$2"
|
|
2850
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2851
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2852
|
+
|
|
2853
|
+
python3 - "$roles_file" "$mapping_file" "$config_path" "$state_path" <<'PY'
|
|
2854
|
+
import json
|
|
2855
|
+
import sys
|
|
2856
|
+
from pathlib import Path
|
|
2857
|
+
|
|
2858
|
+
roles_file, mapping_file, config_path, state_path = map(Path, sys.argv[1:])
|
|
2859
|
+
roles = []
|
|
2860
|
+
for line in roles_file.read_text(encoding="utf-8").splitlines():
|
|
2861
|
+
if not line:
|
|
2862
|
+
continue
|
|
2863
|
+
name, mode, description = (line.split("\t") + ["", "", ""])[:3]
|
|
2864
|
+
roles.append({"name": name, "mode": mode, "description": description})
|
|
2865
|
+
|
|
2866
|
+
mapping = {}
|
|
2867
|
+
for line in mapping_file.read_text(encoding="utf-8").splitlines():
|
|
2868
|
+
if not line:
|
|
2869
|
+
continue
|
|
2870
|
+
name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
|
|
2871
|
+
mapping[name] = {"model": model, "fallback": [fallback] if fallback and fallback != model else []}
|
|
2872
|
+
|
|
2873
|
+
try:
|
|
2874
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
2875
|
+
except Exception:
|
|
2876
|
+
data = {}
|
|
2877
|
+
if not isinstance(data, dict):
|
|
2878
|
+
data = {}
|
|
2879
|
+
agents = data.setdefault("agent", {})
|
|
2880
|
+
for role in roles:
|
|
2881
|
+
selected = mapping.get(role["name"])
|
|
2882
|
+
if not selected:
|
|
2883
|
+
continue
|
|
2884
|
+
current = agents.get(role["name"])
|
|
2885
|
+
if not isinstance(current, dict):
|
|
2886
|
+
current = {}
|
|
2887
|
+
current.update({
|
|
2888
|
+
"mode": current.get("mode") or role["mode"],
|
|
2889
|
+
"description": current.get("description") or role["description"],
|
|
2890
|
+
"model": selected["model"],
|
|
2891
|
+
"fallback": selected["fallback"],
|
|
2892
|
+
})
|
|
2893
|
+
agents[role["name"]] = current
|
|
2894
|
+
|
|
2895
|
+
config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2896
|
+
state_path.write_text(json.dumps({
|
|
2897
|
+
"configured": True,
|
|
2898
|
+
"roles": [role["name"] for role in roles],
|
|
2899
|
+
}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2900
|
+
PY
|
|
2901
|
+
|
|
2902
|
+
register_managed_file "$config_path" "generated:opencode-agent-model-mapper-config" "config"
|
|
2903
|
+
register_managed_file "$state_path" "generated:opencode-agent-model-mapper-state" "config"
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
confirm_opencode_agent_model_mapping() {
|
|
2907
|
+
local mapping_file="$1"
|
|
2908
|
+
if fzf_available; then
|
|
2909
|
+
local summary selected
|
|
2910
|
+
summary="$(python3 - "$mapping_file" <<'PY'
|
|
2911
|
+
import sys
|
|
2912
|
+
from pathlib import Path
|
|
2913
|
+
|
|
2914
|
+
for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
|
|
2915
|
+
if not line:
|
|
2916
|
+
continue
|
|
2917
|
+
name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
|
|
2918
|
+
print(f"{name}: main={model} fallback={fallback}")
|
|
2919
|
+
PY
|
|
2920
|
+
)"
|
|
2921
|
+
set +e
|
|
2922
|
+
selected="$(printf '%s\n' "Confirm" "Cancel" | fzf \
|
|
2923
|
+
--ansi \
|
|
2924
|
+
--border \
|
|
2925
|
+
--height=70% \
|
|
2926
|
+
--layout=reverse \
|
|
2927
|
+
--no-sort \
|
|
2928
|
+
--prompt "Save OpenCode model mapping? " \
|
|
2929
|
+
--header "$summary")"
|
|
2930
|
+
local fzf_status=$?
|
|
2931
|
+
set -e
|
|
2932
|
+
[[ "$fzf_status" -eq 0 ]] || return 1
|
|
2933
|
+
[[ "$selected" == "Confirm" ]]
|
|
2934
|
+
return
|
|
2935
|
+
fi
|
|
2936
|
+
|
|
2937
|
+
out "agent-model-mapper selected mapping:"
|
|
2938
|
+
python3 - "$mapping_file" <<'PY'
|
|
2939
|
+
import sys
|
|
2940
|
+
from pathlib import Path
|
|
2941
|
+
|
|
2942
|
+
for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
|
|
2943
|
+
if not line:
|
|
2944
|
+
continue
|
|
2945
|
+
name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
|
|
2946
|
+
print(f" - {name}: main={model} fallback={fallback}")
|
|
2947
|
+
PY
|
|
2948
|
+
confirm_action_interactive "Write .opencode/opencode.json agent model mapping?"
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
configure_opencode_agent_model_mapper_if_needed() {
|
|
2952
|
+
selected_agent_os_contains "opencode" || return 0
|
|
2953
|
+
opencode_agent_model_mapper_config_enabled || return 0
|
|
2954
|
+
|
|
2955
|
+
if ! is_interactive_terminal; then
|
|
2956
|
+
log "agent-model-mapper install-time setup skipped because no interactive terminal is available"
|
|
2957
|
+
return 0
|
|
2958
|
+
fi
|
|
2959
|
+
|
|
2960
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2961
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2962
|
+
can_write_managed_file "$config_path" || return 0
|
|
2963
|
+
if [[ -e "$state_path" ]]; then
|
|
2964
|
+
can_write_managed_file "$state_path" || return 0
|
|
2965
|
+
fi
|
|
2966
|
+
|
|
2967
|
+
local roles_file models_file mapping_file
|
|
2968
|
+
roles_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-roles.XXXXXX")"
|
|
2969
|
+
models_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-models.XXXXXX")"
|
|
2970
|
+
mapping_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-mapping.XXXXXX")"
|
|
2971
|
+
opencode_mapper_read_roles > "$roles_file"
|
|
2972
|
+
|
|
2973
|
+
if [[ ! -s "$roles_file" ]]; then
|
|
2974
|
+
log "agent-model-mapper: skipped because .opencode/agents/*.md was not found"
|
|
2975
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2976
|
+
return 0
|
|
2977
|
+
fi
|
|
2978
|
+
|
|
2979
|
+
if opencode_mapper_has_complete_mapping "$roles_file"; then
|
|
2980
|
+
log "agent-model-mapper: skipped because all Agentic roles already have model mappings"
|
|
2981
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2982
|
+
return 0
|
|
2983
|
+
fi
|
|
2984
|
+
|
|
2985
|
+
opencode_mapper_discover_models > "$models_file"
|
|
2986
|
+
local models=()
|
|
2987
|
+
readlines models < "$models_file"
|
|
2988
|
+
if [[ "${#models[@]}" -eq 0 ]]; then
|
|
2989
|
+
models=("opencode/minimax-m2.5-free")
|
|
2990
|
+
fi
|
|
2991
|
+
|
|
2992
|
+
out "agent-model-mapper: choose OpenCode models for Agentic roles"
|
|
2993
|
+
local role_name role_mode role_description model fallback
|
|
2994
|
+
exec 3<&0
|
|
2995
|
+
while IFS=$'\t' read -r role_name role_mode role_description || [[ -n "${role_name:-}" ]]; do
|
|
2996
|
+
[[ -n "$role_name" ]] || continue
|
|
2997
|
+
model="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "main" "${models[@]}" <&3)"
|
|
2998
|
+
fallback="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "fallback" "${models[@]}" <&3)"
|
|
2999
|
+
printf '%s\t%s\t%s\n' "$role_name" "$model" "$fallback" >> "$mapping_file"
|
|
3000
|
+
done < "$roles_file"
|
|
3001
|
+
|
|
3002
|
+
exec 3<&-
|
|
3003
|
+
if ! confirm_opencode_agent_model_mapping "$mapping_file"; then
|
|
3004
|
+
log "agent-model-mapper: skipped by user; no files changed"
|
|
3005
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
3006
|
+
return 0
|
|
3007
|
+
fi
|
|
3008
|
+
|
|
3009
|
+
write_opencode_agent_model_mapping "$roles_file" "$mapping_file"
|
|
3010
|
+
log "agent-model-mapper: updated .opencode/opencode.json"
|
|
3011
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2030
3012
|
}
|
|
2031
3013
|
|
|
2032
3014
|
normalize_selected_agent_os() {
|
|
@@ -2052,7 +3034,6 @@ copy_extension_for_agent() {
|
|
|
2052
3034
|
local project_dir="$2"
|
|
2053
3035
|
|
|
2054
3036
|
if [[ "$agent_os" == "$DEFAULT_AGENT_OS" ]] || [[ "$agent_os" == "agents" ]]; then
|
|
2055
|
-
log "Agent OS '$agent_os': skipping extension copy"
|
|
2056
3037
|
return
|
|
2057
3038
|
fi
|
|
2058
3039
|
|
|
@@ -2064,7 +3045,6 @@ copy_extension_for_agent() {
|
|
|
2064
3045
|
return
|
|
2065
3046
|
fi
|
|
2066
3047
|
|
|
2067
|
-
log "Copy extension: $src -> $dest"
|
|
2068
3048
|
copy_dir_contents "$src" "$dest"
|
|
2069
3049
|
}
|
|
2070
3050
|
|
|
@@ -2109,7 +3089,6 @@ copy_specialization_assets() {
|
|
|
2109
3089
|
local dest_dir
|
|
2110
3090
|
dest_dir="$(get_dest_dir "$target" "$bucket")"
|
|
2111
3091
|
if [[ "$dest_dir" == "-" ]]; then
|
|
2112
|
-
log "Skip $spec_key/$bucket (not supported by '$target')"
|
|
2113
3092
|
continue
|
|
2114
3093
|
fi
|
|
2115
3094
|
unique_append "$dest_dir" dest_dirs
|
|
@@ -2118,7 +3097,6 @@ copy_specialization_assets() {
|
|
|
2118
3097
|
local resolved_dir
|
|
2119
3098
|
for resolved_dir in "${dest_dirs[@]}"; do
|
|
2120
3099
|
local dest="$project_dir/$resolved_dir"
|
|
2121
|
-
log "Copy $spec_key/$bucket -> $dest"
|
|
2122
3100
|
copy_dir_contents "$src" "$dest"
|
|
2123
3101
|
done
|
|
2124
3102
|
done
|
|
@@ -2232,6 +3210,40 @@ generate_agents_md() {
|
|
|
2232
3210
|
rm -f "$tmp"
|
|
2233
3211
|
}
|
|
2234
3212
|
|
|
3213
|
+
copy_memory_md() {
|
|
3214
|
+
local project_dir="$1"
|
|
3215
|
+
local src="$REPO_ROOT/MEMORY.md"
|
|
3216
|
+
|
|
3217
|
+
if [[ ! -f "$src" ]]; then
|
|
3218
|
+
warn "MEMORY.md not found in knowledge base at $src; skipping"
|
|
3219
|
+
return
|
|
3220
|
+
fi
|
|
3221
|
+
|
|
3222
|
+
local outputs=()
|
|
3223
|
+
|
|
3224
|
+
if selected_agent_os_contains "opencode"; then
|
|
3225
|
+
unique_append "$project_dir/.opencode/MEMORY.md" outputs
|
|
3226
|
+
fi
|
|
3227
|
+
|
|
3228
|
+
local needs_root=false
|
|
3229
|
+
local agent_os
|
|
3230
|
+
for agent_os in "${SELECTED_AGENT_OS[@]}"; do
|
|
3231
|
+
if [[ "$agent_os" != "opencode" ]]; then
|
|
3232
|
+
needs_root=true
|
|
3233
|
+
break
|
|
3234
|
+
fi
|
|
3235
|
+
done
|
|
3236
|
+
|
|
3237
|
+
if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
|
|
3238
|
+
unique_append "$project_dir/MEMORY.md" outputs
|
|
3239
|
+
fi
|
|
3240
|
+
|
|
3241
|
+
local out
|
|
3242
|
+
for out in "${outputs[@]}"; do
|
|
3243
|
+
write_file_with_agentic_marker "$src" "$out" "generated:MEMORY.md"
|
|
3244
|
+
done
|
|
3245
|
+
}
|
|
3246
|
+
|
|
2235
3247
|
validate_inputs() {
|
|
2236
3248
|
local available_areas
|
|
2237
3249
|
available_areas="$(list_areas || true)"
|
|
@@ -2299,6 +3311,8 @@ validate_inputs() {
|
|
|
2299
3311
|
}
|
|
2300
3312
|
|
|
2301
3313
|
print_report() {
|
|
3314
|
+
write_changed_paths_report
|
|
3315
|
+
|
|
2302
3316
|
out
|
|
2303
3317
|
out "=== Installation report ===" "$COLOR_HEADER"
|
|
2304
3318
|
out "Agentic version: $(app_version_label)"
|
|
@@ -2310,26 +3324,9 @@ print_report() {
|
|
|
2310
3324
|
out "Specializations: ${SELECTED_SPECS[*]}"
|
|
2311
3325
|
|
|
2312
3326
|
out
|
|
2313
|
-
out "Created directories:"
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
else
|
|
2317
|
-
local created_path
|
|
2318
|
-
for created_path in "${CREATED_PATHS[@]}"; do
|
|
2319
|
-
out "- $created_path"
|
|
2320
|
-
done
|
|
2321
|
-
fi
|
|
2322
|
-
|
|
2323
|
-
out
|
|
2324
|
-
out "Copied/generated paths:"
|
|
2325
|
-
if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
|
|
2326
|
-
out "- (none)"
|
|
2327
|
-
else
|
|
2328
|
-
local copied_path
|
|
2329
|
-
for copied_path in "${COPIED_PATHS[@]}"; do
|
|
2330
|
-
out "- $copied_path"
|
|
2331
|
-
done
|
|
2332
|
-
fi
|
|
3327
|
+
out "Created directories: ${#CREATED_PATHS[@]}"
|
|
3328
|
+
out "Copied/generated paths: ${#COPIED_PATHS[@]}"
|
|
3329
|
+
out "Changed paths report: $CHANGED_PATHS_REPORT_FILE"
|
|
2333
3330
|
|
|
2334
3331
|
out
|
|
2335
3332
|
out "Warnings:"
|
|
@@ -2469,7 +3466,15 @@ doctor_enabled() {
|
|
|
2469
3466
|
}
|
|
2470
3467
|
|
|
2471
3468
|
doctor_prompt() {
|
|
2472
|
-
printf '%s\n' "
|
|
3469
|
+
printf '%s\n' "Reply with exactly: AGENTIC_DOCTOR_OK"
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
doctor_prompt_for_agent() {
|
|
3473
|
+
doctor_prompt
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
doctor_smoke_label() {
|
|
3477
|
+
printf '%s\n' "lightweight smoke"
|
|
2473
3478
|
}
|
|
2474
3479
|
|
|
2475
3480
|
doctor_output_has_fatal_patterns() {
|
|
@@ -2477,6 +3482,40 @@ doctor_output_has_fatal_patterns() {
|
|
|
2477
3482
|
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
3483
|
}
|
|
2479
3484
|
|
|
3485
|
+
doctor_timeout_seconds() {
|
|
3486
|
+
local value="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
|
|
3487
|
+
if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then
|
|
3488
|
+
value=10
|
|
3489
|
+
fi
|
|
3490
|
+
printf '%s\n' "$value"
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
run_with_doctor_timeout() {
|
|
3494
|
+
local timeout_seconds="$1"
|
|
3495
|
+
shift
|
|
3496
|
+
|
|
3497
|
+
"$@" &
|
|
3498
|
+
local child_pid=$!
|
|
3499
|
+
local elapsed=0
|
|
3500
|
+
local status=0
|
|
3501
|
+
while kill -0 "$child_pid" 2>/dev/null; do
|
|
3502
|
+
if (( elapsed >= timeout_seconds )); then
|
|
3503
|
+
pkill -TERM -P "$child_pid" 2>/dev/null || true
|
|
3504
|
+
kill "$child_pid" 2>/dev/null || true
|
|
3505
|
+
sleep 1
|
|
3506
|
+
pkill -KILL -P "$child_pid" 2>/dev/null || true
|
|
3507
|
+
kill -9 "$child_pid" 2>/dev/null || true
|
|
3508
|
+
wait "$child_pid" 2>/dev/null || true
|
|
3509
|
+
return 124
|
|
3510
|
+
fi
|
|
3511
|
+
sleep 1
|
|
3512
|
+
elapsed=$((elapsed + 1))
|
|
3513
|
+
done
|
|
3514
|
+
wait "$child_pid"
|
|
3515
|
+
status=$?
|
|
3516
|
+
return "$status"
|
|
3517
|
+
}
|
|
3518
|
+
|
|
2480
3519
|
doctor_copy_project() {
|
|
2481
3520
|
local dest="$1"
|
|
2482
3521
|
mkdir -p "$dest"
|
|
@@ -2490,14 +3529,14 @@ run_doctor_command() {
|
|
|
2490
3529
|
local work_dir="$2"
|
|
2491
3530
|
local output_file="$3"
|
|
2492
3531
|
local prompt
|
|
2493
|
-
prompt="$(
|
|
3532
|
+
prompt="$(doctor_prompt_for_agent "$agent_os")"
|
|
2494
3533
|
|
|
2495
3534
|
case "$agent_os" in
|
|
2496
3535
|
codex)
|
|
2497
|
-
codex exec --skip-git-repo-check --
|
|
3536
|
+
codex exec --skip-git-repo-check --ephemeral --sandbox workspace-write -C "$work_dir" "$prompt" </dev/null >"$output_file" 2>&1
|
|
2498
3537
|
;;
|
|
2499
3538
|
opencode)
|
|
2500
|
-
opencode run --dir "$work_dir" --dangerously-skip-permissions --format json --
|
|
3539
|
+
OPENCODE_DISABLE_AUTOUPDATE=1 opencode run --pure --dir "$work_dir" --dangerously-skip-permissions --format json --log-level ERROR "$prompt" >"$output_file" 2>&1
|
|
2501
3540
|
;;
|
|
2502
3541
|
claude)
|
|
2503
3542
|
(cd "$work_dir" && claude -p --permission-mode bypassPermissions --output-format stream-json "$prompt") >"$output_file" 2>&1
|
|
@@ -2522,29 +3561,40 @@ run_doctor_for_agent() {
|
|
|
2522
3561
|
return 1
|
|
2523
3562
|
fi
|
|
2524
3563
|
|
|
2525
|
-
local work_dir output_file status
|
|
3564
|
+
local work_dir output_file status timeout_seconds started_at elapsed smoke_label
|
|
2526
3565
|
work_dir="$doctor_root/$agent_os"
|
|
2527
3566
|
output_file="$doctor_root/$agent_os.log"
|
|
3567
|
+
timeout_seconds="$(doctor_timeout_seconds)"
|
|
3568
|
+
smoke_label="$(doctor_smoke_label "$agent_os")"
|
|
2528
3569
|
doctor_copy_project "$work_dir"
|
|
2529
3570
|
|
|
2530
3571
|
set +e
|
|
2531
|
-
|
|
3572
|
+
started_at="$(date +%s)"
|
|
3573
|
+
run_with_doctor_timeout "$timeout_seconds" run_doctor_command "$agent_os" "$work_dir" "$output_file"
|
|
2532
3574
|
status=$?
|
|
3575
|
+
elapsed=$(( $(date +%s) - started_at ))
|
|
2533
3576
|
set -e
|
|
2534
3577
|
|
|
3578
|
+
log "$agent_os doctor finished: timeout=${timeout_seconds}s exit=$status elapsed=${elapsed}s"
|
|
3579
|
+
|
|
2535
3580
|
log_file_block "doctor $agent_os" "$output_file"
|
|
2536
3581
|
|
|
3582
|
+
if [[ "$status" -eq 124 || "$status" -eq 137 ]]; then
|
|
3583
|
+
out "❌ $agent_os: $smoke_label timed out after ${timeout_seconds}s (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
3584
|
+
return 1
|
|
3585
|
+
fi
|
|
3586
|
+
|
|
2537
3587
|
if [[ "$status" -ne 0 ]]; then
|
|
2538
|
-
out "❌ $agent_os:
|
|
3588
|
+
out "❌ $agent_os: $smoke_label failed (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
2539
3589
|
return 1
|
|
2540
3590
|
fi
|
|
2541
3591
|
|
|
2542
3592
|
if doctor_output_has_fatal_patterns "$output_file"; then
|
|
2543
|
-
out "❌ $agent_os:
|
|
3593
|
+
out "❌ $agent_os: $smoke_label reported integration errors (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
2544
3594
|
return 1
|
|
2545
3595
|
fi
|
|
2546
3596
|
|
|
2547
|
-
out "✅ $agent_os:
|
|
3597
|
+
out "✅ $agent_os: $smoke_label passed (exit $status, elapsed ${elapsed}s)"
|
|
2548
3598
|
return 0
|
|
2549
3599
|
}
|
|
2550
3600
|
|
|
@@ -2572,6 +3622,7 @@ run_agentic_doctor() {
|
|
|
2572
3622
|
out
|
|
2573
3623
|
out "=== Agentic doctor ===" "$COLOR_HEADER"
|
|
2574
3624
|
out "Doctor temp root: $doctor_root"
|
|
3625
|
+
out "Doctor timeout: $(doctor_timeout_seconds)s per agent"
|
|
2575
3626
|
|
|
2576
3627
|
local failures=0
|
|
2577
3628
|
for agent_os in "${selected_doctor_agents[@]}"; do
|
|
@@ -2603,8 +3654,10 @@ run_install() {
|
|
|
2603
3654
|
ensure_dir "$PROJECT_DIR"
|
|
2604
3655
|
configure_opencode_plugins_if_needed
|
|
2605
3656
|
copy_extensions "$PROJECT_DIR"
|
|
3657
|
+
configure_opencode_agent_model_mapper_if_needed
|
|
2606
3658
|
copy_specialization_assets "$PROJECT_DIR"
|
|
2607
3659
|
generate_agents_md "$PROJECT_DIR"
|
|
3660
|
+
copy_memory_md "$PROJECT_DIR"
|
|
2608
3661
|
configure_context7_if_needed
|
|
2609
3662
|
configure_mempalace_if_needed
|
|
2610
3663
|
write_agentic_manifest "$PROJECT_DIR"
|
|
@@ -2681,6 +3734,84 @@ prompt_with_default_fzf() {
|
|
|
2681
3734
|
printf '%s\n' "$default"
|
|
2682
3735
|
}
|
|
2683
3736
|
|
|
3737
|
+
prompt_text_fzf() {
|
|
3738
|
+
local prompt="$1"
|
|
3739
|
+
local default="${2:-}"
|
|
3740
|
+
local header="Type value and press Enter to confirm"
|
|
3741
|
+
if [[ -n "$default" ]]; then
|
|
3742
|
+
header="$header (empty = current value)"
|
|
3743
|
+
fi
|
|
3744
|
+
local fzf_args=(
|
|
3745
|
+
--ansi
|
|
3746
|
+
--border
|
|
3747
|
+
--height=70%
|
|
3748
|
+
--layout=reverse
|
|
3749
|
+
--cycle
|
|
3750
|
+
--no-sort
|
|
3751
|
+
--phony
|
|
3752
|
+
--print-query
|
|
3753
|
+
--bind "enter:accept"
|
|
3754
|
+
--prompt "$prompt "
|
|
3755
|
+
--header "$header"
|
|
3756
|
+
)
|
|
3757
|
+
if [[ "${#FZF_COLOR_ARGS[@]}" -gt 0 ]]; then
|
|
3758
|
+
fzf_args+=("${FZF_COLOR_ARGS[@]}")
|
|
3759
|
+
fi
|
|
3760
|
+
|
|
3761
|
+
local output selected
|
|
3762
|
+
if ! output="$(printf '%s\n' "<press Enter to confirm>" | fzf "${fzf_args[@]}")"; then
|
|
3763
|
+
return 1
|
|
3764
|
+
fi
|
|
3765
|
+
selected="$(printf '%s\n' "$output" | sed -n '1p')"
|
|
3766
|
+
selected="$(trim "$selected")"
|
|
3767
|
+
if [[ -n "$selected" ]]; then
|
|
3768
|
+
printf '%s\n' "$selected"
|
|
3769
|
+
else
|
|
3770
|
+
printf '%s\n' "$default"
|
|
3771
|
+
fi
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
prompt_text_tui() {
|
|
3775
|
+
local prompt="$1"
|
|
3776
|
+
local default="${2:-}"
|
|
3777
|
+
local answer
|
|
3778
|
+
if [[ -n "$default" ]]; then
|
|
3779
|
+
read -r -p "$prompt [keep current]: " answer
|
|
3780
|
+
else
|
|
3781
|
+
read -r -p "$prompt: " answer
|
|
3782
|
+
fi
|
|
3783
|
+
answer="$(trim "$answer")"
|
|
3784
|
+
if [[ -n "$answer" ]]; then
|
|
3785
|
+
printf '%s\n' "$answer"
|
|
3786
|
+
else
|
|
3787
|
+
printf '%s\n' "$default"
|
|
3788
|
+
fi
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
prompt_text_interactive() {
|
|
3792
|
+
local prompt="$1"
|
|
3793
|
+
local default="${2:-}"
|
|
3794
|
+
if fzf_available; then
|
|
3795
|
+
prompt_text_fzf "$prompt" "$default"
|
|
3796
|
+
else
|
|
3797
|
+
prompt_text_tui "$prompt" "$default"
|
|
3798
|
+
fi
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
confirm_action_interactive() {
|
|
3802
|
+
local prompt="$1"
|
|
3803
|
+
local selected
|
|
3804
|
+
if fzf_available; then
|
|
3805
|
+
selected="$(choose_single_fzf "$prompt" "Confirm" "Cancel" || true)"
|
|
3806
|
+
[[ "$selected" == "Confirm" ]]
|
|
3807
|
+
return
|
|
3808
|
+
fi
|
|
3809
|
+
local answer
|
|
3810
|
+
read -r -p "$prompt [y/N]: " answer
|
|
3811
|
+
answer="$(trim "$answer")"
|
|
3812
|
+
[[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]]
|
|
3813
|
+
}
|
|
3814
|
+
|
|
2684
3815
|
choose_single_by_index() {
|
|
2685
3816
|
local prompt="$1"
|
|
2686
3817
|
shift
|
|
@@ -3237,6 +4368,45 @@ sync_current_project_after_upgrade() {
|
|
|
3237
4368
|
load_install_settings_from_manifest "$manifest"
|
|
3238
4369
|
ensure_repo_layout
|
|
3239
4370
|
run_install
|
|
4371
|
+
upgrade_mempalace_graph
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
upgrade_mempalace_graph() {
|
|
4375
|
+
local project_wing shared_docs_wing
|
|
4376
|
+
# Only run if mempalace was enabled for this project
|
|
4377
|
+
if [[ ! "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy] ]]; then
|
|
4378
|
+
return
|
|
4379
|
+
fi
|
|
4380
|
+
|
|
4381
|
+
if ! command -v mempalace >/dev/null 2>&1; then
|
|
4382
|
+
return
|
|
4383
|
+
fi
|
|
4384
|
+
|
|
4385
|
+
project_wing="$(mempalace_project_wing)"
|
|
4386
|
+
shared_docs_wing="$(mempalace_shared_docs_wing)"
|
|
4387
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
4388
|
+
log "DRY-RUN mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
|
|
4389
|
+
if [[ -d "$PROJECT_DIR/docs" ]]; then
|
|
4390
|
+
log "DRY-RUN mempalace mine \"$PROJECT_DIR/docs\" --wing \"$shared_docs_wing\""
|
|
4391
|
+
fi
|
|
4392
|
+
return
|
|
4393
|
+
fi
|
|
4394
|
+
|
|
4395
|
+
log "Refreshing MemPalace knowledge graph for $PROJECT_DIR (wing: $project_wing)"
|
|
4396
|
+
if mempalace mine "$PROJECT_DIR" --wing "$project_wing" >/dev/null 2>&1; then
|
|
4397
|
+
log "MemPalace graph updated"
|
|
4398
|
+
else
|
|
4399
|
+
warn "mempalace mine failed; graph may be stale — run manually: mempalace mine \"$PROJECT_DIR\" --wing \"$project_wing\""
|
|
4400
|
+
fi
|
|
4401
|
+
|
|
4402
|
+
if [[ -d "$PROJECT_DIR/docs" ]]; then
|
|
4403
|
+
log "Refreshing shared MemPalace docs wing from $PROJECT_DIR/docs"
|
|
4404
|
+
if mempalace mine "$PROJECT_DIR/docs" --wing "$shared_docs_wing" >/dev/null 2>&1; then
|
|
4405
|
+
log "MemPalace shared docs wing updated"
|
|
4406
|
+
else
|
|
4407
|
+
warn "mempalace docs mine failed; shared docs may be stale — run manually: mempalace mine \"$PROJECT_DIR/docs\" --wing \"$shared_docs_wing\""
|
|
4408
|
+
fi
|
|
4409
|
+
fi
|
|
3240
4410
|
}
|
|
3241
4411
|
|
|
3242
4412
|
parse_theme_option() {
|