agentram 0.1.47 → 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 +102 -9
- package/agentram/orchestration.py +17 -3
- 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
|
|
|
@@ -593,6 +603,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
593
603
|
self.context.profile_store.set_agent(slot, endpoint)
|
|
594
604
|
set_global_agent_if_available(self.context, slot, endpoint)
|
|
595
605
|
if saved_env_name:
|
|
606
|
+
save_profile_env_if_available(self.context, saved_env_name)
|
|
596
607
|
save_global_env_if_available(self.context, saved_env_name)
|
|
597
608
|
if saved_env_name:
|
|
598
609
|
return f"{slot}={provider}:{model} key=global-jsonl:{saved_env_name}"
|
|
@@ -668,11 +679,17 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
668
679
|
self.hide_setup()
|
|
669
680
|
self.query_one("#input", Input).focus()
|
|
670
681
|
|
|
671
|
-
def show_approval_popup(self, prompt: str) -> None:
|
|
682
|
+
def show_approval_popup(self, prompt: str, kind: str = "main") -> None:
|
|
672
683
|
panel = self.query_one("#approval", Vertical)
|
|
673
|
-
|
|
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)
|
|
674
691
|
panel.styles.display = "block"
|
|
675
|
-
self.chat.write(
|
|
692
|
+
self.chat.write(note)
|
|
676
693
|
|
|
677
694
|
def hide_approval_popup(self) -> None:
|
|
678
695
|
try:
|
|
@@ -688,8 +705,13 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
688
705
|
self.pending_approval_prompt = None
|
|
689
706
|
self.hide_approval_popup()
|
|
690
707
|
if decision == "yes_project":
|
|
691
|
-
self.
|
|
692
|
-
|
|
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")
|
|
693
715
|
self.enqueue_prompt(approved)
|
|
694
716
|
return
|
|
695
717
|
if decision == "yes_once":
|
|
@@ -778,10 +800,16 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
778
800
|
return
|
|
779
801
|
if decision == "yes_project":
|
|
780
802
|
approved = self.pending_approval_prompt
|
|
803
|
+
kind = self.pending_approval_kind
|
|
781
804
|
self.pending_approval_prompt = None
|
|
782
805
|
self.hide_approval_popup()
|
|
783
|
-
|
|
784
|
-
|
|
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")
|
|
785
813
|
self.enqueue_prompt(approved)
|
|
786
814
|
return
|
|
787
815
|
if decision == "no":
|
|
@@ -799,8 +827,14 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
799
827
|
self.chat.clear()
|
|
800
828
|
self.chat.write("system > Chat cleared.")
|
|
801
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
|
|
802
835
|
if requires_main_agent_approval(self.context, text) and not self.project_main_agent_approved:
|
|
803
836
|
self.pending_approval_prompt = text
|
|
837
|
+
self.pending_approval_kind = "main"
|
|
804
838
|
self.show_approval_popup(text)
|
|
805
839
|
return
|
|
806
840
|
self.enqueue_prompt(text)
|
|
@@ -823,10 +857,23 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
823
857
|
pending = len(self.prompt_queue)
|
|
824
858
|
suffix = f" ({pending} queued)" if pending else ""
|
|
825
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)
|
|
826
870
|
try:
|
|
827
871
|
response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
|
|
828
872
|
except Exception as error: # noqa: BLE001 - TUI boundary
|
|
829
873
|
response = f"error: {error}"
|
|
874
|
+
finally:
|
|
875
|
+
set_cli_stream_callback(previous_callback)
|
|
876
|
+
self.update_activity("")
|
|
830
877
|
if response == "__clear__":
|
|
831
878
|
self.chat.clear()
|
|
832
879
|
self.chat.write("system > Chat cleared.")
|
|
@@ -883,6 +930,13 @@ def create_prompt_session() -> Any | None:
|
|
|
883
930
|
)
|
|
884
931
|
|
|
885
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
|
+
|
|
886
940
|
def read_tui_input(prompt_session: Any | None) -> str:
|
|
887
941
|
if prompt_session is None:
|
|
888
942
|
return input("You > ")
|
|
@@ -898,6 +952,32 @@ def normalize_approval_decision(text: str) -> str:
|
|
|
898
952
|
return "no"
|
|
899
953
|
return ""
|
|
900
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
|
+
|
|
901
981
|
def requires_main_agent_approval(context: McpContext, prompt: str) -> bool:
|
|
902
982
|
main_agent = context.profile_store.list_agents().get("main")
|
|
903
983
|
if not main_agent:
|
|
@@ -1230,6 +1310,16 @@ def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
|
|
|
1230
1310
|
return f"Bound model: {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')}"
|
|
1231
1311
|
|
|
1232
1312
|
|
|
1313
|
+
def save_profile_env_if_available(context: McpContext, env_name: str) -> bool:
|
|
1314
|
+
if not env_name:
|
|
1315
|
+
return False
|
|
1316
|
+
value = os.getenv(env_name, "")
|
|
1317
|
+
if not value:
|
|
1318
|
+
return False
|
|
1319
|
+
context.profile_store.set_env_value(env_name, value)
|
|
1320
|
+
return True
|
|
1321
|
+
|
|
1322
|
+
|
|
1233
1323
|
def save_global_env_if_available(context: McpContext, env_name: str) -> bool:
|
|
1234
1324
|
if not env_name or not global_agent_profile_enabled():
|
|
1235
1325
|
return False
|
|
@@ -1743,7 +1833,7 @@ def model_error_hint(message: str) -> str:
|
|
|
1743
1833
|
if "command not found" in lower or "winerror 2" in lower or "the system cannot find the file" in lower:
|
|
1744
1834
|
return message + r" | Fix: install the CLI or set Main base_url to full command path. For Codex use base_url='C:\Users\admin\AppData\Roaming\npm\codex.cmd exec' or paste codex.cmd path; AgentRAM auto-adds exec when missing."
|
|
1745
1835
|
if "missing api key env" in lower:
|
|
1746
|
-
return message + " | Fix:
|
|
1836
|
+
return message + " | Fix: run AgentRAM setup again and paste raw key; AgentRAM stores it in agent_profile.jsonl, not .env."
|
|
1747
1837
|
return message
|
|
1748
1838
|
|
|
1749
1839
|
def claude_template_root() -> Path:
|
|
@@ -1854,6 +1944,9 @@ def sidebar_lines(context: McpContext) -> list[str]:
|
|
|
1854
1944
|
"TASK",
|
|
1855
1945
|
f" {trim(goal.current_task or '-', 24)}",
|
|
1856
1946
|
"",
|
|
1947
|
+
"SHELL",
|
|
1948
|
+
f" bash: {'yes' if shutil.which('bash') else 'no'}",
|
|
1949
|
+
"",
|
|
1857
1950
|
"COMMANDS",
|
|
1858
1951
|
" / = command palette",
|
|
1859
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]:
|
|
@@ -152,7 +166,7 @@ def env_value(name: str) -> str:
|
|
|
152
166
|
key, raw_value = stripped.split("=", 1)
|
|
153
167
|
if key.strip() == name:
|
|
154
168
|
return raw_value.strip().strip('"').strip("'")
|
|
155
|
-
return
|
|
169
|
+
return global_profile_env_value(name)
|
|
156
170
|
|
|
157
171
|
|
|
158
172
|
@dataclass(frozen=True)
|
package/package.json
CHANGED