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 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
- 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)
674
691
  panel.styles.display = "block"
675
- self.chat.write("agentram > approval needed: click Yes / Yes, do this project / No")
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.project_main_agent_approved = True
692
- 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")
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
- self.project_main_agent_approved = True
784
- 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")
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: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
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
- 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]:
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.47",
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.47"
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"