@jetrabbits/agentic 0.0.5 → 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)
@@ -731,11 +922,6 @@ elif suffix == ".json":
731
922
  data = json.loads(text)
732
923
  if not isinstance(data, dict):
733
924
  raise SystemExit(f"Cannot add agentic metadata to non-object JSON: {dest}")
734
- data["_agentic"] = {
735
- "generated_by": "agentic",
736
- "source": source_ref,
737
- "repository": repo,
738
- }
739
925
  output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
740
926
  elif suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
741
927
  output = commented(text, "//")
@@ -744,9 +930,23 @@ elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
744
930
  else:
745
931
  output = commented(text, "#")
746
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
+
747
941
  dest.write_text(output, encoding="utf-8")
942
+ print("written")
748
943
  PY
749
- 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
750
950
  }
751
951
 
752
952
  write_agentic_manifest() {
@@ -780,7 +980,8 @@ write_agentic_manifest() {
780
980
  specs_csv="${SELECTED_SPECS[*]}"
781
981
  IFS="$old_ifs"
782
982
 
783
- 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'
784
985
  import json
785
986
  import sys
786
987
  from datetime import datetime, timezone
@@ -794,37 +995,55 @@ repo_root = sys.argv[5]
794
995
  agent_os = [x for x in sys.argv[6].split(",") if x]
795
996
  areas = [x for x in sys.argv[7].split(",") if x]
796
997
  specs = [x for x in sys.argv[8].split(",") if x]
998
+ app_version = sys.argv[9]
797
999
  now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
798
1000
 
799
1001
  existing = {}
800
1002
  created_at = now
1003
+ old_data = None
801
1004
  if manifest.exists():
802
1005
  try:
803
1006
  old = json.loads(manifest.read_text(encoding="utf-8"))
1007
+ old_data = old
804
1008
  created_at = old.get("created_at", created_at)
805
1009
  for item in old.get("managed_files", []):
806
1010
  if item.get("path"):
807
1011
  existing[item["path"]] = item
808
1012
  except Exception:
809
1013
  existing = {}
1014
+ original_existing = json.loads(json.dumps(existing))
810
1015
 
811
1016
  for line in records_file.read_text(encoding="utf-8").splitlines():
812
1017
  if not line:
813
1018
  continue
814
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
815
1030
  existing[path] = {
816
1031
  "path": path,
817
1032
  "source": source,
818
1033
  "content_hash": digest,
819
1034
  "marker": marker,
820
- "updated_at": now,
1035
+ "updated_at": item_updated_at,
821
1036
  }
822
1037
 
823
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)
824
1041
  data = {
825
1042
  "_agentic": {
826
1043
  "generated_by": "agentic",
827
1044
  "repository": repo_link,
1045
+ "created_by": created_by,
1046
+ "updated_by": app_version,
828
1047
  },
829
1048
  "version": 1,
830
1049
  "created_at": created_at,
@@ -839,9 +1058,26 @@ data = {
839
1058
  "managed_files": sorted(existing.values(), key=lambda x: x["path"]),
840
1059
  "skipped_files": skipped,
841
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
+
842
1073
  manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1074
+ print("written")
843
1075
  PY
1076
+ )"
844
1077
  rm -f "$records_file" "$skipped_file"
1078
+ if [[ "$manifest_status" == "unchanged" ]]; then
1079
+ return
1080
+ fi
845
1081
  unique_append "$manifest" COPIED_PATHS
846
1082
  }
847
1083
 
@@ -981,9 +1217,10 @@ copy_dir_contents() {
981
1217
 
982
1218
  local event kind value events_file
983
1219
  events_file="$(mktemp "${TMPDIR:-/tmp}/agentic-copy-events.XXXXXX")"
984
- 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'
985
1221
  import hashlib
986
1222
  import json
1223
+ import re
987
1224
  import sys
988
1225
  from pathlib import Path
989
1226
 
@@ -993,6 +1230,7 @@ repo_root = Path(sys.argv[3])
993
1230
  project_dir = Path(sys.argv[4])
994
1231
  manifest = Path(sys.argv[5])
995
1232
  repo = sys.argv[6]
1233
+ version = sys.argv[7]
996
1234
 
997
1235
 
998
1236
  def emit(kind: str, value: str) -> None:
@@ -1015,7 +1253,7 @@ if manifest.exists():
1015
1253
  for item in data.get("managed_files", []):
1016
1254
  rel = item.get("path")
1017
1255
  if rel:
1018
- managed[rel] = item.get("content_hash", "")
1256
+ managed[rel] = item
1019
1257
  except Exception:
1020
1258
  managed = {}
1021
1259
 
@@ -1038,12 +1276,28 @@ def yaml_quote(value: str) -> str:
1038
1276
  return json.dumps(value, ensure_ascii=False)
1039
1277
 
1040
1278
 
1041
- 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)
1042
1294
  block = (
1043
1295
  "agentic:\n"
1044
1296
  " generated_by: agentic\n"
1045
1297
  f" source: {yaml_quote(source_ref)}\n"
1046
1298
  f" repository: {yaml_quote(repo)}\n"
1299
+ f" created_by: {yaml_quote(created_by)}\n"
1300
+ f" updated_by: {yaml_quote(version)}\n"
1047
1301
  )
1048
1302
  if body.startswith("---\n"):
1049
1303
  end = body.find("\n---", 4)
@@ -1066,16 +1320,11 @@ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
1066
1320
  text = file_path.read_text(encoding="utf-8")
1067
1321
  suffix = target.suffix.lower()
1068
1322
  if suffix == ".md":
1069
- return markdown_with_marker(text, source_ref)
1323
+ return markdown_with_marker(text, source_ref, target)
1070
1324
  if suffix == ".json":
1071
1325
  data = json.loads(text)
1072
1326
  if not isinstance(data, dict):
1073
1327
  raise SystemExit(f"Cannot add agentic metadata to non-object JSON: {target}")
1074
- data["_agentic"] = {
1075
- "generated_by": "agentic",
1076
- "source": source_ref,
1077
- "repository": repo,
1078
- }
1079
1328
  return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
1080
1329
  if suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
1081
1330
  return commented(text, "//", source_ref)
@@ -1097,15 +1346,27 @@ for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
1097
1346
  emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
1098
1347
  emit("SKIP", project_rel)
1099
1348
  continue
1100
- 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", "")
1101
1353
  if target.exists() and expected_hash and sha256(target) != expected_hash:
1102
1354
  emit("WARN", f"Skipping user-modified managed file: {project_rel}")
1103
1355
  emit("SKIP", project_rel)
1104
1356
  continue
1105
1357
 
1358
+ output = add_marker(file_path, target, source_ref)
1106
1359
  target.parent.mkdir(parents=True, exist_ok=True)
1107
1360
  emit("DIR", str(target.parent))
1108
- 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")
1109
1370
  digest = sha256(target)
1110
1371
  emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
1111
1372
  emit("COPIED", str(target))
@@ -1156,7 +1417,8 @@ write_json_file_with_agentic_metadata() {
1156
1417
 
1157
1418
  can_write_managed_file "$dest" || return 0
1158
1419
  ensure_dir "$(dirname -- "$dest")"
1159
- 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'
1160
1422
  import json
1161
1423
  import sys
1162
1424
  from pathlib import Path
@@ -1166,11 +1428,15 @@ source_ref = sys.argv[2]
1166
1428
  repo = sys.argv[3]
1167
1429
  context7_api_key = sys.argv[4]
1168
1430
  body = sys.argv[5]
1431
+ version = sys.argv[6]
1169
1432
 
1170
1433
  data = {}
1434
+ created_by = version
1171
1435
  if path.exists():
1172
1436
  try:
1173
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)
1174
1440
  except Exception:
1175
1441
  data = {}
1176
1442
  if not isinstance(data, dict):
@@ -1182,14 +1448,28 @@ namespace = {
1182
1448
  }
1183
1449
  exec(body, namespace)
1184
1450
  data = namespace["data"]
1185
- data["_agentic"] = {
1186
- "generated_by": "agentic",
1187
- "source": source_ref,
1188
- "repository": repo,
1189
- }
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
1190
1464
  path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1465
+ print("written")
1191
1466
  PY
1192
- 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
1193
1473
  }
1194
1474
 
1195
1475
  write_json_config_file() {
@@ -1205,7 +1485,8 @@ write_json_config_file() {
1205
1485
 
1206
1486
  can_write_managed_file "$dest" || return 0
1207
1487
  ensure_dir "$(dirname -- "$dest")"
1208
- python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1488
+ local write_status
1489
+ write_status="$(python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
1209
1490
  import json
1210
1491
  import sys
1211
1492
  from pathlib import Path
@@ -1229,9 +1510,62 @@ namespace = {
1229
1510
  }
1230
1511
  exec(body, namespace)
1231
1512
  data = namespace["data"]
1232
- 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")
1233
1523
  PY
1234
- 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
1235
1569
  }
1236
1570
 
1237
1571
  write_context7_opencode_config() {
@@ -1265,22 +1599,32 @@ if context7_api_key:
1265
1599
  context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1266
1600
  mcp["context7"] = context7
1267
1601
  '
1268
- 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"
1269
1603
  }
1270
1604
 
1271
1605
  write_context7_codex_config() {
1272
1606
  local dest="$PROJECT_DIR/.codex/config.toml"
1273
- local headers=""
1274
- if [[ -n "$CONTEXT7_API_KEY" ]]; then
1275
- local escaped_key
1276
- escaped_key="${CONTEXT7_API_KEY//\\/\\\\}"
1277
- escaped_key="${escaped_key//\"/\\\"}"
1278
- headers="http_headers = { \"CONTEXT7_API_KEY\" = \"$escaped_key\" }
1279
- "
1280
- fi
1281
- write_generated_text_file "$dest" "generated:context7-codex-config" "[mcp_servers.context7]
1282
- url = \"https://mcp.context7.com/mcp\"
1283
- ${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"
1284
1628
  }
1285
1629
 
1286
1630
  write_context7_claude_config() {
@@ -1314,6 +1658,37 @@ mcp_servers["context7"] = context7
1314
1658
  write_json_config_file "$dest" "generated:context7-cursor-config" "$body"
1315
1659
  }
1316
1660
 
1661
+
1662
+ write_context7_kilocode_config() {
1663
+ local dest="$PROJECT_DIR/.kilocode/mcp.json"
1664
+ local body
1665
+ body='
1666
+ mcp_servers = data.setdefault("mcpServers", {})
1667
+ context7 = {
1668
+ "url": "https://mcp.context7.com/mcp",
1669
+ }
1670
+ if context7_api_key:
1671
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1672
+ mcp_servers["context7"] = context7
1673
+ '
1674
+ write_json_config_file "$dest" "generated:context7-kilocode-config" "$body"
1675
+ }
1676
+
1677
+ write_context7_antigravity_config() {
1678
+ local dest="$HOME/.gemini/antigravity/mcp_config.json"
1679
+ local body
1680
+ body='
1681
+ mcp_servers = data.setdefault("mcpServers", {})
1682
+ context7 = {
1683
+ "url": "https://mcp.context7.com/mcp",
1684
+ }
1685
+ if context7_api_key:
1686
+ context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
1687
+ mcp_servers["context7"] = context7
1688
+ '
1689
+ write_json_config_file "$dest" "generated:context7-antigravity-config" "$body"
1690
+ }
1691
+
1317
1692
  write_context7_gemini_config() {
1318
1693
  local dest="$PROJECT_DIR/.gemini/settings.json"
1319
1694
  local body
@@ -1329,19 +1704,190 @@ mcp_servers["context7"] = context7
1329
1704
  write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
1330
1705
  }
1331
1706
 
1707
+ write_mempalace_opencode_config() {
1708
+ local dest="$1"
1709
+ local body
1710
+ body='
1711
+ mcp = data.setdefault("mcp", {})
1712
+ mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"]}
1713
+ '
1714
+ write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
1715
+ }
1716
+
1717
+ write_mempalace_codex_config() {
1718
+ local dest="$PROJECT_DIR/.codex/config.toml"
1719
+ local body
1720
+ body="$(python3 - "$dest" <<'PYCODE'
1721
+ import re
1722
+ import pathlib
1723
+ import sys
1724
+
1725
+ path = pathlib.Path(sys.argv[1])
1726
+ text = path.read_text(encoding='utf-8') if path.exists() else ''
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="")
1733
+ PYCODE
1734
+ )"
1735
+ write_text_config_file "$dest" "generated:mempalace-codex-config" "$body"
1736
+ }
1737
+
1738
+ write_mempalace_generic_json_config() {
1739
+ local dest="$1"
1740
+ local marker="$2"
1741
+ local body
1742
+ body='
1743
+ servers = data.setdefault("mcpServers", {})
1744
+ servers["mempalace"] = {"command": "mempalace-mcp"}
1745
+ '
1746
+ write_json_config_file "$dest" "$marker" "$body"
1747
+ }
1748
+
1749
+ print_mempalace_project_setup_instructions() {
1750
+ log "MemPalace setup instructions for target project: $PROJECT_DIR"
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."
1761
+ }
1762
+
1763
+ setup_mempalace_for_agentic_opencode() {
1764
+ local step_prefix="MemPalace setup"
1765
+
1766
+ log "$step_prefix [1/4] Checking Python availability"
1767
+ if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
1768
+ warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
1769
+ warn "Install help: https://www.python.org/downloads/"
1770
+ return 1
1771
+ fi
1772
+ log "$step_prefix [1/4] Python check passed"
1773
+
1774
+ log "$step_prefix [2/4] Checking pip availability"
1775
+ local pip_bin
1776
+ if ! pip_bin="$(pip_command)"; then
1777
+ warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
1778
+ return 1
1779
+ fi
1780
+ log "$step_prefix [2/4] pip check passed"
1781
+
1782
+ log "$step_prefix [3/4] Installing mempalace package"
1783
+ if $pip_bin install mempalace >/dev/null 2>&1; then
1784
+ log "MemPalace package installed via '$pip_bin install mempalace'"
1785
+ else
1786
+ warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions"
1787
+ print_mempalace_project_setup_instructions
1788
+ return 1
1789
+ fi
1790
+
1791
+ log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR"
1792
+ if command -v mempalace >/dev/null 2>&1; then
1793
+ if mempalace init "$PROJECT_DIR" --yes --auto-mine >/dev/null 2>&1; then
1794
+ log "MemPalace init completed"
1795
+ else
1796
+ warn "Failed: mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
1797
+ fi
1798
+ if mempalace mine "$PROJECT_DIR" >/dev/null 2>&1; then
1799
+ log "MemPalace mine completed"
1800
+ else
1801
+ warn "Failed: mempalace mine \"$PROJECT_DIR\""
1802
+ fi
1803
+ log "$step_prefix [4/4] Initialization step finished"
1804
+ else
1805
+ warn "mempalace command is unavailable after install; please run setup manually"
1806
+ print_mempalace_project_setup_instructions
1807
+ return 1
1808
+ fi
1809
+ }
1810
+
1811
+ configure_mempalace_if_needed() {
1812
+ if ! selected_agent_os_contains "opencode" \
1813
+ && ! selected_agent_os_contains "codex" \
1814
+ && ! selected_agent_os_contains "claude" \
1815
+ && ! selected_agent_os_contains "cursor" \
1816
+ && ! selected_agent_os_contains "gemini" \
1817
+ && ! selected_agent_os_contains "kilocode" \
1818
+ && ! selected_agent_os_contains "antigravity"; then
1819
+ return
1820
+ fi
1821
+
1822
+ local enable_mempalace="N"
1823
+ if [[ -n "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
1824
+ enable_mempalace="$(trim "${AGENTIC_ENABLE_MEMPALACE}")"
1825
+ elif is_interactive_terminal && [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]]; then
1826
+ read -r -p "Enable MemPalace MCP memory integration? [y/N]: " enable_mempalace
1827
+ enable_mempalace="$(trim "${enable_mempalace:-n}")"
1828
+ if [[ -z "$enable_mempalace" ]]; then enable_mempalace="n"; fi
1829
+ fi
1830
+ if [[ "$enable_mempalace" =~ ^[Nn]$ ]]; then
1831
+ log "Skipped MemPalace MCP configuration"
1832
+ return
1833
+ fi
1834
+
1835
+ if selected_agent_os_contains "opencode"; then
1836
+ setup_mempalace_for_agentic_opencode || true
1837
+ else
1838
+ print_mempalace_project_setup_instructions
1839
+ fi
1840
+
1841
+ if command -v mempalace-mcp >/dev/null 2>&1; then
1842
+ log "MemPalace MCP binary found: mempalace-mcp"
1843
+ else
1844
+ warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
1845
+ fi
1846
+
1847
+ if selected_agent_os_contains "opencode"; then
1848
+ write_mempalace_opencode_config "$PROJECT_DIR/opencode.json"
1849
+ write_mempalace_opencode_config "$PROJECT_DIR/.opencode/opencode.json"
1850
+ fi
1851
+ if selected_agent_os_contains "codex"; then write_mempalace_codex_config; fi
1852
+ if selected_agent_os_contains "claude"; then
1853
+ write_mempalace_generic_json_config "$PROJECT_DIR/.mcp.json" "generated:mempalace-claude-config"
1854
+ fi
1855
+ if selected_agent_os_contains "cursor"; then
1856
+ write_mempalace_generic_json_config "$PROJECT_DIR/.cursor/mcp.json" "generated:mempalace-cursor-config"
1857
+ fi
1858
+ if selected_agent_os_contains "gemini"; then
1859
+ write_mempalace_generic_json_config "$PROJECT_DIR/.gemini/settings.json" "generated:mempalace-gemini-config"
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
1864
+ if selected_agent_os_contains "antigravity"; then
1865
+ write_mempalace_generic_json_config "$HOME/.gemini/antigravity/mcp_config.json" "generated:mempalace-antigravity-config"
1866
+ fi
1867
+ }
1868
+
1332
1869
  configure_context7_if_needed() {
1333
1870
  if ! selected_agent_os_contains "opencode" \
1334
1871
  && ! selected_agent_os_contains "codex" \
1335
1872
  && ! selected_agent_os_contains "claude" \
1336
1873
  && ! selected_agent_os_contains "cursor" \
1337
- && ! selected_agent_os_contains "gemini"; then
1874
+ && ! selected_agent_os_contains "gemini" \
1875
+ && ! selected_agent_os_contains "kilocode" \
1876
+ && ! selected_agent_os_contains "antigravity"; then
1338
1877
  return
1339
1878
  fi
1340
1879
 
1341
- if is_interactive_terminal; then
1342
- local enable_context7 answer
1343
- read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
1880
+ local enable_context7="${AGENTIC_ENABLE_CONTEXT7:-}"
1881
+ if [[ -n "$enable_context7" ]]; then
1344
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
1345
1891
  if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
1346
1892
  log "Context7 MCP configuration disabled"
1347
1893
  return
@@ -1376,6 +1922,14 @@ configure_context7_if_needed() {
1376
1922
  if selected_agent_os_contains "gemini"; then
1377
1923
  write_context7_gemini_config
1378
1924
  fi
1925
+
1926
+ if selected_agent_os_contains "kilocode"; then
1927
+ write_context7_kilocode_config
1928
+ fi
1929
+
1930
+ if selected_agent_os_contains "antigravity"; then
1931
+ write_context7_antigravity_config
1932
+ fi
1379
1933
  }
1380
1934
 
1381
1935
  write_default_opencode_plugin_config() {
@@ -1415,9 +1969,34 @@ configure_opencode_plugins_if_needed() {
1415
1969
  return
1416
1970
  fi
1417
1971
 
1418
- local enable_telegram telegram_token telegram_chat enable_model_checker
1419
- read -r -p "Enable OpenCode Telegram notifications? [y/N]: " enable_telegram
1420
- 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
+
1421
2000
  telegram_token=""
1422
2001
  telegram_chat=""
1423
2002
  if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
@@ -1427,9 +2006,6 @@ configure_opencode_plugins_if_needed() {
1427
2006
  telegram_chat="$(trim "$telegram_chat")"
1428
2007
  fi
1429
2008
 
1430
- read -r -p "Enable OpenCode model checker plugin? [y/N]: " enable_model_checker
1431
- enable_model_checker="$(trim "$enable_model_checker")"
1432
-
1433
2009
  python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$telegram_token" "$telegram_chat" "$enable_model_checker" <<'PY'
1434
2010
  import json
1435
2011
  import sys
@@ -1556,7 +2132,7 @@ build_header() {
1556
2132
  {
1557
2133
  echo "# Agentic Project Guidelines"
1558
2134
  echo
1559
- echo "Generated by $SCRIPT_NAME on $(date -u +'%Y-%m-%dT%H:%M:%SZ')."
2135
+ echo "Generated by $SCRIPT_NAME."
1560
2136
  echo
1561
2137
  echo "## Installation Context"
1562
2138
  echo "- Agent OS targets: ${SELECTED_AGENT_OS[*]}"
@@ -1610,11 +2186,31 @@ append_root_agents_template() {
1610
2186
 
1611
2187
  generate_agents_md() {
1612
2188
  local project_dir="$1"
1613
- local out="$project_dir/AGENTS.md"
2189
+ local outputs=()
2190
+ local needs_root=false
2191
+ local agent_os
2192
+
2193
+ if selected_agent_os_contains "opencode"; then
2194
+ unique_append "$project_dir/.opencode/AGENTS.md" outputs
2195
+ fi
2196
+
2197
+ for agent_os in "${SELECTED_AGENT_OS[@]}"; do
2198
+ if [[ "$agent_os" != "opencode" ]]; then
2199
+ needs_root=true
2200
+ break
2201
+ fi
2202
+ done
2203
+
2204
+ if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
2205
+ unique_append "$project_dir/AGENTS.md" outputs
2206
+ fi
1614
2207
 
1615
2208
  if [[ "$DRY_RUN" == true ]]; then
1616
- log "DRY-RUN generate $out"
1617
- unique_append "$out" COPIED_PATHS
2209
+ local dry_run_out
2210
+ for dry_run_out in "${outputs[@]}"; do
2211
+ log "DRY-RUN generate $dry_run_out"
2212
+ unique_append "$dry_run_out" COPIED_PATHS
2213
+ done
1618
2214
  return
1619
2215
  fi
1620
2216
 
@@ -1629,7 +2225,10 @@ generate_agents_md() {
1629
2225
  append_specialization_template "$tmp" "$spec_key"
1630
2226
  done
1631
2227
 
1632
- write_file_with_agentic_marker "$tmp" "$out" "generated:AGENTS.md"
2228
+ local out
2229
+ for out in "${outputs[@]}"; do
2230
+ write_file_with_agentic_marker "$tmp" "$out" "generated:AGENTS.md"
2231
+ done
1633
2232
  rm -f "$tmp"
1634
2233
  }
1635
2234
 
@@ -1700,43 +2299,304 @@ validate_inputs() {
1700
2299
  }
1701
2300
 
1702
2301
  print_report() {
1703
- echo
1704
- echo "${COLOR_HEADER}=== Installation report ===${COLOR_RESET}"
1705
- echo "Project dir: $PROJECT_DIR"
1706
- echo "Knowledge base repo: $REPO_ROOT"
1707
- echo "Config file: $APP_CONFIG_FILE"
1708
- echo "Agent OS targets: ${SELECTED_AGENT_OS[*]}"
1709
- echo "Areas: ${SELECTED_AREAS[*]}"
1710
- echo "Specializations: ${SELECTED_SPECS[*]}"
1711
-
1712
- echo
1713
- 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:"
1714
2314
  if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
1715
- echo "- (none)"
2315
+ out "- (none)"
1716
2316
  else
1717
- printf -- '- %s\n' "${CREATED_PATHS[@]}"
2317
+ local created_path
2318
+ for created_path in "${CREATED_PATHS[@]}"; do
2319
+ out "- $created_path"
2320
+ done
1718
2321
  fi
1719
2322
 
1720
- echo
1721
- echo "Copied/generated paths:"
2323
+ out
2324
+ out "Copied/generated paths:"
1722
2325
  if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
1723
- echo "- (none)"
2326
+ out "- (none)"
1724
2327
  else
1725
- printf -- '- %s\n' "${COPIED_PATHS[@]}"
2328
+ local copied_path
2329
+ for copied_path in "${COPIED_PATHS[@]}"; do
2330
+ out "- $copied_path"
2331
+ done
1726
2332
  fi
1727
2333
 
1728
- echo
1729
- echo "Warnings:"
2334
+ out
2335
+ out "Warnings:"
1730
2336
  if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
1731
- echo "- (none)"
2337
+ out "- (none)"
2338
+ else
2339
+ local warning
2340
+ for warning in "${WARNINGS[@]}"; do
2341
+ out "- $warning"
2342
+ done
2343
+ fi
2344
+ }
2345
+
2346
+ detect_runtime_platform_label() {
2347
+ local platform
2348
+ platform="$(detect_platform)"
2349
+ if [[ "$platform" == "linux" ]] && grep -qiE "(microsoft|wsl)" /proc/version 2>/dev/null; then
2350
+ echo "wsl"
2351
+ return
2352
+ fi
2353
+ echo "$platform"
2354
+ }
2355
+
2356
+ get_agent_binary_name() {
2357
+ local agent_os="$1"
2358
+ case "$agent_os" in
2359
+ codex) echo "codex" ;;
2360
+ claude) echo "claude" ;;
2361
+ opencode) echo "opencode" ;;
2362
+ cursor) echo "cursor-agent" ;;
2363
+ gemini) echo "gemini" ;;
2364
+ antigravity) echo "antigravity" ;;
2365
+ *) echo "" ;;
2366
+ esac
2367
+ }
2368
+
2369
+ print_missing_agent_binary_guides() {
2370
+ local platform_label
2371
+ platform_label="$(detect_runtime_platform_label)"
2372
+ local missing_lines=()
2373
+ local agent_os
2374
+
2375
+ for agent_os in "${SELECTED_AGENT_OS[@]}"; do
2376
+ local binary_name
2377
+ binary_name="$(get_agent_binary_name "$agent_os")"
2378
+ if [[ -z "$binary_name" ]]; then
2379
+ continue
2380
+ fi
2381
+ if command -v "$binary_name" >/dev/null 2>&1; then
2382
+ continue
2383
+ fi
2384
+
2385
+ local install_link=""
2386
+ case "$agent_os" in
2387
+ codex) install_link="https://github.com/openai/codex" ;;
2388
+ claude) install_link="https://docs.anthropic.com/en/docs/claude-code/quickstart" ;;
2389
+ opencode) install_link="https://opencode.ai/docs" ;;
2390
+ cursor) install_link="https://docs.cursor.com/get-started/installation" ;;
2391
+ gemini) install_link="https://github.com/google-gemini/gemini-cli" ;;
2392
+ antigravity) install_link="https://github.com/getantigravity/antigravity" ;;
2393
+ esac
2394
+
2395
+ missing_lines+=("- $agent_os: binary '$binary_name' is not installed on $platform_label.")
2396
+ if [[ -n "$install_link" ]]; then
2397
+ missing_lines+=(" Install guide: $install_link")
2398
+ fi
2399
+ done
2400
+
2401
+ if [[ "${#missing_lines[@]}" -eq 0 ]]; then
2402
+ return
2403
+ fi
2404
+
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"
1732
2585
  else
1733
- printf -- '- %s\n' "${WARNINGS[@]}"
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"
1734
2593
  fi
1735
2594
  }
1736
2595
 
1737
2596
  run_install() {
2597
+ init_run_logging
1738
2598
  ensure_repo_layout
1739
- ensure_python_available
2599
+ ensure_agentic_runtime_requirements
1740
2600
  normalize_selected_agent_os
1741
2601
  validate_inputs
1742
2602
 
@@ -1746,8 +2606,13 @@ run_install() {
1746
2606
  copy_specialization_assets "$PROJECT_DIR"
1747
2607
  generate_agents_md "$PROJECT_DIR"
1748
2608
  configure_context7_if_needed
2609
+ configure_mempalace_if_needed
1749
2610
  write_agentic_manifest "$PROJECT_DIR"
1750
2611
  print_report
2612
+ print_missing_agent_binary_guides
2613
+ print_current_changelog
2614
+ run_agentic_doctor
2615
+ out "Agentic log file: $RUN_LOG_FILE"
1751
2616
  }
1752
2617
 
1753
2618
  ascii_banner() {
@@ -2041,6 +2906,28 @@ choose_single_fzf() {
2041
2906
  printf '%s\n' "${options[@]}" | fzf "${fzf_args[@]}"
2042
2907
  }
2043
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
+
2044
2931
  choose_multi_fzf() {
2045
2932
  local prompt="$1"
2046
2933
  shift
@@ -2096,11 +2983,13 @@ run_tui() {
2096
2983
  exit 1
2097
2984
  fi
2098
2985
 
2986
+ ensure_agentic_runtime_requirements
2987
+
2099
2988
  pick_theme_if_needed
2100
2989
  set_theme_colors
2101
2990
 
2102
2991
  ascii_banner
2103
- echo "${COLOR_HEADER}$APP_TUI_TITLE${COLOR_RESET}"
2992
+ echo "${COLOR_HEADER}$APP_TUI_TITLE $(app_version_label)${COLOR_RESET}"
2104
2993
  echo "${COLOR_DIM}Theme: $THEME (resolved: $ACTIVE_THEME)${COLOR_RESET}"
2105
2994
  echo
2106
2995
 
@@ -2136,6 +3025,26 @@ run_tui() {
2136
3025
  SELECTED_AGENT_OS=("${picked_agent_os[@]}")
2137
3026
  fi
2138
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
+
2139
3048
  local areas=()
2140
3049
  readlines areas < <(list_areas)
2141
3050
 
@@ -2274,8 +3183,13 @@ update_installed_binary_from_repo() {
2274
3183
  return
2275
3184
  fi
2276
3185
 
2277
- cp "$source_path" "$target"
2278
- chmod +x "$target"
3186
+ local target_dir target_tmp
3187
+ target_dir="$(dirname "$target")"
3188
+ target_tmp="$(mktemp "$target_dir/.agentic-update.XXXXXX")"
3189
+
3190
+ cp "$source_path" "$target_tmp"
3191
+ chmod +x "$target_tmp"
3192
+ mv -f "$target_tmp" "$target"
2279
3193
  log "Updated installed binary: $target"
2280
3194
  }
2281
3195
 
@@ -2396,6 +3310,9 @@ case "$COMMAND" in
2396
3310
  shift 2
2397
3311
  ;;
2398
3312
  --agent-os)
3313
+ if [[ "${#SELECTED_AGENT_OS[@]}" -eq 1 && "${SELECTED_AGENT_OS[0]}" == "$DEFAULT_AGENT_OS" ]]; then
3314
+ SELECTED_AGENT_OS=()
3315
+ fi
2399
3316
  split_csv "$2" SELECTED_AGENT_OS
2400
3317
  shift 2
2401
3318
  ;;
@@ -2419,6 +3336,10 @@ case "$COMMAND" in
2419
3336
  DRY_RUN=true
2420
3337
  shift
2421
3338
  ;;
3339
+ --no-doctor)
3340
+ AGENTIC_DOCTOR=0
3341
+ shift
3342
+ ;;
2422
3343
  -h|--help)
2423
3344
  usage
2424
3345
  exit 0
@@ -2460,6 +3381,10 @@ case "$COMMAND" in
2460
3381
  DRY_RUN=true
2461
3382
  shift
2462
3383
  ;;
3384
+ --no-doctor)
3385
+ AGENTIC_DOCTOR=0
3386
+ shift
3387
+ ;;
2463
3388
  -h|--help)
2464
3389
  usage
2465
3390
  exit 0
@@ -2546,6 +3471,10 @@ case "$COMMAND" in
2546
3471
  usage
2547
3472
  ;;
2548
3473
 
3474
+ -V|--version|version)
3475
+ app_version_label
3476
+ ;;
3477
+
2549
3478
  *)
2550
3479
  usage
2551
3480
  exit 1