agentram 0.1.0 → 0.1.2

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
@@ -1,242 +1,549 @@
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
+ /models List bound coding models
34
+ /bind-model provider model Bind model endpoint
35
+ /orchestrate <task> Build multi-model agent plan
36
+ /claude-install [path] Install .claude commands and agents
37
+ /note <text> Save decision note
38
+ /clear Clear chat
39
+ /exit Quit
40
+ Plain text Capture coding chat message and retrieve context
41
+ """.strip()
42
+
43
+
44
+ def build_parser() -> argparse.ArgumentParser:
45
+ parser = argparse.ArgumentParser(description="AgentRAM terminal UI and CLI.")
46
+ parser.add_argument("--ram-root", default=None, help="AgentRAM storage root. Defaults to .agentram in the current project directory.")
47
+ subparsers = parser.add_subparsers(dest="command")
48
+
49
+ subparsers.add_parser("tui", help="Open terminal chat UI.")
50
+ subparsers.add_parser("init", help="Create AgentRAM files.")
51
+ subparsers.add_parser("status", help="Show RAM file and item counts.")
52
+ subparsers.add_parser("goal", help="Show goal stack.")
53
+ subparsers.add_parser("agents", help="List subagent presets.")
54
+ subparsers.add_parser("models", help="List bound coding model endpoints.")
55
+
56
+ bind_model = subparsers.add_parser("bind-model", help="Bind a coding model endpoint.")
57
+ bind_model.add_argument("provider", help="Provider name, e.g. claude, openai-compatible, groq.")
58
+ bind_model.add_argument("model", help="Model name.")
59
+ bind_model.add_argument("--role", default="coder", help="Agent role.")
60
+ bind_model.add_argument("--base-url", default="", help="Provider base URL.")
61
+ bind_model.add_argument("--api-key-env", default="", help="Environment variable containing API key.")
62
+ bind_model.add_argument("--modalities", default="text", help="Comma-separated modalities.")
63
+
64
+ orchestrate = subparsers.add_parser("orchestrate", help="Build or run multi-model coding orchestration.")
65
+ orchestrate.add_argument("task", nargs="?", default="", help="Coding task.")
66
+ orchestrate.add_argument("--file", action="append", default=[], help="File path to include.")
67
+ orchestrate.add_argument("--execute", action="store_true", help="Call model endpoints concurrently.")
68
+
69
+ retrieve = subparsers.add_parser("retrieve", help="Retrieve memory context.")
70
+ retrieve.add_argument("query", nargs="?", default="", help="Search query.")
71
+ retrieve.add_argument("--task-id", default="codex", help="Task id.")
72
+ retrieve.add_argument("--limit", type=int, default=8, help="Max items.")
73
+
74
+ drift = subparsers.add_parser("drift", help="Check goal drift for a task.")
75
+ drift.add_argument("task", nargs="?", default="", help="Task to check.")
76
+
77
+ replay = subparsers.add_parser("replay", help="Replay events into memory.")
78
+ replay.add_argument("--task-id", default=None, help="Optional task id filter.")
79
+
80
+ claude_install = subparsers.add_parser("claude-install", help="Install Claude Code slash commands and subagents into a project.")
81
+ claude_install.add_argument("--target", default=".", help="Target project root.")
82
+ claude_install.add_argument("--force", action="store_true", help="Overwrite existing templates.")
83
+
84
+ agent = subparsers.add_parser("agent", help="Build subagent prompt.")
85
+ agent.add_argument("name", choices=sorted(SUBAGENTS), help="Subagent preset.")
86
+ agent.add_argument("task", nargs="*", help="Task for subagent.")
87
+
88
+ return parser
89
+
90
+
91
+ def context_from_args(args: argparse.Namespace) -> McpContext:
92
+ ram_root = Path(args.ram_root) if args.ram_root else default_ram_root()
93
+ return McpContext(ram_root=ram_root)
94
+
95
+
96
+ def payload(result: dict[str, Any]) -> dict[str, Any]:
97
+ return dict(result.get("structuredContent", {}))
98
+
99
+
100
+ def run_command(args: argparse.Namespace) -> int:
101
+ context = context_from_args(args)
102
+ command = args.command or "tui"
103
+
104
+ if command == "tui":
105
+ run_tui(context)
106
+ return 0
107
+ if command == "init":
108
+ print(init_text(context))
109
+ return 0
110
+ if command == "status":
111
+ print(status_text(context))
112
+ return 0
113
+ if command == "retrieve":
114
+ query = args.query or input("Query > ").strip()
115
+ print(retrieve_text(context, query, task_id=args.task_id, limit=args.limit))
116
+ return 0
117
+ if command == "goal":
118
+ print(goal_text(context))
119
+ return 0
120
+ if command == "drift":
121
+ task = args.task or input("Task > ").strip()
122
+ print(drift_text(context, task))
123
+ return 0
124
+ if command == "replay":
125
+ arguments = {"task_id": args.task_id} if args.task_id else {}
126
+ print(replay_text(context, arguments))
127
+ return 0
128
+ if command == "agents":
129
+ print(agents_text())
130
+ return 0
131
+ if command == "models":
132
+ print(models_text(context))
133
+ return 0
134
+ if command == "bind-model":
135
+ print(bind_model_text(context, vars(args)))
136
+ return 0
137
+ if command == "orchestrate":
138
+ task = args.task or input("Task > ").strip()
139
+ print(orchestrate_text(context, task, files=args.file, execute=args.execute))
140
+ return 0
141
+ if command == "claude-install":
142
+ print(install_claude_templates_text(Path(args.target), force=args.force))
143
+ return 0
144
+ if command == "agent":
145
+ print(agent_prompt_text(context, args.name, " ".join(args.task)))
146
+ return 0
147
+
102
148
  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__":
149
+
150
+
151
+ def run_tui(context: McpContext) -> None:
152
+ 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."]
154
+ while True:
155
+ draw_screen(context, messages)
156
+ user_input = input("You > ").strip()
157
+ if user_input in {"0", "q", "quit", "exit", "/exit", "/quit"}:
158
+ return
159
+ if user_input in BUTTONS:
160
+ user_input = BUTTONS[user_input]
161
+ if user_input:
162
+ messages.append(f"user > {user_input}")
163
+ response = handle_chat_input(context, user_input, messages)
164
+ if response == "__clear__":
165
+ messages.clear()
166
+ messages.append("system > Chat cleared.")
167
+ elif response:
168
+ messages.append("agentram > " + response)
169
+ del messages[:-10]
170
+
171
+
172
+ def handle_chat_input(context: McpContext, text: str, messages: list[str] | None = None) -> str:
173
+ if not text:
174
+ return SLASH_HELP
175
+ if text.startswith("/"):
176
+ return handle_slash_command(context, text, messages)
177
+ call_tool(
178
+ context,
179
+ "agentram_emit_event",
180
+ {"type": "USER_MESSAGE", "payload": {"content": text}, "task_id": "codex", "ingest": False},
181
+ )
182
+ context_block = retrieve_text(context, text, task_id="codex", limit=6)
183
+ return "Coding chat captured. Relevant context:\n" + context_block
184
+
185
+
186
+ def handle_slash_command(context: McpContext, text: str, messages: list[str] | None = None) -> str:
187
+ parts = shlex.split(text, posix=os.name != 'nt')
188
+ command = parts[0].lower()
189
+ rest = parts[1:]
190
+ raw_rest = text[len(parts[0]) :].strip()
191
+
192
+ if command == "/help":
193
+ return SLASH_HELP
194
+ if command == "/init":
195
+ return init_text(context)
196
+ if command == "/status":
197
+ return status_text(context)
198
+ if command == "/retrieve":
199
+ return retrieve_text(context, raw_rest, task_id="codex", limit=8)
200
+ if command == "/goal":
201
+ return goal_text(context)
202
+ if command == "/set-goal":
203
+ return set_goal_text(context, rest)
204
+ if command == "/drift":
205
+ return drift_text(context, raw_rest)
206
+ if command == "/events":
207
+ return events_text(context)
208
+ if command == "/replay":
209
+ return replay_text(context, {})
210
+ if command == "/agents":
211
+ return agents_text()
212
+ if command == "/models":
213
+ return models_text(context)
214
+ if command == "/bind-model":
215
+ 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)
223
+ if command == "/orchestrate":
224
+ return orchestrate_text(context, raw_rest, execute=False)
225
+ if command == "/agent":
226
+ if not rest:
227
+ return "Usage: /agent <memory|planner|reviewer|documenter> <task>"
228
+ return agent_prompt_text(context, rest[0], " ".join(rest[1:]))
229
+ if command == "/claude-install":
230
+ force = "--force" in raw_rest
231
+ target_text = raw_rest.replace("--force", "").strip().strip('"')
232
+ target = Path(target_text) if target_text else Path.cwd()
233
+ return install_claude_templates_text(target, force=force)
234
+ if command == "/note":
235
+ return note_text(context, raw_rest)
236
+ if command == "/clear":
237
+ return "__clear__"
238
+ return f"Unknown slash command: {command}\n\n{SLASH_HELP}"
239
+
240
+
241
+ def init_text(context: McpContext) -> str:
242
+ data = payload(call_tool(context, "agentram_init", {}))
243
+ created = data.get("created", [])
244
+ return f"AgentRAM root: {data['ram_root']}\nCreated: {', '.join(created) if created else 'none'}"
245
+
246
+
247
+ def status_text(context: McpContext) -> str:
248
+ context.init_storage()
249
+ events = context.event_log.read_all()
250
+ items = context.memory_store.list_items()
251
+ goal_state = context.goal_store.load()
252
+ return "\n".join(
253
+ [
254
+ f"RAM root: {context.ram_root}",
255
+ f"Events: {len(events)}",
256
+ f"Memory items: {len(items)}",
257
+ f"Mission: {goal_state.mission or '-'}",
258
+ f"Current goal: {goal_state.current_goal or '-'}",
259
+ f"Current task: {goal_state.current_task or '-'}",
260
+ ]
261
+ )
262
+
263
+
264
+ def retrieve_text(context: McpContext, query: str, task_id: str = "codex", limit: int = 8) -> str:
265
+ if not query.strip():
266
+ return "Usage: /retrieve <query>"
267
+ data = payload(call_tool(context, "agentram_retrieve", {"query": query, "task_id": task_id, "limit": limit}))
268
+ return data.get("context") or "No matching memory."
269
+
270
+
271
+ def goal_text(context: McpContext) -> str:
272
+ data = payload(call_tool(context, "agentram_read_goal_state", {}))
273
+ return data.get("context") or "No goal stack yet."
274
+
275
+
276
+ def set_goal_text(context: McpContext, assignments: list[str]) -> str:
277
+ if not assignments:
278
+ return "Usage: /set-goal current_goal='Ship CLI' current_task='Add slash commands'"
279
+ patch: dict[str, object] = {}
280
+ for item in assignments:
281
+ if "=" not in item:
282
+ return f"Invalid assignment: {item}"
283
+ key, value = item.split("=", 1)
284
+ if key == "next_tasks":
285
+ patch[key] = [part.strip() for part in value.split(",") if part.strip()]
286
+ else:
287
+ patch[key] = value
288
+ data = payload(call_tool(context, "agentram_update_goal_state", patch))
289
+ return data.get("context") or "Goal updated."
290
+
291
+
292
+ def drift_text(context: McpContext, task: str) -> str:
293
+ if not task.strip():
294
+ return "Usage: /drift <task>"
295
+ data = payload(call_tool(context, "agentram_check_goal_drift", {"task": task}))
296
+ lines = [f"Status: {data.get('status')}", f"Aligned: {data.get('aligned')}", f"Reason: {data.get('reason')}"]
297
+ if data.get("warning"):
298
+ lines.append(f"Warning: {data['warning']}")
299
+ return "\n".join(lines)
300
+
301
+
302
+ def events_text(context: McpContext) -> str:
303
+ events = context.event_log.read_all()
304
+ latest = events[-8:]
305
+ lines = [f"Events: {len(events)}"]
306
+ lines.extend(f"- {event.type.value} {event.payload}" for event in latest)
307
+ return "\n".join(lines)
308
+
309
+
310
+ def replay_text(context: McpContext, arguments: dict[str, object]) -> str:
311
+ data = payload(call_tool(context, "agentram_replay_events", arguments))
312
+ return f"Events: {data.get('event_count', 0)}\nPatches: {data.get('patch_count', 0)}\nMemory items: {data.get('memory_count', 0)}"
313
+
314
+
315
+ def agents_text() -> str:
316
+ lines = ["Subagent presets:"]
317
+ lines.extend(f"- {name}: {description}" for name, description in SUBAGENTS.items())
318
+ return "\n".join(lines)
319
+
320
+
321
+ def agent_prompt_text(context: McpContext, name: str, task: str) -> str:
322
+ if name not in SUBAGENTS:
323
+ return f"Unknown subagent: {name}\n\n{agents_text()}"
324
+ memory = retrieve_text(context, task or name, task_id="codex", limit=6) if task else goal_text(context)
325
+ return "\n".join(
326
+ [
327
+ f"Subagent: {name}",
328
+ f"Role: {SUBAGENTS[name]}",
329
+ f"Task: {task or '-'}",
330
+ "Context:",
331
+ memory,
332
+ "Output contract:",
333
+ "- Return concise findings.",
334
+ "- Mention files touched or inspected.",
335
+ "- Do not invent facts outside repo/RAM.",
336
+ ]
337
+ )
338
+
339
+
340
+
341
+
342
+ def models_text(context: McpContext) -> str:
343
+ data = payload(call_tool(context, "agentram_read_coding_models", {}))
344
+ endpoints = data.get("endpoints", [])
345
+ if not endpoints:
346
+ return "No coding models bound. Use /bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY"
347
+ lines = ["Bound coding models:"]
348
+ for endpoint in endpoints:
349
+ lines.append(f"- {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')} modalities={','.join(endpoint.get('modalities', []))}")
350
+ return "\n".join(lines)
351
+
352
+
353
+ def bind_model_text(context: McpContext, options: dict[str, object]) -> str:
354
+ modalities_value = str(options.get("modalities", "text"))
355
+ arguments = {
356
+ "id": str(options.get("id", "")) or None,
357
+ "provider": str(options.get("provider", "openai-compatible")),
358
+ "model": str(options.get("model", "")),
359
+ "role": str(options.get("role", "coder")),
360
+ "base_url": str(options.get("base_url", "")),
361
+ "api_key_env": str(options.get("api_key_env", "")),
362
+ "modalities": [item.strip() for item in modalities_value.split(",") if item.strip()],
363
+ "enabled": bool(options.get("enabled", True)),
364
+ }
365
+ arguments = {key: value for key, value in arguments.items() if value is not None}
366
+ data = payload(call_tool(context, "agentram_bind_coding_model", arguments))
367
+ endpoint = data.get("endpoint", {})
368
+ return f"Bound model: {endpoint.get('id')} provider={endpoint.get('provider')} model={endpoint.get('model')} role={endpoint.get('role')}"
369
+
370
+
371
+ def orchestrate_text(context: McpContext, task: str, files: list[str] | None = None, execute: bool = False) -> str:
372
+ if not task.strip():
373
+ return "Usage: /orchestrate <coding task>"
374
+ tool = "agentram_run_coding_orchestration" if execute else "agentram_build_coding_orchestration"
375
+ data = payload(call_tool(context, tool, {"task": task, "files": files or [], "execute": execute}))
376
+ if not execute:
377
+ lines = [f"Orchestration plan: agents={data.get('agent_count', 0)}"]
378
+ for agent in data.get("agents", []):
379
+ endpoint = agent.get("endpoint", {})
380
+ lines.append(f"- {endpoint.get('id')} {endpoint.get('provider')}:{endpoint.get('model')} role={endpoint.get('role')}")
381
+ return "\n".join(lines)
382
+ lines = [f"Orchestration run: agents={data.get('agent_count', 0)}"]
383
+ for result in data.get("results", []):
384
+ endpoint = result.get("endpoint", {})
385
+ status = "ok" if result.get("ok") else "error"
386
+ output = result.get("output") or result.get("error", "")
387
+ lines.append(f"- {endpoint.get('id')} {status}: {str(output)[:300]}")
388
+ return "\n".join(lines)
389
+
390
+ def claude_template_root() -> Path:
391
+ return Path(__file__).resolve().parent.parent / ".claude"
392
+
393
+
394
+ def install_claude_templates_text(target: Path, force: bool = False) -> str:
395
+ source_root = claude_template_root()
396
+ if not source_root.exists():
397
+ return f"Claude templates not found: {source_root}"
398
+ target_root = target.resolve() / ".claude"
399
+ copied: list[str] = []
400
+ skipped: list[str] = []
401
+ for source in source_root.rglob("*.md"):
402
+ relative = source.relative_to(source_root)
403
+ destination = target_root / relative
404
+ destination.parent.mkdir(parents=True, exist_ok=True)
405
+ if destination.exists() and not force:
406
+ skipped.append(str(destination))
407
+ continue
408
+ destination.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
409
+ copied.append(str(destination))
410
+ lines = [f"Claude templates target: {target_root}", f"Copied: {len(copied)}", f"Skipped: {len(skipped)}"]
411
+ if copied:
412
+ lines.append("Installed:")
413
+ lines.extend(f"- {item}" for item in copied)
414
+ if skipped:
415
+ lines.append("Skipped existing files. Use --force to overwrite.")
416
+ return "\n".join(lines)
417
+
418
+ def note_text(context: McpContext, content: str) -> str:
419
+ if not content.strip():
420
+ return "Usage: /note <decision or fact>"
421
+ data = payload(call_tool(context, "agentram_emit_event", {"type": "DECISION", "payload": {"content": content}, "task_id": "codex", "ingest": True}))
422
+ ingest = data.get("ingest") or {}
423
+ return f"Note saved. Patches: {ingest.get('patch_count', 0)}"
424
+
425
+
426
+ def update_goal_from_prompt(context: McpContext, messages: list[str]) -> None:
427
+ patch: dict[str, object] = {}
428
+ fields = ["mission", "project_goal", "current_milestone", "current_goal", "current_task"]
429
+ for field in fields:
430
+ value = input(f"{field} (blank skip) > ").strip()
431
+ if value:
432
+ patch[field] = value
433
+ next_tasks = input("next_tasks comma list (blank skip) > ").strip()
434
+ if next_tasks:
435
+ patch["next_tasks"] = [item.strip() for item in next_tasks.split(",") if item.strip()]
436
+ if not patch:
437
+ messages.append("Goal update skipped.")
438
+ return
439
+ data = payload(call_tool(context, "agentram_update_goal_state", patch))
440
+ messages.append(data.get("context") or "Goal updated.")
441
+
442
+
443
+ BUTTONS = {
444
+ "1": "/init",
445
+ "2": "/status",
446
+ "3": "/retrieve",
447
+ "4": "/goal",
448
+ "5": "/set-goal",
449
+ "6": "/drift",
450
+ "7": "/events",
451
+ "8": "/replay",
452
+ "9": "/help",
453
+ }
454
+
455
+
456
+ def draw_screen(context: McpContext, messages: list[str]) -> None:
457
+ clear_terminal()
458
+ width = min(max(shutil.get_terminal_size((110, 34)).columns, 86), 132)
459
+ left_width = 30
460
+ right_width = width - left_width - 3
461
+ print("+" + "-" * (width - 2) + "+")
462
+ print(box_text("AgentRAM Code Agent CLI | RAM + Claude subagents + slash commands", width))
463
+ print("+" + "-" * (width - 2) + "+")
464
+
465
+ sidebar = sidebar_lines(context)
466
+ chat = wrap_messages(messages, right_width - 4)[-22:]
467
+ rows = max(len(sidebar), len(chat), 22)
468
+ for index in range(rows):
469
+ left = sidebar[index] if index < len(sidebar) else ""
470
+ right = chat[index] if index < len(chat) else ""
471
+ print("| " + left[: left_width - 2].ljust(left_width - 2) + "| " + right[: right_width - 2].ljust(right_width - 2) + "|")
472
+ print("+" + "-" * (left_width - 1) + "+" + "-" * (right_width) + "+")
473
+ print(box_text("Input: plain coding request, /help, /agent reviewer <task>, /claude-install .", width))
474
+ print("+" + "-" * (width - 2) + "+")
475
+
476
+
477
+ def sidebar_lines(context: McpContext) -> list[str]:
478
+ context.init_storage()
479
+ events = context.event_log.read_all()
480
+ items = context.memory_store.list_items()
481
+ goal = context.goal_store.load()
482
+ return [
483
+ "MODE",
484
+ " coding-agent TUI",
485
+ "",
486
+ "RAM",
487
+ f" events: {len(events)}",
488
+ f" notes: {len(items)}",
489
+ "",
490
+ "GOAL",
491
+ f" {trim(goal.current_goal or '-', 24)}",
492
+ "",
493
+ "TASK",
494
+ f" {trim(goal.current_task or '-', 24)}",
495
+ "",
496
+ "BUTTONS",
497
+ " 1 init 2 status",
498
+ " 3 retrieve 4 goal",
499
+ " 5 set-goal 6 drift",
500
+ " 7 events 8 replay",
501
+ " 9 help 0 exit",
502
+ "",
503
+ "CLAUDE",
504
+ " /claude-install .",
505
+ " /models",
506
+ " /bind-model claude <model>",
507
+ " /orchestrate <task>",
508
+ ]
509
+
510
+
511
+ def trim(text: str, limit: int) -> str:
512
+ return text if len(text) <= limit else text[: limit - 1] + "…"
513
+
514
+
515
+ def menu_lines() -> list[str]:
516
+ return [
517
+ "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>",
519
+ ]
520
+
521
+
522
+ def wrap_messages(messages: list[str], width: int) -> list[str]:
523
+ lines: list[str] = []
524
+ for message in messages:
525
+ for raw_line in str(message).splitlines() or [""]:
526
+ wrapped = textwrap.wrap(raw_line, width=width, replace_whitespace=False) or [""]
527
+ lines.extend(wrapped)
528
+ return lines
529
+
530
+
531
+ def box_line(left: str, fill: str, right: str, width: int) -> str:
532
+ return left + fill * (width - 2) + right
533
+
534
+
535
+ def box_text(text: str, width: int) -> str:
536
+ return "| " + text[: width - 4].ljust(width - 4) + " |"
537
+
538
+
539
+ def clear_terminal() -> None:
540
+ os.system("cls" if os.name == "nt" else "clear")
541
+
542
+
543
+ def main(argv: list[str] | None = None) -> int:
544
+ args = build_parser().parse_args(argv)
545
+ return run_command(args)
546
+
547
+
548
+ if __name__ == "__main__":
242
549
  raise SystemExit(main())