@jetrabbits/agentic 0.1.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
@@ -15,7 +15,7 @@ APP_REPO_LINK="https://github.com/sawrus/agent-guides"
15
15
  PROJECT_MANIFEST_NAME=".agentic.json"
16
16
 
17
17
  DEFAULT_AGENT_OS="default"
18
- STATIC_AGENT_OS=(default opencode codex claude antigravity cursor agents)
18
+ STATIC_AGENT_OS=(opencode codex claude antigravity cursor kilocode)
19
19
  INSTALL_DIRS=(rules skills workflows prompts)
20
20
  THEME_CHOICES=(auto dark light)
21
21
 
@@ -55,6 +55,13 @@ SKIPPED_MANAGED_PATHS=()
55
55
  WARNINGS=()
56
56
 
57
57
  CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
58
+ AGENTIC_ENABLE_CONTEXT7="${AGENTIC_ENABLE_CONTEXT7:-}"
59
+ AGENTIC_DOCTOR="${AGENTIC_DOCTOR:-1}"
60
+ AGENTIC_DOCTOR_KEEP_TMP="${AGENTIC_DOCTOR_KEEP_TMP:-0}"
61
+ AGENTIC_DOCTOR_TIMEOUT_SECONDS="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
62
+
63
+ RUN_LOG_ACTIVE=false
64
+ RUN_LOG_FILE=""
58
65
 
59
66
  COLOR_RESET=""
60
67
  COLOR_HEADER=""
@@ -67,7 +74,7 @@ FZF_COLOR_ARGS=()
67
74
 
68
75
  usage() {
69
76
  cat <<USAGE
70
- $APP_TITLE
77
+ $APP_TITLE $(app_version_label)
71
78
 
72
79
  Usage:
73
80
  $SCRIPT_NAME list [agentos|areas|specs --area <name>]
@@ -75,6 +82,7 @@ Usage:
75
82
  $SCRIPT_NAME tui [--theme auto|dark|light]
76
83
  $SCRIPT_NAME upgrade
77
84
  $SCRIPT_NAME self-install [--bin-dir <dir>] [--force] [--install-fzf] [--dry-run]
85
+ $SCRIPT_NAME --version
78
86
 
79
87
  Behavior:
80
88
  - No arguments in interactive terminal: runs TUI mode
@@ -87,11 +95,13 @@ Options:
87
95
  --areas Comma-separated area list (example: software)
88
96
  --specializations Comma-separated specializations in area.spec format (example: software.backend,software.frontend)
89
97
  --theme Interface theme: auto|dark|light (default: config value or auto)
98
+ --no-doctor Skip real agent smoke checks after install
90
99
  --bin-dir Installation directory for self-install (default: ~/.local/bin)
91
100
  --force Overwrite existing binary for self-install
92
101
  --install-fzf During self-install, try to auto-install fzf (optional)
93
102
  --dry-run Show actions without writing files
94
103
  -h, --help Show this help
104
+ -V, --version Show agentic version
95
105
 
96
106
  Examples:
97
107
  $SCRIPT_NAME list agentos
@@ -102,6 +112,31 @@ Examples:
102
112
  USAGE
103
113
  }
104
114
 
115
+ read_package_version() {
116
+ local package_file="$1"
117
+ [[ -f "$package_file" ]] || return 1
118
+
119
+ sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$package_file" | head -n 1
120
+ }
121
+
122
+ app_version() {
123
+ local version=""
124
+ local candidate
125
+ for candidate in "$SCRIPT_DIR/package.json" "$REPO_ROOT/package.json" "$APP_REPO_DIR/package.json"; do
126
+ [[ -n "$candidate" ]] || continue
127
+ version="$(read_package_version "$candidate" || true)"
128
+ if [[ -n "$version" ]]; then
129
+ printf '%s\n' "$version"
130
+ return
131
+ fi
132
+ done
133
+ printf 'unknown\n'
134
+ }
135
+
136
+ app_version_label() {
137
+ printf 'v%s\n' "$(app_version)"
138
+ }
139
+
105
140
  is_interactive_terminal() {
106
141
  if [[ "${AGENTIC_FORCE_INTERACTIVE:-${AGENTOS_FORCE_INTERACTIVE:-}}" == "1" ]]; then
107
142
  return 0
@@ -217,16 +252,116 @@ set_theme_colors() {
217
252
  }
218
253
 
219
254
  log() {
220
- printf '%s[agentic]%s %s\n' "$COLOR_INFO" "$COLOR_RESET" "$1"
255
+ emit_log_line stdout "[agentic]" "$1" "$COLOR_INFO"
221
256
  }
222
257
 
223
258
  warn() {
224
- printf '%s[agentic][warn]%s %s\n' "$COLOR_WARN" "$COLOR_RESET" "$1"
259
+ emit_log_line stdout "[agentic][warn]" "$1" "$COLOR_WARN"
225
260
  WARNINGS+=("$1")
226
261
  }
227
262
 
228
263
  error() {
229
- printf '%s[agentic][error]%s %s\n' "$COLOR_ERROR" "$COLOR_RESET" "$1" >&2
264
+ emit_log_line stderr "[agentic][error]" "$1" "$COLOR_ERROR"
265
+ }
266
+
267
+ timestamp_now() {
268
+ date '+%Y-%m-%d %H:%M:%S'
269
+ }
270
+
271
+ init_run_logging() {
272
+ if [[ "$RUN_LOG_ACTIVE" == true ]]; then
273
+ return
274
+ fi
275
+
276
+ local base_dir="${TMPDIR:-/tmp}"
277
+ local stamp
278
+ stamp="$(date '+%Y%m%d-%H%M%S')"
279
+ RUN_LOG_FILE="$(mktemp "$base_dir/agentic-$stamp.XXXXXX")"
280
+ RUN_LOG_ACTIVE=true
281
+ log "Run log initialized: $RUN_LOG_FILE"
282
+ }
283
+
284
+ write_run_log_line() {
285
+ local line="$1"
286
+ if [[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" ]]; then
287
+ printf '%s\n' "$line" >> "$RUN_LOG_FILE"
288
+ fi
289
+ }
290
+
291
+ emit_log_line() {
292
+ local stream="$1"
293
+ local tag="$2"
294
+ local message="$3"
295
+ local color="${4:-}"
296
+
297
+ local line plain_line ts
298
+ if [[ "$RUN_LOG_ACTIVE" == true ]]; then
299
+ ts="$(timestamp_now)"
300
+ plain_line="$ts $tag $message"
301
+ if [[ -n "$color" ]]; then
302
+ line="$ts ${color}${tag}${COLOR_RESET} $message"
303
+ else
304
+ line="$plain_line"
305
+ fi
306
+ else
307
+ plain_line="$tag $message"
308
+ if [[ -n "$color" ]]; then
309
+ line="${color}${tag}${COLOR_RESET} $message"
310
+ else
311
+ line="$plain_line"
312
+ fi
313
+ fi
314
+
315
+ if [[ "$stream" == "stderr" ]]; then
316
+ printf '%s\n' "$line" >&2
317
+ else
318
+ printf '%s\n' "$line"
319
+ fi
320
+ write_run_log_line "$plain_line"
321
+ }
322
+
323
+ out() {
324
+ local message="${1:-}"
325
+ local color="${2:-}"
326
+ local line plain_line ts
327
+
328
+ if [[ -z "$message" ]]; then
329
+ printf '\n'
330
+ write_run_log_line ""
331
+ return
332
+ fi
333
+
334
+ if [[ "$RUN_LOG_ACTIVE" == true ]]; then
335
+ ts="$(timestamp_now)"
336
+ plain_line="$ts $message"
337
+ if [[ -n "$color" ]]; then
338
+ line="$ts ${color}${message}${COLOR_RESET}"
339
+ else
340
+ line="$plain_line"
341
+ fi
342
+ else
343
+ plain_line="$message"
344
+ if [[ -n "$color" ]]; then
345
+ line="${color}${message}${COLOR_RESET}"
346
+ else
347
+ line="$plain_line"
348
+ fi
349
+ fi
350
+
351
+ printf '%s\n' "$line"
352
+ write_run_log_line "$plain_line"
353
+ }
354
+
355
+ log_file_block() {
356
+ local label="$1"
357
+ local path="$2"
358
+ [[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" && -f "$path" ]] || return 0
359
+
360
+ write_run_log_line "$(timestamp_now) --- $label output begin ---"
361
+ while IFS= read -r line || [[ -n "$line" ]]; do
362
+ write_run_log_line "$(timestamp_now) $line"
363
+ done < "$path"
364
+ write_run_log_line "$(timestamp_now) --- $label output end ---"
230
365
  }
231
366
 
232
367
  unique_append() {
@@ -519,6 +654,42 @@ ensure_python_available() {
519
654
  fi
520
655
  }
521
656
 
657
+ pip_command() {
658
+ if command -v pip >/dev/null 2>&1; then
659
+ printf '%s\n' "pip"
660
+ return 0
661
+ fi
662
+ if command -v pip3 >/dev/null 2>&1; then
663
+ printf '%s\n' "pip3"
664
+ return 0
665
+ fi
666
+ if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then
667
+ printf '%s\n' "python3 -m pip"
668
+ return 0
669
+ fi
670
+ return 1
671
+ }
672
+
673
+ ensure_pip_available() {
674
+ if ! pip_command >/dev/null; then
675
+ error "pip is required to run agentic install/tui. Install pip for Python 3 and make 'pip3', 'pip', or 'python3 -m pip' available."
676
+ exit 1
677
+ fi
678
+ }
679
+
680
+ ensure_hash_available() {
681
+ if ! command -v shasum >/dev/null 2>&1 && ! command -v sha256sum >/dev/null 2>&1; then
682
+ error "shasum or sha256sum is required to track managed files"
683
+ exit 1
684
+ fi
685
+ }
686
+
687
+ ensure_agentic_runtime_requirements() {
688
+ ensure_python_available
689
+ ensure_pip_available
690
+ ensure_hash_available
691
+ }
692
+
522
693
  selected_agent_os_contains() {
523
694
  local expected="$1"
524
695
  local agent
@@ -547,8 +718,7 @@ hash_file() {
547
718
  elif command -v sha256sum >/dev/null 2>&1; then
548
719
  sha256sum "$path" | awk '{print $1}'
549
720
  else
550
- error "shasum or sha256sum is required to track managed files"
551
- exit 1
721
+ ensure_hash_available
552
722
  fi
553
723
  }
554
724
 
@@ -634,12 +804,15 @@ register_managed_file() {
634
804
  local dest="$1"
635
805
  local source_ref="$2"
636
806
  local marker="$3"
807
+ local copied="${4:-true}"
637
808
  local rel
638
809
  rel="$(project_rel_path "$dest")"
639
810
  local digest
640
811
  digest="$(hash_file "$dest")"
641
812
  MANAGED_RECORDS+=("$rel|$source_ref|$digest|$marker")
642
- unique_append "$dest" COPIED_PATHS
813
+ if [[ "$copied" == true ]]; then
814
+ unique_append "$dest" COPIED_PATHS
815
+ fi
643
816
  }
644
817
 
645
818
  record_agentic_event() {
@@ -679,8 +852,10 @@ write_file_with_agentic_marker() {
679
852
  can_write_managed_file "$dest" || return 0
680
853
 
681
854
  ensure_dir "$(dirname -- "$dest")"
682
- python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" <<'PY'
855
+ local write_status
856
+ write_status="$(python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" "$(app_version_label)" <<'PY'
683
857
  import json
858
+ import re
684
859
  import sys
685
860
  from pathlib import Path
686
861
 
@@ -688,6 +863,7 @@ src = Path(sys.argv[1])
688
863
  dest = Path(sys.argv[2])
689
864
  source_ref = sys.argv[3]
690
865
  repo = sys.argv[4]
866
+ version = sys.argv[5]
691
867
  text = src.read_text(encoding="utf-8")
692
868
  suffix = dest.suffix.lower()
693
869
  marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
@@ -697,12 +873,28 @@ def yaml_quote(value: str) -> str:
697
873
  return json.dumps(value, ensure_ascii=False)
698
874
 
699
875
 
876
+ def existing_created_by() -> str:
877
+ if not dest.exists():
878
+ return version
879
+ try:
880
+ old = dest.read_text(encoding="utf-8")
881
+ except Exception:
882
+ return version
883
+ match = re.search(r"(?m)^ created_by:\s*(.+?)\s*$", old)
884
+ if not match:
885
+ return version
886
+ return match.group(1).strip().strip('"')
887
+
888
+
700
889
  def markdown_with_marker(body: str) -> str:
890
+ created_by = existing_created_by()
701
891
  block = (
702
892
  "agentic:\n"
703
893
  " generated_by: agentic\n"
704
894
  f" source: {yaml_quote(source_ref)}\n"
705
895
  f" repository: {yaml_quote(repo)}\n"
896
+ f" created_by: {yaml_quote(created_by)}\n"
897
+ f" updated_by: {yaml_quote(version)}\n"
706
898
  )
707
899
  if body.startswith("---\n"):
708
900
  end = body.find("\n---", 4)
@@ -739,9 +931,23 @@ elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
739
931
  else:
740
932
  output = commented(text, "#")
741
933
 
934
+ if dest.exists():
935
+ try:
936
+ if dest.read_text(encoding="utf-8") == output:
937
+ print("unchanged")
938
+ raise SystemExit(0)
939
+ except UnicodeDecodeError:
940
+ pass
941
+
742
942
  dest.write_text(output, encoding="utf-8")
943
+ print("written")
743
944
  PY
744
- register_managed_file "$dest" "$source_ref" "internal"
945
+ )"
946
+ if [[ "$write_status" == "unchanged" ]]; then
947
+ register_managed_file "$dest" "$source_ref" "internal" false
948
+ else
949
+ register_managed_file "$dest" "$source_ref" "internal"
950
+ fi
745
951
  }
746
952
 
747
953
  write_agentic_manifest() {
@@ -767,7 +973,7 @@ write_agentic_manifest() {
767
973
  : > "$skipped_file"
768
974
  fi
769
975
 
770
- local agent_os_csv areas_csv specs_csv
976
+ local agent_os_csv areas_csv specs_csv mcp_integrations_csv
771
977
  local old_ifs="$IFS"
772
978
  IFS=,
773
979
  agent_os_csv="${SELECTED_AGENT_OS[*]}"
@@ -775,7 +981,21 @@ write_agentic_manifest() {
775
981
  specs_csv="${SELECTED_SPECS[*]}"
776
982
  IFS="$old_ifs"
777
983
 
778
- python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" <<'PY'
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
+
997
+ local manifest_status
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'
779
999
  import json
780
1000
  import sys
781
1001
  from datetime import datetime, timezone
@@ -789,37 +1009,56 @@ repo_root = sys.argv[5]
789
1009
  agent_os = [x for x in sys.argv[6].split(",") if x]
790
1010
  areas = [x for x in sys.argv[7].split(",") if x]
791
1011
  specs = [x for x in sys.argv[8].split(",") if x]
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 []
792
1014
  now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
793
1015
 
794
1016
  existing = {}
795
1017
  created_at = now
1018
+ old_data = None
796
1019
  if manifest.exists():
797
1020
  try:
798
1021
  old = json.loads(manifest.read_text(encoding="utf-8"))
1022
+ old_data = old
799
1023
  created_at = old.get("created_at", created_at)
800
1024
  for item in old.get("managed_files", []):
801
1025
  if item.get("path"):
802
1026
  existing[item["path"]] = item
803
1027
  except Exception:
804
1028
  existing = {}
1029
+ original_existing = json.loads(json.dumps(existing))
805
1030
 
806
1031
  for line in records_file.read_text(encoding="utf-8").splitlines():
807
1032
  if not line:
808
1033
  continue
809
1034
  path, source, digest, marker = (line.split("|", 3) + ["", "", "", ""])[:4]
1035
+ old_item = original_existing.get(path, {})
1036
+ old_updated_at = old_item.get("updated_at", now)
1037
+ if (
1038
+ old_item.get("source") == source
1039
+ and old_item.get("content_hash") == digest
1040
+ and old_item.get("marker") == marker
1041
+ ):
1042
+ item_updated_at = old_updated_at
1043
+ else:
1044
+ item_updated_at = now
810
1045
  existing[path] = {
811
1046
  "path": path,
812
1047
  "source": source,
813
1048
  "content_hash": digest,
814
1049
  "marker": marker,
815
- "updated_at": now,
1050
+ "updated_at": item_updated_at,
816
1051
  }
817
1052
 
818
1053
  skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
1054
+ old_agentic = old_data.get("_agentic", {}) if isinstance(old_data, dict) else {}
1055
+ created_by = old_agentic.get("created_by", app_version)
819
1056
  data = {
820
1057
  "_agentic": {
821
1058
  "generated_by": "agentic",
822
1059
  "repository": repo_link,
1060
+ "created_by": created_by,
1061
+ "updated_by": app_version,
823
1062
  },
824
1063
  "version": 1,
825
1064
  "created_at": created_at,
@@ -828,15 +1067,33 @@ data = {
828
1067
  "agent_os": agent_os,
829
1068
  "areas": areas,
830
1069
  "specializations": specs,
1070
+ "mcp_integrations": mcp_integrations,
831
1071
  "source_repo": repo_link,
832
1072
  "source_checkout": repo_root,
833
1073
  },
834
1074
  "managed_files": sorted(existing.values(), key=lambda x: x["path"]),
835
1075
  "skipped_files": skipped,
836
1076
  }
1077
+
1078
+ if old_data is not None:
1079
+ old_compare = json.loads(json.dumps(old_data))
1080
+ new_compare = json.loads(json.dumps(data))
1081
+ for payload in (old_compare, new_compare):
1082
+ payload.pop("updated_at", None)
1083
+ if isinstance(payload.get("_agentic"), dict):
1084
+ payload["_agentic"].pop("updated_by", None)
1085
+ if old_compare == new_compare:
1086
+ print("unchanged")
1087
+ raise SystemExit(0)
1088
+
837
1089
  manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1090
+ print("written")
838
1091
  PY
1092
+ )"
839
1093
  rm -f "$records_file" "$skipped_file"
1094
+ if [[ "$manifest_status" == "unchanged" ]]; then
1095
+ return
1096
+ fi
840
1097
  unique_append "$manifest" COPIED_PATHS
841
1098
  }
842
1099
 
@@ -853,7 +1110,7 @@ from pathlib import Path
853
1110
 
854
1111
  data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
855
1112
  settings = data.get("settings", {})
856
- for key in ("agent_os", "areas", "specializations"):
1113
+ for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
857
1114
  print("::" + key)
858
1115
  for value in settings.get(key, []):
859
1116
  print(value)
@@ -864,17 +1121,20 @@ PY
864
1121
  local loaded_agent_os=()
865
1122
  local loaded_areas=()
866
1123
  local loaded_specs=()
1124
+ local loaded_mcp_integrations=()
867
1125
  local value
868
1126
  for value in "${values[@]}"; do
869
1127
  case "$value" in
870
1128
  "::agent_os") section="agent_os" ;;
871
1129
  "::areas") section="areas" ;;
872
1130
  "::specializations") section="specializations" ;;
1131
+ "::mcp_integrations") section="mcp_integrations" ;;
873
1132
  *)
874
1133
  case "$section" in
875
1134
  agent_os) loaded_agent_os+=("$value") ;;
876
1135
  areas) loaded_areas+=("$value") ;;
877
1136
  specializations) loaded_specs+=("$value") ;;
1137
+ mcp_integrations) loaded_mcp_integrations+=("$value") ;;
878
1138
  esac
879
1139
  ;;
880
1140
  esac
@@ -889,6 +1149,23 @@ PY
889
1149
  if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
890
1150
  SELECTED_SPECS=("${loaded_specs[@]}")
891
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
892
1169
  }
893
1170
 
894
1171
  path_ref_for_shell_export() {
@@ -976,9 +1253,10 @@ copy_dir_contents() {
976
1253
 
977
1254
  local event kind value events_file
978
1255
  events_file="$(mktemp "${TMPDIR:-/tmp}/agentic-copy-events.XXXXXX")"
979
- python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" > "$events_file" <<'PY'
1256
+ python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" "$(app_version_label)" > "$events_file" <<'PY'
980
1257
  import hashlib
981
1258
  import json
1259
+ import re
982
1260
  import sys
983
1261
  from pathlib import Path
984
1262
 
@@ -988,6 +1266,7 @@ repo_root = Path(sys.argv[3])
988
1266
  project_dir = Path(sys.argv[4])
989
1267
  manifest = Path(sys.argv[5])
990
1268
  repo = sys.argv[6]
1269
+ version = sys.argv[7]
991
1270
 
992
1271
 
993
1272
  def emit(kind: str, value: str) -> None:
@@ -1010,7 +1289,7 @@ if manifest.exists():
1010
1289
  for item in data.get("managed_files", []):
1011
1290
  rel = item.get("path")
1012
1291
  if rel:
1013
- managed[rel] = item.get("content_hash", "")
1292
+ managed[rel] = item
1014
1293
  except Exception:
1015
1294
  managed = {}
1016
1295
 
@@ -1033,12 +1312,28 @@ def yaml_quote(value: str) -> str:
1033
1312
  return json.dumps(value, ensure_ascii=False)
1034
1313
 
1035
1314
 
1036
- def markdown_with_marker(body: str, source_ref: str) -> str:
1315
+ def existing_created_by(target: Path) -> str:
1316
+ if not target.exists():
1317
+ return version
1318
+ try:
1319
+ old = target.read_text(encoding="utf-8")
1320
+ except Exception:
1321
+ return version
1322
+ match = re.search(r"(?m)^ created_by:\s*(.+?)\s*$", old)
1323
+ if not match:
1324
+ return version
1325
+ return match.group(1).strip().strip('"')
1326
+
1327
+
1328
+ def markdown_with_marker(body: str, source_ref: str, target: Path) -> str:
1329
+ created_by = existing_created_by(target)
1037
1330
  block = (
1038
1331
  "agentic:\n"
1039
1332
  " generated_by: agentic\n"
1040
1333
  f" source: {yaml_quote(source_ref)}\n"
1041
1334
  f" repository: {yaml_quote(repo)}\n"
1335
+ f" created_by: {yaml_quote(created_by)}\n"
1336
+ f" updated_by: {yaml_quote(version)}\n"
1042
1337
  )
1043
1338
  if body.startswith("---\n"):
1044
1339
  end = body.find("\n---", 4)
@@ -1061,7 +1356,7 @@ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
1061
1356
  text = file_path.read_text(encoding="utf-8")
1062
1357
  suffix = target.suffix.lower()
1063
1358
  if suffix == ".md":
1064
- return markdown_with_marker(text, source_ref)
1359
+ return markdown_with_marker(text, source_ref, target)
1065
1360
  if suffix == ".json":
1066
1361
  data = json.loads(text)
1067
1362
  if not isinstance(data, dict):
@@ -1087,15 +1382,27 @@ for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
1087
1382
  emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
1088
1383
  emit("SKIP", project_rel)
1089
1384
  continue
1090
- expected_hash = managed.get(project_rel, "")
1385
+ managed_item = managed.get(project_rel, {})
1386
+ if managed_item.get("marker") == "config":
1387
+ continue
1388
+ expected_hash = managed_item.get("content_hash", "")
1091
1389
  if target.exists() and expected_hash and sha256(target) != expected_hash:
1092
1390
  emit("WARN", f"Skipping user-modified managed file: {project_rel}")
1093
1391
  emit("SKIP", project_rel)
1094
1392
  continue
1095
1393
 
1394
+ output = add_marker(file_path, target, source_ref)
1096
1395
  target.parent.mkdir(parents=True, exist_ok=True)
1097
1396
  emit("DIR", str(target.parent))
1098
- target.write_text(add_marker(file_path, target, source_ref), encoding="utf-8")
1397
+ if target.exists():
1398
+ try:
1399
+ if target.read_text(encoding="utf-8") == output:
1400
+ digest = sha256(target)
1401
+ emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
1402
+ continue
1403
+ except UnicodeDecodeError:
1404
+ pass
1405
+ target.write_text(output, encoding="utf-8")
1099
1406
  digest = sha256(target)
1100
1407
  emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
1101
1408
  emit("COPIED", str(target))
@@ -1146,7 +1453,8 @@ write_json_file_with_agentic_metadata() {
1146
1453
 
1147
1454
  can_write_managed_file "$dest" || return 0
1148
1455
  ensure_dir "$(dirname -- "$dest")"
1149
- python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1456
+ local write_status
1457
+ write_status="$(python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" "$(app_version_label)" <<'PY'
1150
1458
  import json
1151
1459
  import sys
1152
1460
  from pathlib import Path
@@ -1156,11 +1464,15 @@ source_ref = sys.argv[2]
1156
1464
  repo = sys.argv[3]
1157
1465
  context7_api_key = sys.argv[4]
1158
1466
  body = sys.argv[5]
1467
+ version = sys.argv[6]
1159
1468
 
1160
1469
  data = {}
1470
+ created_by = version
1161
1471
  if path.exists():
1162
1472
  try:
1163
1473
  data = json.loads(path.read_text(encoding="utf-8"))
1474
+ if isinstance(data, dict):
1475
+ created_by = data.get("_agentic", {}).get("created_by", version)
1164
1476
  except Exception:
1165
1477
  data = {}
1166
1478
  if not isinstance(data, dict):
@@ -1172,9 +1484,28 @@ namespace = {
1172
1484
  }
1173
1485
  exec(body, namespace)
1174
1486
  data = namespace["data"]
1487
+ metadata = data.setdefault("_agentic", {})
1488
+ metadata["generated_by"] = "agentic"
1489
+ metadata["repository"] = repo
1490
+ metadata["created_by"] = created_by
1491
+ metadata["updated_by"] = version
1492
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
1493
+ if path.exists():
1494
+ try:
1495
+ if path.read_text(encoding="utf-8") == output:
1496
+ print("unchanged")
1497
+ raise SystemExit(0)
1498
+ except UnicodeDecodeError:
1499
+ pass
1175
1500
  path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1501
+ print("written")
1176
1502
  PY
1177
- register_managed_file "$dest" "$source_ref" "internal"
1503
+ )"
1504
+ if [[ "$write_status" == "unchanged" ]]; then
1505
+ register_managed_file "$dest" "$source_ref" "internal" false
1506
+ else
1507
+ register_managed_file "$dest" "$source_ref" "internal"
1508
+ fi
1178
1509
  }
1179
1510
 
1180
1511
  write_json_config_file() {
@@ -1190,7 +1521,8 @@ write_json_config_file() {
1190
1521
 
1191
1522
  can_write_managed_file "$dest" || return 0
1192
1523
  ensure_dir "$(dirname -- "$dest")"
1193
- python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1524
+ local write_status
1525
+ write_status="$(python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1194
1526
  import json
1195
1527
  import sys
1196
1528
  from pathlib import Path
@@ -1214,9 +1546,62 @@ namespace = {
1214
1546
  }
1215
1547
  exec(body, namespace)
1216
1548
  data = namespace["data"]
1217
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1549
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
1550
+ if path.exists():
1551
+ try:
1552
+ if path.read_text(encoding="utf-8") == output:
1553
+ print("unchanged")
1554
+ raise SystemExit(0)
1555
+ except UnicodeDecodeError:
1556
+ pass
1557
+ path.write_text(output, encoding="utf-8")
1558
+ print("written")
1559
+ PY
1560
+ )"
1561
+ if [[ "$write_status" == "unchanged" ]]; then
1562
+ register_managed_file "$dest" "$source_ref" "config" false
1563
+ else
1564
+ register_managed_file "$dest" "$source_ref" "config"
1565
+ fi
1566
+ }
1567
+
1568
+ write_text_config_file() {
1569
+ local dest="$1"
1570
+ local source_ref="$2"
1571
+ local content="$3"
1572
+
1573
+ if [[ "$DRY_RUN" == true ]]; then
1574
+ log "DRY-RUN write text config file $dest"
1575
+ unique_append "$dest" COPIED_PATHS
1576
+ return
1577
+ fi
1578
+
1579
+ can_write_managed_file "$dest" || return 0
1580
+ ensure_dir "$(dirname -- "$dest")"
1581
+
1582
+ local write_status
1583
+ write_status="$(python3 - "$dest" "$content" <<'PY'
1584
+ import sys
1585
+ from pathlib import Path
1586
+
1587
+ path = Path(sys.argv[1])
1588
+ content = sys.argv[2]
1589
+ if path.exists():
1590
+ try:
1591
+ if path.read_text(encoding="utf-8") == content:
1592
+ print("unchanged")
1593
+ raise SystemExit(0)
1594
+ except UnicodeDecodeError:
1595
+ pass
1596
+ path.write_text(content, encoding="utf-8")
1597
+ print("written")
1218
1598
  PY
1219
- register_managed_file "$dest" "$source_ref" "config"
1599
+ )"
1600
+ if [[ "$write_status" == "unchanged" ]]; then
1601
+ register_managed_file "$dest" "$source_ref" "config" false
1602
+ else
1603
+ register_managed_file "$dest" "$source_ref" "config"
1604
+ fi
1220
1605
  }
1221
1606
 
1222
1607
  write_context7_opencode_config() {
@@ -1250,22 +1635,32 @@ if context7_api_key:
1250
1635
  context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1251
1636
  mcp["context7"] = context7
1252
1637
  '
1253
- write_json_file_with_agentic_metadata "$dest" "generated:context7-opencode-legacy-config" "$body"
1638
+ write_json_config_file "$dest" "generated:context7-opencode-legacy-config" "$body"
1254
1639
  }
1255
1640
 
1256
1641
  write_context7_codex_config() {
1257
1642
  local dest="$PROJECT_DIR/.codex/config.toml"
1258
- local headers=""
1259
- if [[ -n "$CONTEXT7_API_KEY" ]]; then
1260
- local escaped_key
1261
- escaped_key="${CONTEXT7_API_KEY//\\/\\\\}"
1262
- escaped_key="${escaped_key//\"/\\\"}"
1263
- headers="http_headers = { \"CONTEXT7_API_KEY\" = \"$escaped_key\" }
1264
- "
1265
- fi
1266
- write_generated_text_file "$dest" "generated:context7-codex-config" "[mcp_servers.context7]
1267
- url = \"https://mcp.context7.com/mcp\"
1268
- ${headers}"
1643
+ local body
1644
+ body="$(python3 - "$dest" "$CONTEXT7_API_KEY" <<'PY'
1645
+ import re
1646
+ import sys
1647
+ from pathlib import Path
1648
+
1649
+ path = Path(sys.argv[1])
1650
+ api_key = sys.argv[2]
1651
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
1652
+ text = re.sub(r"(?ms)^\[mcp_servers\.context7\]\n.*?(?=^\[|\Z)", "", text).strip()
1653
+ block = '[mcp_servers.context7]\nurl = "https://mcp.context7.com/mcp"\n'
1654
+ if api_key:
1655
+ escaped = api_key.replace("\\", "\\\\").replace('"', '\\"')
1656
+ block += f'http_headers = {{ "CONTEXT7_API_KEY" = "{escaped}" }}\n'
1657
+ if text:
1658
+ print(block + "\n" + text.rstrip() + "\n", end="")
1659
+ else:
1660
+ print(block, end="")
1661
+ PY
1662
+ )"
1663
+ write_text_config_file "$dest" "generated:context7-codex-config" "$body"
1269
1664
  }
1270
1665
 
1271
1666
  write_context7_claude_config() {
@@ -1345,12 +1740,49 @@ mcp_servers["context7"] = context7
1345
1740
  write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
1346
1741
  }
1347
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
+
1348
1780
  write_mempalace_opencode_config() {
1349
1781
  local dest="$1"
1350
1782
  local body
1351
1783
  body='
1352
1784
  mcp = data.setdefault("mcp", {})
1353
- mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp", "--palace", ".mempalace"]}
1785
+ mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"]}
1354
1786
  '
1355
1787
  write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
1356
1788
  }
@@ -1359,18 +1791,21 @@ write_mempalace_codex_config() {
1359
1791
  local dest="$PROJECT_DIR/.codex/config.toml"
1360
1792
  local body
1361
1793
  body="$(python3 - "$dest" <<'PYCODE'
1362
- import pathlib, sys
1794
+ import re
1795
+ import pathlib
1796
+ import sys
1797
+
1363
1798
  path = pathlib.Path(sys.argv[1])
1364
1799
  text = path.read_text(encoding='utf-8') if path.exists() else ''
1365
- block = "[mcp_servers.mempalace]\ncommand = \"mempalace-mcp\"\nargs = [\"--palace\", \".mempalace\"]\n"
1366
- if block not in text:
1367
- if text and not text.endswith("\n"):
1368
- text += "\n"
1369
- text += block
1370
- print(text, end="")
1800
+ block = "[mcp_servers.mempalace]\ncommand = \"mempalace-mcp\"\n"
1801
+ text = re.sub(r"(?ms)^\[mcp_servers\.mempalace\]\n.*?(?=^\[|\Z)", "", text).strip()
1802
+ if text:
1803
+ print(text.rstrip() + "\n\n" + block, end="")
1804
+ else:
1805
+ print(block, end="")
1371
1806
  PYCODE
1372
1807
  )"
1373
- write_generated_text_file "$dest" "generated:mempalace-codex-config" "$body"
1808
+ write_text_config_file "$dest" "generated:mempalace-codex-config" "$body"
1374
1809
  }
1375
1810
 
1376
1811
  write_mempalace_generic_json_config() {
@@ -1379,74 +1814,204 @@ write_mempalace_generic_json_config() {
1379
1814
  local body
1380
1815
  body='
1381
1816
  servers = data.setdefault("mcpServers", {})
1382
- servers["mempalace"] = {"command": "mempalace-mcp", "args": ["--palace", ".mempalace"]}
1817
+ servers["mempalace"] = {"command": "mempalace-mcp"}
1383
1818
  '
1384
1819
  write_json_config_file "$dest" "$marker" "$body"
1385
1820
  }
1386
1821
 
1387
1822
  print_mempalace_project_setup_instructions() {
1388
- log "MemPalace setup instructions for target project: $PROJECT_DIR"
1389
- cat <<EOF
1390
- 1) Ensure Python is installed and available in PATH.
1391
- 2) Install MemPalace:
1392
- pip install mempalace
1393
- 3) Initialize project-local MemPalace cache:
1394
- mkdir -p "$PROJECT_DIR/.mempalace"
1395
- mempalace init "$PROJECT_DIR/.mempalace" --yes --auto-mine
1396
- 4) Index existing project memory:
1397
- # optional if --auto-mine was skipped
1398
- mempalace mine "$PROJECT_DIR/.mempalace"
1399
- 5) Verify in your IDE/agent that MemPalace MCP tools are connected.
1400
- Note: Ollama at localhost:11434 is optional; MemPalace can run heuristics-only without it.
1401
- EOF
1402
- }
1403
-
1404
- setup_mempalace_for_agentic_opencode() {
1823
+ log "Optional MemPalace project indexing instructions for target project: $PROJECT_DIR"
1824
+ out "1) Ensure Python is installed and available in PATH."
1825
+ out "2) Install MemPalace:"
1826
+ out " pip install mempalace"
1827
+ out "3) Optionally initialize project-local MemPalace cache:"
1828
+ out " mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
1829
+ out "4) Optionally index existing project memory:"
1830
+ out " # optional if --auto-mine was skipped"
1831
+ out " mempalace mine \"$PROJECT_DIR\""
1832
+ out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
1833
+ out "Note: Ollama at localhost:11434 is optional; MemPalace can run heuristics-only without it."
1834
+ }
1835
+
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}"
1405
1968
  local step_prefix="MemPalace setup"
1406
1969
 
1407
1970
  log "$step_prefix [1/4] Checking Python availability"
1408
1971
  if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
1409
1972
  warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
1410
1973
  warn "Install help: https://www.python.org/downloads/"
1974
+ print_mempalace_project_setup_instructions
1411
1975
  return 1
1412
1976
  fi
1413
1977
  log "$step_prefix [1/4] Python check passed"
1414
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
+
1415
1991
  log "$step_prefix [2/4] Checking pip availability"
1416
- if ! command -v pip >/dev/null 2>&1 && ! command -v pip3 >/dev/null 2>&1; then
1992
+ local pip_bin
1993
+ if ! pip_bin="$(pip_command)"; then
1417
1994
  warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
1995
+ print_mempalace_project_setup_instructions
1418
1996
  return 1
1419
1997
  fi
1420
1998
  log "$step_prefix [2/4] pip check passed"
1421
1999
 
1422
2000
  log "$step_prefix [3/4] Installing mempalace package"
1423
- if pip install mempalace >/dev/null 2>&1; then
1424
- log "MemPalace package installed via 'pip install mempalace'"
2001
+ if $pip_bin install mempalace >/dev/null 2>&1; then
2002
+ log "MemPalace package installed via '$pip_bin install mempalace'"
1425
2003
  else
1426
2004
  warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions"
1427
2005
  print_mempalace_project_setup_instructions
1428
2006
  return 1
1429
2007
  fi
1430
2008
 
1431
- log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR/.mempalace"
1432
- if command -v mempalace >/dev/null 2>&1; then
1433
- mkdir -p "$PROJECT_DIR/.mempalace" || warn "Failed: mkdir -p \"$PROJECT_DIR/.mempalace\""
1434
- if mempalace init "$PROJECT_DIR/.mempalace" --yes --auto-mine >/dev/null 2>&1; then
1435
- log "MemPalace init completed"
1436
- else
1437
- warn "Failed: mempalace init \"$PROJECT_DIR/.mempalace\" --yes --auto-mine"
1438
- fi
1439
- if mempalace mine "$PROJECT_DIR/.mempalace" >/dev/null 2>&1; then
1440
- log "MemPalace mine completed"
1441
- else
1442
- warn "Failed: mempalace mine \"$PROJECT_DIR/.mempalace\""
1443
- fi
1444
- log "$step_prefix [4/4] Initialization step finished"
1445
- else
1446
- warn "mempalace command is unavailable after install; please run setup manually"
1447
- print_mempalace_project_setup_instructions
1448
- 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
1449
2012
  fi
2013
+
2014
+ initialize_mempalace_project "$step_prefix"
1450
2015
  }
1451
2016
 
1452
2017
  configure_mempalace_if_needed() {
@@ -1455,6 +2020,7 @@ configure_mempalace_if_needed() {
1455
2020
  && ! selected_agent_os_contains "claude" \
1456
2021
  && ! selected_agent_os_contains "cursor" \
1457
2022
  && ! selected_agent_os_contains "gemini" \
2023
+ && ! selected_agent_os_contains "kilocode" \
1458
2024
  && ! selected_agent_os_contains "antigravity"; then
1459
2025
  return
1460
2026
  fi
@@ -1464,28 +2030,26 @@ configure_mempalace_if_needed() {
1464
2030
  enable_mempalace="$(trim "${AGENTIC_ENABLE_MEMPALACE}")"
1465
2031
  elif is_interactive_terminal && [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]]; then
1466
2032
  read -r -p "Enable MemPalace MCP memory integration? [y/N]: " enable_mempalace
1467
- enable_mempalace="$(trim "${enable_mempalace:-y}")"
1468
- if [[ -z "$enable_mempalace" ]]; then enable_mempalace="y"; fi
2033
+ enable_mempalace="$(trim "${enable_mempalace:-n}")"
2034
+ if [[ -z "$enable_mempalace" ]]; then enable_mempalace="n"; fi
1469
2035
  fi
1470
2036
  if [[ "$enable_mempalace" =~ ^[Nn]$ ]]; then
1471
2037
  log "Skipped MemPalace MCP configuration"
1472
2038
  return
1473
2039
  fi
1474
2040
 
1475
- if selected_agent_os_contains "opencode"; then
1476
- setup_mempalace_for_agentic_opencode || true
1477
- else
1478
- print_mempalace_project_setup_instructions
1479
- fi
2041
+ write_mempalace_ignore_file
1480
2042
 
1481
- if command -v mempalace-mcp >/dev/null 2>&1; then
1482
- if mempalace-mcp --help >/dev/null 2>&1; then
1483
- log "MemPalace MCP runtime check succeeded via 'mempalace-mcp'"
1484
- else
1485
- warn "MemPalace MCP runtime check failed; continuing without runtime validation"
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"
1486
2050
  fi
1487
2051
  else
1488
- warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
2052
+ log "MemPalace MCP binary found: mempalace-mcp"
1489
2053
  fi
1490
2054
 
1491
2055
  if selected_agent_os_contains "opencode"; then
@@ -1502,6 +2066,9 @@ configure_mempalace_if_needed() {
1502
2066
  if selected_agent_os_contains "gemini"; then
1503
2067
  write_mempalace_generic_json_config "$PROJECT_DIR/.gemini/settings.json" "generated:mempalace-gemini-config"
1504
2068
  fi
2069
+ if selected_agent_os_contains "kilocode"; then
2070
+ write_mempalace_generic_json_config "$PROJECT_DIR/.kilocode/mcp.json" "generated:mempalace-kilocode-config"
2071
+ fi
1505
2072
  if selected_agent_os_contains "antigravity"; then
1506
2073
  write_mempalace_generic_json_config "$HOME/.gemini/antigravity/mcp_config.json" "generated:mempalace-antigravity-config"
1507
2074
  fi
@@ -1518,18 +2085,25 @@ configure_context7_if_needed() {
1518
2085
  return
1519
2086
  fi
1520
2087
 
1521
- if is_interactive_terminal; then
1522
- local enable_context7 answer
1523
- read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
2088
+ local enable_context7="${AGENTIC_ENABLE_CONTEXT7:-}"
2089
+ if [[ -n "$enable_context7" ]]; then
1524
2090
  enable_context7="$(trim "$enable_context7")"
2091
+ fi
2092
+
2093
+ if is_interactive_terminal; then
2094
+ if [[ -z "$enable_context7" ]]; then
2095
+ read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
2096
+ enable_context7="$(trim "$enable_context7")"
2097
+ fi
1525
2098
  if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
1526
2099
  log "Context7 MCP configuration disabled"
1527
2100
  return
1528
2101
  fi
1529
2102
 
1530
- if [[ -z "$CONTEXT7_API_KEY" ]]; then
1531
- read -r -p "Context7 API key (optional, empty = no key): " answer
1532
- 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
1533
2107
  fi
1534
2108
  elif [[ -z "$CONTEXT7_API_KEY" ]]; then
1535
2109
  log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
@@ -1564,6 +2138,8 @@ configure_context7_if_needed() {
1564
2138
  if selected_agent_os_contains "antigravity"; then
1565
2139
  write_context7_antigravity_config
1566
2140
  fi
2141
+
2142
+ print_context7_key_recommendation
1567
2143
  }
1568
2144
 
1569
2145
  write_default_opencode_plugin_config() {
@@ -1578,8 +2154,8 @@ from pathlib import Path
1578
2154
 
1579
2155
  path = Path(sys.argv[1])
1580
2156
  path.write_text(json.dumps({
1581
- "telegram": {"enabled": False, "botToken": "", "chatId": ""},
1582
- "modelChecker": {"enabled": False},
2157
+ "telegram": {"enabled": False},
2158
+ "agentModelMapper": {"enabled": False},
1583
2159
  }, indent=2) + "\n", encoding="utf-8")
1584
2160
  PY
1585
2161
  fi
@@ -1596,6 +2172,12 @@ configure_opencode_plugins_if_needed() {
1596
2172
  ensure_python_available
1597
2173
  ensure_dir "$APP_CONFIG_DIR"
1598
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
+
1599
2181
  if ! is_interactive_terminal; then
1600
2182
  if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
1601
2183
  write_default_opencode_plugin_config
@@ -1603,42 +2185,378 @@ configure_opencode_plugins_if_needed() {
1603
2185
  return
1604
2186
  fi
1605
2187
 
1606
- local enable_telegram telegram_token telegram_chat enable_model_checker
1607
- read -r -p "Enable OpenCode Telegram notifications? [y/N]: " enable_telegram
1608
- enable_telegram="$(trim "$enable_telegram")"
1609
- telegram_token=""
1610
- telegram_chat=""
1611
- if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
1612
- read -r -p "Telegram bot token (empty disables plugin): " telegram_token
1613
- read -r -p "Telegram chat id (empty disables plugin): " telegram_chat
1614
- telegram_token="$(trim "$telegram_token")"
1615
- telegram_chat="$(trim "$telegram_chat")"
2188
+ local plugin_options=("telegram-opencode-notifier" "agent-model-mapper")
2189
+ local selected_plugins=()
2190
+ local use_fzf_plugins=false
2191
+ if fzf_available; then
2192
+ use_fzf_plugins=true
2193
+ elif ensure_fzf_or_fallback; then
2194
+ use_fzf_plugins=true
1616
2195
  fi
1617
2196
 
1618
- read -r -p "Enable OpenCode model checker plugin? [y/N]: " enable_model_checker
1619
- enable_model_checker="$(trim "$enable_model_checker")"
2197
+ if [[ "$use_fzf_plugins" == true ]]; then
2198
+ readlines selected_plugins < <(choose_multi_fzf_strict "Select optional OpenCode plugin(s):" "${plugin_options[@]}")
2199
+ else
2200
+ local selected_plugins_output
2201
+ selected_plugins_output="$(choose_multi_by_index "Select optional OpenCode plugin(s):" "${plugin_options[@]}")"
2202
+ readlines selected_plugins <<< "$selected_plugins_output"
2203
+ fi
2204
+
2205
+ local enable_telegram="n" enable_agent_model_mapper="n"
2206
+ local selected_plugin
2207
+ for selected_plugin in "${selected_plugins[@]}"; do
2208
+ selected_plugin="$(trim "$selected_plugin")"
2209
+ [[ -z "$selected_plugin" ]] && continue
2210
+ case "$selected_plugin" in
2211
+ telegram-opencode-notifier) enable_telegram="y" ;;
2212
+ agent-model-mapper) enable_agent_model_mapper="y" ;;
2213
+ esac
2214
+ done
2215
+
2216
+ if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
2217
+ log "Telegram plugin enabled; credentials are read only from OPENCODE_TELEGRAM_BOT_TOKEN and OPENCODE_TELEGRAM_CHAT_ID"
2218
+ fi
1620
2219
 
1621
- 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'
1622
2221
  import json
1623
2222
  import sys
1624
2223
  from pathlib import Path
1625
2224
 
1626
2225
  path = Path(sys.argv[1])
1627
- token = sys.argv[2]
1628
- chat = sys.argv[3]
1629
- enable_model = sys.argv[4].lower() == "y"
2226
+ enable_telegram = sys.argv[2].lower() == "y"
2227
+ enable_mapper = sys.argv[3].lower() == "y"
1630
2228
  data = {
1631
2229
  "telegram": {
1632
- "enabled": bool(token and chat),
1633
- "botToken": token,
1634
- "chatId": chat,
2230
+ "enabled": enable_telegram,
1635
2231
  },
1636
- "modelChecker": {
1637
- "enabled": enable_model,
2232
+ "agentModelMapper": {
2233
+ "enabled": enable_mapper,
1638
2234
  },
1639
2235
  }
1640
2236
  path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
1641
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"
1642
2560
  }
1643
2561
 
1644
2562
  normalize_selected_agent_os() {
@@ -1744,7 +2662,7 @@ build_header() {
1744
2662
  {
1745
2663
  echo "# Agentic Project Guidelines"
1746
2664
  echo
1747
- echo "Generated by $SCRIPT_NAME on $(date -u +'%Y-%m-%dT%H:%M:%SZ')."
2665
+ echo "Generated by $SCRIPT_NAME."
1748
2666
  echo
1749
2667
  echo "## Installation Context"
1750
2668
  echo "- Agent OS targets: ${SELECTED_AGENT_OS[*]}"
@@ -1844,6 +2762,40 @@ generate_agents_md() {
1844
2762
  rm -f "$tmp"
1845
2763
  }
1846
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
+
1847
2799
  validate_inputs() {
1848
2800
  local available_areas
1849
2801
  available_areas="$(list_areas || true)"
@@ -1911,37 +2863,47 @@ validate_inputs() {
1911
2863
  }
1912
2864
 
1913
2865
  print_report() {
1914
- echo
1915
- echo "${COLOR_HEADER}=== Installation report ===${COLOR_RESET}"
1916
- echo "Project dir: $PROJECT_DIR"
1917
- echo "Knowledge base repo: $REPO_ROOT"
1918
- echo "Config file: $APP_CONFIG_FILE"
1919
- echo "Agent OS targets: ${SELECTED_AGENT_OS[*]}"
1920
- echo "Areas: ${SELECTED_AREAS[*]}"
1921
- echo "Specializations: ${SELECTED_SPECS[*]}"
1922
-
1923
- echo
1924
- echo "Created directories:"
2866
+ out
2867
+ out "=== Installation report ===" "$COLOR_HEADER"
2868
+ out "Agentic version: $(app_version_label)"
2869
+ out "Project dir: $PROJECT_DIR"
2870
+ out "Knowledge base repo: $REPO_ROOT"
2871
+ out "Config file: $APP_CONFIG_FILE"
2872
+ out "Agent OS targets: ${SELECTED_AGENT_OS[*]}"
2873
+ out "Areas: ${SELECTED_AREAS[*]}"
2874
+ out "Specializations: ${SELECTED_SPECS[*]}"
2875
+
2876
+ out
2877
+ out "Created directories:"
1925
2878
  if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
1926
- echo "- (none)"
2879
+ out "- (none)"
1927
2880
  else
1928
- printf -- '- %s\n' "${CREATED_PATHS[@]}"
2881
+ local created_path
2882
+ for created_path in "${CREATED_PATHS[@]}"; do
2883
+ out "- $created_path"
2884
+ done
1929
2885
  fi
1930
2886
 
1931
- echo
1932
- echo "Copied/generated paths:"
2887
+ out
2888
+ out "Copied/generated paths:"
1933
2889
  if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
1934
- echo "- (none)"
2890
+ out "- (none)"
1935
2891
  else
1936
- printf -- '- %s\n' "${COPIED_PATHS[@]}"
2892
+ local copied_path
2893
+ for copied_path in "${COPIED_PATHS[@]}"; do
2894
+ out "- $copied_path"
2895
+ done
1937
2896
  fi
1938
2897
 
1939
- echo
1940
- echo "Warnings:"
2898
+ out
2899
+ out "Warnings:"
1941
2900
  if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
1942
- echo "- (none)"
2901
+ out "- (none)"
1943
2902
  else
1944
- printf -- '- %s\n' "${WARNINGS[@]}"
2903
+ local warning
2904
+ for warning in "${WARNINGS[@]}"; do
2905
+ out "- $warning"
2906
+ done
1945
2907
  fi
1946
2908
  }
1947
2909
 
@@ -2004,27 +2966,289 @@ print_missing_agent_binary_guides() {
2004
2966
  return
2005
2967
  fi
2006
2968
 
2007
- echo
2008
- echo "${COLOR_HEADER}=== Agent binary setup recommendations ===${COLOR_RESET}"
2009
- printf '%s\n' "${missing_lines[@]}"
2969
+ out
2970
+ out "=== Agent binary setup recommendations ===" "$COLOR_HEADER"
2971
+ local missing_line
2972
+ for missing_line in "${missing_lines[@]}"; do
2973
+ out "$missing_line"
2974
+ done
2975
+ }
2976
+
2977
+ changelog_file_path() {
2978
+ local candidate
2979
+ for candidate in "$SCRIPT_DIR/CHANGELOG.md" "$REPO_ROOT/CHANGELOG.md" "$APP_REPO_DIR/CHANGELOG.md"; do
2980
+ [[ -f "$candidate" ]] || continue
2981
+ printf '%s\n' "$candidate"
2982
+ return 0
2983
+ done
2984
+ return 1
2985
+ }
2986
+
2987
+ print_current_changelog() {
2988
+ local changelog version
2989
+ changelog="$(changelog_file_path || true)"
2990
+ version="$(app_version_label)"
2991
+ if [[ -z "$changelog" ]]; then
2992
+ warn "CHANGELOG.md not found; skipping changelog output"
2993
+ return
2994
+ fi
2995
+
2996
+ local section_file
2997
+ section_file="$(mktemp "${TMPDIR:-/tmp}/agentic-changelog.XXXXXX")"
2998
+ awk -v wanted="## $version" '
2999
+ $0 == wanted { in_section = 1; print; next }
3000
+ in_section && /^## / { exit }
3001
+ in_section { print }
3002
+ ' "$changelog" > "$section_file"
3003
+
3004
+ if [[ ! -s "$section_file" ]]; then
3005
+ rm -f "$section_file"
3006
+ warn "No changelog section found for $version"
3007
+ return
3008
+ fi
3009
+
3010
+ out
3011
+ out "=== Changelog $version ===" "$COLOR_HEADER"
3012
+ while IFS= read -r line || [[ -n "$line" ]]; do
3013
+ if [[ "$line" == "## $version" ]]; then
3014
+ continue
3015
+ fi
3016
+ out "$line"
3017
+ done < "$section_file"
3018
+ rm -f "$section_file"
3019
+ }
3020
+
3021
+ doctor_agent_supported() {
3022
+ case "$1" in
3023
+ codex|opencode|claude|gemini) return 0 ;;
3024
+ *) return 1 ;;
3025
+ esac
3026
+ }
3027
+
3028
+ doctor_enabled() {
3029
+ [[ "$DRY_RUN" != true ]] || return 1
3030
+ [[ "${AGENTIC_DOCTOR:-1}" != "0" ]] || return 1
3031
+ [[ "${AGENTIC_TEST_SOURCE_AGENTIC:-}" != "1" ]] || return 1
3032
+ return 0
3033
+ }
3034
+
3035
+ doctor_prompt() {
3036
+ printf '%s\n' "/develop-feature напиши hello world python"
3037
+ }
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
+
3063
+ doctor_output_has_fatal_patterns() {
3064
+ local output_file="$1"
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"
3066
+ }
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
+
3102
+ doctor_copy_project() {
3103
+ local dest="$1"
3104
+ mkdir -p "$dest"
3105
+ if [[ -d "$PROJECT_DIR" ]]; then
3106
+ cp -R "$PROJECT_DIR/." "$dest/"
3107
+ fi
3108
+ }
3109
+
3110
+ run_doctor_command() {
3111
+ local agent_os="$1"
3112
+ local work_dir="$2"
3113
+ local output_file="$3"
3114
+ local prompt
3115
+ prompt="$(doctor_prompt_for_agent "$agent_os")"
3116
+
3117
+ case "$agent_os" in
3118
+ codex)
3119
+ codex exec --skip-git-repo-check --ephemeral --sandbox workspace-write -C "$work_dir" "$prompt" </dev/null >"$output_file" 2>&1
3120
+ ;;
3121
+ opencode)
3122
+ OPENCODE_DISABLE_AUTOUPDATE=1 opencode run --pure --dir "$work_dir" --dangerously-skip-permissions --format json --log-level ERROR "$prompt" >"$output_file" 2>&1
3123
+ ;;
3124
+ claude)
3125
+ (cd "$work_dir" && claude -p --permission-mode bypassPermissions --output-format stream-json "$prompt") >"$output_file" 2>&1
3126
+ ;;
3127
+ gemini)
3128
+ (cd "$work_dir" && gemini --prompt "$prompt") >"$output_file" 2>&1
3129
+ ;;
3130
+ *)
3131
+ return 2
3132
+ ;;
3133
+ esac
3134
+ }
3135
+
3136
+ run_doctor_for_agent() {
3137
+ local agent_os="$1"
3138
+ local doctor_root="$2"
3139
+ local binary_name
3140
+ binary_name="$(get_agent_binary_name "$agent_os")"
3141
+
3142
+ if [[ -z "$binary_name" ]] || ! command -v "$binary_name" >/dev/null 2>&1; then
3143
+ out "❌ $agent_os: binary '$binary_name' is not installed"
3144
+ return 1
3145
+ fi
3146
+
3147
+ local work_dir output_file status timeout_seconds started_at elapsed smoke_label
3148
+ work_dir="$doctor_root/$agent_os"
3149
+ output_file="$doctor_root/$agent_os.log"
3150
+ timeout_seconds="$(doctor_timeout_seconds)"
3151
+ smoke_label="$(doctor_smoke_label "$agent_os")"
3152
+ doctor_copy_project "$work_dir"
3153
+
3154
+ set +e
3155
+ started_at="$(date +%s)"
3156
+ run_with_doctor_timeout "$timeout_seconds" run_doctor_command "$agent_os" "$work_dir" "$output_file"
3157
+ status=$?
3158
+ elapsed=$(( $(date +%s) - started_at ))
3159
+ set -e
3160
+
3161
+ log "$agent_os doctor finished: timeout=${timeout_seconds}s exit=$status elapsed=${elapsed}s"
3162
+
3163
+ log_file_block "doctor $agent_os" "$output_file"
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
+
3170
+ if [[ "$status" -ne 0 ]]; then
3171
+ out "❌ $agent_os: $smoke_label failed (exit $status, elapsed ${elapsed}s, log: $output_file)"
3172
+ return 1
3173
+ fi
3174
+
3175
+ if doctor_output_has_fatal_patterns "$output_file"; then
3176
+ out "❌ $agent_os: $smoke_label reported integration errors (exit $status, elapsed ${elapsed}s, log: $output_file)"
3177
+ return 1
3178
+ fi
3179
+
3180
+ out "✅ $agent_os: $smoke_label passed (exit $status, elapsed ${elapsed}s)"
3181
+ return 0
3182
+ }
3183
+
3184
+ run_agentic_doctor() {
3185
+ if ! doctor_enabled; then
3186
+ log "Agentic doctor skipped"
3187
+ return
3188
+ fi
3189
+
3190
+ local selected_doctor_agents=()
3191
+ local agent_os
3192
+ for agent_os in "${SELECTED_AGENT_OS[@]}"; do
3193
+ if doctor_agent_supported "$agent_os"; then
3194
+ selected_doctor_agents+=("$agent_os")
3195
+ fi
3196
+ done
3197
+
3198
+ if [[ "${#selected_doctor_agents[@]}" -eq 0 ]]; then
3199
+ log "Agentic doctor skipped: no supported real agentos selected"
3200
+ return
3201
+ fi
3202
+
3203
+ local doctor_root
3204
+ doctor_root="$(mktemp -d "${TMPDIR:-/tmp}/agentic-doctor.XXXXXX")"
3205
+ out
3206
+ out "=== Agentic doctor ===" "$COLOR_HEADER"
3207
+ out "Doctor temp root: $doctor_root"
3208
+ out "Doctor timeout: $(doctor_timeout_seconds)s per agent"
3209
+
3210
+ local failures=0
3211
+ for agent_os in "${selected_doctor_agents[@]}"; do
3212
+ if ! run_doctor_for_agent "$agent_os" "$doctor_root"; then
3213
+ failures=$((failures + 1))
3214
+ fi
3215
+ done
3216
+
3217
+ if [[ "$AGENTIC_DOCTOR_KEEP_TMP" == "1" || "$failures" -gt 0 ]]; then
3218
+ out "Doctor temp root kept: $doctor_root"
3219
+ else
3220
+ rm -rf "$doctor_root"
3221
+ fi
3222
+
3223
+ if [[ "$failures" -gt 0 ]]; then
3224
+ warn "Agentic doctor completed with $failures failing check(s)"
3225
+ else
3226
+ log "Agentic doctor completed successfully"
3227
+ fi
2010
3228
  }
2011
3229
 
2012
3230
  run_install() {
3231
+ init_run_logging
2013
3232
  ensure_repo_layout
2014
- ensure_python_available
3233
+ ensure_agentic_runtime_requirements
2015
3234
  normalize_selected_agent_os
2016
3235
  validate_inputs
2017
3236
 
2018
3237
  ensure_dir "$PROJECT_DIR"
2019
3238
  configure_opencode_plugins_if_needed
2020
3239
  copy_extensions "$PROJECT_DIR"
3240
+ configure_opencode_agent_model_mapper_if_needed
2021
3241
  copy_specialization_assets "$PROJECT_DIR"
2022
3242
  generate_agents_md "$PROJECT_DIR"
3243
+ copy_memory_md "$PROJECT_DIR"
2023
3244
  configure_context7_if_needed
2024
3245
  configure_mempalace_if_needed
2025
3246
  write_agentic_manifest "$PROJECT_DIR"
2026
3247
  print_report
2027
3248
  print_missing_agent_binary_guides
3249
+ print_current_changelog
3250
+ run_agentic_doctor
3251
+ out "Agentic log file: $RUN_LOG_FILE"
2028
3252
  }
2029
3253
 
2030
3254
  ascii_banner() {
@@ -2318,6 +3542,28 @@ choose_single_fzf() {
2318
3542
  printf '%s\n' "${options[@]}" | fzf "${fzf_args[@]}"
2319
3543
  }
2320
3544
 
3545
+
3546
+ choose_multi_fzf_strict() {
3547
+ local prompt="$1"
3548
+ shift
3549
+ local options=("$@")
3550
+
3551
+ if [[ "${#options[@]}" -eq 0 ]]; then
3552
+ return
3553
+ fi
3554
+
3555
+ local sentinel="<none>"
3556
+ local picked=()
3557
+ readlines picked < <(choose_multi_fzf "$prompt" "$sentinel" "${options[@]}")
3558
+
3559
+ local item
3560
+ for item in "${picked[@]}"; do
3561
+ item="$(trim "$item")"
3562
+ [[ -z "$item" || "$item" == "$sentinel" ]] && continue
3563
+ printf '%s\n' "$item"
3564
+ done
3565
+ }
3566
+
2321
3567
  choose_multi_fzf() {
2322
3568
  local prompt="$1"
2323
3569
  shift
@@ -2373,11 +3619,13 @@ run_tui() {
2373
3619
  exit 1
2374
3620
  fi
2375
3621
 
3622
+ ensure_agentic_runtime_requirements
3623
+
2376
3624
  pick_theme_if_needed
2377
3625
  set_theme_colors
2378
3626
 
2379
3627
  ascii_banner
2380
- echo "${COLOR_HEADER}$APP_TUI_TITLE${COLOR_RESET}"
3628
+ echo "${COLOR_HEADER}$APP_TUI_TITLE $(app_version_label)${COLOR_RESET}"
2381
3629
  echo "${COLOR_DIM}Theme: $THEME (resolved: $ACTIVE_THEME)${COLOR_RESET}"
2382
3630
  echo
2383
3631
 
@@ -2413,6 +3661,26 @@ run_tui() {
2413
3661
  SELECTED_AGENT_OS=("${picked_agent_os[@]}")
2414
3662
  fi
2415
3663
 
3664
+ local mcp_options=("context7" "mempalace")
3665
+ local picked_mcps=()
3666
+ if [[ "$use_fzf" == true ]]; then
3667
+ readlines picked_mcps < <(choose_multi_fzf_strict "Select optional MCP integration(s):" "${mcp_options[@]}")
3668
+ else
3669
+ local picked_mcps_output
3670
+ picked_mcps_output="$(choose_multi_by_index "Select optional MCP integration(s):" "<none>" "${mcp_options[@]}")"
3671
+ readlines picked_mcps <<< "$picked_mcps_output"
3672
+ fi
3673
+
3674
+ AGENTIC_ENABLE_CONTEXT7="n"
3675
+ AGENTIC_ENABLE_MEMPALACE="n"
3676
+ local picked_mcp
3677
+ for picked_mcp in "${picked_mcps[@]}"; do
3678
+ case "$picked_mcp" in
3679
+ context7) AGENTIC_ENABLE_CONTEXT7="y" ;;
3680
+ mempalace) AGENTIC_ENABLE_MEMPALACE="y" ;;
3681
+ esac
3682
+ done
3683
+
2416
3684
  local areas=()
2417
3685
  readlines areas < <(list_areas)
2418
3686
 
@@ -2605,6 +3873,30 @@ sync_current_project_after_upgrade() {
2605
3873
  load_install_settings_from_manifest "$manifest"
2606
3874
  ensure_repo_layout
2607
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
2608
3900
  }
2609
3901
 
2610
3902
  parse_theme_option() {
@@ -2704,6 +3996,10 @@ case "$COMMAND" in
2704
3996
  DRY_RUN=true
2705
3997
  shift
2706
3998
  ;;
3999
+ --no-doctor)
4000
+ AGENTIC_DOCTOR=0
4001
+ shift
4002
+ ;;
2707
4003
  -h|--help)
2708
4004
  usage
2709
4005
  exit 0
@@ -2745,6 +4041,10 @@ case "$COMMAND" in
2745
4041
  DRY_RUN=true
2746
4042
  shift
2747
4043
  ;;
4044
+ --no-doctor)
4045
+ AGENTIC_DOCTOR=0
4046
+ shift
4047
+ ;;
2748
4048
  -h|--help)
2749
4049
  usage
2750
4050
  exit 0
@@ -2831,6 +4131,10 @@ case "$COMMAND" in
2831
4131
  usage
2832
4132
  ;;
2833
4133
 
4134
+ -V|--version|version)
4135
+ app_version_label
4136
+ ;;
4137
+
2834
4138
  *)
2835
4139
  usage
2836
4140
  exit 1