agent-memory-brain 0.2.0__py3-none-any.whl
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.
- agent_memory_brain-0.2.0.dist-info/METADATA +98 -0
- agent_memory_brain-0.2.0.dist-info/RECORD +54 -0
- agent_memory_brain-0.2.0.dist-info/WHEEL +4 -0
- agent_memory_brain-0.2.0.dist-info/entry_points.txt +3 -0
- agent_memory_brain-0.2.0.dist-info/licenses/LICENSE +67 -0
- devmem/__init__.py +3 -0
- devmem/ado.py +433 -0
- devmem/agent_colors.py +74 -0
- devmem/cli.py +500 -0
- devmem/db.py +1069 -0
- devmem/dispatch.py +478 -0
- devmem/embed.py +111 -0
- devmem/filelock.py +53 -0
- devmem/git_sync.py +261 -0
- devmem/guard.py +214 -0
- devmem/hook_log.py +49 -0
- devmem/ingest/__init__.py +1 -0
- devmem/ingest/claude.py +87 -0
- devmem/ingest/codex.py +175 -0
- devmem/ingest/runner.py +318 -0
- devmem/ingest/subagents.py +250 -0
- devmem/maintenance.py +801 -0
- devmem/mcp_server.py +542 -0
- devmem/messages.py +684 -0
- devmem/panel/__init__.py +1 -0
- devmem/panel/app.py +971 -0
- devmem/panel/state.py +358 -0
- devmem/panel/static/base.css +27 -0
- devmem/panel/static/brain.css +473 -0
- devmem/panel/static/brain.js +1118 -0
- devmem/panel/static/brain3d.js +600 -0
- devmem/panel/static/chat.js +18 -0
- devmem/panel/static/common.js +208 -0
- devmem/panel/static/site.css +508 -0
- devmem/panel/static/tokens.css +19 -0
- devmem/panel/templates/ado.html +62 -0
- devmem/panel/templates/agents.html +35 -0
- devmem/panel/templates/audit.html +19 -0
- devmem/panel/templates/base.html +42 -0
- devmem/panel/templates/brain.html +101 -0
- devmem/panel/templates/maintenance.html +32 -0
- devmem/panel/templates/memory.html +71 -0
- devmem/panel/templates/message_thread.html +49 -0
- devmem/panel/templates/messages.html +8 -0
- devmem/panel/templates/overview.html +109 -0
- devmem/panel/templates/project.html +48 -0
- devmem/panel/templates/projects.html +21 -0
- devmem/panel/templates/session.html +49 -0
- devmem/panel/templates/sessions.html +67 -0
- devmem/panel/templates/subagent.html +27 -0
- devmem/procs.py +10 -0
- devmem/projects.py +17 -0
- devmem/secrets.py +9 -0
- devmem/skills.py +105 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
{% block title %}Subagent · Brain{% endblock %}
|
|
3
|
+
{% block body %}
|
|
4
|
+
<h1>{{ agent.agent_type or 'subagent' }}</h1>
|
|
5
|
+
<p class="muted back-row">{% if agent.thread_id %}<a href="/messages?chat={{ ('s:' ~ agent.session_id) | urlencode }}">Back to chat</a><a href="/messages/thread/{{ agent.thread_id }}">Thread</a>{% endif %}<span>{{ agent.description or '' }}</span></p>
|
|
6
|
+
<div class="grid-cards">
|
|
7
|
+
<div class="card"><div class="stat-label">Status</div><div class="stat-value"><span class="badge st-{{ agent.status }}">{{ agent.status }}</span></div></div>
|
|
8
|
+
<div class="card"><div class="stat-label">Started</div><div><time data-rel datetime="{{ agent.started_at }}">{{ agent.started_at }}</time></div>
|
|
9
|
+
<div class="stat-label stat-gap">Last activity</div><div><time data-rel datetime="{{ agent.last_activity_at }}">{{ agent.last_activity_at }}</time></div></div>
|
|
10
|
+
<div class="card"><div class="stat-label">Tokens</div><div class="stat-value" data-tokens="{{ agent.tokens }}">{{ agent.tokens }}</div></div>
|
|
11
|
+
<div class="card"><div class="stat-label">Parent session</div><div class="mono"><a href="/sessions/{{ agent.session_id }}">{{ agent.session_id }}</a></div>
|
|
12
|
+
{% if agent.project %}<div class="muted mono">{{ agent.project }}</div>{% endif %}</div>
|
|
13
|
+
</div>
|
|
14
|
+
<h2>Task</h2>
|
|
15
|
+
<pre class="card msg-body">{{ agent.task or 'No task recorded.' }}</pre>
|
|
16
|
+
<h2>Tool calls</h2>
|
|
17
|
+
<table><thead><tr><th>Time</th><th>Tool</th><th>Argument</th></tr></thead><tbody>
|
|
18
|
+
{% for t in agent.tools %}<tr><td><time data-rel datetime="{{ t.ts }}">{{ t.ts }}</time></td><td class="mono">{{ t.name }}</td><td class="mono muted">{{ t.arg }}</td></tr>
|
|
19
|
+
{% else %}<tr><td colspan="3" class="muted">No tool calls yet.</td></tr>{% endfor %}
|
|
20
|
+
</tbody></table>
|
|
21
|
+
{% if agent.texts %}<h2>Remarks</h2>
|
|
22
|
+
<table><thead><tr><th>Time</th><th>From</th><th>Text</th></tr></thead><tbody>
|
|
23
|
+
{% for t in agent.texts %}<tr><td><time data-rel datetime="{{ t.ts }}">{{ t.ts }}</time></td><td class="muted">{{ 'parent' if t.role == 'parent' else 'agent' }}</td><td class="msg-body">{{ t.body }}</td></tr>{% endfor %}
|
|
24
|
+
</tbody></table>{% endif %}
|
|
25
|
+
<h2>Report</h2>
|
|
26
|
+
<pre class="card msg-body">{% if agent.report %}{{ agent.report }}{% elif agent.status == 'stopped' %}No report: the agent stopped (no write for 2 hours) without finishing.{% else %}Still {{ agent.status }}; the report appears when it finishes.{% endif %}</pre>
|
|
27
|
+
{% endblock %}
|
devmem/procs.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Small helpers for starting external processes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def executable(name: str) -> str:
|
|
9
|
+
"""Return a resolved executable path on Windows, otherwise preserve ``name``."""
|
|
10
|
+
return shutil.which(name) or name if os.name == "nt" else name
|
devmem/projects.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Project-path helpers that are safe for hooks to import."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@lru_cache(maxsize=1024)
|
|
9
|
+
def project_for_cwd(cwd: str | None) -> str | None:
|
|
10
|
+
"""Return git root for cwd, without invoking git; walk for a .git marker."""
|
|
11
|
+
if not cwd:
|
|
12
|
+
return None
|
|
13
|
+
current = Path(cwd).expanduser().resolve()
|
|
14
|
+
for candidate in (current, *current.parents):
|
|
15
|
+
if (candidate / ".git").exists():
|
|
16
|
+
return str(candidate)
|
|
17
|
+
return str(current)
|
devmem/secrets.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Shared secret-redaction patterns, kept free of operational dependencies."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SECRET = re.compile(r"BEGIN [A-Z ]*PRIVATE KEY|\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_\w{20,}|sk-(?:proj-|ant-)?[\w-]{20,}|[sr]k_(?:live|test)_\w{10,}"
|
|
8
|
+
r"|AKIA[0-9A-Z]{16}|xox[abposr]-[A-Za-z0-9-]{10,}|eyJ[\w-]{8,}\.eyJ[\w-]{8,}\.[\w-]{8,})|\bBearer\s+[\w.~+/=-]{16,}"
|
|
9
|
+
r"|(?:api[_-]?key|access[_-]?key|token|secret|passw(?:or)?d|pwd)\s*[:=]\s*['\"]?[A-Za-z0-9_./+=-]{8,}", re.I)
|
devmem/skills.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Small, dependency-free loader and installer for repository coding skills."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def skills_dir() -> Path:
|
|
10
|
+
"""Return the configured skills directory, or this repository's skills directory."""
|
|
11
|
+
configured = os.environ.get("DEVMEM_SKILLS_DIR")
|
|
12
|
+
return Path(configured).expanduser().resolve() if configured else (Path(__file__).resolve().parent.parent / "skills")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _skill(path: Path) -> dict:
|
|
16
|
+
try:
|
|
17
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
18
|
+
except OSError as exc:
|
|
19
|
+
raise ValueError(str(exc)) from exc
|
|
20
|
+
if not lines or lines[0] != "---":
|
|
21
|
+
raise ValueError("frontmatter must start with ---")
|
|
22
|
+
try:
|
|
23
|
+
end = lines.index("---", 1)
|
|
24
|
+
except ValueError as exc:
|
|
25
|
+
raise ValueError("frontmatter is not closed") from exc
|
|
26
|
+
fields = {}
|
|
27
|
+
for line in lines[1:end]:
|
|
28
|
+
if not line.strip():
|
|
29
|
+
continue
|
|
30
|
+
if ":" not in line:
|
|
31
|
+
raise ValueError("frontmatter line must be key: value")
|
|
32
|
+
key, value = line.split(":", 1)
|
|
33
|
+
if not key.strip() or not value.strip():
|
|
34
|
+
raise ValueError("frontmatter line must be key: value")
|
|
35
|
+
fields[key.strip()] = value.strip()
|
|
36
|
+
for key in ("name", "description", "devmem-briefing"):
|
|
37
|
+
if key not in fields:
|
|
38
|
+
raise ValueError(f"missing {key}")
|
|
39
|
+
if fields["devmem-briefing"] not in ("true", "false"):
|
|
40
|
+
raise ValueError("devmem-briefing must be true or false")
|
|
41
|
+
try:
|
|
42
|
+
summary_start = lines.index("## Summary", end + 1) + 1
|
|
43
|
+
except ValueError as exc:
|
|
44
|
+
raise ValueError("missing ## Summary") from exc
|
|
45
|
+
summary_lines = []
|
|
46
|
+
for line in lines[summary_start:]:
|
|
47
|
+
if line.startswith("## "):
|
|
48
|
+
break
|
|
49
|
+
summary_lines.append(line)
|
|
50
|
+
summary = "\n".join(summary_lines).strip()
|
|
51
|
+
if not summary:
|
|
52
|
+
raise ValueError("empty ## Summary")
|
|
53
|
+
return {"name": fields["name"], "description": fields["description"],
|
|
54
|
+
"briefing": fields["devmem-briefing"] == "true", "summary": summary,
|
|
55
|
+
"path": str(path.resolve())}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def list_skills() -> dict:
|
|
59
|
+
"""Return valid repository skills and reports for malformed skill files."""
|
|
60
|
+
skills, invalid = [], []
|
|
61
|
+
try:
|
|
62
|
+
paths = sorted(skills_dir().glob("*/SKILL.md"))
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
return {"skills": [], "invalid": [{"path": str(skills_dir()), "reason": str(exc)}]}
|
|
65
|
+
for path in paths:
|
|
66
|
+
try:
|
|
67
|
+
skills.append(_skill(path))
|
|
68
|
+
except ValueError as exc:
|
|
69
|
+
invalid.append({"path": str(path.resolve()), "reason": str(exc)})
|
|
70
|
+
return {"skills": skills, "invalid": invalid}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def briefing_text() -> str:
|
|
74
|
+
"""Render the compact skill text included in agent briefings."""
|
|
75
|
+
briefing = [skill for skill in list_skills()["skills"] if skill["briefing"]]
|
|
76
|
+
if not briefing:
|
|
77
|
+
return ""
|
|
78
|
+
entries = []
|
|
79
|
+
for skill in briefing:
|
|
80
|
+
entries.append(f"{skill['summary']}\nFull text: skill {skill['name']} ({skill['path']})")
|
|
81
|
+
return "## Coding guidelines\n" + "\n\n".join(entries)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def install(target: Path, dry_run: bool = False) -> bool:
|
|
85
|
+
"""Install skills as directory symlinks, returning false when a conflict exists."""
|
|
86
|
+
data = list_skills()
|
|
87
|
+
failed = False
|
|
88
|
+
for skill in data["skills"]:
|
|
89
|
+
source = Path(skill["path"]).parent
|
|
90
|
+
destination = target / skill["name"]
|
|
91
|
+
if destination.is_symlink() and destination.resolve() == source.resolve():
|
|
92
|
+
print(f"ok {destination}")
|
|
93
|
+
elif destination.exists() or destination.is_symlink():
|
|
94
|
+
print(f"warning: {destination} exists; not touching it", file=sys.stderr)
|
|
95
|
+
failed = True
|
|
96
|
+
elif dry_run:
|
|
97
|
+
print(f"link {destination} -> {source}")
|
|
98
|
+
else:
|
|
99
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
destination.symlink_to(source, target_is_directory=True)
|
|
101
|
+
print(f"linked {destination} -> {source}")
|
|
102
|
+
for item in data["invalid"]:
|
|
103
|
+
print(f"invalid {item['path']}: {item['reason']}", file=sys.stderr)
|
|
104
|
+
failed = True
|
|
105
|
+
return not failed
|