@jetrabbits/agentic 0.1.0 → 0.2.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,12 @@ 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
+
62
+ RUN_LOG_ACTIVE=false
63
+ RUN_LOG_FILE=""
58
64
 
59
65
  COLOR_RESET=""
60
66
  COLOR_HEADER=""
@@ -67,7 +73,7 @@ FZF_COLOR_ARGS=()
67
73
 
68
74
  usage() {
69
75
  cat <<USAGE
70
- $APP_TITLE
76
+ $APP_TITLE $(app_version_label)
71
77
 
72
78
  Usage:
73
79
  $SCRIPT_NAME list [agentos|areas|specs --area <name>]
@@ -75,6 +81,7 @@ Usage:
75
81
  $SCRIPT_NAME tui [--theme auto|dark|light]
76
82
  $SCRIPT_NAME upgrade
77
83
  $SCRIPT_NAME self-install [--bin-dir <dir>] [--force] [--install-fzf] [--dry-run]
84
+ $SCRIPT_NAME --version
78
85
 
79
86
  Behavior:
80
87
  - No arguments in interactive terminal: runs TUI mode
@@ -87,11 +94,13 @@ Options:
87
94
  --areas Comma-separated area list (example: software)
88
95
  --specializations Comma-separated specializations in area.spec format (example: software.backend,software.frontend)
89
96
  --theme Interface theme: auto|dark|light (default: config value or auto)
97
+ --no-doctor Skip real agent smoke checks after install
90
98
  --bin-dir Installation directory for self-install (default: ~/.local/bin)
91
99
  --force Overwrite existing binary for self-install
92
100
  --install-fzf During self-install, try to auto-install fzf (optional)
93
101
  --dry-run Show actions without writing files
94
102
  -h, --help Show this help
103
+ -V, --version Show agentic version
95
104
 
96
105
  Examples:
97
106
  $SCRIPT_NAME list agentos
@@ -102,6 +111,31 @@ Examples:
102
111
  USAGE
103
112
  }
104
113
 
114
+ read_package_version() {
115
+ local package_file="$1"
116
+ [[ -f "$package_file" ]] || return 1
117
+
118
+ sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$package_file" | head -n 1
119
+ }
120
+
121
+ app_version() {
122
+ local version=""
123
+ local candidate
124
+ for candidate in "$SCRIPT_DIR/package.json" "$REPO_ROOT/package.json" "$APP_REPO_DIR/package.json"; do
125
+ [[ -n "$candidate" ]] || continue
126
+ version="$(read_package_version "$candidate" || true)"
127
+ if [[ -n "$version" ]]; then
128
+ printf '%s\n' "$version"
129
+ return
130
+ fi
131
+ done
132
+ printf 'unknown\n'
133
+ }
134
+
135
+ app_version_label() {
136
+ printf 'v%s\n' "$(app_version)"
137
+ }
138
+
105
139
  is_interactive_terminal() {
106
140
  if [[ "${AGENTIC_FORCE_INTERACTIVE:-${AGENTOS_FORCE_INTERACTIVE:-}}" == "1" ]]; then
107
141
  return 0
@@ -217,16 +251,116 @@ set_theme_colors() {
217
251
  }
218
252
 
219
253
  log() {
220
- printf '%s[agentic]%s %s\n' "$COLOR_INFO" "$COLOR_RESET" "$1"
254
+ emit_log_line stdout "[agentic]" "$1" "$COLOR_INFO"
221
255
  }
222
256
 
223
257
  warn() {
224
- printf '%s[agentic][warn]%s %s\n' "$COLOR_WARN" "$COLOR_RESET" "$1"
258
+ emit_log_line stdout "[agentic][warn]" "$1" "$COLOR_WARN"
225
259
  WARNINGS+=("$1")
226
260
  }
227
261
 
228
262
  error() {
229
- printf '%s[agentic][error]%s %s\n' "$COLOR_ERROR" "$COLOR_RESET" "$1" >&2
263
+ emit_log_line stderr "[agentic][error]" "$1" "$COLOR_ERROR"
264
+ }
265
+
266
+ timestamp_now() {
267
+ date '+%Y-%m-%d %H:%M:%S'
268
+ }
269
+
270
+ init_run_logging() {
271
+ if [[ "$RUN_LOG_ACTIVE" == true ]]; then
272
+ return
273
+ fi
274
+
275
+ local base_dir="${TMPDIR:-/tmp}"
276
+ local stamp
277
+ stamp="$(date '+%Y%m%d-%H%M%S')"
278
+ RUN_LOG_FILE="$(mktemp "$base_dir/agentic-$stamp.XXXXXX")"
279
+ RUN_LOG_ACTIVE=true
280
+ log "Run log initialized: $RUN_LOG_FILE"
281
+ }
282
+
283
+ write_run_log_line() {
284
+ local line="$1"
285
+ if [[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" ]]; then
286
+ printf '%s\n' "$line" >> "$RUN_LOG_FILE"
287
+ fi
288
+ }
289
+
290
+ emit_log_line() {
291
+ local stream="$1"
292
+ local tag="$2"
293
+ local message="$3"
294
+ local color="${4:-}"
295
+
296
+ local line plain_line ts
297
+ if [[ "$RUN_LOG_ACTIVE" == true ]]; then
298
+ ts="$(timestamp_now)"
299
+ plain_line="$ts $tag $message"
300
+ if [[ -n "$color" ]]; then
301
+ line="$ts ${color}${tag}${COLOR_RESET} $message"
302
+ else
303
+ line="$plain_line"
304
+ fi
305
+ else
306
+ plain_line="$tag $message"
307
+ if [[ -n "$color" ]]; then
308
+ line="${color}${tag}${COLOR_RESET} $message"
309
+ else
310
+ line="$plain_line"
311
+ fi
312
+ fi
313
+
314
+ if [[ "$stream" == "stderr" ]]; then
315
+ printf '%s\n' "$line" >&2
316
+ else
317
+ printf '%s\n' "$line"
318
+ fi
319
+ write_run_log_line "$plain_line"
320
+ }
321
+
322
+ out() {
323
+ local message="${1:-}"
324
+ local color="${2:-}"
325
+ local line plain_line ts
326
+
327
+ if [[ -z "$message" ]]; then
328
+ printf '\n'
329
+ write_run_log_line ""
330
+ return
331
+ fi
332
+
333
+ if [[ "$RUN_LOG_ACTIVE" == true ]]; then
334
+ ts="$(timestamp_now)"
335
+ plain_line="$ts $message"
336
+ if [[ -n "$color" ]]; then
337
+ line="$ts ${color}${message}${COLOR_RESET}"
338
+ else
339
+ line="$plain_line"
340
+ fi
341
+ else
342
+ plain_line="$message"
343
+ if [[ -n "$color" ]]; then
344
+ line="${color}${message}${COLOR_RESET}"
345
+ else
346
+ line="$plain_line"
347
+ fi
348
+ fi
349
+
350
+ printf '%s\n' "$line"
351
+ write_run_log_line "$plain_line"
352
+ }
353
+
354
+ log_file_block() {
355
+ local label="$1"
356
+ local path="$2"
357
+ [[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" && -f "$path" ]] || return 0
358
+
359
+ write_run_log_line "$(timestamp_now) --- $label output begin ---"
360
+ while IFS= read -r line || [[ -n "$line" ]]; do
361
+ write_run_log_line "$(timestamp_now) $line"
362
+ done < "$path"
363
+ write_run_log_line "$(timestamp_now) --- $label output end ---"
230
364
  }
231
365
 
232
366
  unique_append() {
@@ -519,6 +653,42 @@ ensure_python_available() {
519
653
  fi
520
654
  }
521
655
 
656
+ pip_command() {
657
+ if command -v pip >/dev/null 2>&1; then
658
+ printf '%s\n' "pip"
659
+ return 0
660
+ fi
661
+ if command -v pip3 >/dev/null 2>&1; then
662
+ printf '%s\n' "pip3"
663
+ return 0
664
+ fi
665
+ if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then
666
+ printf '%s\n' "python3 -m pip"
667
+ return 0
668
+ fi
669
+ return 1
670
+ }
671
+
672
+ ensure_pip_available() {
673
+ if ! pip_command >/dev/null; then
674
+ error "pip is required to run agentic install/tui. Install pip for Python 3 and make 'pip3', 'pip', or 'python3 -m pip' available."
675
+ exit 1
676
+ fi
677
+ }
678
+
679
+ ensure_hash_available() {
680
+ if ! command -v shasum >/dev/null 2>&1 && ! command -v sha256sum >/dev/null 2>&1; then
681
+ error "shasum or sha256sum is required to track managed files"
682
+ exit 1
683
+ fi
684
+ }
685
+
686
+ ensure_agentic_runtime_requirements() {
687
+ ensure_python_available
688
+ ensure_pip_available
689
+ ensure_hash_available
690
+ }
691
+
522
692
  selected_agent_os_contains() {
523
693
  local expected="$1"
524
694
  local agent
@@ -547,8 +717,7 @@ hash_file() {
547
717
  elif command -v sha256sum >/dev/null 2>&1; then
548
718
  sha256sum "$path" | awk '{print $1}'
549
719
  else
550
- error "shasum or sha256sum is required to track managed files"
551
- exit 1
720
+ ensure_hash_available
552
721
  fi
553
722
  }
554
723
 
@@ -634,12 +803,15 @@ register_managed_file() {
634
803
  local dest="$1"
635
804
  local source_ref="$2"
636
805
  local marker="$3"
806
+ local copied="${4:-true}"
637
807
  local rel
638
808
  rel="$(project_rel_path "$dest")"
639
809
  local digest
640
810
  digest="$(hash_file "$dest")"
641
811
  MANAGED_RECORDS+=("$rel|$source_ref|$digest|$marker")
642
- unique_append "$dest" COPIED_PATHS
812
+ if [[ "$copied" == true ]]; then
813
+ unique_append "$dest" COPIED_PATHS
814
+ fi
643
815
  }
644
816
 
645
817
  record_agentic_event() {
@@ -679,8 +851,10 @@ write_file_with_agentic_marker() {
679
851
  can_write_managed_file "$dest" || return 0
680
852
 
681
853
  ensure_dir "$(dirname -- "$dest")"
682
- python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" <<'PY'
854
+ local write_status
855
+ write_status="$(python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" "$(app_version_label)" <<'PY'
683
856
  import json
857
+ import re
684
858
  import sys
685
859
  from pathlib import Path
686
860
 
@@ -688,6 +862,7 @@ src = Path(sys.argv[1])
688
862
  dest = Path(sys.argv[2])
689
863
  source_ref = sys.argv[3]
690
864
  repo = sys.argv[4]
865
+ version = sys.argv[5]
691
866
  text = src.read_text(encoding="utf-8")
692
867
  suffix = dest.suffix.lower()
693
868
  marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
@@ -697,12 +872,28 @@ def yaml_quote(value: str) -> str:
697
872
  return json.dumps(value, ensure_ascii=False)
698
873
 
699
874
 
875
+ def existing_created_by() -> str:
876
+ if not dest.exists():
877
+ return version
878
+ try:
879
+ old = dest.read_text(encoding="utf-8")
880
+ except Exception:
881
+ return version
882
+ match = re.search(r"(?m)^ created_by:\s*(.+?)\s*$", old)
883
+ if not match:
884
+ return version
885
+ return match.group(1).strip().strip('"')
886
+
887
+
700
888
  def markdown_with_marker(body: str) -> str:
889
+ created_by = existing_created_by()
701
890
  block = (
702
891
  "agentic:\n"
703
892
  " generated_by: agentic\n"
704
893
  f" source: {yaml_quote(source_ref)}\n"
705
894
  f" repository: {yaml_quote(repo)}\n"
895
+ f" created_by: {yaml_quote(created_by)}\n"
896
+ f" updated_by: {yaml_quote(version)}\n"
706
897
  )
707
898
  if body.startswith("---\n"):
708
899
  end = body.find("\n---", 4)
@@ -739,9 +930,23 @@ elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
739
930
  else:
740
931
  output = commented(text, "#")
741
932
 
933
+ if dest.exists():
934
+ try:
935
+ if dest.read_text(encoding="utf-8") == output:
936
+ print("unchanged")
937
+ raise SystemExit(0)
938
+ except UnicodeDecodeError:
939
+ pass
940
+
742
941
  dest.write_text(output, encoding="utf-8")
942
+ print("written")
743
943
  PY
744
- register_managed_file "$dest" "$source_ref" "internal"
944
+ )"
945
+ if [[ "$write_status" == "unchanged" ]]; then
946
+ register_managed_file "$dest" "$source_ref" "internal" false
947
+ else
948
+ register_managed_file "$dest" "$source_ref" "internal"
949
+ fi
745
950
  }
746
951
 
747
952
  write_agentic_manifest() {
@@ -775,7 +980,8 @@ write_agentic_manifest() {
775
980
  specs_csv="${SELECTED_SPECS[*]}"
776
981
  IFS="$old_ifs"
777
982
 
778
- python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" <<'PY'
983
+ 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'
779
985
  import json
780
986
  import sys
781
987
  from datetime import datetime, timezone
@@ -789,37 +995,55 @@ repo_root = sys.argv[5]
789
995
  agent_os = [x for x in sys.argv[6].split(",") if x]
790
996
  areas = [x for x in sys.argv[7].split(",") if x]
791
997
  specs = [x for x in sys.argv[8].split(",") if x]
998
+ app_version = sys.argv[9]
792
999
  now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
793
1000
 
794
1001
  existing = {}
795
1002
  created_at = now
1003
+ old_data = None
796
1004
  if manifest.exists():
797
1005
  try:
798
1006
  old = json.loads(manifest.read_text(encoding="utf-8"))
1007
+ old_data = old
799
1008
  created_at = old.get("created_at", created_at)
800
1009
  for item in old.get("managed_files", []):
801
1010
  if item.get("path"):
802
1011
  existing[item["path"]] = item
803
1012
  except Exception:
804
1013
  existing = {}
1014
+ original_existing = json.loads(json.dumps(existing))
805
1015
 
806
1016
  for line in records_file.read_text(encoding="utf-8").splitlines():
807
1017
  if not line:
808
1018
  continue
809
1019
  path, source, digest, marker = (line.split("|", 3) + ["", "", "", ""])[:4]
1020
+ old_item = original_existing.get(path, {})
1021
+ old_updated_at = old_item.get("updated_at", now)
1022
+ if (
1023
+ old_item.get("source") == source
1024
+ and old_item.get("content_hash") == digest
1025
+ and old_item.get("marker") == marker
1026
+ ):
1027
+ item_updated_at = old_updated_at
1028
+ else:
1029
+ item_updated_at = now
810
1030
  existing[path] = {
811
1031
  "path": path,
812
1032
  "source": source,
813
1033
  "content_hash": digest,
814
1034
  "marker": marker,
815
- "updated_at": now,
1035
+ "updated_at": item_updated_at,
816
1036
  }
817
1037
 
818
1038
  skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
1039
+ old_agentic = old_data.get("_agentic", {}) if isinstance(old_data, dict) else {}
1040
+ created_by = old_agentic.get("created_by", app_version)
819
1041
  data = {
820
1042
  "_agentic": {
821
1043
  "generated_by": "agentic",
822
1044
  "repository": repo_link,
1045
+ "created_by": created_by,
1046
+ "updated_by": app_version,
823
1047
  },
824
1048
  "version": 1,
825
1049
  "created_at": created_at,
@@ -834,9 +1058,26 @@ data = {
834
1058
  "managed_files": sorted(existing.values(), key=lambda x: x["path"]),
835
1059
  "skipped_files": skipped,
836
1060
  }
1061
+
1062
+ if old_data is not None:
1063
+ old_compare = json.loads(json.dumps(old_data))
1064
+ new_compare = json.loads(json.dumps(data))
1065
+ for payload in (old_compare, new_compare):
1066
+ payload.pop("updated_at", None)
1067
+ if isinstance(payload.get("_agentic"), dict):
1068
+ payload["_agentic"].pop("updated_by", None)
1069
+ if old_compare == new_compare:
1070
+ print("unchanged")
1071
+ raise SystemExit(0)
1072
+
837
1073
  manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1074
+ print("written")
838
1075
  PY
1076
+ )"
839
1077
  rm -f "$records_file" "$skipped_file"
1078
+ if [[ "$manifest_status" == "unchanged" ]]; then
1079
+ return
1080
+ fi
840
1081
  unique_append "$manifest" COPIED_PATHS
841
1082
  }
842
1083
 
@@ -976,9 +1217,10 @@ copy_dir_contents() {
976
1217
 
977
1218
  local event kind value events_file
978
1219
  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'
1220
+ python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" "$(app_version_label)" > "$events_file" <<'PY'
980
1221
  import hashlib
981
1222
  import json
1223
+ import re
982
1224
  import sys
983
1225
  from pathlib import Path
984
1226
 
@@ -988,6 +1230,7 @@ repo_root = Path(sys.argv[3])
988
1230
  project_dir = Path(sys.argv[4])
989
1231
  manifest = Path(sys.argv[5])
990
1232
  repo = sys.argv[6]
1233
+ version = sys.argv[7]
991
1234
 
992
1235
 
993
1236
  def emit(kind: str, value: str) -> None:
@@ -1010,7 +1253,7 @@ if manifest.exists():
1010
1253
  for item in data.get("managed_files", []):
1011
1254
  rel = item.get("path")
1012
1255
  if rel:
1013
- managed[rel] = item.get("content_hash", "")
1256
+ managed[rel] = item
1014
1257
  except Exception:
1015
1258
  managed = {}
1016
1259
 
@@ -1033,12 +1276,28 @@ def yaml_quote(value: str) -> str:
1033
1276
  return json.dumps(value, ensure_ascii=False)
1034
1277
 
1035
1278
 
1036
- def markdown_with_marker(body: str, source_ref: str) -> str:
1279
+ def existing_created_by(target: Path) -> str:
1280
+ if not target.exists():
1281
+ return version
1282
+ try:
1283
+ old = target.read_text(encoding="utf-8")
1284
+ except Exception:
1285
+ return version
1286
+ match = re.search(r"(?m)^ created_by:\s*(.+?)\s*$", old)
1287
+ if not match:
1288
+ return version
1289
+ return match.group(1).strip().strip('"')
1290
+
1291
+
1292
+ def markdown_with_marker(body: str, source_ref: str, target: Path) -> str:
1293
+ created_by = existing_created_by(target)
1037
1294
  block = (
1038
1295
  "agentic:\n"
1039
1296
  " generated_by: agentic\n"
1040
1297
  f" source: {yaml_quote(source_ref)}\n"
1041
1298
  f" repository: {yaml_quote(repo)}\n"
1299
+ f" created_by: {yaml_quote(created_by)}\n"
1300
+ f" updated_by: {yaml_quote(version)}\n"
1042
1301
  )
1043
1302
  if body.startswith("---\n"):
1044
1303
  end = body.find("\n---", 4)
@@ -1061,7 +1320,7 @@ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
1061
1320
  text = file_path.read_text(encoding="utf-8")
1062
1321
  suffix = target.suffix.lower()
1063
1322
  if suffix == ".md":
1064
- return markdown_with_marker(text, source_ref)
1323
+ return markdown_with_marker(text, source_ref, target)
1065
1324
  if suffix == ".json":
1066
1325
  data = json.loads(text)
1067
1326
  if not isinstance(data, dict):
@@ -1087,15 +1346,27 @@ for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
1087
1346
  emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
1088
1347
  emit("SKIP", project_rel)
1089
1348
  continue
1090
- expected_hash = managed.get(project_rel, "")
1349
+ managed_item = managed.get(project_rel, {})
1350
+ if managed_item.get("marker") == "config":
1351
+ continue
1352
+ expected_hash = managed_item.get("content_hash", "")
1091
1353
  if target.exists() and expected_hash and sha256(target) != expected_hash:
1092
1354
  emit("WARN", f"Skipping user-modified managed file: {project_rel}")
1093
1355
  emit("SKIP", project_rel)
1094
1356
  continue
1095
1357
 
1358
+ output = add_marker(file_path, target, source_ref)
1096
1359
  target.parent.mkdir(parents=True, exist_ok=True)
1097
1360
  emit("DIR", str(target.parent))
1098
- target.write_text(add_marker(file_path, target, source_ref), encoding="utf-8")
1361
+ if target.exists():
1362
+ try:
1363
+ if target.read_text(encoding="utf-8") == output:
1364
+ digest = sha256(target)
1365
+ emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
1366
+ continue
1367
+ except UnicodeDecodeError:
1368
+ pass
1369
+ target.write_text(output, encoding="utf-8")
1099
1370
  digest = sha256(target)
1100
1371
  emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
1101
1372
  emit("COPIED", str(target))
@@ -1146,7 +1417,8 @@ write_json_file_with_agentic_metadata() {
1146
1417
 
1147
1418
  can_write_managed_file "$dest" || return 0
1148
1419
  ensure_dir "$(dirname -- "$dest")"
1149
- python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1420
+ local write_status
1421
+ write_status="$(python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" "$(app_version_label)" <<'PY'
1150
1422
  import json
1151
1423
  import sys
1152
1424
  from pathlib import Path
@@ -1156,11 +1428,15 @@ source_ref = sys.argv[2]
1156
1428
  repo = sys.argv[3]
1157
1429
  context7_api_key = sys.argv[4]
1158
1430
  body = sys.argv[5]
1431
+ version = sys.argv[6]
1159
1432
 
1160
1433
  data = {}
1434
+ created_by = version
1161
1435
  if path.exists():
1162
1436
  try:
1163
1437
  data = json.loads(path.read_text(encoding="utf-8"))
1438
+ if isinstance(data, dict):
1439
+ created_by = data.get("_agentic", {}).get("created_by", version)
1164
1440
  except Exception:
1165
1441
  data = {}
1166
1442
  if not isinstance(data, dict):
@@ -1172,9 +1448,28 @@ namespace = {
1172
1448
  }
1173
1449
  exec(body, namespace)
1174
1450
  data = namespace["data"]
1451
+ metadata = data.setdefault("_agentic", {})
1452
+ metadata["generated_by"] = "agentic"
1453
+ metadata["repository"] = repo
1454
+ metadata["created_by"] = created_by
1455
+ metadata["updated_by"] = version
1456
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
1457
+ if path.exists():
1458
+ try:
1459
+ if path.read_text(encoding="utf-8") == output:
1460
+ print("unchanged")
1461
+ raise SystemExit(0)
1462
+ except UnicodeDecodeError:
1463
+ pass
1175
1464
  path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1465
+ print("written")
1176
1466
  PY
1177
- register_managed_file "$dest" "$source_ref" "internal"
1467
+ )"
1468
+ if [[ "$write_status" == "unchanged" ]]; then
1469
+ register_managed_file "$dest" "$source_ref" "internal" false
1470
+ else
1471
+ register_managed_file "$dest" "$source_ref" "internal"
1472
+ fi
1178
1473
  }
1179
1474
 
1180
1475
  write_json_config_file() {
@@ -1190,7 +1485,8 @@ write_json_config_file() {
1190
1485
 
1191
1486
  can_write_managed_file "$dest" || return 0
1192
1487
  ensure_dir "$(dirname -- "$dest")"
1193
- python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1488
+ local write_status
1489
+ write_status="$(python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1194
1490
  import json
1195
1491
  import sys
1196
1492
  from pathlib import Path
@@ -1214,9 +1510,62 @@ namespace = {
1214
1510
  }
1215
1511
  exec(body, namespace)
1216
1512
  data = namespace["data"]
1217
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1513
+ output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
1514
+ if path.exists():
1515
+ try:
1516
+ if path.read_text(encoding="utf-8") == output:
1517
+ print("unchanged")
1518
+ raise SystemExit(0)
1519
+ except UnicodeDecodeError:
1520
+ pass
1521
+ path.write_text(output, encoding="utf-8")
1522
+ print("written")
1218
1523
  PY
1219
- register_managed_file "$dest" "$source_ref" "config"
1524
+ )"
1525
+ if [[ "$write_status" == "unchanged" ]]; then
1526
+ register_managed_file "$dest" "$source_ref" "config" false
1527
+ else
1528
+ register_managed_file "$dest" "$source_ref" "config"
1529
+ fi
1530
+ }
1531
+
1532
+ write_text_config_file() {
1533
+ local dest="$1"
1534
+ local source_ref="$2"
1535
+ local content="$3"
1536
+
1537
+ if [[ "$DRY_RUN" == true ]]; then
1538
+ log "DRY-RUN write text config file $dest"
1539
+ unique_append "$dest" COPIED_PATHS
1540
+ return
1541
+ fi
1542
+
1543
+ can_write_managed_file "$dest" || return 0
1544
+ ensure_dir "$(dirname -- "$dest")"
1545
+
1546
+ local write_status
1547
+ write_status="$(python3 - "$dest" "$content" <<'PY'
1548
+ import sys
1549
+ from pathlib import Path
1550
+
1551
+ path = Path(sys.argv[1])
1552
+ content = sys.argv[2]
1553
+ if path.exists():
1554
+ try:
1555
+ if path.read_text(encoding="utf-8") == content:
1556
+ print("unchanged")
1557
+ raise SystemExit(0)
1558
+ except UnicodeDecodeError:
1559
+ pass
1560
+ path.write_text(content, encoding="utf-8")
1561
+ print("written")
1562
+ PY
1563
+ )"
1564
+ if [[ "$write_status" == "unchanged" ]]; then
1565
+ register_managed_file "$dest" "$source_ref" "config" false
1566
+ else
1567
+ register_managed_file "$dest" "$source_ref" "config"
1568
+ fi
1220
1569
  }
1221
1570
 
1222
1571
  write_context7_opencode_config() {
@@ -1250,22 +1599,32 @@ if context7_api_key:
1250
1599
  context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1251
1600
  mcp["context7"] = context7
1252
1601
  '
1253
- write_json_file_with_agentic_metadata "$dest" "generated:context7-opencode-legacy-config" "$body"
1602
+ write_json_config_file "$dest" "generated:context7-opencode-legacy-config" "$body"
1254
1603
  }
1255
1604
 
1256
1605
  write_context7_codex_config() {
1257
1606
  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}"
1607
+ local body
1608
+ body="$(python3 - "$dest" "$CONTEXT7_API_KEY" <<'PY'
1609
+ import re
1610
+ import sys
1611
+ from pathlib import Path
1612
+
1613
+ path = Path(sys.argv[1])
1614
+ api_key = sys.argv[2]
1615
+ text = path.read_text(encoding="utf-8") if path.exists() else ""
1616
+ text = re.sub(r"(?ms)^\[mcp_servers\.context7\]\n.*?(?=^\[|\Z)", "", text).strip()
1617
+ block = '[mcp_servers.context7]\nurl = "https://mcp.context7.com/mcp"\n'
1618
+ if api_key:
1619
+ escaped = api_key.replace("\\", "\\\\").replace('"', '\\"')
1620
+ block += f'http_headers = {{ "CONTEXT7_API_KEY" = "{escaped}" }}\n'
1621
+ if text:
1622
+ print(block + "\n" + text.rstrip() + "\n", end="")
1623
+ else:
1624
+ print(block, end="")
1625
+ PY
1626
+ )"
1627
+ write_text_config_file "$dest" "generated:context7-codex-config" "$body"
1269
1628
  }
1270
1629
 
1271
1630
  write_context7_claude_config() {
@@ -1350,7 +1709,7 @@ write_mempalace_opencode_config() {
1350
1709
  local body
1351
1710
  body='
1352
1711
  mcp = data.setdefault("mcp", {})
1353
- mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp", "--palace", ".mempalace"]}
1712
+ mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"]}
1354
1713
  '
1355
1714
  write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
1356
1715
  }
@@ -1359,18 +1718,21 @@ write_mempalace_codex_config() {
1359
1718
  local dest="$PROJECT_DIR/.codex/config.toml"
1360
1719
  local body
1361
1720
  body="$(python3 - "$dest" <<'PYCODE'
1362
- import pathlib, sys
1721
+ import re
1722
+ import pathlib
1723
+ import sys
1724
+
1363
1725
  path = pathlib.Path(sys.argv[1])
1364
1726
  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="")
1727
+ block = "[mcp_servers.mempalace]\ncommand = \"mempalace-mcp\"\n"
1728
+ text = re.sub(r"(?ms)^\[mcp_servers\.mempalace\]\n.*?(?=^\[|\Z)", "", text).strip()
1729
+ if text:
1730
+ print(text.rstrip() + "\n\n" + block, end="")
1731
+ else:
1732
+ print(block, end="")
1371
1733
  PYCODE
1372
1734
  )"
1373
- write_generated_text_file "$dest" "generated:mempalace-codex-config" "$body"
1735
+ write_text_config_file "$dest" "generated:mempalace-codex-config" "$body"
1374
1736
  }
1375
1737
 
1376
1738
  write_mempalace_generic_json_config() {
@@ -1379,26 +1741,23 @@ write_mempalace_generic_json_config() {
1379
1741
  local body
1380
1742
  body='
1381
1743
  servers = data.setdefault("mcpServers", {})
1382
- servers["mempalace"] = {"command": "mempalace-mcp", "args": ["--palace", ".mempalace"]}
1744
+ servers["mempalace"] = {"command": "mempalace-mcp"}
1383
1745
  '
1384
1746
  write_json_config_file "$dest" "$marker" "$body"
1385
1747
  }
1386
1748
 
1387
1749
  print_mempalace_project_setup_instructions() {
1388
1750
  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
1751
+ out "1) Ensure Python is installed and available in PATH."
1752
+ out "2) Install MemPalace:"
1753
+ out " pip install mempalace"
1754
+ out "3) Initialize project-local MemPalace cache:"
1755
+ out " mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
1756
+ out "4) Index existing project memory:"
1757
+ out " # optional if --auto-mine was skipped"
1758
+ out " mempalace mine \"$PROJECT_DIR\""
1759
+ out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
1760
+ out "Note: Ollama at localhost:11434 is optional; MemPalace can run heuristics-only without it."
1402
1761
  }
1403
1762
 
1404
1763
  setup_mempalace_for_agentic_opencode() {
@@ -1413,33 +1772,33 @@ setup_mempalace_for_agentic_opencode() {
1413
1772
  log "$step_prefix [1/4] Python check passed"
1414
1773
 
1415
1774
  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
1775
+ local pip_bin
1776
+ if ! pip_bin="$(pip_command)"; then
1417
1777
  warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
1418
1778
  return 1
1419
1779
  fi
1420
1780
  log "$step_prefix [2/4] pip check passed"
1421
1781
 
1422
1782
  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'"
1783
+ if $pip_bin install mempalace >/dev/null 2>&1; then
1784
+ log "MemPalace package installed via '$pip_bin install mempalace'"
1425
1785
  else
1426
1786
  warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions"
1427
1787
  print_mempalace_project_setup_instructions
1428
1788
  return 1
1429
1789
  fi
1430
1790
 
1431
- log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR/.mempalace"
1791
+ log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR"
1432
1792
  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
1793
+ if mempalace init "$PROJECT_DIR" --yes --auto-mine >/dev/null 2>&1; then
1435
1794
  log "MemPalace init completed"
1436
1795
  else
1437
- warn "Failed: mempalace init \"$PROJECT_DIR/.mempalace\" --yes --auto-mine"
1796
+ warn "Failed: mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
1438
1797
  fi
1439
- if mempalace mine "$PROJECT_DIR/.mempalace" >/dev/null 2>&1; then
1798
+ if mempalace mine "$PROJECT_DIR" >/dev/null 2>&1; then
1440
1799
  log "MemPalace mine completed"
1441
1800
  else
1442
- warn "Failed: mempalace mine \"$PROJECT_DIR/.mempalace\""
1801
+ warn "Failed: mempalace mine \"$PROJECT_DIR\""
1443
1802
  fi
1444
1803
  log "$step_prefix [4/4] Initialization step finished"
1445
1804
  else
@@ -1455,6 +1814,7 @@ configure_mempalace_if_needed() {
1455
1814
  && ! selected_agent_os_contains "claude" \
1456
1815
  && ! selected_agent_os_contains "cursor" \
1457
1816
  && ! selected_agent_os_contains "gemini" \
1817
+ && ! selected_agent_os_contains "kilocode" \
1458
1818
  && ! selected_agent_os_contains "antigravity"; then
1459
1819
  return
1460
1820
  fi
@@ -1464,8 +1824,8 @@ configure_mempalace_if_needed() {
1464
1824
  enable_mempalace="$(trim "${AGENTIC_ENABLE_MEMPALACE}")"
1465
1825
  elif is_interactive_terminal && [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]]; then
1466
1826
  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
1827
+ enable_mempalace="$(trim "${enable_mempalace:-n}")"
1828
+ if [[ -z "$enable_mempalace" ]]; then enable_mempalace="n"; fi
1469
1829
  fi
1470
1830
  if [[ "$enable_mempalace" =~ ^[Nn]$ ]]; then
1471
1831
  log "Skipped MemPalace MCP configuration"
@@ -1479,11 +1839,7 @@ configure_mempalace_if_needed() {
1479
1839
  fi
1480
1840
 
1481
1841
  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"
1486
- fi
1842
+ log "MemPalace MCP binary found: mempalace-mcp"
1487
1843
  else
1488
1844
  warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
1489
1845
  fi
@@ -1502,6 +1858,9 @@ configure_mempalace_if_needed() {
1502
1858
  if selected_agent_os_contains "gemini"; then
1503
1859
  write_mempalace_generic_json_config "$PROJECT_DIR/.gemini/settings.json" "generated:mempalace-gemini-config"
1504
1860
  fi
1861
+ if selected_agent_os_contains "kilocode"; then
1862
+ write_mempalace_generic_json_config "$PROJECT_DIR/.kilocode/mcp.json" "generated:mempalace-kilocode-config"
1863
+ fi
1505
1864
  if selected_agent_os_contains "antigravity"; then
1506
1865
  write_mempalace_generic_json_config "$HOME/.gemini/antigravity/mcp_config.json" "generated:mempalace-antigravity-config"
1507
1866
  fi
@@ -1518,10 +1877,17 @@ configure_context7_if_needed() {
1518
1877
  return
1519
1878
  fi
1520
1879
 
1521
- if is_interactive_terminal; then
1522
- local enable_context7 answer
1523
- read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
1880
+ local enable_context7="${AGENTIC_ENABLE_CONTEXT7:-}"
1881
+ if [[ -n "$enable_context7" ]]; then
1524
1882
  enable_context7="$(trim "$enable_context7")"
1883
+ fi
1884
+
1885
+ if is_interactive_terminal; then
1886
+ local answer
1887
+ if [[ -z "$enable_context7" ]]; then
1888
+ read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
1889
+ enable_context7="$(trim "$enable_context7")"
1890
+ fi
1525
1891
  if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
1526
1892
  log "Context7 MCP configuration disabled"
1527
1893
  return
@@ -1603,9 +1969,34 @@ configure_opencode_plugins_if_needed() {
1603
1969
  return
1604
1970
  fi
1605
1971
 
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")"
1972
+ local plugin_options=("telegram-opencode-notifier" "llm-quota-checker")
1973
+ local selected_plugins=()
1974
+ local use_fzf_plugins=false
1975
+ if fzf_available; then
1976
+ use_fzf_plugins=true
1977
+ elif ensure_fzf_or_fallback; then
1978
+ use_fzf_plugins=true
1979
+ fi
1980
+
1981
+ if [[ "$use_fzf_plugins" == true ]]; then
1982
+ readlines selected_plugins < <(choose_multi_fzf_strict "Select optional OpenCode plugin(s):" "${plugin_options[@]}")
1983
+ else
1984
+ local selected_plugins_output
1985
+ selected_plugins_output="$(choose_multi_by_index "Select optional OpenCode plugin(s):" "${plugin_options[@]}")"
1986
+ readlines selected_plugins <<< "$selected_plugins_output"
1987
+ fi
1988
+
1989
+ local enable_telegram="n" telegram_token telegram_chat enable_model_checker="n"
1990
+ local selected_plugin
1991
+ for selected_plugin in "${selected_plugins[@]}"; do
1992
+ selected_plugin="$(trim "$selected_plugin")"
1993
+ [[ -z "$selected_plugin" ]] && continue
1994
+ case "$selected_plugin" in
1995
+ telegram-opencode-notifier) enable_telegram="y" ;;
1996
+ llm-quota-checker) enable_model_checker="y" ;;
1997
+ esac
1998
+ done
1999
+
1609
2000
  telegram_token=""
1610
2001
  telegram_chat=""
1611
2002
  if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
@@ -1615,9 +2006,6 @@ configure_opencode_plugins_if_needed() {
1615
2006
  telegram_chat="$(trim "$telegram_chat")"
1616
2007
  fi
1617
2008
 
1618
- read -r -p "Enable OpenCode model checker plugin? [y/N]: " enable_model_checker
1619
- enable_model_checker="$(trim "$enable_model_checker")"
1620
-
1621
2009
  python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$telegram_token" "$telegram_chat" "$enable_model_checker" <<'PY'
1622
2010
  import json
1623
2011
  import sys
@@ -1744,7 +2132,7 @@ build_header() {
1744
2132
  {
1745
2133
  echo "# Agentic Project Guidelines"
1746
2134
  echo
1747
- echo "Generated by $SCRIPT_NAME on $(date -u +'%Y-%m-%dT%H:%M:%SZ')."
2135
+ echo "Generated by $SCRIPT_NAME."
1748
2136
  echo
1749
2137
  echo "## Installation Context"
1750
2138
  echo "- Agent OS targets: ${SELECTED_AGENT_OS[*]}"
@@ -1911,37 +2299,47 @@ validate_inputs() {
1911
2299
  }
1912
2300
 
1913
2301
  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:"
2302
+ out
2303
+ out "=== Installation report ===" "$COLOR_HEADER"
2304
+ out "Agentic version: $(app_version_label)"
2305
+ out "Project dir: $PROJECT_DIR"
2306
+ out "Knowledge base repo: $REPO_ROOT"
2307
+ out "Config file: $APP_CONFIG_FILE"
2308
+ out "Agent OS targets: ${SELECTED_AGENT_OS[*]}"
2309
+ out "Areas: ${SELECTED_AREAS[*]}"
2310
+ out "Specializations: ${SELECTED_SPECS[*]}"
2311
+
2312
+ out
2313
+ out "Created directories:"
1925
2314
  if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
1926
- echo "- (none)"
2315
+ out "- (none)"
1927
2316
  else
1928
- printf -- '- %s\n' "${CREATED_PATHS[@]}"
2317
+ local created_path
2318
+ for created_path in "${CREATED_PATHS[@]}"; do
2319
+ out "- $created_path"
2320
+ done
1929
2321
  fi
1930
2322
 
1931
- echo
1932
- echo "Copied/generated paths:"
2323
+ out
2324
+ out "Copied/generated paths:"
1933
2325
  if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
1934
- echo "- (none)"
2326
+ out "- (none)"
1935
2327
  else
1936
- printf -- '- %s\n' "${COPIED_PATHS[@]}"
2328
+ local copied_path
2329
+ for copied_path in "${COPIED_PATHS[@]}"; do
2330
+ out "- $copied_path"
2331
+ done
1937
2332
  fi
1938
2333
 
1939
- echo
1940
- echo "Warnings:"
2334
+ out
2335
+ out "Warnings:"
1941
2336
  if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
1942
- echo "- (none)"
2337
+ out "- (none)"
1943
2338
  else
1944
- printf -- '- %s\n' "${WARNINGS[@]}"
2339
+ local warning
2340
+ for warning in "${WARNINGS[@]}"; do
2341
+ out "- $warning"
2342
+ done
1945
2343
  fi
1946
2344
  }
1947
2345
 
@@ -2004,14 +2402,201 @@ print_missing_agent_binary_guides() {
2004
2402
  return
2005
2403
  fi
2006
2404
 
2007
- echo
2008
- echo "${COLOR_HEADER}=== Agent binary setup recommendations ===${COLOR_RESET}"
2009
- printf '%s\n' "${missing_lines[@]}"
2405
+ out
2406
+ out "=== Agent binary setup recommendations ===" "$COLOR_HEADER"
2407
+ local missing_line
2408
+ for missing_line in "${missing_lines[@]}"; do
2409
+ out "$missing_line"
2410
+ done
2411
+ }
2412
+
2413
+ changelog_file_path() {
2414
+ local candidate
2415
+ for candidate in "$SCRIPT_DIR/CHANGELOG.md" "$REPO_ROOT/CHANGELOG.md" "$APP_REPO_DIR/CHANGELOG.md"; do
2416
+ [[ -f "$candidate" ]] || continue
2417
+ printf '%s\n' "$candidate"
2418
+ return 0
2419
+ done
2420
+ return 1
2421
+ }
2422
+
2423
+ print_current_changelog() {
2424
+ local changelog version
2425
+ changelog="$(changelog_file_path || true)"
2426
+ version="$(app_version_label)"
2427
+ if [[ -z "$changelog" ]]; then
2428
+ warn "CHANGELOG.md not found; skipping changelog output"
2429
+ return
2430
+ fi
2431
+
2432
+ local section_file
2433
+ section_file="$(mktemp "${TMPDIR:-/tmp}/agentic-changelog.XXXXXX")"
2434
+ awk -v wanted="## $version" '
2435
+ $0 == wanted { in_section = 1; print; next }
2436
+ in_section && /^## / { exit }
2437
+ in_section { print }
2438
+ ' "$changelog" > "$section_file"
2439
+
2440
+ if [[ ! -s "$section_file" ]]; then
2441
+ rm -f "$section_file"
2442
+ warn "No changelog section found for $version"
2443
+ return
2444
+ fi
2445
+
2446
+ out
2447
+ out "=== Changelog $version ===" "$COLOR_HEADER"
2448
+ while IFS= read -r line || [[ -n "$line" ]]; do
2449
+ if [[ "$line" == "## $version" ]]; then
2450
+ continue
2451
+ fi
2452
+ out "$line"
2453
+ done < "$section_file"
2454
+ rm -f "$section_file"
2455
+ }
2456
+
2457
+ doctor_agent_supported() {
2458
+ case "$1" in
2459
+ codex|opencode|claude|gemini) return 0 ;;
2460
+ *) return 1 ;;
2461
+ esac
2462
+ }
2463
+
2464
+ doctor_enabled() {
2465
+ [[ "$DRY_RUN" != true ]] || return 1
2466
+ [[ "${AGENTIC_DOCTOR:-1}" != "0" ]] || return 1
2467
+ [[ "${AGENTIC_TEST_SOURCE_AGENTIC:-}" != "1" ]] || return 1
2468
+ return 0
2469
+ }
2470
+
2471
+ doctor_prompt() {
2472
+ printf '%s\n' "/develop-feature напиши hello world python"
2473
+ }
2474
+
2475
+ doctor_output_has_fatal_patterns() {
2476
+ local output_file="$1"
2477
+ 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
+ }
2479
+
2480
+ doctor_copy_project() {
2481
+ local dest="$1"
2482
+ mkdir -p "$dest"
2483
+ if [[ -d "$PROJECT_DIR" ]]; then
2484
+ cp -R "$PROJECT_DIR/." "$dest/"
2485
+ fi
2486
+ }
2487
+
2488
+ run_doctor_command() {
2489
+ local agent_os="$1"
2490
+ local work_dir="$2"
2491
+ local output_file="$3"
2492
+ local prompt
2493
+ prompt="$(doctor_prompt)"
2494
+
2495
+ case "$agent_os" in
2496
+ codex)
2497
+ codex exec --skip-git-repo-check --full-auto -C "$work_dir" "$prompt" >"$output_file" 2>&1
2498
+ ;;
2499
+ opencode)
2500
+ opencode run --dir "$work_dir" --dangerously-skip-permissions --format json --command develop-feature "напиши hello world python" >"$output_file" 2>&1
2501
+ ;;
2502
+ claude)
2503
+ (cd "$work_dir" && claude -p --permission-mode bypassPermissions --output-format stream-json "$prompt") >"$output_file" 2>&1
2504
+ ;;
2505
+ gemini)
2506
+ (cd "$work_dir" && gemini --prompt "$prompt") >"$output_file" 2>&1
2507
+ ;;
2508
+ *)
2509
+ return 2
2510
+ ;;
2511
+ esac
2512
+ }
2513
+
2514
+ run_doctor_for_agent() {
2515
+ local agent_os="$1"
2516
+ local doctor_root="$2"
2517
+ local binary_name
2518
+ binary_name="$(get_agent_binary_name "$agent_os")"
2519
+
2520
+ if [[ -z "$binary_name" ]] || ! command -v "$binary_name" >/dev/null 2>&1; then
2521
+ out "❌ $agent_os: binary '$binary_name' is not installed"
2522
+ return 1
2523
+ fi
2524
+
2525
+ local work_dir output_file status
2526
+ work_dir="$doctor_root/$agent_os"
2527
+ output_file="$doctor_root/$agent_os.log"
2528
+ doctor_copy_project "$work_dir"
2529
+
2530
+ set +e
2531
+ run_doctor_command "$agent_os" "$work_dir" "$output_file"
2532
+ status=$?
2533
+ set -e
2534
+
2535
+ log_file_block "doctor $agent_os" "$output_file"
2536
+
2537
+ if [[ "$status" -ne 0 ]]; then
2538
+ out "❌ $agent_os: /develop-feature smoke failed (exit $status, log: $output_file)"
2539
+ return 1
2540
+ fi
2541
+
2542
+ if doctor_output_has_fatal_patterns "$output_file"; then
2543
+ out "❌ $agent_os: /develop-feature smoke reported integration errors (log: $output_file)"
2544
+ return 1
2545
+ fi
2546
+
2547
+ out "✅ $agent_os: /develop-feature smoke passed"
2548
+ return 0
2549
+ }
2550
+
2551
+ run_agentic_doctor() {
2552
+ if ! doctor_enabled; then
2553
+ log "Agentic doctor skipped"
2554
+ return
2555
+ fi
2556
+
2557
+ local selected_doctor_agents=()
2558
+ local agent_os
2559
+ for agent_os in "${SELECTED_AGENT_OS[@]}"; do
2560
+ if doctor_agent_supported "$agent_os"; then
2561
+ selected_doctor_agents+=("$agent_os")
2562
+ fi
2563
+ done
2564
+
2565
+ if [[ "${#selected_doctor_agents[@]}" -eq 0 ]]; then
2566
+ log "Agentic doctor skipped: no supported real agentos selected"
2567
+ return
2568
+ fi
2569
+
2570
+ local doctor_root
2571
+ doctor_root="$(mktemp -d "${TMPDIR:-/tmp}/agentic-doctor.XXXXXX")"
2572
+ out
2573
+ out "=== Agentic doctor ===" "$COLOR_HEADER"
2574
+ out "Doctor temp root: $doctor_root"
2575
+
2576
+ local failures=0
2577
+ for agent_os in "${selected_doctor_agents[@]}"; do
2578
+ if ! run_doctor_for_agent "$agent_os" "$doctor_root"; then
2579
+ failures=$((failures + 1))
2580
+ fi
2581
+ done
2582
+
2583
+ if [[ "$AGENTIC_DOCTOR_KEEP_TMP" == "1" || "$failures" -gt 0 ]]; then
2584
+ out "Doctor temp root kept: $doctor_root"
2585
+ else
2586
+ rm -rf "$doctor_root"
2587
+ fi
2588
+
2589
+ if [[ "$failures" -gt 0 ]]; then
2590
+ warn "Agentic doctor completed with $failures failing check(s)"
2591
+ else
2592
+ log "Agentic doctor completed successfully"
2593
+ fi
2010
2594
  }
2011
2595
 
2012
2596
  run_install() {
2597
+ init_run_logging
2013
2598
  ensure_repo_layout
2014
- ensure_python_available
2599
+ ensure_agentic_runtime_requirements
2015
2600
  normalize_selected_agent_os
2016
2601
  validate_inputs
2017
2602
 
@@ -2025,6 +2610,9 @@ run_install() {
2025
2610
  write_agentic_manifest "$PROJECT_DIR"
2026
2611
  print_report
2027
2612
  print_missing_agent_binary_guides
2613
+ print_current_changelog
2614
+ run_agentic_doctor
2615
+ out "Agentic log file: $RUN_LOG_FILE"
2028
2616
  }
2029
2617
 
2030
2618
  ascii_banner() {
@@ -2318,6 +2906,28 @@ choose_single_fzf() {
2318
2906
  printf '%s\n' "${options[@]}" | fzf "${fzf_args[@]}"
2319
2907
  }
2320
2908
 
2909
+
2910
+ choose_multi_fzf_strict() {
2911
+ local prompt="$1"
2912
+ shift
2913
+ local options=("$@")
2914
+
2915
+ if [[ "${#options[@]}" -eq 0 ]]; then
2916
+ return
2917
+ fi
2918
+
2919
+ local sentinel="<none>"
2920
+ local picked=()
2921
+ readlines picked < <(choose_multi_fzf "$prompt" "$sentinel" "${options[@]}")
2922
+
2923
+ local item
2924
+ for item in "${picked[@]}"; do
2925
+ item="$(trim "$item")"
2926
+ [[ -z "$item" || "$item" == "$sentinel" ]] && continue
2927
+ printf '%s\n' "$item"
2928
+ done
2929
+ }
2930
+
2321
2931
  choose_multi_fzf() {
2322
2932
  local prompt="$1"
2323
2933
  shift
@@ -2373,11 +2983,13 @@ run_tui() {
2373
2983
  exit 1
2374
2984
  fi
2375
2985
 
2986
+ ensure_agentic_runtime_requirements
2987
+
2376
2988
  pick_theme_if_needed
2377
2989
  set_theme_colors
2378
2990
 
2379
2991
  ascii_banner
2380
- echo "${COLOR_HEADER}$APP_TUI_TITLE${COLOR_RESET}"
2992
+ echo "${COLOR_HEADER}$APP_TUI_TITLE $(app_version_label)${COLOR_RESET}"
2381
2993
  echo "${COLOR_DIM}Theme: $THEME (resolved: $ACTIVE_THEME)${COLOR_RESET}"
2382
2994
  echo
2383
2995
 
@@ -2413,6 +3025,26 @@ run_tui() {
2413
3025
  SELECTED_AGENT_OS=("${picked_agent_os[@]}")
2414
3026
  fi
2415
3027
 
3028
+ local mcp_options=("context7" "mempalace")
3029
+ local picked_mcps=()
3030
+ if [[ "$use_fzf" == true ]]; then
3031
+ readlines picked_mcps < <(choose_multi_fzf_strict "Select optional MCP integration(s):" "${mcp_options[@]}")
3032
+ else
3033
+ local picked_mcps_output
3034
+ picked_mcps_output="$(choose_multi_by_index "Select optional MCP integration(s):" "<none>" "${mcp_options[@]}")"
3035
+ readlines picked_mcps <<< "$picked_mcps_output"
3036
+ fi
3037
+
3038
+ AGENTIC_ENABLE_CONTEXT7="n"
3039
+ AGENTIC_ENABLE_MEMPALACE="n"
3040
+ local picked_mcp
3041
+ for picked_mcp in "${picked_mcps[@]}"; do
3042
+ case "$picked_mcp" in
3043
+ context7) AGENTIC_ENABLE_CONTEXT7="y" ;;
3044
+ mempalace) AGENTIC_ENABLE_MEMPALACE="y" ;;
3045
+ esac
3046
+ done
3047
+
2416
3048
  local areas=()
2417
3049
  readlines areas < <(list_areas)
2418
3050
 
@@ -2704,6 +3336,10 @@ case "$COMMAND" in
2704
3336
  DRY_RUN=true
2705
3337
  shift
2706
3338
  ;;
3339
+ --no-doctor)
3340
+ AGENTIC_DOCTOR=0
3341
+ shift
3342
+ ;;
2707
3343
  -h|--help)
2708
3344
  usage
2709
3345
  exit 0
@@ -2745,6 +3381,10 @@ case "$COMMAND" in
2745
3381
  DRY_RUN=true
2746
3382
  shift
2747
3383
  ;;
3384
+ --no-doctor)
3385
+ AGENTIC_DOCTOR=0
3386
+ shift
3387
+ ;;
2748
3388
  -h|--help)
2749
3389
  usage
2750
3390
  exit 0
@@ -2831,6 +3471,10 @@ case "$COMMAND" in
2831
3471
  usage
2832
3472
  ;;
2833
3473
 
3474
+ -V|--version|version)
3475
+ app_version_label
3476
+ ;;
3477
+
2834
3478
  *)
2835
3479
  usage
2836
3480
  exit 1