agentram 0.1.2 → 0.1.5

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
@@ -53,9 +53,10 @@ If using the Claude plugin, `.mcp.json` sets `AGENTRAM_AUTO_INIT=true`, so these
53
53
 
54
54
  ## Terminal UI CLI
55
55
 
56
- AgentRAM also includes a dependency-free terminal UI with a Claude Code-like chat frame and button menu:
56
+ AgentRAM includes a terminal UI with Claude-like slash autocomplete. It uses `prompt_toolkit` when installed and falls back to plain `input()` otherwise:
57
57
 
58
58
  ```bash
59
+ pip install -e ".[tui]"
59
60
  agentram
60
61
  ```
61
62
 
@@ -170,7 +171,7 @@ Run TUI:
170
171
  agentram tui
171
172
  ```
172
173
 
173
- Supported TUI slash commands:
174
+ Supported TUI slash commands. Plain text only records/retrieves RAM; use `/ask` to call bound models:
174
175
 
175
176
  ```text
176
177
  /help
@@ -385,12 +386,13 @@ agentram_build_coding_orchestration
385
386
  agentram_run_coding_orchestration
386
387
  ```
387
388
 
388
- TUI slash commands:
389
+ TUI slash commands. Plain text only records/retrieves RAM; use `/ask` to call bound models:
389
390
 
390
391
  ```text
391
392
  /bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY
392
393
  /models
393
394
  /orchestrate review CLI orchestration
395
+ /ask xin chào
394
396
  ```
395
397
 
396
398
  Storage:
@@ -844,3 +846,39 @@ Use `.env.example` as the shareable template.
844
846
  - Model-based ingestion is best run through daemon/wrapper configuration.
845
847
  - Wrapper CLI records command-level events, not deep internal agent tool events.
846
848
  - Rich file/tool hooks need direct agent hooks or disciplined MCP calls.
849
+
850
+
851
+ ### Why TUI May Not Respond Like A Model
852
+
853
+ Plain text in `agentram tui` records a user message and retrieves RAM context. It does not call paid/local models by default. To get a model response:
854
+
855
+ 1. Put the real API key in an environment variable.
856
+ 2. Bind the model using the env var name, not the raw key.
857
+ 3. Use `/ask` or `agentram orchestrate --execute`.
858
+
859
+ Example:
860
+
861
+ ```bash
862
+ set OPENAI_API_KEY=sk-your-key
863
+ agentram tui
864
+ ```
865
+
866
+ Inside TUI:
867
+
868
+ ```text
869
+ /bind-model openai-compatible Agentic --role coder --base-url http://localhost:8000/v1 --api-key-env OPENAI_API_KEY
870
+ /ask xin chào
871
+ ```
872
+
873
+ Do not paste raw API keys into `/bind-model`. `api_key_env` means environment variable name, e.g. `OPENAI_API_KEY`.
874
+
875
+
876
+ ### Optional Rich TUI
877
+
878
+ For live slash-command dropdown and command metadata, install the optional TUI extra:
879
+
880
+ ```bash
881
+ pip install agentram[tui]
882
+ ```
883
+
884
+ Without `prompt_toolkit`, AgentRAM still works with basic terminal input; `/` then Enter opens the full slash palette.
package/agentram/cli.py CHANGED
@@ -18,6 +18,30 @@ SUBAGENTS: dict[str, str] = {
18
18
  "documenter": "Update README or usage notes from current implementation. Keep docs concise and runnable.",
19
19
  }
20
20
 
21
+ SLASH_COMMANDS: list[tuple[str, str]] = [
22
+ ("/help", "Show commands"),
23
+ ("/init", "Create .agentram files"),
24
+ ("/status", "Show RAM status"),
25
+ ("/retrieve <query>", "Print relevant RAM context"),
26
+ ("/goal", "Show Goal Stack"),
27
+ ("/set-goal key=value ...", "Update mission/current_goal/current_task/etc"),
28
+ ("/drift <task>", "Check goal drift"),
29
+ ("/events", "Show latest events"),
30
+ ("/replay", "Replay events into memory"),
31
+ ("/agents", "List Claude-style subagent presets"),
32
+ ("/agent <name> <task>", "Build Claude Code subagent prompt"),
33
+ ("/models", "List bound coding models"),
34
+ ("/bind-model provider model", "Bind model endpoint"),
35
+ ("/orchestrate <task>", "Build multi-model agent plan"),
36
+ ("/ask <prompt>", "Execute bound models and return outputs"),
37
+ ("/claude-install [path]", "Install .claude commands and agents"),
38
+ ("/note <text>", "Save decision note"),
39
+ ("/clear", "Clear chat"),
40
+ ("/exit", "Quit"),
41
+ ("Plain text", "Capture coding chat message and retrieve context"),
42
+ ]
43
+
44
+
21
45
  SLASH_HELP = """Slash commands:
22
46
  /help Show commands
23
47
  /init Create .agentram files
@@ -33,6 +57,7 @@ SLASH_HELP = """Slash commands:
33
57
  /models List bound coding models
34
58
  /bind-model provider model Bind model endpoint
35
59
  /orchestrate <task> Build multi-model agent plan
60
+ /ask <prompt> Execute bound models and return outputs
36
61
  /claude-install [path] Install .claude commands and agents
37
62
  /note <text> Save decision note
38
63
  /clear Clear chat
@@ -150,10 +175,19 @@ def run_command(args: argparse.Namespace) -> int:
150
175
 
151
176
  def run_tui(context: McpContext) -> None:
152
177
  context.init_storage()
153
- messages = ["system > AgentRAM coding-agent CLI ready", f"system > RAM root: {context.ram_root}", "system > Type /help, /agent reviewer <task>, or plain coding request."]
178
+ prompt_session = create_prompt_session()
179
+ input_hint = "live slash autocomplete enabled" if prompt_session else "basic input fallback; install prompt_toolkit for live slash dropdown"
180
+ messages = [
181
+ "system > AgentRAM coding-agent CLI ready",
182
+ f"system > RAM root: {context.ram_root}",
183
+ f"system > {input_hint}",
184
+ ]
154
185
  while True:
155
186
  draw_screen(context, messages)
156
- user_input = input("You > ").strip()
187
+ try:
188
+ user_input = read_tui_input(prompt_session).strip()
189
+ except (EOFError, KeyboardInterrupt):
190
+ return
157
191
  if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
158
192
  return
159
193
  if user_input in BUTTONS:
@@ -169,9 +203,46 @@ def run_tui(context: McpContext) -> None:
169
203
  del messages[:-10]
170
204
 
171
205
 
206
+
207
+ def slash_command_names() -> list[str]:
208
+ return [command.split()[0] for command, _ in SLASH_COMMANDS if command.startswith("/")]
209
+
210
+
211
+ def slash_command_meta() -> dict[str, str]:
212
+ return {command.split()[0]: description for command, description in SLASH_COMMANDS if command.startswith("/")}
213
+
214
+
215
+ def create_prompt_session() -> Any | None:
216
+ try:
217
+ from prompt_toolkit import PromptSession
218
+ from prompt_toolkit.completion import WordCompleter
219
+ except ImportError:
220
+ return None
221
+
222
+ completer = WordCompleter(
223
+ slash_command_names(),
224
+ meta_dict=slash_command_meta(),
225
+ ignore_case=True,
226
+ sentence=True,
227
+ )
228
+ return PromptSession(
229
+ completer=completer,
230
+ complete_while_typing=True,
231
+ reserve_space_for_menu=12,
232
+ bottom_toolbar="Type / for slash commands · /agents for subagents · /ask to call bound models",
233
+ )
234
+
235
+
236
+ def read_tui_input(prompt_session: Any | None) -> str:
237
+ if prompt_session is None:
238
+ return input("You > ")
239
+ return prompt_session.prompt("You > ")
240
+
172
241
  def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
173
242
  if not text:
174
243
  return SLASH_HELP
244
+ if text == "/":
245
+ return slash_palette_text()
175
246
  if text.startswith("/"):
176
247
  return handle_slash_command(context, text, messages)
177
248
  call_tool(
@@ -213,15 +284,14 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
213
284
  return models_text(context)
214
285
  if command == "/bind-model":
215
286
  if len(rest) < 2:
216
- return "Usage: /bind-model <provider> <model> [role=coder] [base_url=...] [api_key_env=...] [modalities=text,image]"
217
- options = {"provider": rest[0], "model": rest[1]}
218
- for item in rest[2:]:
219
- if "=" in item:
220
- key, value = item.split("=", 1)
221
- options[key.replace("-", "_")] = value
222
- return bind_model_text(context, options)
287
+ return "Usage: /bind-model <provider> <model> [--role coder] [--base-url URL] [--api-key-env ENV_NAME]"
288
+ return bind_model_text(context, parse_bind_model_args(rest))
223
289
  if command == "/orchestrate":
224
- return orchestrate_text(context, raw_rest, execute=False)
290
+ execute = "--execute" in rest
291
+ task = raw_rest.replace("--execute", "").strip()
292
+ return orchestrate_text(context, task, execute=execute)
293
+ if command == "/ask":
294
+ return orchestrate_text(context, raw_rest, execute=True)
225
295
  if command == "/agent":
226
296
  if not rest:
227
297
  return "Usage: /agent <memory|planner|reviewer|documenter> <task>"
@@ -313,8 +383,34 @@ def replay_text(context: McpContext, arguments: dict[str, object]) -> str:
313
383
 
314
384
 
315
385
  def agents_text() -> str:
316
- lines = ["Subagent presets:"]
317
- lines.extend(f"- {name}: {description}" for name, description in SUBAGENTS.items())
386
+ rows = [
387
+ ("agentram-memory", "Memory", "Records durable notes. Never edits code."),
388
+ ("agentram-planner", "Planner", "Checks Goal Stack and decomposes work."),
389
+ ("agentram-reviewer", "Reviewer", "Reviews changes, tests, and drift risk."),
390
+ ("agentram-docs", "Docs", "Updates README and usage docs."),
391
+ ]
392
+ lines = ["Claude Code subagents", ""]
393
+ for name, label, description in rows:
394
+ lines.append(f" {name.ljust(22)} {label}")
395
+ lines.append(f" {description}")
396
+ lines.append("")
397
+ lines.append("Use: /agent reviewer <task>")
398
+ lines.append("Install: /claude-install .")
399
+ return "\n".join(lines)
400
+
401
+
402
+ def slash_palette_text(filter_text: str = "") -> str:
403
+ normalized = filter_text.strip().lower().lstrip("/")
404
+ rows = []
405
+ for command, description in SLASH_COMMANDS:
406
+ command_key = command.lower().lstrip("/")
407
+ if normalized and normalized not in command_key:
408
+ continue
409
+ rows.append((command, description))
410
+ command_width = max([len(command) for command, _ in rows] + [12])
411
+ lines = ["Slash commands", ""]
412
+ for command, description in rows:
413
+ lines.append(f" {command.ljust(command_width)} {description}")
318
414
  return "\n".join(lines)
319
415
 
320
416
 
@@ -350,7 +446,32 @@ def models_text(context: McpContext) -> str:
350
446
  return "\n".join(lines)
351
447
 
352
448
 
449
+ def parse_bind_model_args(parts: list[str]) -> dict[str, object]:
450
+ options: dict[str, object] = {"provider": parts[0], "model": parts[1]}
451
+ index = 2
452
+ while index < len(parts):
453
+ item = parts[index]
454
+ if item.startswith("--"):
455
+ key = item[2:].replace("-", "_")
456
+ if index + 1 < len(parts) and not parts[index + 1].startswith("--"):
457
+ options[key] = parts[index + 1]
458
+ index += 2
459
+ else:
460
+ options[key] = True
461
+ index += 1
462
+ elif "=" in item:
463
+ key, value = item.split("=", 1)
464
+ options[key.replace("-", "_")] = value
465
+ index += 1
466
+ else:
467
+ index += 1
468
+ return options
469
+
470
+
353
471
  def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
472
+ api_key_env = str(options.get("api_key_env", ""))
473
+ if api_key_env.startswith(("sk-", "gsk_", "sk_")):
474
+ return "Do not paste raw API key into api_key_env. Set env var first, e.g. OPENAI_API_KEY=sk-..., then use api_key_env=OPENAI_API_KEY."
354
475
  modalities_value = str(options.get("modalities", "text"))
355
476
  arguments = {
356
477
  "id": str(options.get("id", "")) or None,
@@ -459,7 +580,7 @@ def draw_screen(context: McpContext, messages: list[str]) -> None:
459
580
  left_width = 30
460
581
  right_width = width - left_width - 3
461
582
  print("+" + "-" * (width - 2) + "+")
462
- print(box_text("AgentRAM Code Agent CLI | RAM + Claude subagents + slash commands", width))
583
+ print(box_text("AgentRAM Code | RAM + Claude subagents + slash commands", width))
463
584
  print("+" + "-" * (width - 2) + "+")
464
585
 
465
586
  sidebar = sidebar_lines(context)
@@ -470,7 +591,7 @@ def draw_screen(context: McpContext, messages: list[str]) -> None:
470
591
  right = chat[index] if index < len(chat) else ""
471
592
  print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
472
593
  print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
473
- print(box_text("Input: plain coding request, /help, /agent reviewer <task>, /claude-install .", width))
594
+ print(box_text("Input: type / for live dropdown. Enter '/' for full palette. /ask calls models.", width))
474
595
  print("+" + "-" * (width - 2) + "+")
475
596
 
476
597
 
@@ -501,10 +622,11 @@ def sidebar_lines(context: McpContext) -> list[str]:
501
622
  " 9 help 0 exit",
502
623
  "",
503
624
  "CLAUDE",
625
+ " / = command palette",
626
+ " /agents",
504
627
  " /claude-install .",
505
628
  " /models",
506
- " /bind-model claude <model>",
507
- " /orchestrate <task>",
629
+ " /ask <prompt>",
508
630
  ]
509
631
 
510
632
 
@@ -515,7 +637,7 @@ def trim(text: str, limit: int) -> str:
515
637
  def menu_lines() -> list[str]:
516
638
  return [
517
639
  "1=/init 2=/status 3=/retrieve 4=/goal 5=/set-goal 6=/drift 7=/events 8=/replay 9=/help",
518
- "Plain text captures coding request. Claude: /claude-install ., /agents, /agent reviewer <task>",
640
+ "Plain text saves RAM only. Use /ask <prompt> to call bound models.",
519
641
  ]
520
642
 
521
643
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
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.2"
7
+ version = "0.1.5"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -21,6 +21,9 @@ classifiers = [
21
21
  requires-python = ">=3.10"
22
22
  dependencies = []
23
23
 
24
+ [project.optional-dependencies]
25
+ tui = ["prompt_toolkit>=3.0"]
26
+
24
27
  [project.urls]
25
28
  Homepage = "https://github.com/your-org/agentram"
26
29
  Repository = "https://github.com/your-org/agentram"