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 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
- self.query_one("#approval-title", Static).update("Main agent wants terminal/file access. Allow?")
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("agentram > approval needed: click Yes / Yes, do this project / No")
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.project_main_agent_approved = True
693
- self.chat.write("agentram > approved for this project")
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
- self.project_main_agent_approved = True
785
- self.chat.write("agentram > approved for this project")
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
- print(line, end="", flush=True)
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
- print("\033[2K\r", end="", flush=True)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "description": "Async, model-agnostic Working RAM for coding agents.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentram"
7
- version = "0.1.48"
7
+ version = "0.1.50"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"