memory-boost 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.
- memory_boost/__init__.py +3 -0
- memory_boost/cli.py +389 -0
- memory_boost/core.py +681 -0
- memory_boost/drift.py +145 -0
- memory_boost/example_transcripts/-work-acme-api/0a1b2c3d-1111-4aaa-8bbb-000000000001.jsonl +136 -0
- memory_boost/example_transcripts/-work-acme-api/0a1b2c3d-3333-4aaa-8bbb-000000000003.jsonl +46 -0
- memory_boost/example_transcripts/-work-acme-api/0a1b2c3d-4444-4aaa-8bbb-000000000004.jsonl +10 -0
- memory_boost/example_transcripts/-work-pixel-notes/0a1b2c3d-2222-4aaa-8bbb-000000000002.jsonl +54 -0
- memory_boost/example_wiki/concepts/money-as-integer-cents.md +15 -0
- memory_boost/example_wiki/concepts/retry-jobs-idempotently.md +16 -0
- memory_boost/example_wiki/context/preferences.md +7 -0
- memory_boost/example_wiki/decisions/cors-allow-all.md +9 -0
- memory_boost/example_wiki/decisions/float-money.md +9 -0
- memory_boost/example_wiki/decisions/pin-postgres-15.md +14 -0
- memory_boost/example_wiki/decisions/queue-in-postgres.md +15 -0
- memory_boost/example_wiki/decisions/sessions-in-redis.md +9 -0
- memory_boost/example_wiki/log.md +37 -0
- memory_boost/example_wiki/projects/acme-api.md +24 -0
- memory_boost/example_wiki/projects/legacy-dashboard.md +13 -0
- memory_boost/example_wiki/projects/pixel-notes.md +9 -0
- memory_boost/lessons.py +77 -0
- memory_boost/mine.py +172 -0
- memory_boost/server.py +102 -0
- memory_boost-0.1.0.dist-info/METADATA +229 -0
- memory_boost-0.1.0.dist-info/RECORD +28 -0
- memory_boost-0.1.0.dist-info/WHEEL +4 -0
- memory_boost-0.1.0.dist-info/entry_points.txt +2 -0
- memory_boost-0.1.0.dist-info/licenses/LICENSE +21 -0
memory_boost/__init__.py
ADDED
memory_boost/cli.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""memory-boost CLI: the same operations as the MCP tools, plus `init`, `index`,
|
|
2
|
+
`serve` and the harness hook. The hook path imports only the stdlib so session
|
|
3
|
+
start stays instant.
|
|
4
|
+
|
|
5
|
+
Contract for agents: data on stdout, warnings and errors on stderr, `--json` on
|
|
6
|
+
every reading command (always with a "version" field), exit 0 = stdout is
|
|
7
|
+
trustworthy, 1 = nothing found / nothing to read, 2 = usage or invalid input."""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import datetime as dt
|
|
12
|
+
import json
|
|
13
|
+
import shutil
|
|
14
|
+
import sys
|
|
15
|
+
from importlib import resources
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from memory_boost import __version__, core
|
|
19
|
+
|
|
20
|
+
JSON_VERSION = 1
|
|
21
|
+
EXIT_NOT_FOUND = 1
|
|
22
|
+
EXIT_USAGE = 2
|
|
23
|
+
|
|
24
|
+
EXAMPLES = """\
|
|
25
|
+
examples:
|
|
26
|
+
memory-boost init --example seed a sample wiki + synthetic transcripts
|
|
27
|
+
memory-boost drift what in the wiki expired, went silent or was never written
|
|
28
|
+
memory-boost mine --since 7d digest of your local Claude Code transcripts (aggregates)
|
|
29
|
+
memory-boost brief --project acme-api what an agent gets at session start (add --json)
|
|
30
|
+
memory-boost recall "retry jobs" --json full-text search, machine-readable
|
|
31
|
+
memory-boost lessons --project acme-api lessons from *other* projects that apply here
|
|
32
|
+
memory-boost page queue-in-postgres print a page; exit 1 if it does not exist
|
|
33
|
+
|
|
34
|
+
exit codes: 0 ok · 1 not found / nothing to read · 2 usage or invalid input
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _json_out(obj) -> None:
|
|
39
|
+
"""Every JSON result carries the schema version so agents can pin it."""
|
|
40
|
+
if isinstance(obj, list):
|
|
41
|
+
obj = {"results": obj}
|
|
42
|
+
print(json.dumps({"version": JSON_VERSION, **obj}, ensure_ascii=False, indent=2,
|
|
43
|
+
default=lambda v: sorted(v) if isinstance(v, set) else str(v)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _fail(msg: str, fix: str | None = None, code: int = EXIT_NOT_FOUND) -> None:
|
|
47
|
+
print(f"error: {msg}", file=sys.stderr)
|
|
48
|
+
if fix:
|
|
49
|
+
print(f"fix: {fix}", file=sys.stderr)
|
|
50
|
+
sys.exit(code)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _warn(msg: str) -> None:
|
|
54
|
+
print(f"warning: {msg}", file=sys.stderr)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _hook_envelope(text: str) -> str:
|
|
58
|
+
return json.dumps({"hookSpecificOutput": {"hookEventName": "SessionStart",
|
|
59
|
+
"additionalContext": text}}, ensure_ascii=False)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def session_start_payload(event: dict) -> str | None:
|
|
63
|
+
"""Text to inject for a SessionStart event ({cwd, session_id, source}).
|
|
64
|
+
After a compaction the brief is already in the summary: only the exact
|
|
65
|
+
checkpoint of this session is missing, so that is all we inject."""
|
|
66
|
+
cwd = event.get("cwd") or str(Path.cwd())
|
|
67
|
+
sid = event.get("session_id") or None
|
|
68
|
+
project = core.resolve_project(cwd)
|
|
69
|
+
if event.get("source") == "compact":
|
|
70
|
+
return core.resume_text(project, sid) if project else None
|
|
71
|
+
|
|
72
|
+
result = core.brief(cwd)
|
|
73
|
+
text = result["text"]
|
|
74
|
+
if sid:
|
|
75
|
+
text += (f"\n\ncheckpoint_session={sid} — after each milestone call memory_checkpoint("
|
|
76
|
+
"project, session_id=this, task, done, next_step, files); it survives compaction "
|
|
77
|
+
"and parallel sessions can see it.")
|
|
78
|
+
if project:
|
|
79
|
+
own = core.load_checkpoint(project, sid) if sid else None
|
|
80
|
+
if own and own.get("status") != "done":
|
|
81
|
+
text += "\n\n" + core.resume_text(project, sid, budget=900)
|
|
82
|
+
else:
|
|
83
|
+
others = [c for c in core.list_checkpoints(project) if c["session_id"] != sid]
|
|
84
|
+
if others:
|
|
85
|
+
text += "\n\nOther active sessions on this project:\n" + "\n".join(
|
|
86
|
+
core.format_other_sessions(others))
|
|
87
|
+
return text
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_hook(args):
|
|
91
|
+
# A hook must never block or break session start: any failure -> no output, exit 0.
|
|
92
|
+
try:
|
|
93
|
+
raw = sys.stdin.read()
|
|
94
|
+
event = json.loads(raw) if raw.strip() else {}
|
|
95
|
+
text = session_start_payload(event)
|
|
96
|
+
except Exception as e: # noqa: BLE001 — logged to stderr, never raised into the harness
|
|
97
|
+
print(f"memory-boost hook: {e}", file=sys.stderr)
|
|
98
|
+
return
|
|
99
|
+
if text:
|
|
100
|
+
print(_hook_envelope(text) if args.format == "claude" else text)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_init(args):
|
|
104
|
+
home = core.home()
|
|
105
|
+
wiki = core.wiki_dir()
|
|
106
|
+
if args.example:
|
|
107
|
+
if any(wiki.glob("**/*.md")):
|
|
108
|
+
_fail(f"{wiki} is not empty; --example only seeds an empty wiki",
|
|
109
|
+
fix="memory-boost init # without --example", code=EXIT_USAGE)
|
|
110
|
+
src = resources.files("memory_boost") / "example_wiki"
|
|
111
|
+
with resources.as_file(src) as p:
|
|
112
|
+
shutil.copytree(p, wiki, dirs_exist_ok=True)
|
|
113
|
+
src = resources.files("memory_boost") / "example_transcripts"
|
|
114
|
+
with resources.as_file(src) as p:
|
|
115
|
+
shutil.copytree(p, home / "example_transcripts", dirs_exist_ok=True)
|
|
116
|
+
for sub in ("projects", "decisions", "concepts", "context"):
|
|
117
|
+
(wiki / sub).mkdir(parents=True, exist_ok=True)
|
|
118
|
+
(wiki / "log.md").touch()
|
|
119
|
+
print(f"memory home: {home}\nwiki: {wiki}")
|
|
120
|
+
if args.example:
|
|
121
|
+
print(f"next: memory-boost drift\n"
|
|
122
|
+
f" memory-boost mine --root {home / 'example_transcripts'}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_brief(args):
|
|
126
|
+
result = core.brief(args.project or args.cwd or str(Path.cwd()), budget=args.budget)
|
|
127
|
+
_json_out(result) if args.json else print(result["text"])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cmd_recall(args):
|
|
131
|
+
results = core.recall(args.query, project=args.project, limit=args.limit,
|
|
132
|
+
include_historic=args.include_historic)
|
|
133
|
+
if args.json:
|
|
134
|
+
_json_out(results)
|
|
135
|
+
return
|
|
136
|
+
if not results:
|
|
137
|
+
_fail(f"no results for {args.query!r}",
|
|
138
|
+
fix="memory-boost recall <fewer words> # or --include-historic")
|
|
139
|
+
for r in results:
|
|
140
|
+
print(f"[{r['kind']} | {r['date'] or '?'} | {r['freshness']}] {r['ref']} — {r['extract']}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_page(args):
|
|
144
|
+
found = core.page_path(args.name)
|
|
145
|
+
if not found:
|
|
146
|
+
_fail(f"page '{args.name}' not found", fix="memory-boost recall <words from the title>")
|
|
147
|
+
text = core.get_page(args.name, args.section)
|
|
148
|
+
if args.section and text.startswith("(section '"):
|
|
149
|
+
_fail(text.strip("()"), fix=f"memory-boost page {args.name} # whole page")
|
|
150
|
+
if args.json:
|
|
151
|
+
_json_out({"name": args.name, "kind": found[0], "section": args.section, "text": text})
|
|
152
|
+
else:
|
|
153
|
+
print(text)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_save(args):
|
|
157
|
+
slug = core.save(args.project, args.action, detail=args.detail, result=args.result,
|
|
158
|
+
agent=args.agent, pending=args.pending)
|
|
159
|
+
print(f"saved: {slug} | {args.action}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _split(v):
|
|
163
|
+
return [x.strip() for x in v.split("||") if x.strip()] if v else None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def cmd_checkpoint(args):
|
|
167
|
+
c = core.save_checkpoint(args.project, args.session, args.task, agent=args.agent,
|
|
168
|
+
done=_split(args.done), next_step=args.next, files=_split(args.files),
|
|
169
|
+
notes=_split(args.notes), status=args.status,
|
|
170
|
+
plan_ref=args.plan_ref, replace_done=args.replace_done)
|
|
171
|
+
print(f"checkpoint: {c['project']} | {c['session_id']} | {c['status']} | done={len(c['done'])}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def cmd_resume(args):
|
|
175
|
+
if args.json:
|
|
176
|
+
own = core.load_checkpoint(args.project, args.session) if args.session else None
|
|
177
|
+
others = [c for c in core.list_checkpoints(args.project) if c["session_id"] != args.session]
|
|
178
|
+
_json_out({"project": args.project, "checkpoint": own, "others": others})
|
|
179
|
+
else:
|
|
180
|
+
print(core.resume_text(args.project, args.session, budget=args.budget))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cmd_checkpoints(args):
|
|
184
|
+
items = core.list_checkpoints(args.project, include_done=args.all)
|
|
185
|
+
if args.json:
|
|
186
|
+
_json_out(items)
|
|
187
|
+
return
|
|
188
|
+
for c in items:
|
|
189
|
+
print(f"[{c['project']} | {c['agent']} | {c['status']} | {c['updated'][:16]}] "
|
|
190
|
+
f"{c['session_id']}: {c['task']}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def cmd_index(args):
|
|
194
|
+
stats = core.refresh_index(force=args.rebuild)
|
|
195
|
+
for w in stats.pop("warnings"):
|
|
196
|
+
_warn(w)
|
|
197
|
+
_json_out(stats)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def cmd_drift(args):
|
|
201
|
+
from memory_boost import drift
|
|
202
|
+
wiki = core.wiki_dir()
|
|
203
|
+
if not wiki.is_dir():
|
|
204
|
+
_fail(f"wiki directory {wiki} does not exist", fix="memory-boost init --example")
|
|
205
|
+
report = drift.drift_report()
|
|
206
|
+
if args.json:
|
|
207
|
+
_json_out(report)
|
|
208
|
+
elif args.project:
|
|
209
|
+
print("\n".join(drift.for_project(args.project, n=50)) or "(nothing drifted)")
|
|
210
|
+
else:
|
|
211
|
+
print(drift.format_report(report), end="")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def cmd_mine(args):
|
|
215
|
+
from memory_boost import mine
|
|
216
|
+
roots = [Path(r).expanduser() for r in args.root] if args.root else mine.transcript_roots()
|
|
217
|
+
missing = [r for r in roots if not r.is_dir()]
|
|
218
|
+
for r in missing:
|
|
219
|
+
_warn(f"transcript root {r} does not exist")
|
|
220
|
+
if len(missing) == len(roots):
|
|
221
|
+
_fail("no transcript directory to read",
|
|
222
|
+
fix="memory-boost mine --root ~/.claude/projects # or set MEMORY_BOOST_TRANSCRIPTS")
|
|
223
|
+
since = None
|
|
224
|
+
if args.since:
|
|
225
|
+
days = int(args.since.rstrip("d"))
|
|
226
|
+
since = (dt.date.fromisoformat(core._today()) - dt.timedelta(days=days)).isoformat()
|
|
227
|
+
agg = mine.aggregate(mine.scan(roots, since=since, min_turns=args.min_turns))
|
|
228
|
+
if args.json:
|
|
229
|
+
_json_out(agg)
|
|
230
|
+
else:
|
|
231
|
+
print(mine.format_digest(agg, since), end="")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def cmd_lessons(args):
|
|
235
|
+
from memory_boost import lessons
|
|
236
|
+
found = lessons.lessons_for(args.project, limit=args.limit) if args.project else lessons.all_lessons()
|
|
237
|
+
if args.json:
|
|
238
|
+
_json_out(found)
|
|
239
|
+
return
|
|
240
|
+
if args.project:
|
|
241
|
+
print("\n".join(lessons.format_lessons(found)) or f"(no lessons match {args.project})")
|
|
242
|
+
else:
|
|
243
|
+
for lesson in found:
|
|
244
|
+
print(f"{lesson['name']} [{', '.join(sorted(lesson['applies_when']))}] "
|
|
245
|
+
f"from {lesson['learned_on'] or '?'} — {lesson['summary'][:100]}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def cmd_lesson(args):
|
|
249
|
+
from memory_boost import lessons
|
|
250
|
+
body = args.body if args.body != "-" else sys.stdin.read()
|
|
251
|
+
p = lessons.save_lesson(args.name, args.title, args.applies_when.split(","), body,
|
|
252
|
+
learned_on=args.learned_on)
|
|
253
|
+
print(f"lesson saved: {p}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def cmd_serve(_args):
|
|
257
|
+
from memory_boost.server import main as serve # imports `mcp` only when serving
|
|
258
|
+
serve()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _json_flag(s: argparse.ArgumentParser) -> None:
|
|
262
|
+
s.add_argument("--json", action="store_true", help="machine-readable output (with 'version')")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
266
|
+
p = argparse.ArgumentParser(prog="memory-boost",
|
|
267
|
+
description="Memory that reviews itself: a markdown wiki for coding "
|
|
268
|
+
"agents, plus drift reports and session digests.",
|
|
269
|
+
epilog=EXAMPLES, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
270
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
271
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
272
|
+
|
|
273
|
+
s = sub.add_parser("init", help="create the memory home (MEMORY_BOOST_HOME)")
|
|
274
|
+
s.add_argument("--example", action="store_true",
|
|
275
|
+
help="seed an empty wiki with the example wiki and four synthetic transcripts")
|
|
276
|
+
s.set_defaults(func=cmd_init)
|
|
277
|
+
|
|
278
|
+
s = sub.add_parser("serve", help="run the MCP server on stdio")
|
|
279
|
+
s.set_defaults(func=cmd_serve)
|
|
280
|
+
|
|
281
|
+
s = sub.add_parser("hook", help="harness hook entry point (reads the event JSON on stdin)")
|
|
282
|
+
s.add_argument("event", choices=["session-start"])
|
|
283
|
+
s.add_argument("--format", choices=["claude", "text"], default="claude",
|
|
284
|
+
help="claude = hookSpecificOutput envelope; text = plain context")
|
|
285
|
+
s.set_defaults(func=cmd_hook)
|
|
286
|
+
|
|
287
|
+
s = sub.add_parser("brief", help="orientation for a project or directory (what the hook injects)")
|
|
288
|
+
s.add_argument("--project", default=None, help="project slug; default: resolve from --cwd")
|
|
289
|
+
s.add_argument("--cwd", default=None, help="directory to resolve the project from (default: .)")
|
|
290
|
+
s.add_argument("--budget", type=int, default=1200, help="approximate size in characters")
|
|
291
|
+
_json_flag(s)
|
|
292
|
+
s.set_defaults(func=cmd_brief)
|
|
293
|
+
|
|
294
|
+
s = sub.add_parser("recall", help="full-text search over the wiki and log")
|
|
295
|
+
s.add_argument("query", help="words to search (FTS5 syntax allowed)")
|
|
296
|
+
s.add_argument("--project", default=None, help="restrict to one project")
|
|
297
|
+
s.add_argument("--limit", type=int, default=8, help="max results")
|
|
298
|
+
s.add_argument("--include-historic", action="store_true",
|
|
299
|
+
help="also return historic / superseded pages")
|
|
300
|
+
_json_flag(s)
|
|
301
|
+
s.set_defaults(func=cmd_recall)
|
|
302
|
+
|
|
303
|
+
s = sub.add_parser("page", help="print a wiki page or one section (exit 1 if missing)")
|
|
304
|
+
s.add_argument("name", help="page slug, e.g. acme-api or queue-in-postgres")
|
|
305
|
+
s.add_argument("--section", default=None, help="only this section (by title)")
|
|
306
|
+
_json_flag(s)
|
|
307
|
+
s.set_defaults(func=cmd_page)
|
|
308
|
+
|
|
309
|
+
s = sub.add_parser("save", help="record a session event + log.md entry")
|
|
310
|
+
s.add_argument("--project", required=True, help="project slug")
|
|
311
|
+
s.add_argument("--action", required=True, help="one line: what was done")
|
|
312
|
+
s.add_argument("--agent", default="cli", help="who did it (default: cli)")
|
|
313
|
+
s.add_argument("--result", default="ok", help="ok | partial | failed")
|
|
314
|
+
s.add_argument("--detail", default=None, help="longer markdown detail")
|
|
315
|
+
s.add_argument("--pending", default=None, help="what is left to do")
|
|
316
|
+
s.set_defaults(func=cmd_save)
|
|
317
|
+
|
|
318
|
+
s = sub.add_parser("checkpoint", help="save/merge a session checkpoint (lists split by '||')")
|
|
319
|
+
s.add_argument("--project", required=True, help="project slug")
|
|
320
|
+
s.add_argument("--session", required=True, help="session id (from the hook)")
|
|
321
|
+
s.add_argument("--task", required=True, help="what this session is doing")
|
|
322
|
+
s.add_argument("--agent", default="cli", help="who is working (default: cli)")
|
|
323
|
+
s.add_argument("--done", default=None, help="steps done, split by '||'")
|
|
324
|
+
s.add_argument("--next", default=None, help="the next step")
|
|
325
|
+
s.add_argument("--files", default=None, help="files touched, split by '||'")
|
|
326
|
+
s.add_argument("--notes", default=None, help="notes, split by '||'")
|
|
327
|
+
s.add_argument("--status", default="active", choices=core.CHECKPOINT_STATUSES)
|
|
328
|
+
s.add_argument("--plan-ref", default=None, help="path or URL of the plan")
|
|
329
|
+
s.add_argument("--replace-done", action="store_true", help="replace the done list instead of merging")
|
|
330
|
+
s.set_defaults(func=cmd_checkpoint)
|
|
331
|
+
|
|
332
|
+
s = sub.add_parser("resume", help="print a session checkpoint + other active sessions")
|
|
333
|
+
s.add_argument("--project", required=True, help="project slug")
|
|
334
|
+
s.add_argument("--session", default=None, help="session id; omit for others only")
|
|
335
|
+
s.add_argument("--budget", type=int, default=1200, help="approximate size in characters")
|
|
336
|
+
_json_flag(s)
|
|
337
|
+
s.set_defaults(func=cmd_resume)
|
|
338
|
+
|
|
339
|
+
s = sub.add_parser("checkpoints", help="list active checkpoints")
|
|
340
|
+
s.add_argument("--project", default=None, help="restrict to one project")
|
|
341
|
+
s.add_argument("--all", action="store_true", help="include finished ones")
|
|
342
|
+
_json_flag(s)
|
|
343
|
+
s.set_defaults(func=cmd_checkpoints)
|
|
344
|
+
|
|
345
|
+
s = sub.add_parser("drift", help="what in the wiki is stale, silent or never written down")
|
|
346
|
+
s.add_argument("--project", default=None, help="only this project's findings, as bullet lines")
|
|
347
|
+
_json_flag(s)
|
|
348
|
+
s.set_defaults(func=cmd_drift)
|
|
349
|
+
|
|
350
|
+
s = sub.add_parser("mine", help="digest of your local agent transcripts (aggregates only)")
|
|
351
|
+
s.add_argument("--root", action="append", default=None,
|
|
352
|
+
help="transcript directory (repeatable; default MEMORY_BOOST_TRANSCRIPTS "
|
|
353
|
+
"or ~/.claude/projects)")
|
|
354
|
+
s.add_argument("--since", default=None, help="only sessions newer than e.g. 7d, 30d")
|
|
355
|
+
s.add_argument("--min-turns", type=int, default=20, help="ignore shorter sessions")
|
|
356
|
+
_json_flag(s)
|
|
357
|
+
s.set_defaults(func=cmd_mine)
|
|
358
|
+
|
|
359
|
+
s = sub.add_parser("lessons", help="list lessons, or those that apply to a project")
|
|
360
|
+
s.add_argument("--project", default=None, help="lessons from other projects matching its tags")
|
|
361
|
+
s.add_argument("--limit", type=int, default=3, help="max lessons with --project")
|
|
362
|
+
_json_flag(s)
|
|
363
|
+
s.set_defaults(func=cmd_lessons)
|
|
364
|
+
|
|
365
|
+
s = sub.add_parser("lesson", help="record a lesson that travels to other projects")
|
|
366
|
+
s.add_argument("--name", required=True, help="slug, e.g. retry-jobs-idempotently")
|
|
367
|
+
s.add_argument("--title", required=True, help="one imperative line")
|
|
368
|
+
s.add_argument("--applies-when", required=True, help="comma-separated tags")
|
|
369
|
+
s.add_argument("--learned-on", default=None, help="project slug it was learned on")
|
|
370
|
+
s.add_argument("--body", required=True, help="markdown body, or - for stdin")
|
|
371
|
+
s.set_defaults(func=cmd_lesson)
|
|
372
|
+
|
|
373
|
+
s = sub.add_parser("index", help="refresh the search index (automatic on recall)")
|
|
374
|
+
s.add_argument("--rebuild", action="store_true", help="drop and rebuild from scratch")
|
|
375
|
+
s.add_argument("--json", action="store_true", help="(always JSON; accepted for symmetry)")
|
|
376
|
+
s.set_defaults(func=cmd_index)
|
|
377
|
+
return p
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def main(argv: list[str] | None = None) -> None:
|
|
381
|
+
args = build_parser().parse_args(argv)
|
|
382
|
+
try:
|
|
383
|
+
args.func(args)
|
|
384
|
+
except ValueError as e:
|
|
385
|
+
_fail(str(e), fix="memory-boost <command> --help", code=EXIT_USAGE)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
if __name__ == "__main__":
|
|
389
|
+
main()
|