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.
- package/.claude/agents/agentram-docs.md +18 -0
- package/.claude/agents/agentram-memory.md +20 -0
- package/.claude/agents/agentram-planner.md +18 -0
- package/.claude/agents/agentram-reviewer.md +18 -0
- package/.claude/commands/ram-drift.md +11 -0
- package/.claude/commands/ram-note.md +17 -0
- package/.claude/commands/ram-status.md +10 -0
- package/.claude/commands/ram.md +14 -0
- package/README.md +66 -0
- package/agentram/cli.py +457 -240
- package/package.json +3 -1
- package/pyproject.toml +1 -1
|
@@ -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
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from .
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
print(
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
108
|
-
while True:
|
|
109
|
-
draw_screen(context, messages)
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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.
|
|
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",
|