@jetrabbits/agentic 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -11
- package/CHANGELOG.md +23 -0
- package/MEMORY.md +113 -0
- package/Makefile +55 -3
- package/README.md +23 -5
- package/agentic +1458 -154
- package/docs/agentic-lifecycle.md +3 -3
- package/docs/agentic-stabilization/README.md +33 -0
- package/docs/agentic-token-minimization/README.md +1 -1
- package/docs/agentic-usage.md +51 -10
- package/docs/opencode_setup.md +4 -2
- package/extensions/opencode/opencode.json +1 -1
- package/extensions/opencode/plugins/agent-model-mapper.ts +106 -0
- package/extensions/opencode/plugins/telegram-notification.ts +21 -11
- package/package.json +3 -1
- package/extensions/opencode/plugins/model-checker.json +0 -13
- package/extensions/opencode/plugins/model-checker.ts +0 -302
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=(
|
|
18
|
+
STATIC_AGENT_OS=(opencode codex claude antigravity cursor kilocode)
|
|
19
19
|
INSTALL_DIRS=(rules skills workflows prompts)
|
|
20
20
|
THEME_CHOICES=(auto dark light)
|
|
21
21
|
|
|
@@ -55,6 +55,13 @@ SKIPPED_MANAGED_PATHS=()
|
|
|
55
55
|
WARNINGS=()
|
|
56
56
|
|
|
57
57
|
CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
|
|
58
|
+
AGENTIC_ENABLE_CONTEXT7="${AGENTIC_ENABLE_CONTEXT7:-}"
|
|
59
|
+
AGENTIC_DOCTOR="${AGENTIC_DOCTOR:-1}"
|
|
60
|
+
AGENTIC_DOCTOR_KEEP_TMP="${AGENTIC_DOCTOR_KEEP_TMP:-0}"
|
|
61
|
+
AGENTIC_DOCTOR_TIMEOUT_SECONDS="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
|
|
62
|
+
|
|
63
|
+
RUN_LOG_ACTIVE=false
|
|
64
|
+
RUN_LOG_FILE=""
|
|
58
65
|
|
|
59
66
|
COLOR_RESET=""
|
|
60
67
|
COLOR_HEADER=""
|
|
@@ -67,7 +74,7 @@ FZF_COLOR_ARGS=()
|
|
|
67
74
|
|
|
68
75
|
usage() {
|
|
69
76
|
cat <<USAGE
|
|
70
|
-
$APP_TITLE
|
|
77
|
+
$APP_TITLE $(app_version_label)
|
|
71
78
|
|
|
72
79
|
Usage:
|
|
73
80
|
$SCRIPT_NAME list [agentos|areas|specs --area <name>]
|
|
@@ -75,6 +82,7 @@ Usage:
|
|
|
75
82
|
$SCRIPT_NAME tui [--theme auto|dark|light]
|
|
76
83
|
$SCRIPT_NAME upgrade
|
|
77
84
|
$SCRIPT_NAME self-install [--bin-dir <dir>] [--force] [--install-fzf] [--dry-run]
|
|
85
|
+
$SCRIPT_NAME --version
|
|
78
86
|
|
|
79
87
|
Behavior:
|
|
80
88
|
- No arguments in interactive terminal: runs TUI mode
|
|
@@ -87,11 +95,13 @@ Options:
|
|
|
87
95
|
--areas Comma-separated area list (example: software)
|
|
88
96
|
--specializations Comma-separated specializations in area.spec format (example: software.backend,software.frontend)
|
|
89
97
|
--theme Interface theme: auto|dark|light (default: config value or auto)
|
|
98
|
+
--no-doctor Skip real agent smoke checks after install
|
|
90
99
|
--bin-dir Installation directory for self-install (default: ~/.local/bin)
|
|
91
100
|
--force Overwrite existing binary for self-install
|
|
92
101
|
--install-fzf During self-install, try to auto-install fzf (optional)
|
|
93
102
|
--dry-run Show actions without writing files
|
|
94
103
|
-h, --help Show this help
|
|
104
|
+
-V, --version Show agentic version
|
|
95
105
|
|
|
96
106
|
Examples:
|
|
97
107
|
$SCRIPT_NAME list agentos
|
|
@@ -102,6 +112,31 @@ Examples:
|
|
|
102
112
|
USAGE
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
read_package_version() {
|
|
116
|
+
local package_file="$1"
|
|
117
|
+
[[ -f "$package_file" ]] || return 1
|
|
118
|
+
|
|
119
|
+
sed -n 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$package_file" | head -n 1
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
app_version() {
|
|
123
|
+
local version=""
|
|
124
|
+
local candidate
|
|
125
|
+
for candidate in "$SCRIPT_DIR/package.json" "$REPO_ROOT/package.json" "$APP_REPO_DIR/package.json"; do
|
|
126
|
+
[[ -n "$candidate" ]] || continue
|
|
127
|
+
version="$(read_package_version "$candidate" || true)"
|
|
128
|
+
if [[ -n "$version" ]]; then
|
|
129
|
+
printf '%s\n' "$version"
|
|
130
|
+
return
|
|
131
|
+
fi
|
|
132
|
+
done
|
|
133
|
+
printf 'unknown\n'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
app_version_label() {
|
|
137
|
+
printf 'v%s\n' "$(app_version)"
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
is_interactive_terminal() {
|
|
106
141
|
if [[ "${AGENTIC_FORCE_INTERACTIVE:-${AGENTOS_FORCE_INTERACTIVE:-}}" == "1" ]]; then
|
|
107
142
|
return 0
|
|
@@ -217,16 +252,116 @@ set_theme_colors() {
|
|
|
217
252
|
}
|
|
218
253
|
|
|
219
254
|
log() {
|
|
220
|
-
|
|
255
|
+
emit_log_line stdout "[agentic]" "$1" "$COLOR_INFO"
|
|
221
256
|
}
|
|
222
257
|
|
|
223
258
|
warn() {
|
|
224
|
-
|
|
259
|
+
emit_log_line stdout "[agentic][warn]" "$1" "$COLOR_WARN"
|
|
225
260
|
WARNINGS+=("$1")
|
|
226
261
|
}
|
|
227
262
|
|
|
228
263
|
error() {
|
|
229
|
-
|
|
264
|
+
emit_log_line stderr "[agentic][error]" "$1" "$COLOR_ERROR"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
timestamp_now() {
|
|
268
|
+
date '+%Y-%m-%d %H:%M:%S'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
init_run_logging() {
|
|
272
|
+
if [[ "$RUN_LOG_ACTIVE" == true ]]; then
|
|
273
|
+
return
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
local base_dir="${TMPDIR:-/tmp}"
|
|
277
|
+
local stamp
|
|
278
|
+
stamp="$(date '+%Y%m%d-%H%M%S')"
|
|
279
|
+
RUN_LOG_FILE="$(mktemp "$base_dir/agentic-$stamp.XXXXXX")"
|
|
280
|
+
RUN_LOG_ACTIVE=true
|
|
281
|
+
log "Run log initialized: $RUN_LOG_FILE"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
write_run_log_line() {
|
|
285
|
+
local line="$1"
|
|
286
|
+
if [[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" ]]; then
|
|
287
|
+
printf '%s\n' "$line" >> "$RUN_LOG_FILE"
|
|
288
|
+
fi
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
emit_log_line() {
|
|
292
|
+
local stream="$1"
|
|
293
|
+
local tag="$2"
|
|
294
|
+
local message="$3"
|
|
295
|
+
local color="${4:-}"
|
|
296
|
+
|
|
297
|
+
local line plain_line ts
|
|
298
|
+
if [[ "$RUN_LOG_ACTIVE" == true ]]; then
|
|
299
|
+
ts="$(timestamp_now)"
|
|
300
|
+
plain_line="$ts $tag $message"
|
|
301
|
+
if [[ -n "$color" ]]; then
|
|
302
|
+
line="$ts ${color}${tag}${COLOR_RESET} $message"
|
|
303
|
+
else
|
|
304
|
+
line="$plain_line"
|
|
305
|
+
fi
|
|
306
|
+
else
|
|
307
|
+
plain_line="$tag $message"
|
|
308
|
+
if [[ -n "$color" ]]; then
|
|
309
|
+
line="${color}${tag}${COLOR_RESET} $message"
|
|
310
|
+
else
|
|
311
|
+
line="$plain_line"
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
if [[ "$stream" == "stderr" ]]; then
|
|
316
|
+
printf '%s\n' "$line" >&2
|
|
317
|
+
else
|
|
318
|
+
printf '%s\n' "$line"
|
|
319
|
+
fi
|
|
320
|
+
write_run_log_line "$plain_line"
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
out() {
|
|
324
|
+
local message="${1:-}"
|
|
325
|
+
local color="${2:-}"
|
|
326
|
+
local line plain_line ts
|
|
327
|
+
|
|
328
|
+
if [[ -z "$message" ]]; then
|
|
329
|
+
printf '\n'
|
|
330
|
+
write_run_log_line ""
|
|
331
|
+
return
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
if [[ "$RUN_LOG_ACTIVE" == true ]]; then
|
|
335
|
+
ts="$(timestamp_now)"
|
|
336
|
+
plain_line="$ts $message"
|
|
337
|
+
if [[ -n "$color" ]]; then
|
|
338
|
+
line="$ts ${color}${message}${COLOR_RESET}"
|
|
339
|
+
else
|
|
340
|
+
line="$plain_line"
|
|
341
|
+
fi
|
|
342
|
+
else
|
|
343
|
+
plain_line="$message"
|
|
344
|
+
if [[ -n "$color" ]]; then
|
|
345
|
+
line="${color}${message}${COLOR_RESET}"
|
|
346
|
+
else
|
|
347
|
+
line="$plain_line"
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
printf '%s\n' "$line"
|
|
352
|
+
write_run_log_line "$plain_line"
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
log_file_block() {
|
|
356
|
+
local label="$1"
|
|
357
|
+
local path="$2"
|
|
358
|
+
[[ "$RUN_LOG_ACTIVE" == true && -n "$RUN_LOG_FILE" && -f "$path" ]] || return 0
|
|
359
|
+
|
|
360
|
+
write_run_log_line "$(timestamp_now) --- $label output begin ---"
|
|
361
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
362
|
+
write_run_log_line "$(timestamp_now) $line"
|
|
363
|
+
done < "$path"
|
|
364
|
+
write_run_log_line "$(timestamp_now) --- $label output end ---"
|
|
230
365
|
}
|
|
231
366
|
|
|
232
367
|
unique_append() {
|
|
@@ -519,6 +654,42 @@ ensure_python_available() {
|
|
|
519
654
|
fi
|
|
520
655
|
}
|
|
521
656
|
|
|
657
|
+
pip_command() {
|
|
658
|
+
if command -v pip >/dev/null 2>&1; then
|
|
659
|
+
printf '%s\n' "pip"
|
|
660
|
+
return 0
|
|
661
|
+
fi
|
|
662
|
+
if command -v pip3 >/dev/null 2>&1; then
|
|
663
|
+
printf '%s\n' "pip3"
|
|
664
|
+
return 0
|
|
665
|
+
fi
|
|
666
|
+
if command -v python3 >/dev/null 2>&1 && python3 -m pip --version >/dev/null 2>&1; then
|
|
667
|
+
printf '%s\n' "python3 -m pip"
|
|
668
|
+
return 0
|
|
669
|
+
fi
|
|
670
|
+
return 1
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
ensure_pip_available() {
|
|
674
|
+
if ! pip_command >/dev/null; then
|
|
675
|
+
error "pip is required to run agentic install/tui. Install pip for Python 3 and make 'pip3', 'pip', or 'python3 -m pip' available."
|
|
676
|
+
exit 1
|
|
677
|
+
fi
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
ensure_hash_available() {
|
|
681
|
+
if ! command -v shasum >/dev/null 2>&1 && ! command -v sha256sum >/dev/null 2>&1; then
|
|
682
|
+
error "shasum or sha256sum is required to track managed files"
|
|
683
|
+
exit 1
|
|
684
|
+
fi
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
ensure_agentic_runtime_requirements() {
|
|
688
|
+
ensure_python_available
|
|
689
|
+
ensure_pip_available
|
|
690
|
+
ensure_hash_available
|
|
691
|
+
}
|
|
692
|
+
|
|
522
693
|
selected_agent_os_contains() {
|
|
523
694
|
local expected="$1"
|
|
524
695
|
local agent
|
|
@@ -547,8 +718,7 @@ hash_file() {
|
|
|
547
718
|
elif command -v sha256sum >/dev/null 2>&1; then
|
|
548
719
|
sha256sum "$path" | awk '{print $1}'
|
|
549
720
|
else
|
|
550
|
-
|
|
551
|
-
exit 1
|
|
721
|
+
ensure_hash_available
|
|
552
722
|
fi
|
|
553
723
|
}
|
|
554
724
|
|
|
@@ -634,12 +804,15 @@ register_managed_file() {
|
|
|
634
804
|
local dest="$1"
|
|
635
805
|
local source_ref="$2"
|
|
636
806
|
local marker="$3"
|
|
807
|
+
local copied="${4:-true}"
|
|
637
808
|
local rel
|
|
638
809
|
rel="$(project_rel_path "$dest")"
|
|
639
810
|
local digest
|
|
640
811
|
digest="$(hash_file "$dest")"
|
|
641
812
|
MANAGED_RECORDS+=("$rel|$source_ref|$digest|$marker")
|
|
642
|
-
|
|
813
|
+
if [[ "$copied" == true ]]; then
|
|
814
|
+
unique_append "$dest" COPIED_PATHS
|
|
815
|
+
fi
|
|
643
816
|
}
|
|
644
817
|
|
|
645
818
|
record_agentic_event() {
|
|
@@ -679,8 +852,10 @@ write_file_with_agentic_marker() {
|
|
|
679
852
|
can_write_managed_file "$dest" || return 0
|
|
680
853
|
|
|
681
854
|
ensure_dir "$(dirname -- "$dest")"
|
|
682
|
-
|
|
855
|
+
local write_status
|
|
856
|
+
write_status="$(python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" "$(app_version_label)" <<'PY'
|
|
683
857
|
import json
|
|
858
|
+
import re
|
|
684
859
|
import sys
|
|
685
860
|
from pathlib import Path
|
|
686
861
|
|
|
@@ -688,6 +863,7 @@ src = Path(sys.argv[1])
|
|
|
688
863
|
dest = Path(sys.argv[2])
|
|
689
864
|
source_ref = sys.argv[3]
|
|
690
865
|
repo = sys.argv[4]
|
|
866
|
+
version = sys.argv[5]
|
|
691
867
|
text = src.read_text(encoding="utf-8")
|
|
692
868
|
suffix = dest.suffix.lower()
|
|
693
869
|
marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
|
|
@@ -697,12 +873,28 @@ def yaml_quote(value: str) -> str:
|
|
|
697
873
|
return json.dumps(value, ensure_ascii=False)
|
|
698
874
|
|
|
699
875
|
|
|
876
|
+
def existing_created_by() -> str:
|
|
877
|
+
if not dest.exists():
|
|
878
|
+
return version
|
|
879
|
+
try:
|
|
880
|
+
old = dest.read_text(encoding="utf-8")
|
|
881
|
+
except Exception:
|
|
882
|
+
return version
|
|
883
|
+
match = re.search(r"(?m)^ created_by:\s*(.+?)\s*$", old)
|
|
884
|
+
if not match:
|
|
885
|
+
return version
|
|
886
|
+
return match.group(1).strip().strip('"')
|
|
887
|
+
|
|
888
|
+
|
|
700
889
|
def markdown_with_marker(body: str) -> str:
|
|
890
|
+
created_by = existing_created_by()
|
|
701
891
|
block = (
|
|
702
892
|
"agentic:\n"
|
|
703
893
|
" generated_by: agentic\n"
|
|
704
894
|
f" source: {yaml_quote(source_ref)}\n"
|
|
705
895
|
f" repository: {yaml_quote(repo)}\n"
|
|
896
|
+
f" created_by: {yaml_quote(created_by)}\n"
|
|
897
|
+
f" updated_by: {yaml_quote(version)}\n"
|
|
706
898
|
)
|
|
707
899
|
if body.startswith("---\n"):
|
|
708
900
|
end = body.find("\n---", 4)
|
|
@@ -739,9 +931,23 @@ elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
|
|
|
739
931
|
else:
|
|
740
932
|
output = commented(text, "#")
|
|
741
933
|
|
|
934
|
+
if dest.exists():
|
|
935
|
+
try:
|
|
936
|
+
if dest.read_text(encoding="utf-8") == output:
|
|
937
|
+
print("unchanged")
|
|
938
|
+
raise SystemExit(0)
|
|
939
|
+
except UnicodeDecodeError:
|
|
940
|
+
pass
|
|
941
|
+
|
|
742
942
|
dest.write_text(output, encoding="utf-8")
|
|
943
|
+
print("written")
|
|
743
944
|
PY
|
|
744
|
-
|
|
945
|
+
)"
|
|
946
|
+
if [[ "$write_status" == "unchanged" ]]; then
|
|
947
|
+
register_managed_file "$dest" "$source_ref" "internal" false
|
|
948
|
+
else
|
|
949
|
+
register_managed_file "$dest" "$source_ref" "internal"
|
|
950
|
+
fi
|
|
745
951
|
}
|
|
746
952
|
|
|
747
953
|
write_agentic_manifest() {
|
|
@@ -767,7 +973,7 @@ write_agentic_manifest() {
|
|
|
767
973
|
: > "$skipped_file"
|
|
768
974
|
fi
|
|
769
975
|
|
|
770
|
-
local agent_os_csv areas_csv specs_csv
|
|
976
|
+
local agent_os_csv areas_csv specs_csv mcp_integrations_csv
|
|
771
977
|
local old_ifs="$IFS"
|
|
772
978
|
IFS=,
|
|
773
979
|
agent_os_csv="${SELECTED_AGENT_OS[*]}"
|
|
@@ -775,7 +981,21 @@ write_agentic_manifest() {
|
|
|
775
981
|
specs_csv="${SELECTED_SPECS[*]}"
|
|
776
982
|
IFS="$old_ifs"
|
|
777
983
|
|
|
778
|
-
|
|
984
|
+
# Build mcp_integrations list from current env selections
|
|
985
|
+
local mcp_integrations=()
|
|
986
|
+
if [[ "${AGENTIC_ENABLE_CONTEXT7:-}" =~ ^[Yy](es)?$ ]]; then
|
|
987
|
+
mcp_integrations+=("context7")
|
|
988
|
+
fi
|
|
989
|
+
if [[ "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy](es)?$ ]]; then
|
|
990
|
+
mcp_integrations+=("mempalace")
|
|
991
|
+
fi
|
|
992
|
+
old_ifs="$IFS"
|
|
993
|
+
IFS=,
|
|
994
|
+
mcp_integrations_csv="${mcp_integrations[*]:-}"
|
|
995
|
+
IFS="$old_ifs"
|
|
996
|
+
|
|
997
|
+
local manifest_status
|
|
998
|
+
manifest_status="$(python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" "$(app_version_label)" "$mcp_integrations_csv" <<'PY'
|
|
779
999
|
import json
|
|
780
1000
|
import sys
|
|
781
1001
|
from datetime import datetime, timezone
|
|
@@ -789,37 +1009,56 @@ repo_root = sys.argv[5]
|
|
|
789
1009
|
agent_os = [x for x in sys.argv[6].split(",") if x]
|
|
790
1010
|
areas = [x for x in sys.argv[7].split(",") if x]
|
|
791
1011
|
specs = [x for x in sys.argv[8].split(",") if x]
|
|
1012
|
+
app_version = sys.argv[9]
|
|
1013
|
+
mcp_integrations = [x for x in sys.argv[10].split(",") if x] if len(sys.argv) > 10 else []
|
|
792
1014
|
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
793
1015
|
|
|
794
1016
|
existing = {}
|
|
795
1017
|
created_at = now
|
|
1018
|
+
old_data = None
|
|
796
1019
|
if manifest.exists():
|
|
797
1020
|
try:
|
|
798
1021
|
old = json.loads(manifest.read_text(encoding="utf-8"))
|
|
1022
|
+
old_data = old
|
|
799
1023
|
created_at = old.get("created_at", created_at)
|
|
800
1024
|
for item in old.get("managed_files", []):
|
|
801
1025
|
if item.get("path"):
|
|
802
1026
|
existing[item["path"]] = item
|
|
803
1027
|
except Exception:
|
|
804
1028
|
existing = {}
|
|
1029
|
+
original_existing = json.loads(json.dumps(existing))
|
|
805
1030
|
|
|
806
1031
|
for line in records_file.read_text(encoding="utf-8").splitlines():
|
|
807
1032
|
if not line:
|
|
808
1033
|
continue
|
|
809
1034
|
path, source, digest, marker = (line.split("|", 3) + ["", "", "", ""])[:4]
|
|
1035
|
+
old_item = original_existing.get(path, {})
|
|
1036
|
+
old_updated_at = old_item.get("updated_at", now)
|
|
1037
|
+
if (
|
|
1038
|
+
old_item.get("source") == source
|
|
1039
|
+
and old_item.get("content_hash") == digest
|
|
1040
|
+
and old_item.get("marker") == marker
|
|
1041
|
+
):
|
|
1042
|
+
item_updated_at = old_updated_at
|
|
1043
|
+
else:
|
|
1044
|
+
item_updated_at = now
|
|
810
1045
|
existing[path] = {
|
|
811
1046
|
"path": path,
|
|
812
1047
|
"source": source,
|
|
813
1048
|
"content_hash": digest,
|
|
814
1049
|
"marker": marker,
|
|
815
|
-
"updated_at":
|
|
1050
|
+
"updated_at": item_updated_at,
|
|
816
1051
|
}
|
|
817
1052
|
|
|
818
1053
|
skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
|
|
1054
|
+
old_agentic = old_data.get("_agentic", {}) if isinstance(old_data, dict) else {}
|
|
1055
|
+
created_by = old_agentic.get("created_by", app_version)
|
|
819
1056
|
data = {
|
|
820
1057
|
"_agentic": {
|
|
821
1058
|
"generated_by": "agentic",
|
|
822
1059
|
"repository": repo_link,
|
|
1060
|
+
"created_by": created_by,
|
|
1061
|
+
"updated_by": app_version,
|
|
823
1062
|
},
|
|
824
1063
|
"version": 1,
|
|
825
1064
|
"created_at": created_at,
|
|
@@ -828,15 +1067,33 @@ data = {
|
|
|
828
1067
|
"agent_os": agent_os,
|
|
829
1068
|
"areas": areas,
|
|
830
1069
|
"specializations": specs,
|
|
1070
|
+
"mcp_integrations": mcp_integrations,
|
|
831
1071
|
"source_repo": repo_link,
|
|
832
1072
|
"source_checkout": repo_root,
|
|
833
1073
|
},
|
|
834
1074
|
"managed_files": sorted(existing.values(), key=lambda x: x["path"]),
|
|
835
1075
|
"skipped_files": skipped,
|
|
836
1076
|
}
|
|
1077
|
+
|
|
1078
|
+
if old_data is not None:
|
|
1079
|
+
old_compare = json.loads(json.dumps(old_data))
|
|
1080
|
+
new_compare = json.loads(json.dumps(data))
|
|
1081
|
+
for payload in (old_compare, new_compare):
|
|
1082
|
+
payload.pop("updated_at", None)
|
|
1083
|
+
if isinstance(payload.get("_agentic"), dict):
|
|
1084
|
+
payload["_agentic"].pop("updated_by", None)
|
|
1085
|
+
if old_compare == new_compare:
|
|
1086
|
+
print("unchanged")
|
|
1087
|
+
raise SystemExit(0)
|
|
1088
|
+
|
|
837
1089
|
manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
1090
|
+
print("written")
|
|
838
1091
|
PY
|
|
1092
|
+
)"
|
|
839
1093
|
rm -f "$records_file" "$skipped_file"
|
|
1094
|
+
if [[ "$manifest_status" == "unchanged" ]]; then
|
|
1095
|
+
return
|
|
1096
|
+
fi
|
|
840
1097
|
unique_append "$manifest" COPIED_PATHS
|
|
841
1098
|
}
|
|
842
1099
|
|
|
@@ -853,7 +1110,7 @@ from pathlib import Path
|
|
|
853
1110
|
|
|
854
1111
|
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
855
1112
|
settings = data.get("settings", {})
|
|
856
|
-
for key in ("agent_os", "areas", "specializations"):
|
|
1113
|
+
for key in ("agent_os", "areas", "specializations", "mcp_integrations"):
|
|
857
1114
|
print("::" + key)
|
|
858
1115
|
for value in settings.get(key, []):
|
|
859
1116
|
print(value)
|
|
@@ -864,17 +1121,20 @@ PY
|
|
|
864
1121
|
local loaded_agent_os=()
|
|
865
1122
|
local loaded_areas=()
|
|
866
1123
|
local loaded_specs=()
|
|
1124
|
+
local loaded_mcp_integrations=()
|
|
867
1125
|
local value
|
|
868
1126
|
for value in "${values[@]}"; do
|
|
869
1127
|
case "$value" in
|
|
870
1128
|
"::agent_os") section="agent_os" ;;
|
|
871
1129
|
"::areas") section="areas" ;;
|
|
872
1130
|
"::specializations") section="specializations" ;;
|
|
1131
|
+
"::mcp_integrations") section="mcp_integrations" ;;
|
|
873
1132
|
*)
|
|
874
1133
|
case "$section" in
|
|
875
1134
|
agent_os) loaded_agent_os+=("$value") ;;
|
|
876
1135
|
areas) loaded_areas+=("$value") ;;
|
|
877
1136
|
specializations) loaded_specs+=("$value") ;;
|
|
1137
|
+
mcp_integrations) loaded_mcp_integrations+=("$value") ;;
|
|
878
1138
|
esac
|
|
879
1139
|
;;
|
|
880
1140
|
esac
|
|
@@ -889,6 +1149,23 @@ PY
|
|
|
889
1149
|
if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
|
|
890
1150
|
SELECTED_SPECS=("${loaded_specs[@]}")
|
|
891
1151
|
fi
|
|
1152
|
+
|
|
1153
|
+
# Restore MCP integration selections so configure_*_if_needed skip interactive prompts
|
|
1154
|
+
local mcp_item
|
|
1155
|
+
for mcp_item in "${loaded_mcp_integrations[@]}"; do
|
|
1156
|
+
case "$mcp_item" in
|
|
1157
|
+
context7)
|
|
1158
|
+
if [[ -z "${AGENTIC_ENABLE_CONTEXT7:-}" ]]; then
|
|
1159
|
+
AGENTIC_ENABLE_CONTEXT7="y"
|
|
1160
|
+
fi
|
|
1161
|
+
;;
|
|
1162
|
+
mempalace)
|
|
1163
|
+
if [[ -z "${AGENTIC_ENABLE_MEMPALACE:-}" ]]; then
|
|
1164
|
+
AGENTIC_ENABLE_MEMPALACE="y"
|
|
1165
|
+
fi
|
|
1166
|
+
;;
|
|
1167
|
+
esac
|
|
1168
|
+
done
|
|
892
1169
|
}
|
|
893
1170
|
|
|
894
1171
|
path_ref_for_shell_export() {
|
|
@@ -976,9 +1253,10 @@ copy_dir_contents() {
|
|
|
976
1253
|
|
|
977
1254
|
local event kind value events_file
|
|
978
1255
|
events_file="$(mktemp "${TMPDIR:-/tmp}/agentic-copy-events.XXXXXX")"
|
|
979
|
-
python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" > "$events_file" <<'PY'
|
|
1256
|
+
python3 - "$src" "$dest" "$REPO_ROOT" "$PROJECT_DIR" "$(project_manifest_path)" "$APP_REPO_LINK" "$(app_version_label)" > "$events_file" <<'PY'
|
|
980
1257
|
import hashlib
|
|
981
1258
|
import json
|
|
1259
|
+
import re
|
|
982
1260
|
import sys
|
|
983
1261
|
from pathlib import Path
|
|
984
1262
|
|
|
@@ -988,6 +1266,7 @@ repo_root = Path(sys.argv[3])
|
|
|
988
1266
|
project_dir = Path(sys.argv[4])
|
|
989
1267
|
manifest = Path(sys.argv[5])
|
|
990
1268
|
repo = sys.argv[6]
|
|
1269
|
+
version = sys.argv[7]
|
|
991
1270
|
|
|
992
1271
|
|
|
993
1272
|
def emit(kind: str, value: str) -> None:
|
|
@@ -1010,7 +1289,7 @@ if manifest.exists():
|
|
|
1010
1289
|
for item in data.get("managed_files", []):
|
|
1011
1290
|
rel = item.get("path")
|
|
1012
1291
|
if rel:
|
|
1013
|
-
managed[rel] = item
|
|
1292
|
+
managed[rel] = item
|
|
1014
1293
|
except Exception:
|
|
1015
1294
|
managed = {}
|
|
1016
1295
|
|
|
@@ -1033,12 +1312,28 @@ def yaml_quote(value: str) -> str:
|
|
|
1033
1312
|
return json.dumps(value, ensure_ascii=False)
|
|
1034
1313
|
|
|
1035
1314
|
|
|
1036
|
-
def
|
|
1315
|
+
def existing_created_by(target: Path) -> str:
|
|
1316
|
+
if not target.exists():
|
|
1317
|
+
return version
|
|
1318
|
+
try:
|
|
1319
|
+
old = target.read_text(encoding="utf-8")
|
|
1320
|
+
except Exception:
|
|
1321
|
+
return version
|
|
1322
|
+
match = re.search(r"(?m)^ created_by:\s*(.+?)\s*$", old)
|
|
1323
|
+
if not match:
|
|
1324
|
+
return version
|
|
1325
|
+
return match.group(1).strip().strip('"')
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def markdown_with_marker(body: str, source_ref: str, target: Path) -> str:
|
|
1329
|
+
created_by = existing_created_by(target)
|
|
1037
1330
|
block = (
|
|
1038
1331
|
"agentic:\n"
|
|
1039
1332
|
" generated_by: agentic\n"
|
|
1040
1333
|
f" source: {yaml_quote(source_ref)}\n"
|
|
1041
1334
|
f" repository: {yaml_quote(repo)}\n"
|
|
1335
|
+
f" created_by: {yaml_quote(created_by)}\n"
|
|
1336
|
+
f" updated_by: {yaml_quote(version)}\n"
|
|
1042
1337
|
)
|
|
1043
1338
|
if body.startswith("---\n"):
|
|
1044
1339
|
end = body.find("\n---", 4)
|
|
@@ -1061,7 +1356,7 @@ def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
|
|
|
1061
1356
|
text = file_path.read_text(encoding="utf-8")
|
|
1062
1357
|
suffix = target.suffix.lower()
|
|
1063
1358
|
if suffix == ".md":
|
|
1064
|
-
return markdown_with_marker(text, source_ref)
|
|
1359
|
+
return markdown_with_marker(text, source_ref, target)
|
|
1065
1360
|
if suffix == ".json":
|
|
1066
1361
|
data = json.loads(text)
|
|
1067
1362
|
if not isinstance(data, dict):
|
|
@@ -1087,15 +1382,27 @@ for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
|
|
|
1087
1382
|
emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
|
|
1088
1383
|
emit("SKIP", project_rel)
|
|
1089
1384
|
continue
|
|
1090
|
-
|
|
1385
|
+
managed_item = managed.get(project_rel, {})
|
|
1386
|
+
if managed_item.get("marker") == "config":
|
|
1387
|
+
continue
|
|
1388
|
+
expected_hash = managed_item.get("content_hash", "")
|
|
1091
1389
|
if target.exists() and expected_hash and sha256(target) != expected_hash:
|
|
1092
1390
|
emit("WARN", f"Skipping user-modified managed file: {project_rel}")
|
|
1093
1391
|
emit("SKIP", project_rel)
|
|
1094
1392
|
continue
|
|
1095
1393
|
|
|
1394
|
+
output = add_marker(file_path, target, source_ref)
|
|
1096
1395
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1097
1396
|
emit("DIR", str(target.parent))
|
|
1098
|
-
target.
|
|
1397
|
+
if target.exists():
|
|
1398
|
+
try:
|
|
1399
|
+
if target.read_text(encoding="utf-8") == output:
|
|
1400
|
+
digest = sha256(target)
|
|
1401
|
+
emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
|
|
1402
|
+
continue
|
|
1403
|
+
except UnicodeDecodeError:
|
|
1404
|
+
pass
|
|
1405
|
+
target.write_text(output, encoding="utf-8")
|
|
1099
1406
|
digest = sha256(target)
|
|
1100
1407
|
emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
|
|
1101
1408
|
emit("COPIED", str(target))
|
|
@@ -1146,7 +1453,8 @@ write_json_file_with_agentic_metadata() {
|
|
|
1146
1453
|
|
|
1147
1454
|
can_write_managed_file "$dest" || return 0
|
|
1148
1455
|
ensure_dir "$(dirname -- "$dest")"
|
|
1149
|
-
|
|
1456
|
+
local write_status
|
|
1457
|
+
write_status="$(python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" "$(app_version_label)" <<'PY'
|
|
1150
1458
|
import json
|
|
1151
1459
|
import sys
|
|
1152
1460
|
from pathlib import Path
|
|
@@ -1156,11 +1464,15 @@ source_ref = sys.argv[2]
|
|
|
1156
1464
|
repo = sys.argv[3]
|
|
1157
1465
|
context7_api_key = sys.argv[4]
|
|
1158
1466
|
body = sys.argv[5]
|
|
1467
|
+
version = sys.argv[6]
|
|
1159
1468
|
|
|
1160
1469
|
data = {}
|
|
1470
|
+
created_by = version
|
|
1161
1471
|
if path.exists():
|
|
1162
1472
|
try:
|
|
1163
1473
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
1474
|
+
if isinstance(data, dict):
|
|
1475
|
+
created_by = data.get("_agentic", {}).get("created_by", version)
|
|
1164
1476
|
except Exception:
|
|
1165
1477
|
data = {}
|
|
1166
1478
|
if not isinstance(data, dict):
|
|
@@ -1172,9 +1484,28 @@ namespace = {
|
|
|
1172
1484
|
}
|
|
1173
1485
|
exec(body, namespace)
|
|
1174
1486
|
data = namespace["data"]
|
|
1487
|
+
metadata = data.setdefault("_agentic", {})
|
|
1488
|
+
metadata["generated_by"] = "agentic"
|
|
1489
|
+
metadata["repository"] = repo
|
|
1490
|
+
metadata["created_by"] = created_by
|
|
1491
|
+
metadata["updated_by"] = version
|
|
1492
|
+
output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
1493
|
+
if path.exists():
|
|
1494
|
+
try:
|
|
1495
|
+
if path.read_text(encoding="utf-8") == output:
|
|
1496
|
+
print("unchanged")
|
|
1497
|
+
raise SystemExit(0)
|
|
1498
|
+
except UnicodeDecodeError:
|
|
1499
|
+
pass
|
|
1175
1500
|
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
1501
|
+
print("written")
|
|
1176
1502
|
PY
|
|
1177
|
-
|
|
1503
|
+
)"
|
|
1504
|
+
if [[ "$write_status" == "unchanged" ]]; then
|
|
1505
|
+
register_managed_file "$dest" "$source_ref" "internal" false
|
|
1506
|
+
else
|
|
1507
|
+
register_managed_file "$dest" "$source_ref" "internal"
|
|
1508
|
+
fi
|
|
1178
1509
|
}
|
|
1179
1510
|
|
|
1180
1511
|
write_json_config_file() {
|
|
@@ -1190,7 +1521,8 @@ write_json_config_file() {
|
|
|
1190
1521
|
|
|
1191
1522
|
can_write_managed_file "$dest" || return 0
|
|
1192
1523
|
ensure_dir "$(dirname -- "$dest")"
|
|
1193
|
-
|
|
1524
|
+
local write_status
|
|
1525
|
+
write_status="$(python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
|
|
1194
1526
|
import json
|
|
1195
1527
|
import sys
|
|
1196
1528
|
from pathlib import Path
|
|
@@ -1214,9 +1546,62 @@ namespace = {
|
|
|
1214
1546
|
}
|
|
1215
1547
|
exec(body, namespace)
|
|
1216
1548
|
data = namespace["data"]
|
|
1217
|
-
|
|
1549
|
+
output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
1550
|
+
if path.exists():
|
|
1551
|
+
try:
|
|
1552
|
+
if path.read_text(encoding="utf-8") == output:
|
|
1553
|
+
print("unchanged")
|
|
1554
|
+
raise SystemExit(0)
|
|
1555
|
+
except UnicodeDecodeError:
|
|
1556
|
+
pass
|
|
1557
|
+
path.write_text(output, encoding="utf-8")
|
|
1558
|
+
print("written")
|
|
1559
|
+
PY
|
|
1560
|
+
)"
|
|
1561
|
+
if [[ "$write_status" == "unchanged" ]]; then
|
|
1562
|
+
register_managed_file "$dest" "$source_ref" "config" false
|
|
1563
|
+
else
|
|
1564
|
+
register_managed_file "$dest" "$source_ref" "config"
|
|
1565
|
+
fi
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
write_text_config_file() {
|
|
1569
|
+
local dest="$1"
|
|
1570
|
+
local source_ref="$2"
|
|
1571
|
+
local content="$3"
|
|
1572
|
+
|
|
1573
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1574
|
+
log "DRY-RUN write text config file $dest"
|
|
1575
|
+
unique_append "$dest" COPIED_PATHS
|
|
1576
|
+
return
|
|
1577
|
+
fi
|
|
1578
|
+
|
|
1579
|
+
can_write_managed_file "$dest" || return 0
|
|
1580
|
+
ensure_dir "$(dirname -- "$dest")"
|
|
1581
|
+
|
|
1582
|
+
local write_status
|
|
1583
|
+
write_status="$(python3 - "$dest" "$content" <<'PY'
|
|
1584
|
+
import sys
|
|
1585
|
+
from pathlib import Path
|
|
1586
|
+
|
|
1587
|
+
path = Path(sys.argv[1])
|
|
1588
|
+
content = sys.argv[2]
|
|
1589
|
+
if path.exists():
|
|
1590
|
+
try:
|
|
1591
|
+
if path.read_text(encoding="utf-8") == content:
|
|
1592
|
+
print("unchanged")
|
|
1593
|
+
raise SystemExit(0)
|
|
1594
|
+
except UnicodeDecodeError:
|
|
1595
|
+
pass
|
|
1596
|
+
path.write_text(content, encoding="utf-8")
|
|
1597
|
+
print("written")
|
|
1218
1598
|
PY
|
|
1219
|
-
|
|
1599
|
+
)"
|
|
1600
|
+
if [[ "$write_status" == "unchanged" ]]; then
|
|
1601
|
+
register_managed_file "$dest" "$source_ref" "config" false
|
|
1602
|
+
else
|
|
1603
|
+
register_managed_file "$dest" "$source_ref" "config"
|
|
1604
|
+
fi
|
|
1220
1605
|
}
|
|
1221
1606
|
|
|
1222
1607
|
write_context7_opencode_config() {
|
|
@@ -1250,22 +1635,32 @@ if context7_api_key:
|
|
|
1250
1635
|
context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
|
|
1251
1636
|
mcp["context7"] = context7
|
|
1252
1637
|
'
|
|
1253
|
-
|
|
1638
|
+
write_json_config_file "$dest" "generated:context7-opencode-legacy-config" "$body"
|
|
1254
1639
|
}
|
|
1255
1640
|
|
|
1256
1641
|
write_context7_codex_config() {
|
|
1257
1642
|
local dest="$PROJECT_DIR/.codex/config.toml"
|
|
1258
|
-
local
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1643
|
+
local body
|
|
1644
|
+
body="$(python3 - "$dest" "$CONTEXT7_API_KEY" <<'PY'
|
|
1645
|
+
import re
|
|
1646
|
+
import sys
|
|
1647
|
+
from pathlib import Path
|
|
1648
|
+
|
|
1649
|
+
path = Path(sys.argv[1])
|
|
1650
|
+
api_key = sys.argv[2]
|
|
1651
|
+
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
1652
|
+
text = re.sub(r"(?ms)^\[mcp_servers\.context7\]\n.*?(?=^\[|\Z)", "", text).strip()
|
|
1653
|
+
block = '[mcp_servers.context7]\nurl = "https://mcp.context7.com/mcp"\n'
|
|
1654
|
+
if api_key:
|
|
1655
|
+
escaped = api_key.replace("\\", "\\\\").replace('"', '\\"')
|
|
1656
|
+
block += f'http_headers = {{ "CONTEXT7_API_KEY" = "{escaped}" }}\n'
|
|
1657
|
+
if text:
|
|
1658
|
+
print(block + "\n" + text.rstrip() + "\n", end="")
|
|
1659
|
+
else:
|
|
1660
|
+
print(block, end="")
|
|
1661
|
+
PY
|
|
1662
|
+
)"
|
|
1663
|
+
write_text_config_file "$dest" "generated:context7-codex-config" "$body"
|
|
1269
1664
|
}
|
|
1270
1665
|
|
|
1271
1666
|
write_context7_claude_config() {
|
|
@@ -1345,12 +1740,49 @@ mcp_servers["context7"] = context7
|
|
|
1345
1740
|
write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
|
|
1346
1741
|
}
|
|
1347
1742
|
|
|
1743
|
+
print_context7_key_recommendation() {
|
|
1744
|
+
[[ -z "$CONTEXT7_API_KEY" ]] || return 0
|
|
1745
|
+
|
|
1746
|
+
out "Context7 MCP configured without an API key."
|
|
1747
|
+
out "To add a Context7 API key later, set CONTEXT7_API_KEY before rerunning agentic or edit the generated config:"
|
|
1748
|
+
|
|
1749
|
+
if selected_agent_os_contains "opencode"; then
|
|
1750
|
+
out " - $PROJECT_DIR/opencode.json"
|
|
1751
|
+
out " - $PROJECT_DIR/.opencode/opencode.json"
|
|
1752
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1753
|
+
fi
|
|
1754
|
+
if selected_agent_os_contains "codex"; then
|
|
1755
|
+
out " - $PROJECT_DIR/.codex/config.toml"
|
|
1756
|
+
out ' Example: http_headers = { "CONTEXT7_API_KEY" = "ctx7_your_api_key_here" }'
|
|
1757
|
+
fi
|
|
1758
|
+
if selected_agent_os_contains "claude"; then
|
|
1759
|
+
out " - $PROJECT_DIR/.mcp.json"
|
|
1760
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1761
|
+
fi
|
|
1762
|
+
if selected_agent_os_contains "cursor"; then
|
|
1763
|
+
out " - $PROJECT_DIR/.cursor/mcp.json"
|
|
1764
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1765
|
+
fi
|
|
1766
|
+
if selected_agent_os_contains "gemini"; then
|
|
1767
|
+
out " - $PROJECT_DIR/.gemini/settings.json"
|
|
1768
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1769
|
+
fi
|
|
1770
|
+
if selected_agent_os_contains "kilocode"; then
|
|
1771
|
+
out " - $PROJECT_DIR/.kilocode/mcp.json"
|
|
1772
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1773
|
+
fi
|
|
1774
|
+
if selected_agent_os_contains "antigravity"; then
|
|
1775
|
+
out " - $HOME/.gemini/antigravity/mcp_config.json"
|
|
1776
|
+
out ' Example: "headers": {"CONTEXT7_API_KEY": "ctx7_your_api_key_here"}'
|
|
1777
|
+
fi
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1348
1780
|
write_mempalace_opencode_config() {
|
|
1349
1781
|
local dest="$1"
|
|
1350
1782
|
local body
|
|
1351
1783
|
body='
|
|
1352
1784
|
mcp = data.setdefault("mcp", {})
|
|
1353
|
-
mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"
|
|
1785
|
+
mcp["mempalace"] = {"type": "local", "command": ["mempalace-mcp"]}
|
|
1354
1786
|
'
|
|
1355
1787
|
write_json_config_file "$dest" "generated:mempalace-opencode-config" "$body"
|
|
1356
1788
|
}
|
|
@@ -1359,18 +1791,21 @@ write_mempalace_codex_config() {
|
|
|
1359
1791
|
local dest="$PROJECT_DIR/.codex/config.toml"
|
|
1360
1792
|
local body
|
|
1361
1793
|
body="$(python3 - "$dest" <<'PYCODE'
|
|
1362
|
-
import
|
|
1794
|
+
import re
|
|
1795
|
+
import pathlib
|
|
1796
|
+
import sys
|
|
1797
|
+
|
|
1363
1798
|
path = pathlib.Path(sys.argv[1])
|
|
1364
1799
|
text = path.read_text(encoding='utf-8') if path.exists() else ''
|
|
1365
|
-
block = "[mcp_servers.mempalace]\ncommand = \"mempalace-mcp\"\
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
print(
|
|
1800
|
+
block = "[mcp_servers.mempalace]\ncommand = \"mempalace-mcp\"\n"
|
|
1801
|
+
text = re.sub(r"(?ms)^\[mcp_servers\.mempalace\]\n.*?(?=^\[|\Z)", "", text).strip()
|
|
1802
|
+
if text:
|
|
1803
|
+
print(text.rstrip() + "\n\n" + block, end="")
|
|
1804
|
+
else:
|
|
1805
|
+
print(block, end="")
|
|
1371
1806
|
PYCODE
|
|
1372
1807
|
)"
|
|
1373
|
-
|
|
1808
|
+
write_text_config_file "$dest" "generated:mempalace-codex-config" "$body"
|
|
1374
1809
|
}
|
|
1375
1810
|
|
|
1376
1811
|
write_mempalace_generic_json_config() {
|
|
@@ -1379,74 +1814,204 @@ write_mempalace_generic_json_config() {
|
|
|
1379
1814
|
local body
|
|
1380
1815
|
body='
|
|
1381
1816
|
servers = data.setdefault("mcpServers", {})
|
|
1382
|
-
servers["mempalace"] = {"command": "mempalace-mcp"
|
|
1817
|
+
servers["mempalace"] = {"command": "mempalace-mcp"}
|
|
1383
1818
|
'
|
|
1384
1819
|
write_json_config_file "$dest" "$marker" "$body"
|
|
1385
1820
|
}
|
|
1386
1821
|
|
|
1387
1822
|
print_mempalace_project_setup_instructions() {
|
|
1388
|
-
log "MemPalace
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1823
|
+
log "Optional MemPalace project indexing instructions for target project: $PROJECT_DIR"
|
|
1824
|
+
out "1) Ensure Python is installed and available in PATH."
|
|
1825
|
+
out "2) Install MemPalace:"
|
|
1826
|
+
out " pip install mempalace"
|
|
1827
|
+
out "3) Optionally initialize project-local MemPalace cache:"
|
|
1828
|
+
out " mempalace init \"$PROJECT_DIR\" --yes --auto-mine"
|
|
1829
|
+
out "4) Optionally index existing project memory:"
|
|
1830
|
+
out " # optional if --auto-mine was skipped"
|
|
1831
|
+
out " mempalace mine \"$PROJECT_DIR\""
|
|
1832
|
+
out "5) Verify in your IDE/agent that MemPalace MCP tools are connected."
|
|
1833
|
+
out "Note: Ollama at localhost:11434 is optional; MemPalace can run heuristics-only without it."
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
write_mempalace_ignore_file() {
|
|
1837
|
+
local dest="$PROJECT_DIR/.mempalaceignore"
|
|
1838
|
+
local content
|
|
1839
|
+
content='node_modules/
|
|
1840
|
+
.venv/
|
|
1841
|
+
venv/
|
|
1842
|
+
dist/
|
|
1843
|
+
build/
|
|
1844
|
+
target/
|
|
1845
|
+
coverage/
|
|
1846
|
+
.git/
|
|
1847
|
+
|
|
1848
|
+
*.csv
|
|
1849
|
+
*.parquet
|
|
1850
|
+
*.log
|
|
1851
|
+
*.jsonl
|
|
1852
|
+
|
|
1853
|
+
data/
|
|
1854
|
+
tmp/
|
|
1855
|
+
'
|
|
1856
|
+
|
|
1857
|
+
if [[ -e "$dest" ]]; then
|
|
1858
|
+
log "MemPalace ignore file already exists: $dest"
|
|
1859
|
+
return 0
|
|
1860
|
+
fi
|
|
1861
|
+
|
|
1862
|
+
write_text_config_file "$dest" "generated:mempalace-ignore" "$content"
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
warn_mempalace_failure_reason() {
|
|
1866
|
+
local output_file="$1"
|
|
1867
|
+
[[ -f "$output_file" ]] || return 0
|
|
1868
|
+
|
|
1869
|
+
if grep -Fq "incompatible architecture" "$output_file" && grep -Fq "numpy" "$output_file"; then
|
|
1870
|
+
warn "MemPalace failed because Python/NumPy architecture is inconsistent. Reinstall MemPalace dependencies with the same architecture as the Python running 'mempalace'."
|
|
1871
|
+
warn "Typical fix: reinstall numpy/chromadb/mempalace in the active Python environment, or use a matching arm64/x86_64 Python. See the MemPalace log above for the exact Python path."
|
|
1872
|
+
return 0
|
|
1873
|
+
fi
|
|
1874
|
+
|
|
1875
|
+
if grep -Fq "No LLM provider reachable" "$output_file"; then
|
|
1876
|
+
warn "MemPalace could not reach an LLM provider and continued heuristics-only; this is non-fatal unless a later dependency error appears."
|
|
1877
|
+
fi
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
run_mempalace_command() {
|
|
1881
|
+
local label="$1"
|
|
1882
|
+
shift
|
|
1883
|
+
local output_file
|
|
1884
|
+
output_file="$(mktemp "${TMPDIR:-/tmp}/agentic-mempalace.XXXXXX")"
|
|
1885
|
+
if "$@" >"$output_file" 2>&1; then
|
|
1886
|
+
log "$label completed"
|
|
1887
|
+
log_file_block "$label" "$output_file"
|
|
1888
|
+
rm -f "$output_file"
|
|
1889
|
+
return 0
|
|
1890
|
+
fi
|
|
1891
|
+
|
|
1892
|
+
warn "Failed: $* (log: $output_file)"
|
|
1893
|
+
log_file_block "$label" "$output_file"
|
|
1894
|
+
warn_mempalace_failure_reason "$output_file"
|
|
1895
|
+
return 1
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
mempalace_venv_dir() {
|
|
1899
|
+
printf '%s\n' "${AGENTIC_MEMPALACE_VENV:-$HOME/.venvs/mempalace}"
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
mempalace_bin_dir() {
|
|
1903
|
+
printf '%s\n' "${AGENTIC_MEMPALACE_BIN_DIR:-$HOME/.local/bin}"
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
python3_command() {
|
|
1907
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
1908
|
+
printf '%s\n' "python3"
|
|
1909
|
+
return 0
|
|
1910
|
+
fi
|
|
1911
|
+
if command -v python >/dev/null 2>&1; then
|
|
1912
|
+
printf '%s\n' "python"
|
|
1913
|
+
return 0
|
|
1914
|
+
fi
|
|
1915
|
+
return 1
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
install_mempalace_managed() {
|
|
1919
|
+
local py_bin venv_dir bin_dir venv_python venv_mempalace
|
|
1920
|
+
|
|
1921
|
+
py_bin="$(python3_command)" || return 1
|
|
1922
|
+
venv_dir="$(mempalace_venv_dir)"
|
|
1923
|
+
bin_dir="$(mempalace_bin_dir)"
|
|
1924
|
+
|
|
1925
|
+
mkdir -p "$(dirname "$venv_dir")" "$bin_dir"
|
|
1926
|
+
|
|
1927
|
+
if [[ ! -x "$venv_dir/bin/python" ]]; then
|
|
1928
|
+
"$py_bin" -m venv "$venv_dir" || return 1
|
|
1929
|
+
fi
|
|
1930
|
+
|
|
1931
|
+
venv_python="$venv_dir/bin/python"
|
|
1932
|
+
venv_mempalace="$venv_dir/bin/mempalace"
|
|
1933
|
+
|
|
1934
|
+
"$venv_python" -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || return 1
|
|
1935
|
+
"$venv_python" -m pip install --upgrade --no-cache-dir mempalace >/dev/null 2>&1 || return 1
|
|
1936
|
+
|
|
1937
|
+
[[ -x "$venv_mempalace" ]] || return 1
|
|
1938
|
+
|
|
1939
|
+
ln -sf "$venv_mempalace" "$bin_dir/mempalace"
|
|
1940
|
+
|
|
1941
|
+
if [[ -x "$venv_dir/bin/mempalace-mcp" ]]; then
|
|
1942
|
+
ln -sf "$venv_dir/bin/mempalace-mcp" "$bin_dir/mempalace-mcp"
|
|
1943
|
+
fi
|
|
1944
|
+
|
|
1945
|
+
export PATH="$bin_dir:$PATH"
|
|
1946
|
+
|
|
1947
|
+
command -v mempalace >/dev/null 2>&1
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
initialize_mempalace_project() {
|
|
1951
|
+
local step_prefix="$1"
|
|
1952
|
+
log "$step_prefix [4/4] Initializing project memory at $PROJECT_DIR"
|
|
1953
|
+
if ! command -v mempalace >/dev/null 2>&1; then
|
|
1954
|
+
warn "mempalace command is unavailable after install; please run setup manually"
|
|
1955
|
+
print_mempalace_project_setup_instructions
|
|
1956
|
+
return 1
|
|
1957
|
+
fi
|
|
1958
|
+
|
|
1959
|
+
if ! run_mempalace_command "MemPalace init" mempalace init "$PROJECT_DIR" --yes --auto-mine; then
|
|
1960
|
+
print_mempalace_project_setup_instructions
|
|
1961
|
+
return 1
|
|
1962
|
+
fi
|
|
1963
|
+
log "$step_prefix [4/4] Initialization step finished"
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
setup_mempalace_for_agentic() {
|
|
1967
|
+
local initialize_project="${1:-false}"
|
|
1405
1968
|
local step_prefix="MemPalace setup"
|
|
1406
1969
|
|
|
1407
1970
|
log "$step_prefix [1/4] Checking Python availability"
|
|
1408
1971
|
if ! command -v python3 >/dev/null 2>&1 && ! command -v python >/dev/null 2>&1; then
|
|
1409
1972
|
warn "Python is not installed. Install Python 3 first, then run: pip install mempalace"
|
|
1410
1973
|
warn "Install help: https://www.python.org/downloads/"
|
|
1974
|
+
print_mempalace_project_setup_instructions
|
|
1411
1975
|
return 1
|
|
1412
1976
|
fi
|
|
1413
1977
|
log "$step_prefix [1/4] Python check passed"
|
|
1414
1978
|
|
|
1979
|
+
if [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]] && command -v mempalace-mcp >/dev/null 2>&1; then
|
|
1980
|
+
if [[ "$initialize_project" != "true" ]] || command -v mempalace >/dev/null 2>&1; then
|
|
1981
|
+
log "$step_prefix [2/4] MemPalace binaries already available; skipping pip install"
|
|
1982
|
+
if [[ "$initialize_project" != "true" ]]; then
|
|
1983
|
+
log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
|
|
1984
|
+
return 0
|
|
1985
|
+
fi
|
|
1986
|
+
initialize_mempalace_project "$step_prefix"
|
|
1987
|
+
return $?
|
|
1988
|
+
fi
|
|
1989
|
+
fi
|
|
1990
|
+
|
|
1415
1991
|
log "$step_prefix [2/4] Checking pip availability"
|
|
1416
|
-
|
|
1992
|
+
local pip_bin
|
|
1993
|
+
if ! pip_bin="$(pip_command)"; then
|
|
1417
1994
|
warn "pip is not available. Install pip for Python 3, then run: pip install mempalace"
|
|
1995
|
+
print_mempalace_project_setup_instructions
|
|
1418
1996
|
return 1
|
|
1419
1997
|
fi
|
|
1420
1998
|
log "$step_prefix [2/4] pip check passed"
|
|
1421
1999
|
|
|
1422
2000
|
log "$step_prefix [3/4] Installing mempalace package"
|
|
1423
|
-
if
|
|
1424
|
-
log "MemPalace package installed via '
|
|
2001
|
+
if $pip_bin install mempalace >/dev/null 2>&1; then
|
|
2002
|
+
log "MemPalace package installed via '$pip_bin install mempalace'"
|
|
1425
2003
|
else
|
|
1426
2004
|
warn "Unable to auto-install mempalace via pip; continuing with manual setup instructions"
|
|
1427
2005
|
print_mempalace_project_setup_instructions
|
|
1428
2006
|
return 1
|
|
1429
2007
|
fi
|
|
1430
2008
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
if mempalace init "$PROJECT_DIR/.mempalace" --yes --auto-mine >/dev/null 2>&1; then
|
|
1435
|
-
log "MemPalace init completed"
|
|
1436
|
-
else
|
|
1437
|
-
warn "Failed: mempalace init \"$PROJECT_DIR/.mempalace\" --yes --auto-mine"
|
|
1438
|
-
fi
|
|
1439
|
-
if mempalace mine "$PROJECT_DIR/.mempalace" >/dev/null 2>&1; then
|
|
1440
|
-
log "MemPalace mine completed"
|
|
1441
|
-
else
|
|
1442
|
-
warn "Failed: mempalace mine \"$PROJECT_DIR/.mempalace\""
|
|
1443
|
-
fi
|
|
1444
|
-
log "$step_prefix [4/4] Initialization step finished"
|
|
1445
|
-
else
|
|
1446
|
-
warn "mempalace command is unavailable after install; please run setup manually"
|
|
1447
|
-
print_mempalace_project_setup_instructions
|
|
1448
|
-
return 1
|
|
2009
|
+
if [[ "$initialize_project" != "true" ]]; then
|
|
2010
|
+
log "$step_prefix [4/4] Project memory initialization skipped for selected agent target(s)"
|
|
2011
|
+
return 0
|
|
1449
2012
|
fi
|
|
2013
|
+
|
|
2014
|
+
initialize_mempalace_project "$step_prefix"
|
|
1450
2015
|
}
|
|
1451
2016
|
|
|
1452
2017
|
configure_mempalace_if_needed() {
|
|
@@ -1455,6 +2020,7 @@ configure_mempalace_if_needed() {
|
|
|
1455
2020
|
&& ! selected_agent_os_contains "claude" \
|
|
1456
2021
|
&& ! selected_agent_os_contains "cursor" \
|
|
1457
2022
|
&& ! selected_agent_os_contains "gemini" \
|
|
2023
|
+
&& ! selected_agent_os_contains "kilocode" \
|
|
1458
2024
|
&& ! selected_agent_os_contains "antigravity"; then
|
|
1459
2025
|
return
|
|
1460
2026
|
fi
|
|
@@ -1464,28 +2030,26 @@ configure_mempalace_if_needed() {
|
|
|
1464
2030
|
enable_mempalace="$(trim "${AGENTIC_ENABLE_MEMPALACE}")"
|
|
1465
2031
|
elif is_interactive_terminal && [[ -z "${AGENTIC_TEST_SOURCE_AGENTIC:-}" ]]; then
|
|
1466
2032
|
read -r -p "Enable MemPalace MCP memory integration? [y/N]: " enable_mempalace
|
|
1467
|
-
enable_mempalace="$(trim "${enable_mempalace:-
|
|
1468
|
-
if [[ -z "$enable_mempalace" ]]; then enable_mempalace="
|
|
2033
|
+
enable_mempalace="$(trim "${enable_mempalace:-n}")"
|
|
2034
|
+
if [[ -z "$enable_mempalace" ]]; then enable_mempalace="n"; fi
|
|
1469
2035
|
fi
|
|
1470
2036
|
if [[ "$enable_mempalace" =~ ^[Nn]$ ]]; then
|
|
1471
2037
|
log "Skipped MemPalace MCP configuration"
|
|
1472
2038
|
return
|
|
1473
2039
|
fi
|
|
1474
2040
|
|
|
1475
|
-
|
|
1476
|
-
setup_mempalace_for_agentic_opencode || true
|
|
1477
|
-
else
|
|
1478
|
-
print_mempalace_project_setup_instructions
|
|
1479
|
-
fi
|
|
2041
|
+
write_mempalace_ignore_file
|
|
1480
2042
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
2043
|
+
local initialize_mempalace_project="true"
|
|
2044
|
+
local mempalace_setup_ok="true"
|
|
2045
|
+
setup_mempalace_for_agentic "$initialize_mempalace_project" || mempalace_setup_ok="false"
|
|
2046
|
+
|
|
2047
|
+
if [[ "$mempalace_setup_ok" != "true" ]]; then
|
|
2048
|
+
if ! command -v mempalace-mcp >/dev/null 2>&1; then
|
|
2049
|
+
warn "mempalace-mcp is unavailable; install/repair MemPalace and re-run setup"
|
|
1486
2050
|
fi
|
|
1487
2051
|
else
|
|
1488
|
-
|
|
2052
|
+
log "MemPalace MCP binary found: mempalace-mcp"
|
|
1489
2053
|
fi
|
|
1490
2054
|
|
|
1491
2055
|
if selected_agent_os_contains "opencode"; then
|
|
@@ -1502,6 +2066,9 @@ configure_mempalace_if_needed() {
|
|
|
1502
2066
|
if selected_agent_os_contains "gemini"; then
|
|
1503
2067
|
write_mempalace_generic_json_config "$PROJECT_DIR/.gemini/settings.json" "generated:mempalace-gemini-config"
|
|
1504
2068
|
fi
|
|
2069
|
+
if selected_agent_os_contains "kilocode"; then
|
|
2070
|
+
write_mempalace_generic_json_config "$PROJECT_DIR/.kilocode/mcp.json" "generated:mempalace-kilocode-config"
|
|
2071
|
+
fi
|
|
1505
2072
|
if selected_agent_os_contains "antigravity"; then
|
|
1506
2073
|
write_mempalace_generic_json_config "$HOME/.gemini/antigravity/mcp_config.json" "generated:mempalace-antigravity-config"
|
|
1507
2074
|
fi
|
|
@@ -1518,18 +2085,25 @@ configure_context7_if_needed() {
|
|
|
1518
2085
|
return
|
|
1519
2086
|
fi
|
|
1520
2087
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
|
|
2088
|
+
local enable_context7="${AGENTIC_ENABLE_CONTEXT7:-}"
|
|
2089
|
+
if [[ -n "$enable_context7" ]]; then
|
|
1524
2090
|
enable_context7="$(trim "$enable_context7")"
|
|
2091
|
+
fi
|
|
2092
|
+
|
|
2093
|
+
if is_interactive_terminal; then
|
|
2094
|
+
if [[ -z "$enable_context7" ]]; then
|
|
2095
|
+
read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
|
|
2096
|
+
enable_context7="$(trim "$enable_context7")"
|
|
2097
|
+
fi
|
|
1525
2098
|
if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
|
|
1526
2099
|
log "Context7 MCP configuration disabled"
|
|
1527
2100
|
return
|
|
1528
2101
|
fi
|
|
1529
2102
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2103
|
+
elif [[ -n "$enable_context7" ]]; then
|
|
2104
|
+
if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
|
|
2105
|
+
log "Context7 MCP configuration disabled"
|
|
2106
|
+
return
|
|
1533
2107
|
fi
|
|
1534
2108
|
elif [[ -z "$CONTEXT7_API_KEY" ]]; then
|
|
1535
2109
|
log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
|
|
@@ -1564,6 +2138,8 @@ configure_context7_if_needed() {
|
|
|
1564
2138
|
if selected_agent_os_contains "antigravity"; then
|
|
1565
2139
|
write_context7_antigravity_config
|
|
1566
2140
|
fi
|
|
2141
|
+
|
|
2142
|
+
print_context7_key_recommendation
|
|
1567
2143
|
}
|
|
1568
2144
|
|
|
1569
2145
|
write_default_opencode_plugin_config() {
|
|
@@ -1578,8 +2154,8 @@ from pathlib import Path
|
|
|
1578
2154
|
|
|
1579
2155
|
path = Path(sys.argv[1])
|
|
1580
2156
|
path.write_text(json.dumps({
|
|
1581
|
-
"telegram": {"enabled": False
|
|
1582
|
-
"
|
|
2157
|
+
"telegram": {"enabled": False},
|
|
2158
|
+
"agentModelMapper": {"enabled": False},
|
|
1583
2159
|
}, indent=2) + "\n", encoding="utf-8")
|
|
1584
2160
|
PY
|
|
1585
2161
|
fi
|
|
@@ -1596,6 +2172,12 @@ configure_opencode_plugins_if_needed() {
|
|
|
1596
2172
|
ensure_python_available
|
|
1597
2173
|
ensure_dir "$APP_CONFIG_DIR"
|
|
1598
2174
|
|
|
2175
|
+
# During upgrade/re-install with existing plugin config, keep current settings
|
|
2176
|
+
if [[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" && ( -n "${AGENTIC_ENABLE_MEMPALACE:-}" || -n "${AGENTIC_ENABLE_CONTEXT7:-}" ) ]]; then
|
|
2177
|
+
log "OpenCode plugin config already exists; keeping current settings"
|
|
2178
|
+
return
|
|
2179
|
+
fi
|
|
2180
|
+
|
|
1599
2181
|
if ! is_interactive_terminal; then
|
|
1600
2182
|
if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
|
|
1601
2183
|
write_default_opencode_plugin_config
|
|
@@ -1603,42 +2185,378 @@ configure_opencode_plugins_if_needed() {
|
|
|
1603
2185
|
return
|
|
1604
2186
|
fi
|
|
1605
2187
|
|
|
1606
|
-
local
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
read -r -p "Telegram chat id (empty disables plugin): " telegram_chat
|
|
1614
|
-
telegram_token="$(trim "$telegram_token")"
|
|
1615
|
-
telegram_chat="$(trim "$telegram_chat")"
|
|
2188
|
+
local plugin_options=("telegram-opencode-notifier" "agent-model-mapper")
|
|
2189
|
+
local selected_plugins=()
|
|
2190
|
+
local use_fzf_plugins=false
|
|
2191
|
+
if fzf_available; then
|
|
2192
|
+
use_fzf_plugins=true
|
|
2193
|
+
elif ensure_fzf_or_fallback; then
|
|
2194
|
+
use_fzf_plugins=true
|
|
1616
2195
|
fi
|
|
1617
2196
|
|
|
1618
|
-
|
|
1619
|
-
|
|
2197
|
+
if [[ "$use_fzf_plugins" == true ]]; then
|
|
2198
|
+
readlines selected_plugins < <(choose_multi_fzf_strict "Select optional OpenCode plugin(s):" "${plugin_options[@]}")
|
|
2199
|
+
else
|
|
2200
|
+
local selected_plugins_output
|
|
2201
|
+
selected_plugins_output="$(choose_multi_by_index "Select optional OpenCode plugin(s):" "${plugin_options[@]}")"
|
|
2202
|
+
readlines selected_plugins <<< "$selected_plugins_output"
|
|
2203
|
+
fi
|
|
2204
|
+
|
|
2205
|
+
local enable_telegram="n" enable_agent_model_mapper="n"
|
|
2206
|
+
local selected_plugin
|
|
2207
|
+
for selected_plugin in "${selected_plugins[@]}"; do
|
|
2208
|
+
selected_plugin="$(trim "$selected_plugin")"
|
|
2209
|
+
[[ -z "$selected_plugin" ]] && continue
|
|
2210
|
+
case "$selected_plugin" in
|
|
2211
|
+
telegram-opencode-notifier) enable_telegram="y" ;;
|
|
2212
|
+
agent-model-mapper) enable_agent_model_mapper="y" ;;
|
|
2213
|
+
esac
|
|
2214
|
+
done
|
|
2215
|
+
|
|
2216
|
+
if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
|
|
2217
|
+
log "Telegram plugin enabled; credentials are read only from OPENCODE_TELEGRAM_BOT_TOKEN and OPENCODE_TELEGRAM_CHAT_ID"
|
|
2218
|
+
fi
|
|
1620
2219
|
|
|
1621
|
-
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$
|
|
2220
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$enable_telegram" "$enable_agent_model_mapper" <<'PY'
|
|
1622
2221
|
import json
|
|
1623
2222
|
import sys
|
|
1624
2223
|
from pathlib import Path
|
|
1625
2224
|
|
|
1626
2225
|
path = Path(sys.argv[1])
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
enable_model = sys.argv[4].lower() == "y"
|
|
2226
|
+
enable_telegram = sys.argv[2].lower() == "y"
|
|
2227
|
+
enable_mapper = sys.argv[3].lower() == "y"
|
|
1630
2228
|
data = {
|
|
1631
2229
|
"telegram": {
|
|
1632
|
-
"enabled":
|
|
1633
|
-
"botToken": token,
|
|
1634
|
-
"chatId": chat,
|
|
2230
|
+
"enabled": enable_telegram,
|
|
1635
2231
|
},
|
|
1636
|
-
"
|
|
1637
|
-
"enabled":
|
|
2232
|
+
"agentModelMapper": {
|
|
2233
|
+
"enabled": enable_mapper,
|
|
1638
2234
|
},
|
|
1639
2235
|
}
|
|
1640
2236
|
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
1641
2237
|
PY
|
|
2238
|
+
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
opencode_agent_model_mapper_config_enabled() {
|
|
2242
|
+
[[ -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]] || return 1
|
|
2243
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
|
|
2244
|
+
import json
|
|
2245
|
+
import sys
|
|
2246
|
+
from pathlib import Path
|
|
2247
|
+
|
|
2248
|
+
try:
|
|
2249
|
+
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
2250
|
+
except Exception:
|
|
2251
|
+
raise SystemExit(1)
|
|
2252
|
+
raise SystemExit(0 if data.get("agentModelMapper", {}).get("enabled") is True else 1)
|
|
2253
|
+
PY
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
opencode_mapper_read_roles() {
|
|
2257
|
+
local agents_dir="$PROJECT_DIR/.opencode/agents"
|
|
2258
|
+
[[ -d "$agents_dir" ]] || return 0
|
|
2259
|
+
python3 - "$agents_dir" <<'PY'
|
|
2260
|
+
import sys
|
|
2261
|
+
from pathlib import Path
|
|
2262
|
+
|
|
2263
|
+
agents_dir = Path(sys.argv[1])
|
|
2264
|
+
|
|
2265
|
+
def parse_frontmatter(text):
|
|
2266
|
+
if not text.startswith("---\n"):
|
|
2267
|
+
return {}
|
|
2268
|
+
end = text.find("\n---", 4)
|
|
2269
|
+
if end == -1:
|
|
2270
|
+
return {}
|
|
2271
|
+
result = {}
|
|
2272
|
+
for line in text[4:end].splitlines():
|
|
2273
|
+
if ":" not in line:
|
|
2274
|
+
continue
|
|
2275
|
+
key, value = line.split(":", 1)
|
|
2276
|
+
result[key.strip()] = value.strip().strip("'\"")
|
|
2277
|
+
return result
|
|
2278
|
+
|
|
2279
|
+
for path in sorted(agents_dir.glob("*.md")):
|
|
2280
|
+
frontmatter = parse_frontmatter(path.read_text(encoding="utf-8"))
|
|
2281
|
+
name = path.stem.replace("\t", " ")
|
|
2282
|
+
mode = (frontmatter.get("mode") or "subagent").replace("\t", " ")
|
|
2283
|
+
description = (frontmatter.get("description") or "OpenCode agent").replace("\t", " ")
|
|
2284
|
+
print(f"{name}\t{mode}\t{description}")
|
|
2285
|
+
PY
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
opencode_mapper_discover_models() {
|
|
2289
|
+
local config_path="$HOME/.config/opencode/opencode.json"
|
|
2290
|
+
python3 - "$config_path" <<'PY'
|
|
2291
|
+
import json
|
|
2292
|
+
import sys
|
|
2293
|
+
from pathlib import Path
|
|
2294
|
+
|
|
2295
|
+
fallback = ["opencode/minimax-m2.5-free"]
|
|
2296
|
+
path = Path(sys.argv[1])
|
|
2297
|
+
models = []
|
|
2298
|
+
|
|
2299
|
+
def collect_provider_models(data):
|
|
2300
|
+
"""Extract models from provider.<name>.models dict keys."""
|
|
2301
|
+
providers = data.get("provider")
|
|
2302
|
+
if not isinstance(providers, dict):
|
|
2303
|
+
return
|
|
2304
|
+
for provider_name, provider_data in providers.items():
|
|
2305
|
+
if not isinstance(provider_data, dict):
|
|
2306
|
+
continue
|
|
2307
|
+
provider_models = provider_data.get("models")
|
|
2308
|
+
if not isinstance(provider_models, dict):
|
|
2309
|
+
continue
|
|
2310
|
+
for model_name in provider_models:
|
|
2311
|
+
if isinstance(model_name, str) and model_name.strip():
|
|
2312
|
+
models.append(f"{provider_name}/{model_name}")
|
|
2313
|
+
|
|
2314
|
+
def collect(value):
|
|
2315
|
+
if isinstance(value, list):
|
|
2316
|
+
for item in value:
|
|
2317
|
+
collect(item)
|
|
2318
|
+
return
|
|
2319
|
+
if not isinstance(value, dict):
|
|
2320
|
+
return
|
|
2321
|
+
for key, item in value.items():
|
|
2322
|
+
if key in {"model", "id"} and isinstance(item, str) and "/" in item:
|
|
2323
|
+
models.append(item)
|
|
2324
|
+
if key == "fallback" and isinstance(item, list):
|
|
2325
|
+
models.extend(model for model in item if isinstance(model, str))
|
|
2326
|
+
collect(item)
|
|
2327
|
+
|
|
2328
|
+
try:
|
|
2329
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
2330
|
+
collect_provider_models(data)
|
|
2331
|
+
collect(data)
|
|
2332
|
+
except Exception:
|
|
2333
|
+
pass
|
|
2334
|
+
|
|
2335
|
+
seen = set()
|
|
2336
|
+
for model in models or fallback:
|
|
2337
|
+
model = model.strip()
|
|
2338
|
+
if model and model not in seen:
|
|
2339
|
+
seen.add(model)
|
|
2340
|
+
print(model)
|
|
2341
|
+
PY
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
opencode_mapper_has_complete_mapping() {
|
|
2345
|
+
local roles_file="$1"
|
|
2346
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2347
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2348
|
+
python3 - "$roles_file" "$config_path" "$state_path" <<'PY'
|
|
2349
|
+
import json
|
|
2350
|
+
import sys
|
|
2351
|
+
from pathlib import Path
|
|
2352
|
+
|
|
2353
|
+
roles_file, config_path, state_path = map(Path, sys.argv[1:])
|
|
2354
|
+
try:
|
|
2355
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
2356
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
2357
|
+
except Exception:
|
|
2358
|
+
raise SystemExit(1)
|
|
2359
|
+
if not state.get("configured"):
|
|
2360
|
+
raise SystemExit(1)
|
|
2361
|
+
agents = config.get("agent")
|
|
2362
|
+
if not isinstance(agents, dict):
|
|
2363
|
+
raise SystemExit(1)
|
|
2364
|
+
roles = [line.split("\t", 1)[0] for line in roles_file.read_text(encoding="utf-8").splitlines() if line]
|
|
2365
|
+
for role in roles:
|
|
2366
|
+
agent = agents.get(role)
|
|
2367
|
+
if not isinstance(agent, dict) or not str(agent.get("model", "")).strip():
|
|
2368
|
+
raise SystemExit(1)
|
|
2369
|
+
raise SystemExit(0)
|
|
2370
|
+
PY
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
choose_opencode_mapper_model() {
|
|
2374
|
+
local role_name="$1"
|
|
2375
|
+
local role_mode="$2"
|
|
2376
|
+
local role_description="$3"
|
|
2377
|
+
local kind="$4"
|
|
2378
|
+
shift 4
|
|
2379
|
+
local models=("$@")
|
|
2380
|
+
|
|
2381
|
+
if [[ "${AGENTIC_AGENT_MODEL_MAPPER_NO_FZF:-}" != "1" ]] && fzf_available; then
|
|
2382
|
+
local selected selected_model fzf_status
|
|
2383
|
+
set +e
|
|
2384
|
+
selected="$(for i in "${!models[@]}"; do printf '%s\t%s\n' "$((i + 1))" "${models[$i]}"; done | fzf \
|
|
2385
|
+
--ansi \
|
|
2386
|
+
--border \
|
|
2387
|
+
--height=70% \
|
|
2388
|
+
--layout=reverse \
|
|
2389
|
+
--no-sort \
|
|
2390
|
+
--prompt "$role_name $kind> " \
|
|
2391
|
+
--header "Select $kind model for $role_name" \
|
|
2392
|
+
--with-nth=2..)"
|
|
2393
|
+
fzf_status=$?
|
|
2394
|
+
set -e
|
|
2395
|
+
if [[ "$fzf_status" -eq 0 && -n "$(trim "$selected")" ]]; then
|
|
2396
|
+
selected_model="${selected#* }"
|
|
2397
|
+
local model
|
|
2398
|
+
for model in "${models[@]}"; do
|
|
2399
|
+
if [[ "$model" == "$selected_model" ]]; then
|
|
2400
|
+
printf '%s\n' "$selected_model"
|
|
2401
|
+
return 0
|
|
2402
|
+
fi
|
|
2403
|
+
done
|
|
2404
|
+
fi
|
|
2405
|
+
fi
|
|
2406
|
+
|
|
2407
|
+
echo >&2
|
|
2408
|
+
echo "$role_name ($role_mode) - $role_description" >&2
|
|
2409
|
+
local i
|
|
2410
|
+
for i in "${!models[@]}"; do
|
|
2411
|
+
echo " $((i + 1))) ${models[$i]}" >&2
|
|
2412
|
+
done
|
|
2413
|
+
local answer
|
|
2414
|
+
read -r -p "Select $kind model for $role_name [1]: " answer
|
|
2415
|
+
answer="$(trim "$answer")"
|
|
2416
|
+
if [[ -z "$answer" ]]; then
|
|
2417
|
+
printf '%s\n' "${models[0]}"
|
|
2418
|
+
return 0
|
|
2419
|
+
fi
|
|
2420
|
+
if [[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#models[@]} )); then
|
|
2421
|
+
printf '%s\n' "${models[$((answer - 1))]}"
|
|
2422
|
+
return 0
|
|
2423
|
+
fi
|
|
2424
|
+
local model
|
|
2425
|
+
for model in "${models[@]}"; do
|
|
2426
|
+
if [[ "$model" == "$answer" ]]; then
|
|
2427
|
+
printf '%s\n' "$answer"
|
|
2428
|
+
return 0
|
|
2429
|
+
fi
|
|
2430
|
+
done
|
|
2431
|
+
warn "Unknown model '$answer', using ${models[0]}"
|
|
2432
|
+
printf '%s\n' "${models[0]}"
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
write_opencode_agent_model_mapping() {
|
|
2436
|
+
local roles_file="$1"
|
|
2437
|
+
local mapping_file="$2"
|
|
2438
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2439
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2440
|
+
|
|
2441
|
+
python3 - "$roles_file" "$mapping_file" "$config_path" "$state_path" <<'PY'
|
|
2442
|
+
import json
|
|
2443
|
+
import sys
|
|
2444
|
+
from pathlib import Path
|
|
2445
|
+
|
|
2446
|
+
roles_file, mapping_file, config_path, state_path = map(Path, sys.argv[1:])
|
|
2447
|
+
roles = []
|
|
2448
|
+
for line in roles_file.read_text(encoding="utf-8").splitlines():
|
|
2449
|
+
if not line:
|
|
2450
|
+
continue
|
|
2451
|
+
name, mode, description = (line.split("\t") + ["", "", ""])[:3]
|
|
2452
|
+
roles.append({"name": name, "mode": mode, "description": description})
|
|
2453
|
+
|
|
2454
|
+
mapping = {}
|
|
2455
|
+
for line in mapping_file.read_text(encoding="utf-8").splitlines():
|
|
2456
|
+
if not line:
|
|
2457
|
+
continue
|
|
2458
|
+
name, model, fallback = (line.split("\t") + ["", "", ""])[:3]
|
|
2459
|
+
mapping[name] = {"model": model, "fallback": [fallback] if fallback and fallback != model else []}
|
|
2460
|
+
|
|
2461
|
+
try:
|
|
2462
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
2463
|
+
except Exception:
|
|
2464
|
+
data = {}
|
|
2465
|
+
if not isinstance(data, dict):
|
|
2466
|
+
data = {}
|
|
2467
|
+
agents = data.setdefault("agent", {})
|
|
2468
|
+
for role in roles:
|
|
2469
|
+
selected = mapping.get(role["name"])
|
|
2470
|
+
if not selected:
|
|
2471
|
+
continue
|
|
2472
|
+
current = agents.get(role["name"])
|
|
2473
|
+
if not isinstance(current, dict):
|
|
2474
|
+
current = {}
|
|
2475
|
+
current.update({
|
|
2476
|
+
"mode": current.get("mode") or role["mode"],
|
|
2477
|
+
"description": current.get("description") or role["description"],
|
|
2478
|
+
"model": selected["model"],
|
|
2479
|
+
"fallback": selected["fallback"],
|
|
2480
|
+
})
|
|
2481
|
+
agents[role["name"]] = current
|
|
2482
|
+
|
|
2483
|
+
config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2484
|
+
state_path.write_text(json.dumps({
|
|
2485
|
+
"configured": True,
|
|
2486
|
+
"roles": [role["name"] for role in roles],
|
|
2487
|
+
}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
2488
|
+
PY
|
|
2489
|
+
|
|
2490
|
+
register_managed_file "$config_path" "generated:opencode-agent-model-mapper-config" "config"
|
|
2491
|
+
register_managed_file "$state_path" "generated:opencode-agent-model-mapper-state" "config"
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
configure_opencode_agent_model_mapper_if_needed() {
|
|
2495
|
+
selected_agent_os_contains "opencode" || return 0
|
|
2496
|
+
opencode_agent_model_mapper_config_enabled || return 0
|
|
2497
|
+
|
|
2498
|
+
if ! is_interactive_terminal; then
|
|
2499
|
+
log "agent-model-mapper install-time setup skipped because no interactive terminal is available"
|
|
2500
|
+
return 0
|
|
2501
|
+
fi
|
|
2502
|
+
|
|
2503
|
+
local config_path="$PROJECT_DIR/.opencode/opencode.json"
|
|
2504
|
+
local state_path="$PROJECT_DIR/.opencode/agent-model-mapper.state.json"
|
|
2505
|
+
can_write_managed_file "$config_path" || return 0
|
|
2506
|
+
if [[ -e "$state_path" ]]; then
|
|
2507
|
+
can_write_managed_file "$state_path" || return 0
|
|
2508
|
+
fi
|
|
2509
|
+
|
|
2510
|
+
local roles_file models_file mapping_file
|
|
2511
|
+
roles_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-roles.XXXXXX")"
|
|
2512
|
+
models_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-models.XXXXXX")"
|
|
2513
|
+
mapping_file="$(mktemp "${TMPDIR:-/tmp}/agentic-opencode-mapping.XXXXXX")"
|
|
2514
|
+
opencode_mapper_read_roles > "$roles_file"
|
|
2515
|
+
|
|
2516
|
+
if [[ ! -s "$roles_file" ]]; then
|
|
2517
|
+
log "agent-model-mapper: skipped because .opencode/agents/*.md was not found"
|
|
2518
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2519
|
+
return 0
|
|
2520
|
+
fi
|
|
2521
|
+
|
|
2522
|
+
if opencode_mapper_has_complete_mapping "$roles_file"; then
|
|
2523
|
+
log "agent-model-mapper: skipped because all Agentic roles already have model mappings"
|
|
2524
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2525
|
+
return 0
|
|
2526
|
+
fi
|
|
2527
|
+
|
|
2528
|
+
opencode_mapper_discover_models > "$models_file"
|
|
2529
|
+
local models=()
|
|
2530
|
+
readlines models < "$models_file"
|
|
2531
|
+
if [[ "${#models[@]}" -eq 0 ]]; then
|
|
2532
|
+
models=("opencode/minimax-m2.5-free")
|
|
2533
|
+
fi
|
|
2534
|
+
|
|
2535
|
+
out "agent-model-mapper: choose OpenCode models for Agentic roles"
|
|
2536
|
+
local role_name role_mode role_description model fallback
|
|
2537
|
+
exec 3<&0
|
|
2538
|
+
while IFS=$'\t' read -r role_name role_mode role_description || [[ -n "${role_name:-}" ]]; do
|
|
2539
|
+
[[ -n "$role_name" ]] || continue
|
|
2540
|
+
model="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "main" "${models[@]}" <&3)"
|
|
2541
|
+
fallback="$(choose_opencode_mapper_model "$role_name" "$role_mode" "$role_description" "fallback" "${models[@]}" <&3)"
|
|
2542
|
+
printf '%s\t%s\t%s\n' "$role_name" "$model" "$fallback" >> "$mapping_file"
|
|
2543
|
+
done < "$roles_file"
|
|
2544
|
+
|
|
2545
|
+
local confirm
|
|
2546
|
+
if ! read -r -p "Write .opencode/opencode.json agent model mapping? [y/N]: " confirm <&3; then
|
|
2547
|
+
confirm=""
|
|
2548
|
+
fi
|
|
2549
|
+
exec 3<&-
|
|
2550
|
+
confirm="$(trim "$confirm")"
|
|
2551
|
+
if [[ ! "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]]; then
|
|
2552
|
+
log "agent-model-mapper: skipped by user; no files changed"
|
|
2553
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
2554
|
+
return 0
|
|
2555
|
+
fi
|
|
2556
|
+
|
|
2557
|
+
write_opencode_agent_model_mapping "$roles_file" "$mapping_file"
|
|
2558
|
+
log "agent-model-mapper: updated .opencode/opencode.json"
|
|
2559
|
+
rm -f "$roles_file" "$models_file" "$mapping_file"
|
|
1642
2560
|
}
|
|
1643
2561
|
|
|
1644
2562
|
normalize_selected_agent_os() {
|
|
@@ -1744,7 +2662,7 @@ build_header() {
|
|
|
1744
2662
|
{
|
|
1745
2663
|
echo "# Agentic Project Guidelines"
|
|
1746
2664
|
echo
|
|
1747
|
-
echo "Generated by $SCRIPT_NAME
|
|
2665
|
+
echo "Generated by $SCRIPT_NAME."
|
|
1748
2666
|
echo
|
|
1749
2667
|
echo "## Installation Context"
|
|
1750
2668
|
echo "- Agent OS targets: ${SELECTED_AGENT_OS[*]}"
|
|
@@ -1844,6 +2762,40 @@ generate_agents_md() {
|
|
|
1844
2762
|
rm -f "$tmp"
|
|
1845
2763
|
}
|
|
1846
2764
|
|
|
2765
|
+
copy_memory_md() {
|
|
2766
|
+
local project_dir="$1"
|
|
2767
|
+
local src="$REPO_ROOT/MEMORY.md"
|
|
2768
|
+
|
|
2769
|
+
if [[ ! -f "$src" ]]; then
|
|
2770
|
+
warn "MEMORY.md not found in knowledge base at $src; skipping"
|
|
2771
|
+
return
|
|
2772
|
+
fi
|
|
2773
|
+
|
|
2774
|
+
local outputs=()
|
|
2775
|
+
|
|
2776
|
+
if selected_agent_os_contains "opencode"; then
|
|
2777
|
+
unique_append "$project_dir/.opencode/MEMORY.md" outputs
|
|
2778
|
+
fi
|
|
2779
|
+
|
|
2780
|
+
local needs_root=false
|
|
2781
|
+
local agent_os
|
|
2782
|
+
for agent_os in "${SELECTED_AGENT_OS[@]}"; do
|
|
2783
|
+
if [[ "$agent_os" != "opencode" ]]; then
|
|
2784
|
+
needs_root=true
|
|
2785
|
+
break
|
|
2786
|
+
fi
|
|
2787
|
+
done
|
|
2788
|
+
|
|
2789
|
+
if [[ "$needs_root" == true ]] || ! selected_agent_os_contains "opencode"; then
|
|
2790
|
+
unique_append "$project_dir/MEMORY.md" outputs
|
|
2791
|
+
fi
|
|
2792
|
+
|
|
2793
|
+
local out
|
|
2794
|
+
for out in "${outputs[@]}"; do
|
|
2795
|
+
write_file_with_agentic_marker "$src" "$out" "generated:MEMORY.md"
|
|
2796
|
+
done
|
|
2797
|
+
}
|
|
2798
|
+
|
|
1847
2799
|
validate_inputs() {
|
|
1848
2800
|
local available_areas
|
|
1849
2801
|
available_areas="$(list_areas || true)"
|
|
@@ -1911,37 +2863,47 @@ validate_inputs() {
|
|
|
1911
2863
|
}
|
|
1912
2864
|
|
|
1913
2865
|
print_report() {
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2866
|
+
out
|
|
2867
|
+
out "=== Installation report ===" "$COLOR_HEADER"
|
|
2868
|
+
out "Agentic version: $(app_version_label)"
|
|
2869
|
+
out "Project dir: $PROJECT_DIR"
|
|
2870
|
+
out "Knowledge base repo: $REPO_ROOT"
|
|
2871
|
+
out "Config file: $APP_CONFIG_FILE"
|
|
2872
|
+
out "Agent OS targets: ${SELECTED_AGENT_OS[*]}"
|
|
2873
|
+
out "Areas: ${SELECTED_AREAS[*]}"
|
|
2874
|
+
out "Specializations: ${SELECTED_SPECS[*]}"
|
|
2875
|
+
|
|
2876
|
+
out
|
|
2877
|
+
out "Created directories:"
|
|
1925
2878
|
if [[ "${#CREATED_PATHS[@]}" -eq 0 ]]; then
|
|
1926
|
-
|
|
2879
|
+
out "- (none)"
|
|
1927
2880
|
else
|
|
1928
|
-
|
|
2881
|
+
local created_path
|
|
2882
|
+
for created_path in "${CREATED_PATHS[@]}"; do
|
|
2883
|
+
out "- $created_path"
|
|
2884
|
+
done
|
|
1929
2885
|
fi
|
|
1930
2886
|
|
|
1931
|
-
|
|
1932
|
-
|
|
2887
|
+
out
|
|
2888
|
+
out "Copied/generated paths:"
|
|
1933
2889
|
if [[ "${#COPIED_PATHS[@]}" -eq 0 ]]; then
|
|
1934
|
-
|
|
2890
|
+
out "- (none)"
|
|
1935
2891
|
else
|
|
1936
|
-
|
|
2892
|
+
local copied_path
|
|
2893
|
+
for copied_path in "${COPIED_PATHS[@]}"; do
|
|
2894
|
+
out "- $copied_path"
|
|
2895
|
+
done
|
|
1937
2896
|
fi
|
|
1938
2897
|
|
|
1939
|
-
|
|
1940
|
-
|
|
2898
|
+
out
|
|
2899
|
+
out "Warnings:"
|
|
1941
2900
|
if [[ "${#WARNINGS[@]}" -eq 0 ]]; then
|
|
1942
|
-
|
|
2901
|
+
out "- (none)"
|
|
1943
2902
|
else
|
|
1944
|
-
|
|
2903
|
+
local warning
|
|
2904
|
+
for warning in "${WARNINGS[@]}"; do
|
|
2905
|
+
out "- $warning"
|
|
2906
|
+
done
|
|
1945
2907
|
fi
|
|
1946
2908
|
}
|
|
1947
2909
|
|
|
@@ -2004,27 +2966,289 @@ print_missing_agent_binary_guides() {
|
|
|
2004
2966
|
return
|
|
2005
2967
|
fi
|
|
2006
2968
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2969
|
+
out
|
|
2970
|
+
out "=== Agent binary setup recommendations ===" "$COLOR_HEADER"
|
|
2971
|
+
local missing_line
|
|
2972
|
+
for missing_line in "${missing_lines[@]}"; do
|
|
2973
|
+
out "$missing_line"
|
|
2974
|
+
done
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
changelog_file_path() {
|
|
2978
|
+
local candidate
|
|
2979
|
+
for candidate in "$SCRIPT_DIR/CHANGELOG.md" "$REPO_ROOT/CHANGELOG.md" "$APP_REPO_DIR/CHANGELOG.md"; do
|
|
2980
|
+
[[ -f "$candidate" ]] || continue
|
|
2981
|
+
printf '%s\n' "$candidate"
|
|
2982
|
+
return 0
|
|
2983
|
+
done
|
|
2984
|
+
return 1
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
print_current_changelog() {
|
|
2988
|
+
local changelog version
|
|
2989
|
+
changelog="$(changelog_file_path || true)"
|
|
2990
|
+
version="$(app_version_label)"
|
|
2991
|
+
if [[ -z "$changelog" ]]; then
|
|
2992
|
+
warn "CHANGELOG.md not found; skipping changelog output"
|
|
2993
|
+
return
|
|
2994
|
+
fi
|
|
2995
|
+
|
|
2996
|
+
local section_file
|
|
2997
|
+
section_file="$(mktemp "${TMPDIR:-/tmp}/agentic-changelog.XXXXXX")"
|
|
2998
|
+
awk -v wanted="## $version" '
|
|
2999
|
+
$0 == wanted { in_section = 1; print; next }
|
|
3000
|
+
in_section && /^## / { exit }
|
|
3001
|
+
in_section { print }
|
|
3002
|
+
' "$changelog" > "$section_file"
|
|
3003
|
+
|
|
3004
|
+
if [[ ! -s "$section_file" ]]; then
|
|
3005
|
+
rm -f "$section_file"
|
|
3006
|
+
warn "No changelog section found for $version"
|
|
3007
|
+
return
|
|
3008
|
+
fi
|
|
3009
|
+
|
|
3010
|
+
out
|
|
3011
|
+
out "=== Changelog $version ===" "$COLOR_HEADER"
|
|
3012
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
3013
|
+
if [[ "$line" == "## $version" ]]; then
|
|
3014
|
+
continue
|
|
3015
|
+
fi
|
|
3016
|
+
out "$line"
|
|
3017
|
+
done < "$section_file"
|
|
3018
|
+
rm -f "$section_file"
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
doctor_agent_supported() {
|
|
3022
|
+
case "$1" in
|
|
3023
|
+
codex|opencode|claude|gemini) return 0 ;;
|
|
3024
|
+
*) return 1 ;;
|
|
3025
|
+
esac
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
doctor_enabled() {
|
|
3029
|
+
[[ "$DRY_RUN" != true ]] || return 1
|
|
3030
|
+
[[ "${AGENTIC_DOCTOR:-1}" != "0" ]] || return 1
|
|
3031
|
+
[[ "${AGENTIC_TEST_SOURCE_AGENTIC:-}" != "1" ]] || return 1
|
|
3032
|
+
return 0
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
doctor_prompt() {
|
|
3036
|
+
printf '%s\n' "/develop-feature напиши hello world python"
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
doctor_prompt_for_agent() {
|
|
3040
|
+
local agent_os="$1"
|
|
3041
|
+
case "$agent_os" in
|
|
3042
|
+
opencode)
|
|
3043
|
+
printf '%s\n' "Reply with exactly: AGENTIC_DOCTOR_OK"
|
|
3044
|
+
;;
|
|
3045
|
+
*)
|
|
3046
|
+
doctor_prompt
|
|
3047
|
+
;;
|
|
3048
|
+
esac
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
doctor_smoke_label() {
|
|
3052
|
+
local agent_os="$1"
|
|
3053
|
+
case "$agent_os" in
|
|
3054
|
+
opencode)
|
|
3055
|
+
printf '%s\n' "lightweight smoke"
|
|
3056
|
+
;;
|
|
3057
|
+
*)
|
|
3058
|
+
printf '%s\n' "/develop-feature smoke"
|
|
3059
|
+
;;
|
|
3060
|
+
esac
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
doctor_output_has_fatal_patterns() {
|
|
3064
|
+
local output_file="$1"
|
|
3065
|
+
grep -Eiq 'MCP.*(error|failed|failure|connection|connect|startup)|plugin.*(error|failed|failure)|auth.*(required|failed)|login required|permission.*(denied|required)|SyntaxError|Traceback|Invalid regular expression flags|An unexpected critical error occurred|FatalError|RuntimeError|EPERM|EACCES|panic:' "$output_file"
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
doctor_timeout_seconds() {
|
|
3069
|
+
local value="${AGENTIC_DOCTOR_TIMEOUT_SECONDS:-10}"
|
|
3070
|
+
if [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 1 )); then
|
|
3071
|
+
value=10
|
|
3072
|
+
fi
|
|
3073
|
+
printf '%s\n' "$value"
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
run_with_doctor_timeout() {
|
|
3077
|
+
local timeout_seconds="$1"
|
|
3078
|
+
shift
|
|
3079
|
+
|
|
3080
|
+
"$@" &
|
|
3081
|
+
local child_pid=$!
|
|
3082
|
+
local elapsed=0
|
|
3083
|
+
local status=0
|
|
3084
|
+
while kill -0 "$child_pid" 2>/dev/null; do
|
|
3085
|
+
if (( elapsed >= timeout_seconds )); then
|
|
3086
|
+
pkill -TERM -P "$child_pid" 2>/dev/null || true
|
|
3087
|
+
kill "$child_pid" 2>/dev/null || true
|
|
3088
|
+
sleep 1
|
|
3089
|
+
pkill -KILL -P "$child_pid" 2>/dev/null || true
|
|
3090
|
+
kill -9 "$child_pid" 2>/dev/null || true
|
|
3091
|
+
wait "$child_pid" 2>/dev/null || true
|
|
3092
|
+
return 124
|
|
3093
|
+
fi
|
|
3094
|
+
sleep 1
|
|
3095
|
+
elapsed=$((elapsed + 1))
|
|
3096
|
+
done
|
|
3097
|
+
wait "$child_pid"
|
|
3098
|
+
status=$?
|
|
3099
|
+
return "$status"
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
doctor_copy_project() {
|
|
3103
|
+
local dest="$1"
|
|
3104
|
+
mkdir -p "$dest"
|
|
3105
|
+
if [[ -d "$PROJECT_DIR" ]]; then
|
|
3106
|
+
cp -R "$PROJECT_DIR/." "$dest/"
|
|
3107
|
+
fi
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
run_doctor_command() {
|
|
3111
|
+
local agent_os="$1"
|
|
3112
|
+
local work_dir="$2"
|
|
3113
|
+
local output_file="$3"
|
|
3114
|
+
local prompt
|
|
3115
|
+
prompt="$(doctor_prompt_for_agent "$agent_os")"
|
|
3116
|
+
|
|
3117
|
+
case "$agent_os" in
|
|
3118
|
+
codex)
|
|
3119
|
+
codex exec --skip-git-repo-check --ephemeral --sandbox workspace-write -C "$work_dir" "$prompt" </dev/null >"$output_file" 2>&1
|
|
3120
|
+
;;
|
|
3121
|
+
opencode)
|
|
3122
|
+
OPENCODE_DISABLE_AUTOUPDATE=1 opencode run --pure --dir "$work_dir" --dangerously-skip-permissions --format json --log-level ERROR "$prompt" >"$output_file" 2>&1
|
|
3123
|
+
;;
|
|
3124
|
+
claude)
|
|
3125
|
+
(cd "$work_dir" && claude -p --permission-mode bypassPermissions --output-format stream-json "$prompt") >"$output_file" 2>&1
|
|
3126
|
+
;;
|
|
3127
|
+
gemini)
|
|
3128
|
+
(cd "$work_dir" && gemini --prompt "$prompt") >"$output_file" 2>&1
|
|
3129
|
+
;;
|
|
3130
|
+
*)
|
|
3131
|
+
return 2
|
|
3132
|
+
;;
|
|
3133
|
+
esac
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
run_doctor_for_agent() {
|
|
3137
|
+
local agent_os="$1"
|
|
3138
|
+
local doctor_root="$2"
|
|
3139
|
+
local binary_name
|
|
3140
|
+
binary_name="$(get_agent_binary_name "$agent_os")"
|
|
3141
|
+
|
|
3142
|
+
if [[ -z "$binary_name" ]] || ! command -v "$binary_name" >/dev/null 2>&1; then
|
|
3143
|
+
out "❌ $agent_os: binary '$binary_name' is not installed"
|
|
3144
|
+
return 1
|
|
3145
|
+
fi
|
|
3146
|
+
|
|
3147
|
+
local work_dir output_file status timeout_seconds started_at elapsed smoke_label
|
|
3148
|
+
work_dir="$doctor_root/$agent_os"
|
|
3149
|
+
output_file="$doctor_root/$agent_os.log"
|
|
3150
|
+
timeout_seconds="$(doctor_timeout_seconds)"
|
|
3151
|
+
smoke_label="$(doctor_smoke_label "$agent_os")"
|
|
3152
|
+
doctor_copy_project "$work_dir"
|
|
3153
|
+
|
|
3154
|
+
set +e
|
|
3155
|
+
started_at="$(date +%s)"
|
|
3156
|
+
run_with_doctor_timeout "$timeout_seconds" run_doctor_command "$agent_os" "$work_dir" "$output_file"
|
|
3157
|
+
status=$?
|
|
3158
|
+
elapsed=$(( $(date +%s) - started_at ))
|
|
3159
|
+
set -e
|
|
3160
|
+
|
|
3161
|
+
log "$agent_os doctor finished: timeout=${timeout_seconds}s exit=$status elapsed=${elapsed}s"
|
|
3162
|
+
|
|
3163
|
+
log_file_block "doctor $agent_os" "$output_file"
|
|
3164
|
+
|
|
3165
|
+
if [[ "$status" -eq 124 || "$status" -eq 137 ]]; then
|
|
3166
|
+
out "❌ $agent_os: $smoke_label timed out after ${timeout_seconds}s (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
3167
|
+
return 1
|
|
3168
|
+
fi
|
|
3169
|
+
|
|
3170
|
+
if [[ "$status" -ne 0 ]]; then
|
|
3171
|
+
out "❌ $agent_os: $smoke_label failed (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
3172
|
+
return 1
|
|
3173
|
+
fi
|
|
3174
|
+
|
|
3175
|
+
if doctor_output_has_fatal_patterns "$output_file"; then
|
|
3176
|
+
out "❌ $agent_os: $smoke_label reported integration errors (exit $status, elapsed ${elapsed}s, log: $output_file)"
|
|
3177
|
+
return 1
|
|
3178
|
+
fi
|
|
3179
|
+
|
|
3180
|
+
out "✅ $agent_os: $smoke_label passed (exit $status, elapsed ${elapsed}s)"
|
|
3181
|
+
return 0
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
run_agentic_doctor() {
|
|
3185
|
+
if ! doctor_enabled; then
|
|
3186
|
+
log "Agentic doctor skipped"
|
|
3187
|
+
return
|
|
3188
|
+
fi
|
|
3189
|
+
|
|
3190
|
+
local selected_doctor_agents=()
|
|
3191
|
+
local agent_os
|
|
3192
|
+
for agent_os in "${SELECTED_AGENT_OS[@]}"; do
|
|
3193
|
+
if doctor_agent_supported "$agent_os"; then
|
|
3194
|
+
selected_doctor_agents+=("$agent_os")
|
|
3195
|
+
fi
|
|
3196
|
+
done
|
|
3197
|
+
|
|
3198
|
+
if [[ "${#selected_doctor_agents[@]}" -eq 0 ]]; then
|
|
3199
|
+
log "Agentic doctor skipped: no supported real agentos selected"
|
|
3200
|
+
return
|
|
3201
|
+
fi
|
|
3202
|
+
|
|
3203
|
+
local doctor_root
|
|
3204
|
+
doctor_root="$(mktemp -d "${TMPDIR:-/tmp}/agentic-doctor.XXXXXX")"
|
|
3205
|
+
out
|
|
3206
|
+
out "=== Agentic doctor ===" "$COLOR_HEADER"
|
|
3207
|
+
out "Doctor temp root: $doctor_root"
|
|
3208
|
+
out "Doctor timeout: $(doctor_timeout_seconds)s per agent"
|
|
3209
|
+
|
|
3210
|
+
local failures=0
|
|
3211
|
+
for agent_os in "${selected_doctor_agents[@]}"; do
|
|
3212
|
+
if ! run_doctor_for_agent "$agent_os" "$doctor_root"; then
|
|
3213
|
+
failures=$((failures + 1))
|
|
3214
|
+
fi
|
|
3215
|
+
done
|
|
3216
|
+
|
|
3217
|
+
if [[ "$AGENTIC_DOCTOR_KEEP_TMP" == "1" || "$failures" -gt 0 ]]; then
|
|
3218
|
+
out "Doctor temp root kept: $doctor_root"
|
|
3219
|
+
else
|
|
3220
|
+
rm -rf "$doctor_root"
|
|
3221
|
+
fi
|
|
3222
|
+
|
|
3223
|
+
if [[ "$failures" -gt 0 ]]; then
|
|
3224
|
+
warn "Agentic doctor completed with $failures failing check(s)"
|
|
3225
|
+
else
|
|
3226
|
+
log "Agentic doctor completed successfully"
|
|
3227
|
+
fi
|
|
2010
3228
|
}
|
|
2011
3229
|
|
|
2012
3230
|
run_install() {
|
|
3231
|
+
init_run_logging
|
|
2013
3232
|
ensure_repo_layout
|
|
2014
|
-
|
|
3233
|
+
ensure_agentic_runtime_requirements
|
|
2015
3234
|
normalize_selected_agent_os
|
|
2016
3235
|
validate_inputs
|
|
2017
3236
|
|
|
2018
3237
|
ensure_dir "$PROJECT_DIR"
|
|
2019
3238
|
configure_opencode_plugins_if_needed
|
|
2020
3239
|
copy_extensions "$PROJECT_DIR"
|
|
3240
|
+
configure_opencode_agent_model_mapper_if_needed
|
|
2021
3241
|
copy_specialization_assets "$PROJECT_DIR"
|
|
2022
3242
|
generate_agents_md "$PROJECT_DIR"
|
|
3243
|
+
copy_memory_md "$PROJECT_DIR"
|
|
2023
3244
|
configure_context7_if_needed
|
|
2024
3245
|
configure_mempalace_if_needed
|
|
2025
3246
|
write_agentic_manifest "$PROJECT_DIR"
|
|
2026
3247
|
print_report
|
|
2027
3248
|
print_missing_agent_binary_guides
|
|
3249
|
+
print_current_changelog
|
|
3250
|
+
run_agentic_doctor
|
|
3251
|
+
out "Agentic log file: $RUN_LOG_FILE"
|
|
2028
3252
|
}
|
|
2029
3253
|
|
|
2030
3254
|
ascii_banner() {
|
|
@@ -2318,6 +3542,28 @@ choose_single_fzf() {
|
|
|
2318
3542
|
printf '%s\n' "${options[@]}" | fzf "${fzf_args[@]}"
|
|
2319
3543
|
}
|
|
2320
3544
|
|
|
3545
|
+
|
|
3546
|
+
choose_multi_fzf_strict() {
|
|
3547
|
+
local prompt="$1"
|
|
3548
|
+
shift
|
|
3549
|
+
local options=("$@")
|
|
3550
|
+
|
|
3551
|
+
if [[ "${#options[@]}" -eq 0 ]]; then
|
|
3552
|
+
return
|
|
3553
|
+
fi
|
|
3554
|
+
|
|
3555
|
+
local sentinel="<none>"
|
|
3556
|
+
local picked=()
|
|
3557
|
+
readlines picked < <(choose_multi_fzf "$prompt" "$sentinel" "${options[@]}")
|
|
3558
|
+
|
|
3559
|
+
local item
|
|
3560
|
+
for item in "${picked[@]}"; do
|
|
3561
|
+
item="$(trim "$item")"
|
|
3562
|
+
[[ -z "$item" || "$item" == "$sentinel" ]] && continue
|
|
3563
|
+
printf '%s\n' "$item"
|
|
3564
|
+
done
|
|
3565
|
+
}
|
|
3566
|
+
|
|
2321
3567
|
choose_multi_fzf() {
|
|
2322
3568
|
local prompt="$1"
|
|
2323
3569
|
shift
|
|
@@ -2373,11 +3619,13 @@ run_tui() {
|
|
|
2373
3619
|
exit 1
|
|
2374
3620
|
fi
|
|
2375
3621
|
|
|
3622
|
+
ensure_agentic_runtime_requirements
|
|
3623
|
+
|
|
2376
3624
|
pick_theme_if_needed
|
|
2377
3625
|
set_theme_colors
|
|
2378
3626
|
|
|
2379
3627
|
ascii_banner
|
|
2380
|
-
echo "${COLOR_HEADER}$APP_TUI_TITLE${COLOR_RESET}"
|
|
3628
|
+
echo "${COLOR_HEADER}$APP_TUI_TITLE $(app_version_label)${COLOR_RESET}"
|
|
2381
3629
|
echo "${COLOR_DIM}Theme: $THEME (resolved: $ACTIVE_THEME)${COLOR_RESET}"
|
|
2382
3630
|
echo
|
|
2383
3631
|
|
|
@@ -2413,6 +3661,26 @@ run_tui() {
|
|
|
2413
3661
|
SELECTED_AGENT_OS=("${picked_agent_os[@]}")
|
|
2414
3662
|
fi
|
|
2415
3663
|
|
|
3664
|
+
local mcp_options=("context7" "mempalace")
|
|
3665
|
+
local picked_mcps=()
|
|
3666
|
+
if [[ "$use_fzf" == true ]]; then
|
|
3667
|
+
readlines picked_mcps < <(choose_multi_fzf_strict "Select optional MCP integration(s):" "${mcp_options[@]}")
|
|
3668
|
+
else
|
|
3669
|
+
local picked_mcps_output
|
|
3670
|
+
picked_mcps_output="$(choose_multi_by_index "Select optional MCP integration(s):" "<none>" "${mcp_options[@]}")"
|
|
3671
|
+
readlines picked_mcps <<< "$picked_mcps_output"
|
|
3672
|
+
fi
|
|
3673
|
+
|
|
3674
|
+
AGENTIC_ENABLE_CONTEXT7="n"
|
|
3675
|
+
AGENTIC_ENABLE_MEMPALACE="n"
|
|
3676
|
+
local picked_mcp
|
|
3677
|
+
for picked_mcp in "${picked_mcps[@]}"; do
|
|
3678
|
+
case "$picked_mcp" in
|
|
3679
|
+
context7) AGENTIC_ENABLE_CONTEXT7="y" ;;
|
|
3680
|
+
mempalace) AGENTIC_ENABLE_MEMPALACE="y" ;;
|
|
3681
|
+
esac
|
|
3682
|
+
done
|
|
3683
|
+
|
|
2416
3684
|
local areas=()
|
|
2417
3685
|
readlines areas < <(list_areas)
|
|
2418
3686
|
|
|
@@ -2605,6 +3873,30 @@ sync_current_project_after_upgrade() {
|
|
|
2605
3873
|
load_install_settings_from_manifest "$manifest"
|
|
2606
3874
|
ensure_repo_layout
|
|
2607
3875
|
run_install
|
|
3876
|
+
upgrade_mempalace_graph
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
upgrade_mempalace_graph() {
|
|
3880
|
+
# Only run if mempalace was enabled for this project
|
|
3881
|
+
if [[ ! "${AGENTIC_ENABLE_MEMPALACE:-}" =~ ^[Yy] ]]; then
|
|
3882
|
+
return
|
|
3883
|
+
fi
|
|
3884
|
+
|
|
3885
|
+
if ! command -v mempalace >/dev/null 2>&1; then
|
|
3886
|
+
return
|
|
3887
|
+
fi
|
|
3888
|
+
|
|
3889
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
3890
|
+
log "DRY-RUN mempalace mine \"$PROJECT_DIR\""
|
|
3891
|
+
return
|
|
3892
|
+
fi
|
|
3893
|
+
|
|
3894
|
+
log "Refreshing MemPalace knowledge graph for $PROJECT_DIR"
|
|
3895
|
+
if mempalace mine "$PROJECT_DIR" >/dev/null 2>&1; then
|
|
3896
|
+
log "MemPalace graph updated"
|
|
3897
|
+
else
|
|
3898
|
+
warn "mempalace mine failed; graph may be stale — run manually: mempalace mine \"$PROJECT_DIR\""
|
|
3899
|
+
fi
|
|
2608
3900
|
}
|
|
2609
3901
|
|
|
2610
3902
|
parse_theme_option() {
|
|
@@ -2704,6 +3996,10 @@ case "$COMMAND" in
|
|
|
2704
3996
|
DRY_RUN=true
|
|
2705
3997
|
shift
|
|
2706
3998
|
;;
|
|
3999
|
+
--no-doctor)
|
|
4000
|
+
AGENTIC_DOCTOR=0
|
|
4001
|
+
shift
|
|
4002
|
+
;;
|
|
2707
4003
|
-h|--help)
|
|
2708
4004
|
usage
|
|
2709
4005
|
exit 0
|
|
@@ -2745,6 +4041,10 @@ case "$COMMAND" in
|
|
|
2745
4041
|
DRY_RUN=true
|
|
2746
4042
|
shift
|
|
2747
4043
|
;;
|
|
4044
|
+
--no-doctor)
|
|
4045
|
+
AGENTIC_DOCTOR=0
|
|
4046
|
+
shift
|
|
4047
|
+
;;
|
|
2748
4048
|
-h|--help)
|
|
2749
4049
|
usage
|
|
2750
4050
|
exit 0
|
|
@@ -2831,6 +4131,10 @@ case "$COMMAND" in
|
|
|
2831
4131
|
usage
|
|
2832
4132
|
;;
|
|
2833
4133
|
|
|
4134
|
+
-V|--version|version)
|
|
4135
|
+
app_version_label
|
|
4136
|
+
;;
|
|
4137
|
+
|
|
2834
4138
|
*)
|
|
2835
4139
|
usage
|
|
2836
4140
|
exit 1
|