agentram 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -998,3 +998,43 @@ Supervisor creates plan -> Main agent executes -> Small agent records RAM notes
998
998
  ```
999
999
 
1000
1000
  Small agent should not own big direction. It records plans, decisions, files, risks, next tasks, and coding facts into RAM.
1001
+
1002
+ ## Textual TUI
1003
+
1004
+ Install rich terminal UI with native scrollable chat pane:
1005
+
1006
+ ```bash
1007
+ pip install agentram[tui]
1008
+ agentram tui
1009
+ ```
1010
+
1011
+ When `textual` is installed, AgentRAM uses a real full-screen TUI with mouse wheel scrolling inside the chat frame. If `textual` is missing, it falls back to the basic print-based terminal UI.
1012
+
1013
+ ## npm Python Dependencies
1014
+
1015
+ When installed through npm, AgentRAM runs a `postinstall` step that tries to install Python TUI dependencies for the Python interpreter used by `agentram`:
1016
+
1017
+ ```text
1018
+ textual>=0.80
1019
+ prompt_toolkit>=3.0
1020
+ ```
1021
+
1022
+ Set target Python before install when needed:
1023
+
1024
+ ```powershell
1025
+ $env:PYTHON="C:\Users\admin\anaconda3\python.exe"
1026
+ npm install -g agentram@latest
1027
+ ```
1028
+
1029
+ Skip automatic Python dependency install:
1030
+
1031
+ ```powershell
1032
+ $env:AGENTRAM_SKIP_PY_DEPS="1"
1033
+ npm install -g agentram@latest
1034
+ ```
1035
+
1036
+ If auto install fails, install manually:
1037
+
1038
+ ```powershell
1039
+ python -m pip install textual prompt_toolkit
1040
+ ```
package/agentram/cli.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import asyncio
4
5
  from concurrent.futures import ThreadPoolExecutor
5
6
  import json
6
7
  import os
@@ -44,10 +45,10 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
44
45
  ("/workflow", "Show supervisor workflow agents"),
45
46
  ("/route on|off|status", "Toggle auto prompt routing"),
46
47
  ("/orchestrate <task>", "Build multi-model agent plan"),
47
- ("/ask <prompt>", "Execute bound models and return outputs"),
48
- ("/claude-install [path]", "Install .claude commands and agents"),
49
- ("/note <text>", "Save decision note"),
50
- ("/clear", "Clear chat"),
48
+ ("/ask <prompt>", "Execute bound models and return outputs"),
49
+ ("/claude-install [path]", "Install .claude commands and agents"),
50
+ ("/note <text>", "Save decision note"),
51
+ ("/clear", "Clear chat"),
51
52
  ("/exit", "Quit"),
52
53
  ("Plain text", "Capture coding chat message and retrieve context"),
53
54
  ]
@@ -76,9 +77,9 @@ SLASH_HELP = """Slash commands:
76
77
  /route on|off|status Toggle auto prompt routing
77
78
  /orchestrate <task> Build multi-model agent plan
78
79
  /ask <prompt> Execute bound models and return outputs
79
- /claude-install [path] Install .claude commands and agents
80
- /note <text> Save decision note
81
- /clear Clear chat
80
+ /claude-install [path] Install .claude commands and agents
81
+ /note <text> Save decision note
82
+ /clear Clear chat
82
83
  /exit Quit
83
84
  Plain text Capture coding chat message and retrieve context
84
85
  """.strip()
@@ -237,34 +238,121 @@ def run_command(args: argparse.Namespace) -> int:
237
238
  raise SystemExit(f"unknown command: {command}")
238
239
 
239
240
 
240
- def run_tui(context: McpContext) -> None:
241
- context.init_storage()
241
+ def run_tui(context: McpContext) -> None:
242
+ if run_textual_tui(context):
243
+ return
244
+ context.init_storage()
242
245
  prompt_session = create_prompt_session()
243
246
  input_hint = "live slash autocomplete enabled" if prompt_session else "basic input fallback; install prompt_toolkit for live slash dropdown"
244
- messages = [
245
- "system > AgentRAM coding-agent CLI ready",
246
- f"system > RAM root: {context.ram_root}",
247
- f"system > {input_hint}",
248
- ]
249
- while True:
250
- draw_screen(context, messages)
247
+ messages = [
248
+ "system > AgentRAM coding-agent CLI ready",
249
+ f"system > RAM root: {context.ram_root}",
250
+ f"system > {input_hint}",
251
+ ]
252
+ while True:
253
+ draw_screen(context, messages)
251
254
  try:
252
255
  user_input = read_tui_input(prompt_session).strip()
253
256
  except (EOFError, KeyboardInterrupt):
254
257
  return
255
- if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
256
- return
257
- if user_input in BUTTONS:
258
- user_input = BUTTONS[user_input]
259
- if user_input:
260
- messages.append(f"user > {user_input}")
258
+ if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
259
+ return
260
+ if user_input in BUTTONS:
261
+ user_input = BUTTONS[user_input]
262
+ if user_input:
263
+ messages.append(f"user > {user_input}")
261
264
  response = run_with_activity(context, messages, user_input)
262
265
  if response == "__clear__":
263
266
  messages.clear()
264
267
  messages.append("system > Chat cleared.")
265
268
  elif response:
266
269
  messages.append("agentram > " + response)
267
- del messages[:-10]
270
+ del messages[:-200]
271
+
272
+ def run_textual_tui(context: McpContext) -> bool:
273
+ try:
274
+ from textual.app import App, ComposeResult
275
+ from textual.containers import Horizontal, Vertical
276
+ from textual.widgets import Footer, Header, Input, RichLog, Static
277
+ except ImportError:
278
+ return False
279
+
280
+ class AgentRAMTextualApp(App[None]):
281
+ CSS = """
282
+ Screen { background: #0b0f14; color: #d7dde8; }
283
+ #layout { height: 1fr; }
284
+ #sidebar { width: 32; border: solid #263241; padding: 1; }
285
+ #main { width: 1fr; }
286
+ #chat { height: 1fr; border: solid #263241; padding: 1; overflow-y: scroll; }
287
+ #input { dock: bottom; border: solid #3b82f6; }
288
+ .muted { color: #8b9bb0; }
289
+ """
290
+ BINDINGS = [("ctrl+c", "quit", "Quit")]
291
+
292
+ def __init__(self, mcp_context: McpContext) -> None:
293
+ super().__init__()
294
+ self.context = mcp_context
295
+ self.messages: list[str] = []
296
+ self.busy = False
297
+
298
+ def compose(self) -> ComposeResult:
299
+ yield Header(show_clock=True)
300
+ with Horizontal(id="layout"):
301
+ yield Static(id="sidebar")
302
+ with Vertical(id="main"):
303
+ yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
304
+ yield Input(placeholder="Prompt hoặc /command. Mouse wheel scroll trong khung chat.", id="input")
305
+ yield Footer()
306
+
307
+ def on_mount(self) -> None:
308
+ self.context.init_storage()
309
+ self.chat.write("system > AgentRAM Textual TUI ready")
310
+ self.chat.write(f"system > RAM root: {self.context.ram_root}")
311
+ self.refresh_sidebar()
312
+ self.set_interval(1.0, self.refresh_sidebar)
313
+
314
+ @property
315
+ def chat(self) -> RichLog:
316
+ return self.query_one("#chat", RichLog)
317
+
318
+ def refresh_sidebar(self) -> None:
319
+ self.query_one("#sidebar", Static).update("\n".join(sidebar_lines(self.context)))
320
+
321
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
322
+ text = event.value.strip()
323
+ event.input.value = ""
324
+ if not text:
325
+ return
326
+ if text in {"0", "q", "quit", "exit", "/exit", "/quit"}:
327
+ self.exit()
328
+ return
329
+ if text in BUTTONS:
330
+ text = BUTTONS[text]
331
+ self.chat.write(f"user > {text}")
332
+ if text == "/clear":
333
+ self.chat.clear()
334
+ self.chat.write("system > Chat cleared.")
335
+ return
336
+ if self.busy:
337
+ self.chat.write("agentram > busy, wait current task")
338
+ return
339
+ self.busy = True
340
+ self.chat.write("agentram > thinking...")
341
+ try:
342
+ response = await asyncio.to_thread(handle_chat_input, self.context, text, self.messages)
343
+ except Exception as error: # noqa: BLE001 - TUI boundary
344
+ response = f"error: {error}"
345
+ finally:
346
+ self.busy = False
347
+ if response == "__clear__":
348
+ self.chat.clear()
349
+ self.chat.write("system > Chat cleared.")
350
+ elif response:
351
+ self.chat.write("agentram > " + response)
352
+ self.refresh_sidebar()
353
+
354
+ AgentRAMTextualApp(context).run()
355
+ return True
268
356
 
269
357
  def run_with_activity(context: McpContext, messages: list[str], user_input: str) -> str:
270
358
  with ThreadPoolExecutor(max_workers=1) as executor:
@@ -663,12 +751,7 @@ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
663
751
  outputs[slot] = f"error: {model_error_hint(str(error))}"
664
752
  if not main_agent:
665
753
  outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
666
- if small_agent and outputs.get("small") and not outputs["small"].startswith("error:"):
667
- call_tool(
668
- context,
669
- "agentram_emit_event",
670
- {"type": "DECISION", "payload": {"content": outputs["small"]}, "task_id": "codex", "ingest": True},
671
- )
754
+ persist_workflow_memory(context, prompt, plan, outputs)
672
755
  return "\n".join(
673
756
  [
674
757
  "Workflow: supervisor -> main + small -> merge",
@@ -683,6 +766,28 @@ def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
683
766
  ]
684
767
  )
685
768
 
769
+ def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
770
+ small_notes = outputs.get("small", "").strip()
771
+ main_result = outputs.get("main", "").strip()
772
+ note_parts = [
773
+ f"User request: {prompt}",
774
+ f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
775
+ ]
776
+ if small_notes and not small_notes.startswith("error:"):
777
+ note_parts.append(f"Small memory notes: {trim(small_notes, 1200)}")
778
+ else:
779
+ note_parts.append("Small memory notes: unavailable; persisted supervisor plan as fallback RAM note.")
780
+ if main_result and not main_result.startswith("error:"):
781
+ note_parts.append(f"Main result summary: {trim(main_result, 600)}")
782
+ content = "\n".join(note_parts).strip()
783
+ if not content:
784
+ return
785
+ call_tool(
786
+ context,
787
+ "agentram_emit_event",
788
+ {"type": "DECISION", "payload": {"content": content}, "task_id": "codex", "ingest": True},
789
+ )
790
+
686
791
  def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str) -> str:
687
792
  if not supervisor:
688
793
  return "No supervisor bound. Fallback plan: handle user request, run main coding agent, ask small agent to update RAM notes."
@@ -908,7 +1013,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
908
1013
  print(box_text("◆ AgentRAM Code | RAM + Claude subagents + slash commands", width))
909
1014
  print("+" + "-" * (width - 2) + "+")
910
1015
 
911
- sidebar = sidebar_lines(context)
1016
+ sidebar = sidebar_lines(context)
912
1017
  display_messages = [*messages, activity] if activity else messages
913
1018
  chat = wrap_messages(display_messages, right_width - 4)[-22:]
914
1019
  rows = max(len(sidebar), len(chat), 22)
@@ -917,7 +1022,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
917
1022
  right = chat[index] if index < len(chat) else ""
918
1023
  print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
919
1024
  print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
920
- footer = "Thinking/acting..." if activity else "Input: type / for live dropdown. Enter '/' for full palette. /ask calls models."
1025
+ footer = "Thinking/acting..." if activity else "Input: type / for commands. Use mouse wheel or terminal scrollbar for previous output."
921
1026
  print(box_text(footer, width))
922
1027
  print("+" + "-" * (width - 2) + "+")
923
1028
 
@@ -957,7 +1062,8 @@ def sidebar_lines(context: McpContext) -> list[str]:
957
1062
  " /ask <prompt>",
958
1063
  ]
959
1064
 
960
-
1065
+
1066
+
961
1067
  def trim(text: str, limit: int) -> str:
962
1068
  return text if len(text) <= limit else text[: limit - 1] + "…"
963
1069
 
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ const { spawnSync } = require("node:child_process");
3
+
4
+ if (process.env.AGENTRAM_SKIP_PY_DEPS === "1") {
5
+ console.log("AgentRAM: skip Python TUI deps because AGENTRAM_SKIP_PY_DEPS=1");
6
+ process.exit(0);
7
+ }
8
+
9
+ const candidates = process.env.PYTHON ? [process.env.PYTHON] : ["python", ...(process.platform === "win32" ? ["py"] : [])];
10
+ const deps = ["textual>=0.80", "prompt_toolkit>=3.0"];
11
+
12
+ function runPython(command, args) {
13
+ const finalArgs = command === "py" ? ["-3", ...args] : args;
14
+ return spawnSync(command, finalArgs, { stdio: "inherit" });
15
+ }
16
+
17
+ let lastError = null;
18
+ for (const command of candidates) {
19
+ const probe = runPython(command, ["-c", "import sys; print(sys.executable)"]);
20
+ if (probe.error || probe.status !== 0) {
21
+ lastError = probe.error || new Error(`${command} exited ${probe.status}`);
22
+ continue;
23
+ }
24
+ console.log(`AgentRAM: installing Python TUI deps with ${command}`);
25
+ const install = runPython(command, ["-m", "pip", "install", ...deps]);
26
+ if (!install.error && install.status === 0) {
27
+ process.exit(0);
28
+ }
29
+ lastError = install.error || new Error(`${command} pip install exited ${install.status}`);
30
+ }
31
+
32
+ console.warn("AgentRAM: Python TUI deps were not installed automatically.");
33
+ console.warn("AgentRAM: run `python -m pip install textual prompt_toolkit` manually, or set PYTHON to the target interpreter.");
34
+ if (lastError) console.warn(`AgentRAM: ${lastError.message}`);
35
+ process.exit(process.env.AGENTRAM_STRICT_POSTINSTALL === "1" ? 1 : 0);
package/package.json CHANGED
@@ -1,51 +1,52 @@
1
- {
2
- "name": "agentram",
3
- "version": "0.1.9",
4
- "description": "Async, model-agnostic Working RAM for coding agents.",
5
- "license": "MIT",
6
- "homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/trancongnghia/AGENT_RAM.git"
10
- },
11
- "bugs": {
12
- "url": "https://github.com/trancongnghia/AGENT_RAM/issues"
13
- },
14
- "keywords": [
15
- "agent",
16
- "memory",
17
- "codex",
18
- "claude",
19
- "mcp",
20
- "llm"
21
- ],
22
- "bin": {
23
- "agentram": "bin/agentram.js",
24
- "agentram-mcp": "bin/agentram-mcp.js"
25
- },
26
- "scripts": {
27
- "agentram": "node bin/agentram.js",
28
- "agentram:mcp": "node bin/agentram-mcp.js",
29
- "test": "python -m pytest -q",
30
- "pack:dry-run": "npm pack --dry-run",
31
- "prepublishOnly": "npm test"
32
- },
33
- "files": [
34
- "agentram/*.py",
35
- "bin/*.js",
36
- ".claude/agents/*.md",
37
- ".claude/commands/*.md",
38
- "agentram_mcp_server.py",
39
- "agentram_daemon.py",
40
- "codex_ram.py",
41
- "pyproject.toml",
42
- "README.md",
43
- "LICENSE"
44
- ],
45
- "engines": {
46
- "node": ">=16"
47
- },
48
- "publishConfig": {
49
- "access": "public"
50
- }
51
- }
1
+ {
2
+ "name": "agentram",
3
+ "version": "0.1.11",
4
+ "description": "Async, model-agnostic Working RAM for coding agents.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/trancongnghia/AGENT_RAM#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/trancongnghia/AGENT_RAM.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/trancongnghia/AGENT_RAM/issues"
13
+ },
14
+ "keywords": [
15
+ "agent",
16
+ "memory",
17
+ "codex",
18
+ "claude",
19
+ "mcp",
20
+ "llm"
21
+ ],
22
+ "bin": {
23
+ "agentram": "bin/agentram.js",
24
+ "agentram-mcp": "bin/agentram-mcp.js"
25
+ },
26
+ "scripts": {
27
+ "agentram": "node bin/agentram.js",
28
+ "agentram:mcp": "node bin/agentram-mcp.js",
29
+ "test": "python -m pytest -q",
30
+ "pack:dry-run": "npm pack --dry-run",
31
+ "prepublishOnly": "npm test",
32
+ "postinstall": "node bin/agentram-postinstall.js"
33
+ },
34
+ "files": [
35
+ "agentram/*.py",
36
+ "bin/*.js",
37
+ ".claude/agents/*.md",
38
+ ".claude/commands/*.md",
39
+ "agentram_mcp_server.py",
40
+ "agentram_daemon.py",
41
+ "codex_ram.py",
42
+ "pyproject.toml",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "engines": {
47
+ "node": ">=16"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
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.9"
7
+ version = "0.1.11"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -22,7 +22,7 @@ requires-python = ">=3.10"
22
22
  dependencies = []
23
23
 
24
24
  [project.optional-dependencies]
25
- tui = ["prompt_toolkit>=3.0"]
25
+ tui = ["prompt_toolkit>=3.0", "textual>=0.80"]
26
26
 
27
27
  [project.urls]
28
28
  Homepage = "https://github.com/your-org/agentram"