memoryledger 0.1.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.
- memoryledger/__init__.py +8 -0
- memoryledger/_version.py +24 -0
- memoryledger/adopt.py +173 -0
- memoryledger/cli.py +700 -0
- memoryledger/errors.py +18 -0
- memoryledger/evidence_scan.py +218 -0
- memoryledger/guardrails.py +157 -0
- memoryledger/intake.py +111 -0
- memoryledger/launcher.py +7 -0
- memoryledger/models.py +172 -0
- memoryledger/py.typed +0 -0
- memoryledger/render.py +313 -0
- memoryledger/review.py +19 -0
- memoryledger/run_import.py +136 -0
- memoryledger/storage.py +463 -0
- memoryledger/templates.py +161 -0
- memoryledger-0.1.0.dist-info/METADATA +67 -0
- memoryledger-0.1.0.dist-info/RECORD +24 -0
- memoryledger-0.1.0.dist-info/WHEEL +5 -0
- memoryledger-0.1.0.dist-info/entry_points.txt +3 -0
- memoryledger-0.1.0.dist-info/licenses/LICENSE +201 -0
- memoryledger-0.1.0.dist-info/scm_file_list.json +147 -0
- memoryledger-0.1.0.dist-info/scm_version.json +8 -0
- memoryledger-0.1.0.dist-info/top_level.txt +1 -0
memoryledger/cli.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .adopt import adopt, parse_markdown
|
|
12
|
+
from .errors import MemoryledgerError
|
|
13
|
+
from .evidence_scan import apply as apply_scan
|
|
14
|
+
from .evidence_scan import scan as scan_evidence
|
|
15
|
+
from .guardrails import validate_memory, validate_scope_path
|
|
16
|
+
from .intake import import_run_html as intake_run_html
|
|
17
|
+
from .intake import import_text as intake_text
|
|
18
|
+
from .models import EVIDENCE_KINDS, KINDS, RENDER_TARGETS, SCOPES, Config, EvidenceRef
|
|
19
|
+
from .render import export as export_rendered
|
|
20
|
+
from .render import render_all, write_rendered
|
|
21
|
+
from .review import transition
|
|
22
|
+
from .storage import Store, find_config, init_workspace, load_config
|
|
23
|
+
from .templates import (
|
|
24
|
+
apply_template,
|
|
25
|
+
find_template,
|
|
26
|
+
load_global_config,
|
|
27
|
+
remove_template,
|
|
28
|
+
template_content,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(no_args_is_help=True)
|
|
32
|
+
memory_app = typer.Typer(no_args_is_help=True)
|
|
33
|
+
evidence_app = typer.Typer(no_args_is_help=True)
|
|
34
|
+
review_app = typer.Typer(no_args_is_help=True)
|
|
35
|
+
import_app = typer.Typer(no_args_is_help=True)
|
|
36
|
+
agents_app = typer.Typer(no_args_is_help=True)
|
|
37
|
+
templates_app = typer.Typer(no_args_is_help=True)
|
|
38
|
+
scan_app = typer.Typer(no_args_is_help=True)
|
|
39
|
+
app.add_typer(memory_app, name="memory")
|
|
40
|
+
memory_app.add_typer(evidence_app, name="evidence")
|
|
41
|
+
app.add_typer(review_app, name="review")
|
|
42
|
+
app.add_typer(import_app, name="import")
|
|
43
|
+
app.add_typer(agents_app, name="agents")
|
|
44
|
+
app.add_typer(templates_app, name="templates")
|
|
45
|
+
app.add_typer(scan_app, name="evidence")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _json(data: object) -> None:
|
|
49
|
+
typer.echo(json.dumps(data, indent=2, sort_keys=True))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load() -> tuple[Config, Store]:
|
|
53
|
+
config = load_config()
|
|
54
|
+
return config, Store(config)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _read_input(text: str | None, file: Path | None, stdin: bool) -> str:
|
|
58
|
+
choices = sum([text is not None, file is not None, stdin])
|
|
59
|
+
if choices != 1:
|
|
60
|
+
raise MemoryledgerError(
|
|
61
|
+
"INPUT_REQUIRED", "Provide exactly one of --text, --file, or --stdin"
|
|
62
|
+
)
|
|
63
|
+
if text is not None:
|
|
64
|
+
return text
|
|
65
|
+
if file is not None:
|
|
66
|
+
return file.read_text()
|
|
67
|
+
return sys.stdin.read()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _handle_error(exc: Exception) -> None:
|
|
71
|
+
if isinstance(exc, MemoryledgerError):
|
|
72
|
+
typer.echo(f"error: {exc.code}: {exc.message}", err=True)
|
|
73
|
+
raise typer.Exit(1) from exc
|
|
74
|
+
raise exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.callback()
|
|
78
|
+
def main(
|
|
79
|
+
version: bool = typer.Option(False, "--version", help="Show version and exit."),
|
|
80
|
+
) -> None:
|
|
81
|
+
if version:
|
|
82
|
+
typer.echo(__version__)
|
|
83
|
+
raise typer.Exit()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def init(
|
|
88
|
+
project_name: str | None = None,
|
|
89
|
+
memoryledger_dir: str = ".memoryledger",
|
|
90
|
+
hidden_config: bool = False,
|
|
91
|
+
) -> None:
|
|
92
|
+
try:
|
|
93
|
+
config = init_workspace(project_name, memoryledger_dir, hidden_config)
|
|
94
|
+
typer.echo(f"initialized {config.config_path}")
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
_handle_error(exc)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command()
|
|
100
|
+
def status(
|
|
101
|
+
check: bool = False, json_output: bool = typer.Option(False, "--json")
|
|
102
|
+
) -> None:
|
|
103
|
+
try:
|
|
104
|
+
config_path = find_config()
|
|
105
|
+
if config_path is None:
|
|
106
|
+
data = {"ok": False, "configured": False}
|
|
107
|
+
if json_output:
|
|
108
|
+
_json(data)
|
|
109
|
+
else:
|
|
110
|
+
typer.echo("No memoryledger config found.")
|
|
111
|
+
if check:
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
return
|
|
114
|
+
config, store = _load()
|
|
115
|
+
memories = store.all_memories()
|
|
116
|
+
status_data: dict[str, object] = {
|
|
117
|
+
"ok": True,
|
|
118
|
+
"configured": True,
|
|
119
|
+
"config": str(config.config_path),
|
|
120
|
+
"storage": str(config.storage_dir),
|
|
121
|
+
"memories": len(memories),
|
|
122
|
+
}
|
|
123
|
+
if json_output:
|
|
124
|
+
_json(status_data)
|
|
125
|
+
else:
|
|
126
|
+
typer.echo(f"config: {config.config_path}")
|
|
127
|
+
typer.echo(f"storage: {config.storage_dir}")
|
|
128
|
+
typer.echo(f"memories: {len(memories)}")
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
_handle_error(exc)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command()
|
|
134
|
+
def doctor(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
135
|
+
try:
|
|
136
|
+
config, _store = _load()
|
|
137
|
+
issues: list[str] = []
|
|
138
|
+
if not config.storage_dir.exists():
|
|
139
|
+
issues.append("storage directory missing")
|
|
140
|
+
data = {"ok": not issues, "issues": issues}
|
|
141
|
+
if json_output:
|
|
142
|
+
_json(data)
|
|
143
|
+
else:
|
|
144
|
+
typer.echo("ok" if not issues else "issues: " + ", ".join(issues))
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
_handle_error(exc)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command()
|
|
150
|
+
def info(
|
|
151
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
152
|
+
paths_only: bool = False,
|
|
153
|
+
no_content: bool = False,
|
|
154
|
+
) -> None:
|
|
155
|
+
try:
|
|
156
|
+
config, _store = _load()
|
|
157
|
+
data = {
|
|
158
|
+
"config": str(config.config_path),
|
|
159
|
+
"root": str(config.root),
|
|
160
|
+
"storage": str(config.storage_dir),
|
|
161
|
+
}
|
|
162
|
+
if json_output:
|
|
163
|
+
_json(data)
|
|
164
|
+
else:
|
|
165
|
+
for value in data.values() if paths_only else data.items():
|
|
166
|
+
typer.echo(value if paths_only else f"{value[0]}: {value[1]}")
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
_handle_error(exc)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@memory_app.command("create")
|
|
172
|
+
def memory_create(
|
|
173
|
+
kind: str = typer.Option(..., "--kind"),
|
|
174
|
+
title: str = typer.Option(..., "--title"),
|
|
175
|
+
text: str | None = typer.Option(None, "--text"),
|
|
176
|
+
file: Path | None = typer.Option(None, "--file"),
|
|
177
|
+
stdin: bool = typer.Option(False, "--stdin"),
|
|
178
|
+
evidence: str = typer.Option("", "--evidence"),
|
|
179
|
+
scope: str = typer.Option("global", "--scope"),
|
|
180
|
+
scope_path: str = typer.Option("", "--scope-path"),
|
|
181
|
+
render_target: str = typer.Option("root_agents", "--render-target"),
|
|
182
|
+
section: str = typer.Option("", "--section"),
|
|
183
|
+
) -> None:
|
|
184
|
+
try:
|
|
185
|
+
if (
|
|
186
|
+
kind not in KINDS
|
|
187
|
+
or scope not in SCOPES
|
|
188
|
+
or render_target not in RENDER_TARGETS
|
|
189
|
+
):
|
|
190
|
+
raise MemoryledgerError(
|
|
191
|
+
"INVALID_ARGUMENT", "invalid kind, scope, or render target"
|
|
192
|
+
)
|
|
193
|
+
config, store = _load()
|
|
194
|
+
validate_scope_path(config.root, scope_path)
|
|
195
|
+
content = _read_input(text, file, stdin)
|
|
196
|
+
memory = store.create(
|
|
197
|
+
kind,
|
|
198
|
+
title,
|
|
199
|
+
content,
|
|
200
|
+
evidence,
|
|
201
|
+
scope,
|
|
202
|
+
scope_path,
|
|
203
|
+
render_target,
|
|
204
|
+
section=section,
|
|
205
|
+
)
|
|
206
|
+
typer.echo(memory.id)
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
_handle_error(exc)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@memory_app.command("list")
|
|
212
|
+
def memory_list(
|
|
213
|
+
kind: str | None = typer.Option(None, "--kind"),
|
|
214
|
+
status: str | None = typer.Option(None, "--status"),
|
|
215
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
216
|
+
) -> None:
|
|
217
|
+
try:
|
|
218
|
+
_config, store = _load()
|
|
219
|
+
items = [
|
|
220
|
+
m
|
|
221
|
+
for m in store.all_memories()
|
|
222
|
+
if (kind is None or m.kind == kind)
|
|
223
|
+
and (status is None or m.status == status)
|
|
224
|
+
]
|
|
225
|
+
if json_output:
|
|
226
|
+
_json({"memories": [m.to_dict() for m in items]})
|
|
227
|
+
else:
|
|
228
|
+
for m in items:
|
|
229
|
+
typer.echo(f"{m.id} {m.status} {m.kind} {m.title}")
|
|
230
|
+
except Exception as exc:
|
|
231
|
+
_handle_error(exc)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@memory_app.command("show")
|
|
235
|
+
def memory_show(
|
|
236
|
+
memory_id: str,
|
|
237
|
+
content: bool = False,
|
|
238
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
239
|
+
) -> None:
|
|
240
|
+
try:
|
|
241
|
+
_config, store = _load()
|
|
242
|
+
memory = store.get(memory_id)
|
|
243
|
+
body = store.read_content(memory_id)
|
|
244
|
+
if json_output:
|
|
245
|
+
data = memory.to_dict()
|
|
246
|
+
if content:
|
|
247
|
+
data["content"] = body
|
|
248
|
+
_json(data)
|
|
249
|
+
else:
|
|
250
|
+
typer.echo(f"{memory.id} {memory.status} {memory.kind} {memory.title}")
|
|
251
|
+
if content:
|
|
252
|
+
typer.echo(body.rstrip())
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
_handle_error(exc)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@memory_app.command("status")
|
|
258
|
+
def memory_status(
|
|
259
|
+
memory_id: str, status: str, reason: str = typer.Option(..., "--reason")
|
|
260
|
+
) -> None:
|
|
261
|
+
try:
|
|
262
|
+
_config, store = _load()
|
|
263
|
+
memory = transition(store, memory_id, status, reason)
|
|
264
|
+
typer.echo(f"{memory.id} {memory.status}")
|
|
265
|
+
except Exception as exc:
|
|
266
|
+
_handle_error(exc)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@memory_app.command("edit")
|
|
270
|
+
def memory_edit(
|
|
271
|
+
memory_id: str,
|
|
272
|
+
reason: str = typer.Option(..., "--reason"),
|
|
273
|
+
text: str | None = typer.Option(None, "--text"),
|
|
274
|
+
file: Path | None = typer.Option(None, "--file"),
|
|
275
|
+
stdin: bool = typer.Option(False, "--stdin"),
|
|
276
|
+
section: str | None = typer.Option(None, "--section"),
|
|
277
|
+
) -> None:
|
|
278
|
+
try:
|
|
279
|
+
_config, store = _load()
|
|
280
|
+
memory = store.update_content(
|
|
281
|
+
memory_id, _read_input(text, file, stdin), reason, section=section
|
|
282
|
+
)
|
|
283
|
+
typer.echo(f"{memory.id} v{memory.version:04d}")
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
_handle_error(exc)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@memory_app.command("append")
|
|
289
|
+
def memory_append(
|
|
290
|
+
memory_id: str,
|
|
291
|
+
reason: str = typer.Option(..., "--reason"),
|
|
292
|
+
text: str | None = typer.Option(None, "--text"),
|
|
293
|
+
file: Path | None = typer.Option(None, "--file"),
|
|
294
|
+
stdin: bool = typer.Option(False, "--stdin"),
|
|
295
|
+
) -> None:
|
|
296
|
+
try:
|
|
297
|
+
_config, store = _load()
|
|
298
|
+
memory = store.update_content(
|
|
299
|
+
memory_id, _read_input(text, file, stdin), reason, append=True
|
|
300
|
+
)
|
|
301
|
+
typer.echo(f"{memory.id} v{memory.version:04d}")
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
_handle_error(exc)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@memory_app.command("validate")
|
|
307
|
+
def memory_validate(
|
|
308
|
+
memory_id: str, json_output: bool = typer.Option(False, "--json")
|
|
309
|
+
) -> None:
|
|
310
|
+
try:
|
|
311
|
+
_config, store = _load()
|
|
312
|
+
memory = store.get(memory_id)
|
|
313
|
+
validate_memory(
|
|
314
|
+
memory,
|
|
315
|
+
store.read_content(memory_id),
|
|
316
|
+
store.read_evidence(memory_id),
|
|
317
|
+
store.config.root,
|
|
318
|
+
)
|
|
319
|
+
if json_output:
|
|
320
|
+
_json({"ok": True, "memory_id": memory_id})
|
|
321
|
+
else:
|
|
322
|
+
typer.echo("ok")
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
_handle_error(exc)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@memory_app.command("versions")
|
|
328
|
+
def memory_versions(
|
|
329
|
+
memory_id: str, json_output: bool = typer.Option(False, "--json")
|
|
330
|
+
) -> None:
|
|
331
|
+
try:
|
|
332
|
+
_config, store = _load()
|
|
333
|
+
versions = sorted(
|
|
334
|
+
p.stem for p in (store.memory_dir(memory_id) / "versions").glob("v*.yaml")
|
|
335
|
+
)
|
|
336
|
+
if json_output:
|
|
337
|
+
_json({"versions": versions})
|
|
338
|
+
else:
|
|
339
|
+
typer.echo("\n".join(versions))
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
_handle_error(exc)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@memory_app.command("diff")
|
|
345
|
+
def memory_diff(
|
|
346
|
+
memory_id: str,
|
|
347
|
+
from_version: str = typer.Option(..., "--from"),
|
|
348
|
+
to: str = typer.Option(..., "--to"),
|
|
349
|
+
) -> None:
|
|
350
|
+
try:
|
|
351
|
+
_config, store = _load()
|
|
352
|
+
base = store.memory_dir(memory_id) / "versions"
|
|
353
|
+
a = (base / f"{from_version}.md").read_text().splitlines()
|
|
354
|
+
b = (base / f"{to}.md").read_text().splitlines()
|
|
355
|
+
typer.echo("\n".join(difflib.unified_diff(a, b, from_version, to)))
|
|
356
|
+
except Exception as exc:
|
|
357
|
+
_handle_error(exc)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@evidence_app.command("list")
|
|
361
|
+
def evidence_list(
|
|
362
|
+
memory_id: str, json_output: bool = typer.Option(False, "--json")
|
|
363
|
+
) -> None:
|
|
364
|
+
try:
|
|
365
|
+
_config, store = _load()
|
|
366
|
+
refs = [ref.to_dict() for ref in store.get(memory_id).evidence_refs]
|
|
367
|
+
if json_output:
|
|
368
|
+
_json({"memory_id": memory_id, "evidence": refs})
|
|
369
|
+
else:
|
|
370
|
+
for ref in refs:
|
|
371
|
+
typer.echo(f"{ref['kind']} {ref['title']} {ref['uri']}")
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
_handle_error(exc)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@evidence_app.command("add")
|
|
377
|
+
def evidence_add(
|
|
378
|
+
memory_id: str,
|
|
379
|
+
kind: str = typer.Option(..., "--kind"),
|
|
380
|
+
title: str = typer.Option(..., "--title"),
|
|
381
|
+
uri: str = typer.Option(..., "--uri"),
|
|
382
|
+
reason: str = typer.Option(..., "--reason"),
|
|
383
|
+
excerpt: str = typer.Option("", "--excerpt"),
|
|
384
|
+
line_start: int | None = typer.Option(None, "--line-start"),
|
|
385
|
+
line_end: int | None = typer.Option(None, "--line-end"),
|
|
386
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
387
|
+
) -> None:
|
|
388
|
+
try:
|
|
389
|
+
if kind not in EVIDENCE_KINDS:
|
|
390
|
+
raise MemoryledgerError("INVALID_EVIDENCE_KIND", kind)
|
|
391
|
+
_config, store = _load()
|
|
392
|
+
memory = store.add_evidence(
|
|
393
|
+
memory_id,
|
|
394
|
+
EvidenceRef(
|
|
395
|
+
kind=kind,
|
|
396
|
+
title=title,
|
|
397
|
+
uri=uri,
|
|
398
|
+
excerpt=excerpt,
|
|
399
|
+
line_start=line_start,
|
|
400
|
+
line_end=line_end,
|
|
401
|
+
),
|
|
402
|
+
reason,
|
|
403
|
+
)
|
|
404
|
+
data = {"memory_id": memory.id, "version": memory.version}
|
|
405
|
+
_json(data) if json_output else typer.echo(f"{memory.id} v{memory.version:04d}")
|
|
406
|
+
except Exception as exc:
|
|
407
|
+
_handle_error(exc)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@review_app.command("list")
|
|
411
|
+
def review_list(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
412
|
+
return memory_list(status="candidate", json_output=json_output)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@review_app.command("accept")
|
|
416
|
+
def review_accept(memory_id: str, reason: str = typer.Option(..., "--reason")) -> None:
|
|
417
|
+
memory_status(memory_id, "accepted", reason)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@review_app.command("reject")
|
|
421
|
+
def review_reject(memory_id: str, reason: str = typer.Option(..., "--reason")) -> None:
|
|
422
|
+
memory_status(memory_id, "rejected", reason)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@review_app.command("archive")
|
|
426
|
+
def review_archive(memory_id: str, reason: str = typer.Option(..., "--reason")) -> None:
|
|
427
|
+
memory_status(memory_id, "archived", reason)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@app.command()
|
|
431
|
+
def render(
|
|
432
|
+
out: Path | None = None,
|
|
433
|
+
print_output: bool = typer.Option(False, "--print"),
|
|
434
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
435
|
+
) -> None:
|
|
436
|
+
try:
|
|
437
|
+
config = load_config()
|
|
438
|
+
result = render_all(config)
|
|
439
|
+
written = write_rendered(config, result, out)
|
|
440
|
+
if print_output:
|
|
441
|
+
typer.echo(result.root_text, nl=False)
|
|
442
|
+
if json_output:
|
|
443
|
+
_json(
|
|
444
|
+
{
|
|
445
|
+
"root": str(written[0]),
|
|
446
|
+
"linked_docs": sorted(result.linked_docs),
|
|
447
|
+
"nested_docs": sorted(result.nested_docs),
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
except Exception as exc:
|
|
451
|
+
_handle_error(exc)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@app.command()
|
|
455
|
+
def export(
|
|
456
|
+
out: Path | None = None,
|
|
457
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
458
|
+
backup: bool = False,
|
|
459
|
+
include_nested: bool = False,
|
|
460
|
+
) -> None:
|
|
461
|
+
try:
|
|
462
|
+
config = load_config()
|
|
463
|
+
result = render_all(config)
|
|
464
|
+
written = export_rendered(config, result, out, backup, include_nested)
|
|
465
|
+
if json_output:
|
|
466
|
+
_json({"written": [str(p) for p in written]})
|
|
467
|
+
else:
|
|
468
|
+
for path in written:
|
|
469
|
+
typer.echo(path)
|
|
470
|
+
except Exception as exc:
|
|
471
|
+
_handle_error(exc)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@agents_app.command("plan")
|
|
475
|
+
def agents_plan(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
476
|
+
data = {"commands": ["memoryledger render", "memoryledger export"]}
|
|
477
|
+
_json(data) if json_output else typer.echo(
|
|
478
|
+
"memoryledger render\nmemoryledger export"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@agents_app.command("render")
|
|
483
|
+
def agents_render(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
484
|
+
render(json_output=json_output)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@agents_app.command("export")
|
|
488
|
+
def agents_export(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
489
|
+
export(json_output=json_output)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@agents_app.command("adopt")
|
|
493
|
+
def agents_adopt(
|
|
494
|
+
target: Path = typer.Argument(Path("AGENTS.md")),
|
|
495
|
+
apply: bool = typer.Option(False, "--apply"),
|
|
496
|
+
backup: bool = typer.Option(False, "--backup"),
|
|
497
|
+
accept: bool = typer.Option(False, "--accept"),
|
|
498
|
+
reason: str = typer.Option("", "--reason"),
|
|
499
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
500
|
+
) -> None:
|
|
501
|
+
try:
|
|
502
|
+
config, store = _load()
|
|
503
|
+
path = target if target.is_absolute() else config.root / target
|
|
504
|
+
try:
|
|
505
|
+
path.resolve().relative_to(config.root.resolve())
|
|
506
|
+
except ValueError as exc:
|
|
507
|
+
raise MemoryledgerError(
|
|
508
|
+
"INVALID_ADOPTION_PATH", "adoption target must be in the workspace"
|
|
509
|
+
) from exc
|
|
510
|
+
if accept and not reason.strip():
|
|
511
|
+
raise MemoryledgerError(
|
|
512
|
+
"MISSING_REASON", "--accept requires a review reason"
|
|
513
|
+
)
|
|
514
|
+
digest, proposals = parse_markdown(path, config.root)
|
|
515
|
+
if not apply:
|
|
516
|
+
data = {
|
|
517
|
+
"target": str(path),
|
|
518
|
+
"source_hash": digest,
|
|
519
|
+
"proposals": [proposal.to_dict() for proposal in proposals],
|
|
520
|
+
"mutated": False,
|
|
521
|
+
}
|
|
522
|
+
else:
|
|
523
|
+
ids, backup_path = adopt(
|
|
524
|
+
store, path, backup=backup, accept=accept, reason=reason
|
|
525
|
+
)
|
|
526
|
+
data = {
|
|
527
|
+
"target": str(path),
|
|
528
|
+
"candidates": ids,
|
|
529
|
+
"backup": str(backup_path),
|
|
530
|
+
"mutated": True,
|
|
531
|
+
}
|
|
532
|
+
if json_output:
|
|
533
|
+
_json(data)
|
|
534
|
+
elif apply:
|
|
535
|
+
typer.echo("\n".join(ids))
|
|
536
|
+
else:
|
|
537
|
+
for proposal in proposals:
|
|
538
|
+
typer.echo(f"{proposal.kind} {proposal.title}")
|
|
539
|
+
except Exception as exc:
|
|
540
|
+
_handle_error(exc)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@templates_app.command("list")
|
|
544
|
+
def templates_list(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
545
|
+
try:
|
|
546
|
+
templates = load_global_config().templates
|
|
547
|
+
data = [
|
|
548
|
+
{"id": item.id, "version": item.version, "title": item.title}
|
|
549
|
+
for item in templates
|
|
550
|
+
]
|
|
551
|
+
if json_output:
|
|
552
|
+
_json({"templates": data})
|
|
553
|
+
else:
|
|
554
|
+
for item in data:
|
|
555
|
+
typer.echo(f"{item['id']} {item['version']} {item['title']}")
|
|
556
|
+
except Exception as exc:
|
|
557
|
+
_handle_error(exc)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@templates_app.command("show")
|
|
561
|
+
def templates_show(
|
|
562
|
+
template_id: str, json_output: bool = typer.Option(False, "--json")
|
|
563
|
+
) -> None:
|
|
564
|
+
try:
|
|
565
|
+
template = find_template(load_global_config(), template_id)
|
|
566
|
+
data = {
|
|
567
|
+
"id": template.id,
|
|
568
|
+
"version": template.version,
|
|
569
|
+
"title": template.title,
|
|
570
|
+
"kind": template.kind,
|
|
571
|
+
"content": template_content(template),
|
|
572
|
+
}
|
|
573
|
+
_json(data) if json_output else typer.echo(data["content"], nl=False)
|
|
574
|
+
except Exception as exc:
|
|
575
|
+
_handle_error(exc)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _template_apply(
|
|
579
|
+
template_id: str,
|
|
580
|
+
json_output: bool,
|
|
581
|
+
*,
|
|
582
|
+
accept: bool = False,
|
|
583
|
+
reason: str = "",
|
|
584
|
+
) -> None:
|
|
585
|
+
config, store = _load()
|
|
586
|
+
if config.template_policy.ids and template_id not in config.template_policy.ids:
|
|
587
|
+
raise MemoryledgerError("TEMPLATE_NOT_ENABLED", template_id)
|
|
588
|
+
if accept and not reason.strip():
|
|
589
|
+
raise MemoryledgerError("MISSING_REASON", "--accept requires a review reason")
|
|
590
|
+
template = find_template(load_global_config(), template_id)
|
|
591
|
+
action, memory = apply_template(store, template)
|
|
592
|
+
if accept and memory.status != "accepted":
|
|
593
|
+
memory = transition(store, memory.id, "accepted", reason)
|
|
594
|
+
data = {"action": action, "memory_id": memory.id, "status": memory.status}
|
|
595
|
+
_json(data) if json_output else typer.echo(f"{action} {memory.id}")
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
@templates_app.command("apply")
|
|
599
|
+
def templates_apply(
|
|
600
|
+
template_id: str,
|
|
601
|
+
accept: bool = typer.Option(False, "--accept"),
|
|
602
|
+
reason: str = typer.Option("", "--reason"),
|
|
603
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
604
|
+
) -> None:
|
|
605
|
+
try:
|
|
606
|
+
_template_apply(template_id, json_output, accept=accept, reason=reason)
|
|
607
|
+
except Exception as exc:
|
|
608
|
+
_handle_error(exc)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@templates_app.command("sync")
|
|
612
|
+
def templates_sync(
|
|
613
|
+
template_id: str,
|
|
614
|
+
accept: bool = typer.Option(False, "--accept"),
|
|
615
|
+
reason: str = typer.Option("", "--reason"),
|
|
616
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
617
|
+
) -> None:
|
|
618
|
+
try:
|
|
619
|
+
_template_apply(template_id, json_output, accept=accept, reason=reason)
|
|
620
|
+
except Exception as exc:
|
|
621
|
+
_handle_error(exc)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@templates_app.command("remove")
|
|
625
|
+
def templates_remove(
|
|
626
|
+
template_id: str,
|
|
627
|
+
reason: str = typer.Option(..., "--reason"),
|
|
628
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
629
|
+
) -> None:
|
|
630
|
+
try:
|
|
631
|
+
_config, store = _load()
|
|
632
|
+
memory = remove_template(store, template_id, reason)
|
|
633
|
+
data = {"memory_id": memory.id, "status": memory.status}
|
|
634
|
+
_json(data) if json_output else typer.echo(f"{memory.id} archived")
|
|
635
|
+
except Exception as exc:
|
|
636
|
+
_handle_error(exc)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@scan_app.command("scan")
|
|
640
|
+
def evidence_scan(
|
|
641
|
+
apply_candidates: bool = typer.Option(False, "--apply-candidates"),
|
|
642
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
643
|
+
) -> None:
|
|
644
|
+
try:
|
|
645
|
+
config, store = _load()
|
|
646
|
+
proposals = scan_evidence(config.root)
|
|
647
|
+
results = apply_scan(store, proposals) if apply_candidates else []
|
|
648
|
+
data = {
|
|
649
|
+
"proposals": [proposal.to_dict() for proposal in proposals],
|
|
650
|
+
"applied": results,
|
|
651
|
+
}
|
|
652
|
+
if json_output:
|
|
653
|
+
_json(data)
|
|
654
|
+
elif apply_candidates:
|
|
655
|
+
for result in results:
|
|
656
|
+
typer.echo(f"{result['action']} {result['memory_id']}")
|
|
657
|
+
else:
|
|
658
|
+
for proposal in proposals:
|
|
659
|
+
typer.echo(f"{proposal.path}:{proposal.line} {proposal.observed}")
|
|
660
|
+
except Exception as exc:
|
|
661
|
+
_handle_error(exc)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@import_app.command("text")
|
|
665
|
+
def import_text_cmd(
|
|
666
|
+
text: str | None = typer.Option(None, "--text"),
|
|
667
|
+
file: Path | None = typer.Option(None, "--file"),
|
|
668
|
+
stdin: bool = typer.Option(False, "--stdin"),
|
|
669
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
670
|
+
) -> None:
|
|
671
|
+
try:
|
|
672
|
+
_config, store = _load()
|
|
673
|
+
ids = intake_text(store, _read_input(text, file, stdin))
|
|
674
|
+
_json({"candidates": ids}) if json_output else typer.echo("\n".join(ids))
|
|
675
|
+
except Exception as exc:
|
|
676
|
+
_handle_error(exc)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@import_app.command("run-html")
|
|
680
|
+
def import_run_html_cmd(
|
|
681
|
+
file: Path = typer.Option(..., "--file"),
|
|
682
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
683
|
+
) -> None:
|
|
684
|
+
try:
|
|
685
|
+
_config, store = _load()
|
|
686
|
+
ids = intake_run_html(store, file)
|
|
687
|
+
_json({"candidates": ids}) if json_output else typer.echo("\n".join(ids))
|
|
688
|
+
except Exception as exc:
|
|
689
|
+
_handle_error(exc)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@import_app.command("current-run")
|
|
693
|
+
def import_current_run(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
694
|
+
data = {
|
|
695
|
+
"ok": False,
|
|
696
|
+
"unsupported": True,
|
|
697
|
+
"message": "current-run import is not supported in this runtime",
|
|
698
|
+
}
|
|
699
|
+
_json(data) if json_output else typer.echo(data["message"])
|
|
700
|
+
raise typer.Exit(2)
|