agentram 0.1.48 → 0.1.50
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/agentram/cli.py +90 -8
- package/agentram/orchestration.py +16 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/agentram/cli.py
CHANGED
|
@@ -15,7 +15,7 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
from .config import default_ram_root, global_agent_profile_enabled
|
|
17
17
|
from .mcp_server import McpContext, call_tool
|
|
18
|
-
from .orchestration import AgentModelEndpoint, MultiModelCodingOrchestrator
|
|
18
|
+
from .orchestration import AgentModelEndpoint, MultiModelCodingOrchestrator, set_cli_stream_callback
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
ENV_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
@@ -434,7 +434,9 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
434
434
|
self.prompt_queue: list[str] = []
|
|
435
435
|
self.processing_task: asyncio.Task[None] | None = None
|
|
436
436
|
self.pending_approval_prompt: str | None = None
|
|
437
|
+
self.pending_approval_kind = "main"
|
|
437
438
|
self.project_main_agent_approved = False
|
|
439
|
+
self.project_git_agent_approved = False
|
|
438
440
|
self.suggestions: list[str] = []
|
|
439
441
|
self.setup_done = False
|
|
440
442
|
self.prompt_history: list[str] = []
|
|
@@ -478,6 +480,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
478
480
|
yield Button("Next", id="setup-next", variant="primary")
|
|
479
481
|
yield Button("Skip", id="setup-skip")
|
|
480
482
|
yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
|
|
483
|
+
yield Static("", id="activity")
|
|
481
484
|
with Vertical(id="approval"):
|
|
482
485
|
yield Static("Main agent wants terminal/file access. Allow?", id="approval-title")
|
|
483
486
|
yield Static("This can run commands or edit files in the current project.")
|
|
@@ -493,6 +496,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
493
496
|
self.context.init_storage()
|
|
494
497
|
self.chat.write("system > AgentRAM Textual TUI ready")
|
|
495
498
|
self.chat.write(f"system > RAM root: {self.context.ram_root}")
|
|
499
|
+
self.chat.write(bash_hint_text())
|
|
496
500
|
self.prefill_setup_from_profile()
|
|
497
501
|
self.refresh_setup_step()
|
|
498
502
|
self.refresh_sidebar()
|
|
@@ -506,6 +510,12 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
506
510
|
def chat(self) -> RichLog:
|
|
507
511
|
return self.query_one("#chat", RichLog)
|
|
508
512
|
|
|
513
|
+
def update_activity(self, text: str) -> None:
|
|
514
|
+
try:
|
|
515
|
+
self.query_one("#activity", Static).update(text)
|
|
516
|
+
except Exception:
|
|
517
|
+
return
|
|
518
|
+
|
|
509
519
|
def refresh_sidebar(self) -> None:
|
|
510
520
|
self.query_one("#sidebar", Static).update("\n".join(sidebar_lines(self.context)))
|
|
511
521
|
|
|
@@ -669,11 +679,17 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
669
679
|
self.hide_setup()
|
|
670
680
|
self.query_one("#input", Input).focus()
|
|
671
681
|
|
|
672
|
-
def show_approval_popup(self, prompt: str) -> None:
|
|
682
|
+
def show_approval_popup(self, prompt: str, kind: str = "main") -> None:
|
|
673
683
|
panel = self.query_one("#approval", Vertical)
|
|
674
|
-
|
|
684
|
+
if kind == "git":
|
|
685
|
+
title = "Main agent wants to change Git repo. Allow git init/add/commit?"
|
|
686
|
+
note = "agentram > git approval needed: click Yes / Yes, do this project / No"
|
|
687
|
+
else:
|
|
688
|
+
title = "Main agent wants terminal/file access. Allow?"
|
|
689
|
+
note = "agentram > approval needed: click Yes / Yes, do this project / No"
|
|
690
|
+
self.query_one("#approval-title", Static).update(title)
|
|
675
691
|
panel.styles.display = "block"
|
|
676
|
-
self.chat.write(
|
|
692
|
+
self.chat.write(note)
|
|
677
693
|
|
|
678
694
|
def hide_approval_popup(self) -> None:
|
|
679
695
|
try:
|
|
@@ -689,8 +705,13 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
689
705
|
self.pending_approval_prompt = None
|
|
690
706
|
self.hide_approval_popup()
|
|
691
707
|
if decision == "yes_project":
|
|
692
|
-
self.
|
|
693
|
-
|
|
708
|
+
if self.pending_approval_kind == "git":
|
|
709
|
+
self.project_git_agent_approved = True
|
|
710
|
+
self.project_main_agent_approved = True
|
|
711
|
+
self.chat.write("agentram > git approved for this project")
|
|
712
|
+
else:
|
|
713
|
+
self.project_main_agent_approved = True
|
|
714
|
+
self.chat.write("agentram > approved for this project")
|
|
694
715
|
self.enqueue_prompt(approved)
|
|
695
716
|
return
|
|
696
717
|
if decision == "yes_once":
|
|
@@ -779,10 +800,16 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
779
800
|
return
|
|
780
801
|
if decision == "yes_project":
|
|
781
802
|
approved = self.pending_approval_prompt
|
|
803
|
+
kind = self.pending_approval_kind
|
|
782
804
|
self.pending_approval_prompt = None
|
|
783
805
|
self.hide_approval_popup()
|
|
784
|
-
|
|
785
|
-
|
|
806
|
+
if kind == "git":
|
|
807
|
+
self.project_git_agent_approved = True
|
|
808
|
+
self.project_main_agent_approved = True
|
|
809
|
+
self.chat.write("agentram > git approved for this project")
|
|
810
|
+
else:
|
|
811
|
+
self.project_main_agent_approved = True
|
|
812
|
+
self.chat.write("agentram > approved for this project")
|
|
786
813
|
self.enqueue_prompt(approved)
|
|
787
814
|
return
|
|
788
815
|
if decision == "no":
|
|
@@ -800,8 +827,14 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
800
827
|
self.chat.clear()
|
|
801
828
|
self.chat.write("system > Chat cleared.")
|
|
802
829
|
return
|
|
830
|
+
if requires_git_repo_approval(self.context, text) and not self.project_git_agent_approved:
|
|
831
|
+
self.pending_approval_prompt = text
|
|
832
|
+
self.pending_approval_kind = "git"
|
|
833
|
+
self.show_approval_popup(text, kind="git")
|
|
834
|
+
return
|
|
803
835
|
if requires_main_agent_approval(self.context, text) and not self.project_main_agent_approved:
|
|
804
836
|
self.pending_approval_prompt = text
|
|
837
|
+
self.pending_approval_kind = "main"
|
|
805
838
|
self.show_approval_popup(text)
|
|
806
839
|
return
|
|
807
840
|
self.enqueue_prompt(text)
|
|
@@ -824,10 +857,23 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
824
857
|
pending = len(self.prompt_queue)
|
|
825
858
|
suffix = f" ({pending} queued)" if pending else ""
|
|
826
859
|
self.chat.write(f"agentram > thinking...{suffix}")
|
|
860
|
+
def stream_to_activity(line: str) -> None:
|
|
861
|
+
clean = line.rstrip()
|
|
862
|
+
try:
|
|
863
|
+
if clean == "__agentram_stream_clear__":
|
|
864
|
+
self.call_from_thread(self.update_activity, "")
|
|
865
|
+
elif clean:
|
|
866
|
+
self.call_from_thread(self.update_activity, "cli > " + trim(clean, 140))
|
|
867
|
+
except Exception:
|
|
868
|
+
return
|
|
869
|
+
previous_callback = set_cli_stream_callback(stream_to_activity)
|
|
827
870
|
try:
|
|
828
871
|
response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
|
|
829
872
|
except Exception as error: # noqa: BLE001 - TUI boundary
|
|
830
873
|
response = f"error: {error}"
|
|
874
|
+
finally:
|
|
875
|
+
set_cli_stream_callback(previous_callback)
|
|
876
|
+
self.update_activity("")
|
|
831
877
|
if response == "__clear__":
|
|
832
878
|
self.chat.clear()
|
|
833
879
|
self.chat.write("system > Chat cleared.")
|
|
@@ -884,6 +930,13 @@ def create_prompt_session() -> Any | None:
|
|
|
884
930
|
)
|
|
885
931
|
|
|
886
932
|
|
|
933
|
+
def bash_hint_text() -> str:
|
|
934
|
+
bash_path = shutil.which("bash")
|
|
935
|
+
if bash_path:
|
|
936
|
+
return f"system > bash detected: {bash_path}. Use `bash -lc \"...\"` when needed."
|
|
937
|
+
return "system > bash not detected. Use PowerShell, or install Git Bash/WSL for bash commands."
|
|
938
|
+
|
|
939
|
+
|
|
887
940
|
def read_tui_input(prompt_session: Any | None) -> str:
|
|
888
941
|
if prompt_session is None:
|
|
889
942
|
return input("You > ")
|
|
@@ -899,6 +952,32 @@ def normalize_approval_decision(text: str) -> str:
|
|
|
899
952
|
return "no"
|
|
900
953
|
return ""
|
|
901
954
|
|
|
955
|
+
def git_repo_mutation_requested(prompt: str) -> bool:
|
|
956
|
+
lower = prompt.lower()
|
|
957
|
+
patterns = [
|
|
958
|
+
r"\bgit\s+init\b",
|
|
959
|
+
r"\bgit\s+add\b",
|
|
960
|
+
r"\bgit\s+commit\b",
|
|
961
|
+
r"\bgit\s+reset\b",
|
|
962
|
+
r"\bgit\s+checkout\b",
|
|
963
|
+
r"\bgit\s+clean\b",
|
|
964
|
+
r"\binitial\s+commit\b",
|
|
965
|
+
r"\btạo\s+git\b",
|
|
966
|
+
r"\btao\s+git\b",
|
|
967
|
+
r"\bkhởi\s+tạo\s+git\b",
|
|
968
|
+
r"\bkhoi\s+tao\s+git\b",
|
|
969
|
+
]
|
|
970
|
+
return any(re.search(pattern, lower) for pattern in patterns)
|
|
971
|
+
|
|
972
|
+
def requires_git_repo_approval(context: McpContext, prompt: str) -> bool:
|
|
973
|
+
main_agent = context.profile_store.list_agents().get("main")
|
|
974
|
+
if not main_agent:
|
|
975
|
+
return False
|
|
976
|
+
provider = main_agent.provider.lower()
|
|
977
|
+
if provider not in {"codex-cli", "claude-code", "claude-cli", "claude-subagent", "claude-code-subagent"}:
|
|
978
|
+
return False
|
|
979
|
+
return git_repo_mutation_requested(prompt)
|
|
980
|
+
|
|
902
981
|
def requires_main_agent_approval(context: McpContext, prompt: str) -> bool:
|
|
903
982
|
main_agent = context.profile_store.list_agents().get("main")
|
|
904
983
|
if not main_agent:
|
|
@@ -1865,6 +1944,9 @@ def sidebar_lines(context: McpContext) -> list[str]:
|
|
|
1865
1944
|
"TASK",
|
|
1866
1945
|
f" {trim(goal.current_task or '-', 24)}",
|
|
1867
1946
|
"",
|
|
1947
|
+
"SHELL",
|
|
1948
|
+
f" bash: {'yes' if shutil.which('bash') else 'no'}",
|
|
1949
|
+
"",
|
|
1868
1950
|
"COMMANDS",
|
|
1869
1951
|
" / = command palette",
|
|
1870
1952
|
" /setup",
|
|
@@ -18,6 +18,20 @@ from uuid import uuid4
|
|
|
18
18
|
from .config import global_agent_profile_enabled, global_agent_profile_path
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
CLI_STREAM_CALLBACK: Callable[[str], None] | None = None
|
|
22
|
+
|
|
23
|
+
def set_cli_stream_callback(callback: Callable[[str], None] | None) -> Callable[[str], None] | None:
|
|
24
|
+
global CLI_STREAM_CALLBACK
|
|
25
|
+
previous = CLI_STREAM_CALLBACK
|
|
26
|
+
CLI_STREAM_CALLBACK = callback
|
|
27
|
+
return previous
|
|
28
|
+
|
|
29
|
+
def emit_cli_stream(line: str) -> None:
|
|
30
|
+
if CLI_STREAM_CALLBACK:
|
|
31
|
+
CLI_STREAM_CALLBACK(line)
|
|
32
|
+
else:
|
|
33
|
+
print(line, end="", flush=True)
|
|
34
|
+
|
|
21
35
|
@dataclass(frozen=True)
|
|
22
36
|
class CliProcessResult:
|
|
23
37
|
returncode: int
|
|
@@ -54,14 +68,14 @@ def run_cli_process(args: list[str], timeout: int) -> CliProcessResult:
|
|
|
54
68
|
assert process.stdout is not None
|
|
55
69
|
for line in process.stdout:
|
|
56
70
|
lines.append(line)
|
|
57
|
-
|
|
71
|
+
emit_cli_stream(line)
|
|
58
72
|
returncode = process.wait(timeout=timeout)
|
|
59
73
|
except subprocess.TimeoutExpired:
|
|
60
74
|
process.kill()
|
|
61
75
|
returncode = process.wait()
|
|
62
76
|
lines.append(f"\ncommand timed out after {timeout}s\n")
|
|
63
77
|
finally:
|
|
64
|
-
|
|
78
|
+
emit_cli_stream("__agentram_stream_clear__")
|
|
65
79
|
return CliProcessResult(returncode, "".join(lines), "")
|
|
66
80
|
|
|
67
81
|
def add_codex_output_last_message(args: list[str], output_path: str) -> list[str]:
|
package/package.json
CHANGED