@jetrabbits/agentic 0.0.3 → 0.0.5
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 +15 -0
- package/Makefile +40 -0
- package/README.md +1 -0
- package/UPGRADE.md +61 -0
- package/agentic +948 -10
- package/areas/devops/ci-cd/prompts/release-pipeline.md +69 -79
- package/areas/devops/ci-cd/rules/supply-chain-security.md +39 -19
- package/areas/devops/ci-cd/skills/github-actions-patterns/SKILL.md +6 -1
- package/areas/devops/ci-cd/skills/pipeline-security/SKILL.md +54 -119
- package/areas/devops/ci-cd/workflows/release-pipeline.md +72 -62
- package/areas/devops/kubernetes/skills/pod-troubleshooting/SKILL.md +1 -1
- package/areas/devops/observability/rules/alerting-standards.md +37 -31
- package/areas/devops/observability/rules/golden-signals.md +29 -20
- package/areas/devops/observability/skills/distributed-tracing/SKILL.md +10 -1
- package/areas/software/backend/rules/security.md +32 -12
- package/areas/software/frontend/skills/component-design/SKILL.md +13 -1
- package/areas/software/full-stack/AGENTS.md +1 -4
- package/areas/software/full-stack/rules/security-guide.md +48 -12
- package/areas/software/full-stack/workflows/debug-issue.md +2 -2
- package/areas/software/security/prompts/security-scan.md +47 -55
- package/areas/software/security/rules/dependency-policy.md +43 -8
- package/areas/software/security/skills/dependency-audit/SKILL.md +46 -25
- package/areas/software/security/skills/threat-modeling/SKILL.md +26 -0
- package/docs/agentic-lifecycle.md +103 -0
- package/docs/agentic-token-minimization/README.md +79 -0
- package/docs/agentic-usage.md +145 -0
- package/docs/catalog.schema.json +203 -0
- package/docs/guidance-updates/2026-04-10-software-devops-best-practices.md +26 -0
- package/docs/opencode_prepare_agents.md +40 -0
- package/docs/opencode_setup.md +45 -0
- package/docs/prompt-format.md +80 -0
- package/docs/site/README.md +44 -0
- package/docs/site/app.js +127 -0
- package/docs/site/catalog.json +5002 -0
- package/docs/site/index.html +52 -0
- package/docs/site/styles.css +177 -0
- package/extensions/codex/agents/developer.toml +1 -1
- package/extensions/codex/agents/devops-engineer.toml +1 -1
- package/extensions/codex/agents/product-owner.toml +1 -1
- package/extensions/codex/agents/team-lead.toml +1 -1
- package/extensions/opencode/plugins/model-checker.json +2 -3
- package/extensions/opencode/plugins/model-checker.ts +23 -0
- package/extensions/opencode/plugins/telegram-notification.ts +33 -5
- package/package.json +6 -2
- package/scripts/assess_area_quality.py +216 -0
- package/scripts/build_docs_catalog.py +283 -0
- package/scripts/lint_prompts.py +113 -0
- package/areas/software/full-stack/skills/bash-pro/SKILL.md +0 -310
- package/areas/software/full-stack/skills/python-pro/SKILL.md +0 -158
- package/areas/software/full-stack/skills/skill-creator/LICENSE.txt +0 -202
- package/areas/software/full-stack/skills/skill-creator/SKILL.md +0 -356
- package/areas/software/full-stack/skills/skill-creator/references/output-patterns.md +0 -82
- package/areas/software/full-stack/skills/skill-creator/references/workflows.md +0 -28
- package/areas/software/full-stack/skills/skill-creator/scripts/init_skill.py +0 -303
- package/areas/software/full-stack/skills/skill-creator/scripts/package_skill.py +0 -110
- package/areas/software/full-stack/skills/skill-creator/scripts/quick_validate.py +0 -95
- package/extensions/codex/skills/babysit-pr/SKILL.md +0 -187
- package/extensions/codex/skills/babysit-pr/agents/openai.yaml +0 -4
- package/extensions/codex/skills/babysit-pr/references/github-api-notes.md +0 -72
- package/extensions/codex/skills/babysit-pr/references/heuristics.md +0 -58
- package/extensions/codex/skills/babysit-pr/scripts/gh_pr_watch.py +0 -806
- package/extensions/codex/skills/babysit-pr/scripts/test_gh_pr_watch.py +0 -155
- package/extensions/opencode/skills/code_review_expert/SKILL.md +0 -144
- package/extensions/opencode/skills/design_expert/SKILL.md +0 -42
- package/extensions/opencode/skills/qa_expert/SKILL.md +0 -116
package/agentic
CHANGED
|
@@ -11,6 +11,8 @@ APP_NAME="agentic"
|
|
|
11
11
|
APP_TITLE="Agentic Installer"
|
|
12
12
|
APP_TUI_TITLE="Agentic installer (TUI mode)"
|
|
13
13
|
APP_REPO_URL="https://github.com/sawrus/agent-guides.git"
|
|
14
|
+
APP_REPO_LINK="https://github.com/sawrus/agent-guides"
|
|
15
|
+
PROJECT_MANIFEST_NAME=".agentic.json"
|
|
14
16
|
|
|
15
17
|
DEFAULT_AGENT_OS="default"
|
|
16
18
|
STATIC_AGENT_OS=(default opencode codex claude antigravity cursor agents)
|
|
@@ -21,6 +23,7 @@ XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
|
21
23
|
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
22
24
|
APP_CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME"
|
|
23
25
|
APP_CONFIG_FILE="$APP_CONFIG_DIR/config"
|
|
26
|
+
OPENCODE_PLUGIN_CONFIG_FILE="$APP_CONFIG_DIR/opencode-plugins.json"
|
|
24
27
|
APP_DATA_DIR="$XDG_DATA_HOME/$APP_NAME"
|
|
25
28
|
APP_REPO_DIR="$APP_DATA_DIR/repo"
|
|
26
29
|
|
|
@@ -47,8 +50,12 @@ SELF_INSTALL_WITH_FZF=false
|
|
|
47
50
|
|
|
48
51
|
CREATED_PATHS=()
|
|
49
52
|
COPIED_PATHS=()
|
|
53
|
+
MANAGED_RECORDS=()
|
|
54
|
+
SKIPPED_MANAGED_PATHS=()
|
|
50
55
|
WARNINGS=()
|
|
51
56
|
|
|
57
|
+
CONTEXT7_API_KEY="${CONTEXT7_API_KEY:-}"
|
|
58
|
+
|
|
52
59
|
COLOR_RESET=""
|
|
53
60
|
COLOR_HEADER=""
|
|
54
61
|
COLOR_INFO=""
|
|
@@ -505,6 +512,390 @@ ensure_dir() {
|
|
|
505
512
|
unique_append "$path" CREATED_PATHS
|
|
506
513
|
}
|
|
507
514
|
|
|
515
|
+
ensure_python_available() {
|
|
516
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
517
|
+
error "python3 is required for managed install metadata and generated markers"
|
|
518
|
+
exit 1
|
|
519
|
+
fi
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
selected_agent_os_contains() {
|
|
523
|
+
local expected="$1"
|
|
524
|
+
local agent
|
|
525
|
+
for agent in "${SELECTED_AGENT_OS[@]}"; do
|
|
526
|
+
if [[ "$agent" == "$expected" ]]; then
|
|
527
|
+
return 0
|
|
528
|
+
fi
|
|
529
|
+
done
|
|
530
|
+
return 1
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
project_manifest_path() {
|
|
534
|
+
printf '%s\n' "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
project_rel_path() {
|
|
538
|
+
local path="$1"
|
|
539
|
+
local rel="${path#"$PROJECT_DIR"/}"
|
|
540
|
+
printf '%s\n' "$rel"
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
hash_file() {
|
|
544
|
+
local path="$1"
|
|
545
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
546
|
+
shasum -a 256 "$path" | awk '{print $1}'
|
|
547
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
548
|
+
sha256sum "$path" | awk '{print $1}'
|
|
549
|
+
else
|
|
550
|
+
error "shasum or sha256sum is required to track managed files"
|
|
551
|
+
exit 1
|
|
552
|
+
fi
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
manifest_has_path() {
|
|
556
|
+
local rel="$1"
|
|
557
|
+
local manifest
|
|
558
|
+
manifest="$(project_manifest_path)"
|
|
559
|
+
[[ -f "$manifest" ]] || return 1
|
|
560
|
+
python3 - "$manifest" "$rel" <<'PY'
|
|
561
|
+
import json
|
|
562
|
+
import sys
|
|
563
|
+
from pathlib import Path
|
|
564
|
+
|
|
565
|
+
manifest = Path(sys.argv[1])
|
|
566
|
+
rel = sys.argv[2]
|
|
567
|
+
try:
|
|
568
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
569
|
+
except Exception:
|
|
570
|
+
sys.exit(1)
|
|
571
|
+
for item in data.get("managed_files", []):
|
|
572
|
+
if item.get("path") == rel:
|
|
573
|
+
sys.exit(0)
|
|
574
|
+
sys.exit(1)
|
|
575
|
+
PY
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
manifest_hash_for_path() {
|
|
579
|
+
local rel="$1"
|
|
580
|
+
local manifest
|
|
581
|
+
manifest="$(project_manifest_path)"
|
|
582
|
+
[[ -f "$manifest" ]] || return 0
|
|
583
|
+
python3 - "$manifest" "$rel" <<'PY'
|
|
584
|
+
import json
|
|
585
|
+
import sys
|
|
586
|
+
from pathlib import Path
|
|
587
|
+
|
|
588
|
+
manifest = Path(sys.argv[1])
|
|
589
|
+
rel = sys.argv[2]
|
|
590
|
+
try:
|
|
591
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
592
|
+
except Exception:
|
|
593
|
+
sys.exit(0)
|
|
594
|
+
for item in data.get("managed_files", []):
|
|
595
|
+
if item.get("path") == rel:
|
|
596
|
+
print(item.get("content_hash", ""))
|
|
597
|
+
break
|
|
598
|
+
PY
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
can_write_managed_file() {
|
|
602
|
+
local dest="$1"
|
|
603
|
+
local rel
|
|
604
|
+
rel="$(project_rel_path "$dest")"
|
|
605
|
+
|
|
606
|
+
if [[ -f "$(project_manifest_path)" ]]; then
|
|
607
|
+
if ! manifest_has_path "$rel"; then
|
|
608
|
+
if [[ -e "$dest" ]]; then
|
|
609
|
+
warn "Skipping unmanaged target on rerun: $rel"
|
|
610
|
+
unique_append "$rel" SKIPPED_MANAGED_PATHS
|
|
611
|
+
return 1
|
|
612
|
+
fi
|
|
613
|
+
return 0
|
|
614
|
+
fi
|
|
615
|
+
|
|
616
|
+
if [[ -f "$dest" ]]; then
|
|
617
|
+
local expected_hash current_hash
|
|
618
|
+
expected_hash="$(manifest_hash_for_path "$rel")"
|
|
619
|
+
if [[ -n "$expected_hash" ]]; then
|
|
620
|
+
current_hash="$(hash_file "$dest")"
|
|
621
|
+
if [[ "$current_hash" != "$expected_hash" ]]; then
|
|
622
|
+
warn "Skipping user-modified managed file: $rel"
|
|
623
|
+
unique_append "$rel" SKIPPED_MANAGED_PATHS
|
|
624
|
+
return 1
|
|
625
|
+
fi
|
|
626
|
+
fi
|
|
627
|
+
fi
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
return 0
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
register_managed_file() {
|
|
634
|
+
local dest="$1"
|
|
635
|
+
local source_ref="$2"
|
|
636
|
+
local marker="$3"
|
|
637
|
+
local rel
|
|
638
|
+
rel="$(project_rel_path "$dest")"
|
|
639
|
+
local digest
|
|
640
|
+
digest="$(hash_file "$dest")"
|
|
641
|
+
MANAGED_RECORDS+=("$rel|$source_ref|$digest|$marker")
|
|
642
|
+
unique_append "$dest" COPIED_PATHS
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
record_agentic_event() {
|
|
646
|
+
local kind="$1"
|
|
647
|
+
local value="${2:-}"
|
|
648
|
+
|
|
649
|
+
case "$kind" in
|
|
650
|
+
DIR)
|
|
651
|
+
unique_append "$value" CREATED_PATHS
|
|
652
|
+
;;
|
|
653
|
+
COPIED)
|
|
654
|
+
unique_append "$value" COPIED_PATHS
|
|
655
|
+
;;
|
|
656
|
+
RECORD)
|
|
657
|
+
MANAGED_RECORDS+=("$value")
|
|
658
|
+
;;
|
|
659
|
+
SKIP)
|
|
660
|
+
unique_append "$value" SKIPPED_MANAGED_PATHS
|
|
661
|
+
;;
|
|
662
|
+
WARN)
|
|
663
|
+
warn "$value"
|
|
664
|
+
;;
|
|
665
|
+
esac
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
write_file_with_agentic_marker() {
|
|
669
|
+
local src="$1"
|
|
670
|
+
local dest="$2"
|
|
671
|
+
local source_ref="$3"
|
|
672
|
+
|
|
673
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
674
|
+
log "DRY-RUN write managed file $dest"
|
|
675
|
+
unique_append "$dest" COPIED_PATHS
|
|
676
|
+
return
|
|
677
|
+
fi
|
|
678
|
+
|
|
679
|
+
can_write_managed_file "$dest" || return 0
|
|
680
|
+
|
|
681
|
+
ensure_dir "$(dirname -- "$dest")"
|
|
682
|
+
python3 - "$src" "$dest" "$source_ref" "$APP_REPO_LINK" <<'PY'
|
|
683
|
+
import json
|
|
684
|
+
import sys
|
|
685
|
+
from pathlib import Path
|
|
686
|
+
|
|
687
|
+
src = Path(sys.argv[1])
|
|
688
|
+
dest = Path(sys.argv[2])
|
|
689
|
+
source_ref = sys.argv[3]
|
|
690
|
+
repo = sys.argv[4]
|
|
691
|
+
text = src.read_text(encoding="utf-8")
|
|
692
|
+
suffix = dest.suffix.lower()
|
|
693
|
+
marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def yaml_quote(value: str) -> str:
|
|
697
|
+
return json.dumps(value, ensure_ascii=False)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def markdown_with_marker(body: str) -> str:
|
|
701
|
+
block = (
|
|
702
|
+
"agentic:\n"
|
|
703
|
+
" generated_by: agentic\n"
|
|
704
|
+
f" source: {yaml_quote(source_ref)}\n"
|
|
705
|
+
f" repository: {yaml_quote(repo)}\n"
|
|
706
|
+
)
|
|
707
|
+
if body.startswith("---\n"):
|
|
708
|
+
end = body.find("\n---", 4)
|
|
709
|
+
if end != -1:
|
|
710
|
+
return body[: end + 1] + block + body[end + 1 :]
|
|
711
|
+
return (
|
|
712
|
+
"---\n"
|
|
713
|
+
+ block
|
|
714
|
+
+ "---\n"
|
|
715
|
+
+ body
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def commented(body: str, prefix: str) -> str:
|
|
720
|
+
line = f"{prefix} {marker}\n"
|
|
721
|
+
if body.startswith("#!"):
|
|
722
|
+
first, sep, rest = body.partition("\n")
|
|
723
|
+
if sep:
|
|
724
|
+
return first + sep + line + rest
|
|
725
|
+
return line + body
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
if suffix == ".md":
|
|
729
|
+
output = markdown_with_marker(text)
|
|
730
|
+
elif suffix == ".json":
|
|
731
|
+
data = json.loads(text)
|
|
732
|
+
if not isinstance(data, dict):
|
|
733
|
+
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
|
+
output = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
740
|
+
elif suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
|
|
741
|
+
output = commented(text, "//")
|
|
742
|
+
elif suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
|
|
743
|
+
output = commented(text, "#")
|
|
744
|
+
else:
|
|
745
|
+
output = commented(text, "#")
|
|
746
|
+
|
|
747
|
+
dest.write_text(output, encoding="utf-8")
|
|
748
|
+
PY
|
|
749
|
+
register_managed_file "$dest" "$source_ref" "internal"
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
write_agentic_manifest() {
|
|
753
|
+
local project_dir="$1"
|
|
754
|
+
local manifest="$project_dir/$PROJECT_MANIFEST_NAME"
|
|
755
|
+
|
|
756
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
757
|
+
log "DRY-RUN write $manifest"
|
|
758
|
+
return
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
local records_file skipped_file
|
|
762
|
+
records_file="$(mktemp "${TMPDIR:-/tmp}/agentic-records.XXXXXX")"
|
|
763
|
+
skipped_file="$(mktemp "${TMPDIR:-/tmp}/agentic-skipped.XXXXXX")"
|
|
764
|
+
if [[ "${#MANAGED_RECORDS[@]}" -gt 0 ]]; then
|
|
765
|
+
printf '%s\n' "${MANAGED_RECORDS[@]}" > "$records_file"
|
|
766
|
+
else
|
|
767
|
+
: > "$records_file"
|
|
768
|
+
fi
|
|
769
|
+
if [[ "${#SKIPPED_MANAGED_PATHS[@]}" -gt 0 ]]; then
|
|
770
|
+
printf '%s\n' "${SKIPPED_MANAGED_PATHS[@]}" > "$skipped_file"
|
|
771
|
+
else
|
|
772
|
+
: > "$skipped_file"
|
|
773
|
+
fi
|
|
774
|
+
|
|
775
|
+
local agent_os_csv areas_csv specs_csv
|
|
776
|
+
local old_ifs="$IFS"
|
|
777
|
+
IFS=,
|
|
778
|
+
agent_os_csv="${SELECTED_AGENT_OS[*]}"
|
|
779
|
+
areas_csv="${SELECTED_AREAS[*]}"
|
|
780
|
+
specs_csv="${SELECTED_SPECS[*]}"
|
|
781
|
+
IFS="$old_ifs"
|
|
782
|
+
|
|
783
|
+
python3 - "$manifest" "$records_file" "$skipped_file" "$APP_REPO_LINK" "$REPO_ROOT" "$agent_os_csv" "$areas_csv" "$specs_csv" <<'PY'
|
|
784
|
+
import json
|
|
785
|
+
import sys
|
|
786
|
+
from datetime import datetime, timezone
|
|
787
|
+
from pathlib import Path
|
|
788
|
+
|
|
789
|
+
manifest = Path(sys.argv[1])
|
|
790
|
+
records_file = Path(sys.argv[2])
|
|
791
|
+
skipped_file = Path(sys.argv[3])
|
|
792
|
+
repo_link = sys.argv[4]
|
|
793
|
+
repo_root = sys.argv[5]
|
|
794
|
+
agent_os = [x for x in sys.argv[6].split(",") if x]
|
|
795
|
+
areas = [x for x in sys.argv[7].split(",") if x]
|
|
796
|
+
specs = [x for x in sys.argv[8].split(",") if x]
|
|
797
|
+
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
798
|
+
|
|
799
|
+
existing = {}
|
|
800
|
+
created_at = now
|
|
801
|
+
if manifest.exists():
|
|
802
|
+
try:
|
|
803
|
+
old = json.loads(manifest.read_text(encoding="utf-8"))
|
|
804
|
+
created_at = old.get("created_at", created_at)
|
|
805
|
+
for item in old.get("managed_files", []):
|
|
806
|
+
if item.get("path"):
|
|
807
|
+
existing[item["path"]] = item
|
|
808
|
+
except Exception:
|
|
809
|
+
existing = {}
|
|
810
|
+
|
|
811
|
+
for line in records_file.read_text(encoding="utf-8").splitlines():
|
|
812
|
+
if not line:
|
|
813
|
+
continue
|
|
814
|
+
path, source, digest, marker = (line.split("|", 3) + ["", "", "", ""])[:4]
|
|
815
|
+
existing[path] = {
|
|
816
|
+
"path": path,
|
|
817
|
+
"source": source,
|
|
818
|
+
"content_hash": digest,
|
|
819
|
+
"marker": marker,
|
|
820
|
+
"updated_at": now,
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
skipped = [x for x in skipped_file.read_text(encoding="utf-8").splitlines() if x]
|
|
824
|
+
data = {
|
|
825
|
+
"_agentic": {
|
|
826
|
+
"generated_by": "agentic",
|
|
827
|
+
"repository": repo_link,
|
|
828
|
+
},
|
|
829
|
+
"version": 1,
|
|
830
|
+
"created_at": created_at,
|
|
831
|
+
"updated_at": now,
|
|
832
|
+
"settings": {
|
|
833
|
+
"agent_os": agent_os,
|
|
834
|
+
"areas": areas,
|
|
835
|
+
"specializations": specs,
|
|
836
|
+
"source_repo": repo_link,
|
|
837
|
+
"source_checkout": repo_root,
|
|
838
|
+
},
|
|
839
|
+
"managed_files": sorted(existing.values(), key=lambda x: x["path"]),
|
|
840
|
+
"skipped_files": skipped,
|
|
841
|
+
}
|
|
842
|
+
manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
843
|
+
PY
|
|
844
|
+
rm -f "$records_file" "$skipped_file"
|
|
845
|
+
unique_append "$manifest" COPIED_PATHS
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
load_install_settings_from_manifest() {
|
|
849
|
+
local manifest="$1"
|
|
850
|
+
[[ -f "$manifest" ]] || return 1
|
|
851
|
+
ensure_python_available
|
|
852
|
+
|
|
853
|
+
local values=()
|
|
854
|
+
readlines values < <(python3 - "$manifest" <<'PY'
|
|
855
|
+
import json
|
|
856
|
+
import sys
|
|
857
|
+
from pathlib import Path
|
|
858
|
+
|
|
859
|
+
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
860
|
+
settings = data.get("settings", {})
|
|
861
|
+
for key in ("agent_os", "areas", "specializations"):
|
|
862
|
+
print("::" + key)
|
|
863
|
+
for value in settings.get(key, []):
|
|
864
|
+
print(value)
|
|
865
|
+
PY
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
local section=""
|
|
869
|
+
local loaded_agent_os=()
|
|
870
|
+
local loaded_areas=()
|
|
871
|
+
local loaded_specs=()
|
|
872
|
+
local value
|
|
873
|
+
for value in "${values[@]}"; do
|
|
874
|
+
case "$value" in
|
|
875
|
+
"::agent_os") section="agent_os" ;;
|
|
876
|
+
"::areas") section="areas" ;;
|
|
877
|
+
"::specializations") section="specializations" ;;
|
|
878
|
+
*)
|
|
879
|
+
case "$section" in
|
|
880
|
+
agent_os) loaded_agent_os+=("$value") ;;
|
|
881
|
+
areas) loaded_areas+=("$value") ;;
|
|
882
|
+
specializations) loaded_specs+=("$value") ;;
|
|
883
|
+
esac
|
|
884
|
+
;;
|
|
885
|
+
esac
|
|
886
|
+
done
|
|
887
|
+
|
|
888
|
+
if [[ "${#SELECTED_AGENT_OS[@]}" -eq 1 && "${SELECTED_AGENT_OS[0]}" == "$DEFAULT_AGENT_OS" && "${#loaded_agent_os[@]}" -gt 0 ]]; then
|
|
889
|
+
SELECTED_AGENT_OS=("${loaded_agent_os[@]}")
|
|
890
|
+
fi
|
|
891
|
+
if [[ "${#SELECTED_AREAS[@]}" -eq 0 && "${#loaded_areas[@]}" -gt 0 ]]; then
|
|
892
|
+
SELECTED_AREAS=("${loaded_areas[@]}")
|
|
893
|
+
fi
|
|
894
|
+
if [[ "${#SELECTED_SPECS[@]}" -eq 0 && "${#loaded_specs[@]}" -gt 0 ]]; then
|
|
895
|
+
SELECTED_SPECS=("${loaded_specs[@]}")
|
|
896
|
+
fi
|
|
897
|
+
}
|
|
898
|
+
|
|
508
899
|
path_ref_for_shell_export() {
|
|
509
900
|
local dir="$1"
|
|
510
901
|
if [[ "$dir" == "$HOME/"* ]]; then
|
|
@@ -583,11 +974,483 @@ copy_dir_contents() {
|
|
|
583
974
|
local dest="$2"
|
|
584
975
|
ensure_dir "$dest"
|
|
585
976
|
if [[ "$DRY_RUN" == true ]]; then
|
|
586
|
-
log "DRY-RUN
|
|
977
|
+
log "DRY-RUN copy managed contents $src -> $dest"
|
|
978
|
+
unique_append "$dest" COPIED_PATHS
|
|
979
|
+
return
|
|
980
|
+
fi
|
|
981
|
+
|
|
982
|
+
local event kind value events_file
|
|
983
|
+
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'
|
|
985
|
+
import hashlib
|
|
986
|
+
import json
|
|
987
|
+
import sys
|
|
988
|
+
from pathlib import Path
|
|
989
|
+
|
|
990
|
+
src = Path(sys.argv[1])
|
|
991
|
+
dest_root = Path(sys.argv[2])
|
|
992
|
+
repo_root = Path(sys.argv[3])
|
|
993
|
+
project_dir = Path(sys.argv[4])
|
|
994
|
+
manifest = Path(sys.argv[5])
|
|
995
|
+
repo = sys.argv[6]
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def emit(kind: str, value: str) -> None:
|
|
999
|
+
print(f"{kind}\t{value}")
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def sha256(path: Path) -> str:
|
|
1003
|
+
h = hashlib.sha256()
|
|
1004
|
+
with path.open("rb") as fh:
|
|
1005
|
+
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
|
1006
|
+
h.update(chunk)
|
|
1007
|
+
return h.hexdigest()
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
managed = None
|
|
1011
|
+
if manifest.exists():
|
|
1012
|
+
managed = {}
|
|
1013
|
+
try:
|
|
1014
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
1015
|
+
for item in data.get("managed_files", []):
|
|
1016
|
+
rel = item.get("path")
|
|
1017
|
+
if rel:
|
|
1018
|
+
managed[rel] = item.get("content_hash", "")
|
|
1019
|
+
except Exception:
|
|
1020
|
+
managed = {}
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def rel_to_project(path: Path) -> str:
|
|
1024
|
+
try:
|
|
1025
|
+
return str(path.relative_to(project_dir))
|
|
1026
|
+
except ValueError:
|
|
1027
|
+
return str(path)
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def rel_to_repo(path: Path) -> str:
|
|
1031
|
+
try:
|
|
1032
|
+
return str(path.relative_to(repo_root))
|
|
1033
|
+
except ValueError:
|
|
1034
|
+
return str(path)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def yaml_quote(value: str) -> str:
|
|
1038
|
+
return json.dumps(value, ensure_ascii=False)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def markdown_with_marker(body: str, source_ref: str) -> str:
|
|
1042
|
+
block = (
|
|
1043
|
+
"agentic:\n"
|
|
1044
|
+
" generated_by: agentic\n"
|
|
1045
|
+
f" source: {yaml_quote(source_ref)}\n"
|
|
1046
|
+
f" repository: {yaml_quote(repo)}\n"
|
|
1047
|
+
)
|
|
1048
|
+
if body.startswith("---\n"):
|
|
1049
|
+
end = body.find("\n---", 4)
|
|
1050
|
+
if end != -1:
|
|
1051
|
+
return body[: end + 1] + block + body[end + 1 :]
|
|
1052
|
+
return "---\n" + block + "---\n" + body
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def commented(body: str, prefix: str, source_ref: str) -> str:
|
|
1056
|
+
marker = f"Generated by agentic; source: {source_ref}; repository: {repo}"
|
|
1057
|
+
line = f"{prefix} {marker}\n"
|
|
1058
|
+
if body.startswith("#!"):
|
|
1059
|
+
first, sep, rest = body.partition("\n")
|
|
1060
|
+
if sep:
|
|
1061
|
+
return first + sep + line + rest
|
|
1062
|
+
return line + body
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def add_marker(file_path: Path, target: Path, source_ref: str) -> str:
|
|
1066
|
+
text = file_path.read_text(encoding="utf-8")
|
|
1067
|
+
suffix = target.suffix.lower()
|
|
1068
|
+
if suffix == ".md":
|
|
1069
|
+
return markdown_with_marker(text, source_ref)
|
|
1070
|
+
if suffix == ".json":
|
|
1071
|
+
data = json.loads(text)
|
|
1072
|
+
if not isinstance(data, dict):
|
|
1073
|
+
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
|
+
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
1080
|
+
if suffix in {".ts", ".tsx", ".js", ".jsx", ".css"}:
|
|
1081
|
+
return commented(text, "//", source_ref)
|
|
1082
|
+
if suffix in {".sh", ".toml", ".py", ".yml", ".yaml"}:
|
|
1083
|
+
return commented(text, "#", source_ref)
|
|
1084
|
+
return commented(text, "#", source_ref)
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
emit("DIR", str(dest_root))
|
|
1088
|
+
for file_path in sorted(p for p in src.rglob("*") if p.is_file()):
|
|
1089
|
+
rel = file_path.relative_to(src)
|
|
1090
|
+
target = dest_root / rel
|
|
1091
|
+
project_rel = rel_to_project(target)
|
|
1092
|
+
source_ref = rel_to_repo(file_path)
|
|
1093
|
+
|
|
1094
|
+
if managed is not None:
|
|
1095
|
+
if project_rel not in managed:
|
|
1096
|
+
if target.exists():
|
|
1097
|
+
emit("WARN", f"Skipping unmanaged target on rerun: {project_rel}")
|
|
1098
|
+
emit("SKIP", project_rel)
|
|
1099
|
+
continue
|
|
1100
|
+
expected_hash = managed.get(project_rel, "")
|
|
1101
|
+
if target.exists() and expected_hash and sha256(target) != expected_hash:
|
|
1102
|
+
emit("WARN", f"Skipping user-modified managed file: {project_rel}")
|
|
1103
|
+
emit("SKIP", project_rel)
|
|
1104
|
+
continue
|
|
1105
|
+
|
|
1106
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1107
|
+
emit("DIR", str(target.parent))
|
|
1108
|
+
target.write_text(add_marker(file_path, target, source_ref), encoding="utf-8")
|
|
1109
|
+
digest = sha256(target)
|
|
1110
|
+
emit("RECORD", f"{project_rel}|{source_ref}|{digest}|internal")
|
|
1111
|
+
emit("COPIED", str(target))
|
|
1112
|
+
PY
|
|
1113
|
+
while IFS= read -r event || [[ -n "$event" ]]; do
|
|
1114
|
+
kind="${event%% *}"
|
|
1115
|
+
if [[ "$kind" == "$event" ]]; then
|
|
1116
|
+
value=""
|
|
1117
|
+
else
|
|
1118
|
+
value="${event#* }"
|
|
1119
|
+
fi
|
|
1120
|
+
record_agentic_event "$kind" "$value"
|
|
1121
|
+
done < "$events_file"
|
|
1122
|
+
rm -f "$events_file"
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
write_generated_text_file() {
|
|
1126
|
+
local dest="$1"
|
|
1127
|
+
local source_ref="$2"
|
|
1128
|
+
local content="$3"
|
|
1129
|
+
|
|
1130
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1131
|
+
log "DRY-RUN write generated file $dest"
|
|
1132
|
+
unique_append "$dest" COPIED_PATHS
|
|
1133
|
+
return
|
|
1134
|
+
fi
|
|
1135
|
+
|
|
1136
|
+
can_write_managed_file "$dest" || return 0
|
|
1137
|
+
ensure_dir "$(dirname -- "$dest")"
|
|
1138
|
+
|
|
1139
|
+
local tmp
|
|
1140
|
+
tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-generated.XXXXXX")"
|
|
1141
|
+
printf '%s' "$content" > "$tmp"
|
|
1142
|
+
write_file_with_agentic_marker "$tmp" "$dest" "$source_ref"
|
|
1143
|
+
rm -f "$tmp"
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
write_json_file_with_agentic_metadata() {
|
|
1147
|
+
local dest="$1"
|
|
1148
|
+
local source_ref="$2"
|
|
1149
|
+
local python_body="$3"
|
|
1150
|
+
|
|
1151
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1152
|
+
log "DRY-RUN update JSON managed file $dest"
|
|
1153
|
+
unique_append "$dest" COPIED_PATHS
|
|
1154
|
+
return
|
|
1155
|
+
fi
|
|
1156
|
+
|
|
1157
|
+
can_write_managed_file "$dest" || return 0
|
|
1158
|
+
ensure_dir "$(dirname -- "$dest")"
|
|
1159
|
+
python3 - "$dest" "$source_ref" "$APP_REPO_LINK" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
|
|
1160
|
+
import json
|
|
1161
|
+
import sys
|
|
1162
|
+
from pathlib import Path
|
|
1163
|
+
|
|
1164
|
+
path = Path(sys.argv[1])
|
|
1165
|
+
source_ref = sys.argv[2]
|
|
1166
|
+
repo = sys.argv[3]
|
|
1167
|
+
context7_api_key = sys.argv[4]
|
|
1168
|
+
body = sys.argv[5]
|
|
1169
|
+
|
|
1170
|
+
data = {}
|
|
1171
|
+
if path.exists():
|
|
1172
|
+
try:
|
|
1173
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
1174
|
+
except Exception:
|
|
1175
|
+
data = {}
|
|
1176
|
+
if not isinstance(data, dict):
|
|
1177
|
+
data = {}
|
|
1178
|
+
|
|
1179
|
+
namespace = {
|
|
1180
|
+
"data": data,
|
|
1181
|
+
"context7_api_key": context7_api_key,
|
|
1182
|
+
}
|
|
1183
|
+
exec(body, namespace)
|
|
1184
|
+
data = namespace["data"]
|
|
1185
|
+
data["_agentic"] = {
|
|
1186
|
+
"generated_by": "agentic",
|
|
1187
|
+
"source": source_ref,
|
|
1188
|
+
"repository": repo,
|
|
1189
|
+
}
|
|
1190
|
+
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
1191
|
+
PY
|
|
1192
|
+
register_managed_file "$dest" "$source_ref" "internal"
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
write_json_config_file() {
|
|
1196
|
+
local dest="$1"
|
|
1197
|
+
local source_ref="$2"
|
|
1198
|
+
local python_body="$3"
|
|
1199
|
+
|
|
1200
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1201
|
+
log "DRY-RUN update JSON config file $dest"
|
|
1202
|
+
unique_append "$dest" COPIED_PATHS
|
|
1203
|
+
return
|
|
1204
|
+
fi
|
|
1205
|
+
|
|
1206
|
+
can_write_managed_file "$dest" || return 0
|
|
1207
|
+
ensure_dir "$(dirname -- "$dest")"
|
|
1208
|
+
python3 - "$dest" "$CONTEXT7_API_KEY" "$python_body" <<'PY'
|
|
1209
|
+
import json
|
|
1210
|
+
import sys
|
|
1211
|
+
from pathlib import Path
|
|
1212
|
+
|
|
1213
|
+
path = Path(sys.argv[1])
|
|
1214
|
+
context7_api_key = sys.argv[2]
|
|
1215
|
+
body = sys.argv[3]
|
|
1216
|
+
|
|
1217
|
+
data = {}
|
|
1218
|
+
if path.exists():
|
|
1219
|
+
try:
|
|
1220
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
1221
|
+
except Exception:
|
|
1222
|
+
data = {}
|
|
1223
|
+
if not isinstance(data, dict):
|
|
1224
|
+
data = {}
|
|
1225
|
+
|
|
1226
|
+
namespace = {
|
|
1227
|
+
"data": data,
|
|
1228
|
+
"context7_api_key": context7_api_key,
|
|
1229
|
+
}
|
|
1230
|
+
exec(body, namespace)
|
|
1231
|
+
data = namespace["data"]
|
|
1232
|
+
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
1233
|
+
PY
|
|
1234
|
+
register_managed_file "$dest" "$source_ref" "config"
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
write_context7_opencode_config() {
|
|
1238
|
+
local dest="$PROJECT_DIR/opencode.json"
|
|
1239
|
+
local body
|
|
1240
|
+
body='
|
|
1241
|
+
mcp = data.setdefault("mcp", {})
|
|
1242
|
+
context7 = {
|
|
1243
|
+
"type": "remote",
|
|
1244
|
+
"url": "https://mcp.context7.com/mcp",
|
|
1245
|
+
"enabled": True,
|
|
1246
|
+
}
|
|
1247
|
+
if context7_api_key:
|
|
1248
|
+
context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
|
|
1249
|
+
mcp["context7"] = context7
|
|
1250
|
+
'
|
|
1251
|
+
write_json_config_file "$dest" "generated:context7-opencode-config" "$body"
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
write_context7_opencode_legacy_config() {
|
|
1255
|
+
local dest="$PROJECT_DIR/.opencode/opencode.json"
|
|
1256
|
+
local body
|
|
1257
|
+
body='
|
|
1258
|
+
mcp = data.setdefault("mcp", {})
|
|
1259
|
+
context7 = {
|
|
1260
|
+
"type": "remote",
|
|
1261
|
+
"url": "https://mcp.context7.com/mcp",
|
|
1262
|
+
"enabled": True,
|
|
1263
|
+
}
|
|
1264
|
+
if context7_api_key:
|
|
1265
|
+
context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
|
|
1266
|
+
mcp["context7"] = context7
|
|
1267
|
+
'
|
|
1268
|
+
write_json_file_with_agentic_metadata "$dest" "generated:context7-opencode-legacy-config" "$body"
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
write_context7_codex_config() {
|
|
1272
|
+
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}"
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
write_context7_claude_config() {
|
|
1287
|
+
local dest="$PROJECT_DIR/.mcp.json"
|
|
1288
|
+
local body
|
|
1289
|
+
body='
|
|
1290
|
+
mcp_servers = data.setdefault("mcpServers", {})
|
|
1291
|
+
context7 = {
|
|
1292
|
+
"type": "http",
|
|
1293
|
+
"url": "https://mcp.context7.com/mcp",
|
|
1294
|
+
}
|
|
1295
|
+
if context7_api_key:
|
|
1296
|
+
context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
|
|
1297
|
+
mcp_servers["context7"] = context7
|
|
1298
|
+
'
|
|
1299
|
+
write_json_config_file "$dest" "generated:context7-claude-config" "$body"
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
write_context7_cursor_config() {
|
|
1303
|
+
local dest="$PROJECT_DIR/.cursor/mcp.json"
|
|
1304
|
+
local body
|
|
1305
|
+
body='
|
|
1306
|
+
mcp_servers = data.setdefault("mcpServers", {})
|
|
1307
|
+
context7 = {
|
|
1308
|
+
"url": "https://mcp.context7.com/mcp",
|
|
1309
|
+
}
|
|
1310
|
+
if context7_api_key:
|
|
1311
|
+
context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
|
|
1312
|
+
mcp_servers["context7"] = context7
|
|
1313
|
+
'
|
|
1314
|
+
write_json_config_file "$dest" "generated:context7-cursor-config" "$body"
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
write_context7_gemini_config() {
|
|
1318
|
+
local dest="$PROJECT_DIR/.gemini/settings.json"
|
|
1319
|
+
local body
|
|
1320
|
+
body='
|
|
1321
|
+
mcp_servers = data.setdefault("mcpServers", {})
|
|
1322
|
+
context7 = {
|
|
1323
|
+
"httpUrl": "https://mcp.context7.com/mcp",
|
|
1324
|
+
}
|
|
1325
|
+
if context7_api_key:
|
|
1326
|
+
context7["headers"] = {"CONTEXT7_API_KEY": context7_api_key}
|
|
1327
|
+
mcp_servers["context7"] = context7
|
|
1328
|
+
'
|
|
1329
|
+
write_json_config_file "$dest" "generated:context7-gemini-config" "$body"
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
configure_context7_if_needed() {
|
|
1333
|
+
if ! selected_agent_os_contains "opencode" \
|
|
1334
|
+
&& ! selected_agent_os_contains "codex" \
|
|
1335
|
+
&& ! selected_agent_os_contains "claude" \
|
|
1336
|
+
&& ! selected_agent_os_contains "cursor" \
|
|
1337
|
+
&& ! selected_agent_os_contains "gemini"; then
|
|
1338
|
+
return
|
|
1339
|
+
fi
|
|
1340
|
+
|
|
1341
|
+
if is_interactive_terminal; then
|
|
1342
|
+
local enable_context7 answer
|
|
1343
|
+
read -r -p "Enable Context7 MCP configuration? [y/N]: " enable_context7
|
|
1344
|
+
enable_context7="$(trim "$enable_context7")"
|
|
1345
|
+
if [[ ! "$enable_context7" =~ ^[Yy]$ ]]; then
|
|
1346
|
+
log "Context7 MCP configuration disabled"
|
|
1347
|
+
return
|
|
1348
|
+
fi
|
|
1349
|
+
|
|
1350
|
+
if [[ -z "$CONTEXT7_API_KEY" ]]; then
|
|
1351
|
+
read -r -p "Context7 API key (optional, empty = no key): " answer
|
|
1352
|
+
CONTEXT7_API_KEY="$(trim "$answer")"
|
|
1353
|
+
fi
|
|
1354
|
+
elif [[ -z "$CONTEXT7_API_KEY" ]]; then
|
|
1355
|
+
log "Context7 MCP configuration skipped; set CONTEXT7_API_KEY or use an interactive install to enable it"
|
|
1356
|
+
return
|
|
1357
|
+
fi
|
|
1358
|
+
|
|
1359
|
+
if selected_agent_os_contains "opencode"; then
|
|
1360
|
+
write_context7_opencode_config
|
|
1361
|
+
write_context7_opencode_legacy_config
|
|
1362
|
+
fi
|
|
1363
|
+
|
|
1364
|
+
if selected_agent_os_contains "codex"; then
|
|
1365
|
+
write_context7_codex_config
|
|
1366
|
+
fi
|
|
1367
|
+
|
|
1368
|
+
if selected_agent_os_contains "claude"; then
|
|
1369
|
+
write_context7_claude_config
|
|
1370
|
+
fi
|
|
1371
|
+
|
|
1372
|
+
if selected_agent_os_contains "cursor"; then
|
|
1373
|
+
write_context7_cursor_config
|
|
1374
|
+
fi
|
|
1375
|
+
|
|
1376
|
+
if selected_agent_os_contains "gemini"; then
|
|
1377
|
+
write_context7_gemini_config
|
|
1378
|
+
fi
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
write_default_opencode_plugin_config() {
|
|
1382
|
+
ensure_dir "$APP_CONFIG_DIR"
|
|
1383
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1384
|
+
log "DRY-RUN write disabled opencode plugin config to $OPENCODE_PLUGIN_CONFIG_FILE"
|
|
587
1385
|
else
|
|
588
|
-
|
|
1386
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" <<'PY'
|
|
1387
|
+
import json
|
|
1388
|
+
import sys
|
|
1389
|
+
from pathlib import Path
|
|
1390
|
+
|
|
1391
|
+
path = Path(sys.argv[1])
|
|
1392
|
+
path.write_text(json.dumps({
|
|
1393
|
+
"telegram": {"enabled": False, "botToken": "", "chatId": ""},
|
|
1394
|
+
"modelChecker": {"enabled": False},
|
|
1395
|
+
}, indent=2) + "\n", encoding="utf-8")
|
|
1396
|
+
PY
|
|
589
1397
|
fi
|
|
590
|
-
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
configure_opencode_plugins_if_needed() {
|
|
1401
|
+
selected_agent_os_contains "opencode" || return 0
|
|
1402
|
+
|
|
1403
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
1404
|
+
log "DRY-RUN configure optional opencode plugins"
|
|
1405
|
+
return
|
|
1406
|
+
fi
|
|
1407
|
+
|
|
1408
|
+
ensure_python_available
|
|
1409
|
+
ensure_dir "$APP_CONFIG_DIR"
|
|
1410
|
+
|
|
1411
|
+
if ! is_interactive_terminal; then
|
|
1412
|
+
if [[ ! -f "$OPENCODE_PLUGIN_CONFIG_FILE" ]]; then
|
|
1413
|
+
write_default_opencode_plugin_config
|
|
1414
|
+
fi
|
|
1415
|
+
return
|
|
1416
|
+
fi
|
|
1417
|
+
|
|
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")"
|
|
1421
|
+
telegram_token=""
|
|
1422
|
+
telegram_chat=""
|
|
1423
|
+
if [[ "$enable_telegram" =~ ^[Yy]$ ]]; then
|
|
1424
|
+
read -r -p "Telegram bot token (empty disables plugin): " telegram_token
|
|
1425
|
+
read -r -p "Telegram chat id (empty disables plugin): " telegram_chat
|
|
1426
|
+
telegram_token="$(trim "$telegram_token")"
|
|
1427
|
+
telegram_chat="$(trim "$telegram_chat")"
|
|
1428
|
+
fi
|
|
1429
|
+
|
|
1430
|
+
read -r -p "Enable OpenCode model checker plugin? [y/N]: " enable_model_checker
|
|
1431
|
+
enable_model_checker="$(trim "$enable_model_checker")"
|
|
1432
|
+
|
|
1433
|
+
python3 - "$OPENCODE_PLUGIN_CONFIG_FILE" "$telegram_token" "$telegram_chat" "$enable_model_checker" <<'PY'
|
|
1434
|
+
import json
|
|
1435
|
+
import sys
|
|
1436
|
+
from pathlib import Path
|
|
1437
|
+
|
|
1438
|
+
path = Path(sys.argv[1])
|
|
1439
|
+
token = sys.argv[2]
|
|
1440
|
+
chat = sys.argv[3]
|
|
1441
|
+
enable_model = sys.argv[4].lower() == "y"
|
|
1442
|
+
data = {
|
|
1443
|
+
"telegram": {
|
|
1444
|
+
"enabled": bool(token and chat),
|
|
1445
|
+
"botToken": token,
|
|
1446
|
+
"chatId": chat,
|
|
1447
|
+
},
|
|
1448
|
+
"modelChecker": {
|
|
1449
|
+
"enabled": enable_model,
|
|
1450
|
+
},
|
|
1451
|
+
}
|
|
1452
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
1453
|
+
PY
|
|
591
1454
|
}
|
|
592
1455
|
|
|
593
1456
|
normalize_selected_agent_os() {
|
|
@@ -756,15 +1619,18 @@ generate_agents_md() {
|
|
|
756
1619
|
fi
|
|
757
1620
|
|
|
758
1621
|
ensure_dir "$project_dir"
|
|
759
|
-
|
|
760
|
-
|
|
1622
|
+
local tmp
|
|
1623
|
+
tmp="$(mktemp "${TMPDIR:-/tmp}/agentic-agents.XXXXXX")"
|
|
1624
|
+
build_header "$tmp"
|
|
1625
|
+
append_root_agents_template "$tmp"
|
|
761
1626
|
|
|
762
1627
|
local spec_key
|
|
763
1628
|
for spec_key in "${SELECTED_SPECS[@]}"; do
|
|
764
|
-
append_specialization_template "$
|
|
1629
|
+
append_specialization_template "$tmp" "$spec_key"
|
|
765
1630
|
done
|
|
766
1631
|
|
|
767
|
-
|
|
1632
|
+
write_file_with_agentic_marker "$tmp" "$out" "generated:AGENTS.md"
|
|
1633
|
+
rm -f "$tmp"
|
|
768
1634
|
}
|
|
769
1635
|
|
|
770
1636
|
validate_inputs() {
|
|
@@ -870,13 +1736,17 @@ print_report() {
|
|
|
870
1736
|
|
|
871
1737
|
run_install() {
|
|
872
1738
|
ensure_repo_layout
|
|
1739
|
+
ensure_python_available
|
|
873
1740
|
normalize_selected_agent_os
|
|
874
1741
|
validate_inputs
|
|
875
1742
|
|
|
876
1743
|
ensure_dir "$PROJECT_DIR"
|
|
1744
|
+
configure_opencode_plugins_if_needed
|
|
877
1745
|
copy_extensions "$PROJECT_DIR"
|
|
878
1746
|
copy_specialization_assets "$PROJECT_DIR"
|
|
879
1747
|
generate_agents_md "$PROJECT_DIR"
|
|
1748
|
+
configure_context7_if_needed
|
|
1749
|
+
write_agentic_manifest "$PROJECT_DIR"
|
|
880
1750
|
print_report
|
|
881
1751
|
}
|
|
882
1752
|
|
|
@@ -1256,7 +2126,9 @@ run_tui() {
|
|
|
1256
2126
|
if [[ "$use_fzf" == true ]]; then
|
|
1257
2127
|
readlines picked_agent_os < <(choose_multi_fzf "Select Agent OS target(s):" "${agentos_choices[@]}")
|
|
1258
2128
|
else
|
|
1259
|
-
|
|
2129
|
+
local picked_agent_os_output
|
|
2130
|
+
picked_agent_os_output="$(choose_multi_by_index "Select Agent OS target(s):" "${agentos_choices[@]}")"
|
|
2131
|
+
readlines picked_agent_os <<< "$picked_agent_os_output"
|
|
1260
2132
|
fi
|
|
1261
2133
|
if [[ "${#picked_agent_os[@]}" -eq 0 ]]; then
|
|
1262
2134
|
SELECTED_AGENT_OS=("$DEFAULT_AGENT_OS")
|
|
@@ -1271,7 +2143,9 @@ run_tui() {
|
|
|
1271
2143
|
if [[ "$use_fzf" == true ]]; then
|
|
1272
2144
|
readlines picked_areas < <(choose_multi_fzf "Select area(s):" "${areas[@]}")
|
|
1273
2145
|
else
|
|
1274
|
-
|
|
2146
|
+
local picked_areas_output
|
|
2147
|
+
picked_areas_output="$(choose_multi_by_index "Select area(s):" "${areas[@]}")"
|
|
2148
|
+
readlines picked_areas <<< "$picked_areas_output"
|
|
1275
2149
|
fi
|
|
1276
2150
|
|
|
1277
2151
|
if [[ "${#picked_areas[@]}" -eq 0 ]]; then
|
|
@@ -1290,7 +2164,9 @@ run_tui() {
|
|
|
1290
2164
|
if [[ "$use_fzf" == true ]]; then
|
|
1291
2165
|
readlines chosen_specs < <(choose_multi_fzf "Select specialization(s) for '$area':" "${specs[@]}")
|
|
1292
2166
|
else
|
|
1293
|
-
|
|
2167
|
+
local chosen_specs_output
|
|
2168
|
+
chosen_specs_output="$(choose_multi_by_index "Select specialization(s) for '$area':" "${specs[@]}")"
|
|
2169
|
+
readlines chosen_specs <<< "$chosen_specs_output"
|
|
1294
2170
|
fi
|
|
1295
2171
|
|
|
1296
2172
|
if [[ "${#chosen_specs[@]}" -eq 0 ]]; then
|
|
@@ -1325,6 +2201,22 @@ self_install() {
|
|
|
1325
2201
|
|
|
1326
2202
|
ensure_dir "$bin_dir"
|
|
1327
2203
|
|
|
2204
|
+
if [[ -e "$source_path" && -e "$target" ]]; then
|
|
2205
|
+
local source_real target_real
|
|
2206
|
+
source_real="$(cd -- "$(dirname -- "$source_path")" && pwd -P)/$(basename -- "$source_path")"
|
|
2207
|
+
target_real="$(cd -- "$(dirname -- "$target")" && pwd -P)/$(basename -- "$target")"
|
|
2208
|
+
if [[ "$source_real" == "$target_real" ]]; then
|
|
2209
|
+
if [[ -e "$APP_REPO_DIR/agentic" ]]; then
|
|
2210
|
+
source_path="$APP_REPO_DIR/agentic"
|
|
2211
|
+
else
|
|
2212
|
+
log "Source and target are already the same file: $target"
|
|
2213
|
+
log "Nothing to copy. Run '$APP_NAME upgrade' first to refresh the knowledge base checkout, or run self-install from a checkout."
|
|
2214
|
+
self_install_fzf_optional
|
|
2215
|
+
return
|
|
2216
|
+
fi
|
|
2217
|
+
fi
|
|
2218
|
+
fi
|
|
2219
|
+
|
|
1328
2220
|
if [[ -e "$target" ]] && [[ "$SELF_INSTALL_FORCE" != true ]]; then
|
|
1329
2221
|
error "Target already exists: $target"
|
|
1330
2222
|
error "Use --force to overwrite"
|
|
@@ -1363,6 +2255,30 @@ self_install() {
|
|
|
1363
2255
|
echo "Dry-run: $DRY_RUN"
|
|
1364
2256
|
}
|
|
1365
2257
|
|
|
2258
|
+
update_installed_binary_from_repo() {
|
|
2259
|
+
local source_path="$APP_REPO_DIR/agentic"
|
|
2260
|
+
local target="$SCRIPT_SOURCE"
|
|
2261
|
+
|
|
2262
|
+
if [[ ! -r "$source_path" ]]; then
|
|
2263
|
+
warn "Cannot self-update installed binary; source not found: $source_path"
|
|
2264
|
+
return
|
|
2265
|
+
fi
|
|
2266
|
+
|
|
2267
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
2268
|
+
log "DRY-RUN self-update installed binary $target from $source_path"
|
|
2269
|
+
return
|
|
2270
|
+
fi
|
|
2271
|
+
|
|
2272
|
+
if cmp -s "$source_path" "$target"; then
|
|
2273
|
+
log "Installed binary already up to date: $target"
|
|
2274
|
+
return
|
|
2275
|
+
fi
|
|
2276
|
+
|
|
2277
|
+
cp "$source_path" "$target"
|
|
2278
|
+
chmod +x "$target"
|
|
2279
|
+
log "Updated installed binary: $target"
|
|
2280
|
+
}
|
|
2281
|
+
|
|
1366
2282
|
upgrade_repo_checkout() {
|
|
1367
2283
|
refresh_repo_paths
|
|
1368
2284
|
|
|
@@ -1392,6 +2308,21 @@ upgrade_repo_checkout() {
|
|
|
1392
2308
|
git -C "$APP_REPO_DIR" pull --ff-only
|
|
1393
2309
|
refresh_repo_paths
|
|
1394
2310
|
ensure_repo_layout
|
|
2311
|
+
update_installed_binary_from_repo
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
sync_current_project_after_upgrade() {
|
|
2315
|
+
local manifest="$PWD/$PROJECT_MANIFEST_NAME"
|
|
2316
|
+
if [[ ! -f "$manifest" ]]; then
|
|
2317
|
+
log "No $PROJECT_MANIFEST_NAME in current directory; knowledge base upgrade complete"
|
|
2318
|
+
return
|
|
2319
|
+
fi
|
|
2320
|
+
|
|
2321
|
+
log "Detected managed project in $PWD; syncing from upgraded knowledge base"
|
|
2322
|
+
PROJECT_DIR="$(pwd -P)"
|
|
2323
|
+
load_install_settings_from_manifest "$manifest"
|
|
2324
|
+
ensure_repo_layout
|
|
2325
|
+
run_install
|
|
1395
2326
|
}
|
|
1396
2327
|
|
|
1397
2328
|
parse_theme_option() {
|
|
@@ -1499,6 +2430,12 @@ case "$COMMAND" in
|
|
|
1499
2430
|
;;
|
|
1500
2431
|
esac
|
|
1501
2432
|
done
|
|
2433
|
+
if [[ -z "$PROJECT_DIR" && -f "$PWD/$PROJECT_MANIFEST_NAME" ]]; then
|
|
2434
|
+
PROJECT_DIR="$PWD"
|
|
2435
|
+
load_install_settings_from_manifest "$PWD/$PROJECT_MANIFEST_NAME"
|
|
2436
|
+
elif [[ -n "$PROJECT_DIR" && -f "$PROJECT_DIR/$PROJECT_MANIFEST_NAME" ]]; then
|
|
2437
|
+
load_install_settings_from_manifest "$PROJECT_DIR/$PROJECT_MANIFEST_NAME"
|
|
2438
|
+
fi
|
|
1502
2439
|
ensure_repo_available_for_command
|
|
1503
2440
|
ensure_repo_layout
|
|
1504
2441
|
set_theme_colors
|
|
@@ -1557,6 +2494,7 @@ case "$COMMAND" in
|
|
|
1557
2494
|
esac
|
|
1558
2495
|
done
|
|
1559
2496
|
upgrade_repo_checkout
|
|
2497
|
+
sync_current_project_after_upgrade
|
|
1560
2498
|
;;
|
|
1561
2499
|
|
|
1562
2500
|
self-install)
|