agentram 0.1.0 → 0.1.1

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.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: agentram-docs
3
+ description: Updates or reviews docs from AgentRAM memory and current repo state.
4
+ tools: Read, Grep, Glob, Edit, mcp__agentram__agentram_retrieve
5
+ ---
6
+
7
+ You are AgentRAM Docs Subagent.
8
+
9
+ Scope:
10
+ - Read current README/docs.
11
+ - Pull relevant AgentRAM notes.
12
+ - Update usage docs for commands, MCP, npm, and integrations.
13
+
14
+ Rules:
15
+ - Keep docs runnable.
16
+ - Prefer short examples.
17
+ - Do not expose secrets.
18
+ - Mention Python 3.10+ requirement for npm wrapper.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: agentram-memory
3
+ description: Records decisions, files, task state, and risks into AgentRAM. Use after major coding actions or before context compaction.
4
+ tools: Read, Grep, Glob, mcp__agentram__agentram_emit_event, mcp__agentram__agentram_retrieve
5
+ ---
6
+
7
+ You are AgentRAM Memory Subagent.
8
+
9
+ Scope:
10
+ - Observe coding work.
11
+ - Extract durable notes: decisions, file changes, task state, open questions, risks.
12
+ - Write notes through `agentram_emit_event`.
13
+ - Do not edit code.
14
+
15
+ Rules:
16
+ - Use only facts visible in repo, tool results, or conversation.
17
+ - Keep notes short.
18
+ - Include file paths when known.
19
+ - Prefer `ingest: true` for important decisions.
20
+ - Prefer `ingest: false` for noisy chat/tool events.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: agentram-planner
3
+ description: Builds task plans using AgentRAM Goal Stack and checks drift before scope changes.
4
+ tools: Read, Grep, Glob, mcp__agentram__agentram_retrieve, mcp__agentram__agentram_check_goal_drift, mcp__agentram__agentram_update_goal_state
5
+ ---
6
+
7
+ You are AgentRAM Planner Subagent.
8
+
9
+ Scope:
10
+ - Read Goal Stack first.
11
+ - Check whether requested task aligns with mission/current goal/current task.
12
+ - Produce small ordered implementation plan.
13
+ - Update Goal Stack only when user intent is clear.
14
+
15
+ Rules:
16
+ - No code edits.
17
+ - No broad refactors unless current goal requires them.
18
+ - If drift risk appears, ask for confirmation.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: agentram-reviewer
3
+ description: Reviews changed code against AgentRAM context, tests, and goal drift.
4
+ tools: Read, Grep, Glob, mcp__agentram__agentram_retrieve, mcp__agentram__agentram_check_goal_drift
5
+ ---
6
+
7
+ You are AgentRAM Reviewer Subagent.
8
+
9
+ Scope:
10
+ - Review current changes.
11
+ - Compare implementation to Goal Stack and RAM notes.
12
+ - Find correctness gaps, missing tests, risky assumptions, and docs drift.
13
+
14
+ Rules:
15
+ - Return concise findings.
16
+ - Cite file paths.
17
+ - Separate blockers from non-blockers.
18
+ - Do not edit code.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Check whether current task drifts from AgentRAM Goal Stack.
3
+ argument-hint: [task]
4
+ ---
5
+
6
+ Check AgentRAM goal drift.
7
+
8
+ 1. Call MCP tool `agentram_check_goal_drift` with `task`: `$ARGUMENTS`.
9
+ 2. If `status` is `ok`, continue.
10
+ 3. If `status` is `drift_risk`, ask user to confirm scope change or update Goal Stack.
11
+ 4. Do not suggest extra next actions when AgentRAM returns `next_actions: []`.
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: Save a decision or important fact into AgentRAM.
3
+ argument-hint: [note]
4
+ ---
5
+
6
+ Save this note to AgentRAM.
7
+
8
+ Call MCP tool `agentram_emit_event` with:
9
+
10
+ ```json
11
+ {
12
+ "type": "DECISION",
13
+ "payload": {"content": "$ARGUMENTS"},
14
+ "task_id": "codex",
15
+ "ingest": true
16
+ }
17
+ ```
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Show AgentRAM status and Goal Stack.
3
+ ---
4
+
5
+ Show AgentRAM project memory status.
6
+
7
+ 1. Call `agentram_init` if files do not exist.
8
+ 2. Call `agentram_read_goal_state`.
9
+ 3. Call `agentram_read_task_state` with `task_id`: `codex`.
10
+ 4. Summarize current goal, current task, and recent memory notes.
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: Retrieve AgentRAM context before coding.
3
+ argument-hint: [query]
4
+ ---
5
+
6
+ Use AgentRAM before coding.
7
+
8
+ 1. Call MCP tool `agentram_retrieve` with:
9
+ - `query`: `$ARGUMENTS`
10
+ - `task_id`: `codex`
11
+ - `limit`: `8`
12
+ 2. Read Goal Stack first.
13
+ 3. Use returned RAM notes as extra context.
14
+ 4. If no notes exist, continue normally and emit new events after important actions.
package/README.md CHANGED
@@ -65,6 +65,12 @@ Equivalent explicit command:
65
65
  agentram tui
66
66
  ```
67
67
 
68
+ Install Claude Code commands and subagents into the current project:
69
+
70
+ ```bash
71
+ agentram claude-install --target .
72
+ ```
73
+
68
74
  Buttons inside the UI:
69
75
 
70
76
  ```text
@@ -153,6 +159,66 @@ If `python` is not the right binary, set `PYTHON`:
153
159
  ```bash
154
160
  PYTHON=python3 npx agentram status
155
161
  ```
162
+
163
+ ## Coding TUI, Slash Commands, And Subagents
164
+
165
+ AgentRAM TUI now behaves like a coding-agent CLI inspired by `freecodexyz/free-code`: left status panel, chat transcript, slash command registry, RAM tool calls, and Claude subagent prompt templates.
166
+
167
+ Run TUI:
168
+
169
+ ```bash
170
+ agentram tui
171
+ ```
172
+
173
+ Supported TUI slash commands:
174
+
175
+ ```text
176
+ /help
177
+ /init
178
+ /status
179
+ /retrieve <query>
180
+ /goal
181
+ /set-goal current_goal="Ship CLI" current_task="Add slash commands"
182
+ /drift <task>
183
+ /events
184
+ /replay
185
+ /agents
186
+ /agent reviewer <task>
187
+ /claude-install .
188
+ /note <decision>
189
+ /clear
190
+ /exit
191
+ ```
192
+
193
+ Claude Code repo templates are included:
194
+
195
+ ```text
196
+ .claude/commands/ram.md
197
+ .claude/commands/ram-drift.md
198
+ .claude/commands/ram-note.md
199
+ .claude/commands/ram-status.md
200
+ .claude/agents/agentram-memory.md
201
+ .claude/agents/agentram-planner.md
202
+ .claude/agents/agentram-reviewer.md
203
+ .claude/agents/agentram-docs.md
204
+ ```
205
+
206
+ Usage inside Claude Code:
207
+
208
+ ```text
209
+ /ram adapter memory writer
210
+ /ram-drift refactor MCP server
211
+ /ram-note Use npm wrapper only as Python launcher.
212
+ /ram-status
213
+ ```
214
+
215
+ Subagent intent:
216
+
217
+ - `agentram-memory`: records durable notes, no code edits.
218
+ - `agentram-planner`: checks Goal Stack and plans work.
219
+ - `agentram-reviewer`: reviews changes against RAM and goal drift.
220
+ - `agentram-docs`: updates docs from repo state and RAM notes.
221
+
156
222
  ## Install In Coding Agents
157
223
 
158
224
  ### Codex MCP
package/agentram/cli.py CHANGED
@@ -1,242 +1,459 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import os
5
- import shutil
6
- import textwrap
7
- from pathlib import Path
8
- from typing import Any
9
-
10
- from .config import default_ram_root
11
- from .mcp_server import McpContext, call_tool
12
-
13
-
14
- def build_parser() -> argparse.ArgumentParser:
15
- parser = argparse.ArgumentParser(description="AgentRAM terminal UI and CLI.")
16
- parser.add_argument("--ram-root", default=None, help="AgentRAM storage root. Defaults to .agentram in the current project directory.")
17
- subparsers = parser.add_subparsers(dest="command")
18
-
19
- subparsers.add_parser("tui", help="Open terminal chat UI.")
20
- subparsers.add_parser("init", help="Create AgentRAM files.")
21
- subparsers.add_parser("status", help="Show RAM file and item counts.")
22
- subparsers.add_parser("goal", help="Show goal stack.")
23
-
24
- retrieve = subparsers.add_parser("retrieve", help="Retrieve memory context.")
25
- retrieve.add_argument("query", nargs="?", default="", help="Search query.")
26
- retrieve.add_argument("--task-id", default="codex", help="Task id.")
27
- retrieve.add_argument("--limit", type=int, default=8, help="Max items.")
28
-
29
- drift = subparsers.add_parser("drift", help="Check goal drift for a task.")
30
- drift.add_argument("task", nargs="?", default="", help="Task to check.")
31
-
32
- replay = subparsers.add_parser("replay", help="Replay events into memory.")
33
- replay.add_argument("--task-id", default=None, help="Optional task id filter.")
34
-
35
- return parser
36
-
37
-
38
- def context_from_args(args: argparse.Namespace) -> McpContext:
39
- ram_root = Path(args.ram_root) if args.ram_root else default_ram_root()
40
- return McpContext(ram_root=ram_root)
41
-
42
-
43
- def payload(result: dict[str, Any]) -> dict[str, Any]:
44
- return dict(result.get("structuredContent", {}))
45
-
46
-
47
- def print_status(context: McpContext) -> None:
48
- context.init_storage()
49
- memory_items = context.memory_store.list_items()
50
- events = context.event_log.read_all()
51
- goal_state = context.goal_store.load()
52
- print(f"AgentRAM root: {context.ram_root}")
53
- print(f"Events: {len(events)}")
54
- print(f"Memory items: {len(memory_items)}")
55
- print(f"Mission: {goal_state.mission or '-'}")
56
- print(f"Current goal: {goal_state.current_goal or '-'}")
57
- print(f"Current task: {goal_state.current_task or '-'}")
58
-
59
-
60
- def run_command(args: argparse.Namespace) -> int:
61
- context = context_from_args(args)
62
- command = args.command or "tui"
63
-
64
- if command == "tui":
65
- run_tui(context)
66
- return 0
67
- if command == "init":
68
- data = payload(call_tool(context, "agentram_init", {}))
69
- print(f"AgentRAM root: {data['ram_root']}")
70
- created = data.get("created", [])
71
- print("Created: " + (", ".join(created) if created else "none"))
72
- return 0
73
- if command == "status":
74
- print_status(context)
75
- return 0
76
- if command == "retrieve":
77
- query = args.query or input("Query > ").strip()
78
- data = payload(call_tool(context, "agentram_retrieve", {"query": query, "task_id": args.task_id, "limit": args.limit}))
79
- print(data.get("context") or "No matching memory.")
80
- return 0
81
- if command == "goal":
82
- data = payload(call_tool(context, "agentram_read_goal_state", {}))
83
- print(data.get("context") or "No goal stack yet.")
84
- return 0
85
- if command == "drift":
86
- task = args.task or input("Task > ").strip()
87
- data = payload(call_tool(context, "agentram_check_goal_drift", {"task": task}))
88
- print(f"Status: {data.get('status')}")
89
- print(f"Aligned: {data.get('aligned')}")
90
- print(f"Reason: {data.get('reason')}")
91
- if data.get("warning"):
92
- print(f"Warning: {data['warning']}")
93
- return 0
94
- if command == "replay":
95
- arguments = {"task_id": args.task_id} if args.task_id else {}
96
- data = payload(call_tool(context, "agentram_replay_events", arguments))
97
- print(f"Events: {data.get('event_count', 0)}")
98
- print(f"Patches: {data.get('patch_count', 0)}")
99
- print(f"Memory items: {data.get('memory_count', 0)}")
100
- return 0
101
-
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import shlex
6
+ import shutil
7
+ import textwrap
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .config import default_ram_root
12
+ from .mcp_server import McpContext, call_tool
13
+
14
+ SUBAGENTS: dict[str, str] = {
15
+ "memory": "Summarize important facts, decisions, files, risks, and next tasks into AgentRAM events. Do not edit code.",
16
+ "planner": "Break the coding request into small ordered steps. Check AgentRAM goal drift before scope changes.",
17
+ "reviewer": "Review code changes for correctness, missing tests, regressions, and risky assumptions.",
18
+ "documenter": "Update README or usage notes from current implementation. Keep docs concise and runnable.",
19
+ }
20
+
21
+ SLASH_HELP = """Slash commands:
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 subagent presets
32
+ /agent <name> <task> Build Claude Code subagent prompt
33
+ /claude-install [path] Install .claude commands and agents
34
+ /note <text> Save decision note
35
+ /clear Clear chat
36
+ /exit Quit
37
+ Plain text Capture coding chat message and retrieve context
38
+ """.strip()
39
+
40
+
41
+ def build_parser() -> argparse.ArgumentParser:
42
+ parser = argparse.ArgumentParser(description="AgentRAM terminal UI and CLI.")
43
+ parser.add_argument("--ram-root", default=None, help="AgentRAM storage root. Defaults to .agentram in the current project directory.")
44
+ subparsers = parser.add_subparsers(dest="command")
45
+
46
+ subparsers.add_parser("tui", help="Open terminal chat UI.")
47
+ subparsers.add_parser("init", help="Create AgentRAM files.")
48
+ subparsers.add_parser("status", help="Show RAM file and item counts.")
49
+ subparsers.add_parser("goal", help="Show goal stack.")
50
+ subparsers.add_parser("agents", help="List subagent presets.")
51
+
52
+ retrieve = subparsers.add_parser("retrieve", help="Retrieve memory context.")
53
+ retrieve.add_argument("query", nargs="?", default="", help="Search query.")
54
+ retrieve.add_argument("--task-id", default="codex", help="Task id.")
55
+ retrieve.add_argument("--limit", type=int, default=8, help="Max items.")
56
+
57
+ drift = subparsers.add_parser("drift", help="Check goal drift for a task.")
58
+ drift.add_argument("task", nargs="?", default="", help="Task to check.")
59
+
60
+ replay = subparsers.add_parser("replay", help="Replay events into memory.")
61
+ replay.add_argument("--task-id", default=None, help="Optional task id filter.")
62
+
63
+ claude_install = subparsers.add_parser("claude-install", help="Install Claude Code slash commands and subagents into a project.")
64
+ claude_install.add_argument("--target", default=".", help="Target project root.")
65
+ claude_install.add_argument("--force", action="store_true", help="Overwrite existing templates.")
66
+
67
+ agent = subparsers.add_parser("agent", help="Build subagent prompt.")
68
+ agent.add_argument("name", choices=sorted(SUBAGENTS), help="Subagent preset.")
69
+ agent.add_argument("task", nargs="*", help="Task for subagent.")
70
+
71
+ return parser
72
+
73
+
74
+ def context_from_args(args: argparse.Namespace) -> McpContext:
75
+ ram_root = Path(args.ram_root) if args.ram_root else default_ram_root()
76
+ return McpContext(ram_root=ram_root)
77
+
78
+
79
+ def payload(result: dict[str, Any]) -> dict[str, Any]:
80
+ return dict(result.get("structuredContent", {}))
81
+
82
+
83
+ def run_command(args: argparse.Namespace) -> int:
84
+ context = context_from_args(args)
85
+ command = args.command or "tui"
86
+
87
+ if command == "tui":
88
+ run_tui(context)
89
+ return 0
90
+ if command == "init":
91
+ print(init_text(context))
92
+ return 0
93
+ if command == "status":
94
+ print(status_text(context))
95
+ return 0
96
+ if command == "retrieve":
97
+ query = args.query or input("Query > ").strip()
98
+ print(retrieve_text(context, query, task_id=args.task_id, limit=args.limit))
99
+ return 0
100
+ if command == "goal":
101
+ print(goal_text(context))
102
+ return 0
103
+ if command == "drift":
104
+ task = args.task or input("Task > ").strip()
105
+ print(drift_text(context, task))
106
+ return 0
107
+ if command == "replay":
108
+ arguments = {"task_id": args.task_id} if args.task_id else {}
109
+ print(replay_text(context, arguments))
110
+ return 0
111
+ if command == "agents":
112
+ print(agents_text())
113
+ return 0
114
+ if command == "claude-install":
115
+ print(install_claude_templates_text(Path(args.target), force=args.force))
116
+ return 0
117
+ if command == "agent":
118
+ print(agent_prompt_text(context, args.name, " ".join(args.task)))
119
+ return 0
120
+
102
121
  raise SystemExit(f"unknown command: {command}")
103
-
104
-
105
- def run_tui(context: McpContext) -> None:
106
- context.init_storage()
107
- messages = ["AgentRAM terminal UI ready.", f"RAM root: {context.ram_root}"]
108
- while True:
109
- draw_screen(context, messages)
110
- choice = input("You > ").strip()
111
- if choice in {"0", "q", "quit", "exit"}:
112
- return
113
- handle_tui_choice(context, choice, messages)
114
-
115
-
116
- def handle_tui_choice(context: McpContext, choice: str, messages: list[str]) -> None:
117
- try:
118
- if choice == "1":
119
- data = payload(call_tool(context, "agentram_init", {}))
120
- created = data.get("created", [])
121
- messages.append("Init: " + (", ".join(created) if created else "already ready"))
122
- elif choice == "2":
123
- messages.append(status_text(context))
124
- elif choice == "3":
125
- query = input("Query > ").strip()
126
- data = payload(call_tool(context, "agentram_retrieve", {"query": query, "task_id": "codex", "limit": 8}))
127
- messages.append(data.get("context") or "No matching memory.")
128
- elif choice == "4":
129
- data = payload(call_tool(context, "agentram_read_goal_state", {}))
130
- messages.append(data.get("context") or "No goal stack yet.")
131
- elif choice == "5":
132
- update_goal_from_prompt(context, messages)
133
- elif choice == "6":
134
- task = input("Task > ").strip()
135
- data = payload(call_tool(context, "agentram_check_goal_drift", {"task": task}))
136
- messages.append(f"Drift: {data.get('status')} | aligned={data.get('aligned')} | {data.get('reason')}")
137
- elif choice == "7":
138
- events = context.event_log.read_all()
139
- latest = events[-5:]
140
- lines = [f"Events: {len(events)}"]
141
- lines.extend(f"- {event.type.value} {event.payload}" for event in latest)
142
- messages.append("\n".join(lines))
143
- elif choice == "8":
144
- data = payload(call_tool(context, "agentram_replay_events", {}))
145
- messages.append(f"Replay: events={data.get('event_count', 0)} patches={data.get('patch_count', 0)} memory={data.get('memory_count', 0)}")
146
- elif choice == "9" or not choice:
147
- messages.append("Choose button number. Use retrieve before injecting context into agent prompt.")
148
- else:
149
- messages.append(f"Unknown button: {choice}")
150
- except Exception as error: # noqa: BLE001 - terminal boundary
151
- messages.append(f"Error: {error}")
152
- del messages[:-8]
153
-
154
-
155
- def update_goal_from_prompt(context: McpContext, messages: list[str]) -> None:
156
- patch: dict[str, object] = {}
157
- fields = ["mission", "project_goal", "current_milestone", "current_goal", "current_task"]
158
- for field in fields:
159
- value = input(f"{field} (blank skip) > ").strip()
160
- if value:
161
- patch[field] = value
162
- next_tasks = input("next_tasks comma list (blank skip) > ").strip()
163
- if next_tasks:
164
- patch["next_tasks"] = [item.strip() for item in next_tasks.split(",") if item.strip()]
165
- if not patch:
166
- messages.append("Goal update skipped.")
167
- return
168
- data = payload(call_tool(context, "agentram_update_goal_state", patch))
169
- messages.append(data.get("context") or "Goal updated.")
170
-
171
-
172
- def status_text(context: McpContext) -> str:
173
- context.init_storage()
174
- events = context.event_log.read_all()
175
- items = context.memory_store.list_items()
176
- goal_state = context.goal_store.load()
177
- return "\n".join(
178
- [
179
- f"RAM root: {context.ram_root}",
180
- f"Events: {len(events)}",
181
- f"Memory items: {len(items)}",
182
- f"Current goal: {goal_state.current_goal or '-'}",
183
- f"Current task: {goal_state.current_task or '-'}",
184
- ]
185
- )
186
-
187
-
188
- def draw_screen(context: McpContext, messages: list[str]) -> None:
189
- clear_terminal()
190
- width = min(max(shutil.get_terminal_size((100, 30)).columns, 72), 120)
191
- print(box_line("┌", "─", "┐", width))
192
- print(box_text("AgentRAM CLI", width))
193
- print(box_text(f"root: {context.ram_root}", width))
194
- print(box_line("├", "─", "┤", width))
195
- for line in menu_lines():
196
- print(box_text(line, width))
197
- print(box_line("├", "─", "┤", width))
198
- chat_lines = wrap_messages(messages, width - 4)
199
- visible = chat_lines[-14:]
200
- for line in visible:
201
- print(box_text(line, width))
202
- for _ in range(max(0, 14 - len(visible))):
203
- print(box_text("", width))
204
- print(box_line("└", "─", "┘", width))
205
-
206
-
207
- def menu_lines() -> list[str]:
208
- return [
209
- "[1] Init RAM [2] Status [3] Retrieve",
210
- "[4] Goal Show [5] Goal Set [6] Drift Check",
211
- "[7] Events [8] Replay [9] Help [0] Exit",
212
- ]
213
-
214
-
215
- def wrap_messages(messages: list[str], width: int) -> list[str]:
216
- lines: list[str] = []
217
- for message in messages:
218
- for raw_line in str(message).splitlines() or [""]:
219
- wrapped = textwrap.wrap(raw_line, width=width, replace_whitespace=False) or [""]
220
- lines.extend(wrapped)
221
- return lines
222
-
223
-
224
- def box_line(left: str, fill: str, right: str, width: int) -> str:
225
- return left + fill * (width - 2) + right
226
-
227
-
228
- def box_text(text: str, width: int) -> str:
229
- return "│ " + text[: width - 4].ljust(width - 4) + " │"
230
-
231
-
232
- def clear_terminal() -> None:
233
- os.system("cls" if os.name == "nt" else "clear")
234
-
235
-
236
- def main(argv: list[str] | None = None) -> int:
237
- args = build_parser().parse_args(argv)
238
- return run_command(args)
239
-
240
-
241
- if __name__ == "__main__":
122
+
123
+
124
+ def run_tui(context: McpContext) -> None:
125
+ context.init_storage()
126
+ messages = ["system > AgentRAM coding-agent CLI ready", f"system > RAM root: {context.ram_root}", "system > Type /help, /agent reviewer <task>, or plain coding request."]
127
+ while True:
128
+ draw_screen(context, messages)
129
+ user_input = input("You > ").strip()
130
+ if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
131
+ return
132
+ if user_input in BUTTONS:
133
+ user_input = BUTTONS[user_input]
134
+ if user_input:
135
+ messages.append(f"user > {user_input}")
136
+ response = handle_chat_input(context, user_input, messages)
137
+ if response == "__clear__":
138
+ messages.clear()
139
+ messages.append("system > Chat cleared.")
140
+ elif response:
141
+ messages.append("agentram > " + response)
142
+ del messages[:-10]
143
+
144
+
145
+ def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
146
+ if not text:
147
+ return SLASH_HELP
148
+ if text.startswith("/"):
149
+ return handle_slash_command(context, text, messages)
150
+ call_tool(
151
+ context,
152
+ "agentram_emit_event",
153
+ {"type": "USER_MESSAGE", "payload": {"content": text}, "task_id": "codex", "ingest": False},
154
+ )
155
+ context_block = retrieve_text(context, text, task_id="codex", limit=6)
156
+ return "Coding chat captured. Relevant context:\n" + context_block
157
+
158
+
159
+ def handle_slash_command(context: McpContext, text: str, messages: list[str] | None = None) -> str:
160
+ parts = shlex.split(text, posix=os.name != 'nt')
161
+ command = parts[0].lower()
162
+ rest = parts[1:]
163
+ raw_rest = text[len(parts[0]) :].strip()
164
+
165
+ if command == "/help":
166
+ return SLASH_HELP
167
+ if command == "/init":
168
+ return init_text(context)
169
+ if command == "/status":
170
+ return status_text(context)
171
+ if command == "/retrieve":
172
+ return retrieve_text(context, raw_rest, task_id="codex", limit=8)
173
+ if command == "/goal":
174
+ return goal_text(context)
175
+ if command == "/set-goal":
176
+ return set_goal_text(context, rest)
177
+ if command == "/drift":
178
+ return drift_text(context, raw_rest)
179
+ if command == "/events":
180
+ return events_text(context)
181
+ if command == "/replay":
182
+ return replay_text(context, {})
183
+ if command == "/agents":
184
+ return agents_text()
185
+ if command == "/agent":
186
+ if not rest:
187
+ return "Usage: /agent <memory|planner|reviewer|documenter> <task>"
188
+ return agent_prompt_text(context, rest[0], " ".join(rest[1:]))
189
+ if command == "/claude-install":
190
+ force = "--force" in raw_rest
191
+ target_text = raw_rest.replace("--force", "").strip().strip('"')
192
+ target = Path(target_text) if target_text else Path.cwd()
193
+ return install_claude_templates_text(target, force=force)
194
+ if command == "/note":
195
+ return note_text(context, raw_rest)
196
+ if command == "/clear":
197
+ return "__clear__"
198
+ return f"Unknown slash command: {command}\n\n{SLASH_HELP}"
199
+
200
+
201
+ def init_text(context: McpContext) -> str:
202
+ data = payload(call_tool(context, "agentram_init", {}))
203
+ created = data.get("created", [])
204
+ return f"AgentRAM root: {data['ram_root']}\nCreated: {', '.join(created) if created else 'none'}"
205
+
206
+
207
+ def status_text(context: McpContext) -> str:
208
+ context.init_storage()
209
+ events = context.event_log.read_all()
210
+ items = context.memory_store.list_items()
211
+ goal_state = context.goal_store.load()
212
+ return "\n".join(
213
+ [
214
+ f"RAM root: {context.ram_root}",
215
+ f"Events: {len(events)}",
216
+ f"Memory items: {len(items)}",
217
+ f"Mission: {goal_state.mission or '-'}",
218
+ f"Current goal: {goal_state.current_goal or '-'}",
219
+ f"Current task: {goal_state.current_task or '-'}",
220
+ ]
221
+ )
222
+
223
+
224
+ def retrieve_text(context: McpContext, query: str, task_id: str = "codex", limit: int = 8) -> str:
225
+ if not query.strip():
226
+ return "Usage: /retrieve <query>"
227
+ data = payload(call_tool(context, "agentram_retrieve", {"query": query, "task_id": task_id, "limit": limit}))
228
+ return data.get("context") or "No matching memory."
229
+
230
+
231
+ def goal_text(context: McpContext) -> str:
232
+ data = payload(call_tool(context, "agentram_read_goal_state", {}))
233
+ return data.get("context") or "No goal stack yet."
234
+
235
+
236
+ def set_goal_text(context: McpContext, assignments: list[str]) -> str:
237
+ if not assignments:
238
+ return "Usage: /set-goal current_goal='Ship CLI' current_task='Add slash commands'"
239
+ patch: dict[str, object] = {}
240
+ for item in assignments:
241
+ if "=" not in item:
242
+ return f"Invalid assignment: {item}"
243
+ key, value = item.split("=", 1)
244
+ if key == "next_tasks":
245
+ patch[key] = [part.strip() for part in value.split(",") if part.strip()]
246
+ else:
247
+ patch[key] = value
248
+ data = payload(call_tool(context, "agentram_update_goal_state", patch))
249
+ return data.get("context") or "Goal updated."
250
+
251
+
252
+ def drift_text(context: McpContext, task: str) -> str:
253
+ if not task.strip():
254
+ return "Usage: /drift <task>"
255
+ data = payload(call_tool(context, "agentram_check_goal_drift", {"task": task}))
256
+ lines = [f"Status: {data.get('status')}", f"Aligned: {data.get('aligned')}", f"Reason: {data.get('reason')}"]
257
+ if data.get("warning"):
258
+ lines.append(f"Warning: {data['warning']}")
259
+ return "\n".join(lines)
260
+
261
+
262
+ def events_text(context: McpContext) -> str:
263
+ events = context.event_log.read_all()
264
+ latest = events[-8:]
265
+ lines = [f"Events: {len(events)}"]
266
+ lines.extend(f"- {event.type.value} {event.payload}" for event in latest)
267
+ return "\n".join(lines)
268
+
269
+
270
+ def replay_text(context: McpContext, arguments: dict[str, object]) -> str:
271
+ data = payload(call_tool(context, "agentram_replay_events", arguments))
272
+ return f"Events: {data.get('event_count', 0)}\nPatches: {data.get('patch_count', 0)}\nMemory items: {data.get('memory_count', 0)}"
273
+
274
+
275
+ def agents_text() -> str:
276
+ lines = ["Subagent presets:"]
277
+ lines.extend(f"- {name}: {description}" for name, description in SUBAGENTS.items())
278
+ return "\n".join(lines)
279
+
280
+
281
+ def agent_prompt_text(context: McpContext, name: str, task: str) -> str:
282
+ if name not in SUBAGENTS:
283
+ return f"Unknown subagent: {name}\n\n{agents_text()}"
284
+ memory = retrieve_text(context, task or name, task_id="codex", limit=6) if task else goal_text(context)
285
+ return "\n".join(
286
+ [
287
+ f"Subagent: {name}",
288
+ f"Role: {SUBAGENTS[name]}",
289
+ f"Task: {task or '-'}",
290
+ "Context:",
291
+ memory,
292
+ "Output contract:",
293
+ "- Return concise findings.",
294
+ "- Mention files touched or inspected.",
295
+ "- Do not invent facts outside repo/RAM.",
296
+ ]
297
+ )
298
+
299
+
300
+
301
+ def claude_template_root() -> Path:
302
+ return Path(__file__).resolve().parent.parent / ".claude"
303
+
304
+
305
+ def install_claude_templates_text(target: Path, force: bool = False) -> str:
306
+ source_root = claude_template_root()
307
+ if not source_root.exists():
308
+ return f"Claude templates not found: {source_root}"
309
+ target_root = target.resolve() / ".claude"
310
+ copied: list[str] = []
311
+ skipped: list[str] = []
312
+ for source in source_root.rglob("*.md"):
313
+ relative = source.relative_to(source_root)
314
+ destination = target_root / relative
315
+ destination.parent.mkdir(parents=True, exist_ok=True)
316
+ if destination.exists() and not force:
317
+ skipped.append(str(destination))
318
+ continue
319
+ destination.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
320
+ copied.append(str(destination))
321
+ lines = [f"Claude templates target: {target_root}", f"Copied: {len(copied)}", f"Skipped: {len(skipped)}"]
322
+ if copied:
323
+ lines.append("Installed:")
324
+ lines.extend(f"- {item}" for item in copied)
325
+ if skipped:
326
+ lines.append("Skipped existing files. Use --force to overwrite.")
327
+ return "\n".join(lines)
328
+
329
+ def note_text(context: McpContext, content: str) -> str:
330
+ if not content.strip():
331
+ return "Usage: /note <decision or fact>"
332
+ data = payload(call_tool(context, "agentram_emit_event", {"type": "DECISION", "payload": {"content": content}, "task_id": "codex", "ingest": True}))
333
+ ingest = data.get("ingest") or {}
334
+ return f"Note saved. Patches: {ingest.get('patch_count', 0)}"
335
+
336
+
337
+ def update_goal_from_prompt(context: McpContext, messages: list[str]) -> None:
338
+ patch: dict[str, object] = {}
339
+ fields = ["mission", "project_goal", "current_milestone", "current_goal", "current_task"]
340
+ for field in fields:
341
+ value = input(f"{field} (blank skip) > ").strip()
342
+ if value:
343
+ patch[field] = value
344
+ next_tasks = input("next_tasks comma list (blank skip) > ").strip()
345
+ if next_tasks:
346
+ patch["next_tasks"] = [item.strip() for item in next_tasks.split(",") if item.strip()]
347
+ if not patch:
348
+ messages.append("Goal update skipped.")
349
+ return
350
+ data = payload(call_tool(context, "agentram_update_goal_state", patch))
351
+ messages.append(data.get("context") or "Goal updated.")
352
+
353
+
354
+ BUTTONS = {
355
+ "1": "/init",
356
+ "2": "/status",
357
+ "3": "/retrieve",
358
+ "4": "/goal",
359
+ "5": "/set-goal",
360
+ "6": "/drift",
361
+ "7": "/events",
362
+ "8": "/replay",
363
+ "9": "/help",
364
+ }
365
+
366
+
367
+ def draw_screen(context: McpContext, messages: list[str]) -> None:
368
+ clear_terminal()
369
+ width = min(max(shutil.get_terminal_size((110, 34)).columns, 86), 132)
370
+ left_width = 30
371
+ right_width = width - left_width - 3
372
+ print("+" + "-" * (width - 2) + "+")
373
+ print(box_text("AgentRAM Code Agent CLI | RAM + Claude subagents + slash commands", width))
374
+ print("+" + "-" * (width - 2) + "+")
375
+
376
+ sidebar = sidebar_lines(context)
377
+ chat = wrap_messages(messages, right_width - 4)[-22:]
378
+ rows = max(len(sidebar), len(chat), 22)
379
+ for index in range(rows):
380
+ left = sidebar[index] if index < len(sidebar) else ""
381
+ right = chat[index] if index < len(chat) else ""
382
+ print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
383
+ print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
384
+ print(box_text("Input: plain coding request, /help, /agent reviewer <task>, /claude-install .", width))
385
+ print("+" + "-" * (width - 2) + "+")
386
+
387
+
388
+ def sidebar_lines(context: McpContext) -> list[str]:
389
+ context.init_storage()
390
+ events = context.event_log.read_all()
391
+ items = context.memory_store.list_items()
392
+ goal = context.goal_store.load()
393
+ return [
394
+ "MODE",
395
+ " coding-agent TUI",
396
+ "",
397
+ "RAM",
398
+ f" events: {len(events)}",
399
+ f" notes: {len(items)}",
400
+ "",
401
+ "GOAL",
402
+ f" {trim(goal.current_goal or '-', 24)}",
403
+ "",
404
+ "TASK",
405
+ f" {trim(goal.current_task or '-', 24)}",
406
+ "",
407
+ "BUTTONS",
408
+ " 1 init 2 status",
409
+ " 3 retrieve 4 goal",
410
+ " 5 set-goal 6 drift",
411
+ " 7 events 8 replay",
412
+ " 9 help 0 exit",
413
+ "",
414
+ "CLAUDE",
415
+ " /claude-install .",
416
+ " /agents",
417
+ " /agent reviewer <task>",
418
+ ]
419
+
420
+
421
+ def trim(text: str, limit: int) -> str:
422
+ return text if len(text) <= limit else text[: limit - 1] + "…"
423
+
424
+
425
+ def menu_lines() -> list[str]:
426
+ return [
427
+ "1=/init 2=/status 3=/retrieve 4=/goal 5=/set-goal 6=/drift 7=/events 8=/replay 9=/help",
428
+ "Plain text captures coding request. Claude: /claude-install ., /agents, /agent reviewer <task>",
429
+ ]
430
+
431
+
432
+ def wrap_messages(messages: list[str], width: int) -> list[str]:
433
+ lines: list[str] = []
434
+ for message in messages:
435
+ for raw_line in str(message).splitlines() or [""]:
436
+ wrapped = textwrap.wrap(raw_line, width=width, replace_whitespace=False) or [""]
437
+ lines.extend(wrapped)
438
+ return lines
439
+
440
+
441
+ def box_line(left: str, fill: str, right: str, width: int) -> str:
442
+ return left + fill * (width - 2) + right
443
+
444
+
445
+ def box_text(text: str, width: int) -> str:
446
+ return "| " + text[: width - 4].ljust(width - 4) + " |"
447
+
448
+
449
+ def clear_terminal() -> None:
450
+ os.system("cls" if os.name == "nt" else "clear")
451
+
452
+
453
+ def main(argv: list[str] | None = None) -> int:
454
+ args = build_parser().parse_args(argv)
455
+ return run_command(args)
456
+
457
+
458
+ if __name__ == "__main__":
242
459
  raise SystemExit(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentram",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -33,6 +33,8 @@
33
33
  "files": [
34
34
  "agentram/*.py",
35
35
  "bin/*.js",
36
+ ".claude/agents/*.md",
37
+ ".claude/commands/*.md",
36
38
  "agentram_mcp_server.py",
37
39
  "agentram_daemon.py",
38
40
  "codex_ram.py",
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.0"
7
+ version = "0.1.1"
8
8
  description = "Async, model-agnostic RAM layer for agentic coding tools."
9
9
  readme = "README.md"
10
10
  license = "MIT"