agent-notes 2.0.4__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_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Manage agent memory stored in ~/.claude/agent-memory/."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..config import MEMORY_DIR, BACKUP_DIR, Color
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_memory_config():
|
|
11
|
+
from ..services.state_store import load_state
|
|
12
|
+
from ..config import memory_dir_for_backend
|
|
13
|
+
state = load_state()
|
|
14
|
+
if state is None:
|
|
15
|
+
return "local", MEMORY_DIR
|
|
16
|
+
backend = state.memory.backend
|
|
17
|
+
path = memory_dir_for_backend(backend, state.memory.path)
|
|
18
|
+
return backend, path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def do_vault() -> None:
|
|
22
|
+
"""Show current backend and memory path."""
|
|
23
|
+
backend, path = _load_memory_config()
|
|
24
|
+
if backend == "none":
|
|
25
|
+
print("Memory backend: disabled (none)")
|
|
26
|
+
return
|
|
27
|
+
if backend == "obsidian":
|
|
28
|
+
print(f"Memory backend: obsidian")
|
|
29
|
+
print(f"Vault path: {path}")
|
|
30
|
+
else:
|
|
31
|
+
print(f"Memory backend: local")
|
|
32
|
+
print(f"Memory path: {path}")
|
|
33
|
+
initialized = path is not None and path.exists()
|
|
34
|
+
print(f"Initialized: {'yes' if initialized else 'no — run: agent-notes memory init'}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def do_init() -> None:
|
|
38
|
+
"""Initialize the memory vault — create folder structure and Index.md."""
|
|
39
|
+
backend, path = _load_memory_config()
|
|
40
|
+
if backend == "none":
|
|
41
|
+
print("Memory is disabled. Re-run `agent-notes install` and choose a memory backend.")
|
|
42
|
+
return
|
|
43
|
+
if path is None:
|
|
44
|
+
print("Memory path not configured.")
|
|
45
|
+
return
|
|
46
|
+
if backend == "obsidian":
|
|
47
|
+
from ..services.memory_backend import obsidian_init, OBSIDIAN_CATEGORIES
|
|
48
|
+
obsidian_init(path)
|
|
49
|
+
print(f"{Color.GREEN}Obsidian vault initialised at {path}{Color.NC}")
|
|
50
|
+
print(f" Folders: {', '.join(OBSIDIAN_CATEGORIES)}")
|
|
51
|
+
print(f" Index: {path / 'Index.md'}")
|
|
52
|
+
print(f"\nOpen the folder as a vault in Obsidian to start browsing.")
|
|
53
|
+
else:
|
|
54
|
+
from ..services.memory_backend import local_init
|
|
55
|
+
local_init(path)
|
|
56
|
+
print(f"{Color.GREEN}Memory directory initialised at {path}{Color.NC}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def do_index() -> None:
|
|
60
|
+
"""Regenerate Index.md for the current backend."""
|
|
61
|
+
backend, path = _load_memory_config()
|
|
62
|
+
if backend == "none":
|
|
63
|
+
print("Memory is disabled. Run `agent-notes memory vault` to check configuration.")
|
|
64
|
+
return
|
|
65
|
+
if path is None:
|
|
66
|
+
print("Memory path not configured.")
|
|
67
|
+
return
|
|
68
|
+
if backend == "obsidian":
|
|
69
|
+
from ..services.memory_backend import obsidian_regenerate_index
|
|
70
|
+
obsidian_regenerate_index(path)
|
|
71
|
+
print(f"{Color.GREEN}Index.md regenerated at {path / 'Index.md'}{Color.NC}")
|
|
72
|
+
else:
|
|
73
|
+
from ..services.memory_backend import local_regenerate_index
|
|
74
|
+
local_regenerate_index(path)
|
|
75
|
+
print(f"{Color.GREEN}Index.md regenerated at {path / 'Index.md'}{Color.NC}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def do_add(title: str, body: str, note_type: str = "context", agent: str = "", project: str = "", tags: Optional[list] = None) -> None:
|
|
79
|
+
"""Add a note to memory (obsidian backend only for structured notes)."""
|
|
80
|
+
backend, path = _load_memory_config()
|
|
81
|
+
if backend == "none":
|
|
82
|
+
print("Memory is disabled. Run `agent-notes memory vault` to check configuration.")
|
|
83
|
+
return
|
|
84
|
+
if path is None:
|
|
85
|
+
print("Memory path not configured.")
|
|
86
|
+
return
|
|
87
|
+
if backend == "obsidian":
|
|
88
|
+
from ..services.memory_backend import obsidian_init, obsidian_write_note
|
|
89
|
+
obsidian_init(path)
|
|
90
|
+
note_path = obsidian_write_note(
|
|
91
|
+
path,
|
|
92
|
+
title=title,
|
|
93
|
+
body=body,
|
|
94
|
+
note_type=note_type,
|
|
95
|
+
agent=agent,
|
|
96
|
+
project=project,
|
|
97
|
+
tags=tags or [],
|
|
98
|
+
)
|
|
99
|
+
print(f"{Color.GREEN}Note saved: {note_path}{Color.NC}")
|
|
100
|
+
else:
|
|
101
|
+
print("The `add` subcommand is for the obsidian backend.")
|
|
102
|
+
print("For local backend, write files directly to the agent subdirectory.")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def do_list() -> None:
|
|
106
|
+
"""List all agent memories with sizes."""
|
|
107
|
+
backend, path = _load_memory_config()
|
|
108
|
+
|
|
109
|
+
if backend == "none":
|
|
110
|
+
print("Memory is disabled. Run `agent-notes memory backend` to enable it.")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if backend == "obsidian":
|
|
114
|
+
if path is None or not path.exists():
|
|
115
|
+
print(f"Obsidian vault not found at {path}")
|
|
116
|
+
return
|
|
117
|
+
from ..services.memory_backend import obsidian_list_notes
|
|
118
|
+
notes = obsidian_list_notes(path)
|
|
119
|
+
if not notes:
|
|
120
|
+
print(f"No notes found in vault {path}")
|
|
121
|
+
return
|
|
122
|
+
print(f"Obsidian vault ({path}):")
|
|
123
|
+
print("")
|
|
124
|
+
current_cat = None
|
|
125
|
+
for note in notes:
|
|
126
|
+
if note["category"] != current_cat:
|
|
127
|
+
current_cat = note["category"]
|
|
128
|
+
print(f" {Color.CYAN}{current_cat}{Color.NC}")
|
|
129
|
+
print(f" {note['file']}")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# local backend
|
|
133
|
+
if path is None or not path.exists() or not any(path.iterdir()):
|
|
134
|
+
print(f"No agent memories found in {path}")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
print(f"Agent memories ({path}):")
|
|
138
|
+
print("")
|
|
139
|
+
print(f" {'AGENT':<25} {'SIZE'}")
|
|
140
|
+
print(f" {'-' * 25} {'-' * 4}")
|
|
141
|
+
|
|
142
|
+
for d in path.iterdir():
|
|
143
|
+
if d.is_dir():
|
|
144
|
+
name = d.name
|
|
145
|
+
size = get_directory_size(d)
|
|
146
|
+
size_str = format_size(size)
|
|
147
|
+
print(f" {name:<25} {size_str}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def do_size() -> None:
|
|
151
|
+
"""Show total memory usage."""
|
|
152
|
+
backend, path = _load_memory_config()
|
|
153
|
+
|
|
154
|
+
if backend == "none":
|
|
155
|
+
print("Memory is disabled.")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if path is None or not path.exists():
|
|
159
|
+
print("No agent memories found.")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
total_size = get_directory_size(path)
|
|
163
|
+
size_str = format_size(total_size)
|
|
164
|
+
print("Total memory usage:")
|
|
165
|
+
print(f" {size_str}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def do_show(name: str) -> None:
|
|
169
|
+
"""Show memory contents for one agent (local) or category (obsidian)."""
|
|
170
|
+
backend, path = _load_memory_config()
|
|
171
|
+
|
|
172
|
+
if backend == "none":
|
|
173
|
+
print("Memory is disabled.")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if backend == "obsidian":
|
|
177
|
+
if path is None:
|
|
178
|
+
print("Memory path not configured.")
|
|
179
|
+
return
|
|
180
|
+
cat_dir = path / name
|
|
181
|
+
if not cat_dir.exists():
|
|
182
|
+
print(f"Category '{name}' not found in vault {path}")
|
|
183
|
+
return
|
|
184
|
+
print(f"Notes in category '{name}':")
|
|
185
|
+
print("")
|
|
186
|
+
for f in sorted(cat_dir.glob("*.md")):
|
|
187
|
+
print(f"{Color.CYAN}--- {f.name} ---{Color.NC}")
|
|
188
|
+
try:
|
|
189
|
+
print(f.read_text())
|
|
190
|
+
except (UnicodeDecodeError, OSError):
|
|
191
|
+
print("(binary file or read error)")
|
|
192
|
+
print("")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# local backend
|
|
196
|
+
if path is None:
|
|
197
|
+
path = MEMORY_DIR
|
|
198
|
+
agent_dir = path / name
|
|
199
|
+
if not agent_dir.exists():
|
|
200
|
+
print(f"No memory found for agent '{name}'")
|
|
201
|
+
available = sorted(d.name for d in path.iterdir() if d.is_dir()) if path.exists() else []
|
|
202
|
+
if available:
|
|
203
|
+
print(f"Available: {' '.join(available)}")
|
|
204
|
+
exit(1)
|
|
205
|
+
|
|
206
|
+
print(f"Memory for agent '{name}' ({agent_dir}):")
|
|
207
|
+
print("")
|
|
208
|
+
|
|
209
|
+
for f in agent_dir.iterdir():
|
|
210
|
+
if f.is_file():
|
|
211
|
+
print(f"{Color.CYAN}--- {f.name} ---{Color.NC}")
|
|
212
|
+
try:
|
|
213
|
+
content = f.read_text()
|
|
214
|
+
print(content)
|
|
215
|
+
except (UnicodeDecodeError, OSError):
|
|
216
|
+
print("(binary file or read error)")
|
|
217
|
+
print("")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def do_reset(name: Optional[str] = None) -> None:
|
|
221
|
+
"""Clear agent memory (all or specific agent)."""
|
|
222
|
+
backend, path = _load_memory_config()
|
|
223
|
+
|
|
224
|
+
if backend == "none":
|
|
225
|
+
print("Memory is disabled.")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if path is None:
|
|
229
|
+
path = MEMORY_DIR
|
|
230
|
+
|
|
231
|
+
if name:
|
|
232
|
+
agent_dir = path / name
|
|
233
|
+
if not agent_dir.exists():
|
|
234
|
+
print(f"No memory found for agent '{name}'")
|
|
235
|
+
exit(1)
|
|
236
|
+
|
|
237
|
+
print(f"{Color.YELLOW}This will delete all memory for agent '{name}'.{Color.NC}")
|
|
238
|
+
confirm = input("Continue? [y/N] ")
|
|
239
|
+
if confirm.lower() == 'y':
|
|
240
|
+
shutil.rmtree(agent_dir)
|
|
241
|
+
print(f"{Color.GREEN}Memory for '{name}' cleared.{Color.NC}")
|
|
242
|
+
else:
|
|
243
|
+
print("Cancelled.")
|
|
244
|
+
else:
|
|
245
|
+
if not path.exists() or not any(path.iterdir()):
|
|
246
|
+
print("No agent memories to clear.")
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
print(f"{Color.RED}This will delete ALL agent memories.{Color.NC}")
|
|
250
|
+
print(f"Contents of {path}:")
|
|
251
|
+
for d in path.iterdir():
|
|
252
|
+
if d.is_dir():
|
|
253
|
+
print(f" {d.name}")
|
|
254
|
+
print("")
|
|
255
|
+
|
|
256
|
+
confirm = input("Type 'yes' to confirm: ")
|
|
257
|
+
if confirm == "yes":
|
|
258
|
+
for item in path.iterdir():
|
|
259
|
+
if item.is_dir():
|
|
260
|
+
shutil.rmtree(item)
|
|
261
|
+
else:
|
|
262
|
+
item.unlink()
|
|
263
|
+
print(f"{Color.GREEN}All agent memories cleared.{Color.NC}")
|
|
264
|
+
else:
|
|
265
|
+
print("Cancelled.")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def do_export() -> None:
|
|
269
|
+
"""Copy memories to agent-notes/memory-backup/."""
|
|
270
|
+
backend, path = _load_memory_config()
|
|
271
|
+
|
|
272
|
+
if backend == "none":
|
|
273
|
+
print("Memory is disabled.")
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
if path is None:
|
|
277
|
+
path = MEMORY_DIR
|
|
278
|
+
|
|
279
|
+
if not path.exists() or not any(path.iterdir()):
|
|
280
|
+
print("No agent memories to export.")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
|
|
285
|
+
for item in path.iterdir():
|
|
286
|
+
if item.is_dir():
|
|
287
|
+
dest = BACKUP_DIR / item.name
|
|
288
|
+
if dest.exists():
|
|
289
|
+
shutil.rmtree(dest)
|
|
290
|
+
shutil.copytree(item, dest)
|
|
291
|
+
elif item.is_file():
|
|
292
|
+
shutil.copy2(item, BACKUP_DIR)
|
|
293
|
+
|
|
294
|
+
print(f"{Color.GREEN}Exported to {BACKUP_DIR}{Color.NC}")
|
|
295
|
+
print("Contents:")
|
|
296
|
+
for item in BACKUP_DIR.iterdir():
|
|
297
|
+
print(f" {item.name}")
|
|
298
|
+
print("")
|
|
299
|
+
print(f"{Color.YELLOW}Note: memory-backup/ is in .gitignore — these are personal learnings.{Color.NC}")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def do_import() -> None:
|
|
303
|
+
"""Restore from agent-notes/memory-backup/."""
|
|
304
|
+
backend, path = _load_memory_config()
|
|
305
|
+
|
|
306
|
+
if backend == "none":
|
|
307
|
+
print("Memory is disabled.")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
if path is None:
|
|
311
|
+
path = MEMORY_DIR
|
|
312
|
+
|
|
313
|
+
if not BACKUP_DIR.exists() or not any(BACKUP_DIR.iterdir()):
|
|
314
|
+
print(f"No backup found in {BACKUP_DIR}")
|
|
315
|
+
exit(1)
|
|
316
|
+
|
|
317
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
318
|
+
|
|
319
|
+
for item in BACKUP_DIR.iterdir():
|
|
320
|
+
if item.is_dir():
|
|
321
|
+
dest = path / item.name
|
|
322
|
+
if dest.exists():
|
|
323
|
+
shutil.rmtree(dest)
|
|
324
|
+
shutil.copytree(item, dest)
|
|
325
|
+
elif item.is_file():
|
|
326
|
+
shutil.copy2(item, path)
|
|
327
|
+
|
|
328
|
+
print(f"{Color.GREEN}Imported from {BACKUP_DIR} to {path}{Color.NC}")
|
|
329
|
+
print("Restored agents:")
|
|
330
|
+
for item in path.iterdir():
|
|
331
|
+
if item.is_dir():
|
|
332
|
+
print(f" {item.name}")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def get_directory_size(path: Path) -> int:
|
|
336
|
+
"""Calculate total size of directory in bytes."""
|
|
337
|
+
total = 0
|
|
338
|
+
try:
|
|
339
|
+
for item in path.rglob('*'):
|
|
340
|
+
if item.is_file():
|
|
341
|
+
total += item.stat().st_size
|
|
342
|
+
except (OSError, PermissionError):
|
|
343
|
+
pass
|
|
344
|
+
return total
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def format_size(size_bytes: int) -> str:
|
|
348
|
+
"""Format size in human-readable format."""
|
|
349
|
+
if size_bytes == 0:
|
|
350
|
+
return "0B"
|
|
351
|
+
|
|
352
|
+
original_size = size_bytes
|
|
353
|
+
for unit in ['B', 'K', 'M', 'G', 'T']:
|
|
354
|
+
if original_size < 1024:
|
|
355
|
+
if unit == 'B':
|
|
356
|
+
return f"{original_size}B"
|
|
357
|
+
else:
|
|
358
|
+
return f"{original_size:.1f}{unit}"
|
|
359
|
+
original_size /= 1024
|
|
360
|
+
|
|
361
|
+
return f"{original_size:.1f}P"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def show_help() -> None:
|
|
365
|
+
"""Show memory command help."""
|
|
366
|
+
help_text = """Usage: agent-notes memory [command] [args]
|
|
367
|
+
|
|
368
|
+
Manage agent memory.
|
|
369
|
+
|
|
370
|
+
Commands:
|
|
371
|
+
init Create folder structure and Index.md
|
|
372
|
+
list List all agent memories with sizes (default)
|
|
373
|
+
vault Show current backend and memory path
|
|
374
|
+
index Regenerate Index.md for the current backend
|
|
375
|
+
add <title> <body> Add a note (obsidian backend)
|
|
376
|
+
size Total disk usage
|
|
377
|
+
show <name> Show memory contents for one agent/category
|
|
378
|
+
reset Clear ALL memories (requires confirmation)
|
|
379
|
+
reset <name> Clear one agent's memory
|
|
380
|
+
export Back up memories to agent-notes/memory-backup/
|
|
381
|
+
import Restore from agent-notes/memory-backup/
|
|
382
|
+
|
|
383
|
+
Examples:
|
|
384
|
+
agent-notes memory List all memories
|
|
385
|
+
agent-notes memory vault Show backend configuration
|
|
386
|
+
agent-notes memory index Regenerate Index.md
|
|
387
|
+
agent-notes memory show coder View coder agent's memory
|
|
388
|
+
agent-notes memory reset reviewer Clear reviewer's memory
|
|
389
|
+
agent-notes memory export Back up before cleanup"""
|
|
390
|
+
|
|
391
|
+
print(help_text)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def memory(action: str = "list", name: Optional[str] = None, extra: Optional[list] = None) -> None:
|
|
395
|
+
"""Manage agent memory."""
|
|
396
|
+
if action == "list":
|
|
397
|
+
do_list()
|
|
398
|
+
elif action == "init":
|
|
399
|
+
do_init()
|
|
400
|
+
elif action == "vault":
|
|
401
|
+
do_vault()
|
|
402
|
+
elif action == "index":
|
|
403
|
+
do_index()
|
|
404
|
+
elif action == "add":
|
|
405
|
+
# name is title, extra[0] is body
|
|
406
|
+
if not name:
|
|
407
|
+
print("Error: add requires a title.")
|
|
408
|
+
exit(1)
|
|
409
|
+
body = extra[0] if extra else ""
|
|
410
|
+
note_type = extra[1] if extra and len(extra) > 1 else "context"
|
|
411
|
+
agent = extra[2] if extra and len(extra) > 2 else ""
|
|
412
|
+
project = extra[3] if extra and len(extra) > 3 else ""
|
|
413
|
+
do_add(name, body, note_type=note_type, agent=agent, project=project)
|
|
414
|
+
elif action == "size":
|
|
415
|
+
do_size()
|
|
416
|
+
elif action == "show":
|
|
417
|
+
if not name:
|
|
418
|
+
print("Error: show requires an agent name.")
|
|
419
|
+
exit(1)
|
|
420
|
+
do_show(name)
|
|
421
|
+
elif action == "reset":
|
|
422
|
+
do_reset(name)
|
|
423
|
+
elif action == "export":
|
|
424
|
+
do_export()
|
|
425
|
+
elif action == "import":
|
|
426
|
+
do_import()
|
|
427
|
+
else:
|
|
428
|
+
print(f"Unknown command: {action}")
|
|
429
|
+
show_help()
|
|
430
|
+
exit(1)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Regenerate agent files from current state.json."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..config import Color
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def regenerate(scope: Optional[str] = None, cli: Optional[str] = None, local: bool = False, project_path: Optional[Path] = None) -> None:
|
|
11
|
+
"""Rebuild agent/skill/config files from current state.json.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
scope: 'global' or 'local' (auto-detect if omitted)
|
|
15
|
+
cli: Target CLI (regenerate all if omitted)
|
|
16
|
+
local: Shortcut for scope='local'
|
|
17
|
+
project_path: Explicit project path for local scope
|
|
18
|
+
"""
|
|
19
|
+
from .. import state as state_mod
|
|
20
|
+
from ..state import get_scope
|
|
21
|
+
from ..config import DATA_DIR
|
|
22
|
+
from .build import generate_agent_files
|
|
23
|
+
from ..registries.cli_registry import load_registry
|
|
24
|
+
from .. import install_state
|
|
25
|
+
from ..config import PKG_DIR
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
28
|
+
# Load state
|
|
29
|
+
current_state = state_mod.load()
|
|
30
|
+
if current_state is None:
|
|
31
|
+
print("No state.json found. Nothing to regenerate.")
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
# Determine scope
|
|
35
|
+
if local:
|
|
36
|
+
scope = 'local'
|
|
37
|
+
elif scope is None:
|
|
38
|
+
# Auto-detect
|
|
39
|
+
if current_state.global_install:
|
|
40
|
+
scope = 'global'
|
|
41
|
+
elif current_state.local_installs:
|
|
42
|
+
scope = 'local'
|
|
43
|
+
else:
|
|
44
|
+
print("No installation found in state")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
if project_path is None and scope == 'local':
|
|
48
|
+
project_path = Path.cwd()
|
|
49
|
+
|
|
50
|
+
scope_state = get_scope(current_state, scope, project_path)
|
|
51
|
+
if scope_state is None:
|
|
52
|
+
print(f"No {scope} installation found")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
# Determine target CLIs
|
|
56
|
+
if cli:
|
|
57
|
+
if cli not in scope_state.clis:
|
|
58
|
+
print(f"CLI '{cli}' not in {scope} installation")
|
|
59
|
+
print(f"Installed: {', '.join(scope_state.clis.keys())}")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
target_clis = [cli]
|
|
62
|
+
else:
|
|
63
|
+
target_clis = list(scope_state.clis.keys())
|
|
64
|
+
|
|
65
|
+
# Load agent config
|
|
66
|
+
agents_yaml = DATA_DIR / "agents" / "agents.yaml"
|
|
67
|
+
if not agents_yaml.exists():
|
|
68
|
+
print(f"Error: {agents_yaml} not found")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
with open(agents_yaml) as f:
|
|
72
|
+
agents_data = yaml.safe_load(f)
|
|
73
|
+
|
|
74
|
+
agents_config = agents_data.get('agents', {})
|
|
75
|
+
|
|
76
|
+
# Regenerate per CLI
|
|
77
|
+
registry = load_registry()
|
|
78
|
+
|
|
79
|
+
print(f"Regenerating {scope} installation...")
|
|
80
|
+
|
|
81
|
+
total_files = 0
|
|
82
|
+
|
|
83
|
+
for cli_name in target_clis:
|
|
84
|
+
backend = registry.get(cli_name)
|
|
85
|
+
print(f"\n{backend.label}:")
|
|
86
|
+
|
|
87
|
+
# Generate agents
|
|
88
|
+
if backend.supports("agents"):
|
|
89
|
+
files = generate_agent_files(
|
|
90
|
+
agents_config,
|
|
91
|
+
{}, # empty tiers - state-driven only
|
|
92
|
+
state=current_state,
|
|
93
|
+
scope=scope,
|
|
94
|
+
project_path=project_path
|
|
95
|
+
)
|
|
96
|
+
# Count files for this CLI
|
|
97
|
+
try:
|
|
98
|
+
cli_files = [f for f in files if backend.name in str(f)]
|
|
99
|
+
if cli_files:
|
|
100
|
+
print(f" ✓ {len(cli_files)} agents regenerated")
|
|
101
|
+
total_files += len(cli_files)
|
|
102
|
+
except (TypeError, AttributeError):
|
|
103
|
+
# Handle case where files is mocked or has unexpected structure
|
|
104
|
+
print(f" ✓ agents regenerated")
|
|
105
|
+
|
|
106
|
+
# Regenerate other components as needed
|
|
107
|
+
from ..services import installer
|
|
108
|
+
|
|
109
|
+
# Regenerate rules for backends that support them
|
|
110
|
+
if backend.supports("rules"):
|
|
111
|
+
installer.install_component_for_backend(backend, "rules", scope, scope_state.mode == "copy")
|
|
112
|
+
print(f" ✓ rules regenerated for {backend.label}")
|
|
113
|
+
|
|
114
|
+
# Regenerate global config files
|
|
115
|
+
if backend.layout.get("config"):
|
|
116
|
+
installer.install_component_for_backend(backend, "config", scope, scope_state.mode == "copy")
|
|
117
|
+
print(f" ✓ config regenerated for {backend.label}")
|
|
118
|
+
|
|
119
|
+
# Regenerate skills (static files, just ensure they're synced)
|
|
120
|
+
if backend.supports("skills"):
|
|
121
|
+
installer.install_component_for_backend(backend, "skills", scope, scope_state.mode == "copy")
|
|
122
|
+
print(f" ✓ skills regenerated for {backend.label}")
|
|
123
|
+
|
|
124
|
+
# Update installed manifest to reflect current state
|
|
125
|
+
try:
|
|
126
|
+
# Get current role_models from state to preserve them
|
|
127
|
+
existing_role_models = {}
|
|
128
|
+
for cli_name, backend_state in scope_state.clis.items():
|
|
129
|
+
existing_role_models[cli_name] = backend_state.role_models
|
|
130
|
+
|
|
131
|
+
# Build new state with current files but preserve role_models
|
|
132
|
+
new_state = install_state.build_install_state(
|
|
133
|
+
mode=scope_state.mode,
|
|
134
|
+
scope=scope,
|
|
135
|
+
repo_root=PKG_DIR.parent,
|
|
136
|
+
project_path=project_path,
|
|
137
|
+
role_models=existing_role_models
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Merge back into current_state preserving other scopes
|
|
141
|
+
if scope == 'global':
|
|
142
|
+
current_state.global_install = new_state.global_install
|
|
143
|
+
else:
|
|
144
|
+
# Update the specific local install
|
|
145
|
+
if project_path:
|
|
146
|
+
current_state.local_installs[str(project_path.resolve())] = new_state.local_installs[str(project_path.resolve())]
|
|
147
|
+
|
|
148
|
+
install_state.record_install_state(current_state)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"{Color.YELLOW}Warning: failed to update install state: {e}{Color.NC}")
|
|
151
|
+
|
|
152
|
+
print(f"\n{Color.GREEN}Regenerated {total_files} files.{Color.NC}")
|